diff --git a/build/wpt_test.bzl b/build/wpt_test.bzl index d19723e6392..8a9520c6b07 100644 --- a/build/wpt_test.bzl +++ b/build/wpt_test.bzl @@ -22,7 +22,7 @@ PORT_BINDINGS = [ ### (Invokes wpt_js_test_gen, wpt_wd_test_gen and wd_test to assemble a complete test suite.) ### ----------------------------------------------------------------------------------------- -def wpt_test(name, wpt_directory, config, compat_date = "", compat_flags = [], autogates = [], start_server = False, **kwargs): +def wpt_test(name, wpt_directory, config, compat_date = "", compat_flags = [], autogates = [], start_server = False, include_tentative = False, **kwargs): """ Main entry point. @@ -51,6 +51,7 @@ def wpt_test(name, wpt_directory, config, compat_date = "", compat_flags = [], a wpt_directory = wpt_directory, test_config = test_config_as_js, wpt_tsproject = wpt_tsproject, + include_tentative = include_tentative, ) wd_test_gen_rule = "{}@_wpt_wd_test_gen".format(name) @@ -153,7 +154,7 @@ def _wpt_js_test_gen_impl(ctx): base = ctx.attr.wpt_directory[WPTModuleInfo].base files = ctx.attr.wpt_directory.files.to_list() - test_files = [file for file in files if is_test_file(file)] + test_files = [file for file in files if is_test_file(file, ctx.attr.include_tentative)] ctx.actions.write( output = src, @@ -180,6 +181,8 @@ _wpt_js_test_gen = rule( "test_config": attr.label(allow_single_file = True), # Dependency: The ts_project rule that compiles the tests to JS "wpt_tsproject": attr.label(), + # Whether to include tentative test files + "include_tentative": attr.bool(default = False), }, ) @@ -196,7 +199,7 @@ const {{ run, printResults }} = createRunner(config, '{test_name}', allTestFiles export const zzz_results = printResults(); """ -def is_test_file(file): +def is_test_file(file, include_tentative = False): if not file.path.endswith(".js"): # Not JS code, not a test return False @@ -207,7 +210,7 @@ def is_test_file(file): # into the main directory, and would need to manually be marked as skipAllTests return False - if ".tentative." in file.path or "/tentative/" in file.path: + if not include_tentative and (".tentative." in file.path or "/tentative/" in file.path): # Tentative tests are for proposed features that are not yet standardized. # We skip these to avoid noise from unstable specifications. return False diff --git a/src/workerd/api/crypto/aes.c++ b/src/workerd/api/crypto/aes.c++ index 8a059d9e1a7..2d48332bcb1 100644 --- a/src/workerd/api/crypto/aes.c++ +++ b/src/workerd/api/crypto/aes.c++ @@ -47,22 +47,6 @@ auto lookupAesGcmType(uint bitLength) { } } -// Ensure the tagLength passed to the AES-GCM algorithm is one of the allowed bit lengths. -void validateAesGcmTagLength(int tagLength) { - switch (tagLength) { - case 32: - case 64: - case 96: - case 104: - case 112: - case 120: - case 128: - break; - default: - JSG_FAIL_REQUIRE(DOMOperationError, "Invalid AES-GCM tag length ", tagLength, "."); - } -} - int decryptFinalHelper(kj::StringPtr algorithm, size_t inputLength, size_t outputLength, @@ -151,8 +135,9 @@ class AesKeyBase: public CryptoKey::Impl { } SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override final { - JSG_REQUIRE(format == "raw" || format == "jwk", DOMNotSupportedError, getAlgorithmName(), - " key only supports exporting \"raw\" & \"jwk\", not \"", format, "\"."); + JSG_REQUIRE(format == "raw" || format == "raw-secret" || format == "jwk", DOMNotSupportedError, + getAlgorithmName(), + " key only supports exporting \"raw\", \"raw-secret\" & \"jwk\", not \"", format, "\"."); if (format == "jwk") { auto lengthInBytes = keyData.size(); @@ -786,7 +771,7 @@ kj::Own CryptoKey::Impl::importAes(jsg::Lock& js, kj::Array keyDataArray; - if (format == "raw") { + if (format == "raw" || format == "raw-secret") { // NOTE: Checked in SubtleCrypto::importKey(). auto& source = keyData.get>(); auto handle = source.getHandle(js); diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index 7495a5c65dd..99df74f5e02 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -5,6 +5,7 @@ #include "crypto.h" #include "impl.h" +#include "rsa.h" #include #include @@ -20,6 +21,7 @@ #include #include +#include #include #include #include @@ -44,6 +46,10 @@ kj::StringPtr CryptoKeyUsageSet::name() const { if (*this == deriveBits()) return "deriveBits"; if (*this == wrapKey()) return "wrapKey"; if (*this == unwrapKey()) return "unwrapKey"; + if (*this == encapsulateKey()) return "encapsulateKey"; + if (*this == encapsulateBits()) return "encapsulateBits"; + if (*this == decapsulateKey()) return "decapsulateKey"; + if (*this == decapsulateBits()) return "decapsulateBits"; KJ_FAIL_REQUIRE("CryptoKeyUsageSet does not contain exactly one key usage"); } @@ -55,8 +61,9 @@ CryptoKeyUsageSet CryptoKeyUsageSet::byName(kj::StringPtr name) { } kj::ArrayPtr CryptoKeyUsageSet::singletons() { - static const workerd::api::CryptoKeyUsageSet singletons[] = { - encrypt(), decrypt(), sign(), verify(), deriveKey(), deriveBits(), wrapKey(), unwrapKey()}; + static const workerd::api::CryptoKeyUsageSet singletons[] = {encrypt(), decrypt(), sign(), + verify(), deriveKey(), deriveBits(), wrapKey(), unwrapKey(), encapsulateKey(), + encapsulateBits(), decapsulateKey(), decapsulateBits()}; return singletons; } @@ -131,6 +138,11 @@ static kj::Maybe lookupAlgorithm(kj::StringPtr name) { {"Ed25519"_kj, &CryptoKey::Impl::importEddsa, &CryptoKey::Impl::generateEddsa}, {"X25519"_kj, &CryptoKey::Impl::importEddsa, &CryptoKey::Impl::generateEddsa}, {"RSA-RAW"_kj, &CryptoKey::Impl::importRsaRaw}, + {"ML-DSA-44"_kj, &CryptoKey::Impl::importMlDsa, &CryptoKey::Impl::generateMlDsa}, + {"ML-DSA-65"_kj, &CryptoKey::Impl::importMlDsa, &CryptoKey::Impl::generateMlDsa}, + {"ML-DSA-87"_kj, &CryptoKey::Impl::importMlDsa, &CryptoKey::Impl::generateMlDsa}, + {"ML-KEM-768"_kj, &CryptoKey::Impl::importMlKem, &CryptoKey::Impl::generateMlKem}, + {"ML-KEM-1024"_kj, &CryptoKey::Impl::importMlKem, &CryptoKey::Impl::generateMlKem}, }; auto iter = ALGORITHMS.find(CryptoAlgorithm{name}); @@ -139,6 +151,10 @@ static kj::Maybe lookupAlgorithm(kj::StringPtr name) { // algorithm registered. return Worker::Api::current().getCryptoAlgorithm(name); } else { + if ((iter->name.startsWith("ML-DSA-"_kj) || iter->name.startsWith("ML-KEM-"_kj)) && + !Worker::Api::current().getFeatureFlags().getWebCryptoModernAlgorithms()) { + return kj::none; + } return *iter; } } @@ -226,6 +242,292 @@ kj::Maybe getKeyLength(const SubtleCrypto::ImportKeyAlgorithm& derived } } +enum class SubtleOperation { + ENCRYPT, + DECRYPT, + SIGN, + VERIFY, + DIGEST, + GENERATE_KEY, + DERIVE_KEY, + DERIVE_BITS, + IMPORT_KEY, + EXPORT_KEY, + WRAP_KEY, + UNWRAP_KEY, + ENCAPSULATE_KEY, + ENCAPSULATE_BITS, + DECAPSULATE_KEY, + DECAPSULATE_BITS, + GET_PUBLIC_KEY, +}; + +kj::Maybe parseSubtleOperation(kj::StringPtr operation) { + struct OperationMapping { + kj::StringPtr name; + SubtleOperation operation; + }; + + static constexpr OperationMapping OPERATIONS[] = { + {"encrypt"_kj, SubtleOperation::ENCRYPT}, + {"decrypt"_kj, SubtleOperation::DECRYPT}, + {"sign"_kj, SubtleOperation::SIGN}, + {"verify"_kj, SubtleOperation::VERIFY}, + {"digest"_kj, SubtleOperation::DIGEST}, + {"generateKey"_kj, SubtleOperation::GENERATE_KEY}, + {"deriveKey"_kj, SubtleOperation::DERIVE_KEY}, + {"deriveBits"_kj, SubtleOperation::DERIVE_BITS}, + {"importKey"_kj, SubtleOperation::IMPORT_KEY}, + {"exportKey"_kj, SubtleOperation::EXPORT_KEY}, + {"wrapKey"_kj, SubtleOperation::WRAP_KEY}, + {"unwrapKey"_kj, SubtleOperation::UNWRAP_KEY}, + {"encapsulateKey"_kj, SubtleOperation::ENCAPSULATE_KEY}, + {"encapsulateBits"_kj, SubtleOperation::ENCAPSULATE_BITS}, + {"decapsulateKey"_kj, SubtleOperation::DECAPSULATE_KEY}, + {"decapsulateBits"_kj, SubtleOperation::DECAPSULATE_BITS}, + {"getPublicKey"_kj, SubtleOperation::GET_PUBLIC_KEY}, + }; + + for (const auto& item: OPERATIONS) { + if (operation == item.name) return item.operation; + } + return kj::none; +} + +template +Algorithm normalizeSupportAlgorithm(jsg::Lock& js, + v8::Local value, + const jsg::TypeHandler>& handler) { + KJ_IF_SOME(algorithm, handler.tryUnwrap(js, value)) { + return interpretAlgorithmParam(kj::mv(algorithm)); + } + + JSG_FAIL_REQUIRE(TypeError, "AlgorithmIdentifier could not be converted."); +} + +bool isOneOf(kj::StringPtr value, std::initializer_list names) { + return std::find(names.begin(), names.end(), value) != names.end(); +} + +void validateAesKeyLength(int length) { + JSG_REQUIRE(length == 128 || length == 192 || length == 256, DOMOperationError, + "AES key length must be 128, 192, or 256 bits."); +} + +void validateNamedCurve(kj::StringPtr namedCurve) { + JSG_REQUIRE(namedCurve == "P-256" || namedCurve == "P-384" || namedCurve == "P-521", + DOMNotSupportedError, "Unsupported namedCurve \"", namedCurve, "\"."); +} + +void validateHashAlgorithm( + const kj::Maybe>& hash, + kj::StringPtr fieldName = "algorithm") { + lookupDigestAlgorithm(api::getAlgorithmName( + JSG_REQUIRE_NONNULL(hash, TypeError, "Missing field \"hash\" in \"", fieldName, "\"."))); +} + +uint32_t getEcdhMaxDeriveBits(jsg::Lock& js, const CryptoKey& publicKey) { + auto algorithm = publicKey.getAlgorithmName(); + if (algorithm == "X25519") { + return 256; + } + + auto keyAlgorithm = publicKey.getAlgorithm(js).get(); + auto namedCurve = keyAlgorithm.namedCurve; + if (namedCurve == "P-256") return 256; + if (namedCurve == "P-384") return 384; + KJ_ASSERT(namedCurve == "P-521", namedCurve); + + // WebCrypto compares `length` to the bit length of the derived secret byte sequence: + // https://www.w3.org/TR/WebCryptoAPI/#ecdh-operations + // P-521's field element is encoded in ceil(521 / 8) = 66 bytes, so the returned secret has + // 528 bits available for truncation. + return 528; +} + +void validateGenerateKeyAlgorithm(jsg::Lock& js, + kj::StringPtr normalizedName, + const SubtleCrypto::GenerateKeyAlgorithm& algorithm) { + if (normalizedName.startsWith("AES-")) { + validateAesKeyLength(JSG_REQUIRE_NONNULL( + algorithm.length, TypeError, "Missing field \"length\" in \"algorithm\".")); + } else if (normalizedName == "HMAC") { + validateHashAlgorithm(algorithm.hash); + KJ_IF_SOME(length, algorithm.length) { + JSG_REQUIRE(length > 0, DOMOperationError, + "HMAC key length must be a non-zero unsigned long integer."); + } + } else if (normalizedName == "RSASSA-PKCS1-v1_5" || normalizedName == "RSA-PSS" || + normalizedName == "RSA-OAEP") { + auto publicExponent = JSG_REQUIRE_NONNULL( + algorithm.publicExponent, TypeError, "Missing field \"publicExponent\" in \"algorithm\".") + .getHandle(js); + validateHashAlgorithm(algorithm.hash); + auto modulusLength = JSG_REQUIRE_NONNULL( + algorithm.modulusLength, TypeError, "Missing field \"modulusLength\" in \"algorithm\"."); + JSG_REQUIRE(modulusLength > 0, DOMOperationError, + "modulusLength must be greater than zero (requested ", modulusLength, ")."); + Rsa::validateRsaParams(js, modulusLength, publicExponent.asArrayPtr()); + JSG_REQUIRE(!(FeatureFlags::get(js).getStrictCrypto() && (modulusLength & 127)), + DOMOperationError, "Can't generate key: RSA key size is required to be a multiple of 128"); + } else if (normalizedName == "ECDSA" || normalizedName == "ECDH") { + validateNamedCurve(JSG_REQUIRE_NONNULL( + algorithm.namedCurve, TypeError, "Missing field \"namedCurve\" in \"algorithm\".")); + } else if (normalizedName == "NODE-ED25519") { + const auto& namedCurve = JSG_REQUIRE_NONNULL( + algorithm.namedCurve, TypeError, "Missing field \"namedCurve\" in \"algorithm\"."); + JSG_REQUIRE(namedCurve == "NODE-ED25519", DOMNotSupportedError, "EDDSA curve \"", namedCurve, + "\" isn't supported."); + } +} + +void validateImportKeyAlgorithm( + kj::StringPtr normalizedName, const SubtleCrypto::ImportKeyAlgorithm& algorithm) { + if (normalizedName == "HMAC" || normalizedName == "RSASSA-PKCS1-v1_5" || + normalizedName == "RSA-PSS" || normalizedName == "RSA-OAEP") { + validateHashAlgorithm(algorithm.hash); + if (normalizedName == "HMAC") { + KJ_IF_SOME(length, algorithm.length) { + JSG_REQUIRE(length > 0, DOMDataError, "Imported HMAC key length (", length, + ") must be a non-zero value."); + } + } + } else if (normalizedName == "ECDSA" || normalizedName == "ECDH") { + validateNamedCurve(JSG_REQUIRE_NONNULL( + algorithm.namedCurve, TypeError, "Missing field \"namedCurve\" in \"algorithm\".")); + } else if (normalizedName == "NODE-ED25519") { + const auto& namedCurve = JSG_REQUIRE_NONNULL( + algorithm.namedCurve, TypeError, "Missing field \"namedCurve\" in \"algorithm\"."); + JSG_REQUIRE(namedCurve == "NODE-ED25519", DOMNotSupportedError, "EDDSA curve \"", namedCurve, + "\" isn't supported."); + } +} + +bool supportsEncryptDecrypt(kj::StringPtr normalizedName) { + return isOneOf(normalizedName, {"AES-CTR"_kj, "AES-CBC"_kj, "AES-GCM"_kj, "RSA-OAEP"_kj}); +} + +bool supportsSign(kj::StringPtr normalizedName) { + return isOneOf(normalizedName, + {"RSASSA-PKCS1-v1_5"_kj, "RSA-PSS"_kj, "ECDSA"_kj, "NODE-ED25519"_kj, "Ed25519"_kj, "HMAC"_kj, + "RSA-RAW"_kj, "ML-DSA-44"_kj, "ML-DSA-65"_kj, "ML-DSA-87"_kj}); +} + +bool supportsVerify(kj::StringPtr normalizedName) { + return isOneOf(normalizedName, + {"RSASSA-PKCS1-v1_5"_kj, "RSA-PSS"_kj, "ECDSA"_kj, "NODE-ED25519"_kj, "Ed25519"_kj, "HMAC"_kj, + "ML-DSA-44"_kj, "ML-DSA-65"_kj, "ML-DSA-87"_kj}); +} + +bool supportsDeriveBits(kj::StringPtr normalizedName) { + return isOneOf(normalizedName, {"PBKDF2"_kj, "HKDF"_kj, "ECDH"_kj, "X25519"_kj}); +} + +bool supportsExportKey(kj::StringPtr normalizedName) { + return isOneOf(normalizedName, + {"AES-CTR"_kj, "AES-CBC"_kj, "AES-GCM"_kj, "AES-KW"_kj, "HMAC"_kj, "RSASSA-PKCS1-v1_5"_kj, + "RSA-PSS"_kj, "RSA-OAEP"_kj, "RSA-RAW"_kj, "ECDSA"_kj, "ECDH"_kj, "NODE-ED25519"_kj, + "Ed25519"_kj, "X25519"_kj, "ML-DSA-44"_kj, "ML-DSA-65"_kj, "ML-DSA-87"_kj, "ML-KEM-768"_kj, + "ML-KEM-1024"_kj}); +} + +bool supportsEncapsulate(kj::StringPtr normalizedName) { + return isOneOf(normalizedName, {"ML-KEM-768"_kj, "ML-KEM-1024"_kj}); +} + +bool supportsGetPublicKey(kj::StringPtr normalizedName) { + return isOneOf(normalizedName, + {"RSA-OAEP"_kj, "ECDH"_kj, "X25519"_kj, "ML-KEM-768"_kj, "ML-KEM-1024"_kj, "ECDSA"_kj, + "Ed25519"_kj, "RSA-PSS"_kj, "RSASSA-PKCS1-v1_5"_kj, "ML-DSA-44"_kj, "ML-DSA-65"_kj, + "ML-DSA-87"_kj}); +} + +void validateEncryptAlgorithm( + jsg::Lock& js, kj::StringPtr normalizedName, const SubtleCrypto::EncryptAlgorithm& algorithm) { + if (normalizedName == "AES-GCM") { + auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".") + .getHandle(js); + JSG_REQUIRE(iv.size() != 0, DOMOperationError, "AES-GCM IV must not be empty."); + validateAesGcmTagLength(algorithm.tagLength.orDefault(128)); + } else if (normalizedName == "AES-CBC") { + auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".") + .getHandle(js); + JSG_REQUIRE(iv.size() == 16, DOMOperationError, "AES-CBC IV must be 16 bytes long."); + } else if (normalizedName == "AES-CTR") { + auto counter = JSG_REQUIRE_NONNULL( + algorithm.counter, TypeError, "Missing \"counter\" member in \"algorithm\".") + .getHandle(js); + JSG_REQUIRE(counter.size() == 16, DOMOperationError, "Counter must have length of 16 bytes."); + auto counterLength = JSG_REQUIRE_NONNULL( + algorithm.length, TypeError, "Missing \"length\" member in \"algorithm\"."); + JSG_REQUIRE(counterLength > 0 && counterLength <= 128, DOMOperationError, "Invalid counter of ", + counterLength, " bits length provided."); + } +} + +void validateSignAlgorithm( + kj::StringPtr normalizedName, const SubtleCrypto::SignAlgorithm& algorithm) { + if (normalizedName == "ECDSA") { + validateHashAlgorithm(algorithm.hash, "AlgorithmIdentifier"); + } else if (normalizedName == "RSA-PSS") { + auto saltLength = JSG_REQUIRE_NONNULL(algorithm.saltLength, TypeError, + "Failed to provide salt for RSA-PSS key operation which requires a salt"); + JSG_REQUIRE(saltLength >= 0, DOMDataError, "SaltLength for RSA-PSS must be non-negative."); + } +} + +void validateDeriveBitsAlgorithm(jsg::Lock& js, + kj::StringPtr normalizedName, + const SubtleCrypto::DeriveKeyAlgorithm& algorithm, + kj::Maybe maybeLength) { + if (normalizedName == "HKDF" || normalizedName == "PBKDF2") { + validateHashAlgorithm(algorithm.hash); + JSG_REQUIRE_NONNULL(algorithm.salt, TypeError, "Missing field \"salt\" in \"algorithm\"."); + auto length = JSG_REQUIRE_NONNULL(maybeLength, DOMOperationError, normalizedName, + " cannot derive a key " + "with null length."); + JSG_REQUIRE(length % 8 == 0, DOMOperationError, normalizedName, + " requires a derived key length that is a multiple of eight."); + + if (normalizedName == "HKDF") { + JSG_REQUIRE_NONNULL(algorithm.info, TypeError, "Missing field \"info\" in \"algorithm\"."); + } else { + auto iterations = JSG_REQUIRE_NONNULL( + algorithm.iterations, TypeError, "Missing field \"iterations\" in \"algorithm\"."); + JSG_REQUIRE(iterations > 0, DOMOperationError, + "PBKDF2 requires a positive iteration count (requested ", iterations, ")."); + checkPbkdfLimits(js, iterations); + } + } else if (normalizedName == "ECDH" || normalizedName == "X25519") { + auto& publicKey = JSG_REQUIRE_NONNULL( + algorithm.$public, TypeError, "Missing field \"public\" in \"derivedKeyParams\"."); + JSG_REQUIRE(publicKey->getType() == "public"_kj, DOMInvalidAccessError, + "The provided key has type \"", publicKey->getType(), "\", not \"public\""); + JSG_REQUIRE(strcasecmp(publicKey->getAlgorithmName().cStr(), normalizedName.cStr()) == 0, + DOMInvalidAccessError, "Public key algorithm does not match the requested algorithm."); + KJ_IF_SOME(length, maybeLength) { + auto maxDeriveBits = getEcdhMaxDeriveBits(js, *publicKey); + JSG_REQUIRE(length <= maxDeriveBits, DOMOperationError, "Derived key length (", length, + " bits) is too long (should be at most ", maxDeriveBits, " bits)."); + } + } +} + +kj::Maybe readSupportsLength(jsg::Lock& js, v8::Local value) { + if (value->IsUndefined() || value->IsNull()) return kj::none; + if (!value->IsNumber()) return kj::none; + + auto number = jsg::check(value->NumberValue(js.v8Context())); + JSG_REQUIRE(std::isfinite(number) && number >= 0 && + number <= std::numeric_limits::max() && number == std::floor(number), + TypeError, "length must be an unsigned long integer."); + return static_cast(number); +} + +bool isAdditionalAlgorithmArgument(v8::Local value) { + return !value->IsUndefined() && !value->IsNull() && !value->IsNumber(); +} + auto webCryptoOperationBegin( const char* operation, kj::StringPtr algorithm, kj::Maybe context = kj::none) { // This clears all OpenSSL errors & errno at the start & returns a deferred evaluation to make @@ -473,8 +775,8 @@ jsg::Promise> SubtleCrypto::deriveKey(jsg::Lock& js, // TODO(perf): For conformance, importKey() makes a copy of `secret`. In this case we really // don't need to, but rather we ought to call the appropriate CryptoKey::Impl::import*() // function directly. - return importKeySync( - js, "raw", secret.addRef(js), kj::mv(derivedKeyAlgorithm), extractable, kj::mv(keyUsages)); + return importKeySync(js, "raw-secret", secret.addRef(js), kj::mv(derivedKeyAlgorithm), + extractable, kj::mv(keyUsages)); }); } @@ -609,10 +911,10 @@ jsg::Ref SubtleCrypto::importKeySync(jsg::Lock& js, ImportKeyAlgorithm algorithm, bool extractable, kj::ArrayPtr keyUsages) { - if (format == "raw" || format == "pkcs8" || format == "spki") { + if (format == "raw" || format == "pkcs8" || format == "spki" || format == "raw-public" || + format == "raw-private" || format == "raw-seed" || format == "raw-secret") { auto& key = JSG_REQUIRE_NONNULL(keyData.tryGet>(), TypeError, - "Import data provided for \"raw\", \"pkcs8\", or \"spki\" import formats must be a buffer " - "source."); + "Import data provided for buffer-based import formats must be a buffer source."); auto keyHandle = key.getHandle(js); // Make a copy of the key import data. @@ -667,6 +969,321 @@ jsg::Promise SubtleCrypto::exportKey( }); } +jsg::Promise SubtleCrypto::encapsulateBits(jsg::Lock& js, + kj::OneOf encapsulationAlgorithmParam, + const CryptoKey& encapsulationKey) { + auto algorithm = interpretAlgorithmParam(kj::mv(encapsulationAlgorithmParam)); + + auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, algorithm); + + return js.evalNow([&]() -> EncapsulatedBits { + validateOperation(encapsulationKey, algorithm.name, CryptoKeyUsageSet::encapsulateBits()); + auto [sharedKey, ciphertext] = encapsulationKey.impl->encapsulate(js); + return EncapsulatedBits{ + .sharedKey = sharedKey.addRef(js), + .ciphertext = ciphertext.addRef(js), + }; + }); +} + +jsg::Promise SubtleCrypto::encapsulateKey(jsg::Lock& js, + kj::OneOf encapsulationAlgorithmParam, + const CryptoKey& encapsulationKey, + kj::OneOf sharedKeyAlgorithmParam, + bool extractable, + kj::Array keyUsages) { + auto algorithm = interpretAlgorithmParam(kj::mv(encapsulationAlgorithmParam)); + auto sharedKeyAlgorithm = interpretAlgorithmParam(kj::mv(sharedKeyAlgorithmParam)); + + auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, algorithm); + + return js.evalNow([&]() -> EncapsulatedKey { + validateOperation(encapsulationKey, algorithm.name, CryptoKeyUsageSet::encapsulateKey()); + auto [sharedKey, ciphertext] = encapsulationKey.impl->encapsulate(js); + auto sharedKeyBuffer = jsg::JsUint8Array::create(js, sharedKey.asArrayPtr()); + auto sharedKeyRef = + importKeySync(js, "raw-secret", jsg::JsBufferSource(sharedKeyBuffer).addRef(js), + kj::mv(sharedKeyAlgorithm), extractable, kj::mv(keyUsages)); + return EncapsulatedKey{ + .sharedKey = kj::mv(sharedKeyRef), + .ciphertext = ciphertext.addRef(js), + }; + }); +} + +jsg::Promise> SubtleCrypto::decapsulateBits(jsg::Lock& js, + kj::OneOf decapsulationAlgorithmParam, + const CryptoKey& decapsulationKey, + kj::Array ciphertext) { + auto algorithm = interpretAlgorithmParam(kj::mv(decapsulationAlgorithmParam)); + + auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, algorithm); + + return js.evalNow([&] { + validateOperation(decapsulationKey, algorithm.name, CryptoKeyUsageSet::decapsulateBits()); + return decapsulationKey.impl->decapsulate(js, ciphertext).addRef(js); + }); +} + +jsg::Promise> SubtleCrypto::decapsulateKey(jsg::Lock& js, + kj::OneOf decapsulationAlgorithmParam, + const CryptoKey& decapsulationKey, + kj::Array ciphertext, + kj::OneOf sharedKeyAlgorithmParam, + bool extractable, + kj::Array keyUsages) { + auto algorithm = interpretAlgorithmParam(kj::mv(decapsulationAlgorithmParam)); + auto sharedKeyAlgorithm = interpretAlgorithmParam(kj::mv(sharedKeyAlgorithmParam)); + + auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, algorithm); + + return js.evalNow([&] { + validateOperation(decapsulationKey, algorithm.name, CryptoKeyUsageSet::decapsulateKey()); + auto secret = decapsulationKey.impl->decapsulate(js, ciphertext); + auto secretBuffer = jsg::JsUint8Array::create(js, secret.asArrayPtr()); + return importKeySync(js, "raw-secret", jsg::JsBufferSource(secretBuffer).addRef(js), + kj::mv(sharedKeyAlgorithm), extractable, kj::mv(keyUsages)); + }); +} + +jsg::Promise> SubtleCrypto::getPublicKey( + jsg::Lock& js, const CryptoKey& key, kj::Array keyUsages) { + auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, key.getAlgorithmName()); + + return js.evalNow([&] { + JSG_REQUIRE(key.getType() != "secret"_kj, DOMNotSupportedError, + "The getPublicKey operation is not supported for symmetric keys."); + JSG_REQUIRE(key.getType() == "private"_kj, DOMInvalidAccessError, + "The getPublicKey operation requires a private key."); + + // Determine the algorithm-specific allowed public key usages. + auto algorithmName = key.getAlgorithmName(); + CryptoKeyUsageSet allowedUsages; + if (algorithmName == "RSA-OAEP") { + allowedUsages = CryptoKeyUsageSet::encrypt() | CryptoKeyUsageSet::wrapKey(); + } else if (algorithmName == "ECDH" || algorithmName == "X25519") { + allowedUsages = CryptoKeyUsageSet(); + } else if (algorithmName == "ML-KEM-768" || algorithmName == "ML-KEM-1024") { + allowedUsages = CryptoKeyUsageSet::encapsulateKey() | CryptoKeyUsageSet::encapsulateBits(); + } else if (algorithmName == "ECDSA" || algorithmName == "Ed25519" || + algorithmName == "RSA-PSS" || algorithmName == "RSASSA-PKCS1-v1_5" || + algorithmName == "ML-DSA-44" || algorithmName == "ML-DSA-65" || + algorithmName == "ML-DSA-87") { + allowedUsages = CryptoKeyUsageSet::verify(); + } else { + JSG_FAIL_REQUIRE(DOMNotSupportedError, "The getPublicKey operation is not supported for \"", + algorithmName, "\"."); + } + + auto usages = CryptoKeyUsageSet::validate( + algorithmName, CryptoKeyUsageSet::Context::importPublic, keyUsages, allowedUsages); + + auto publicKeyImpl = key.impl->getPublicKey(js, usages); + return js.alloc(kj::mv(publicKeyImpl)); + }); +} + +bool SubtleCrypto::supports(jsg::Lock& js, + kj::String operation, + v8::Local algorithm, + jsg::Optional> lengthOrAdditionalAlgorithm, + const jsg::TypeHandler>& encryptAlgorithmHandler, + const jsg::TypeHandler>& signAlgorithmHandler, + const jsg::TypeHandler>& digestAlgorithmHandler, + const jsg::TypeHandler>& + generateKeyAlgorithmHandler, + const jsg::TypeHandler>& deriveKeyAlgorithmHandler, + const jsg::TypeHandler>& importKeyAlgorithmHandler) { + KJ_IF_SOME(parsedOperation, parseSubtleOperation(operation)) { + kj::Maybe parsedLength = kj::none; + KJ_IF_SOME(thirdArgument, lengthOrAdditionalAlgorithm) { + if (!isAdditionalAlgorithmArgument(thirdArgument)) { + parsedLength = readSupportsLength(js, thirdArgument); + } + } + + return js.tryCatch([&]() -> bool { + auto checkSupportForAlgorithm = [&](SubtleOperation checkedOperation, + v8::Local algorithmValue, + kj::Maybe maybeLength) -> bool { + switch (checkedOperation) { + case SubtleOperation::ENCRYPT: + [[fallthrough]]; + case SubtleOperation::DECRYPT: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, encryptAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + if (!supportsEncryptDecrypt(algoImpl.name)) return false; + validateEncryptAlgorithm(js, algoImpl.name, normalizedAlgorithm); + return true; + } + return false; + } + + case SubtleOperation::SIGN: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, signAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + if (!supportsSign(algoImpl.name)) return false; + validateSignAlgorithm(algoImpl.name, normalizedAlgorithm); + return true; + } + return false; + } + + case SubtleOperation::VERIFY: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, signAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + if (!supportsVerify(algoImpl.name)) return false; + validateSignAlgorithm(algoImpl.name, normalizedAlgorithm); + return true; + } + return false; + } + + case SubtleOperation::DIGEST: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, digestAlgorithmHandler); + KJ_IF_SOME(exception, kj::runCatchingExceptions([&] { + lookupDigestAlgorithm(normalizedAlgorithm.name); + })) { + (void)exception; + return false; + } + return true; + } + + case SubtleOperation::GENERATE_KEY: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, generateKeyAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + if (algoImpl.generateFunc == nullptr) return false; + validateGenerateKeyAlgorithm(js, algoImpl.name, normalizedAlgorithm); + return true; + } + return false; + } + + case SubtleOperation::DERIVE_KEY: + return false; + + case SubtleOperation::DERIVE_BITS: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, deriveKeyAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + if (!supportsDeriveBits(algoImpl.name)) return false; + validateDeriveBitsAlgorithm(js, algoImpl.name, normalizedAlgorithm, maybeLength); + return true; + } + return false; + } + + case SubtleOperation::IMPORT_KEY: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, importKeyAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + if (algoImpl.importFunc == nullptr) return false; + validateImportKeyAlgorithm(algoImpl.name, normalizedAlgorithm); + return true; + } + return false; + } + + case SubtleOperation::EXPORT_KEY: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, importKeyAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + return supportsExportKey(algoImpl.name); + } + return false; + } + + case SubtleOperation::WRAP_KEY: + [[fallthrough]]; + case SubtleOperation::UNWRAP_KEY: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, encryptAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + if (algoImpl.name == "AES-KW") return true; + if (!supportsEncryptDecrypt(algoImpl.name)) return false; + validateEncryptAlgorithm(js, algoImpl.name, normalizedAlgorithm); + return true; + } + return false; + } + + case SubtleOperation::ENCAPSULATE_KEY: + [[fallthrough]]; + case SubtleOperation::ENCAPSULATE_BITS: + [[fallthrough]]; + case SubtleOperation::DECAPSULATE_KEY: + [[fallthrough]]; + case SubtleOperation::DECAPSULATE_BITS: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, importKeyAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + return supportsEncapsulate(algoImpl.name); + } + return false; + } + + case SubtleOperation::GET_PUBLIC_KEY: { + auto normalizedAlgorithm = + normalizeSupportAlgorithm(js, algorithmValue, importKeyAlgorithmHandler); + KJ_IF_SOME(algoImpl, lookupAlgorithm(normalizedAlgorithm.name)) { + return supportsGetPublicKey(algoImpl.name); + } + return false; + } + } + + KJ_UNREACHABLE; + }; + + KJ_IF_SOME(thirdArgument, lengthOrAdditionalAlgorithm) { + if (isAdditionalAlgorithmArgument(thirdArgument)) { + auto additionalAlgorithm = + normalizeSupportAlgorithm(js, thirdArgument, importKeyAlgorithmHandler); + + switch (parsedOperation) { + case SubtleOperation::DERIVE_KEY: { + if (!checkSupportForAlgorithm(SubtleOperation::IMPORT_KEY, thirdArgument, kj::none)) { + return false; + } + auto length = getKeyLength(additionalAlgorithm); + return checkSupportForAlgorithm(SubtleOperation::DERIVE_BITS, algorithm, length); + } + + case SubtleOperation::UNWRAP_KEY: + [[fallthrough]]; + case SubtleOperation::ENCAPSULATE_KEY: + [[fallthrough]]; + case SubtleOperation::DECAPSULATE_KEY: + if (!checkSupportForAlgorithm(SubtleOperation::IMPORT_KEY, thirdArgument, kj::none)) { + return false; + } + return checkSupportForAlgorithm(parsedOperation, algorithm, kj::none); + + case SubtleOperation::WRAP_KEY: + if (!checkSupportForAlgorithm(SubtleOperation::EXPORT_KEY, thirdArgument, kj::none)) { + return false; + } + return checkSupportForAlgorithm(parsedOperation, algorithm, kj::none); + + default: + return checkSupportForAlgorithm(parsedOperation, algorithm, kj::none); + } + } + } + + return checkSupportForAlgorithm(parsedOperation, algorithm, parsedLength); + }, [](jsg::Value&&) { return false; }); + } + + return false; +} + bool SubtleCrypto::timingSafeEqual(jsg::JsBufferSource a, jsg::JsBufferSource b) { JSG_REQUIRE(a.size() == b.size(), TypeError, "Input buffers must have the same byte length."); diff --git a/src/workerd/api/crypto/crypto.h b/src/workerd/api/crypto/crypto.h index da0073b7ee8..14e213ef869 100644 --- a/src/workerd/api/crypto/crypto.h +++ b/src/workerd/api/crypto/crypto.h @@ -52,13 +52,26 @@ class CryptoKeyUsageSet { static constexpr CryptoKeyUsageSet unwrapKey() { return 1 << 7; } + static constexpr CryptoKeyUsageSet encapsulateKey() { + return 1 << 8; + } + static constexpr CryptoKeyUsageSet encapsulateBits() { + return 1 << 9; + } + static constexpr CryptoKeyUsageSet decapsulateKey() { + return 1 << 10; + } + static constexpr CryptoKeyUsageSet decapsulateBits() { + return 1 << 11; + } static constexpr CryptoKeyUsageSet publicKeyMask() { - return encrypt() | verify() | wrapKey(); + return encrypt() | verify() | wrapKey() | encapsulateKey() | encapsulateBits(); } static constexpr CryptoKeyUsageSet privateKeyMask() { - return decrypt() | sign() | unwrapKey() | deriveKey() | deriveBits(); + return decrypt() | sign() | unwrapKey() | deriveKey() | deriveBits() | decapsulateKey() | + decapsulateBits(); } static constexpr CryptoKeyUsageSet derivationKeyMask() { @@ -127,8 +140,8 @@ class CryptoKeyUsageSet { } private: - constexpr CryptoKeyUsageSet(uint8_t set): set(set) {} - uint8_t set; + constexpr CryptoKeyUsageSet(uint16_t set): set(set) {} + uint16_t set; }; // ======================================================================================= @@ -398,7 +411,10 @@ class SubtleCrypto: public jsg::Object { // Used for RSA-PSS jsg::Optional saltLength; - JSG_STRUCT(name, hash, dataLength, saltLength); + // Used for ML-DSA context parameter. + jsg::Optional> context; + + JSG_STRUCT(name, hash, dataLength, saltLength, context); }; // Type of the `algorithm` parameter passed to `generateKey()`. Different algorithms call for @@ -515,7 +531,12 @@ class SubtleCrypto: public jsg::Object { // to bother adding support for multiprime RSA keys? Chromium doesn't AFAICT... jsg::Optional k; - JSG_STRUCT(kty, use, key_ops, alg, ext, crv, x, y, d, n, e, p, q, dp, dq, qi, oth, k); + // The following fields are defined in draft-ietf-cose-dilithium for the AKP key type + jsg::Optional pub; + jsg::Optional priv; + + JSG_STRUCT( + kty, use, key_ops, alg, ext, crv, x, y, d, n, e, p, q, dp, dq, qi, oth, k, pub, priv); JSG_STRUCT_TS_OVERRIDE(JsonWebKey); // Rename from SubtleCryptoJsonWebKey }; @@ -598,10 +619,65 @@ class SubtleCrypto: public jsg::Object { kj::Array keyUsages, const jsg::TypeHandler& jwkHandler); + // Result type for encapsulateBits() + struct EncapsulatedBits { + jsg::JsRef sharedKey; + jsg::JsRef ciphertext; + JSG_STRUCT(sharedKey, ciphertext); + JSG_MEMORY_INFO(EncapsulatedBits) {} + }; + + // Result type for encapsulateKey() + struct EncapsulatedKey { + jsg::Ref sharedKey; + jsg::JsRef ciphertext; + JSG_STRUCT(sharedKey, ciphertext); + JSG_MEMORY_INFO(EncapsulatedKey) {} + }; + + jsg::Promise encapsulateKey(jsg::Lock& js, + kj::OneOf encapsulationAlgorithm, + const CryptoKey& encapsulationKey, + kj::OneOf sharedKeyAlgorithm, + bool extractable, + kj::Array keyUsages); + + jsg::Promise encapsulateBits(jsg::Lock& js, + kj::OneOf encapsulationAlgorithm, + const CryptoKey& encapsulationKey); + + jsg::Promise> decapsulateKey(jsg::Lock& js, + kj::OneOf decapsulationAlgorithm, + const CryptoKey& decapsulationKey, + kj::Array ciphertext, + kj::OneOf sharedKeyAlgorithm, + bool extractable, + kj::Array keyUsages); + + jsg::Promise> decapsulateBits(jsg::Lock& js, + kj::OneOf decapsulationAlgorithm, + const CryptoKey& decapsulationKey, + kj::Array ciphertext); + + jsg::Promise> getPublicKey( + jsg::Lock& js, const CryptoKey& key, kj::Array keyUsages); + + static bool supports(jsg::Lock& js, + kj::String operation, + v8::Local algorithm, + jsg::Optional> lengthOrAdditionalAlgorithm, + const jsg::TypeHandler>& encryptAlgorithmHandler, + const jsg::TypeHandler>& signAlgorithmHandler, + const jsg::TypeHandler>& digestAlgorithmHandler, + const jsg::TypeHandler>& + generateKeyAlgorithmHandler, + const jsg::TypeHandler>& deriveKeyAlgorithmHandler, + const jsg::TypeHandler>& importKeyAlgorithmHandler); + // This is a non-standard extension based off Node.js' implementation of crypto.timingSafeEqual. bool timingSafeEqual(jsg::JsBufferSource a, jsg::JsBufferSource b); - JSG_RESOURCE_TYPE(SubtleCrypto) { + JSG_RESOURCE_TYPE(SubtleCrypto, CompatibilityFlags::Reader flags) { JSG_METHOD(encrypt); JSG_METHOD(decrypt); JSG_METHOD(sign); @@ -614,35 +690,101 @@ class SubtleCrypto: public jsg::Object { JSG_METHOD(exportKey); JSG_METHOD(wrapKey); JSG_METHOD(unwrapKey); + if (flags.getWebCryptoModernAlgorithms()) { + JSG_STATIC_METHOD(supports); + JSG_METHOD(encapsulateKey); + JSG_METHOD(encapsulateBits); + JSG_METHOD(decapsulateKey); + JSG_METHOD(decapsulateBits); + JSG_METHOD(getPublicKey); + } JSG_METHOD(timingSafeEqual); - JSG_TS_OVERRIDE({ - wrapKey(format: string, - key: CryptoKey, - wrappingKey: CryptoKey, - wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm) - : Promise; - deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, - baseKey : CryptoKey, - length? : number | null) - : Promise; - digest(algorithm: string | SubtleCryptoHashAlgorithm, + if (flags.getWebCryptoModernAlgorithms()) { + JSG_TS_OVERRIDE({ + static supports(operation: string, + algorithm: string | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, + length?: number | null): boolean; + static supports(operation: string, + algorithm: string | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, + additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm): boolean; + wrapKey(format: string, + key: CryptoKey, + wrappingKey: CryptoKey, + wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm) + : Promise; + deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, + baseKey : CryptoKey, + length? : number | null) + : Promise; + digest(algorithm: string | SubtleCryptoHashAlgorithm, + data: ArrayBuffer | ArrayBufferView) + : Promise; + sign(algorithm: string | SubtleCryptoSignAlgorithm, + key: CryptoKey, data: ArrayBuffer | ArrayBufferView) - : Promise; - sign(algorithm: string | SubtleCryptoSignAlgorithm, - key: CryptoKey, - data: ArrayBuffer | ArrayBufferView) - : Promise; - decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, - key: CryptoKey, - cipherText: ArrayBuffer | ArrayBufferView) - : Promise; - encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, - key: CryptoKey, - plainText: ArrayBuffer | ArrayBufferView) - : Promise; - exportKey(format: string, key: CryptoKey) : Promise; - }); + : Promise; + decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, + key: CryptoKey, + cipherText: ArrayBuffer | ArrayBufferView) + : Promise; + encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, + key: CryptoKey, + plainText: ArrayBuffer | ArrayBufferView) + : Promise; + exportKey(format: string, key: CryptoKey) : Promise; + encapsulateKey(encapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + encapsulationKey: CryptoKey, + sharedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[]) + : Promise; + encapsulateBits(encapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + encapsulationKey: CryptoKey) + : Promise; + decapsulateKey(decapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + decapsulationKey: CryptoKey, + ciphertext: ArrayBuffer | ArrayBufferView, + sharedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[]) + : Promise; + decapsulateBits(decapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + decapsulationKey: CryptoKey, + ciphertext: ArrayBuffer | ArrayBufferView) + : Promise; + getPublicKey(key: CryptoKey, keyUsages: string[]) + : Promise; + }); + } else { + JSG_TS_OVERRIDE({ + wrapKey(format: string, + key: CryptoKey, + wrappingKey: CryptoKey, + wrapAlgorithm: string | SubtleCryptoEncryptAlgorithm) + : Promise; + deriveBits(algorithm: string | SubtleCryptoDeriveKeyAlgorithm, + baseKey : CryptoKey, + length? : number | null) + : Promise; + digest(algorithm: string | SubtleCryptoHashAlgorithm, + data: ArrayBuffer | ArrayBufferView) + : Promise; + sign(algorithm: string | SubtleCryptoSignAlgorithm, + key: CryptoKey, + data: ArrayBuffer | ArrayBufferView) + : Promise; + decrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, + key: CryptoKey, + cipherText: ArrayBuffer | ArrayBufferView) + : Promise; + encrypt(algorithm: string | SubtleCryptoEncryptAlgorithm, + key: CryptoKey, + plainText: ArrayBuffer | ArrayBufferView) + : Promise; + exportKey(format: string, key: CryptoKey) : Promise; + }); + } } }; @@ -776,6 +918,7 @@ class Crypto: public jsg::Object { #define EW_CRYPTO_ISOLATE_TYPES \ api::Crypto, api::SubtleCrypto, api::CryptoKey, api::CryptoKeyPair, \ api::SubtleCrypto::JsonWebKey, api::SubtleCrypto::JsonWebKey::RsaOtherPrimesInfo, \ + api::SubtleCrypto::EncapsulatedBits, api::SubtleCrypto::EncapsulatedKey, \ api::SubtleCrypto::DeriveKeyAlgorithm, api::SubtleCrypto::EncryptAlgorithm, \ api::SubtleCrypto::GenerateKeyAlgorithm, api::SubtleCrypto::HashAlgorithm, \ api::SubtleCrypto::ImportKeyAlgorithm, api::SubtleCrypto::SignAlgorithm, \ diff --git a/src/workerd/api/crypto/digest.c++ b/src/workerd/api/crypto/digest.c++ index 6c2c888270a..07de6f67916 100644 --- a/src/workerd/api/crypto/digest.c++ +++ b/src/workerd/api/crypto/digest.c++ @@ -70,7 +70,7 @@ class HmacKey final: public CryptoKey::Impl { } SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override { - JSG_REQUIRE(format == "raw" || format == "jwk", DOMNotSupportedError, + JSG_REQUIRE(format == "raw" || format == "raw-secret" || format == "jwk", DOMNotSupportedError, "Unimplemented key export format \"", format, "\"."); if (format == "jwk") { @@ -266,7 +266,7 @@ kj::Own CryptoKey::Impl::importHmac(jsg::Lock& js, kj::StringPtr hash = api::getAlgorithmName( JSG_REQUIRE_NONNULL(algorithm.hash, TypeError, "Missing field \"hash\" in \"algorithm\".")); - if (format == "raw") { + if (format == "raw" || format == "raw-secret") { // NOTE: Checked in SubtleCrypto::importKey(). auto& source = keyData.get>(); auto handle = source.getHandle(js); diff --git a/src/workerd/api/crypto/ec.c++ b/src/workerd/api/crypto/ec.c++ index 3c39d8a530a..3e177a8aa20 100644 --- a/src/workerd/api/crypto/ec.c++ +++ b/src/workerd/api/crypto/ec.c++ @@ -151,6 +151,11 @@ class EllipticKey final: public AsymmetricKeyCryptoKeyImpl { return keyAlgorithm.name; } + kj::Own cloneAsPublicKey( + jsg::Lock& js, AsymmetricKeyData publicKeyData) const override { + return kj::heap(kj::mv(publicKeyData), keyAlgorithm, rsSize, true); + } + void requireSigningAbility() const { // This assert is internal to our WebCrypto implementation because we share the AsymmetricKey // implementation between ECDH & ECDSA (the former only supports deriveBits/deriveKey, not @@ -705,7 +710,7 @@ kj::Own CryptoKey::Impl::importEcdsa(jsg::Lock& js, auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(namedCurve); auto importedKey = [&, curveId = curveId] { - if (format != "raw") { + if (format != "raw" && format != "raw-public") { return importAsymmetricForWebCrypto(js, format, kj::mv(keyData), normalizedName, extractable, keyUsages, // Verbose lambda capture needed because: https://bugs.llvm.org/show_bug.cgi?id=35984 @@ -766,7 +771,7 @@ kj::Own CryptoKey::Impl::importEcdh(jsg::Lock& js, auto strictCrypto = FeatureFlags::get(js).getStrictCrypto(); auto usageSet = strictCrypto ? CryptoKeyUsageSet() : CryptoKeyUsageSet::derivationKeyMask(); - if (format != "raw") { + if (format != "raw" && format != "raw-public") { return importAsymmetricForWebCrypto(js, format, kj::mv(keyData), normalizedName, extractable, keyUsages, // Verbose lambda capture needed because: https://bugs.llvm.org/show_bug.cgi?id=35984 @@ -840,6 +845,11 @@ class EdDsaKey final: public AsymmetricKeyCryptoKeyImpl { return keyAlgorithm; } + kj::Own cloneAsPublicKey( + jsg::Lock& js, AsymmetricKeyData publicKeyData) const override { + return kj::heap(kj::mv(publicKeyData), keyAlgorithm, true); + } + kj::StringPtr chooseHash( const kj::Maybe>& callTimeHash) const override { @@ -1171,7 +1181,7 @@ kj::Own CryptoKey::Impl::importEddsa(jsg::Lock& js, auto importedKey = [&] { auto nid = normalizedName == "X25519" ? NID_X25519 : NID_ED25519; - if (format != "raw") { + if (format != "raw" && format != "raw-public") { return importAsymmetricForWebCrypto(js, format, kj::mv(keyData), normalizedName, extractable, keyUsages, [nid, normalizedName = kj::str(normalizedName)]( diff --git a/src/workerd/api/crypto/hkdf.c++ b/src/workerd/api/crypto/hkdf.c++ index 948f3abff58..d31c582e41d 100644 --- a/src/workerd/api/crypto/hkdf.c++ +++ b/src/workerd/api/crypto/hkdf.c++ @@ -115,7 +115,7 @@ kj::Own CryptoKey::Impl::importHkdf(jsg::Lock& js, CryptoKeyUsageSet::Context::importSecret, keyUsages, CryptoKeyUsageSet::derivationKeyMask()); JSG_REQUIRE(!extractable, DOMSyntaxError, "HKDF key cannot be extractable."); - JSG_REQUIRE(format == "raw", DOMNotSupportedError, + JSG_REQUIRE(format == "raw" || format == "raw-secret", DOMNotSupportedError, "HKDF key must be imported " "in \"raw\" format (requested \"", format, "\")"); diff --git a/src/workerd/api/crypto/impl.h b/src/workerd/api/crypto/impl.h index 1a25fa8f6b9..66544539043 100644 --- a/src/workerd/api/crypto/impl.h +++ b/src/workerd/api/crypto/impl.h @@ -78,6 +78,21 @@ std::pair lookupDigestAlgorithm(kj::StringPtr algo // kj::decodeBase64 can do this in-situ for both cases. kj::EncodingResult> decodeBase64Url(kj::String text); +inline void validateAesGcmTagLength(int tagLength) { + switch (tagLength) { + case 32: + case 64: + case 96: + case 104: + case 112: + case 120: + case 128: + return; + default: + JSG_FAIL_REQUIRE(DOMOperationError, "Invalid AES-GCM tag length ", tagLength, "."); + } +} + // WebCrypto likes to allow algorithms to be specified as a simple string name, or as a struct // containing a `name` field and possibly other fields. This helper collapses that. template @@ -122,6 +137,8 @@ class CryptoKey::Impl { static ImportFunc importEcdh; static ImportFunc importEddsa; static ImportFunc importRsaRaw; + static ImportFunc importMlDsa; + static ImportFunc importMlKem; using GenerateFunc = kj::OneOf, CryptoKeyPair>(jsg::Lock& js, kj::StringPtr normalizedName, @@ -135,6 +152,8 @@ class CryptoKey::Impl { static GenerateFunc generateEcdsa; static GenerateFunc generateEcdh; static GenerateFunc generateEddsa; + static GenerateFunc generateMlDsa; + static GenerateFunc generateMlKem; Impl(bool extractable, CryptoKeyUsageSet usages): extractable(extractable), usages(usages) {} @@ -182,6 +201,23 @@ class CryptoKey::Impl { "\"."); } + // Returns {sharedKey, ciphertext} as a pair of byte arrays for KEM encapsulation. + virtual std::pair encapsulate(jsg::Lock& js) const { + JSG_FAIL_REQUIRE(DOMNotSupportedError, "The encapsulate operation is not implemented for \"", + getAlgorithmName(), "\"."); + } + + // Returns the shared key bytes for KEM decapsulation. + virtual jsg::JsArrayBuffer decapsulate( + jsg::Lock& js, kj::ArrayPtr ciphertext) const { + JSG_FAIL_REQUIRE(DOMNotSupportedError, "The decapsulate operation is not implemented for \"", + getAlgorithmName(), "\"."); + } + // Returns a new public key Impl derived from this private key. + virtual kj::Own getPublicKey(jsg::Lock& js, CryptoKeyUsageSet usages) const { + JSG_FAIL_REQUIRE(DOMNotSupportedError, "The getPublicKey operation is not implemented for \"", + getAlgorithmName(), "\"."); + } virtual jsg::JsArrayBuffer wrapKey(jsg::Lock& js, SubtleCrypto::EncryptAlgorithm&& algorithm, kj::ArrayPtr unwrappedKey) const { diff --git a/src/workerd/api/crypto/keys.c++ b/src/workerd/api/crypto/keys.c++ index 2c6d7d40357..55952a5290e 100644 --- a/src/workerd/api/crypto/keys.c++ +++ b/src/workerd/api/crypto/keys.c++ @@ -30,6 +30,33 @@ AsymmetricKeyCryptoKeyImpl::AsymmetricKeyCryptoKeyImpl(AsymmetricKeyData&& key, KJ_DASSERT(keyType != KeyType::SECRET); } +kj::Own AsymmetricKeyCryptoKeyImpl::getPublicKey( + jsg::Lock& js, CryptoKeyUsageSet usages) const { + JSG_REQUIRE( + keyType == KeyType::PRIVATE, DOMInvalidAccessError, "getPublicKey requires a private key."); + + // Extract the public key by serializing/deserializing through SPKI DER. + uint8_t* der = nullptr; + KJ_DEFER(if (der != nullptr) { OPENSSL_free(der); }); + size_t derLen; + bssl::ScopedCBB cbb; + JSG_REQUIRE(CBB_init(cbb.get(), 0) && EVP_marshal_public_key(cbb.get(), keyData.get()) && + CBB_finish(cbb.get(), &der, &derLen), + InternalDOMOperationError, "Failed to extract public key."); + + CBS cbs; + CBS_init(&cbs, der, derLen); + auto publicEvpPkey = OSSLCALL_OWN(EVP_PKEY, EVP_parse_public_key(&cbs), InternalDOMOperationError, + "Failed to parse extracted public key."); + + return cloneAsPublicKey(js, + { + .evpPkey = kj::mv(publicEvpPkey), + .keyType = KeyType::PUBLIC, + .usages = usages, + }); +} + jsg::JsArrayBuffer AsymmetricKeyCryptoKeyImpl::signatureSslToWebCrypto( jsg::Lock& js, kj::ArrayPtr signature) const { return jsg::JsArrayBuffer::create(js, signature); @@ -74,7 +101,7 @@ SubtleCrypto::ExportKeyData AsymmetricKeyCryptoKeyImpl::exportKey( jwk.ext = true; jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); }); return jwk; - } else if (format == "raw"_kj) { + } else if (format == "raw"_kj || format == "raw-public"_kj) { return exportRaw(js).addRef(js); } else { JSG_FAIL_REQUIRE(DOMInvalidAccessError, "Cannot export \"", getAlgorithmName(), "\" in \"", diff --git a/src/workerd/api/crypto/keys.h b/src/workerd/api/crypto/keys.h index e2a04211ba1..647ac46afca 100644 --- a/src/workerd/api/crypto/keys.h +++ b/src/workerd/api/crypto/keys.h @@ -117,6 +117,12 @@ class AsymmetricKeyCryptoKeyImpl: public CryptoKey::Impl { bool verifyX509Public(const X509* cert) const override; bool verifyX509Private(const X509* cert) const override; + kj::Own getPublicKey(jsg::Lock& js, CryptoKeyUsageSet usages) const override; + + protected: + virtual kj::Own cloneAsPublicKey( + jsg::Lock& js, AsymmetricKeyData publicKeyData) const = 0; + private: virtual SubtleCrypto::JsonWebKey exportJwk() const = 0; virtual jsg::JsArrayBuffer exportRaw(jsg::Lock& js) const = 0; diff --git a/src/workerd/api/crypto/mldsa.c++ b/src/workerd/api/crypto/mldsa.c++ new file mode 100644 index 00000000000..3079ebb1f84 --- /dev/null +++ b/src/workerd/api/crypto/mldsa.c++ @@ -0,0 +1,819 @@ +#include "impl.h" + +#include +#include + +#include + +namespace workerd::api { +namespace { + +// Traits structs for each ML-DSA parameter set. +struct MlDsa44Params { + using PrivateKey = MLDSA44_private_key; + using PublicKey = MLDSA44_public_key; + static constexpr size_t PUBLIC_KEY_BYTES = MLDSA44_PUBLIC_KEY_BYTES; + static constexpr size_t SIGNATURE_BYTES = MLDSA44_SIGNATURE_BYTES; + + static int generateKey(uint8_t* outPk, uint8_t* outSeed, PrivateKey* outSk) { + return MLDSA44_generate_key(outPk, outSeed, outSk); + } + static int privateKeyFromSeed(PrivateKey* outSk, const uint8_t* seed, size_t seedLen) { + return MLDSA44_private_key_from_seed(outSk, seed, seedLen); + } + static int publicFromPrivate(PublicKey* outPk, const PrivateKey* sk) { + return MLDSA44_public_from_private(outPk, sk); + } + static int sign(uint8_t* outSig, + const PrivateKey* sk, + const uint8_t* msg, + size_t msgLen, + const uint8_t* ctx, + size_t ctxLen) { + return MLDSA44_sign(outSig, sk, msg, msgLen, ctx, ctxLen); + } + static int verify(const PublicKey* pk, + const uint8_t* sig, + size_t sigLen, + const uint8_t* msg, + size_t msgLen, + const uint8_t* ctx, + size_t ctxLen) { + return MLDSA44_verify(pk, sig, sigLen, msg, msgLen, ctx, ctxLen); + } + static int marshalPublicKey(CBB* out, const PublicKey* pk) { + return MLDSA44_marshal_public_key(out, pk); + } + static int parsePublicKey(PublicKey* outPk, CBS* in) { + return MLDSA44_parse_public_key(outPk, in); + } +}; + +struct MlDsa65Params { + using PrivateKey = MLDSA65_private_key; + using PublicKey = MLDSA65_public_key; + static constexpr size_t PUBLIC_KEY_BYTES = MLDSA65_PUBLIC_KEY_BYTES; + static constexpr size_t SIGNATURE_BYTES = MLDSA65_SIGNATURE_BYTES; + + static int generateKey(uint8_t* outPk, uint8_t* outSeed, PrivateKey* outSk) { + return MLDSA65_generate_key(outPk, outSeed, outSk); + } + static int privateKeyFromSeed(PrivateKey* outSk, const uint8_t* seed, size_t seedLen) { + return MLDSA65_private_key_from_seed(outSk, seed, seedLen); + } + static int publicFromPrivate(PublicKey* outPk, const PrivateKey* sk) { + return MLDSA65_public_from_private(outPk, sk); + } + static int sign(uint8_t* outSig, + const PrivateKey* sk, + const uint8_t* msg, + size_t msgLen, + const uint8_t* ctx, + size_t ctxLen) { + return MLDSA65_sign(outSig, sk, msg, msgLen, ctx, ctxLen); + } + static int verify(const PublicKey* pk, + const uint8_t* sig, + size_t sigLen, + const uint8_t* msg, + size_t msgLen, + const uint8_t* ctx, + size_t ctxLen) { + return MLDSA65_verify(pk, sig, sigLen, msg, msgLen, ctx, ctxLen); + } + static int marshalPublicKey(CBB* out, const PublicKey* pk) { + return MLDSA65_marshal_public_key(out, pk); + } + static int parsePublicKey(PublicKey* outPk, CBS* in) { + return MLDSA65_parse_public_key(outPk, in); + } +}; + +struct MlDsa87Params { + using PrivateKey = MLDSA87_private_key; + using PublicKey = MLDSA87_public_key; + static constexpr size_t PUBLIC_KEY_BYTES = MLDSA87_PUBLIC_KEY_BYTES; + static constexpr size_t SIGNATURE_BYTES = MLDSA87_SIGNATURE_BYTES; + + static int generateKey(uint8_t* outPk, uint8_t* outSeed, PrivateKey* outSk) { + return MLDSA87_generate_key(outPk, outSeed, outSk); + } + static int privateKeyFromSeed(PrivateKey* outSk, const uint8_t* seed, size_t seedLen) { + return MLDSA87_private_key_from_seed(outSk, seed, seedLen); + } + static int publicFromPrivate(PublicKey* outPk, const PrivateKey* sk) { + return MLDSA87_public_from_private(outPk, sk); + } + static int sign(uint8_t* outSig, + const PrivateKey* sk, + const uint8_t* msg, + size_t msgLen, + const uint8_t* ctx, + size_t ctxLen) { + return MLDSA87_sign(outSig, sk, msg, msgLen, ctx, ctxLen); + } + static int verify(const PublicKey* pk, + const uint8_t* sig, + size_t sigLen, + const uint8_t* msg, + size_t msgLen, + const uint8_t* ctx, + size_t ctxLen) { + return MLDSA87_verify(pk, sig, sigLen, msg, msgLen, ctx, ctxLen); + } + static int marshalPublicKey(CBB* out, const PublicKey* pk) { + return MLDSA87_marshal_public_key(out, pk); + } + static int parsePublicKey(PublicKey* outPk, CBS* in) { + return MLDSA87_parse_public_key(outPk, in); + } +}; + +// OIDs for ML-DSA algorithms (from NIST CSOR) +// id-ml-dsa-44: 2.16.840.1.101.3.4.3.17 +constexpr uint8_t OID_ML_DSA_44[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x11}; +// id-ml-dsa-65: 2.16.840.1.101.3.4.3.18 +constexpr uint8_t OID_ML_DSA_65[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x12}; +// id-ml-dsa-87: 2.16.840.1.101.3.4.3.19 +constexpr uint8_t OID_ML_DSA_87[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x03, 0x13}; + +template +kj::ArrayPtr getOid() { + if constexpr (P::PUBLIC_KEY_BYTES == MLDSA44_PUBLIC_KEY_BYTES) { + return kj::arrayPtr(OID_ML_DSA_44, sizeof(OID_ML_DSA_44)); + } else if constexpr (P::PUBLIC_KEY_BYTES == MLDSA65_PUBLIC_KEY_BYTES) { + return kj::arrayPtr(OID_ML_DSA_65, sizeof(OID_ML_DSA_65)); + } else { + return kj::arrayPtr(OID_ML_DSA_87, sizeof(OID_ML_DSA_87)); + } +} + +// Get context bytes from the SignAlgorithm parameter. +std::pair getContext( + jsg::Lock& js, SubtleCrypto::SignAlgorithm& algorithm) { + KJ_IF_SOME(ctx, algorithm.context) { + auto ctxData = ctx.getHandle(js).asArrayPtr(); + return {ctxData.begin(), ctxData.size()}; + } + // BoringSSL treats (nullptr, 0) as an empty context, equivalent to a zero-length byte string + // per FIPS 204 §5.2. + return {nullptr, 0}; +} + +template +class MlDsaKey final: public CryptoKey::Impl { + public: + // Private key constructor + MlDsaKey(kj::Array seed, + P::PrivateKey privateKey, + kj::Array publicKeyBytes, + kj::StringPtr algorithmName, + bool extractable, + CryptoKeyUsageSet usages) + : CryptoKey::Impl(extractable, usages), + keyType(KeyType::PRIVATE), + algorithmName(algorithmName), + publicKeyBytes(kj::mv(publicKeyBytes)), + seed(kj::mv(seed)), + privateKey(kj::mv(privateKey)) {} + + // Public key constructor + MlDsaKey(P::PublicKey publicKey, + kj::Array publicKeyBytes, + kj::StringPtr algorithmName, + bool extractable, + CryptoKeyUsageSet usages) + : CryptoKey::Impl(extractable, usages), + keyType(KeyType::PUBLIC), + algorithmName(algorithmName), + publicKey(kj::mv(publicKey)), + publicKeyBytes(kj::mv(publicKeyBytes)) {} + + ~MlDsaKey() noexcept(false) { + KJ_IF_SOME(s, seed) { + OPENSSL_cleanse(s.begin(), s.size()); + } + KJ_IF_SOME(k, privateKey) { + OPENSSL_cleanse(&k, sizeof(k)); + } + } + + jsg::JsArrayBuffer sign(jsg::Lock& js, + SubtleCrypto::SignAlgorithm&& algorithm, + kj::ArrayPtr data) const override { + JSG_REQUIRE( + keyType == KeyType::PRIVATE, DOMInvalidAccessError, "Signing requires a private key."); + + auto [ctx, ctxLen] = getContext(js, algorithm); + + auto signature = jsg::JsArrayBuffer::create(js, P::SIGNATURE_BYTES); + JSG_REQUIRE(1 == + P::sign(signature.asArrayPtr().begin(), &KJ_ASSERT_NONNULL(privateKey), data.begin(), + data.size(), ctx, ctxLen), + DOMOperationError, "ML-DSA signing failed", tryDescribeOpensslErrors()); + + return signature; + } + + bool verify(jsg::Lock& js, + SubtleCrypto::SignAlgorithm&& algorithm, + kj::ArrayPtr signature, + kj::ArrayPtr data) const override { + JSG_REQUIRE( + keyType == KeyType::PUBLIC, DOMInvalidAccessError, "Verification requires a public key."); + + auto [ctx, ctxLen] = getContext(js, algorithm); + + auto result = P::verify(&KJ_ASSERT_NONNULL(publicKey), signature.begin(), signature.size(), + data.begin(), data.size(), ctx, ctxLen); + return result == 1; + } + + SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override { + if (format == "raw-public") { + JSG_REQUIRE(keyType == KeyType::PUBLIC, DOMInvalidAccessError, + "raw-public export requires a public key."); + return jsg::JsArrayBuffer::create(js, publicKeyBytes).addRef(js); + } else if (format == "raw-seed") { + JSG_REQUIRE(keyType == KeyType::PRIVATE, DOMInvalidAccessError, + "raw-seed export requires a private key."); + return jsg::JsArrayBuffer::create(js, KJ_ASSERT_NONNULL(seed)).addRef(js); + } else if (format == "spki") { + JSG_REQUIRE( + keyType == KeyType::PUBLIC, DOMInvalidAccessError, "SPKI export requires a public key."); + return exportSpki(js); + } else if (format == "pkcs8") { + JSG_REQUIRE(keyType == KeyType::PRIVATE, DOMInvalidAccessError, + "PKCS8 export requires a private key."); + return exportPkcs8(js); + } else if (format == "jwk") { + return exportJwk(js); + } else { + JSG_FAIL_REQUIRE( + DOMNotSupportedError, "Unrecognized export format \"", format, "\" for ML-DSA."); + } + } + + kj::StringPtr getAlgorithmName() const override { + return algorithmName; + } + + CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override { + return CryptoKey::KeyAlgorithm{algorithmName}; + } + + kj::StringPtr getType() const override { + return keyType == KeyType::PRIVATE ? "private"_kj : "public"_kj; + } + + bool equals(const Impl& other) const override { + auto* otherMlDsa = dynamic_cast*>(&other); + if (otherMlDsa == nullptr) return false; + if (keyType != otherMlDsa->keyType) return false; + if (keyType == KeyType::PUBLIC) { + return CRYPTO_memcmp(publicKeyBytes.begin(), otherMlDsa->publicKeyBytes.begin(), + publicKeyBytes.size()) == 0; + } else { + auto& thisSeed = KJ_ASSERT_NONNULL(seed); + auto& otherSeed = KJ_ASSERT_NONNULL(otherMlDsa->seed); + return CRYPTO_memcmp(thisSeed.begin(), otherSeed.begin(), thisSeed.size()) == 0; + } + } + + kj::StringPtr jsgGetMemoryName() const override { + return "MlDsaKey"; + } + size_t jsgGetMemorySelfSize() const override { + return sizeof(MlDsaKey

); + } + void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override { + tracker.trackFieldWithSize("publicKeyBytes", publicKeyBytes.size()); + KJ_IF_SOME(s, seed) { + tracker.trackFieldWithSize("seed", s.size()); + } + } + + kj::Own getPublicKey(jsg::Lock& js, CryptoKeyUsageSet usages) const override { + JSG_REQUIRE( + keyType == KeyType::PRIVATE, DOMInvalidAccessError, "getPublicKey requires a private key."); + + typename P::PublicKey pk; + JSG_REQUIRE(1 == P::publicFromPrivate(&pk, &KJ_ASSERT_NONNULL(privateKey)), + InternalDOMOperationError, "Failed to derive ", algorithmName, " public key."); + + return kj::heap>( + kj::mv(pk), kj::heapArray(publicKeyBytes), algorithmName, true, usages); + } + + // Static factory: generate key pair + static CryptoKeyPair generateKeyPair(jsg::Lock& js, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet privateKeyUsages, + CryptoKeyUsageSet publicKeyUsages) { + uint8_t encodedPublicKey[P::PUBLIC_KEY_BYTES]; + uint8_t seedBuf[MLDSA_SEED_BYTES]; + typename P::PrivateKey sk; + JSG_REQUIRE(1 == P::generateKey(encodedPublicKey, seedBuf, &sk), DOMOperationError, + "ML-DSA key generation failed", tryDescribeOpensslErrors()); + KJ_DEFER(OPENSSL_cleanse(seedBuf, sizeof(seedBuf))); + KJ_DEFER(OPENSSL_cleanse(&sk, sizeof(sk))); + + auto pkBytes = kj::heapArray(encodedPublicKey, P::PUBLIC_KEY_BYTES); + auto pkBytesCopy = kj::heapArray(encodedPublicKey, P::PUBLIC_KEY_BYTES); + auto seedArray = kj::heapArray(seedBuf, MLDSA_SEED_BYTES); + + typename P::PublicKey pk; + JSG_REQUIRE(1 == P::publicFromPrivate(&pk, &sk), InternalDOMOperationError, "Failed to derive ", + normalizedName, " public key"); + + auto privateKey = js.alloc(kj::heap>(kj::mv(seedArray), kj::mv(sk), + kj::mv(pkBytesCopy), normalizedName, extractable, privateKeyUsages)); + auto publicKey = js.alloc( + kj::heap>(kj::mv(pk), kj::mv(pkBytes), normalizedName, true, publicKeyUsages)); + + return CryptoKeyPair{.publicKey = kj::mv(publicKey), .privateKey = kj::mv(privateKey)}; + } + + // Static factory: import from raw-public + static kj::Own importRawPublic(kj::ArrayPtr keyData, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + JSG_REQUIRE(keyData.size() == P::PUBLIC_KEY_BYTES, DOMDataError, "Invalid ", normalizedName, + " public key length: expected ", P::PUBLIC_KEY_BYTES, " bytes, got ", keyData.size()); + + typename P::PublicKey pk; + CBS cbs; + CBS_init(&cbs, keyData.begin(), keyData.size()); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), DOMDataError, "Failed to parse ", normalizedName, + " public key"); + + return kj::heap>( + kj::mv(pk), kj::heapArray(keyData), normalizedName, extractable, usages); + } + + // Static factory: import from raw-seed + static kj::Own importRawSeed(kj::ArrayPtr keyData, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + JSG_REQUIRE(keyData.size() == MLDSA_SEED_BYTES, DOMDataError, "Invalid ", normalizedName, + " seed length: expected ", MLDSA_SEED_BYTES, " bytes, got ", keyData.size()); + + typename P::PrivateKey sk; + JSG_REQUIRE(1 == P::privateKeyFromSeed(&sk, keyData.begin(), keyData.size()), DOMDataError, + "Failed to regenerate ", normalizedName, " private key from seed"); + KJ_DEFER(OPENSSL_cleanse(&sk, sizeof(sk))); + + // Derive public key bytes for getPublicKey/export + typename P::PublicKey pk; + JSG_REQUIRE(1 == P::publicFromPrivate(&pk, &sk), InternalDOMOperationError, "Failed to derive ", + normalizedName, " public key from private key"); + + bssl::ScopedCBB cbb; + JSG_REQUIRE(CBB_init(cbb.get(), P::PUBLIC_KEY_BYTES) && P::marshalPublicKey(cbb.get(), &pk) && + CBB_flush(cbb.get()), + InternalDOMOperationError, "Failed to marshal ", normalizedName, " public key"); + + auto pkBytes = kj::heapArray(CBB_data(cbb.get()), CBB_len(cbb.get())); + + return kj::heap>(kj::heapArray(keyData), kj::mv(sk), kj::mv(pkBytes), + normalizedName, extractable, usages); + } + + // Static factory: import from SPKI + static kj::Own importSpki(kj::ArrayPtr keyData, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + auto oid = getOid

(); + + // Parse the SubjectPublicKeyInfo structure + CBS cbs, spki, algorithmId, oidCbs, subjectPublicKey; + CBS_init(&cbs, keyData.begin(), keyData.size()); + + JSG_REQUIRE(CBS_get_asn1(&cbs, &spki, CBS_ASN1_SEQUENCE) && CBS_len(&cbs) == 0, DOMDataError, + "Invalid SPKI structure."); + + JSG_REQUIRE(CBS_get_asn1(&spki, &algorithmId, CBS_ASN1_SEQUENCE), DOMDataError, + "Invalid SPKI AlgorithmIdentifier."); + + JSG_REQUIRE(CBS_get_asn1(&algorithmId, &oidCbs, CBS_ASN1_OBJECT), DOMDataError, + "Invalid SPKI algorithm OID."); + + // Verify OID matches + JSG_REQUIRE(CBS_len(&oidCbs) == oid.size() && + CRYPTO_memcmp(CBS_data(&oidCbs), oid.begin(), oid.size()) == 0, + DOMDataError, "SPKI algorithm OID does not match ", normalizedName, "."); + + // Parameters must be absent (per spec) + JSG_REQUIRE(CBS_len(&algorithmId) == 0, DOMDataError, "SPKI AlgorithmIdentifier for ", + normalizedName, " must not have parameters."); + + // Get subjectPublicKey BIT STRING + JSG_REQUIRE(CBS_get_asn1(&spki, &subjectPublicKey, CBS_ASN1_BITSTRING), DOMDataError, + "Invalid SPKI subjectPublicKey."); + + // BIT STRING has a leading byte for the number of unused bits (must be 0) + uint8_t unusedBits; + JSG_REQUIRE(CBS_get_u8(&subjectPublicKey, &unusedBits) && unusedBits == 0, DOMDataError, + "Invalid SPKI subjectPublicKey BIT STRING padding."); + + JSG_REQUIRE(CBS_len(&spki) == 0, DOMDataError, "Trailing data in SPKI."); + + // Parse the raw public key + typename P::PublicKey pk; + CBS pkCbs; + CBS_init(&pkCbs, CBS_data(&subjectPublicKey), CBS_len(&subjectPublicKey)); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &pkCbs), DOMDataError, "Failed to parse ", + normalizedName, " public key from SPKI."); + + auto publicKeyBytes = + kj::heapArray(CBS_data(&subjectPublicKey), CBS_len(&subjectPublicKey)); + + return kj::heap>( + kj::mv(pk), kj::mv(publicKeyBytes), normalizedName, extractable, usages); + } + + // Static factory: import from PKCS8 + static kj::Own importPkcs8(kj::ArrayPtr keyData, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + auto oid = getOid

(); + + // Parse PrivateKeyInfo + CBS cbs, privateKeyInfo, algorithmId, oidCbs, privateKeyCbs; + CBS_init(&cbs, keyData.begin(), keyData.size()); + + JSG_REQUIRE(CBS_get_asn1(&cbs, &privateKeyInfo, CBS_ASN1_SEQUENCE) && CBS_len(&cbs) == 0, + DOMDataError, "Invalid PKCS8 structure."); + + // Version must be 0 + uint64_t version; + JSG_REQUIRE(CBS_get_asn1_uint64(&privateKeyInfo, &version) && version == 0, DOMDataError, + "Invalid PKCS8 version."); + + JSG_REQUIRE(CBS_get_asn1(&privateKeyInfo, &algorithmId, CBS_ASN1_SEQUENCE), DOMDataError, + "Invalid PKCS8 AlgorithmIdentifier."); + + JSG_REQUIRE(CBS_get_asn1(&algorithmId, &oidCbs, CBS_ASN1_OBJECT), DOMDataError, + "Invalid PKCS8 algorithm OID."); + + JSG_REQUIRE(CBS_len(&oidCbs) == oid.size() && + CRYPTO_memcmp(CBS_data(&oidCbs), oid.begin(), oid.size()) == 0, + DOMDataError, "PKCS8 algorithm OID does not match ", normalizedName, "."); + + // Parameters must be absent + JSG_REQUIRE(CBS_len(&algorithmId) == 0, DOMDataError, "PKCS8 AlgorithmIdentifier for ", + normalizedName, " must not have parameters."); + + // Get the privateKey OCTET STRING + JSG_REQUIRE(CBS_get_asn1(&privateKeyInfo, &privateKeyCbs, CBS_ASN1_OCTETSTRING), DOMDataError, + "Invalid PKCS8 privateKey."); + + // Parse the inner ML-DSA-PrivateKey — only the seed format is supported: + // seed [0] IMPLICIT OCTET STRING (SIZE(32)) + CBS innerKey; + CBS_init(&innerKey, CBS_data(&privateKeyCbs), CBS_len(&privateKeyCbs)); + + kj::Array seedArray; + CBS seedCbs; + if (CBS_get_asn1(&innerKey, &seedCbs, CBS_ASN1_CONTEXT_SPECIFIC | 0) && + CBS_len(&innerKey) == 0) { + JSG_REQUIRE(CBS_len(&seedCbs) == MLDSA_SEED_BYTES, DOMDataError, "Invalid ", normalizedName, + " seed length in PKCS8."); + seedArray = kj::heapArray(CBS_data(&seedCbs), CBS_len(&seedCbs)); + } else { + JSG_FAIL_REQUIRE(DOMNotSupportedError, + "Only the seed PKCS8 private key format is supported for ", normalizedName, "."); + } + + // Regenerate private key from seed + typename P::PrivateKey sk; + JSG_REQUIRE(1 == P::privateKeyFromSeed(&sk, seedArray.begin(), seedArray.size()), DOMDataError, + "Failed to regenerate ", normalizedName, " private key from PKCS8 seed."); + KJ_DEFER(OPENSSL_cleanse(&sk, sizeof(sk))); + + // Derive public key + typename P::PublicKey pk; + JSG_REQUIRE(1 == P::publicFromPrivate(&pk, &sk), InternalDOMOperationError, "Failed to derive ", + normalizedName, " public key."); + + bssl::ScopedCBB cbb; + JSG_REQUIRE(CBB_init(cbb.get(), P::PUBLIC_KEY_BYTES) && P::marshalPublicKey(cbb.get(), &pk) && + CBB_flush(cbb.get()), + InternalDOMOperationError, "Failed to marshal ", normalizedName, " public key."); + auto pkBytes = kj::heapArray(CBB_data(cbb.get()), CBB_len(cbb.get())); + + return kj::heap>( + kj::mv(seedArray), kj::mv(sk), kj::mv(pkBytes), normalizedName, extractable, usages); + } + + // Static factory: import from JWK + static kj::Own importJwk(SubtleCrypto::JsonWebKey&& jwk, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + JSG_REQUIRE(jwk.kty == "AKP", DOMDataError, "JWK \"kty\" field must be \"AKP\" for ", + normalizedName, "."); + + auto& alg = JSG_REQUIRE_NONNULL( + jwk.alg, DOMDataError, "JWK \"alg\" field is required for ", normalizedName, "."); + JSG_REQUIRE(alg == normalizedName, DOMDataError, "JWK \"alg\" field \"", alg, + "\" does not match \"", normalizedName, "\"."); + + if (usages.size() != 0) { + KJ_IF_SOME(use, jwk.use) { + JSG_REQUIRE(use == "sig", DOMDataError, "JWK \"use\" field must be \"sig\" for ", + normalizedName, "."); + } + } + + KJ_IF_SOME(ops, jwk.key_ops) { + std::sort(ops.begin(), ops.end()); + auto duplicate = std::adjacent_find(ops.begin(), ops.end()); + JSG_REQUIRE(duplicate == ops.end(), DOMDataError, "JWK contains duplicate value \"", + *duplicate, "\", in \"key_ops\"."); + + for (const auto& usage: CryptoKeyUsageSet::singletons()) { + if (usage <= usages) { + JSG_REQUIRE(std::any_of(ops.begin(), ops.end(), + [&](const auto& op) { return op == usage.name(); }), + DOMDataError, "\"jwk\" key missing usage \"", usage.name(), "\", in \"key_ops\"."); + } + } + } + + KJ_IF_SOME(ext, jwk.ext) { + JSG_REQUIRE( + ext || !extractable, DOMDataError, "JWK \"ext\" is false but extractable is true."); + } + + KJ_IF_SOME(privField, jwk.priv) { + // Private key (seed) + auto seedResult = decodeBase64Url(kj::mv(privField)); + JSG_REQUIRE(!seedResult.hadErrors, DOMDataError, "Invalid base64url in JWK \"priv\" for ", + normalizedName, "."); + auto seedBytes = kj::mv(seedResult); + + JSG_REQUIRE(seedBytes.size() == MLDSA_SEED_BYTES, DOMDataError, + "Invalid JWK \"priv\" length for ", normalizedName, "."); + + // Regenerate private key from seed + typename P::PrivateKey sk; + JSG_REQUIRE(1 == P::privateKeyFromSeed(&sk, seedBytes.begin(), seedBytes.size()), + DOMDataError, "Failed to regenerate ", normalizedName, " private key from JWK seed."); + KJ_DEFER(OPENSSL_cleanse(&sk, sizeof(sk))); + + // Derive public key + typename P::PublicKey pk; + JSG_REQUIRE(1 == P::publicFromPrivate(&pk, &sk), InternalDOMOperationError, + "Failed to derive ", normalizedName, " public key."); + + bssl::ScopedCBB cbb; + JSG_REQUIRE(CBB_init(cbb.get(), P::PUBLIC_KEY_BYTES) && P::marshalPublicKey(cbb.get(), &pk) && + CBB_flush(cbb.get()), + InternalDOMOperationError, "Failed to marshal ", normalizedName, " public key."); + auto pkBytes = kj::heapArray(CBB_data(cbb.get()), CBB_len(cbb.get())); + + // If pub field is present, verify it matches + KJ_IF_SOME(pubField, jwk.pub) { + auto pubResult = decodeBase64Url(kj::mv(pubField)); + JSG_REQUIRE(!pubResult.hadErrors, DOMDataError, "Invalid base64url in JWK \"pub\" for ", + normalizedName, "."); + JSG_REQUIRE(pubResult.size() == pkBytes.size() && + CRYPTO_memcmp(pubResult.begin(), pkBytes.begin(), pkBytes.size()) == 0, + DOMDataError, "JWK \"pub\" does not match the public key derived from \"priv\"."); + } + + return kj::heap>(kj::heapArray(seedBytes.asPtr()), kj::mv(sk), + kj::mv(pkBytes), normalizedName, extractable, usages); + } else { + // Public key only + auto& pubField = JSG_REQUIRE_NONNULL( + jwk.pub, DOMDataError, "JWK for ", normalizedName, " must have \"pub\" field."); + + auto pubResult = decodeBase64Url(kj::mv(pubField)); + JSG_REQUIRE(!pubResult.hadErrors, DOMDataError, "Invalid base64url in JWK \"pub\" for ", + normalizedName, "."); + + JSG_REQUIRE(pubResult.size() == P::PUBLIC_KEY_BYTES, DOMDataError, + "Invalid JWK \"pub\" length for ", normalizedName, "."); + + typename P::PublicKey pk; + CBS cbs; + CBS_init(&cbs, pubResult.begin(), pubResult.size()); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), DOMDataError, "Failed to parse ", + normalizedName, " public key from JWK."); + + return kj::heap>(kj::mv(pk), kj::heapArray(pubResult.asPtr()), + normalizedName, extractable, usages); + } + } + + private: + enum class KeyType { PUBLIC, PRIVATE }; + KeyType keyType; + kj::StringPtr algorithmName; + + // Public key data. The parsed public key is present only for public CryptoKeys; private keys keep + // publicKeyBytes for export and getPublicKey(). + kj::Maybe publicKey; + kj::Array publicKeyBytes; + + // Private key data (present only for private keys) + kj::Maybe> seed; + kj::Maybe privateKey; + + SubtleCrypto::ExportKeyData exportSpki(jsg::Lock& js) const { + auto oid = getOid

(); + + // Manually build the SPKI + bssl::ScopedCBB cbb; + CBB spki, algId, oidCbb, bitString; + JSG_REQUIRE(CBB_init(cbb.get(), P::PUBLIC_KEY_BYTES + 64), InternalDOMOperationError, + "Failed to init SPKI CBB."); + JSG_REQUIRE(CBB_add_asn1(cbb.get(), &spki, CBS_ASN1_SEQUENCE), InternalDOMOperationError, + "Failed to add SPKI SEQUENCE."); + JSG_REQUIRE(CBB_add_asn1(&spki, &algId, CBS_ASN1_SEQUENCE), InternalDOMOperationError, + "Failed to add AlgorithmIdentifier SEQUENCE."); + JSG_REQUIRE(CBB_add_asn1(&algId, &oidCbb, CBS_ASN1_OBJECT), InternalDOMOperationError, + "Failed to add OID."); + JSG_REQUIRE(CBB_add_bytes(&oidCbb, oid.begin(), oid.size()), InternalDOMOperationError, + "Failed to add OID bytes."); + JSG_REQUIRE( + CBB_flush(&algId), InternalDOMOperationError, "Failed to flush AlgorithmIdentifier."); + JSG_REQUIRE(CBB_add_asn1(&spki, &bitString, CBS_ASN1_BITSTRING), InternalDOMOperationError, + "Failed to add BIT STRING."); + JSG_REQUIRE( + CBB_add_u8(&bitString, 0), InternalDOMOperationError, "Failed to add unused bits byte."); + JSG_REQUIRE(CBB_add_bytes(&bitString, publicKeyBytes.begin(), publicKeyBytes.size()), + InternalDOMOperationError, "Failed to add public key bytes."); + JSG_REQUIRE(CBB_flush(cbb.get()), InternalDOMOperationError, "Failed to flush SPKI."); + + uint8_t* der = nullptr; + size_t derLen; + JSG_REQUIRE(CBB_finish(cbb.get(), &der, &derLen), InternalDOMOperationError, + "Failed to finish SPKI encoding."); + KJ_DEFER(OPENSSL_free(der)); + + return jsg::JsArrayBuffer::create(js, kj::arrayPtr(der, derLen)).addRef(js); + } + + SubtleCrypto::ExportKeyData exportPkcs8(jsg::Lock& js) const { + auto oid = getOid

(); + auto& seedData = KJ_ASSERT_NONNULL(seed); + + bssl::ScopedCBB cbb; + CBB pkcs8, algId, oidCbb, privateKeyOctet, innerPrivateKey; + JSG_REQUIRE(CBB_init(cbb.get(), MLDSA_SEED_BYTES + 64), InternalDOMOperationError, + "Failed to init PKCS8 CBB."); + JSG_REQUIRE(CBB_add_asn1(cbb.get(), &pkcs8, CBS_ASN1_SEQUENCE), InternalDOMOperationError, + "Failed to add PKCS8 SEQUENCE."); + + // Version 0 + JSG_REQUIRE( + CBB_add_asn1_uint64(&pkcs8, 0), InternalDOMOperationError, "Failed to add PKCS8 version."); + + // AlgorithmIdentifier + JSG_REQUIRE(CBB_add_asn1(&pkcs8, &algId, CBS_ASN1_SEQUENCE), InternalDOMOperationError, + "Failed to add AlgorithmIdentifier."); + JSG_REQUIRE(CBB_add_asn1(&algId, &oidCbb, CBS_ASN1_OBJECT), InternalDOMOperationError, + "Failed to add OID."); + JSG_REQUIRE(CBB_add_bytes(&oidCbb, oid.begin(), oid.size()), InternalDOMOperationError, + "Failed to add OID bytes."); + JSG_REQUIRE( + CBB_flush(&algId), InternalDOMOperationError, "Failed to flush AlgorithmIdentifier."); + + // PrivateKey: OCTET STRING containing context-specific [0] tag with seed + JSG_REQUIRE(CBB_add_asn1(&pkcs8, &privateKeyOctet, CBS_ASN1_OCTETSTRING), + InternalDOMOperationError, "Failed to add privateKey OCTET STRING."); + JSG_REQUIRE(CBB_add_asn1(&privateKeyOctet, &innerPrivateKey, CBS_ASN1_CONTEXT_SPECIFIC | 0), + InternalDOMOperationError, "Failed to add seed context-specific tag."); + JSG_REQUIRE(CBB_add_bytes(&innerPrivateKey, seedData.begin(), seedData.size()), + InternalDOMOperationError, "Failed to add seed bytes."); + JSG_REQUIRE(CBB_flush(cbb.get()), InternalDOMOperationError, "Failed to flush PKCS8."); + + uint8_t* der = nullptr; + size_t derLen; + JSG_REQUIRE(CBB_finish(cbb.get(), &der, &derLen), InternalDOMOperationError, + "Failed to finish PKCS8 encoding."); + KJ_DEFER(OPENSSL_free(der)); + + return jsg::JsArrayBuffer::create(js, kj::arrayPtr(der, derLen)).addRef(js); + } + + SubtleCrypto::ExportKeyData exportJwk(jsg::Lock& js) const { + SubtleCrypto::JsonWebKey jwk; + jwk.kty = kj::str("AKP"); + jwk.alg = kj::str(algorithmName); + jwk.ext = isExtractable(); + jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); }); + + jwk.pub = kj::encodeBase64Url(publicKeyBytes); + + if (keyType == KeyType::PRIVATE) { + auto& seedData = KJ_ASSERT_NONNULL(seed); + jwk.priv = kj::encodeBase64Url(seedData); + } + + return jwk; + } +}; + +// Dispatch based on algorithm name +template +auto dispatchMlDsa(kj::StringPtr normalizedName, Func&& func) { + if (normalizedName == "ML-DSA-44") { + return func(MlDsa44Params{}); + } else if (normalizedName == "ML-DSA-65") { + return func(MlDsa65Params{}); + } else if (normalizedName == "ML-DSA-87") { + return func(MlDsa87Params{}); + } else { + JSG_FAIL_REQUIRE( + DOMNotSupportedError, "Unsupported ML-DSA algorithm \"", normalizedName, "\"."); + } +} + +} // namespace + +kj::OneOf, CryptoKeyPair> CryptoKey::Impl::generateMlDsa(jsg::Lock& js, + kj::StringPtr normalizedName, + SubtleCrypto::GenerateKeyAlgorithm&& algorithm, + bool extractable, + kj::ArrayPtr keyUsages) { + auto usages = CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::generate, + keyUsages, CryptoKeyUsageSet::sign() | CryptoKeyUsageSet::verify()); + auto privateKeyUsages = usages & CryptoKeyUsageSet::privateKeyMask(); + auto publicKeyUsages = usages & CryptoKeyUsageSet::publicKeyMask(); + + return dispatchMlDsa( + normalizedName, [&](auto params) -> kj::OneOf, CryptoKeyPair> { + using P = decltype(params); + return MlDsaKey

::generateKeyPair( + js, normalizedName, extractable, privateKeyUsages, publicKeyUsages); + }); +} + +kj::Own CryptoKey::Impl::importMlDsa(jsg::Lock& js, + kj::StringPtr normalizedName, + kj::StringPtr format, + SubtleCrypto::ImportKeyData keyData, + SubtleCrypto::ImportKeyAlgorithm&& algorithm, + bool extractable, + kj::ArrayPtr keyUsages) { + return dispatchMlDsa(normalizedName, [&](auto params) -> kj::Own { + using P = decltype(params); + + if (format == "raw-public") { + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPublic, keyUsages, CryptoKeyUsageSet::verify()); + auto& source = JSG_REQUIRE_NONNULL(keyData.tryGet>(), + DOMDataError, "Import data for raw-public must be a buffer."); + auto handle = source.getHandle(js); + return MlDsaKey

::importRawPublic(handle.asArrayPtr(), normalizedName, extractable, usages); + } else if (format == "raw-seed") { + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPrivate, keyUsages, CryptoKeyUsageSet::sign()); + auto& source = JSG_REQUIRE_NONNULL(keyData.tryGet>(), + DOMDataError, "Import data for raw-seed must be a buffer."); + auto handle = source.getHandle(js); + return MlDsaKey

::importRawSeed(handle.asArrayPtr(), normalizedName, extractable, usages); + } else if (format == "spki") { + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPublic, keyUsages, CryptoKeyUsageSet::verify()); + auto& source = JSG_REQUIRE_NONNULL(keyData.tryGet>(), + DOMDataError, "Import data for spki must be a buffer."); + auto handle = source.getHandle(js); + return MlDsaKey

::importSpki(handle.asArrayPtr(), normalizedName, extractable, usages); + } else if (format == "pkcs8") { + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPrivate, keyUsages, CryptoKeyUsageSet::sign()); + auto& source = JSG_REQUIRE_NONNULL(keyData.tryGet>(), + DOMDataError, "Import data for pkcs8 must be a buffer."); + auto handle = source.getHandle(js); + return MlDsaKey

::importPkcs8(handle.asArrayPtr(), normalizedName, extractable, usages); + } else if (format == "jwk") { + auto& jwk = JSG_REQUIRE_NONNULL(keyData.tryGet(), DOMDataError, + "Import data for jwk must be a JsonWebKey."); + // Determine usages based on key type (priv field present = private key) + CryptoKeyUsageSet usages; + if (jwk.priv != kj::none) { + usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPrivate, keyUsages, CryptoKeyUsageSet::sign()); + } else { + usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPublic, keyUsages, CryptoKeyUsageSet::verify()); + } + return MlDsaKey

::importJwk(kj::mv(jwk), normalizedName, extractable, usages); + } else { + JSG_FAIL_REQUIRE( + DOMNotSupportedError, "Unsupported import format \"", format, "\" for ML-DSA."); + } + }); +} + +} // namespace workerd::api diff --git a/src/workerd/api/crypto/mlkem.c++ b/src/workerd/api/crypto/mlkem.c++ new file mode 100644 index 00000000000..66fd6d511ea --- /dev/null +++ b/src/workerd/api/crypto/mlkem.c++ @@ -0,0 +1,735 @@ +#include "impl.h" + +#include +#include + +#include + +namespace workerd::api { +namespace { + +// Traits structs for each ML-KEM parameter set. +struct MlKem768Params { + using PrivateKey = MLKEM768_private_key; + using PublicKey = MLKEM768_public_key; + static constexpr size_t PUBLIC_KEY_BYTES = MLKEM768_PUBLIC_KEY_BYTES; + static constexpr size_t CIPHERTEXT_BYTES = MLKEM768_CIPHERTEXT_BYTES; + + static void generateKey(uint8_t* outPk, uint8_t* outSeed, PrivateKey* outSk) { + MLKEM768_generate_key(outPk, outSeed, outSk); + } + static int privateKeyFromSeed(PrivateKey* outSk, const uint8_t* seed, size_t seedLen) { + return MLKEM768_private_key_from_seed(outSk, seed, seedLen); + } + static void publicFromPrivate(PublicKey* outPk, const PrivateKey* sk) { + MLKEM768_public_from_private(outPk, sk); + } + static void encap(uint8_t* outCiphertext, uint8_t* outSharedSecret, const PublicKey* pk) { + MLKEM768_encap(outCiphertext, outSharedSecret, pk); + } + static int decap(uint8_t* outSharedSecret, + const uint8_t* ciphertext, + size_t ciphertextLen, + const PrivateKey* sk) { + return MLKEM768_decap(outSharedSecret, ciphertext, ciphertextLen, sk); + } + static int marshalPublicKey(CBB* out, const PublicKey* pk) { + return MLKEM768_marshal_public_key(out, pk); + } + static int parsePublicKey(PublicKey* outPk, CBS* in) { + return MLKEM768_parse_public_key(outPk, in); + } +}; + +struct MlKem1024Params { + using PrivateKey = MLKEM1024_private_key; + using PublicKey = MLKEM1024_public_key; + static constexpr size_t PUBLIC_KEY_BYTES = MLKEM1024_PUBLIC_KEY_BYTES; + static constexpr size_t CIPHERTEXT_BYTES = MLKEM1024_CIPHERTEXT_BYTES; + + static void generateKey(uint8_t* outPk, uint8_t* outSeed, PrivateKey* outSk) { + MLKEM1024_generate_key(outPk, outSeed, outSk); + } + static int privateKeyFromSeed(PrivateKey* outSk, const uint8_t* seed, size_t seedLen) { + return MLKEM1024_private_key_from_seed(outSk, seed, seedLen); + } + static void publicFromPrivate(PublicKey* outPk, const PrivateKey* sk) { + MLKEM1024_public_from_private(outPk, sk); + } + static void encap(uint8_t* outCiphertext, uint8_t* outSharedSecret, const PublicKey* pk) { + MLKEM1024_encap(outCiphertext, outSharedSecret, pk); + } + static int decap(uint8_t* outSharedSecret, + const uint8_t* ciphertext, + size_t ciphertextLen, + const PrivateKey* sk) { + return MLKEM1024_decap(outSharedSecret, ciphertext, ciphertextLen, sk); + } + static int marshalPublicKey(CBB* out, const PublicKey* pk) { + return MLKEM1024_marshal_public_key(out, pk); + } + static int parsePublicKey(PublicKey* outPk, CBS* in) { + return MLKEM1024_parse_public_key(outPk, in); + } +}; + +// OIDs for ML-KEM algorithms (from NIST CSOR) +// id-alg-ml-kem-768: 2.16.840.1.101.3.4.4.2 +constexpr uint8_t OID_ML_KEM_768[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x02}; +// id-alg-ml-kem-1024: 2.16.840.1.101.3.4.4.3 +constexpr uint8_t OID_ML_KEM_1024[] = {0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x04, 0x03}; + +template +kj::ArrayPtr getOid() { + if constexpr (P::PUBLIC_KEY_BYTES == MLKEM768_PUBLIC_KEY_BYTES) { + return kj::arrayPtr(OID_ML_KEM_768, sizeof(OID_ML_KEM_768)); + } else { + return kj::arrayPtr(OID_ML_KEM_1024, sizeof(OID_ML_KEM_1024)); + } +} + +// Allowed usages for ML-KEM public keys. +const auto ML_KEM_PUBLIC_USAGES = + CryptoKeyUsageSet::encapsulateKey() | CryptoKeyUsageSet::encapsulateBits(); +// Allowed usages for ML-KEM private keys. +const auto ML_KEM_PRIVATE_USAGES = + CryptoKeyUsageSet::decapsulateKey() | CryptoKeyUsageSet::decapsulateBits(); + +template +class MlKemKey final: public CryptoKey::Impl { + public: + // Private key constructor + MlKemKey(kj::Array seed, + P::PrivateKey privateKey, + kj::Array publicKeyBytes, + kj::StringPtr algorithmName, + bool extractable, + CryptoKeyUsageSet usages) + : CryptoKey::Impl(extractable, usages), + keyType(KeyType::PRIVATE), + algorithmName(algorithmName), + publicKeyBytes(kj::mv(publicKeyBytes)), + seed(kj::mv(seed)), + privateKey(kj::mv(privateKey)) {} + + // Public key constructor + MlKemKey(P::PublicKey publicKey, + kj::Array publicKeyBytes, + kj::StringPtr algorithmName, + bool extractable, + CryptoKeyUsageSet usages) + : CryptoKey::Impl(extractable, usages), + keyType(KeyType::PUBLIC), + algorithmName(algorithmName), + publicKey(kj::mv(publicKey)), + publicKeyBytes(kj::mv(publicKeyBytes)) {} + + ~MlKemKey() noexcept(false) { + KJ_IF_SOME(s, seed) { + OPENSSL_cleanse(s.begin(), s.size()); + } + KJ_IF_SOME(k, privateKey) { + OPENSSL_cleanse(&k, sizeof(k)); + } + } + + std::pair encapsulate(jsg::Lock& js) const override { + JSG_REQUIRE( + keyType == KeyType::PUBLIC, DOMInvalidAccessError, "Encapsulation requires a public key."); + + auto sharedSecret = jsg::JsArrayBuffer::create(js, MLKEM_SHARED_SECRET_BYTES); + auto ciphertext = jsg::JsArrayBuffer::create(js, P::CIPHERTEXT_BYTES); + P::encap(ciphertext.asArrayPtr().begin(), sharedSecret.asArrayPtr().begin(), + &KJ_ASSERT_NONNULL(publicKey)); + + return {kj::mv(sharedSecret), kj::mv(ciphertext)}; + } + + jsg::JsArrayBuffer decapsulate( + jsg::Lock& js, kj::ArrayPtr ciphertext) const override { + JSG_REQUIRE(keyType == KeyType::PRIVATE, DOMInvalidAccessError, + "Decapsulation requires a private key."); + + JSG_REQUIRE(ciphertext.size() == P::CIPHERTEXT_BYTES, DOMOperationError, + "Invalid ciphertext length for ", algorithmName, ": expected ", P::CIPHERTEXT_BYTES, + " bytes, got ", ciphertext.size()); + + auto sharedSecret = jsg::JsArrayBuffer::create(js, MLKEM_SHARED_SECRET_BYTES); + JSG_REQUIRE(1 == + P::decap(sharedSecret.asArrayPtr().begin(), ciphertext.begin(), ciphertext.size(), + &KJ_ASSERT_NONNULL(privateKey)), + DOMOperationError, "ML-KEM decapsulation failed", tryDescribeOpensslErrors()); + + return sharedSecret; + } + + SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override { + if (format == "raw-public") { + JSG_REQUIRE(keyType == KeyType::PUBLIC, DOMInvalidAccessError, + "raw-public export requires a public key."); + return jsg::JsArrayBuffer::create(js, publicKeyBytes).addRef(js); + } else if (format == "raw-seed") { + JSG_REQUIRE(keyType == KeyType::PRIVATE, DOMInvalidAccessError, + "raw-seed export requires a private key."); + return jsg::JsArrayBuffer::create(js, KJ_ASSERT_NONNULL(seed)).addRef(js); + } else if (format == "spki") { + JSG_REQUIRE( + keyType == KeyType::PUBLIC, DOMInvalidAccessError, "SPKI export requires a public key."); + return exportSpki(js); + } else if (format == "pkcs8") { + JSG_REQUIRE(keyType == KeyType::PRIVATE, DOMInvalidAccessError, + "PKCS8 export requires a private key."); + return exportPkcs8(js); + } else if (format == "jwk") { + return exportJwk(js); + } else { + JSG_FAIL_REQUIRE( + DOMNotSupportedError, "Unrecognized export format \"", format, "\" for ML-KEM."); + } + } + + kj::StringPtr getAlgorithmName() const override { + return algorithmName; + } + + CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override { + return CryptoKey::KeyAlgorithm{algorithmName}; + } + + kj::StringPtr getType() const override { + return keyType == KeyType::PRIVATE ? "private"_kj : "public"_kj; + } + + bool equals(const Impl& other) const override { + auto* otherMlKem = dynamic_cast*>(&other); + if (otherMlKem == nullptr) return false; + if (keyType != otherMlKem->keyType) return false; + if (keyType == KeyType::PUBLIC) { + return CRYPTO_memcmp(publicKeyBytes.begin(), otherMlKem->publicKeyBytes.begin(), + publicKeyBytes.size()) == 0; + } else { + auto& thisSeed = KJ_ASSERT_NONNULL(seed); + auto& otherSeed = KJ_ASSERT_NONNULL(otherMlKem->seed); + return CRYPTO_memcmp(thisSeed.begin(), otherSeed.begin(), thisSeed.size()) == 0; + } + } + + kj::StringPtr jsgGetMemoryName() const override { + return "MlKemKey"; + } + size_t jsgGetMemorySelfSize() const override { + return sizeof(MlKemKey

); + } + void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override { + tracker.trackFieldWithSize("publicKeyBytes", publicKeyBytes.size()); + KJ_IF_SOME(s, seed) { + tracker.trackFieldWithSize("seed", s.size()); + } + } + + kj::Own getPublicKey(jsg::Lock& js, CryptoKeyUsageSet usages) const override { + JSG_REQUIRE( + keyType == KeyType::PRIVATE, DOMInvalidAccessError, "getPublicKey requires a private key."); + + typename P::PublicKey pk; + P::publicFromPrivate(&pk, &KJ_ASSERT_NONNULL(privateKey)); + + return kj::heap>( + kj::mv(pk), kj::heapArray(publicKeyBytes), algorithmName, true, usages); + } + + // Static factory: generate key pair + static CryptoKeyPair generateKeyPair(jsg::Lock& js, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet privateKeyUsages, + CryptoKeyUsageSet publicKeyUsages) { + uint8_t encodedPublicKey[P::PUBLIC_KEY_BYTES]; + uint8_t seedBuf[MLKEM_SEED_BYTES]; + typename P::PrivateKey sk; + P::generateKey(encodedPublicKey, seedBuf, &sk); + KJ_DEFER(OPENSSL_cleanse(seedBuf, sizeof(seedBuf))); + KJ_DEFER(OPENSSL_cleanse(&sk, sizeof(sk))); + + auto pkBytes = kj::heapArray(encodedPublicKey, P::PUBLIC_KEY_BYTES); + auto pkBytesCopy = kj::heapArray(encodedPublicKey, P::PUBLIC_KEY_BYTES); + auto seedArray = kj::heapArray(seedBuf, MLKEM_SEED_BYTES); + + typename P::PublicKey pk; + P::publicFromPrivate(&pk, &sk); + + auto privateKey = js.alloc(kj::heap>(kj::mv(seedArray), kj::mv(sk), + kj::mv(pkBytesCopy), normalizedName, extractable, privateKeyUsages)); + auto publicKey = js.alloc( + kj::heap>(kj::mv(pk), kj::mv(pkBytes), normalizedName, true, publicKeyUsages)); + + return CryptoKeyPair{.publicKey = kj::mv(publicKey), .privateKey = kj::mv(privateKey)}; + } + + // Static factory: import from raw-public + static kj::Own importRawPublic(kj::ArrayPtr keyData, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + JSG_REQUIRE(keyData.size() == P::PUBLIC_KEY_BYTES, DOMDataError, "Invalid ", normalizedName, + " public key length: expected ", P::PUBLIC_KEY_BYTES, " bytes, got ", keyData.size()); + + typename P::PublicKey pk; + CBS cbs; + CBS_init(&cbs, keyData.begin(), keyData.size()); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), DOMDataError, "Failed to parse ", normalizedName, + " public key"); + + return kj::heap>( + kj::mv(pk), kj::heapArray(keyData), normalizedName, extractable, usages); + } + + // Static factory: import from raw-seed + static kj::Own importRawSeed(kj::ArrayPtr keyData, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + JSG_REQUIRE(keyData.size() == MLKEM_SEED_BYTES, DOMDataError, "Invalid ", normalizedName, + " seed length: expected ", MLKEM_SEED_BYTES, " bytes, got ", keyData.size()); + + typename P::PrivateKey sk; + JSG_REQUIRE(1 == P::privateKeyFromSeed(&sk, keyData.begin(), keyData.size()), DOMDataError, + "Failed to regenerate ", normalizedName, " private key from seed"); + KJ_DEFER(OPENSSL_cleanse(&sk, sizeof(sk))); + + // Derive public key bytes for export + typename P::PublicKey pk; + P::publicFromPrivate(&pk, &sk); + + bssl::ScopedCBB cbb; + JSG_REQUIRE(CBB_init(cbb.get(), P::PUBLIC_KEY_BYTES) && P::marshalPublicKey(cbb.get(), &pk) && + CBB_flush(cbb.get()), + InternalDOMOperationError, "Failed to marshal ", normalizedName, " public key"); + + auto pkBytes = kj::heapArray(CBB_data(cbb.get()), CBB_len(cbb.get())); + + return kj::heap>(kj::heapArray(keyData), kj::mv(sk), kj::mv(pkBytes), + normalizedName, extractable, usages); + } + + // Static factory: import from SPKI + static kj::Own importSpki(kj::ArrayPtr keyData, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + auto oid = getOid

(); + + CBS cbs, spki, algorithmId, oidCbs, subjectPublicKey; + CBS_init(&cbs, keyData.begin(), keyData.size()); + + JSG_REQUIRE(CBS_get_asn1(&cbs, &spki, CBS_ASN1_SEQUENCE) && CBS_len(&cbs) == 0, DOMDataError, + "Invalid SPKI structure."); + + JSG_REQUIRE(CBS_get_asn1(&spki, &algorithmId, CBS_ASN1_SEQUENCE), DOMDataError, + "Invalid SPKI AlgorithmIdentifier."); + + JSG_REQUIRE(CBS_get_asn1(&algorithmId, &oidCbs, CBS_ASN1_OBJECT), DOMDataError, + "Invalid SPKI algorithm OID."); + + // Verify OID matches + JSG_REQUIRE(CBS_len(&oidCbs) == oid.size() && + CRYPTO_memcmp(CBS_data(&oidCbs), oid.begin(), oid.size()) == 0, + DOMDataError, "SPKI algorithm OID does not match ", normalizedName, "."); + + // Parameters must be absent (per spec) + JSG_REQUIRE(CBS_len(&algorithmId) == 0, DOMDataError, "SPKI AlgorithmIdentifier for ", + normalizedName, " must not have parameters."); + + // Get subjectPublicKey BIT STRING + JSG_REQUIRE(CBS_get_asn1(&spki, &subjectPublicKey, CBS_ASN1_BITSTRING), DOMDataError, + "Invalid SPKI subjectPublicKey."); + + // BIT STRING has a leading byte for the number of unused bits (must be 0) + uint8_t unusedBits; + JSG_REQUIRE(CBS_get_u8(&subjectPublicKey, &unusedBits) && unusedBits == 0, DOMDataError, + "Invalid SPKI subjectPublicKey BIT STRING padding."); + + JSG_REQUIRE(CBS_len(&spki) == 0, DOMDataError, "Trailing data in SPKI."); + + // Parse the raw public key + typename P::PublicKey pk; + CBS pkCbs; + CBS_init(&pkCbs, CBS_data(&subjectPublicKey), CBS_len(&subjectPublicKey)); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &pkCbs), DOMDataError, "Failed to parse ", + normalizedName, " public key from SPKI."); + + auto rawPkBytes = + kj::heapArray(CBS_data(&subjectPublicKey), CBS_len(&subjectPublicKey)); + + return kj::heap>( + kj::mv(pk), kj::mv(rawPkBytes), normalizedName, extractable, usages); + } + + // Static factory: import from PKCS8 + static kj::Own importPkcs8(kj::ArrayPtr keyData, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + auto oid = getOid

(); + + CBS cbs, privateKeyInfo, algorithmId, oidCbs, privateKeyCbs; + CBS_init(&cbs, keyData.begin(), keyData.size()); + + JSG_REQUIRE(CBS_get_asn1(&cbs, &privateKeyInfo, CBS_ASN1_SEQUENCE) && CBS_len(&cbs) == 0, + DOMDataError, "Invalid PKCS8 structure."); + + // Version must be 0 + uint64_t version; + JSG_REQUIRE(CBS_get_asn1_uint64(&privateKeyInfo, &version) && version == 0, DOMDataError, + "Invalid PKCS8 version."); + + JSG_REQUIRE(CBS_get_asn1(&privateKeyInfo, &algorithmId, CBS_ASN1_SEQUENCE), DOMDataError, + "Invalid PKCS8 AlgorithmIdentifier."); + + JSG_REQUIRE(CBS_get_asn1(&algorithmId, &oidCbs, CBS_ASN1_OBJECT), DOMDataError, + "Invalid PKCS8 algorithm OID."); + + JSG_REQUIRE(CBS_len(&oidCbs) == oid.size() && + CRYPTO_memcmp(CBS_data(&oidCbs), oid.begin(), oid.size()) == 0, + DOMDataError, "PKCS8 algorithm OID does not match ", normalizedName, "."); + + // Parameters must be absent + JSG_REQUIRE(CBS_len(&algorithmId) == 0, DOMDataError, "PKCS8 AlgorithmIdentifier for ", + normalizedName, " must not have parameters."); + + // Get the privateKey OCTET STRING + JSG_REQUIRE(CBS_get_asn1(&privateKeyInfo, &privateKeyCbs, CBS_ASN1_OCTETSTRING), DOMDataError, + "Invalid PKCS8 privateKey."); + + // Parse the inner ML-KEM-PrivateKey — only the seed format is supported: + // seed [0] IMPLICIT OCTET STRING (SIZE(64)) + CBS innerKey; + CBS_init(&innerKey, CBS_data(&privateKeyCbs), CBS_len(&privateKeyCbs)); + + kj::Array seedArray; + CBS seedCbs; + if (CBS_get_asn1(&innerKey, &seedCbs, CBS_ASN1_CONTEXT_SPECIFIC | 0) && + CBS_len(&innerKey) == 0) { + JSG_REQUIRE(CBS_len(&seedCbs) == MLKEM_SEED_BYTES, DOMDataError, "Invalid ", normalizedName, + " seed length in PKCS8."); + seedArray = kj::heapArray(CBS_data(&seedCbs), CBS_len(&seedCbs)); + } else { + JSG_FAIL_REQUIRE(DOMNotSupportedError, + "Only the seed PKCS8 private key format is supported for ", normalizedName, "."); + } + + // Regenerate private key from seed + typename P::PrivateKey sk; + JSG_REQUIRE(1 == P::privateKeyFromSeed(&sk, seedArray.begin(), seedArray.size()), DOMDataError, + "Failed to regenerate ", normalizedName, " private key from PKCS8 seed."); + KJ_DEFER(OPENSSL_cleanse(&sk, sizeof(sk))); + + // Derive public key + typename P::PublicKey pk; + P::publicFromPrivate(&pk, &sk); + + bssl::ScopedCBB cbb; + JSG_REQUIRE(CBB_init(cbb.get(), P::PUBLIC_KEY_BYTES) && P::marshalPublicKey(cbb.get(), &pk) && + CBB_flush(cbb.get()), + InternalDOMOperationError, "Failed to marshal ", normalizedName, " public key."); + auto pkBytes = kj::heapArray(CBB_data(cbb.get()), CBB_len(cbb.get())); + + return kj::heap>( + kj::mv(seedArray), kj::mv(sk), kj::mv(pkBytes), normalizedName, extractable, usages); + } + + // Static factory: import from JWK + static kj::Own importJwk(SubtleCrypto::JsonWebKey&& jwk, + kj::StringPtr normalizedName, + bool extractable, + CryptoKeyUsageSet usages) { + JSG_REQUIRE(jwk.kty == "AKP", DOMDataError, "JWK \"kty\" field must be \"AKP\" for ", + normalizedName, "."); + + auto& alg = JSG_REQUIRE_NONNULL( + jwk.alg, DOMDataError, "JWK \"alg\" field is required for ", normalizedName, "."); + JSG_REQUIRE(alg == normalizedName, DOMDataError, "JWK \"alg\" field \"", alg, + "\" does not match \"", normalizedName, "\"."); + + if (usages.size() != 0) { + KJ_IF_SOME(use, jwk.use) { + JSG_REQUIRE(use == "enc", DOMDataError, "JWK \"use\" field must be \"enc\" for ", + normalizedName, "."); + } + } + + KJ_IF_SOME(ops, jwk.key_ops) { + std::sort(ops.begin(), ops.end()); + auto duplicate = std::adjacent_find(ops.begin(), ops.end()); + JSG_REQUIRE(duplicate == ops.end(), DOMDataError, "JWK contains duplicate value \"", + *duplicate, "\", in \"key_ops\"."); + + for (const auto& usage: CryptoKeyUsageSet::singletons()) { + if (usage <= usages) { + JSG_REQUIRE(std::any_of(ops.begin(), ops.end(), + [&](const auto& op) { return op == usage.name(); }), + DOMDataError, "\"jwk\" key missing usage \"", usage.name(), "\", in \"key_ops\"."); + } + } + } + + KJ_IF_SOME(ext, jwk.ext) { + JSG_REQUIRE( + ext || !extractable, DOMDataError, "JWK \"ext\" is false but extractable is true."); + } + + KJ_IF_SOME(privField, jwk.priv) { + auto seedResult = decodeBase64Url(kj::mv(privField)); + JSG_REQUIRE(!seedResult.hadErrors, DOMDataError, "Invalid base64url in JWK \"priv\" for ", + normalizedName, "."); + auto seedBytes = kj::mv(seedResult); + + JSG_REQUIRE(seedBytes.size() == MLKEM_SEED_BYTES, DOMDataError, + "Invalid JWK \"priv\" length for ", normalizedName, "."); + + typename P::PrivateKey sk; + JSG_REQUIRE(1 == P::privateKeyFromSeed(&sk, seedBytes.begin(), seedBytes.size()), + DOMDataError, "Failed to regenerate ", normalizedName, " private key from JWK seed."); + KJ_DEFER(OPENSSL_cleanse(&sk, sizeof(sk))); + + typename P::PublicKey pk; + P::publicFromPrivate(&pk, &sk); + + bssl::ScopedCBB cbb; + JSG_REQUIRE(CBB_init(cbb.get(), P::PUBLIC_KEY_BYTES) && P::marshalPublicKey(cbb.get(), &pk) && + CBB_flush(cbb.get()), + InternalDOMOperationError, "Failed to marshal ", normalizedName, " public key."); + auto pkBytes = kj::heapArray(CBB_data(cbb.get()), CBB_len(cbb.get())); + + auto& pubField = JSG_REQUIRE_NONNULL( + jwk.pub, DOMDataError, "JWK for ", normalizedName, " private keys must have \"pub\"."); + auto pubResult = decodeBase64Url(kj::mv(pubField)); + JSG_REQUIRE(!pubResult.hadErrors, DOMDataError, "Invalid base64url in JWK \"pub\" for ", + normalizedName, "."); + JSG_REQUIRE(pubResult.size() == pkBytes.size() && + CRYPTO_memcmp(pubResult.begin(), pkBytes.begin(), pkBytes.size()) == 0, + DOMDataError, "JWK \"pub\" does not match the public key derived from \"priv\"."); + + return kj::heap>(kj::heapArray(seedBytes.asPtr()), kj::mv(sk), + kj::mv(pkBytes), normalizedName, extractable, usages); + } else { + auto& pubField = JSG_REQUIRE_NONNULL( + jwk.pub, DOMDataError, "JWK for ", normalizedName, " must have \"pub\" field."); + + auto pubResult = decodeBase64Url(kj::mv(pubField)); + JSG_REQUIRE(!pubResult.hadErrors, DOMDataError, "Invalid base64url in JWK \"pub\" for ", + normalizedName, "."); + + JSG_REQUIRE(pubResult.size() == P::PUBLIC_KEY_BYTES, DOMDataError, + "Invalid JWK \"pub\" length for ", normalizedName, "."); + + typename P::PublicKey pk; + CBS cbs; + CBS_init(&cbs, pubResult.begin(), pubResult.size()); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), DOMDataError, "Failed to parse ", + normalizedName, " public key from JWK."); + + return kj::heap>(kj::mv(pk), kj::heapArray(pubResult.asPtr()), + normalizedName, extractable, usages); + } + } + + private: + enum class KeyType { PUBLIC, PRIVATE }; + KeyType keyType; + kj::StringPtr algorithmName; + + // Public key data. The parsed public key is present only for public CryptoKeys; private keys keep + // publicKeyBytes for export and getPublicKey(). + kj::Maybe publicKey; + kj::Array publicKeyBytes; + + // Private key data (present only for private keys) + kj::Maybe> seed; + kj::Maybe privateKey; + + SubtleCrypto::ExportKeyData exportSpki(jsg::Lock& js) const { + auto oid = getOid

(); + + bssl::ScopedCBB cbb; + CBB spki, algId, oidCbb, bitString; + JSG_REQUIRE(CBB_init(cbb.get(), P::PUBLIC_KEY_BYTES + 64), InternalDOMOperationError, + "Failed to init SPKI CBB."); + JSG_REQUIRE(CBB_add_asn1(cbb.get(), &spki, CBS_ASN1_SEQUENCE), InternalDOMOperationError, + "Failed to add SPKI SEQUENCE."); + JSG_REQUIRE(CBB_add_asn1(&spki, &algId, CBS_ASN1_SEQUENCE), InternalDOMOperationError, + "Failed to add AlgorithmIdentifier SEQUENCE."); + JSG_REQUIRE(CBB_add_asn1(&algId, &oidCbb, CBS_ASN1_OBJECT), InternalDOMOperationError, + "Failed to add OID."); + JSG_REQUIRE(CBB_add_bytes(&oidCbb, oid.begin(), oid.size()), InternalDOMOperationError, + "Failed to add OID bytes."); + JSG_REQUIRE( + CBB_flush(&algId), InternalDOMOperationError, "Failed to flush AlgorithmIdentifier."); + JSG_REQUIRE(CBB_add_asn1(&spki, &bitString, CBS_ASN1_BITSTRING), InternalDOMOperationError, + "Failed to add BIT STRING."); + JSG_REQUIRE( + CBB_add_u8(&bitString, 0), InternalDOMOperationError, "Failed to add unused bits byte."); + JSG_REQUIRE(CBB_add_bytes(&bitString, publicKeyBytes.begin(), publicKeyBytes.size()), + InternalDOMOperationError, "Failed to add public key bytes."); + JSG_REQUIRE(CBB_flush(cbb.get()), InternalDOMOperationError, "Failed to flush SPKI."); + + uint8_t* der = nullptr; + size_t derLen; + JSG_REQUIRE(CBB_finish(cbb.get(), &der, &derLen), InternalDOMOperationError, + "Failed to finish SPKI encoding."); + KJ_DEFER(OPENSSL_free(der)); + + return jsg::JsArrayBuffer::create(js, kj::arrayPtr(der, derLen)).addRef(js); + } + + SubtleCrypto::ExportKeyData exportPkcs8(jsg::Lock& js) const { + auto oid = getOid

(); + auto& seedData = KJ_ASSERT_NONNULL(seed); + + bssl::ScopedCBB cbb; + CBB pkcs8, algId, oidCbb, privateKeyOctet, innerPrivateKey; + JSG_REQUIRE(CBB_init(cbb.get(), MLKEM_SEED_BYTES + 64), InternalDOMOperationError, + "Failed to init PKCS8 CBB."); + JSG_REQUIRE(CBB_add_asn1(cbb.get(), &pkcs8, CBS_ASN1_SEQUENCE), InternalDOMOperationError, + "Failed to add PKCS8 SEQUENCE."); + + // Version 0 + JSG_REQUIRE( + CBB_add_asn1_uint64(&pkcs8, 0), InternalDOMOperationError, "Failed to add PKCS8 version."); + + // AlgorithmIdentifier + JSG_REQUIRE(CBB_add_asn1(&pkcs8, &algId, CBS_ASN1_SEQUENCE), InternalDOMOperationError, + "Failed to add AlgorithmIdentifier."); + JSG_REQUIRE(CBB_add_asn1(&algId, &oidCbb, CBS_ASN1_OBJECT), InternalDOMOperationError, + "Failed to add OID."); + JSG_REQUIRE(CBB_add_bytes(&oidCbb, oid.begin(), oid.size()), InternalDOMOperationError, + "Failed to add OID bytes."); + JSG_REQUIRE( + CBB_flush(&algId), InternalDOMOperationError, "Failed to flush AlgorithmIdentifier."); + + // PrivateKey: OCTET STRING containing context-specific [0] tag with seed + JSG_REQUIRE(CBB_add_asn1(&pkcs8, &privateKeyOctet, CBS_ASN1_OCTETSTRING), + InternalDOMOperationError, "Failed to add privateKey OCTET STRING."); + JSG_REQUIRE(CBB_add_asn1(&privateKeyOctet, &innerPrivateKey, CBS_ASN1_CONTEXT_SPECIFIC | 0), + InternalDOMOperationError, "Failed to add seed context-specific tag."); + JSG_REQUIRE(CBB_add_bytes(&innerPrivateKey, seedData.begin(), seedData.size()), + InternalDOMOperationError, "Failed to add seed bytes."); + JSG_REQUIRE(CBB_flush(cbb.get()), InternalDOMOperationError, "Failed to flush PKCS8."); + + uint8_t* der = nullptr; + size_t derLen; + JSG_REQUIRE(CBB_finish(cbb.get(), &der, &derLen), InternalDOMOperationError, + "Failed to finish PKCS8 encoding."); + KJ_DEFER(OPENSSL_free(der)); + + return jsg::JsArrayBuffer::create(js, kj::arrayPtr(der, derLen)).addRef(js); + } + + SubtleCrypto::ExportKeyData exportJwk(jsg::Lock& js) const { + SubtleCrypto::JsonWebKey jwk; + jwk.kty = kj::str("AKP"); + jwk.alg = kj::str(algorithmName); + jwk.ext = isExtractable(); + jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); }); + jwk.pub = kj::encodeBase64Url(publicKeyBytes); + + if (keyType == KeyType::PRIVATE) { + auto& seedData = KJ_ASSERT_NONNULL(seed); + jwk.priv = kj::encodeBase64Url(seedData); + } + + return jwk; + } +}; + +// Dispatch based on algorithm name +template +auto dispatchMlKem(kj::StringPtr normalizedName, Func&& func) { + if (normalizedName == "ML-KEM-768") { + return func(MlKem768Params{}); + } else if (normalizedName == "ML-KEM-1024") { + return func(MlKem1024Params{}); + } else { + JSG_FAIL_REQUIRE( + DOMNotSupportedError, "Unsupported ML-KEM algorithm \"", normalizedName, "\"."); + } +} + +} // namespace + +kj::OneOf, CryptoKeyPair> CryptoKey::Impl::generateMlKem(jsg::Lock& js, + kj::StringPtr normalizedName, + SubtleCrypto::GenerateKeyAlgorithm&& algorithm, + bool extractable, + kj::ArrayPtr keyUsages) { + auto usages = CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::generate, + keyUsages, ML_KEM_PUBLIC_USAGES | ML_KEM_PRIVATE_USAGES); + auto privateKeyUsages = usages & CryptoKeyUsageSet::privateKeyMask(); + auto publicKeyUsages = usages & CryptoKeyUsageSet::publicKeyMask(); + + return dispatchMlKem( + normalizedName, [&](auto params) -> kj::OneOf, CryptoKeyPair> { + using P = decltype(params); + return MlKemKey

::generateKeyPair( + js, normalizedName, extractable, privateKeyUsages, publicKeyUsages); + }); +} + +kj::Own CryptoKey::Impl::importMlKem(jsg::Lock& js, + kj::StringPtr normalizedName, + kj::StringPtr format, + SubtleCrypto::ImportKeyData keyData, + SubtleCrypto::ImportKeyAlgorithm&& algorithm, + bool extractable, + kj::ArrayPtr keyUsages) { + return dispatchMlKem(normalizedName, [&](auto params) -> kj::Own { + using P = decltype(params); + + if (format == "raw-public") { + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPublic, keyUsages, ML_KEM_PUBLIC_USAGES); + auto& source = JSG_REQUIRE_NONNULL(keyData.tryGet>(), + DOMDataError, "Import data for raw-public must be a buffer."); + auto handle = source.getHandle(js); + return MlKemKey

::importRawPublic(handle.asArrayPtr(), normalizedName, extractable, usages); + } else if (format == "raw-seed") { + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPrivate, keyUsages, ML_KEM_PRIVATE_USAGES); + auto& source = JSG_REQUIRE_NONNULL(keyData.tryGet>(), + DOMDataError, "Import data for raw-seed must be a buffer."); + auto handle = source.getHandle(js); + return MlKemKey

::importRawSeed(handle.asArrayPtr(), normalizedName, extractable, usages); + } else if (format == "spki") { + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPublic, keyUsages, ML_KEM_PUBLIC_USAGES); + auto& source = JSG_REQUIRE_NONNULL(keyData.tryGet>(), + DOMDataError, "Import data for spki must be a buffer."); + auto handle = source.getHandle(js); + return MlKemKey

::importSpki(handle.asArrayPtr(), normalizedName, extractable, usages); + } else if (format == "pkcs8") { + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPrivate, keyUsages, ML_KEM_PRIVATE_USAGES); + auto& source = JSG_REQUIRE_NONNULL(keyData.tryGet>(), + DOMDataError, "Import data for pkcs8 must be a buffer."); + auto handle = source.getHandle(js); + return MlKemKey

::importPkcs8(handle.asArrayPtr(), normalizedName, extractable, usages); + } else if (format == "jwk") { + auto& jwk = JSG_REQUIRE_NONNULL(keyData.tryGet(), DOMDataError, + "Import data for jwk must be a JsonWebKey."); + CryptoKeyUsageSet usages; + if (jwk.priv != kj::none) { + usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPrivate, keyUsages, ML_KEM_PRIVATE_USAGES); + } else { + usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPublic, keyUsages, ML_KEM_PUBLIC_USAGES); + } + return MlKemKey

::importJwk(kj::mv(jwk), normalizedName, extractable, usages); + } else { + JSG_FAIL_REQUIRE( + DOMNotSupportedError, "Unsupported import format \"", format, "\" for ML-KEM."); + } + }); +} + +} // namespace workerd::api diff --git a/src/workerd/api/crypto/pbkdf2.c++ b/src/workerd/api/crypto/pbkdf2.c++ index d7f82da7ced..3f9af4e2292 100644 --- a/src/workerd/api/crypto/pbkdf2.c++ +++ b/src/workerd/api/crypto/pbkdf2.c++ @@ -132,7 +132,7 @@ kj::Own CryptoKey::Impl::importPbkdf2(jsg::Lock& js, CryptoKeyUsageSet::Context::importSecret, keyUsages, CryptoKeyUsageSet::derivationKeyMask()); JSG_REQUIRE(!extractable, DOMSyntaxError, "PBKDF2 key cannot be extractable."); - JSG_REQUIRE(format == "raw", DOMNotSupportedError, + JSG_REQUIRE(format == "raw" || format == "raw-secret", DOMNotSupportedError, "PBKDF2 key must be imported in \"raw\" format (requested \"", format, "\")."); // NOTE: Checked in SubtleCrypto::importKey(). diff --git a/src/workerd/api/crypto/rsa.c++ b/src/workerd/api/crypto/rsa.c++ index d6d77c91cc9..7815919f780 100644 --- a/src/workerd/api/crypto/rsa.c++ +++ b/src/workerd/api/crypto/rsa.c++ @@ -539,6 +539,11 @@ class RsassaPkcs1V15Key final: public RsaBase { return "RSASSA-PKCS1-v1_5"; } + kj::Own cloneAsPublicKey( + jsg::Lock& js, AsymmetricKeyData publicKeyData) const override { + return kj::heap(kj::mv(publicKeyData), keyAlgorithm.clone(js), true); + } + kj::StringPtr chooseHash( const kj::Maybe>& callTimeHash) const override { @@ -568,6 +573,11 @@ class RsaPssKey final: public RsaBase { return keyAlgorithm.name; } + kj::Own cloneAsPublicKey( + jsg::Lock& js, AsymmetricKeyData publicKeyData) const override { + return kj::heap(kj::mv(publicKeyData), keyAlgorithm.clone(js), true); + } + kj::StringPtr chooseHash( const kj::Maybe>& callTimeHash) const override { @@ -609,6 +619,11 @@ class RsaOaepKey final: public RsaBase { return keyAlgorithm.name; } + kj::Own cloneAsPublicKey( + jsg::Lock& js, AsymmetricKeyData publicKeyData) const override { + return kj::heap(kj::mv(publicKeyData), keyAlgorithm.clone(js), true); + } + kj::StringPtr chooseHash( const kj::Maybe>& callTimeHash) const override { @@ -668,6 +683,11 @@ class RsaRawKey final: public RsaBase { AsymmetricKeyData keyData, CryptoKey::RsaKeyAlgorithm keyAlgorithm, bool extractable) : RsaBase(kj::mv(keyData), kj::mv(keyAlgorithm), extractable) {} + kj::Own cloneAsPublicKey( + jsg::Lock& js, AsymmetricKeyData publicKeyData) const override { + return kj::heap(kj::mv(publicKeyData), keyAlgorithm.clone(js), true); + } + jsg::JsArrayBuffer sign(jsg::Lock& js, SubtleCrypto::SignAlgorithm&& algorithm, kj::ArrayPtr data) const override { diff --git a/src/workerd/api/tests/BUILD.bazel b/src/workerd/api/tests/BUILD.bazel index a11facc4e8b..3349d375741 100644 --- a/src/workerd/api/tests/BUILD.bazel +++ b/src/workerd/api/tests/BUILD.bazel @@ -424,6 +424,12 @@ wd_test( data = ["crypto-extras-test.js"], ) +wd_test( + src = "crypto-modern-algorithms-test.wd-test", + args = ["--experimental"], + data = ["crypto-modern-algorithms-test.js"], +) + wd_test( src = "resizable-arraybuffer-toctou-test.wd-test", args = ["--experimental"], diff --git a/src/workerd/api/tests/crypto-extras-test.js b/src/workerd/api/tests/crypto-extras-test.js index 4b700bece06..e8bbbe693fc 100644 --- a/src/workerd/api/tests/crypto-extras-test.js +++ b/src/workerd/api/tests/crypto-extras-test.js @@ -420,3 +420,23 @@ export const ecJwkKeyConsistencyCheck = { ok(threw, 'Import should have thrown for inconsistent EC JWK private key'); }, }; + +export const webCryptoModernAlgorithmsRequireCompatFlag = { + async test() { + strictEqual('supports' in crypto.subtle.constructor, false); + strictEqual('encapsulateKey' in crypto.subtle, false); + strictEqual('encapsulateBits' in crypto.subtle, false); + strictEqual('decapsulateKey' in crypto.subtle, false); + strictEqual('decapsulateBits' in crypto.subtle, false); + strictEqual('getPublicKey' in crypto.subtle, false); + + await crypto.subtle.generateKey('ML-DSA-44', true, ['sign']).then( + () => { + throw new Error('generateKey should not have resolved'); + }, + (error) => { + strictEqual(error.name, 'NotSupportedError'); + } + ); + }, +}; diff --git a/src/workerd/api/tests/crypto-modern-algorithms-test.js b/src/workerd/api/tests/crypto-modern-algorithms-test.js new file mode 100644 index 00000000000..491d24f76ac --- /dev/null +++ b/src/workerd/api/tests/crypto-modern-algorithms-test.js @@ -0,0 +1,81 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +import { rejects, strictEqual, throws } from 'node:assert'; + +export const supportsEcdhP521ByteRoundedDeriveBits = { + async test() { + const keyPair = await crypto.subtle.generateKey( + { name: 'ECDH', namedCurve: 'P-521' }, + false, + ['deriveBits'] + ); + const algorithm = { name: 'ECDH', public: keyPair.publicKey }; + + const bits = await crypto.subtle.deriveBits( + algorithm, + keyPair.privateKey, + 528 + ); + strictEqual(bits.byteLength, 66); + + strictEqual( + crypto.subtle.constructor.supports('deriveBits', algorithm, 528), + true + ); + strictEqual( + crypto.subtle.constructor.supports('deriveBits', algorithm, 529), + false + ); + + throws( + () => crypto.subtle.constructor.supports('deriveBits', algorithm, -1), + TypeError + ); + throws( + () => + crypto.subtle.constructor.supports('deriveBits', algorithm, 2 ** 32), + TypeError + ); + }, +}; + +export const mlDsaJwkKeyOpsValidation = { + async test() { + const keyPair = await crypto.subtle.generateKey('ML-DSA-44', true, [ + 'sign', + ]); + const jwk = await crypto.subtle.exportKey('jwk', keyPair.privateKey); + + await rejects( + crypto.subtle.importKey( + 'jwk', + { ...jwk, key_ops: [] }, + 'ML-DSA-44', + true, + ['sign'] + ), + { name: 'DataError' } + ); + + await rejects( + crypto.subtle.importKey( + 'jwk', + { ...jwk, key_ops: ['sign', 'sign'] }, + 'ML-DSA-44', + true, + ['sign'] + ), + { name: 'DataError' } + ); + + const jwkWithoutAlg = { ...jwk }; + delete jwkWithoutAlg.alg; + await rejects( + crypto.subtle.importKey('jwk', jwkWithoutAlg, 'ML-DSA-44', true, [ + 'sign', + ]), + { name: 'DataError' } + ); + }, +}; diff --git a/src/workerd/api/tests/crypto-modern-algorithms-test.wd-test b/src/workerd/api/tests/crypto-modern-algorithms-test.wd-test new file mode 100644 index 00000000000..461d14f3bea --- /dev/null +++ b/src/workerd/api/tests/crypto-modern-algorithms-test.wd-test @@ -0,0 +1,14 @@ +using Workerd = import "/workerd/workerd.capnp"; + +const unitTests :Workerd.Config = ( + services = [ + ( name = "crypto-modern-algorithms-test", + worker = ( + modules = [ + (name = "worker", esModule = embed "crypto-modern-algorithms-test.js") + ], + compatibilityFlags = ["nodejs_compat", "webcrypto_modern_algorithms"], + ) + ), + ], +); diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp index e88f443499c..703f3220ff9 100644 --- a/src/workerd/io/compatibility-date.capnp +++ b/src/workerd/io/compatibility-date.capnp @@ -1596,4 +1596,13 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { # This flag is not enabled by default in local dev on purpose, we want local dev to mimic # the production environment as much as possible, this flag is meant to be used by test # frameworks when running tests that require the inspector to be functional, and not by end users. + + webCryptoModernAlgorithms @181 :Bool + $compatEnableFlag("webcrypto_modern_algorithms") + $experimental; + # Enables experimental WebCrypto modern algorithm support. This currently exposes the subset of + # the evolving WICG Modern Algorithms draft implemented by workerd: ML-KEM, ML-DSA, related + # key encapsulation helpers, getPublicKey(), SubtleCrypto.supports(), and AKP JWK support. It is + # intentionally gated because the draft is still changing and workerd does not implement the full + # proposal. } diff --git a/src/wpt/BUILD.bazel b/src/wpt/BUILD.bazel index 14dd77e2b02..6bf2563c04f 100644 --- a/src/wpt/BUILD.bazel +++ b/src/wpt/BUILD.bazel @@ -98,8 +98,12 @@ wpt_test( wpt_test( name = "WebCryptoAPI", size = "enormous", - compat_flags = ["disable_fast_jsg_struct"], + compat_flags = [ + "disable_fast_jsg_struct", + "webcrypto_modern_algorithms", + ], config = "WebCryptoAPI-test.ts", + include_tentative = True, target_compatible_with = select({ # Too slow on Windows "@platforms//os:windows": ["@platforms//:incompatible"], diff --git a/src/wpt/WebCryptoAPI-test.ts b/src/wpt/WebCryptoAPI-test.ts index 8a2c1472b37..d96131af30b 100644 --- a/src/wpt/WebCryptoAPI-test.ts +++ b/src/wpt/WebCryptoAPI-test.ts @@ -4,11 +4,19 @@ import { type TestRunnerConfig } from 'harness/harness'; -export default { - 'algorithm-discards-context.https.window.js': { - comment: 'Secure context is only relevant in browsers', +const supportFile = { + comment: 'Support file, not a test', + omittedTests: true, +} as const; + +const unsupported = (alg: string) => + ({ + comment: `${alg} is not supported`, omittedTests: true, - }, + }) as const; + +export default { + 'algorithm-discards-context.https.window.js': supportFile, 'crypto_key_cached_slots.https.any.js': { comment: 'Investigate this', expectedFailures: [ @@ -16,47 +24,41 @@ export default { 'CryptoKey.usages getter returns cached object', ], }, - - 'derive_bits_keys/argon2.js': { - comment: 'Argon2 is not supported', - omittedTests: true, - }, - 'derive_bits_keys/argon2_vectors.js': { - comment: 'Argon2 is not supported', - omittedTests: true, - }, - 'derive_bits_keys/cfrg_curves_bits.js': {}, + 'derive_bits_keys/argon2.js': supportFile, + 'derive_bits_keys/argon2.tentative.https.any.js': unsupported('Argon2'), + 'derive_bits_keys/argon2_vectors.js': supportFile, + 'derive_bits_keys/cfrg_curves_bits.js': supportFile, 'derive_bits_keys/cfrg_curves_bits_curve25519.https.any.js': {}, - 'derive_bits_keys/cfrg_curves_bits_fixtures.js': {}, - 'derive_bits_keys/cfrg_curves_keys.js': {}, + 'derive_bits_keys/cfrg_curves_bits_curve448.tentative.https.any.js': + unsupported('X448'), + 'derive_bits_keys/cfrg_curves_bits_fixtures.js': supportFile, + 'derive_bits_keys/cfrg_curves_keys.js': supportFile, 'derive_bits_keys/cfrg_curves_keys_curve25519.https.any.js': {}, + 'derive_bits_keys/cfrg_curves_keys_curve448.tentative.https.any.js': + unsupported('X448'), 'derive_bits_keys/derive_key_and_encrypt.https.any.js': {}, - 'derive_bits_keys/derive_key_and_encrypt.js': {}, + 'derive_bits_keys/derive_key_and_encrypt.js': supportFile, 'derive_bits_keys/derived_bits_length.https.any.js': {}, - 'derive_bits_keys/derived_bits_length.js': {}, - 'derive_bits_keys/derived_bits_length_testcases.js': { - comment: - "This is a resource file but it's not in the resources/ directory; no tests in here", - omittedTests: true, - }, - 'derive_bits_keys/derived_bits_length_vectors.js': {}, + 'derive_bits_keys/derived_bits_length.js': supportFile, + 'derive_bits_keys/derived_bits_length_testcases.js': supportFile, + 'derive_bits_keys/derived_bits_length_vectors.js': supportFile, 'derive_bits_keys/ecdh_bits.https.any.js': {}, - 'derive_bits_keys/ecdh_bits.js': {}, + 'derive_bits_keys/ecdh_bits.js': supportFile, 'derive_bits_keys/ecdh_keys.https.any.js': {}, - 'derive_bits_keys/ecdh_keys.js': {}, + 'derive_bits_keys/ecdh_keys.js': supportFile, 'derive_bits_keys/hkdf.https.any.js': { comment: 'Cannot cope with this many iterations, keeps timing out', omittedTests: [/with 100000 iterations/], }, - 'derive_bits_keys/hkdf.js': {}, - 'derive_bits_keys/hkdf_vectors.js': {}, + 'derive_bits_keys/hkdf.js': supportFile, + 'derive_bits_keys/hkdf_vectors.js': supportFile, 'derive_bits_keys/pbkdf2.https.any.js': { comment: 'Cannot cope with this many iterations, keeps timing out', omittedTests: [/with 100000 iterations/], }, - 'derive_bits_keys/pbkdf2.js': {}, - 'derive_bits_keys/pbkdf2_vectors.js': {}, - + 'derive_bits_keys/pbkdf2.js': supportFile, + 'derive_bits_keys/pbkdf2_vectors.js': supportFile, + 'digest/cshake.tentative.https.any.js': unsupported('cSHAKE'), 'digest/digest.https.any.js': { comment: 'They expect TypeError, we have NotSupportedError', expectedFailures: [ @@ -66,35 +68,37 @@ export default { 'empty algorithm object with long', ], }, - - 'encap_decap/ml_kem_vectors.js': { - comment: 'ML-KEM (post-quantum key encapsulation) is not supported', - omittedTests: true, + 'digest/kangarootwelve.tentative.https.any.js': unsupported('KangarooTwelve'), + 'digest/sha3.tentative.https.any.js': unsupported('SHA3'), + 'digest/turboshake.tentative.https.any.js': unsupported('TurboSHAKE'), + 'encap_decap/encap_decap_bits.tentative.https.any.js': { + comment: 'ML-KEM-512 is not supported', + expectedFailures: [/ML-KEM-512/i], }, - - 'encrypt_decrypt/aes.js': {}, + 'encap_decap/encap_decap_keys.tentative.https.any.js': { + comment: 'ML-KEM-512 is not supported', + expectedFailures: [/ML-KEM-512/i], + }, + 'encap_decap/ml_kem_vectors.js': supportFile, + 'encrypt_decrypt/aes.js': supportFile, 'encrypt_decrypt/aes_cbc.https.any.js': {}, - 'encrypt_decrypt/aes_cbc_vectors.js': {}, + 'encrypt_decrypt/aes_cbc_vectors.js': supportFile, 'encrypt_decrypt/aes_ctr.https.any.js': {}, - 'encrypt_decrypt/aes_ctr_vectors.js': {}, + 'encrypt_decrypt/aes_ctr_vectors.js': supportFile, 'encrypt_decrypt/aes_gcm.https.any.js': {}, 'encrypt_decrypt/aes_gcm_256_iv.https.any.js': {}, - 'encrypt_decrypt/aes_gcm_256_iv_fixtures.js': {}, - 'encrypt_decrypt/aes_gcm_96_iv_fixtures.js': {}, - 'encrypt_decrypt/aes_gcm_vectors.js': {}, - 'encrypt_decrypt/aes_ocb_fixtures.js': { - comment: 'AES-OCB is not supported', - omittedTests: true, - }, - 'encrypt_decrypt/aes_ocb_vectors.js': { - comment: 'AES-OCB is not supported', - omittedTests: true, - }, - 'encrypt_decrypt/rsa.js': {}, + 'encrypt_decrypt/aes_gcm_256_iv_fixtures.js': supportFile, + 'encrypt_decrypt/aes_gcm_96_iv_fixtures.js': supportFile, + 'encrypt_decrypt/aes_gcm_vectors.js': supportFile, + 'encrypt_decrypt/aes_ocb.tentative.https.any.js': unsupported('AES-OCB'), + 'encrypt_decrypt/aes_ocb_fixtures.js': supportFile, + 'encrypt_decrypt/aes_ocb_vectors.js': supportFile, + 'encrypt_decrypt/chacha20_poly1305.tentative.https.any.js': + unsupported('ChaCha20-Poly1305'), + 'encrypt_decrypt/rsa.js': supportFile, 'encrypt_decrypt/rsa_oaep.https.any.js': {}, - 'encrypt_decrypt/rsa_vectors.js': {}, - - 'generateKey/failures.js': {}, + 'encrypt_decrypt/rsa_vectors.js': supportFile, + 'generateKey/failures.js': supportFile, 'generateKey/failures_AES-CBC.https.any.js': { comment: 'Wrong type of error returned', expectedFailures: [/^(Empty|Bad) algorithm:/], @@ -111,6 +115,7 @@ export default { comment: 'Wrong type of error returned', expectedFailures: [/^(Empty|Bad) algorithm:/], }, + 'generateKey/failures_AES-OCB.tentative.https.any.js': unsupported('AES-OCB'), 'generateKey/failures_ECDH.https.any.js': { comment: 'Wrong type of error returned', expectedFailures: [/^(Empty|Bad) algorithm:/], @@ -123,10 +128,19 @@ export default { comment: 'Wrong type of error returned', expectedFailures: [/^(Empty|Bad) algorithm:/], }, + 'generateKey/failures_Ed448.tentative.https.any.js': unsupported('Ed448'), 'generateKey/failures_HMAC.https.any.js': { comment: 'Wrong type of error returned', expectedFailures: [/^(Empty|Bad) algorithm:/], }, + 'generateKey/failures_ML-DSA.tentative.https.any.js': { + comment: 'Wrong type of error returned', + expectedFailures: [/^(Empty|Bad) algorithm:/], + }, + 'generateKey/failures_ML-KEM.tentative.https.any.js': { + comment: 'Wrong type of error returned + ML-KEM-512 is not supported', + expectedFailures: [/^(Empty|Bad) algorithm:/, /ML-KEM-512/i], + }, 'generateKey/failures_RSA-OAEP.https.any.js': { comment: 'Wrong type of error returned', expectedFailures: [/^(Empty|Bad) algorithm:/], @@ -143,56 +157,36 @@ export default { comment: 'Wrong type of error returned', expectedFailures: [/^(Empty|Bad) algorithm:/], }, - 'generateKey/successes.js': {}, - 'generateKey/successes_AES-CBC.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_AES-CTR.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_AES-GCM.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_AES-KW.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_ECDH.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_ECDSA.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_Ed25519.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_HMAC.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_RSA-OAEP.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_RSA-PSS.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_RSASSA-PKCS1-v1_5.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - 'generateKey/successes_X25519.https.any.js': { - comment: 'TODO investigate this', - expectedFailures: [/^undefined: /], - }, - + 'generateKey/failures_X448.tentative.https.any.js': unsupported('X448'), + 'generateKey/failures_chacha20_poly1305.tentative.https.any.js': + unsupported('ChaCha20-Poly1305'), + 'generateKey/failures_kmac.tentative.https.any.js': unsupported('KMAC'), + 'generateKey/successes.js': supportFile, + 'generateKey/successes_AES-CBC.https.any.js': {}, + 'generateKey/successes_AES-CTR.https.any.js': {}, + 'generateKey/successes_AES-GCM.https.any.js': {}, + 'generateKey/successes_AES-KW.https.any.js': {}, + 'generateKey/successes_AES-OCB.tentative.https.any.js': + unsupported('AES-OCB'), + 'generateKey/successes_ECDH.https.any.js': {}, + 'generateKey/successes_ECDSA.https.any.js': {}, + 'generateKey/successes_Ed25519.https.any.js': {}, + 'generateKey/successes_Ed448.tentative.https.any.js': unsupported('Ed448'), + 'generateKey/successes_HMAC.https.any.js': {}, + 'generateKey/successes_ML-DSA.tentative.https.any.js': {}, + 'generateKey/successes_ML-KEM.tentative.https.any.js': { + comment: 'ML-KEM-512 is not supported', + expectedFailures: [/ML-KEM-512/i], + }, + 'generateKey/successes_RSA-OAEP.https.any.js': {}, + 'generateKey/successes_RSA-PSS.https.any.js': {}, + 'generateKey/successes_RSASSA-PKCS1-v1_5.https.any.js': {}, + 'generateKey/successes_X25519.https.any.js': {}, + 'generateKey/successes_X448.tentative.https.any.js': unsupported('X448'), + 'generateKey/successes_chacha20_poly1305.tentative.https.any.js': + unsupported('ChaCha20-Poly1305'), + 'generateKey/successes_kmac.tentative.https.any.js': unsupported('KMAC'), + 'getPublicKey.tentative.https.any.js': {}, 'getRandomValues.any.js': {}, 'historical.any.js': { comment: 'Secure context is only relevant to browsers', @@ -225,23 +219,50 @@ export default { 'Window interface: attribute crypto', ], }, - - 'import_export/ML-DSA_importKey.js': { - comment: 'ML-DSA (post-quantum signature algorithm) is not supported', - omittedTests: true, - }, - 'import_export/ML-DSA_importKey_fixtures.js': { - comment: 'ML-DSA (post-quantum signature algorithm) is not supported', - omittedTests: true, - }, - 'import_export/ML-KEM_importKey.js': { - comment: 'ML-KEM (post-quantum key encapsulation) is not supported', - omittedTests: true, - }, - 'import_export/ML-KEM_importKey_fixtures.js': { - comment: 'ML-KEM (post-quantum key encapsulation) is not supported', - omittedTests: true, + 'idlharness.tentative.https.any.js': { + comment: + 'IDL tests fail because Workers exposes globals differently than browsers (not as own properties of self). Modern-algos operations not yet fully exposed.', + expectedFailures: [ + 'CryptoKey interface: attribute type', + 'CryptoKey interface: attribute extractable', + 'CryptoKey interface: attribute algorithm', + 'CryptoKey interface: attribute usages', + 'SubtleCrypto interface: operation encrypt(AlgorithmIdentifier, CryptoKey, BufferSource)', + 'SubtleCrypto interface: operation decrypt(AlgorithmIdentifier, CryptoKey, BufferSource)', + 'SubtleCrypto interface: operation sign(AlgorithmIdentifier, CryptoKey, BufferSource)', + 'SubtleCrypto interface: operation verify(AlgorithmIdentifier, CryptoKey, BufferSource, BufferSource)', + 'SubtleCrypto interface: operation digest(AlgorithmIdentifier, BufferSource)', + 'SubtleCrypto interface: operation generateKey(AlgorithmIdentifier, boolean, sequence)', + 'SubtleCrypto interface: operation deriveKey(AlgorithmIdentifier, CryptoKey, AlgorithmIdentifier, boolean, sequence)', + 'SubtleCrypto interface: operation deriveBits(AlgorithmIdentifier, CryptoKey, optional unsigned long?)', + 'SubtleCrypto interface: operation importKey(KeyFormat, (BufferSource or JsonWebKey), AlgorithmIdentifier, boolean, sequence)', + 'SubtleCrypto interface: operation exportKey(KeyFormat, CryptoKey)', + 'SubtleCrypto interface: operation wrapKey(KeyFormat, CryptoKey, CryptoKey, AlgorithmIdentifier)', + 'SubtleCrypto interface: operation unwrapKey(KeyFormat, BufferSource, CryptoKey, AlgorithmIdentifier, AlgorithmIdentifier, boolean, sequence)', + 'SubtleCrypto interface: operation encapsulateKey(AlgorithmIdentifier, CryptoKey, AlgorithmIdentifier, boolean, sequence)', + 'SubtleCrypto interface: operation encapsulateBits(AlgorithmIdentifier, CryptoKey)', + 'SubtleCrypto interface: operation decapsulateKey(AlgorithmIdentifier, CryptoKey, BufferSource, AlgorithmIdentifier, boolean, sequence)', + 'SubtleCrypto interface: operation decapsulateBits(AlgorithmIdentifier, CryptoKey, BufferSource)', + 'SubtleCrypto interface: operation getPublicKey(CryptoKey, sequence)', + 'Window interface: attribute crypto', + ], }, + 'import_export/AES-OCB_importKey.tentative.https.any.js': + unsupported('AES-OCB'), + 'import_export/Argon2_importKey.tentative.https.any.js': + unsupported('Argon2'), + 'import_export/ChaCha20-Poly1305_importKey.tentative.https.any.js': + unsupported('ChaCha20-Poly1305'), + 'import_export/KMAC_importKey.tentative.https.any.js': unsupported('KMAC'), + 'import_export/ML-DSA_importKey.js': supportFile, + 'import_export/ML-DSA_importKey.tentative.https.any.js': {}, + 'import_export/ML-DSA_importKey_fixtures.js': supportFile, + 'import_export/ML-KEM_importKey.js': supportFile, + 'import_export/ML-KEM_importKey.tentative.https.any.js': { + comment: 'ML-KEM-512 is not supported', + expectedFailures: [/ML-KEM-512/i], + }, + 'import_export/ML-KEM_importKey_fixtures.js': supportFile, 'import_export/crashtests/importKey-unsettled-promise.https.any.js': {}, 'import_export/ec_importKey.https.any.js': {}, 'import_export/ec_importKey_failures_ECDH.https.any.js': { @@ -266,9 +287,9 @@ export default { "Invalid 'crv' field: importKey(jwk(private), {name: ECDSA, namedCurve: P-521}, true, [sign])", ], }, - 'import_export/ec_importKey_failures_fixtures.js': {}, - 'import_export/importKey_failures.js': {}, - 'import_export/okp_importKey.js': {}, + 'import_export/ec_importKey_failures_fixtures.js': supportFile, + 'import_export/importKey_failures.js': supportFile, + 'import_export/okp_importKey.js': supportFile, 'import_export/okp_importKey_Ed25519.https.any.js': { comment: 'Investigate this', expectedFailures: [ @@ -294,7 +315,11 @@ export default { 'Good parameters with JWK alg EdDSA: Ed25519 (jwk, object(crv, d, x, kty), Ed25519, true, [sign, sign])', ], }, + 'import_export/okp_importKey_Ed448.tentative.https.any.js': + unsupported('Ed448'), 'import_export/okp_importKey_X25519.https.any.js': {}, + 'import_export/okp_importKey_X448.tentative.https.any.js': + unsupported('X448'), 'import_export/okp_importKey_failures_Ed25519.https.any.js': { comment: 'To be investigated - workerd does not reject these invalid key pairs', @@ -305,6 +330,8 @@ export default { /Invalid 'crv' field: importKey\(jwk \(public\) , .*, true, \[verify\]\)/, ], }, + 'import_export/okp_importKey_failures_Ed448.tentative.https.any.js': + unsupported('Ed448'), 'import_export/okp_importKey_failures_X25519.https.any.js': { comment: 'To be investigated - workerd does not reject these invalid key pairs', @@ -317,24 +344,25 @@ export default { /Invalid 'crv' field: importKey\(jwk \(public\) , .*, true, \[\]\)/, ], }, - 'import_export/okp_importKey_failures_fixtures.js': {}, - 'import_export/okp_importKey_fixtures.js': {}, + 'import_export/okp_importKey_failures_X448.tentative.https.any.js': + unsupported('X448'), + 'import_export/okp_importKey_failures_fixtures.js': supportFile, + 'import_export/okp_importKey_fixtures.js': supportFile, 'import_export/rsa_importKey.https.any.js': {}, 'import_export/symmetric_importKey.https.any.js': {}, - 'import_export/symmetric_importKey.js': {}, - + 'import_export/symmetric_importKey.js': supportFile, 'normalize-algorithm-name.https.any.js': { - comment: - 'Algorithm names with Unicode Kelvin sign (U+212A) lookalikes throw SyntaxError instead of NotSupportedError', - expectedFailures: [/does not match "HKDF"/, /does not match "PBKDF2"/], + comment: 'Wrong error type - DOMException code mismatch', + expectedFailures: [ + '"HDF" does not match "HKDF"', + '"PBDF2" does not match "PBKDF2"', + ], }, - 'randomUUID.https.any.js': {}, - 'sign_verify/ecdsa.https.any.js': {}, - 'sign_verify/ecdsa.js': {}, - 'sign_verify/ecdsa_vectors.js': {}, - 'sign_verify/eddsa.js': {}, + 'sign_verify/ecdsa.js': supportFile, + 'sign_verify/ecdsa_vectors.js': supportFile, + 'sign_verify/eddsa.js': supportFile, 'sign_verify/eddsa_curve25519.https.any.js': { comment: 'To be investigated', expectedFailures: [ @@ -342,6 +370,7 @@ export default { 'EdDSA Ed25519 verification with transferred signature during call', ], }, + 'sign_verify/eddsa_curve448.tentative.https.any.js': unsupported('Ed448'), 'sign_verify/eddsa_small_order_points.https.any.js': { comment: 'To be investigated', expectedFailures: [ @@ -354,39 +383,25 @@ export default { 'Ed25519 Verification checks with small-order key of order - Test 13', ], }, - 'sign_verify/eddsa_small_order_points.js': {}, - 'sign_verify/eddsa_vectors.js': {}, + 'sign_verify/eddsa_small_order_points.js': supportFile, + 'sign_verify/eddsa_vectors.js': supportFile, 'sign_verify/hmac.https.any.js': {}, - 'sign_verify/hmac.js': {}, - 'sign_verify/hmac_vectors.js': {}, - 'sign_verify/kmac.js': { - comment: 'KMAC is not supported', - omittedTests: true, - }, - 'sign_verify/kmac_vectors.js': { - comment: 'KMAC is not supported', - omittedTests: true, - }, - 'sign_verify/mldsa.js': { - comment: 'ML-DSA (post-quantum signature algorithm) is not supported', - omittedTests: true, - }, - 'sign_verify/mldsa_vectors.js': { - comment: 'ML-DSA (post-quantum signature algorithm) is not supported', - omittedTests: true, - }, - 'sign_verify/rsa.js': {}, + 'sign_verify/hmac.js': supportFile, + 'sign_verify/hmac_vectors.js': supportFile, + 'sign_verify/kmac.js': supportFile, + 'sign_verify/kmac.tentative.https.any.js': unsupported('KMAC'), + 'sign_verify/kmac_vectors.js': supportFile, + 'sign_verify/mldsa.js': supportFile, + 'sign_verify/mldsa.tentative.https.any.js': {}, + 'sign_verify/mldsa_vectors.js': supportFile, + 'sign_verify/rsa.js': supportFile, 'sign_verify/rsa_pkcs.https.any.js': {}, - 'sign_verify/rsa_pkcs_vectors.js': {}, + 'sign_verify/rsa_pkcs_vectors.js': supportFile, 'sign_verify/rsa_pss.https.any.js': {}, - 'sign_verify/rsa_pss_vectors.js': {}, - - 'util/helpers.js': {}, - 'util/worker-report-crypto-subtle-presence.js': { - comment: 'ReferenceError: postMessage is not defined', - disabledTests: true, - }, - + 'sign_verify/rsa_pss_vectors.js': supportFile, + 'supports.tentative.https.any.js': {}, + 'util/helpers.js': supportFile, + 'util/worker-report-crypto-subtle-presence.js': supportFile, 'wrapKey_unwrapKey/wrapKey_unwrapKey.https.any.js': {}, - 'wrapKey_unwrapKey/wrapKey_unwrapKey_vectors.js': {}, + 'wrapKey_unwrapKey/wrapKey_unwrapKey_vectors.js': supportFile, } satisfies TestRunnerConfig; diff --git a/src/wpt/harness/utils.ts b/src/wpt/harness/utils.ts index 697e835b008..1441121c516 100644 --- a/src/wpt/harness/utils.ts +++ b/src/wpt/harness/utils.ts @@ -24,13 +24,7 @@ // USE OR OTHER DEALINGS IN THE SOFTWARE. import crypto from 'node:crypto'; -import { - type UnknownFunc, - type TestFn, - type PromiseTestFn, - type HostInfo, - getHostInfo, -} from './common'; +import { type UnknownFunc, type HostInfo, getHostInfo } from './common'; declare global { var GLOBAL: { @@ -40,19 +34,14 @@ declare global { }; function done(): undefined; - function subsetTestByKey( + function subsetTestByKey( _key: string, - testType: ( - testCallback: TestFn | PromiseTestFn | string, - testMessage?: string - ) => T, - testCallback: TestFn | PromiseTestFn | string, - testMessage?: string - ): T; + testFunc: (...args: unknown[]) => unknown, + ...args: unknown[] + ): unknown; function subsetTest( - testType: TestRunnerFn, - testCallback: TestFn | PromiseTestFn, - testMessage: string + testFunc: (...args: unknown[]) => void, + ...args: unknown[] ): void; // Used by idlharness.js to determine if a subtest should be run function shouldRunSubTest(name?: string): boolean; @@ -79,8 +68,6 @@ declare global { function fetch_spec(spec: string): Promise<{ spec: string; idl: string }>; } -type TestRunnerFn = (callback: TestFn | PromiseTestFn, message: string) => void; - globalThis.get_host_info = (): HostInfo => { return getHostInfo(); }; @@ -99,32 +86,31 @@ globalThis.GLOBAL = { globalThis.done = (): undefined => undefined; -globalThis.subsetTestByKey = ( +globalThis.subsetTestByKey = ( _key: string, - testType: (testCallback: TestFn | PromiseTestFn, testMessage: string) => T, - testCallback: TestFn | PromiseTestFn | string, - testMessage?: string -): T => { + testFunc: (...args: unknown[]) => unknown, + ...args: unknown[] +): unknown => { // This function is designed to allow selecting only certain tests when // running in a browser, by changing the query string. We'll always run // all the tests. - return testType( - testCallback as TestFn | PromiseTestFn, - testMessage as string - ); + return testFunc(...args); }; // Used by idlharness.js to determine if a subtest should be run. // We always run all subtests. globalThis.shouldRunSubTest = (): boolean => true; -globalThis.subsetTest = (testType, testCallback, testMessage): void => { +globalThis.subsetTest = ( + testFunc: (...args: unknown[]) => void, + ...args: unknown[] +): void => { // This function is designed to allow selecting only certain tests when // running in a browser, by changing the query string. We'll always run // all the tests. // eslint-disable-next-line @typescript-eslint/no-confusing-void-expression -- We are emulating WPT's existing interface which always passes through the returned value - return testType(testCallback, testMessage); + return testFunc(...args); }; /** diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index 05c9f61bea5..d64383ea5e6 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -1449,6 +1449,55 @@ declare abstract class SubtleCrypto { extractable: boolean, keyUsages: string[], ): Promise; + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoHashAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + length?: number | null, + ): boolean; + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoHashAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + ): boolean; + encapsulateKey( + encapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + encapsulationKey: CryptoKey, + sharedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[], + ): Promise; + encapsulateBits( + encapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + encapsulationKey: CryptoKey, + ): Promise; + decapsulateKey( + decapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + decapsulationKey: CryptoKey, + ciphertext: ArrayBuffer | ArrayBufferView, + sharedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[], + ): Promise; + decapsulateBits( + decapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + decapsulationKey: CryptoKey, + ciphertext: ArrayBuffer | ArrayBufferView, + ): Promise; + getPublicKey(key: CryptoKey, keyUsages: string[]): Promise; timingSafeEqual( a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView, @@ -1515,12 +1564,22 @@ interface JsonWebKey { qi?: string; oth?: RsaOtherPrimesInfo[]; k?: string; + pub?: string; + priv?: string; } interface RsaOtherPrimesInfo { r?: string; d?: string; t?: string; } +interface SubtleCryptoEncapsulatedBits { + sharedKey: ArrayBuffer; + ciphertext: ArrayBuffer; +} +interface SubtleCryptoEncapsulatedKey { + sharedKey: CryptoKey; + ciphertext: ArrayBuffer; +} interface SubtleCryptoDeriveKeyAlgorithm { name: string; salt?: ArrayBuffer | ArrayBufferView; @@ -1561,6 +1620,7 @@ interface SubtleCryptoSignAlgorithm { hash?: string | SubtleCryptoHashAlgorithm; dataLength?: number; saltLength?: number; + context?: ArrayBuffer | ArrayBufferView; } interface CryptoKeyKeyAlgorithm { name: string; diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index 92867d33f75..3f430572275 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -1455,6 +1455,55 @@ export declare abstract class SubtleCrypto { extractable: boolean, keyUsages: string[], ): Promise; + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoHashAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + length?: number | null, + ): boolean; + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoHashAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + ): boolean; + encapsulateKey( + encapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + encapsulationKey: CryptoKey, + sharedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[], + ): Promise; + encapsulateBits( + encapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + encapsulationKey: CryptoKey, + ): Promise; + decapsulateKey( + decapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + decapsulationKey: CryptoKey, + ciphertext: ArrayBuffer | ArrayBufferView, + sharedKeyAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + extractable: boolean, + keyUsages: string[], + ): Promise; + decapsulateBits( + decapsulationAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + decapsulationKey: CryptoKey, + ciphertext: ArrayBuffer | ArrayBufferView, + ): Promise; + getPublicKey(key: CryptoKey, keyUsages: string[]): Promise; timingSafeEqual( a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView, @@ -1521,12 +1570,22 @@ export interface JsonWebKey { qi?: string; oth?: RsaOtherPrimesInfo[]; k?: string; + pub?: string; + priv?: string; } export interface RsaOtherPrimesInfo { r?: string; d?: string; t?: string; } +export interface SubtleCryptoEncapsulatedBits { + sharedKey: ArrayBuffer; + ciphertext: ArrayBuffer; +} +export interface SubtleCryptoEncapsulatedKey { + sharedKey: CryptoKey; + ciphertext: ArrayBuffer; +} export interface SubtleCryptoDeriveKeyAlgorithm { name: string; salt?: ArrayBuffer | ArrayBufferView; @@ -1567,6 +1626,7 @@ export interface SubtleCryptoSignAlgorithm { hash?: string | SubtleCryptoHashAlgorithm; dataLength?: number; saltLength?: number; + context?: ArrayBuffer | ArrayBufferView; } export interface CryptoKeyKeyAlgorithm { name: string; diff --git a/types/generated-snapshot/index.d.ts b/types/generated-snapshot/index.d.ts index 12246afaf8d..904b5daad88 100755 --- a/types/generated-snapshot/index.d.ts +++ b/types/generated-snapshot/index.d.ts @@ -1462,6 +1462,8 @@ interface JsonWebKey { qi?: string; oth?: RsaOtherPrimesInfo[]; k?: string; + pub?: string; + priv?: string; } interface RsaOtherPrimesInfo { r?: string; @@ -1508,6 +1510,7 @@ interface SubtleCryptoSignAlgorithm { hash?: string | SubtleCryptoHashAlgorithm; dataLength?: number; saltLength?: number; + context?: ArrayBuffer | ArrayBufferView; } interface CryptoKeyKeyAlgorithm { name: string; diff --git a/types/generated-snapshot/index.ts b/types/generated-snapshot/index.ts index 39357652538..ab439dcda62 100755 --- a/types/generated-snapshot/index.ts +++ b/types/generated-snapshot/index.ts @@ -1468,6 +1468,8 @@ export interface JsonWebKey { qi?: string; oth?: RsaOtherPrimesInfo[]; k?: string; + pub?: string; + priv?: string; } export interface RsaOtherPrimesInfo { r?: string; @@ -1514,6 +1516,7 @@ export interface SubtleCryptoSignAlgorithm { hash?: string | SubtleCryptoHashAlgorithm; dataLength?: number; saltLength?: number; + context?: ArrayBuffer | ArrayBufferView; } export interface CryptoKeyKeyAlgorithm { name: string;