File
Blob: src/workerd/api/crypto/keys.c++
| 1 | #include "keys.h" |
| 2 | |
| 3 | #include <openssl/crypto.h> |
| 4 | #include <openssl/ec_key.h> |
| 5 | #include <openssl/pem.h> |
| 6 | #include <openssl/x509.h> |
| 7 | |
| 8 | namespace workerd::api { |
| 9 | |
| 10 | namespace { |
| 11 | static const char EMPTY_PASSPHRASE[] = ""; |
| 12 | } // namespace |
| 13 | |
| 14 | kj::StringPtr toStringPtr(KeyType type) { |
| 15 | switch (type) { |
| 16 | case KeyType::SECRET: |
| 17 | return "secret"_kj; |
| 18 | case KeyType::PUBLIC: |
| 19 | return "public"_kj; |
| 20 | case KeyType::PRIVATE: |
| 21 | return "private"_kj; |
| 22 | } |
| 23 | KJ_UNREACHABLE; |
| 24 | } |
| 25 | |
| 26 | AsymmetricKeyCryptoKeyImpl::AsymmetricKeyCryptoKeyImpl(AsymmetricKeyData&& key, bool extractable) |
| 27 | : CryptoKey::Impl(extractable, key.usages), |
| 28 | keyData(kj::mv(key.evpPkey)), |
| 29 | keyType(key.keyType) { |
| 30 | KJ_DASSERT(keyType != KeyType::SECRET); |
| 31 | } |
| 32 | |
| 33 | jsg::JsArrayBuffer AsymmetricKeyCryptoKeyImpl::signatureSslToWebCrypto( |
| 34 | jsg::Lock& js, kj::ArrayPtr<kj::byte> signature) const { |
| 35 | return jsg::JsArrayBuffer::create(js, signature); |
| 36 | } |
| 37 | |
| 38 | jsg::JsArrayBuffer AsymmetricKeyCryptoKeyImpl::signatureWebCryptoToSsl( |
| 39 | jsg::Lock& js, kj::ArrayPtr<const kj::byte> signature) const { |
| 40 | return jsg::JsArrayBuffer::create(js, signature); |
| 41 | } |
| 42 | |
| 43 | SubtleCrypto::ExportKeyData AsymmetricKeyCryptoKeyImpl::exportKey( |
| 44 | jsg::Lock& js, kj::StringPtr format) const { |
| 45 | // EVP_marshal_{public,private}_key() functions are BoringSSL |
| 46 | // extensions which export asymmetric keys in DER format. |
| 47 | // DER is the binary format which *should* work to export any EVP_PKEY. |
| 48 | |
| 49 | uint8_t* der = nullptr; |
| 50 | // The caller takes ownership of the buffer and, unless the buffer was fixed with CBB_init_fixed, |
| 51 | // must call OPENSSL_free when done. |
| 52 | // https://commondatastorage.googleapis.com/chromium-boringssl-docs/bytestring.h.html#CBB_finish |
| 53 | KJ_DEFER(if (der != nullptr) { OPENSSL_free(der); }); |
| 54 | size_t derLen; |
| 55 | bssl::ScopedCBB cbb; |
| 56 | if (format == "pkcs8"_kj) { |
| 57 | JSG_REQUIRE(keyType == KeyType::PRIVATE, DOMInvalidAccessError, |
| 58 | "Asymmetric pkcs8 export requires private key (not \"", toStringPtr(keyType), "\")."); |
| 59 | if (!CBB_init(cbb.get(), 0) || !EVP_marshal_private_key(cbb.get(), keyData.get()) || |
| 60 | !CBB_finish(cbb.get(), &der, &derLen)) { |
| 61 | JSG_FAIL_REQUIRE(DOMOperationError, "Private key export failed."); |
| 62 | } |
| 63 | } else if (format == "spki"_kj) { |
| 64 | JSG_REQUIRE(keyType == KeyType::PUBLIC, DOMInvalidAccessError, |
| 65 | "Asymmetric spki export requires public key (not \"", toStringPtr(keyType), "\")."); |
| 66 | if (!CBB_init(cbb.get(), 0) || !EVP_marshal_public_key(cbb.get(), keyData.get()) || |
| 67 | !CBB_finish(cbb.get(), &der, &derLen)) { |
| 68 | JSG_FAIL_REQUIRE(DOMOperationError, "Public key export failed."); |
| 69 | } |
| 70 | } else if (format == "jwk"_kj) { |
| 71 | auto jwk = exportJwk(); |
| 72 | // Implicitly extractable since the normative part of the implementation validates that |
| 73 | // already. |
| 74 | jwk.ext = true; |
| 75 | jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); }); |
| 76 | return jwk; |
| 77 | } else if (format == "raw"_kj) { |
| 78 | return exportRaw(js).addRef(js); |
| 79 | } else { |
| 80 | JSG_FAIL_REQUIRE(DOMInvalidAccessError, "Cannot export \"", getAlgorithmName(), "\" in \"", |
| 81 | format, "\" format."); |
| 82 | } |
| 83 | |
| 84 | return jsg::JsArrayBuffer::create(js, kj::arrayPtr(der, derLen)).addRef(js); |
| 85 | } |
| 86 | |
| 87 | jsg::JsUint8Array AsymmetricKeyCryptoKeyImpl::exportKeyExt(jsg::Lock& js, |
| 88 | kj::StringPtr format, |
| 89 | kj::StringPtr type, |
| 90 | jsg::Optional<kj::String> cipher, |
| 91 | jsg::Optional<kj::Array<kj::byte>> passphrase) const { |
| 92 | KJ_REQUIRE(isExtractable(), "Key is not extractable."); |
| 93 | MarkPopErrorOnReturn mark_pop_error_on_return; |
| 94 | KJ_REQUIRE(format != "jwk", "jwk export not supported for exportKeyExt"); |
| 95 | auto pkey = getEvpPkey(); |
| 96 | auto bio = OSSL_BIO_MEM(); |
| 97 | |
| 98 | struct EncDetail { |
| 99 | // const_cast is acceptable, pass will be reassigned before it is written to. |
| 100 | char* pass = const_cast<char*>(&EMPTY_PASSPHRASE[0]); |
| 101 | size_t pass_len = 0; |
| 102 | const EVP_CIPHER* cipher = nullptr; |
| 103 | }; |
| 104 | |
| 105 | const auto getEncDetail = [&] { |
| 106 | EncDetail detail; |
| 107 | KJ_IF_SOME(pw, passphrase) { |
| 108 | detail.pass = reinterpret_cast<char*>(pw.begin()); |
| 109 | detail.pass_len = pw.size(); |
| 110 | } |
| 111 | KJ_IF_SOME(ciph, cipher) { |
| 112 | detail.cipher = EVP_get_cipherbyname(ciph.cStr()); |
| 113 | JSG_REQUIRE(detail.cipher != nullptr, TypeError, "Unknown cipher ", ciph); |
| 114 | KJ_REQUIRE(detail.pass != nullptr); |
| 115 | } |
| 116 | return detail; |
| 117 | }; |
| 118 | |
| 119 | const auto fromBio = [&](kj::StringPtr format) { |
| 120 | BUF_MEM* bptr; |
| 121 | BIO_get_mem_ptr(bio.get(), &bptr); |
| 122 | auto src = kj::arrayPtr(bptr->data, bptr->length); |
| 123 | return jsg::JsUint8Array::create(js, src.asBytes()); |
| 124 | }; |
| 125 | |
| 126 | if (getType() == "public"_kj) { |
| 127 | // Here we only care about the format and the type. |
| 128 | if (type == "pkcs1"_kj) { |
| 129 | // PKCS#1 is only for RSA keys. |
| 130 | JSG_REQUIRE(EVP_PKEY_id(pkey) == EVP_PKEY_RSA, TypeError, |
| 131 | "The pkcs1 type is only valid for RSA keys."); |
| 132 | auto rsa = EVP_PKEY_get1_RSA(pkey); |
| 133 | KJ_DEFER(RSA_free(rsa)); |
| 134 | if (format == "pem"_kj) { |
| 135 | if (PEM_write_bio_RSAPublicKey(bio.get(), rsa) == 1) { |
| 136 | return fromBio(format); |
| 137 | } |
| 138 | } else if (format == "der"_kj) { |
| 139 | if (i2d_RSAPublicKey_bio(bio.get(), rsa) == 1) { |
| 140 | return fromBio(format); |
| 141 | } |
| 142 | } |
| 143 | } else if (type == "spki"_kj) { |
| 144 | if (format == "pem"_kj) { |
| 145 | if (PEM_write_bio_PUBKEY(bio.get(), pkey) == 1) { |
| 146 | return fromBio(format); |
| 147 | } |
| 148 | } else if (format == "der"_kj) { |
| 149 | if (i2d_PUBKEY_bio(bio.get(), pkey) == 1) { |
| 150 | return fromBio(format); |
| 151 | } |
| 152 | } |
| 153 | } |
| 154 | JSG_FAIL_REQUIRE(TypeError, "Failed to encode public key"); |
| 155 | } |
| 156 | |
| 157 | // Otherwise it's a private key. |
| 158 | KJ_REQUIRE(getType() == "private"_kj); |
| 159 | |
| 160 | if (type == "pkcs1"_kj) { |
| 161 | // PKCS#1 is only for RSA keys. |
| 162 | JSG_REQUIRE( |
| 163 | EVP_PKEY_id(pkey) == EVP_PKEY_RSA, TypeError, "The pkcs1 type is only valid for RSA keys."); |
| 164 | auto rsa = EVP_PKEY_get1_RSA(pkey); |
| 165 | KJ_DEFER(RSA_free(rsa)); |
| 166 | if (format == "pem"_kj) { |
| 167 | auto enc = getEncDetail(); |
| 168 | if (PEM_write_bio_RSAPrivateKey(bio.get(), rsa, enc.cipher, |
| 169 | reinterpret_cast<unsigned char*>(enc.pass), enc.pass_len, nullptr, nullptr) == 1) { |
| 170 | return fromBio(format); |
| 171 | } |
| 172 | } else if (format == "der"_kj) { |
| 173 | // The cipher and passphrase are ignored for DER with PKCS#1. |
| 174 | if (i2d_RSAPrivateKey_bio(bio.get(), rsa) == 1) { |
| 175 | return fromBio(format); |
| 176 | } |
| 177 | } |
| 178 | } else if (type == "pkcs8"_kj) { |
| 179 | auto enc = getEncDetail(); |
| 180 | if (format == "pem"_kj) { |
| 181 | if (PEM_write_bio_PKCS8PrivateKey( |
| 182 | bio.get(), pkey, enc.cipher, enc.pass, enc.pass_len, nullptr, nullptr) == 1) { |
| 183 | return fromBio(format); |
| 184 | } |
| 185 | } else if (format == "der"_kj) { |
| 186 | if (i2d_PKCS8PrivateKey_bio( |
| 187 | bio.get(), pkey, enc.cipher, enc.pass, enc.pass_len, nullptr, nullptr) == 1) { |
| 188 | return fromBio(format); |
| 189 | } |
| 190 | } |
| 191 | } else if (type == "sec1"_kj) { |
| 192 | // SEC1 is only used for EC keys. |
| 193 | JSG_REQUIRE( |
| 194 | EVP_PKEY_id(pkey) == EVP_PKEY_EC, TypeError, "The sec1 type is only valid for EC keys."); |
| 195 | auto ec = EVP_PKEY_get1_EC_KEY(pkey); |
| 196 | KJ_DEFER(EC_KEY_free(ec)); |
| 197 | if (format == "pem"_kj) { |
| 198 | auto enc = getEncDetail(); |
| 199 | if (PEM_write_bio_ECPrivateKey(bio.get(), ec, enc.cipher, |
| 200 | reinterpret_cast<unsigned char*>(enc.pass), enc.pass_len, nullptr, nullptr) == 1) { |
| 201 | return fromBio(format); |
| 202 | } |
| 203 | } else if (format == "der"_kj) { |
| 204 | // The cipher and passphrase are ignored for DER with SEC1 |
| 205 | if (i2d_ECPrivateKey_bio(bio.get(), ec) == 1) { |
| 206 | return fromBio(format); |
| 207 | } |
| 208 | } |
| 209 | } |
| 210 | |
| 211 | JSG_FAIL_REQUIRE(TypeError, "Failed to encode private key"); |
| 212 | } |
| 213 | |
| 214 | jsg::JsArrayBuffer AsymmetricKeyCryptoKeyImpl::sign(jsg::Lock& js, |
| 215 | SubtleCrypto::SignAlgorithm&& algorithm, |
| 216 | kj::ArrayPtr<const kj::byte> data) const { |
| 217 | JSG_REQUIRE(keyType == KeyType::PRIVATE, DOMInvalidAccessError, |
| 218 | "Asymmetric signing requires a private key."); |
| 219 | |
| 220 | auto type = lookupDigestAlgorithm(chooseHash(algorithm.hash)).second; |
| 221 | if (getAlgorithmName() == "RSASSA-PKCS1-v1_5") { |
| 222 | // RSASSA-PKCS1-v1_5 requires the RSA key to be at least as big as the digest size |
| 223 | // plus a 15 to 19 byte digest-specific prefix (see BoringSSL's RSA_add_pkcs1_prefix) plus 11 |
| 224 | // bytes for padding (see RSA_PKCS1_PADDING_SIZE). For simplicity, require the key to be at |
| 225 | // least 32 bytes larger than the hash digest. |
| 226 | // Similar checks could also be adopted for more detailed error handling in verify(), but the |
| 227 | // current approach should be sufficient to avoid internal errors. |
| 228 | RSA& rsa = JSG_REQUIRE_NONNULL(EVP_PKEY_get0_RSA(getEvpPkey()), DOMDataError, "Missing RSA key", |
| 229 | tryDescribeOpensslErrors()); |
| 230 | |
| 231 | JSG_REQUIRE(EVP_MD_size(type) + 32 <= RSA_size(&rsa), DOMOperationError, |
| 232 | "key too small for signing with given digest, need at least ", 8 * (EVP_MD_size(type) + 32), |
| 233 | "bits."); |
| 234 | } else if (getAlgorithmName() == "RSA-PSS") { |
| 235 | // Similarly, RSA-PSS requires keys to be at least the size of the digest and salt plus 2 |
| 236 | // bytes, see https://developer.mozilla.org/en-US/docs/Web/API/RsaPssParams for details. |
| 237 | RSA& rsa = JSG_REQUIRE_NONNULL(EVP_PKEY_get0_RSA(getEvpPkey()), DOMDataError, "Missing RSA key", |
| 238 | tryDescribeOpensslErrors()); |
| 239 | auto salt = JSG_REQUIRE_NONNULL(algorithm.saltLength, DOMDataError, |
| 240 | "Failed to provide salt for RSA-PSS key operation which requires a salt"); |
| 241 | JSG_REQUIRE(salt >= 0, DOMDataError, "SaltLength for RSA-PSS must be non-negative ", |
| 242 | "(provided ", salt, ")."); |
| 243 | JSG_REQUIRE(EVP_MD_size(type) + 2 <= RSA_size(&rsa), DOMOperationError, |
| 244 | "key too small for signing with given digest"); |
| 245 | JSG_REQUIRE(salt <= RSA_size(&rsa) - EVP_MD_size(type) - 2, DOMOperationError, |
| 246 | "key too small for signing with given digest and salt length"); |
| 247 | } |
| 248 | |
| 249 | auto digestCtx = OSSL_NEW(EVP_MD_CTX); |
| 250 | |
| 251 | OSSLCALL(EVP_DigestSignInit(digestCtx.get(), nullptr, type, nullptr, keyData.get())); |
| 252 | addSalt(digestCtx->pctx, algorithm); |
| 253 | // No-op call unless CryptoKey is RsaPss |
| 254 | OSSLCALL(EVP_DigestSignUpdate(digestCtx.get(), data.begin(), data.size())); |
| 255 | size_t signatureSize = 0; |
| 256 | OSSLCALL(EVP_DigestSignFinal(digestCtx.get(), nullptr, &signatureSize)); |
| 257 | |
| 258 | KJ_STACK_ARRAY(kj::byte, signature, signatureSize, 256, 256); |
| 259 | OSSLCALL(EVP_DigestSignFinal(digestCtx.get(), signature.begin(), &signatureSize)); |
| 260 | |
| 261 | KJ_ASSERT(signatureSize <= signature.size()); |
| 262 | if (signatureSize < signature.size()) { |
| 263 | return signatureSslToWebCrypto(js, signature.first(signatureSize)); |
| 264 | } |
| 265 | |
| 266 | return signatureSslToWebCrypto(js, signature); |
| 267 | } |
| 268 | |
| 269 | bool AsymmetricKeyCryptoKeyImpl::verify(jsg::Lock& js, |
| 270 | SubtleCrypto::SignAlgorithm&& algorithm, |
| 271 | kj::ArrayPtr<const kj::byte> signature, |
| 272 | kj::ArrayPtr<const kj::byte> data) const { |
| 273 | ClearErrorOnReturn clearErrorOnReturn; |
| 274 | |
| 275 | JSG_REQUIRE(keyType == KeyType::PUBLIC, DOMInvalidAccessError, |
| 276 | "Asymmetric verification requires a public key."); |
| 277 | |
| 278 | auto sslSignature = signatureWebCryptoToSsl(js, signature); |
| 279 | |
| 280 | auto type = lookupDigestAlgorithm(chooseHash(algorithm.hash)).second; |
| 281 | |
| 282 | auto digestCtx = OSSL_NEW(EVP_MD_CTX); |
| 283 | |
| 284 | OSSLCALL(EVP_DigestVerifyInit(digestCtx.get(), nullptr, type, nullptr, keyData.get())); |
| 285 | addSalt(digestCtx->pctx, algorithm); |
| 286 | // No-op call unless CryptoKey is RsaPss |
| 287 | OSSLCALL(EVP_DigestVerifyUpdate(digestCtx.get(), data.begin(), data.size())); |
| 288 | // EVP_DigestVerifyFinal() returns 1 on success, 0 on invalid signature, and any other value |
| 289 | // indicates "a more serious error". |
| 290 | auto result = EVP_DigestVerifyFinal( |
| 291 | digestCtx.get(), sslSignature.asArrayPtr().begin(), sslSignature.size()); |
| 292 | JSG_REQUIRE(result == 0 || result == 1, InternalDOMOperationError, |
| 293 | "Unexpected return code from digest verify", getAlgorithmName()); |
| 294 | return !!result; |
| 295 | } |
| 296 | |
| 297 | bool AsymmetricKeyCryptoKeyImpl::equals(const CryptoKey::Impl& other) const { |
| 298 | if (this == &other) return true; |
| 299 | KJ_IF_SOME(otherImpl, kj::dynamicDowncastIfAvailable<const AsymmetricKeyCryptoKeyImpl>(other)) { |
| 300 | // EVP_PKEY_cmp will return 1 if the inputs match, 0 if they don't match, |
| 301 | // -1 if the key types are different, and -2 if the operation is not supported. |
| 302 | // We only really care about the first two cases. |
| 303 | return EVP_PKEY_cmp(keyData.get(), otherImpl.keyData.get()) == 1; |
| 304 | } |
| 305 | return false; |
| 306 | } |
| 307 | |
| 308 | kj::StringPtr AsymmetricKeyCryptoKeyImpl::getType() const { |
| 309 | return toStringPtr(keyType); |
| 310 | } |
| 311 | |
| 312 | bool AsymmetricKeyCryptoKeyImpl::verifyX509Public(const X509* cert) const { |
| 313 | ClearErrorOnReturn clearErrorOnReturn; |
| 314 | return X509_verify(const_cast<X509*>(cert), getEvpPkey()) > 0; |
| 315 | } |
| 316 | |
| 317 | bool AsymmetricKeyCryptoKeyImpl::verifyX509Private(const X509* cert) const { |
| 318 | ClearErrorOnReturn clearErrorOnReturn; |
| 319 | return X509_check_private_key(const_cast<X509*>(cert), getEvpPkey()) == 1; |
| 320 | } |
| 321 | |
| 322 | // ====================================================================================== |
| 323 | |
| 324 | AsymmetricKeyData importAsymmetricForWebCrypto(jsg::Lock& js, |
| 325 | kj::StringPtr format, |
| 326 | SubtleCrypto::ImportKeyData keyData, |
| 327 | kj::StringPtr normalizedName, |
| 328 | bool extractable, |
| 329 | kj::ArrayPtr<const kj::String> keyUsages, |
| 330 | kj::FunctionParam<kj::Own<EVP_PKEY>(SubtleCrypto::JsonWebKey)> readJwk, |
| 331 | CryptoKeyUsageSet allowedUsages) { |
| 332 | CryptoKeyUsageSet usages; |
| 333 | if (format == "jwk") { |
| 334 | // I found jww's SO answer immeasurably helpful while writing this: |
| 335 | // https://stackoverflow.com/questions/24093272/how-to-load-a-private-key-from-a-jwk-into-openssl |
| 336 | |
| 337 | auto& keyDataJwk = JSG_REQUIRE_NONNULL(keyData.tryGet<SubtleCrypto::JsonWebKey>(), DOMDataError, |
| 338 | "JSON Web Key import requires a JSON Web Key object."); |
| 339 | |
| 340 | KeyType keyType = KeyType::PRIVATE; |
| 341 | if (keyDataJwk.d != kj::none) { |
| 342 | // Private key (`d` is the private exponent, per RFC 7518). |
| 343 | keyType = KeyType::PRIVATE; |
| 344 | usages = |
| 345 | CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::importPrivate, |
| 346 | keyUsages, allowedUsages & CryptoKeyUsageSet::privateKeyMask()); |
| 347 | |
| 348 | // https://tools.ietf.org/html/rfc7518#section-6.3.2.7 |
| 349 | // We don't support keys with > 2 primes, so error out. |
| 350 | JSG_REQUIRE(keyDataJwk.oth == kj::none, DOMNotSupportedError, |
| 351 | "Multi-prime private keys not supported."); |
| 352 | } else { |
| 353 | // Public key. |
| 354 | keyType = KeyType::PUBLIC; |
| 355 | auto strictCrypto = FeatureFlags::get(js).getStrictCrypto(); |
| 356 | // restrict key usages to public key usages. In the case of ECDH, usages must be empty, but |
| 357 | // if the strict crypto compat flag is not enabled allow the same usages as with private ECDH |
| 358 | // keys, i.e. derivationKeyMask(). |
| 359 | usages = CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::importPublic, |
| 360 | keyUsages, |
| 361 | allowedUsages & |
| 362 | (normalizedName == "ECDH" |
| 363 | ? strictCrypto ? CryptoKeyUsageSet() : CryptoKeyUsageSet::derivationKeyMask() |
| 364 | : CryptoKeyUsageSet::publicKeyMask())); |
| 365 | } |
| 366 | |
| 367 | auto [expectedUse, op0, op1] = [&, normalizedName] { |
| 368 | if (normalizedName == "RSA-OAEP") { |
| 369 | return std::make_tuple("enc", "encrypt", "wrapKey"); |
| 370 | } |
| 371 | if (normalizedName == "ECDH" || normalizedName == "X25519") { |
| 372 | return std::make_tuple("enc", "unused", "unused"); |
| 373 | } |
| 374 | return std::make_tuple("sig", "sign", "verify"); |
| 375 | }(); |
| 376 | |
| 377 | if (keyUsages.size() > 0) { |
| 378 | KJ_IF_SOME(use, keyDataJwk.use) { |
| 379 | JSG_REQUIRE(use == expectedUse, DOMDataError, |
| 380 | "Asymmetric \"jwk\" key import with usages requires a JSON Web Key with " |
| 381 | "Public Key Use parameter \"use\" (\"", |
| 382 | use, "\") equal to \"sig\"."); |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | KJ_IF_SOME(ops, keyDataJwk.key_ops) { |
| 387 | // TODO(cleanup): When we implement other JWK import functions, factor this part out into a |
| 388 | // JWK validation function. |
| 389 | |
| 390 | // "The key operation values are case-sensitive strings. Duplicate key operation values MUST |
| 391 | // NOT be present in the array." -- RFC 7517, section 4.3 |
| 392 | std::sort(ops.begin(), ops.end()); |
| 393 | JSG_REQUIRE(std::adjacent_find(ops.begin(), ops.end()) == ops.end(), DOMDataError, |
| 394 | "A JSON Web Key's Key Operations parameter (\"key_ops\") " |
| 395 | "must not contain duplicates."); |
| 396 | |
| 397 | KJ_IF_SOME(use, keyDataJwk.use) { |
| 398 | // "The "use" and "key_ops" JWK members SHOULD NOT be used together; however, if both are |
| 399 | // used, the information they convey MUST be consistent." -- RFC 7517, section 4.3. |
| 400 | |
| 401 | JSG_REQUIRE(use == expectedUse, DOMDataError, |
| 402 | "Asymmetric \"jwk\" import requires a JSON " |
| 403 | "Web Key with Public Key Use \"use\" (\"", |
| 404 | use, "\") equal to \"", expectedUse, "\"."); |
| 405 | |
| 406 | for (const auto& op: ops) { |
| 407 | JSG_REQUIRE(normalizedName != "ECDH" && normalizedName != "X25519", DOMDataError, |
| 408 | "A JSON Web Key should have either a Public Key Use parameter (\"use\") or a Key " |
| 409 | "Operations parameter (\"key_ops\"); otherwise, the parameters must be consistent " |
| 410 | "with each other. For public ", |
| 411 | normalizedName, |
| 412 | " keys, there are no valid usages," |
| 413 | "so keys with a non-empty \"key_ops\" parameter are not allowed."); |
| 414 | |
| 415 | // TODO(conform): Can a JWK private key actually be used to verify? Not |
| 416 | // using the Web Crypto API... |
| 417 | JSG_REQUIRE(op == op0 || op == op1, DOMDataError, |
| 418 | "A JSON Web Key should have either a Public Key Use parameter (\"use\") or a Key " |
| 419 | "Operations parameter (\"key_ops\"); otherwise, the parameters must be consistent " |
| 420 | "with each other. A Public Key Use for ", |
| 421 | normalizedName, |
| 422 | " would allow a Key " |
| 423 | "Operations array with only \"", |
| 424 | op0, "\" and/or \"", op1, "\" values (not \"", op, "\")."); |
| 425 | } |
| 426 | } |
| 427 | |
| 428 | // We're supposed to verify that `ops` contains all the values listed in `keyUsages`. For any |
| 429 | // of the supported algorithms, a key may have at most two distinct usages ('sig' type keys |
| 430 | // have at most one valid usage, but there may be two for e.g. ECDH). Test the first usage |
| 431 | // and the next usages. Test the first usage and the first usage distinct from the first, if |
| 432 | // present (i.e. the second allowed usage, even if there are duplicates). |
| 433 | if (keyUsages.size() > 0) { |
| 434 | JSG_REQUIRE(std::find(ops.begin(), ops.end(), keyUsages.front()) != ops.end(), DOMDataError, |
| 435 | "All specified key usages must be present in the JSON " |
| 436 | "Web Key's Key Operations parameter (\"key_ops\")."); |
| 437 | auto secondUsage = std::find_end(keyUsages.begin(), keyUsages.end(), keyUsages.begin(), |
| 438 | keyUsages.begin() + 1) + |
| 439 | 1; |
| 440 | if (secondUsage != keyUsages.end()) { |
| 441 | JSG_REQUIRE(std::find(ops.begin(), ops.end(), *secondUsage) != ops.end(), DOMDataError, |
| 442 | "All specified key usages must be present in the JSON " |
| 443 | "Web Key's Key Operations parameter (\"key_ops\")."); |
| 444 | } |
| 445 | } |
| 446 | } |
| 447 | |
| 448 | KJ_IF_SOME(ext, keyDataJwk.ext) { |
| 449 | // If the user requested this key to be extractable, make sure the JWK does not disallow it. |
| 450 | JSG_REQUIRE(!extractable || ext, DOMDataError, |
| 451 | "Cannot create an extractable CryptoKey from an unextractable JSON Web Key."); |
| 452 | } |
| 453 | |
| 454 | return {readJwk(kj::mv(keyDataJwk)), keyType, usages}; |
| 455 | } else if (format == "spki") { |
| 456 | kj::ArrayPtr<const kj::byte> keyBytes = |
| 457 | JSG_REQUIRE_NONNULL(keyData.tryGet<kj::Array<kj::byte>>(), DOMDataError, |
| 458 | "SPKI import requires an ArrayBuffer."); |
| 459 | const kj::byte* ptr = keyBytes.begin(); |
| 460 | auto evpPkey = OSSLCALL_OWN( |
| 461 | EVP_PKEY, d2i_PUBKEY(nullptr, &ptr, keyBytes.size()), DOMDataError, "Invalid SPKI input."); |
| 462 | if (ptr != keyBytes.end()) { |
| 463 | JSG_FAIL_REQUIRE( |
| 464 | DOMDataError, "Invalid ", keyBytes.end() - ptr, " trailing bytes after SPKI input."); |
| 465 | } |
| 466 | |
| 467 | // usages must be empty for ECDH public keys, so use CryptoKeyUsageSet() when validating the |
| 468 | // usage set. |
| 469 | usages = CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::importPublic, |
| 470 | keyUsages, |
| 471 | allowedUsages & |
| 472 | (normalizedName == "ECDH" ? CryptoKeyUsageSet() : CryptoKeyUsageSet::publicKeyMask())); |
| 473 | return {kj::mv(evpPkey), KeyType::PUBLIC, usages}; |
| 474 | } else if (format == "pkcs8") { |
| 475 | kj::ArrayPtr<const kj::byte> keyBytes = |
| 476 | JSG_REQUIRE_NONNULL(keyData.tryGet<kj::Array<kj::byte>>(), DOMDataError, |
| 477 | "PKCS8 import requires an ArrayBuffer."); |
| 478 | const kj::byte* ptr = keyBytes.begin(); |
| 479 | auto evpPkey = OSSLCALL_OWN(EVP_PKEY, d2i_AutoPrivateKey(nullptr, &ptr, keyBytes.size()), |
| 480 | DOMDataError, "Invalid PKCS8 input."); |
| 481 | if (ptr != keyBytes.end()) { |
| 482 | JSG_FAIL_REQUIRE( |
| 483 | DOMDataError, "Invalid ", keyBytes.end() - ptr, " trailing bytes after PKCS8 input."); |
| 484 | } |
| 485 | usages = CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::importPrivate, |
| 486 | keyUsages, allowedUsages & CryptoKeyUsageSet::privateKeyMask()); |
| 487 | return {kj::mv(evpPkey), KeyType::PRIVATE, usages}; |
| 488 | } else { |
| 489 | JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unrecognized key import format \"", format, "\"."); |
| 490 | } |
| 491 | } |
| 492 | |
| 493 | } // namespace workerd::api |