@@ -103,6 +103,59 @@ public:
103
103
" Asymmetric signing requires a private key." );
104
104
105
105
auto type = lookupDigestAlgorithm (chooseHash (algorithm.hash )).second ;
106
+ if (getAlgorithmName () == " RSASSA-PKCS1-v1_5" ) {
107
+ // RSASSA-PKCS1-v1_5 requires the RSA key to be at least as big as the digest size
108
+ // plus a 15 to 19 byte digest-specific prefix (see boringssl's RSA_add_pkcs1_prefix) plus 11
109
+ // bytes for padding (see RSA_PKCS1_PADDING_SIZE). For simplicity, require the key to be at
110
+ // least 32 bytes larger than the hash digest.
111
+ // Similar checks could also be adopted for more detailed error handling in verify(), but the
112
+ // current approach should be sufficient to avoid internal errors.
113
+ RSA& rsa = JSG_REQUIRE_NONNULL (EVP_PKEY_get0_RSA (getEvpPkey ()), DOMDataError,
114
+ " Missing RSA key" , tryDescribeOpensslErrors ());
115
+
116
+ // TODO(soon): Use more thorough checks for now to detect if requiring 32 bytes beyond the
117
+ // digest size would break existing scripts. Prefix sizes derived from boringssl's
118
+ // kPKCS1SigPrefixes.
119
+ #define RSA_PKCS1_PADDING_SIZE 11
120
+ #define RSA_PKCS1_MD5_PREFIX_SIZE 18
121
+ #define RSA_PKCS1_SHA1_PREFIX_SIZE 15
122
+ #define RSA_PKCS1_SHA_PREFIX_SIZE 19
123
+
124
+ const auto & hashName = lookupDigestAlgorithm (chooseHash (algorithm.hash )).first ;
125
+ auto paddingOverhead = RSA_PKCS1_PADDING_SIZE;
126
+ if (hashName == " MD5" ) {
127
+ paddingOverhead += RSA_PKCS1_MD5_PREFIX_SIZE;
128
+ } else if (hashName == " SHA-1" ) {
129
+ paddingOverhead += RSA_PKCS1_SHA1_PREFIX_SIZE;
130
+ } else {
131
+ paddingOverhead += RSA_PKCS1_SHA_PREFIX_SIZE;
132
+ }
133
+
134
+ JSG_REQUIRE (EVP_MD_size (type) + paddingOverhead <= RSA_size (&rsa), DOMOperationError,
135
+ " key too small for signing with given digest" );
136
+ if (RSA_size (&rsa) < EVP_MD_size (type) + 32 ) {
137
+ static bool logOnce KJ_UNUSED = ([rsa] {
138
+ KJ_LOG (WARNING, " Signing with peculiar key size of " , RSA_size (&rsa), " bytes" );
139
+ return true ;
140
+ })();
141
+ }
142
+
143
+ // JSG_REQUIRE(EVP_MD_size(type) + 32 <= RSA_size(&rsa), DOMOperationError,
144
+ // "key too small for signing with given digest");
145
+ } else if (getAlgorithmName () == " RSA-PSS" ) {
146
+ // Similarly, RSA-PSS requires keys to be at least the size of the digest and salt plus 2
147
+ // bytes, see https://developer.mozilla.org/en-US/docs/Web/API/RsaPssParams for details.
148
+ RSA& rsa = JSG_REQUIRE_NONNULL (EVP_PKEY_get0_RSA (getEvpPkey ()), DOMDataError,
149
+ " Missing RSA key" , tryDescribeOpensslErrors ());
150
+ auto salt = JSG_REQUIRE_NONNULL (algorithm.saltLength , DOMDataError,
151
+ " Failed to provide salt for RSA-PSS key operation which requires a salt" );
152
+ JSG_REQUIRE (salt >= 0 , DOMDataError, " SaltLength for RSA-PSS must be non-negative (provided " ,
153
+ salt, " )." );
154
+ JSG_REQUIRE (EVP_MD_size (type) + 2 <= RSA_size (&rsa), DOMOperationError,
155
+ " key too small for signing with given digest" );
156
+ JSG_REQUIRE (salt <= RSA_size (&rsa) - EVP_MD_size (type) - 2 , DOMOperationError,
157
+ " key too small for signing with given digest and salt length" );
158
+ }
106
159
107
160
auto digestCtx = OSSL_NEW (EVP_MD_CTX);
108
161
@@ -551,13 +604,27 @@ public:
551
604
552
605
auto size = RSA_size (rsa);
553
606
607
+ // RSA encryption/decryption requires the key value to be strictly larger than the value to be
608
+ // signed. Ideally we would enforce this by checking that the key size is larger than the input
609
+ // size – having both the same size makes it highly likely that some values are higher than the
610
+ // key value – but there are scripts and test cases that depend on signing data with keys of
611
+ // the same size.
554
612
JSG_REQUIRE (data.size () <= size, DOMDataError,
555
- " Blind Signing requires presigned data (" , data.size (), " bytes) to be the smaller than "
613
+ " Blind Signing requires presigned data (" , data.size (), " bytes) to be smaller than "
556
614
" the key (" , size, " bytes)." );
615
+ if (data.size () == size) {
616
+ auto dataVal = OSSLCALL_OWN (BIGNUM, BN_bin2bn (data.begin (), data.size (), nullptr ),
617
+ InternalDOMOperationError, " Error converting presigned data" ,
618
+ internalDescribeOpensslErrors ());
619
+ JSG_REQUIRE (BN_ucmp (dataVal, rsa->n ) < 0 , DOMDataError,
620
+ " Blind Signing requires presigned data value to be strictly smaller than RSA key"
621
+ " modulus, consider using a larger key size." );
622
+ }
557
623
558
624
auto signature = kj::heapArray<kj::byte>(size);
559
625
size_t signatureSize = 0 ;
560
626
627
+ // Use raw RSA, no padding
561
628
OSSLCALL (RSA_decrypt (rsa, &signatureSize, signature.begin (), size, data.begin (), data.size (),
562
629
RSA_NO_PADDING));
563
630
@@ -644,12 +711,36 @@ kj::Maybe<T> fromBignum(kj::ArrayPtr<kj::byte> value) {
644
711
return asUnsigned;
645
712
}
646
713
647
- void validateRsaParams (int modulusLength, kj::ArrayPtr<kj::byte> publicExponent) {
714
+ void validateRsaParams (int modulusLength, kj::ArrayPtr<kj::byte> publicExponent,
715
+ bool warnImport = false ) {
648
716
// The W3C standard itself doesn't describe any parameter validation but the conformance tests
649
717
// do test "bad" exponents, likely because everyone uses OpenSSL that suffers from poor behavior
650
718
// with these bad exponents (e.g. if an exponent < 3 or 65535 generates an infinite loop, a
651
719
// library might be expected to handle such cases on its own, no?).
652
720
721
+ // TODO(soon): We should also enforce these limitations on imported keys. To see if this breaks
722
+ // existing scripts only provide a warning in sentry for now.
723
+ if (warnImport) {
724
+ if (modulusLength % 8 || modulusLength < 256 || modulusLength > 16384 ) {
725
+ static bool logOnce KJ_UNUSED = ([modulusLength] {
726
+ KJ_LOG (WARNING, " Imported RSA key has invalid modulus length " , modulusLength, " ." );
727
+ return true ;
728
+ })();
729
+ }
730
+ KJ_IF_MAYBE (v, fromBignum<unsigned >(publicExponent)) {
731
+ if (*v != 3 && *v != 65537 ) {
732
+ static bool logOnce KJ_UNUSED = ([v] {
733
+ KJ_LOG (WARNING, " Imported RSA key has invalid publicExponent " , *v," ." );
734
+ return true ;
735
+ })();
736
+ }
737
+ } else {
738
+ JSG_FAIL_REQUIRE (DOMOperationError, " The \" publicExponent\" must be either 3 or 65537, but "
739
+ " got a number larger than 2^32." );
740
+ }
741
+ return ;
742
+ }
743
+
653
744
// Use Chromium's limits for RSA keygen to avoid infinite loops:
654
745
// * Key sizes a multiple of 8 bits.
655
746
// * Key sizes must be in [256, 16k] bits.
@@ -702,6 +793,12 @@ kj::OneOf<jsg::Ref<CryptoKey>, CryptoKeyPair> CryptoKey::Impl::generateRsa(
702
793
publicExponent.size (), nullptr ), InternalDOMOperationError, " Error setting up RSA keygen." );
703
794
704
795
auto rsaPrivateKey = OSSL_NEW (RSA);
796
+ // TODO(later): boringssl silently uses (modulusLength & ~127) for the key size, i.e. it rounds
797
+ // down to the closest multiple of 128 bits. This can easily cause confusion when non-standard
798
+ // key sizes are requested. Ideally we would throw an error when trying to create keys where the
799
+ // size would be rounded down, but this would likely break existing scripts.
800
+ // The modulusLength field of the resulting CryptoKey will be incorrect when the key size is
801
+ // rounded down, but since it is not currently used this is acceptable.
705
802
OSSLCALL (RSA_generate_key_ex (rsaPrivateKey, modulusLength, bnExponent.get (), 0 ));
706
803
auto privateEvpPKey = OSSL_NEW (EVP_PKEY);
707
804
OSSLCALL (EVP_PKEY_set1_RSA (privateEvpPKey.get (), rsaPrivateKey.get ()));
@@ -875,6 +972,9 @@ kj::Own<CryptoKey::Impl> CryptoKey::Impl::importRsa(
875
972
auto publicExponent = kj::heapArray<kj::byte>(BN_num_bytes (e));
876
973
KJ_ASSERT (BN_bn2bin (e, publicExponent.begin ()) == publicExponent.size ());
877
974
975
+ // Validate modulus and exponent, reject imported RSA keys that may be unsafe.
976
+ validateRsaParams (modulusLength, publicExponent, true );
977
+
878
978
auto keyAlgorithm = CryptoKey::RsaKeyAlgorithm {
879
979
.name = normalizedName,
880
980
.modulusLength = static_cast <uint16_t >(modulusLength),
@@ -899,6 +999,8 @@ kj::Own<CryptoKey::Impl> CryptoKey::Impl::importRsaRaw(
899
999
SubtleCrypto::ImportKeyData keyData,
900
1000
SubtleCrypto::ImportKeyAlgorithm&& algorithm, bool extractable,
901
1001
kj::ArrayPtr<const kj::String> keyUsages) {
1002
+ // Note that in this context raw refers to the RSA-RAW algorithm, not to keys represented by raw
1003
+ // data. Importing raw keys is currently not supported for this algorithm.
902
1004
CryptoKeyUsageSet allowedUsages = CryptoKeyUsageSet::sign () | CryptoKeyUsageSet::verify ();
903
1005
auto [evpPkey, keyType, usages] = importAsymmetric (
904
1006
kj::mv (format), kj::mv (keyData), normalizedName, extractable, keyUsages,
@@ -941,6 +1043,9 @@ kj::Own<CryptoKey::Impl> CryptoKey::Impl::importRsaRaw(
941
1043
auto publicExponent = kj::heapArray<kj::byte>(BN_num_bytes (e));
942
1044
KJ_ASSERT (BN_bn2bin (e, publicExponent.begin ()) == publicExponent.size ());
943
1045
1046
+ // Validate modulus and exponent, reject imported RSA keys that may be unsafe.
1047
+ validateRsaParams (modulusLength, publicExponent, true );
1048
+
944
1049
auto keyAlgorithm = CryptoKey::RsaKeyAlgorithm {
945
1050
.name = " RSA-RAW" _kj,
946
1051
.modulusLength = static_cast <uint16_t >(modulusLength),
@@ -1438,6 +1543,7 @@ kj::Own<EVP_PKEY> ellipticJwkReader(int curveId, SubtleCrypto::JsonWebKey keyDat
1438
1543
ImportAsymmetricResult importEllipticRaw (SubtleCrypto::ImportKeyData keyData, int curveId,
1439
1544
kj::StringPtr normalizedName, kj::ArrayPtr<const kj::String> keyUsages,
1440
1545
CryptoKeyUsageSet allowedUsages) {
1546
+ // Import an elliptic key represented by raw data, only public keys are supported.
1441
1547
JSG_REQUIRE (keyData.is <kj::Array<kj::byte>>(), DOMDataError,
1442
1548
" Expected raw EC key but instead got a Json Web Key." );
1443
1549
0 commit comments