Skip to content

Commit c417d75

Browse files
authored
Merge pull request #382 from cloudflare/felix/crypto-errors2
Improve Webcrypto error handling, resolve several internal errors and add clarifying comments
2 parents bd82bf8 + 8044842 commit c417d75

File tree

1 file changed

+108
-2
lines changed

1 file changed

+108
-2
lines changed

src/workerd/api/crypto-impl-asymmetric.c++

Lines changed: 108 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,59 @@ public:
103103
"Asymmetric signing requires a private key.");
104104

105105
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+
}
106159

107160
auto digestCtx = OSSL_NEW(EVP_MD_CTX);
108161

@@ -551,13 +604,27 @@ public:
551604

552605
auto size = RSA_size(rsa);
553606

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.
554612
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 "
556614
"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+
}
557623

558624
auto signature = kj::heapArray<kj::byte>(size);
559625
size_t signatureSize = 0;
560626

627+
// Use raw RSA, no padding
561628
OSSLCALL(RSA_decrypt(rsa, &signatureSize, signature.begin(), size, data.begin(), data.size(),
562629
RSA_NO_PADDING));
563630

@@ -644,12 +711,36 @@ kj::Maybe<T> fromBignum(kj::ArrayPtr<kj::byte> value) {
644711
return asUnsigned;
645712
}
646713

647-
void validateRsaParams(int modulusLength, kj::ArrayPtr<kj::byte> publicExponent) {
714+
void validateRsaParams(int modulusLength, kj::ArrayPtr<kj::byte> publicExponent,
715+
bool warnImport = false) {
648716
// The W3C standard itself doesn't describe any parameter validation but the conformance tests
649717
// do test "bad" exponents, likely because everyone uses OpenSSL that suffers from poor behavior
650718
// with these bad exponents (e.g. if an exponent < 3 or 65535 generates an infinite loop, a
651719
// library might be expected to handle such cases on its own, no?).
652720

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+
653744
// Use Chromium's limits for RSA keygen to avoid infinite loops:
654745
// * Key sizes a multiple of 8 bits.
655746
// * Key sizes must be in [256, 16k] bits.
@@ -702,6 +793,12 @@ kj::OneOf<jsg::Ref<CryptoKey>, CryptoKeyPair> CryptoKey::Impl::generateRsa(
702793
publicExponent.size(), nullptr), InternalDOMOperationError, "Error setting up RSA keygen.");
703794

704795
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.
705802
OSSLCALL(RSA_generate_key_ex(rsaPrivateKey, modulusLength, bnExponent.get(), 0));
706803
auto privateEvpPKey = OSSL_NEW(EVP_PKEY);
707804
OSSLCALL(EVP_PKEY_set1_RSA(privateEvpPKey.get(), rsaPrivateKey.get()));
@@ -875,6 +972,9 @@ kj::Own<CryptoKey::Impl> CryptoKey::Impl::importRsa(
875972
auto publicExponent = kj::heapArray<kj::byte>(BN_num_bytes(e));
876973
KJ_ASSERT(BN_bn2bin(e, publicExponent.begin()) == publicExponent.size());
877974

975+
// Validate modulus and exponent, reject imported RSA keys that may be unsafe.
976+
validateRsaParams(modulusLength, publicExponent, true);
977+
878978
auto keyAlgorithm = CryptoKey::RsaKeyAlgorithm {
879979
.name = normalizedName,
880980
.modulusLength = static_cast<uint16_t>(modulusLength),
@@ -899,6 +999,8 @@ kj::Own<CryptoKey::Impl> CryptoKey::Impl::importRsaRaw(
899999
SubtleCrypto::ImportKeyData keyData,
9001000
SubtleCrypto::ImportKeyAlgorithm&& algorithm, bool extractable,
9011001
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.
9021004
CryptoKeyUsageSet allowedUsages = CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify();
9031005
auto [evpPkey, keyType, usages] = importAsymmetric(
9041006
kj::mv(format), kj::mv(keyData), normalizedName, extractable, keyUsages,
@@ -941,6 +1043,9 @@ kj::Own<CryptoKey::Impl> CryptoKey::Impl::importRsaRaw(
9411043
auto publicExponent = kj::heapArray<kj::byte>(BN_num_bytes(e));
9421044
KJ_ASSERT(BN_bn2bin(e, publicExponent.begin()) == publicExponent.size());
9431045

1046+
// Validate modulus and exponent, reject imported RSA keys that may be unsafe.
1047+
validateRsaParams(modulusLength, publicExponent, true);
1048+
9441049
auto keyAlgorithm = CryptoKey::RsaKeyAlgorithm {
9451050
.name = "RSA-RAW"_kj,
9461051
.modulusLength = static_cast<uint16_t>(modulusLength),
@@ -1438,6 +1543,7 @@ kj::Own<EVP_PKEY> ellipticJwkReader(int curveId, SubtleCrypto::JsonWebKey keyDat
14381543
ImportAsymmetricResult importEllipticRaw(SubtleCrypto::ImportKeyData keyData, int curveId,
14391544
kj::StringPtr normalizedName, kj::ArrayPtr<const kj::String> keyUsages,
14401545
CryptoKeyUsageSet allowedUsages) {
1546+
// Import an elliptic key represented by raw data, only public keys are supported.
14411547
JSG_REQUIRE(keyData.is<kj::Array<kj::byte>>(), DOMDataError,
14421548
"Expected raw EC key but instead got a Json Web Key.");
14431549

0 commit comments

Comments
 (0)