File
Blob: src/workerd/api/crypto/rsa.c++
| 1 | #include "rsa.h" |
| 2 | |
| 3 | #include "impl.h" |
| 4 | #include "keys.h" |
| 5 | #include "util.h" |
| 6 | |
| 7 | #include <openssl/bn.h> |
| 8 | #include <openssl/crypto.h> |
| 9 | #include <openssl/evp.h> |
| 10 | #include <openssl/pem.h> |
| 11 | |
| 12 | #include <kj/array.h> |
| 13 | #include <kj/common.h> |
| 14 | |
| 15 | #include <map> |
| 16 | |
| 17 | namespace workerd::api { |
| 18 | |
| 19 | namespace { |
| 20 | template <typename T> |
| 21 | kj::Maybe<T> fromBignum(kj::ArrayPtr<kj::byte> value) { |
| 22 | static_assert(std::is_unsigned_v<T>, "This can only be invoked when the return type is unsigned"); |
| 23 | |
| 24 | T asUnsigned = 0; |
| 25 | for (size_t i = 0; i < value.size(); ++i) { |
| 26 | size_t bitShift = value.size() - i - 1; |
| 27 | if (bitShift >= sizeof(T) && value[i]) { |
| 28 | // Too large for desired type. |
| 29 | return kj::none; |
| 30 | } |
| 31 | |
| 32 | asUnsigned |= value[i] << 8 * bitShift; |
| 33 | } |
| 34 | |
| 35 | return asUnsigned; |
| 36 | } |
| 37 | |
| 38 | jsg::JsArrayBuffer bioToArray(jsg::Lock& js, BIO* bio) { |
| 39 | BUF_MEM* bptr; |
| 40 | BIO_get_mem_ptr(bio, &bptr); |
| 41 | return jsg::JsArrayBuffer::create(js, kj::asBytes(bptr->data, bptr->length)); |
| 42 | } |
| 43 | } // namespace |
| 44 | |
| 45 | kj::Maybe<Rsa> Rsa::tryGetRsa(const EVP_PKEY* key) { |
| 46 | int type = EVP_PKEY_id(key); |
| 47 | if (type != EVP_PKEY_RSA && type != EVP_PKEY_RSA_PSS) return kj::none; |
| 48 | auto rsa = EVP_PKEY_get0_RSA(key); |
| 49 | if (rsa == nullptr) return kj::none; |
| 50 | return Rsa(rsa); |
| 51 | } |
| 52 | |
| 53 | Rsa::Rsa(RSA* rsa): rsa(rsa) { |
| 54 | RSA_get0_key(rsa, &n, &e, &d); |
| 55 | } |
| 56 | |
| 57 | size_t Rsa::getModulusBits() const { |
| 58 | return getModulusSize() * 8; |
| 59 | } |
| 60 | |
| 61 | size_t Rsa::getModulusSize() const { |
| 62 | return RSA_size(rsa); |
| 63 | } |
| 64 | |
| 65 | jsg::JsUint8Array Rsa::getPublicExponent(jsg::Lock& js) { |
| 66 | return KJ_REQUIRE_NONNULL(bignumToArray(js, *e)); |
| 67 | } |
| 68 | |
| 69 | CryptoKey::AsymmetricKeyDetails Rsa::getAsymmetricKeyDetail(jsg::Lock& js) const { |
| 70 | CryptoKey::AsymmetricKeyDetails details; |
| 71 | |
| 72 | details.modulusLength = BN_num_bits(n); |
| 73 | auto pubExp = |
| 74 | JSG_REQUIRE_NONNULL(bignumToArrayPadded(js, *e), Error, "Failed to extract public exponent"); |
| 75 | auto ab = jsg::JsArrayBuffer::create(js, pubExp.asArrayPtr()); |
| 76 | details.publicExponent = ab.addRef(js); |
| 77 | |
| 78 | // TODO(soon): Does BoringSSL not support retrieving RSA_PSS params? |
| 79 | // if (type == EVP_PKEY_RSA_PSS) { |
| 80 | // // Due to the way ASN.1 encoding works, default values are omitted when |
| 81 | // // encoding the data structure. However, there are also RSA-PSS keys for |
| 82 | // // which no parameters are set. In that case, the ASN.1 RSASSA-PSS-params |
| 83 | // // sequence will be missing entirely and RSA_get0_pss_params will return |
| 84 | // // nullptr. If parameters are present but all parameters are set to their |
| 85 | // // default values, an empty sequence will be stored in the ASN.1 structure. |
| 86 | // // In that case, RSA_get0_pss_params does not return nullptr but all fields |
| 87 | // // of the returned RSA_PSS_PARAMS will be set to nullptr. |
| 88 | |
| 89 | // const RSA_PSS_PARAMS* params = RSA_get0_pss_params(rsa); |
| 90 | // if (params != nullptr) { |
| 91 | // int hash_nid = NID_sha1; |
| 92 | // int mgf_nid = NID_mgf1; |
| 93 | // int mgf1_hash_nid = NID_sha1; |
| 94 | // int64_t salt_length = 20; |
| 95 | |
| 96 | // if (params->hashAlgorithm != nullptr) { |
| 97 | // hash_nid = OBJ_obj2nid(params->hashAlgorithm->algorithm); |
| 98 | // } |
| 99 | // details.hashAlgorithm = kj::str(OBJ_nid2ln(hash_nid)); |
| 100 | |
| 101 | // if (params->maskGenAlgorithm != nullptr) { |
| 102 | // mgf_nid = OBJ_obj2nid(params->maskGenAlgorithm->algorithm); |
| 103 | // if (mgf_nid == NID_mgf1) { |
| 104 | // mgf1_hash_nid = OBJ_obj2nid(params->maskHash->algorithm); |
| 105 | // } |
| 106 | // } |
| 107 | |
| 108 | // // If, for some reason, the MGF is not MGF1, then the MGF1 hash function |
| 109 | // // is intentionally not added to the object. |
| 110 | // if (mgf_nid == NID_mgf1) { |
| 111 | // details.mgf1HashAlgorithm = kj::str(OBJ_nid2ln(mgf1_hash_nid)); |
| 112 | // } |
| 113 | |
| 114 | // if (params->saltLength != nullptr) { |
| 115 | // JSG_REQUIRE(ASN1_INTEGER_get_int64(&salt_length, params->saltLength) == 1, |
| 116 | // Error, "Unable to get salt length from RSA-PSS parameters"); |
| 117 | // } |
| 118 | // details.saltLength = static_cast<double>(salt_length); |
| 119 | // } |
| 120 | // } |
| 121 | |
| 122 | return kj::mv(details); |
| 123 | } |
| 124 | |
| 125 | jsg::JsArrayBuffer Rsa::sign(jsg::Lock& js, const kj::ArrayPtr<const kj::byte> data) const { |
| 126 | size_t size = getModulusSize(); |
| 127 | |
| 128 | // RSA encryption/decryption requires the key value to be strictly larger than the value to be |
| 129 | // signed. Ideally we would enforce this by checking that the key size is larger than the input |
| 130 | // size – having both the same size makes it highly likely that some values are higher than the |
| 131 | // key value – but there are scripts and test cases that depend on signing data with keys of |
| 132 | // the same size. |
| 133 | JSG_REQUIRE(data.size() <= size, DOMDataError, "Blind Signing requires presigned data (", |
| 134 | data.size(), |
| 135 | " bytes) to be smaller than " |
| 136 | "the key (", |
| 137 | size, " bytes)."); |
| 138 | if (data.size() == size) { |
| 139 | auto dataVal = JSG_REQUIRE_NONNULL(toBignum(data), InternalDOMOperationError, |
| 140 | "Error converting presigned data", internalDescribeOpensslErrors()); |
| 141 | JSG_REQUIRE(BN_ucmp(dataVal, getN()) < 0, DOMDataError, |
| 142 | "Blind Signing requires presigned data value to be strictly smaller than RSA key" |
| 143 | "modulus, consider using a larger key size."); |
| 144 | } |
| 145 | |
| 146 | KJ_STACK_ARRAY(kj::byte, signature, size, 256, 256); |
| 147 | size_t signatureSize = 0; |
| 148 | OSSLCALL(RSA_decrypt(rsa, &signatureSize, signature.begin(), signature.size(), data.begin(), |
| 149 | data.size(), RSA_NO_PADDING)); |
| 150 | KJ_ASSERT(signatureSize <= signature.size()); |
| 151 | |
| 152 | return jsg::JsArrayBuffer::create(js, signature.first(signatureSize)); |
| 153 | } |
| 154 | |
| 155 | jsg::JsArrayBuffer Rsa::cipher(jsg::Lock& js, |
| 156 | EVP_PKEY_CTX* ctx, |
| 157 | SubtleCrypto::EncryptAlgorithm&& algorithm, |
| 158 | kj::ArrayPtr<const kj::byte> data, |
| 159 | EncryptDecryptFunction encryptDecrypt, |
| 160 | const EVP_MD* digest) const { |
| 161 | |
| 162 | JSG_REQUIRE(1 == EVP_PKEY_CTX_set_rsa_padding(ctx, RSA_PKCS1_OAEP_PADDING), |
| 163 | InternalDOMOperationError, "Error doing RSA OAEP encrypt/decrypt (", "padding", ")", |
| 164 | internalDescribeOpensslErrors()); |
| 165 | JSG_REQUIRE(1 == EVP_PKEY_CTX_set_rsa_oaep_md(ctx, digest), InternalDOMOperationError, |
| 166 | "Error doing RSA OAEP encrypt/decrypt (", "message digest", ")", |
| 167 | internalDescribeOpensslErrors()); |
| 168 | JSG_REQUIRE(1 == EVP_PKEY_CTX_set_rsa_mgf1_md(ctx, digest), InternalDOMOperationError, |
| 169 | "Error doing RSA OAEP encrypt/decrypt (", "MGF1 digest", ")", |
| 170 | internalDescribeOpensslErrors()); |
| 171 | |
| 172 | KJ_IF_SOME(lRef, algorithm.label) { |
| 173 | auto l = lRef.getHandle(js); |
| 174 | auto labelCopy = reinterpret_cast<uint8_t*>(OPENSSL_malloc(l.size())); |
| 175 | KJ_DEFER(OPENSSL_free(labelCopy)); |
| 176 | // If setting the label fails we need to remember to destroy the buffer. In practice it can't |
| 177 | // actually happen since we set RSA_PKCS1_OAEP_PADDING above & that appears to be the only way |
| 178 | // this API call can fail. |
| 179 | |
| 180 | JSG_REQUIRE(labelCopy != nullptr, DOMOperationError, |
| 181 | "Failed to allocate space for RSA-OAEP label copy", tryDescribeOpensslErrors()); |
| 182 | kj::arrayPtr(labelCopy, l.size()).copyFrom(l.asArrayPtr()); |
| 183 | |
| 184 | // EVP_PKEY_CTX_set0_rsa_oaep_label below takes ownership of the buffer passed in (must have |
| 185 | // been OPENSSL_malloc-allocated). |
| 186 | JSG_REQUIRE(1 == EVP_PKEY_CTX_set0_rsa_oaep_label(ctx, labelCopy, l.size()), DOMOperationError, |
| 187 | "Failed to set RSA-OAEP label", tryDescribeOpensslErrors()); |
| 188 | |
| 189 | // Ownership has now been transferred. The chromium WebCrypto code technically has a potential |
| 190 | // memory leak here in that they check the error for EVP_PKEY_CTX_set0_rsa_oaep_label after |
| 191 | // releasing. It's not actually possible though because the padding mode is set unconditionally |
| 192 | // to RSA_PKCS1_OAEP_PADDING which seems to be the only way setting the label will fail. |
| 193 | labelCopy = nullptr; |
| 194 | } |
| 195 | |
| 196 | size_t maxResultLength = 0; |
| 197 | // First compute an upper bound on the amount of space we need to store the encrypted/decrypted |
| 198 | // result. Then we actually apply the encryption & finally resize to the actual correct length. |
| 199 | JSG_REQUIRE(1 == encryptDecrypt(ctx, nullptr, &maxResultLength, data.begin(), data.size()), |
| 200 | DOMOperationError, "Failed to compute length of RSA-OAEP result", tryDescribeOpensslErrors()); |
| 201 | |
| 202 | kj::Vector<kj::byte> result(maxResultLength); |
| 203 | auto err = encryptDecrypt(ctx, result.begin(), &maxResultLength, data.begin(), data.size()); |
| 204 | JSG_REQUIRE( |
| 205 | 1 == err, DOMOperationError, "RSA-OAEP failed encrypt/decrypt", tryDescribeOpensslErrors()); |
| 206 | result.resize(maxResultLength); |
| 207 | |
| 208 | return jsg::JsArrayBuffer::create(js, result.asPtr()); |
| 209 | } |
| 210 | |
| 211 | SubtleCrypto::JsonWebKey Rsa::toJwk( |
| 212 | KeyType keyType, kj::Maybe<kj::String> maybeHashAlgorithm) const { |
| 213 | SubtleCrypto::JsonWebKey jwk; |
| 214 | jwk.kty = kj::str("RSA"); |
| 215 | KJ_IF_SOME(name, maybeHashAlgorithm) { |
| 216 | jwk.alg = kj::mv(name); |
| 217 | } |
| 218 | |
| 219 | jwk.n = fastEncodeBase64Url(KJ_REQUIRE_NONNULL(bignumToArray(KJ_REQUIRE_NONNULL(n)))); |
| 220 | jwk.e = fastEncodeBase64Url(KJ_REQUIRE_NONNULL(bignumToArray(KJ_REQUIRE_NONNULL(e)))); |
| 221 | |
| 222 | if (keyType == KeyType::PRIVATE) { |
| 223 | jwk.d = fastEncodeBase64Url(KJ_REQUIRE_NONNULL(bignumToArray(KJ_REQUIRE_NONNULL(d)))); |
| 224 | jwk.p = |
| 225 | fastEncodeBase64Url(KJ_REQUIRE_NONNULL(bignumToArray(KJ_REQUIRE_NONNULL(RSA_get0_p(rsa))))); |
| 226 | jwk.q = |
| 227 | fastEncodeBase64Url(KJ_REQUIRE_NONNULL(bignumToArray(KJ_REQUIRE_NONNULL(RSA_get0_q(rsa))))); |
| 228 | jwk.dp = fastEncodeBase64Url( |
| 229 | KJ_REQUIRE_NONNULL(bignumToArray(KJ_REQUIRE_NONNULL(RSA_get0_dmp1(rsa))))); |
| 230 | jwk.dq = fastEncodeBase64Url( |
| 231 | KJ_REQUIRE_NONNULL(bignumToArray(KJ_REQUIRE_NONNULL(RSA_get0_dmq1(rsa))))); |
| 232 | jwk.qi = fastEncodeBase64Url( |
| 233 | KJ_REQUIRE_NONNULL(bignumToArray(KJ_REQUIRE_NONNULL(RSA_get0_iqmp(rsa))))); |
| 234 | } |
| 235 | |
| 236 | return jwk; |
| 237 | } |
| 238 | |
| 239 | kj::Maybe<AsymmetricKeyData> Rsa::fromJwk( |
| 240 | jsg::Lock& js, KeyType keyType, const SubtleCrypto::JsonWebKey& jwk) { |
| 241 | ClearErrorOnReturn clearErrorOnReturn; |
| 242 | |
| 243 | if (jwk.kty != "RSA"_kj) return kj::none; |
| 244 | auto n = JSG_REQUIRE_NONNULL(jwk.n.map([](auto& str) { return str.asPtr(); }), Error, |
| 245 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 246 | "Modulus parameter (\"n\")."); |
| 247 | auto e = JSG_REQUIRE_NONNULL(jwk.e.map([](auto& str) { return str.asPtr(); }), Error, |
| 248 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 249 | "Exponent parameter (\"e\")."); |
| 250 | |
| 251 | auto rsa = OSSL_NEW(RSA); |
| 252 | |
| 253 | static constexpr auto kInvalidBase64Error = "Invalid RSA key in JSON Web Key; invalid base64."_kj; |
| 254 | |
| 255 | auto nBuf = simdutfBase64UrlDecodeChecked(js, n, kInvalidBase64Error); |
| 256 | auto nDecoded = toBignumOwned(nBuf.asArrayPtr()); |
| 257 | auto eBuf = simdutfBase64UrlDecodeChecked(js, e, kInvalidBase64Error); |
| 258 | auto eDecoded = toBignumOwned(eBuf.asArrayPtr()); |
| 259 | JSG_REQUIRE(RSA_set0_key(rsa.get(), nDecoded.get(), eDecoded.get(), nullptr) == 1, Error, |
| 260 | "Invalid RSA key in JSON Web Key; failed to set key parameters"); |
| 261 | nDecoded.release(); |
| 262 | eDecoded.release(); |
| 263 | |
| 264 | if (keyType == KeyType::PRIVATE) { |
| 265 | auto d = JSG_REQUIRE_NONNULL(jwk.d.map([](auto& str) { return str.asPtr(); }), Error, |
| 266 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 267 | "Private Exponent parameter (\"d\")."); |
| 268 | auto p = JSG_REQUIRE_NONNULL(jwk.p.map([](auto& str) { return str.asPtr(); }), Error, |
| 269 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 270 | "First Prime Factor parameter (\"p\")."); |
| 271 | auto q = JSG_REQUIRE_NONNULL(jwk.q.map([](auto& str) { return str.asPtr(); }), Error, |
| 272 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 273 | "Second Prime Factor parameter (\"q\")."); |
| 274 | auto dp = JSG_REQUIRE_NONNULL(jwk.dp.map([](auto& str) { return str.asPtr(); }), Error, |
| 275 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 276 | "First Factor CRT Exponent parameter (\"dp\")."); |
| 277 | auto dq = JSG_REQUIRE_NONNULL(jwk.dq.map([](auto& str) { return str.asPtr(); }), Error, |
| 278 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 279 | "Second Factor CRT Exponent parameter (\"dq\")."); |
| 280 | auto qi = JSG_REQUIRE_NONNULL(jwk.qi.map([](auto& str) { return str.asPtr(); }), Error, |
| 281 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 282 | "First CRT Coefficient parameter (\"qi\")."); |
| 283 | auto dBuf = simdutfBase64UrlDecodeChecked(js, d, "Invalid RSA key in JSON Web Key"_kj); |
| 284 | auto dDecoded = toBignumOwned(dBuf.asArrayPtr()); |
| 285 | auto pBuf = simdutfBase64UrlDecodeChecked(js, p, kInvalidBase64Error); |
| 286 | auto pDecoded = toBignumOwned(pBuf.asArrayPtr()); |
| 287 | auto qBuf = simdutfBase64UrlDecodeChecked(js, q, kInvalidBase64Error); |
| 288 | auto qDecoded = toBignumOwned(qBuf.asArrayPtr()); |
| 289 | auto dpBuf = simdutfBase64UrlDecodeChecked(js, dp, kInvalidBase64Error); |
| 290 | auto dpDecoded = toBignumOwned(dpBuf.asArrayPtr()); |
| 291 | auto dqBuf = simdutfBase64UrlDecodeChecked(js, dq, kInvalidBase64Error); |
| 292 | auto dqDecoded = toBignumOwned(dqBuf.asArrayPtr()); |
| 293 | auto qiBuf = simdutfBase64UrlDecodeChecked(js, qi, kInvalidBase64Error); |
| 294 | auto qiDecoded = toBignumOwned(qiBuf.asArrayPtr()); |
| 295 | |
| 296 | // .release() transfers BIGNUM ownership to the RSA key. UniqueBignum ensures |
| 297 | // cleanup if any earlier allocation or decode throws. |
| 298 | JSG_REQUIRE(RSA_set0_key(rsa.get(), nullptr, nullptr, dDecoded.get()) == 1, Error, |
| 299 | "Invalid RSA key in JSON Web Key; failed to set private exponent"); |
| 300 | dDecoded.release(); |
| 301 | JSG_REQUIRE(RSA_set0_factors(rsa.get(), pDecoded.get(), qDecoded.get()) == 1, Error, |
| 302 | "Invalid RSA key in JSON Web Key; failed to set prime factors"); |
| 303 | pDecoded.release(); |
| 304 | qDecoded.release(); |
| 305 | JSG_REQUIRE( |
| 306 | RSA_set0_crt_params(rsa.get(), dpDecoded.get(), dqDecoded.get(), qiDecoded.get()) == 1, |
| 307 | Error, "Invalid RSA key in JSON Web Key; failed to set CRT parameters"); |
| 308 | dpDecoded.release(); |
| 309 | dqDecoded.release(); |
| 310 | qiDecoded.release(); |
| 311 | } |
| 312 | |
| 313 | auto evpPkey = OSSL_NEW(EVP_PKEY); |
| 314 | OSSLCALL(EVP_PKEY_set1_RSA(evpPkey.get(), rsa.get())); |
| 315 | |
| 316 | auto usages = keyType == KeyType::PRIVATE ? CryptoKeyUsageSet::privateKeyMask() |
| 317 | : CryptoKeyUsageSet::publicKeyMask(); |
| 318 | return AsymmetricKeyData{kj::mv(evpPkey), keyType, usages}; |
| 319 | } |
| 320 | |
| 321 | kj::String Rsa::toPem( |
| 322 | jsg::Lock& js, KeyEncoding encoding, KeyType keyType, kj::Maybe<CipherOptions> options) const { |
| 323 | ClearErrorOnReturn clearErrorOnReturn; |
| 324 | auto bio = OSSL_BIO_MEM(); |
| 325 | switch (keyType) { |
| 326 | case KeyType::PUBLIC: { |
| 327 | switch (encoding) { |
| 328 | case KeyEncoding::PKCS1: { |
| 329 | JSG_REQUIRE(PEM_write_bio_RSAPublicKey(bio.get(), rsa) == 1, Error, |
| 330 | "Failed to write RSA public key to PEM", tryDescribeOpensslErrors()); |
| 331 | break; |
| 332 | } |
| 333 | case workerd::api::KeyEncoding::SPKI: { |
| 334 | JSG_REQUIRE(PEM_write_bio_RSA_PUBKEY(bio.get(), rsa) == 1, Error, |
| 335 | "Failed to write RSA public key to PEM", tryDescribeOpensslErrors()); |
| 336 | break; |
| 337 | } |
| 338 | default: { |
| 339 | JSG_FAIL_REQUIRE(Error, "Unsupported RSA public key encoding: ", encoding); |
| 340 | } |
| 341 | } |
| 342 | break; |
| 343 | } |
| 344 | case KeyType::PRIVATE: { |
| 345 | kj::byte* passphrase = nullptr; |
| 346 | size_t passLen = 0; |
| 347 | const EVP_CIPHER* cipher = nullptr; |
| 348 | KJ_IF_SOME(opts, options) { |
| 349 | passphrase = const_cast<kj::byte*>(opts.passphrase.begin()); |
| 350 | passLen = opts.passphrase.size(); |
| 351 | cipher = opts.cipher; |
| 352 | } |
| 353 | switch (encoding) { |
| 354 | case KeyEncoding::PKCS1: { |
| 355 | JSG_REQUIRE(PEM_write_bio_RSAPrivateKey( |
| 356 | bio.get(), rsa, cipher, passphrase, passLen, nullptr, nullptr) == 1, |
| 357 | Error, "Failed to write RSA private key to PEM", tryDescribeOpensslErrors()); |
| 358 | break; |
| 359 | } |
| 360 | case KeyEncoding::PKCS8: { |
| 361 | auto evpPkey = OSSL_NEW(EVP_PKEY); |
| 362 | OSSLCALL(EVP_PKEY_set1_RSA(evpPkey.get(), rsa)); |
| 363 | JSG_REQUIRE(PEM_write_bio_PKCS8PrivateKey(bio.get(), evpPkey.get(), cipher, |
| 364 | reinterpret_cast<char*>(passphrase), passLen, nullptr, nullptr) == 1, |
| 365 | Error, "Failed to write RSA private key to PKCS8 PEM", tryDescribeOpensslErrors()); |
| 366 | break; |
| 367 | } |
| 368 | default: { |
| 369 | JSG_FAIL_REQUIRE(Error, "Unsupported RSA private key encoding: ", encoding); |
| 370 | } |
| 371 | } |
| 372 | break; |
| 373 | } |
| 374 | default: |
| 375 | KJ_UNREACHABLE; |
| 376 | } |
| 377 | return kj::str(bioToArray(js, bio.get()).asArrayPtr().asChars()); |
| 378 | } |
| 379 | |
| 380 | jsg::JsArrayBuffer Rsa::toDer( |
| 381 | jsg::Lock& js, KeyEncoding encoding, KeyType keyType, kj::Maybe<CipherOptions> options) const { |
| 382 | ClearErrorOnReturn clearErrorOnReturn; |
| 383 | auto bio = OSSL_BIO_MEM(); |
| 384 | switch (keyType) { |
| 385 | case KeyType::PUBLIC: { |
| 386 | switch (encoding) { |
| 387 | case KeyEncoding::PKCS1: { |
| 388 | JSG_REQUIRE(i2d_RSAPublicKey_bio(bio.get(), rsa) == 1, Error, |
| 389 | "Failed to write RSA public key to DER", tryDescribeOpensslErrors()); |
| 390 | break; |
| 391 | } |
| 392 | case workerd::api::KeyEncoding::SPKI: { |
| 393 | auto evpPkey = OSSL_NEW(EVP_PKEY); |
| 394 | OSSLCALL(EVP_PKEY_set1_RSA(evpPkey.get(), rsa)); |
| 395 | JSG_REQUIRE(i2d_PUBKEY_bio(bio.get(), evpPkey.get()) == 1, Error, |
| 396 | "Failed to write RSA public key to SPKI", tryDescribeOpensslErrors()); |
| 397 | break; |
| 398 | } |
| 399 | default: { |
| 400 | JSG_FAIL_REQUIRE(Error, "Unsupported RSA public key encoding: ", encoding); |
| 401 | } |
| 402 | } |
| 403 | break; |
| 404 | } |
| 405 | case KeyType::PRIVATE: { |
| 406 | kj::byte* passphrase = nullptr; |
| 407 | size_t passLen = 0; |
| 408 | const EVP_CIPHER* cipher = nullptr; |
| 409 | KJ_IF_SOME(opts, options) { |
| 410 | passphrase = const_cast<kj::byte*>(opts.passphrase.begin()); |
| 411 | passLen = opts.passphrase.size(); |
| 412 | cipher = opts.cipher; |
| 413 | } |
| 414 | switch (encoding) { |
| 415 | case KeyEncoding::PKCS1: { |
| 416 | // Does not permit encryption |
| 417 | JSG_REQUIRE(i2d_RSAPrivateKey_bio(bio.get(), rsa), Error, |
| 418 | "Failed to write RSA private key to PEM", tryDescribeOpensslErrors()); |
| 419 | break; |
| 420 | } |
| 421 | case KeyEncoding::PKCS8: { |
| 422 | auto evpPkey = OSSL_NEW(EVP_PKEY); |
| 423 | OSSLCALL(EVP_PKEY_set1_RSA(evpPkey.get(), rsa)); |
| 424 | JSG_REQUIRE(i2d_PKCS8PrivateKey_bio(bio.get(), evpPkey.get(), cipher, |
| 425 | reinterpret_cast<char*>(passphrase), passLen, nullptr, nullptr) == 1, |
| 426 | Error, "Failed to write RSA private key to PKCS8 PEM", tryDescribeOpensslErrors()); |
| 427 | break; |
| 428 | } |
| 429 | default: { |
| 430 | JSG_FAIL_REQUIRE(Error, "Unsupported RSA private key encoding: ", encoding); |
| 431 | } |
| 432 | } |
| 433 | break; |
| 434 | } |
| 435 | default: |
| 436 | KJ_UNREACHABLE; |
| 437 | } |
| 438 | return bioToArray(js, bio.get()); |
| 439 | } |
| 440 | |
| 441 | void Rsa::validateRsaParams( |
| 442 | jsg::Lock& js, size_t modulusLength, kj::ArrayPtr<kj::byte> publicExponent, bool isImport) { |
| 443 | KJ_ASSERT(modulusLength <= ~uint16_t(0)); |
| 444 | // Use Chromium's limits for RSA keygen to avoid infinite loops: |
| 445 | // * Key sizes a multiple of 8 bits. |
| 446 | // * Key sizes must be in [256, 16k] bits. |
| 447 | auto strictCrypto = FeatureFlags::get(js).getStrictCrypto(); |
| 448 | JSG_REQUIRE(!(strictCrypto || !isImport) || |
| 449 | (modulusLength % 8 == 0 && modulusLength >= 256 && modulusLength <= 16384), |
| 450 | DOMOperationError, |
| 451 | "The modulus length must be a multiple of 8 and " |
| 452 | "between 256 and 16k, but ", |
| 453 | modulusLength, " was requested."); |
| 454 | |
| 455 | // Now check the public exponent for allow-listed values. |
| 456 | // First see if we can convert the public exponent to an unsigned number. Unfortunately OpenSSL |
| 457 | // doesn't have convenient APIs to do this (since these are bignums) so we have to do it by hand. |
| 458 | // Since the problematic BIGNUMs are within the range of an unsigned int (& technically an |
| 459 | // unsigned short) we can treat an out-of-range issue as valid input. |
| 460 | KJ_IF_SOME(v, fromBignum<unsigned>(publicExponent)) { |
| 461 | if (!isImport) { |
| 462 | JSG_REQUIRE(v == 3 || v == 65537, DOMOperationError, |
| 463 | "The \"publicExponent\" must be either 3 or 65537, but got ", v, "."); |
| 464 | } else if (strictCrypto) { |
| 465 | // While we have long required the exponent to be 3 or 65537 when generating keys, handle |
| 466 | // imported keys more permissively and allow additional exponents that are considered safe |
| 467 | // and commonly used. |
| 468 | JSG_REQUIRE(v == 3 || v == 17 || v == 37 || v == 65537, DOMOperationError, |
| 469 | "Imported RSA key has invalid publicExponent ", v, "."); |
| 470 | } |
| 471 | } else { |
| 472 | JSG_FAIL_REQUIRE(DOMOperationError, |
| 473 | "The \"publicExponent\" must be either 3 or 65537, but " |
| 474 | "got a number larger than 2^32."); |
| 475 | } |
| 476 | } |
| 477 | |
| 478 | bool Rsa::isRSAPrivateKey(kj::ArrayPtr<const kj::byte> keyData) { |
| 479 | KJ_IF_SOME(rem, tryGetAsn1Sequence(keyData)) { |
| 480 | return rem.size() >= 3 && rem[0] == 2 && rem[1] == 1 && !(rem[2] & 0xfe); |
| 481 | } |
| 482 | return false; |
| 483 | } |
| 484 | |
| 485 | // ====================================================================================== |
| 486 | // Web Crypto Impl: RSASSA-PKCS1-V1_5, RSA-PSS, RSA-OEAP, RSA-RAW |
| 487 | |
| 488 | namespace { |
| 489 | class RsaBase: public AsymmetricKeyCryptoKeyImpl { |
| 490 | public: |
| 491 | explicit RsaBase( |
| 492 | AsymmetricKeyData keyData, CryptoKey::RsaKeyAlgorithm keyAlgorithm, bool extractable) |
| 493 | : AsymmetricKeyCryptoKeyImpl(kj::mv(keyData), extractable), |
| 494 | keyAlgorithm(kj::mv(keyAlgorithm)) {} |
| 495 | |
| 496 | kj::StringPtr jsgGetMemoryName() const override { |
| 497 | return "AsymmetricKey"; |
| 498 | } |
| 499 | size_t jsgGetMemorySelfSize() const override { |
| 500 | return sizeof(AsymmetricKeyCryptoKeyImpl); |
| 501 | } |
| 502 | void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override { |
| 503 | AsymmetricKeyCryptoKeyImpl::jsgGetMemoryInfo(tracker); |
| 504 | tracker.trackField("keyAlgorithm", keyAlgorithm); |
| 505 | } |
| 506 | |
| 507 | protected: |
| 508 | CryptoKey::RsaKeyAlgorithm keyAlgorithm; |
| 509 | |
| 510 | private: |
| 511 | SubtleCrypto::JsonWebKey exportJwk() const override final { |
| 512 | auto rsa = JSG_REQUIRE_NONNULL(Rsa::tryGetRsa(getEvpPkey()), DOMDataError, |
| 513 | "No RSA data backing key", tryDescribeOpensslErrors()); |
| 514 | return rsa.toJwk(getTypeEnum(), jwkHashAlgorithmName()); |
| 515 | } |
| 516 | |
| 517 | jsg::JsArrayBuffer exportRaw(jsg::Lock& js) const override final { |
| 518 | JSG_FAIL_REQUIRE( |
| 519 | DOMInvalidAccessError, "Cannot export \"", getAlgorithmName(), "\" in \"raw\" format."); |
| 520 | } |
| 521 | |
| 522 | CryptoKey::AsymmetricKeyDetails getAsymmetricKeyDetail(jsg::Lock& js) const override { |
| 523 | return KJ_ASSERT_NONNULL(Rsa::tryGetRsa(getEvpPkey())).getAsymmetricKeyDetail(js); |
| 524 | } |
| 525 | |
| 526 | virtual kj::String jwkHashAlgorithmName() const = 0; |
| 527 | }; |
| 528 | |
| 529 | class RsassaPkcs1V15Key final: public RsaBase { |
| 530 | public: |
| 531 | explicit RsassaPkcs1V15Key( |
| 532 | AsymmetricKeyData keyData, CryptoKey::RsaKeyAlgorithm keyAlgorithm, bool extractable) |
| 533 | : RsaBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable) {} |
| 534 | |
| 535 | CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override { |
| 536 | return keyAlgorithm.clone(js); |
| 537 | } |
| 538 | kj::StringPtr getAlgorithmName() const override { |
| 539 | return "RSASSA-PKCS1-v1_5"; |
| 540 | } |
| 541 | |
| 542 | kj::StringPtr chooseHash( |
| 543 | const kj::Maybe<kj::OneOf<kj::String, SubtleCrypto::HashAlgorithm>>& callTimeHash) |
| 544 | const override { |
| 545 | // RSASSA-PKCS1-v1_5 attaches the hash to the key, ignoring whatever is specified at call time. |
| 546 | return KJ_REQUIRE_NONNULL(keyAlgorithm.hash).name; |
| 547 | } |
| 548 | |
| 549 | private: |
| 550 | kj::String jwkHashAlgorithmName() const override { |
| 551 | const auto& hashName = KJ_REQUIRE_NONNULL(keyAlgorithm.hash).name; |
| 552 | JSG_REQUIRE(hashName.startsWith("SHA"), DOMNotSupportedError, |
| 553 | "JWK export not supported for hash algorithm \"", hashName, "\"."); |
| 554 | return kj::str("RS", hashName.slice(4, hashName.size())); |
| 555 | } |
| 556 | }; |
| 557 | |
| 558 | class RsaPssKey final: public RsaBase { |
| 559 | public: |
| 560 | explicit RsaPssKey( |
| 561 | AsymmetricKeyData keyData, CryptoKey::RsaKeyAlgorithm keyAlgorithm, bool extractable) |
| 562 | : RsaBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable) {} |
| 563 | |
| 564 | CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override { |
| 565 | return keyAlgorithm.clone(js); |
| 566 | } |
| 567 | kj::StringPtr getAlgorithmName() const override { |
| 568 | return keyAlgorithm.name; |
| 569 | } |
| 570 | |
| 571 | kj::StringPtr chooseHash( |
| 572 | const kj::Maybe<kj::OneOf<kj::String, SubtleCrypto::HashAlgorithm>>& callTimeHash) |
| 573 | const override { |
| 574 | // RSA-PSS attaches the hash to the key, ignoring whatever is specified at call time. |
| 575 | return KJ_REQUIRE_NONNULL(keyAlgorithm.hash).name; |
| 576 | } |
| 577 | |
| 578 | void addSalt(EVP_PKEY_CTX* pctx, const SubtleCrypto::SignAlgorithm& algorithm) const override { |
| 579 | auto salt = JSG_REQUIRE_NONNULL(algorithm.saltLength, TypeError, |
| 580 | "Failed to provide salt for RSA-PSS key operation which requires a salt"); |
| 581 | JSG_REQUIRE(salt >= 0, DOMDataError, "SaltLength for RSA-PSS must be non-negative (provided ", |
| 582 | salt, ")."); |
| 583 | OSSLCALL(EVP_PKEY_CTX_set_rsa_padding(pctx, RSA_PKCS1_PSS_PADDING)); |
| 584 | OSSLCALL(EVP_PKEY_CTX_set_rsa_pss_saltlen(pctx, salt)); |
| 585 | } |
| 586 | |
| 587 | private: |
| 588 | kj::String jwkHashAlgorithmName() const override { |
| 589 | const auto& hashName = KJ_REQUIRE_NONNULL(keyAlgorithm.hash).name; |
| 590 | JSG_REQUIRE(hashName.startsWith("SHA"), DOMNotSupportedError, |
| 591 | "JWK export not supported for hash algorithm \"", hashName, "\"."); |
| 592 | return kj::str("PS", hashName.slice(4, hashName.size())); |
| 593 | } |
| 594 | }; |
| 595 | |
| 596 | class RsaOaepKey final: public RsaBase { |
| 597 | using InitFunction = decltype(EVP_PKEY_encrypt_init); |
| 598 | using EncryptDecryptFunction = decltype(EVP_PKEY_encrypt); |
| 599 | |
| 600 | public: |
| 601 | explicit RsaOaepKey( |
| 602 | AsymmetricKeyData keyData, CryptoKey::RsaKeyAlgorithm keyAlgorithm, bool extractable) |
| 603 | : RsaBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable) {} |
| 604 | |
| 605 | CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override { |
| 606 | return keyAlgorithm.clone(js); |
| 607 | } |
| 608 | kj::StringPtr getAlgorithmName() const override { |
| 609 | return keyAlgorithm.name; |
| 610 | } |
| 611 | |
| 612 | kj::StringPtr chooseHash( |
| 613 | const kj::Maybe<kj::OneOf<kj::String, SubtleCrypto::HashAlgorithm>>& callTimeHash) |
| 614 | const override { |
| 615 | // RSA-OAEP is for encryption/decryption, not signing, but this method is called by the |
| 616 | // parent class when performing sign() or verify(). |
| 617 | JSG_FAIL_REQUIRE(DOMNotSupportedError, |
| 618 | "The sign and verify operations are not implemented for \"", keyAlgorithm.name, "\"."); |
| 619 | } |
| 620 | |
| 621 | jsg::JsArrayBuffer encrypt(jsg::Lock& js, |
| 622 | SubtleCrypto::EncryptAlgorithm&& algorithm, |
| 623 | kj::ArrayPtr<const kj::byte> plainText) const override { |
| 624 | JSG_REQUIRE(getTypeEnum() == KeyType::PUBLIC, DOMInvalidAccessError, |
| 625 | "Encryption/key wrapping only works with public keys, not \"", getType(), "\"."); |
| 626 | return commonEncryptDecrypt( |
| 627 | js, kj::mv(algorithm), plainText, EVP_PKEY_encrypt_init, EVP_PKEY_encrypt); |
| 628 | } |
| 629 | |
| 630 | jsg::JsArrayBuffer decrypt(jsg::Lock& js, |
| 631 | SubtleCrypto::EncryptAlgorithm&& algorithm, |
| 632 | kj::ArrayPtr<const kj::byte> cipherText) const override { |
| 633 | JSG_REQUIRE(getTypeEnum() == KeyType::PRIVATE, DOMInvalidAccessError, |
| 634 | "Decryption/key unwrapping only works with private keys, not \"", getType(), "\"."); |
| 635 | return commonEncryptDecrypt( |
| 636 | js, kj::mv(algorithm), cipherText, EVP_PKEY_decrypt_init, EVP_PKEY_decrypt); |
| 637 | } |
| 638 | |
| 639 | private: |
| 640 | jsg::JsArrayBuffer commonEncryptDecrypt(jsg::Lock& js, |
| 641 | SubtleCrypto::EncryptAlgorithm&& algorithm, |
| 642 | kj::ArrayPtr<const kj::byte> data, |
| 643 | InitFunction init, |
| 644 | EncryptDecryptFunction encryptDecrypt) const { |
| 645 | auto pkey = getEvpPkey(); |
| 646 | auto digest = lookupDigestAlgorithm(KJ_REQUIRE_NONNULL(keyAlgorithm.hash).name).second; |
| 647 | auto ctx = OSSL_NEW(EVP_PKEY_CTX, pkey, nullptr); |
| 648 | JSG_REQUIRE(1 == init(ctx.get()), DOMOperationError, "RSA-OAEP failed to initialize", |
| 649 | tryDescribeOpensslErrors()); |
| 650 | return KJ_ASSERT_NONNULL(Rsa::tryGetRsa(pkey)) |
| 651 | .cipher(js, ctx, kj::mv(algorithm), data, encryptDecrypt, digest); |
| 652 | } |
| 653 | |
| 654 | kj::String jwkHashAlgorithmName() const override { |
| 655 | const auto& hashName = KJ_REQUIRE_NONNULL(keyAlgorithm.hash).name; |
| 656 | JSG_REQUIRE(hashName.startsWith("SHA"), DOMNotSupportedError, |
| 657 | "JWK export not supported for hash algorithm \"", hashName, "\"."); |
| 658 | if (hashName == "SHA-1") { |
| 659 | return kj::str("RSA-OAEP"); |
| 660 | } |
| 661 | return kj::str("RSA-OAEP-", hashName.slice(4, hashName.size())); |
| 662 | } |
| 663 | }; |
| 664 | |
| 665 | class RsaRawKey final: public RsaBase { |
| 666 | public: |
| 667 | explicit RsaRawKey( |
| 668 | AsymmetricKeyData keyData, CryptoKey::RsaKeyAlgorithm keyAlgorithm, bool extractable) |
| 669 | : RsaBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable) {} |
| 670 | |
| 671 | jsg::JsArrayBuffer sign(jsg::Lock& js, |
| 672 | SubtleCrypto::SignAlgorithm&& algorithm, |
| 673 | kj::ArrayPtr<const kj::byte> data) const override { |
| 674 | auto rsa = JSG_REQUIRE_NONNULL(Rsa::tryGetRsa(getEvpPkey()), DOMDataError, "Missing RSA key"); |
| 675 | return rsa.sign(js, data); |
| 676 | } |
| 677 | |
| 678 | bool verify(jsg::Lock& js, |
| 679 | SubtleCrypto::SignAlgorithm&& algorithm, |
| 680 | kj::ArrayPtr<const kj::byte> signature, |
| 681 | kj::ArrayPtr<const kj::byte> data) const override { |
| 682 | KJ_UNIMPLEMENTED("RawRsa Verification currently unsupported"); |
| 683 | } |
| 684 | |
| 685 | CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override { |
| 686 | return keyAlgorithm.clone(js); |
| 687 | } |
| 688 | |
| 689 | kj::StringPtr getAlgorithmName() const override { |
| 690 | return keyAlgorithm.name; |
| 691 | } |
| 692 | |
| 693 | kj::StringPtr chooseHash( |
| 694 | const kj::Maybe<kj::OneOf<kj::String, SubtleCrypto::HashAlgorithm>>& callTimeHash) |
| 695 | const override { |
| 696 | KJ_UNIMPLEMENTED("this should not be called since we overrode sign() and verify()"); |
| 697 | } |
| 698 | |
| 699 | private: |
| 700 | kj::String jwkHashAlgorithmName() const override { |
| 701 | const auto& hashName = KJ_REQUIRE_NONNULL(keyAlgorithm.hash).name; |
| 702 | JSG_REQUIRE(hashName.startsWith("SHA"), DOMNotSupportedError, |
| 703 | "JWK export not supported for hash algorithm \"", hashName, "\"."); |
| 704 | return kj::str("RS", hashName.slice(4, hashName.size())); |
| 705 | } |
| 706 | }; |
| 707 | |
| 708 | CryptoKeyPair generateRsaPair(jsg::Lock& js, |
| 709 | kj::StringPtr normalizedName, |
| 710 | kj::Own<EVP_PKEY> privateEvpPKey, |
| 711 | kj::Own<EVP_PKEY> publicEvpPKey, |
| 712 | CryptoKey::RsaKeyAlgorithm&& keyAlgorithm, |
| 713 | bool privateKeyExtractable, |
| 714 | CryptoKeyUsageSet usages) { |
| 715 | auto privateKeyAlgorithm = keyAlgorithm.clone(js); |
| 716 | |
| 717 | AsymmetricKeyData publicKeyData{ |
| 718 | .evpPkey = kj::mv(publicEvpPKey), |
| 719 | .keyType = KeyType::PUBLIC, |
| 720 | .usages = usages & CryptoKeyUsageSet::publicKeyMask(), |
| 721 | }; |
| 722 | AsymmetricKeyData privateKeyData{ |
| 723 | .evpPkey = kj::mv(privateEvpPKey), |
| 724 | .keyType = KeyType::PRIVATE, |
| 725 | .usages = usages & CryptoKeyUsageSet::privateKeyMask(), |
| 726 | }; |
| 727 | |
| 728 | static constexpr auto createPair = [](jsg::Lock& js, kj::Own<CryptoKey::Impl> publicKey, |
| 729 | kj::Own<CryptoKey::Impl> privateKey) { |
| 730 | return CryptoKeyPair{.publicKey = js.alloc<CryptoKey>(kj::mv(publicKey)), |
| 731 | .privateKey = js.alloc<CryptoKey>(kj::mv(privateKey))}; |
| 732 | }; |
| 733 | |
| 734 | if (normalizedName == "RSASSA-PKCS1-v1_5") { |
| 735 | return createPair(js, |
| 736 | kj::heap<RsassaPkcs1V15Key>(kj::mv(publicKeyData), kj::mv(keyAlgorithm), true), |
| 737 | kj::heap<RsassaPkcs1V15Key>( |
| 738 | kj::mv(privateKeyData), kj::mv(privateKeyAlgorithm), privateKeyExtractable)); |
| 739 | } else if (normalizedName == "RSA-PSS") { |
| 740 | return createPair(js, kj::heap<RsaPssKey>(kj::mv(publicKeyData), kj::mv(keyAlgorithm), true), |
| 741 | kj::heap<RsaPssKey>( |
| 742 | kj::mv(privateKeyData), kj::mv(privateKeyAlgorithm), privateKeyExtractable)); |
| 743 | } else if (normalizedName == "RSA-OAEP") { |
| 744 | return createPair(js, kj::heap<RsaOaepKey>(kj::mv(publicKeyData), kj::mv(keyAlgorithm), true), |
| 745 | kj::heap<RsaOaepKey>( |
| 746 | kj::mv(privateKeyData), kj::mv(privateKeyAlgorithm), privateKeyExtractable)); |
| 747 | } |
| 748 | JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unimplemented RSA generation \"", normalizedName, "\"."); |
| 749 | } |
| 750 | |
| 751 | kj::Own<EVP_PKEY> rsaJwkReader(SubtleCrypto::JsonWebKey&& keyDataJwk) { |
| 752 | auto rsaKey = OSSL_NEW(RSA); |
| 753 | |
| 754 | auto modulus = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.n), DOMDataError, |
| 755 | "Invalid RSA key in JSON Web Key; missing or invalid Modulus " |
| 756 | "parameter (\"n\")."); |
| 757 | auto publicExponent = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.e), DOMDataError, |
| 758 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 759 | "Exponent parameter (\"e\")."); |
| 760 | |
| 761 | auto nBignum = toBignumOwned(modulus); |
| 762 | auto eBignum = toBignumOwned(publicExponent); |
| 763 | OSSLCALL(RSA_set0_key(rsaKey.get(), nBignum.get(), eBignum.get(), nullptr)); |
| 764 | nBignum.release(); |
| 765 | eBignum.release(); |
| 766 | |
| 767 | if (keyDataJwk.d != kj::none) { |
| 768 | // This is a private key. |
| 769 | |
| 770 | auto privateExponent = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.d), DOMDataError, |
| 771 | "Invalid RSA key in JSON Web Key; missing or invalid " |
| 772 | "Private Exponent parameter (\"d\")."); |
| 773 | |
| 774 | auto dBignum = toBignumOwned(privateExponent); |
| 775 | OSSLCALL(RSA_set0_key(rsaKey.get(), nullptr, nullptr, dBignum.get())); |
| 776 | dBignum.release(); |
| 777 | |
| 778 | auto presence = (keyDataJwk.p != kj::none) + (keyDataJwk.q != kj::none) + |
| 779 | (keyDataJwk.dp != kj::none) + (keyDataJwk.dq != kj::none) + (keyDataJwk.qi != kj::none); |
| 780 | |
| 781 | if (presence == 5) { |
| 782 | auto firstPrimeFactor = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.p), DOMDataError, |
| 783 | "Invalid RSA key in JSON Web Key; invalid First Prime " |
| 784 | "Factor parameter (\"p\")."); |
| 785 | auto secondPrimeFactor = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.q), DOMDataError, |
| 786 | "Invalid RSA key in JSON Web Key; invalid Second Prime " |
| 787 | "Factor parameter (\"q\")."); |
| 788 | auto firstFactorCrtExponent = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.dp), DOMDataError, |
| 789 | "Invalid RSA key in JSON Web Key; invalid First Factor " |
| 790 | "CRT Exponent parameter (\"dp\")."); |
| 791 | auto secondFactorCrtExponent = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.dq), DOMDataError, |
| 792 | "Invalid RSA key in JSON Web Key; invalid Second Factor " |
| 793 | "CRT Exponent parameter (\"dq\")."); |
| 794 | auto firstCrtCoefficient = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.qi), DOMDataError, |
| 795 | "Invalid RSA key in JSON Web Key; invalid First CRT " |
| 796 | "Coefficient parameter (\"qi\")."); |
| 797 | |
| 798 | auto pBn = toBignumOwned(firstPrimeFactor); |
| 799 | auto qBn = toBignumOwned(secondPrimeFactor); |
| 800 | auto dpBn = toBignumOwned(firstFactorCrtExponent); |
| 801 | auto dqBn = toBignumOwned(secondFactorCrtExponent); |
| 802 | auto qiBn = toBignumOwned(firstCrtCoefficient); |
| 803 | |
| 804 | OSSLCALL(RSA_set0_factors(rsaKey.get(), pBn.get(), qBn.get())); |
| 805 | pBn.release(); |
| 806 | qBn.release(); |
| 807 | OSSLCALL(RSA_set0_crt_params(rsaKey.get(), dpBn.get(), dqBn.get(), qiBn.get())); |
| 808 | dpBn.release(); |
| 809 | dqBn.release(); |
| 810 | qiBn.release(); |
| 811 | } else { |
| 812 | JSG_REQUIRE(presence == 0, DOMDataError, |
| 813 | "Invalid RSA private key in JSON Web Key; if one Prime " |
| 814 | "Factor or CRT Exponent/Coefficient parameter is present, then they must all be " |
| 815 | "present (\"p\", \"q\", \"dp\", \"dq\", \"qi\")."); |
| 816 | } |
| 817 | } |
| 818 | |
| 819 | auto evpPkey = OSSL_NEW(EVP_PKEY); |
| 820 | OSSLCALL(EVP_PKEY_set1_RSA(evpPkey.get(), rsaKey.get())); |
| 821 | return evpPkey; |
| 822 | } |
| 823 | } // namespace |
| 824 | |
| 825 | kj::OneOf<jsg::Ref<CryptoKey>, CryptoKeyPair> CryptoKey::Impl::generateRsa(jsg::Lock& js, |
| 826 | kj::StringPtr normalizedName, |
| 827 | SubtleCrypto::GenerateKeyAlgorithm&& algorithm, |
| 828 | bool extractable, |
| 829 | kj::ArrayPtr<const kj::String> keyUsages) { |
| 830 | |
| 831 | KJ_ASSERT(normalizedName == "RSASSA-PKCS1-v1_5" || normalizedName == "RSA-PSS" || |
| 832 | normalizedName == "RSA-OAEP", |
| 833 | "generateRsa called on non-RSA cryptoKey", normalizedName); |
| 834 | |
| 835 | auto publicExponent = JSG_REQUIRE_NONNULL(kj::mv(algorithm.publicExponent), TypeError, |
| 836 | "Missing field \"publicExponent\" in \"algorithm\".") |
| 837 | .getHandle(js); |
| 838 | kj::StringPtr hash = api::getAlgorithmName( |
| 839 | JSG_REQUIRE_NONNULL(algorithm.hash, TypeError, "Missing field \"hash\" in \"algorithm\".")); |
| 840 | int modulusLength = JSG_REQUIRE_NONNULL( |
| 841 | algorithm.modulusLength, TypeError, "Missing field \"modulusLength\" in \"algorithm\"."); |
| 842 | JSG_REQUIRE(modulusLength > 0, DOMOperationError, |
| 843 | "modulusLength must be greater than zero " |
| 844 | "(requested ", |
| 845 | modulusLength, ")."); |
| 846 | auto [normalizedHashName, hashEvpMd] = lookupDigestAlgorithm(hash); |
| 847 | |
| 848 | CryptoKeyUsageSet validUsages = (normalizedName == "RSA-OAEP") |
| 849 | ? (CryptoKeyUsageSet::encrypt() | CryptoKeyUsageSet::decrypt() | |
| 850 | CryptoKeyUsageSet::wrapKey() | CryptoKeyUsageSet::unwrapKey()) |
| 851 | : (CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify()); |
| 852 | auto usages = CryptoKeyUsageSet::validate( |
| 853 | normalizedName, CryptoKeyUsageSet::Context::generate, keyUsages, validUsages); |
| 854 | |
| 855 | Rsa::validateRsaParams(js, modulusLength, publicExponent.asArrayPtr()); |
| 856 | // BoringSSL silently uses (modulusLength & ~127) for the key size, i.e. it rounds down to the |
| 857 | // closest multiple of 128 bits. This can easily cause confusion when non-standard key sizes are |
| 858 | // requested. |
| 859 | // The `modulusLength` field of the resulting CryptoKey will be incorrect when the compat flag |
| 860 | // is disabled and the key size is rounded down, but since it is not currently used this is |
| 861 | // acceptable. |
| 862 | JSG_REQUIRE(!(FeatureFlags::get(js).getStrictCrypto() && (modulusLength & 127)), |
| 863 | DOMOperationError, "Can't generate key: RSA key size is required to be a multiple of 128"); |
| 864 | |
| 865 | auto bnExponent = JSG_REQUIRE_NONNULL(toBignum(publicExponent.asArrayPtr()), |
| 866 | InternalDOMOperationError, "Error setting up RSA keygen."); |
| 867 | |
| 868 | auto rsaPrivateKey = OSSL_NEW(RSA); |
| 869 | OSSLCALL(RSA_generate_key_ex(rsaPrivateKey, modulusLength, bnExponent.get(), nullptr)); |
| 870 | auto privateEvpPKey = OSSL_NEW(EVP_PKEY); |
| 871 | OSSLCALL(EVP_PKEY_set1_RSA(privateEvpPKey.get(), rsaPrivateKey.get())); |
| 872 | kj::Own<RSA> rsaPublicKey = OSSLCALL_OWN(RSA, RSAPublicKey_dup(rsaPrivateKey.get()), |
| 873 | InternalDOMOperationError, "Error finalizing RSA keygen", internalDescribeOpensslErrors()); |
| 874 | auto publicEvpPKey = OSSL_NEW(EVP_PKEY); |
| 875 | OSSLCALL(EVP_PKEY_set1_RSA(publicEvpPKey.get(), rsaPublicKey)); |
| 876 | |
| 877 | // Create a JsUint8Array copy of the public exponent for the key algorithm struct. |
| 878 | auto expCopy = jsg::JsUint8Array::create(js, publicExponent.asArrayPtr()); |
| 879 | auto keyAlgorithm = CryptoKey::RsaKeyAlgorithm{.name = normalizedName, |
| 880 | .modulusLength = static_cast<uint16_t>(modulusLength), |
| 881 | .publicExponent = jsg::JsBufferSource(expCopy).addRef(js), |
| 882 | .hash = KeyAlgorithm{normalizedHashName}}; |
| 883 | |
| 884 | return generateRsaPair(js, normalizedName, kj::mv(privateEvpPKey), kj::mv(publicEvpPKey), |
| 885 | kj::mv(keyAlgorithm), extractable, usages); |
| 886 | } |
| 887 | |
| 888 | kj::Own<CryptoKey::Impl> CryptoKey::Impl::importRsa(jsg::Lock& js, |
| 889 | kj::StringPtr normalizedName, |
| 890 | kj::StringPtr format, |
| 891 | SubtleCrypto::ImportKeyData keyData, |
| 892 | SubtleCrypto::ImportKeyAlgorithm&& algorithm, |
| 893 | bool extractable, |
| 894 | kj::ArrayPtr<const kj::String> keyUsages) { |
| 895 | kj::StringPtr hash = api::getAlgorithmName( |
| 896 | JSG_REQUIRE_NONNULL(algorithm.hash, TypeError, "Missing field \"hash\" in \"algorithm\".")); |
| 897 | |
| 898 | CryptoKeyUsageSet allowedUsages = (normalizedName == "RSA-OAEP") |
| 899 | ? (CryptoKeyUsageSet::encrypt() | CryptoKeyUsageSet::decrypt() | |
| 900 | CryptoKeyUsageSet::wrapKey() | CryptoKeyUsageSet::unwrapKey()) |
| 901 | : (CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify()); |
| 902 | |
| 903 | auto [normalizedHashName, hashEvpMd] = lookupDigestAlgorithm(hash); |
| 904 | |
| 905 | auto importedKey = importAsymmetricForWebCrypto(js, kj::mv(format), kj::mv(keyData), |
| 906 | normalizedName, extractable, keyUsages, |
| 907 | // Verbose lambda capture needed because: https://bugs.llvm.org/show_bug.cgi?id=35984 |
| 908 | [hashEvpMd = hashEvpMd, &algorithm]( |
| 909 | SubtleCrypto::JsonWebKey keyDataJwk) -> kj::Own<EVP_PKEY> { |
| 910 | JSG_REQUIRE(keyDataJwk.kty == "RSA", DOMDataError, |
| 911 | "RSASSA-PKCS1-v1_5 \"jwk\" key import requires a JSON Web Key with Key Type parameter " |
| 912 | "\"kty\" (\"", |
| 913 | keyDataJwk.kty, "\") equal to \"RSA\"."); |
| 914 | |
| 915 | KJ_IF_SOME(alg, keyDataJwk.alg) { |
| 916 | // If this JWK specifies an algorithm, make sure it jives with the hash we were passed via |
| 917 | // importKey(). |
| 918 | static const std::map<kj::StringPtr, const EVP_MD*> knownRsaAlgorithms{ |
| 919 | {"RS1", EVP_sha1()}, |
| 920 | {"RS256", EVP_sha256()}, |
| 921 | {"RS384", EVP_sha384()}, |
| 922 | {"RS512", EVP_sha512()}, |
| 923 | {"PS1", EVP_sha1()}, |
| 924 | {"PS256", EVP_sha256()}, |
| 925 | {"PS384", EVP_sha384()}, |
| 926 | {"PS512", EVP_sha512()}, |
| 927 | {"RSA-OAEP", EVP_sha1()}, |
| 928 | {"RSA-OAEP-256", EVP_sha256()}, |
| 929 | {"RSA-OAEP-384", EVP_sha384()}, |
| 930 | {"RSA-OAEP-512", EVP_sha512()}, |
| 931 | }; |
| 932 | const auto tryFindAlgorithm = [&](kj::StringPtr alg) -> kj::Maybe<const EVP_MD*> { |
| 933 | if (algorithm.name == "RSASSA-PKCS1-v1_5" || algorithm.name == "RSA-PSS" || |
| 934 | algorithm.name == "RSA-OAEP") { |
| 935 | auto ret = knownRsaAlgorithms.find(alg); |
| 936 | if (ret != knownRsaAlgorithms.end()) { |
| 937 | return ret->second; |
| 938 | } |
| 939 | return kj::none; |
| 940 | } else { |
| 941 | JSG_FAIL_REQUIRE( |
| 942 | DOMNotSupportedError, "Unrecognized RSA variant \"", algorithm.name, "\"."); |
| 943 | } |
| 944 | }; |
| 945 | auto jwkHash = JSG_REQUIRE_NONNULL(tryFindAlgorithm(alg), DOMNotSupportedError, |
| 946 | "Unrecognized or unimplemented algorithm \"", alg, |
| 947 | "\" listed in JSON Web Key Algorithm " |
| 948 | "parameter."); |
| 949 | |
| 950 | JSG_REQUIRE(jwkHash == hashEvpMd, DOMDataError, |
| 951 | "JSON Web Key Algorithm parameter \"alg\" (\"", alg, |
| 952 | "\") does not match requested hash " |
| 953 | "algorithm \"", |
| 954 | alg, "\"."); |
| 955 | } |
| 956 | |
| 957 | return rsaJwkReader(kj::mv(keyDataJwk)); |
| 958 | }, |
| 959 | allowedUsages); |
| 960 | |
| 961 | // get0 avoids adding a refcount... |
| 962 | auto rsa = JSG_REQUIRE_NONNULL(Rsa::tryGetRsa(importedKey.evpPkey.get()), DOMDataError, |
| 963 | "Input was not an RSA key", tryDescribeOpensslErrors()); |
| 964 | |
| 965 | // TODO(conform): We're supposed to check if PKCS8/SPKI input specified a hash and, if so, |
| 966 | // compare it against the hash requested in `algorithm`. But, I can't find the OpenSSL |
| 967 | // interface to extract the hash from the ASN.1. Oh well... |
| 968 | |
| 969 | size_t modulusLength = rsa.getModulusBits(); |
| 970 | auto publicExponent = rsa.getPublicExponent(js); |
| 971 | |
| 972 | // Validate modulus and exponent, reject imported RSA keys that may be unsafe. |
| 973 | Rsa::validateRsaParams(js, modulusLength, publicExponent.asArrayPtr(), true); |
| 974 | |
| 975 | auto keyAlgorithm = CryptoKey::RsaKeyAlgorithm{.name = normalizedName, |
| 976 | .modulusLength = static_cast<uint16_t>(modulusLength), |
| 977 | .publicExponent = jsg::JsBufferSource(publicExponent).addRef(js), |
| 978 | .hash = KeyAlgorithm{normalizedHashName}}; |
| 979 | if (normalizedName == "RSASSA-PKCS1-v1_5") { |
| 980 | return kj::heap<RsassaPkcs1V15Key>(kj::mv(importedKey), kj::mv(keyAlgorithm), extractable); |
| 981 | } else if (normalizedName == "RSA-PSS") { |
| 982 | return kj::heap<RsaPssKey>(kj::mv(importedKey), kj::mv(keyAlgorithm), extractable); |
| 983 | } else if (normalizedName == "RSA-OAEP") { |
| 984 | return kj::heap<RsaOaepKey>(kj::mv(importedKey), kj::mv(keyAlgorithm), extractable); |
| 985 | } else { |
| 986 | JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unrecognized RSA variant \"", normalizedName, "\"."); |
| 987 | } |
| 988 | } |
| 989 | |
| 990 | kj::Own<CryptoKey::Impl> CryptoKey::Impl::importRsaRaw(jsg::Lock& js, |
| 991 | kj::StringPtr normalizedName, |
| 992 | kj::StringPtr format, |
| 993 | SubtleCrypto::ImportKeyData keyData, |
| 994 | SubtleCrypto::ImportKeyAlgorithm&& algorithm, |
| 995 | bool extractable, |
| 996 | kj::ArrayPtr<const kj::String> keyUsages) { |
| 997 | // Note that in this context raw refers to the RSA-RAW algorithm, not to keys represented by raw |
| 998 | // data. Importing raw keys is currently not supported for this algorithm. |
| 999 | CryptoKeyUsageSet allowedUsages = CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify(); |
| 1000 | auto importedKey = importAsymmetricForWebCrypto(js, kj::mv(format), kj::mv(keyData), |
| 1001 | normalizedName, extractable, keyUsages, |
| 1002 | // Verbose lambda capture needed because: https://bugs.llvm.org/show_bug.cgi?id=35984 |
| 1003 | [](SubtleCrypto::JsonWebKey keyDataJwk) -> kj::Own<EVP_PKEY> { |
| 1004 | JSG_REQUIRE(keyDataJwk.kty == "RSA", DOMDataError, |
| 1005 | "RSA-RAW \"jwk\" key import requires a JSON Web Key with Key Type parameter " |
| 1006 | "\"kty\" (\"", |
| 1007 | keyDataJwk.kty, "\") equal to \"RSA\"."); |
| 1008 | |
| 1009 | KJ_IF_SOME(alg, keyDataJwk.alg) { |
| 1010 | // If this JWK specifies an algorithm, make sure it jives with the hash we were passed via |
| 1011 | // importKey(). |
| 1012 | static const std::map<kj::StringPtr, const EVP_MD*> rsaAlgorithms{ |
| 1013 | {"RS1", EVP_sha1()}, |
| 1014 | {"RS256", EVP_sha256()}, |
| 1015 | {"RS384", EVP_sha384()}, |
| 1016 | {"RS512", EVP_sha512()}, |
| 1017 | }; |
| 1018 | auto jwkHash = rsaAlgorithms.find(alg); |
| 1019 | JSG_REQUIRE(jwkHash != rsaAlgorithms.end(), DOMNotSupportedError, |
| 1020 | "Unrecognized or unimplemented algorithm \"", alg, |
| 1021 | "\" listed in JSON Web Key Algorithm parameter."); |
| 1022 | } |
| 1023 | return rsaJwkReader(kj::mv(keyDataJwk)); |
| 1024 | }, allowedUsages); |
| 1025 | |
| 1026 | JSG_REQUIRE(importedKey.keyType == KeyType::PRIVATE, DOMDataError, |
| 1027 | "RSA-RAW only supports private keys but requested \"", toStringPtr(importedKey.keyType), |
| 1028 | "\"."); |
| 1029 | |
| 1030 | // get0 avoids adding a refcount... |
| 1031 | auto rsa = JSG_REQUIRE_NONNULL(Rsa::tryGetRsa(importedKey.evpPkey.get()), DOMDataError, |
| 1032 | "Input was not an RSA key", tryDescribeOpensslErrors()); |
| 1033 | |
| 1034 | size_t modulusLength = rsa.getModulusBits(); |
| 1035 | auto publicExponent = KJ_REQUIRE_NONNULL(bignumToArray(js, *rsa.getE())); |
| 1036 | |
| 1037 | // Validate modulus and exponent, reject imported RSA keys that may be unsafe. |
| 1038 | Rsa::validateRsaParams(js, modulusLength, publicExponent.asArrayPtr(), true); |
| 1039 | |
| 1040 | auto keyAlgorithm = CryptoKey::RsaKeyAlgorithm{.name = "RSA-RAW"_kj, |
| 1041 | .modulusLength = static_cast<uint16_t>(modulusLength), |
| 1042 | .publicExponent = jsg::JsBufferSource(publicExponent).addRef(js)}; |
| 1043 | |
| 1044 | return kj::heap<RsaRawKey>(kj::mv(importedKey), kj::mv(keyAlgorithm), extractable); |
| 1045 | } |
| 1046 | |
| 1047 | kj::Own<CryptoKey::Impl> fromRsaKey(jsg::Lock& js, kj::Own<EVP_PKEY> key) { |
| 1048 | auto rsa = |
| 1049 | JSG_REQUIRE_NONNULL(Rsa::tryGetRsa(key.get()), DOMDataError, "Input was not an RSA key"); |
| 1050 | |
| 1051 | auto publicExponent = KJ_REQUIRE_NONNULL(bignumToArray(js, *rsa.getE())); |
| 1052 | return kj::heap<RsassaPkcs1V15Key>(AsymmetricKeyData{.evpPkey = kj::mv(key), |
| 1053 | .keyType = KeyType::PUBLIC, |
| 1054 | .usages = CryptoKeyUsageSet::decrypt() | |
| 1055 | CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify()}, |
| 1056 | CryptoKey::RsaKeyAlgorithm{ |
| 1057 | .name = "RSA"_kj, |
| 1058 | .publicExponent = jsg::JsBufferSource(publicExponent).addRef(js), |
| 1059 | }, |
| 1060 | true); |
| 1061 | } |
| 1062 | |
| 1063 | } // namespace workerd::api |