From 2076ff65948c09711c255c039b4b08d22491f46e Mon Sep 17 00:00:00 2001 From: Harshita Yadav Date: Mon, 27 Jul 2026 22:31:54 +0530 Subject: [PATCH] fix(ffi): reject overflowing crypto parameters --- lib/src/impl_ffi/impl_ffi.pbkdf2.dart | 1 + lib/src/impl_ffi/impl_ffi.rsapss.dart | 2 + lib/src/impl_ffi/impl_ffi.utils.dart | 14 ++++ .../regression/uint32_parameter_overflow.dart | 82 +++++++++++++++++++ lib/src/testing/testing.dart | 2 + lib/src/webcrypto/webcrypto.pbkdf2.dart | 1 + lib/src/webcrypto/webcrypto.rsapss.dart | 2 + 7 files changed, 104 insertions(+) create mode 100644 lib/src/testing/regression/uint32_parameter_overflow.dart diff --git a/lib/src/impl_ffi/impl_ffi.pbkdf2.dart b/lib/src/impl_ffi/impl_ffi.pbkdf2.dart index 25dd53933..94f4f8c8d 100644 --- a/lib/src/impl_ffi/impl_ffi.pbkdf2.dart +++ b/lib/src/impl_ffi/impl_ffi.pbkdf2.dart @@ -65,6 +65,7 @@ final class _Pbkdf2SecretKeyImpl implements Pbkdf2SecretKeyImpl { 'Iterations <= 0 is not allowed for Pbkdf2SecretKey.deriveBits', ); } + _checkUnsignedLong(iterations, 'iterations'); if (length == 0) { return Uint8List(0); } diff --git a/lib/src/impl_ffi/impl_ffi.rsapss.dart b/lib/src/impl_ffi/impl_ffi.rsapss.dart index 61ebd6be1..381c808b6 100644 --- a/lib/src/impl_ffi/impl_ffi.rsapss.dart +++ b/lib/src/impl_ffi/impl_ffi.rsapss.dart @@ -140,6 +140,7 @@ final class _RsaPssPrivateKeyImpl implements RsaPssPrivateKeyImpl { 'must be a positive integer', ); } + _checkInt32(saltLength, 'saltLength'); return _signStream( _key, @@ -219,6 +220,7 @@ final class _RsaPssPublicKeyImpl implements RsaPssPublicKeyImpl { 'must be a positive integer', ); } + _checkInt32(saltLength, 'saltLength'); return _verifyStream( _key, diff --git a/lib/src/impl_ffi/impl_ffi.utils.dart b/lib/src/impl_ffi/impl_ffi.utils.dart index 69f9a5a62..4adef2ff1 100644 --- a/lib/src/impl_ffi/impl_ffi.utils.dart +++ b/lib/src/impl_ffi/impl_ffi.utils.dart @@ -125,6 +125,20 @@ void _checkData(bool condition, {String? message, String? fallback}) { void _checkDataIsOne(int retval, {String? message, String? fallback}) => _checkData(retval == 1, message: message, fallback: fallback); +/// Validate that [value] fits Web IDL's `unsigned long` type. +void _checkUnsignedLong(int value, String name) { + if (value < 0 || value > 0xffffffff) { + throw ArgumentError.value(value, name, 'must be between 0 and 4294967295'); + } +} + +/// Validate that [value] can be passed to a signed 32-bit native parameter. +void _checkInt32(int value, String name) { + if (value < -0x80000000 || value > 0x7fffffff) { + throw ArgumentError.value(value, name, 'must fit a signed 32-bit integer'); + } +} + /// Extract latest error on this thread as [String] and clear the error queue /// for this thread. /// diff --git a/lib/src/testing/regression/uint32_parameter_overflow.dart b/lib/src/testing/regression/uint32_parameter_overflow.dart new file mode 100644 index 000000000..35f2ea187 --- /dev/null +++ b/lib/src/testing/regression/uint32_parameter_overflow.dart @@ -0,0 +1,82 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import 'package:webcrypto/webcrypto.dart'; + +const _isFfi = bool.fromEnvironment('dart.library.ffi'); +const _maxUnsignedLong = 0xffffffff; + +List<({String name, Future Function() test})> tests() => !_isFfi + ? const [] + : [ + ( + name: 'PBKDF2 rejects iteration counts above unsigned long', + test: () async { + final key = await Pbkdf2SecretKey.importRawKey([1, 2, 3, 4]); + + for (final iterations in [ + _maxUnsignedLong + 1, + _maxUnsignedLong + 2, + ]) { + await _expectArgumentError( + () => + key.deriveBits(256, Hash.sha256, [5, 6, 7, 8], iterations), + 'Expected PBKDF2 iterations $iterations to be rejected', + ); + } + }, + ), + ( + name: 'RSA-PSS rejects salt lengths that narrow at the FFI boundary', + test: () async { + final pair = await RsaPssPrivateKey.generateKey( + 1024, + BigInt.from(65537), + Hash.sha256, + ); + final message = [1, 2, 3, 4]; + final signature = await pair.privateKey.signBytes(message, 0); + + for (final saltLength in [ + 0x80000000, + _maxUnsignedLong, + _maxUnsignedLong + 1, + _maxUnsignedLong + 2, + ]) { + await _expectArgumentError( + () => pair.privateKey.signBytes(message, saltLength), + 'Expected RSA-PSS signing salt length $saltLength to be rejected', + ); + + await _expectArgumentError( + () => + pair.publicKey.verifyBytes(signature, message, saltLength), + 'Expected RSA-PSS verification salt length $saltLength to be rejected', + ); + } + }, + ), + ]; + +Future _expectArgumentError( + Future Function() callback, + String message, +) async { + try { + await callback(); + } on ArgumentError { + return; + } + throw AssertionError(message); +} diff --git a/lib/src/testing/testing.dart b/lib/src/testing/testing.dart index 21db1cb52..43d5ab35f 100644 --- a/lib/src/testing/testing.dart +++ b/lib/src/testing/testing.dart @@ -33,6 +33,7 @@ import 'webcrypto/digest.dart' as digest; import 'regression/derive_bits_zero_length.dart' as derive_bits_zero_length; import 'regression/issue_60_trailing_bytes.dart' as issue_60_trailing_bytes; import 'regression/rsa_oaep_sha1_jwk_alg.dart' as rsa_oaep_sha1_jwk_alg; +import 'regression/uint32_parameter_overflow.dart' as uint32_parameter_overflow; /// Test runners from all test files except `digest.dart` and /// `random.dart`, which do not use [TestRunner]. @@ -64,6 +65,7 @@ void runAllTests( ...issue_60_trailing_bytes.tests(), ...derive_bits_zero_length.tests(), ...rsa_oaep_sha1_jwk_alg.tests(), + ...uint32_parameter_overflow.tests(), ]; for (final (:name, :test) in allTests) { diff --git a/lib/src/webcrypto/webcrypto.pbkdf2.dart b/lib/src/webcrypto/webcrypto.pbkdf2.dart index 37fcc46e3..65d779a45 100644 --- a/lib/src/webcrypto/webcrypto.pbkdf2.dart +++ b/lib/src/webcrypto/webcrypto.pbkdf2.dart @@ -82,6 +82,7 @@ final class Pbkdf2SecretKey { /// an exhaustive search for the derived key, but it will also make the /// key derivation operation slower. For details on [iterations] see /// [RFC 8018 section 4.2][1]. + /// The [iterations] count must be between `1` and `2^32 - 1`. /// /// {@macro Pbkdf2SecretKey:example} /// diff --git a/lib/src/webcrypto/webcrypto.rsapss.dart b/lib/src/webcrypto/webcrypto.rsapss.dart index c82772b74..6f0124fe4 100644 --- a/lib/src/webcrypto/webcrypto.rsapss.dart +++ b/lib/src/webcrypto/webcrypto.rsapss.dart @@ -248,6 +248,7 @@ final class RsaPssPrivateKey { /// The [saltLength] is typically zero or length of the [Hash], for a /// discussion of appropriate values for [saltLength] see /// [RFC 3447 Section 9.1, Notes 4][1]. + /// The [saltLength] must be between `0` and `2^32 - 1`. /// /// When running in Safari/WebKit on Mac the [saltLength] is restricted to /// `0 <= saltLength <= hashLength` as the underlying cryptography libraries @@ -324,6 +325,7 @@ final class RsaPssPrivateKey { /// The [saltLength] is typically zero or length of the [Hash], for a /// discussion of appropriate values for [saltLength] see /// [RFC 3447 Section 9.1, Notes 4][1]. + /// The [saltLength] must be between `0` and `2^32 - 1`. /// /// When running in Safari/WebKit on Mac the [saltLength] is restricted to /// `0 <= saltLength <= hashLength` as the underlying cryptography libraries