From 59954cebd4b89b3555a9acc7596d37b7310475cb Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 24 Mar 2026 23:12:08 +0100 Subject: [PATCH 01/11] implement a subset of "Modern Algorithms in the Web Cryptography API" Assisted-by: Codex:gpt-5 Signed-off-by: Filip Skokan --- build/wpt_test.bzl | 11 +- deps/rust/Cargo.lock | 51 ++ deps/rust/Cargo.toml | 2 + src/rust/sha3/BUILD.bazel | 11 + src/rust/sha3/lib.rs | 99 +++ src/workerd/api/crypto/aes.c++ | 7 +- src/workerd/api/crypto/chacha20.c++ | 240 ++++++ src/workerd/api/crypto/crypto.c++ | 720 +++++++++++++++- src/workerd/api/crypto/crypto.h | 156 +++- src/workerd/api/crypto/digest.c++ | 4 +- src/workerd/api/crypto/ec.c++ | 16 +- src/workerd/api/crypto/hkdf.c++ | 2 +- src/workerd/api/crypto/impl.h | 23 + src/workerd/api/crypto/keys.c++ | 29 +- src/workerd/api/crypto/keys.h | 6 + src/workerd/api/crypto/mldsa.c++ | 796 ++++++++++++++++++ src/workerd/api/crypto/mlkem.c++ | 733 ++++++++++++++++ src/workerd/api/crypto/pbkdf2.c++ | 2 +- src/workerd/api/crypto/rsa.c++ | 20 + src/workerd/api/tests/crypto-extras-test.js | 90 ++ src/workerd/io/BUILD.bazel | 1 + src/wpt/BUILD.bazel | 1 + src/wpt/WebCryptoAPI-test.ts | 344 ++++---- src/wpt/harness/utils.ts | 48 +- .../experimental/index.d.ts | 69 +- .../generated-snapshot/experimental/index.ts | 69 +- types/generated-snapshot/index.d.ts | 69 +- types/generated-snapshot/index.ts | 69 +- 28 files changed, 3449 insertions(+), 239 deletions(-) create mode 100644 src/rust/sha3/BUILD.bazel create mode 100644 src/rust/sha3/lib.rs create mode 100644 src/workerd/api/crypto/chacha20.c++ create mode 100644 src/workerd/api/crypto/mldsa.c++ create mode 100644 src/workerd/api/crypto/mlkem.c++ 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/deps/rust/Cargo.lock b/deps/rust/Cargo.lock index 1be4329dbda..059c4dad254 100644 --- a/deps/rust/Cargo.lock +++ b/deps/rust/Cargo.lock @@ -92,6 +92,28 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "aws-lc-rs" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.41.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.22.1" @@ -301,6 +323,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.13.1" @@ -444,6 +475,8 @@ dependencies = [ "ada-url", "anyhow", "async-trait", + "aws-lc-rs", + "aws-lc-sys", "capnp", "capnp-rpc", "capnpc", @@ -507,6 +540,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.16.0" @@ -581,6 +620,12 @@ dependencies = [ "syn", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2342,6 +2387,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/deps/rust/Cargo.toml b/deps/rust/Cargo.toml index 91ff8b6657b..9b51eeda8fa 100644 --- a/deps/rust/Cargo.toml +++ b/deps/rust/Cargo.toml @@ -28,6 +28,8 @@ syn = { version = "2", features = ["full"] } ada-url = "3" anyhow = "1" async-trait = { version = "0", default-features = false } +aws-lc-rs = { version = "1", default-features = false, features = ["non-fips"] } +aws-lc-sys = { version = "0", default-features = false } capnp = "0" capnpc = "0" capnp-rpc = "0" diff --git a/src/rust/sha3/BUILD.bazel b/src/rust/sha3/BUILD.bazel new file mode 100644 index 00000000000..384f922ba4f --- /dev/null +++ b/src/rust/sha3/BUILD.bazel @@ -0,0 +1,11 @@ +load("//:build/wd_rust_crate.bzl", "wd_rust_crate") + +wd_rust_crate( + name = "sha3", + cxx_bridge_src = "lib.rs", + visibility = ["//visibility:public"], + deps = [ + "@crates_vendor//:aws-lc-rs", + "@crates_vendor//:aws-lc-sys", + ], +) diff --git a/src/rust/sha3/lib.rs b/src/rust/sha3/lib.rs new file mode 100644 index 00000000000..83d8ad262df --- /dev/null +++ b/src/rust/sha3/lib.rs @@ -0,0 +1,99 @@ +use std::ptr::null_mut; + +use aws_lc_rs::digest; +use aws_lc_sys::EVP_DigestFinalXOF; +use aws_lc_sys::EVP_DigestInit_ex; +use aws_lc_sys::EVP_DigestUpdate; +use aws_lc_sys::EVP_MD; +use aws_lc_sys::EVP_MD_CTX; +use aws_lc_sys::EVP_MD_CTX_free; +use aws_lc_sys::EVP_MD_CTX_new; +use aws_lc_sys::EVP_shake128; +use aws_lc_sys::EVP_shake256; + +#[cxx::bridge(namespace = "workerd::rust::sha3")] +mod ffi { + extern "Rust" { + fn sha3_256(data: &[u8]) -> Vec; + fn sha3_384(data: &[u8]) -> Vec; + fn sha3_512(data: &[u8]) -> Vec; + fn cshake128(data: &[u8], output_len: usize) -> Vec; + fn cshake256(data: &[u8], output_len: usize) -> Vec; + } +} + +#[must_use] +pub fn sha3_256(data: &[u8]) -> Vec { + digest::digest(&digest::SHA3_256, data).as_ref().to_vec() +} + +#[must_use] +pub fn sha3_384(data: &[u8]) -> Vec { + digest::digest(&digest::SHA3_384, data).as_ref().to_vec() +} + +#[must_use] +pub fn sha3_512(data: &[u8]) -> Vec { + digest::digest(&digest::SHA3_512, data).as_ref().to_vec() +} + +struct EvpMdCtx(*mut EVP_MD_CTX); + +impl EvpMdCtx { + fn new(md: *const EVP_MD) -> Self { + assert!(!md.is_null()); + + // SAFETY: EVP_MD_CTX_new does not take arguments and returns an owned context pointer or null. + let ctx = unsafe { EVP_MD_CTX_new() }; + assert!(!ctx.is_null()); + + // SAFETY: ctx is a non-null context created above, md is a non-null AWS-LC digest descriptor, + // and a null ENGINE pointer is accepted by EVP_DigestInit_ex. + let ok = unsafe { EVP_DigestInit_ex(ctx, md, null_mut()) }; + assert_eq!(ok, 1); + Self(ctx) + } +} + +impl Drop for EvpMdCtx { + fn drop(&mut self) { + // SAFETY: self.0 is exclusively owned by this wrapper and was returned by EVP_MD_CTX_new. + unsafe { + EVP_MD_CTX_free(self.0); + } + } +} + +type EvpMdFactory = unsafe extern "C" fn() -> *const EVP_MD; + +fn shake(data: &[u8], output_len: usize, md_factory: EvpMdFactory) -> Vec { + if output_len == 0 { + return Vec::new(); + } + + // SAFETY: md_factory is one of AWS-LC's EVP_shake* functions and returns a static descriptor. + let md = unsafe { md_factory() }; + let ctx = EvpMdCtx::new(md); + if !data.is_empty() { + // SAFETY: ctx owns a valid initialized EVP_MD_CTX, and data.as_ptr() is valid for data.len() + // bytes because data is non-empty. + let ok = unsafe { EVP_DigestUpdate(ctx.0, data.as_ptr().cast(), data.len()) }; + assert_eq!(ok, 1); + } + + let mut output = vec![0u8; output_len]; + // SAFETY: ctx owns a valid SHAKE EVP_MD_CTX, and output is non-empty and writable for output.len(). + let ok = unsafe { EVP_DigestFinalXOF(ctx.0, output.as_mut_ptr(), output.len()) }; + assert_eq!(ok, 1); + output +} + +#[must_use] +pub fn cshake128(data: &[u8], output_len: usize) -> Vec { + shake(data, output_len, EVP_shake128) +} + +#[must_use] +pub fn cshake256(data: &[u8], output_len: usize) -> Vec { + shake(data, output_len, EVP_shake256) +} diff --git a/src/workerd/api/crypto/aes.c++ b/src/workerd/api/crypto/aes.c++ index 8a059d9e1a7..09587ff2bdb 100644 --- a/src/workerd/api/crypto/aes.c++ +++ b/src/workerd/api/crypto/aes.c++ @@ -151,8 +151,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 +787,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/chacha20.c++ b/src/workerd/api/crypto/chacha20.c++ new file mode 100644 index 00000000000..b8353d4715e --- /dev/null +++ b/src/workerd/api/crypto/chacha20.c++ @@ -0,0 +1,240 @@ +#include "impl.h" + +#include +#include +#include + +#include +#include + +namespace workerd::api { +namespace { + +// ChaCha20-Poly1305 constants +constexpr size_t CHACHA20_POLY1305_KEY_SIZE = 32; // 256 bits +constexpr size_t CHACHA20_POLY1305_NONCE_SIZE = 12; // 96 bits +constexpr size_t CHACHA20_POLY1305_TAG_SIZE = 16; // 128 bits + +class ChaCha20Poly1305Key final: public CryptoKey::Impl { + public: + explicit ChaCha20Poly1305Key(kj::Array keyData, + kj::StringPtr algorithmName, + bool extractable, + CryptoKeyUsageSet usages) + : CryptoKey::Impl(extractable, usages), + keyData(kj::mv(keyData)), + algorithmName(algorithmName) {} + + private: + kj::StringPtr getAlgorithmName() const override { + return algorithmName; + } + + CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override { + return CryptoKey::KeyAlgorithm{algorithmName}; + } + + bool equals(const CryptoKey::Impl& other) const override { + return this == &other || (other.getType() == "secret"_kj && other.equals(keyData)); + } + + bool equals(const kj::Array& other) const override { + return keyData.size() == other.size() && + CRYPTO_memcmp(keyData.begin(), other.begin(), keyData.size()) == 0; + } + + SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override { + JSG_REQUIRE(format == "raw-secret" || format == "jwk", DOMNotSupportedError, + "ChaCha20-Poly1305 key only supports exporting \"raw-secret\" & \"jwk\", not \"", format, + "\"."); + + if (format == "jwk") { + SubtleCrypto::JsonWebKey jwk; + jwk.kty = kj::str("oct"); + jwk.k = fastEncodeBase64Url(keyData); + jwk.alg = kj::str("C20P"); + jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); }); + jwk.ext = true; + return jwk; + } + + return jsg::JsArrayBuffer::create(js, keyData).addRef(js); + } + + jsg::JsArrayBuffer encrypt(jsg::Lock& js, + SubtleCrypto::EncryptAlgorithm&& algorithm, + kj::ArrayPtr plainText) const override { + auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".") + .getHandle(js); + JSG_REQUIRE(iv.size() == CHACHA20_POLY1305_NONCE_SIZE, DOMOperationError, + "ChaCha20-Poly1305 IV must be 12 bytes (provided ", iv.size(), ")."); + + KJ_IF_SOME(tagLength, algorithm.tagLength) { + JSG_REQUIRE(tagLength == 128, DOMOperationError, + "ChaCha20-Poly1305 tag length must be 128 (provided ", tagLength, ")."); + } + + kj::ArrayPtr additionalData = nullptr; + KJ_IF_SOME(sourceRef, algorithm.additionalData) { + auto source = sourceRef.getHandle(js); + additionalData = source.asArrayPtr(); + } + + auto aeadCtx = kj::disposeWith(EVP_AEAD_CTX_new( + EVP_aead_chacha20_poly1305(), keyData.begin(), keyData.size(), CHACHA20_POLY1305_TAG_SIZE)); + KJ_ASSERT(aeadCtx.get() != nullptr); + + auto maxOutLen = plainText.size() + CHACHA20_POLY1305_TAG_SIZE; + auto cipherText = jsg::JsArrayBuffer::create(js, maxOutLen); + + size_t outLen = 0; + JSG_REQUIRE(EVP_AEAD_CTX_seal(aeadCtx.get(), cipherText.asArrayPtr().begin(), &outLen, + maxOutLen, iv.asArrayPtr().begin(), iv.size(), plainText.begin(), + plainText.size(), additionalData.begin(), additionalData.size()), + DOMOperationError, "ChaCha20-Poly1305 encryption failed", internalDescribeOpensslErrors()); + KJ_ASSERT(outLen == maxOutLen); + + return cipherText; + } + + jsg::JsArrayBuffer decrypt(jsg::Lock& js, + SubtleCrypto::EncryptAlgorithm&& algorithm, + kj::ArrayPtr cipherText) const override { + auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".") + .getHandle(js); + JSG_REQUIRE(iv.size() == CHACHA20_POLY1305_NONCE_SIZE, DOMOperationError, + "ChaCha20-Poly1305 IV must be 12 bytes (provided ", iv.size(), ")."); + + KJ_IF_SOME(tagLength, algorithm.tagLength) { + JSG_REQUIRE(tagLength == 128, DOMOperationError, + "ChaCha20-Poly1305 tag length must be 128 (provided ", tagLength, ")."); + } + + JSG_REQUIRE(cipherText.size() >= CHACHA20_POLY1305_TAG_SIZE, DOMOperationError, + "Ciphertext is too short to contain a valid ChaCha20-Poly1305 authentication tag."); + + kj::ArrayPtr additionalData = nullptr; + KJ_IF_SOME(sourceRef, algorithm.additionalData) { + auto source = sourceRef.getHandle(js); + additionalData = source.asArrayPtr(); + } + + auto aeadCtx = kj::disposeWith(EVP_AEAD_CTX_new( + EVP_aead_chacha20_poly1305(), keyData.begin(), keyData.size(), CHACHA20_POLY1305_TAG_SIZE)); + KJ_ASSERT(aeadCtx.get() != nullptr); + + auto maxOutLen = cipherText.size(); + auto plainText = jsg::JsArrayBuffer::create(js, maxOutLen); + + size_t outLen = 0; + JSG_REQUIRE(EVP_AEAD_CTX_open(aeadCtx.get(), plainText.asArrayPtr().begin(), &outLen, maxOutLen, + iv.asArrayPtr().begin(), iv.size(), cipherText.begin(), cipherText.size(), + additionalData.begin(), additionalData.size()), + DOMOperationError, "ChaCha20-Poly1305 decryption failed."); + + return jsg::JsArrayBuffer::create(js, plainText.asArrayPtr().first(outLen)); + } + + kj::StringPtr jsgGetMemoryName() const override { + return "ChaCha20Poly1305Key"; + } + size_t jsgGetMemorySelfSize() const override { + return sizeof(ChaCha20Poly1305Key); + } + void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override { + tracker.trackFieldWithSize("keyData", keyData.size()); + } + + ZeroOnFree keyData; + kj::StringPtr algorithmName; +}; + +} // namespace + +kj::OneOf, CryptoKeyPair> CryptoKey::Impl::generateChaCha20Poly1305( + jsg::Lock& js, + kj::StringPtr normalizedName, + SubtleCrypto::GenerateKeyAlgorithm&& algorithm, + bool extractable, + kj::ArrayPtr keyUsages) { + CryptoKeyUsageSet validUsages = CryptoKeyUsageSet::encrypt() | CryptoKeyUsageSet::decrypt() | + CryptoKeyUsageSet::wrapKey() | CryptoKeyUsageSet::unwrapKey(); + auto usages = CryptoKeyUsageSet::validate( + normalizedName, CryptoKeyUsageSet::Context::generate, keyUsages, validUsages); + + auto keyDataArray = kj::heapArray(CHACHA20_POLY1305_KEY_SIZE); + IoContext::current().getEntropySource().generate(keyDataArray); + + return js.alloc( + kj::heap(kj::mv(keyDataArray), normalizedName, extractable, usages)); +} + +kj::Own CryptoKey::Impl::importChaCha20Poly1305(jsg::Lock& js, + kj::StringPtr normalizedName, + kj::StringPtr format, + SubtleCrypto::ImportKeyData keyData, + SubtleCrypto::ImportKeyAlgorithm&& algorithm, + bool extractable, + kj::ArrayPtr keyUsages) { + CryptoKeyUsageSet validUsages = CryptoKeyUsageSet::encrypt() | CryptoKeyUsageSet::decrypt() | + CryptoKeyUsageSet::wrapKey() | CryptoKeyUsageSet::unwrapKey(); + auto usages = CryptoKeyUsageSet::validate( + normalizedName, CryptoKeyUsageSet::Context::importSecret, keyUsages, validUsages); + + kj::Array keyDataArray; + + if (format == "raw-secret") { + keyDataArray = kj::mv(keyData.get>()); + JSG_REQUIRE(keyDataArray.size() == CHACHA20_POLY1305_KEY_SIZE, DOMDataError, + "ChaCha20-Poly1305 key must be 256 bits (provided ", keyDataArray.size() * 8, ")."); + } else if (format == "jwk") { + auto& keyDataJwk = keyData.get(); + JSG_REQUIRE(keyDataJwk.kty == "oct", DOMDataError, + "Symmetric \"jwk\" key import requires a JSON Web Key with Key Type parameter " + "\"kty\" equal to \"oct\" (encountered \"", + keyDataJwk.kty, "\")."); + + keyDataArray = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.k), DOMDataError, + "Symmetric \"jwk\" key import requires a base64Url encoding of the key."); + + JSG_REQUIRE(keyDataArray.size() == CHACHA20_POLY1305_KEY_SIZE, DOMDataError, + "ChaCha20-Poly1305 key must be 256 bits (provided ", keyDataArray.size() * 8, ")."); + + KJ_IF_SOME(alg, keyDataJwk.alg) { + JSG_REQUIRE(alg == "C20P", DOMDataError, + "Symmetric \"jwk\" key contains invalid \"alg\" value \"", alg, "\", expected \"C20P\"."); + } + + if (keyUsages.size() != 0) { + KJ_IF_SOME(u, keyDataJwk.use) { + JSG_REQUIRE(u == "enc", DOMDataError, + "Symmetric \"jwk\" key must have a \"use\" of \"enc\", not \"", u, "\"."); + } + } + + KJ_IF_SOME(ops, keyDataJwk.key_ops) { + std::sort(ops.begin(), ops.end()); + auto duplicate = std::adjacent_find(ops.begin(), ops.end()); + JSG_REQUIRE(duplicate == ops.end(), DOMDataError, + "Symmetric \"jwk\" key contains duplicate value \"", *duplicate, "\", in \"key_op\"."); + + for (const auto& usage: keyUsages) { + JSG_REQUIRE(std::binary_search(ops.begin(), ops.end(), usage), DOMDataError, + "\"jwk\" key missing usage \"", usage, "\", in \"key_ops\"."); + } + } + + KJ_IF_SOME(e, keyDataJwk.ext) { + JSG_REQUIRE(e || !extractable, DOMDataError, "\"jwk\" key has value \"", e ? "true" : "false", + "\", for \"ext\" that is incompatible " + "with import extractability value \"", + extractable ? "true" : "false", "\"."); + } + } else { + JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unrecognized key import format \"", format, "\"."); + } + + return kj::heap(kj::mv(keyDataArray), normalizedName, extractable, usages); +} + +} // namespace workerd::api diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index 7495a5c65dd..a850c58e625 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 @@ -12,6 +13,7 @@ #include #include #include +#include #include #include @@ -20,6 +22,7 @@ #include #include +#include #include #include #include @@ -44,6 +47,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 +62,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 +139,13 @@ 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}, + {"ChaCha20-Poly1305"_kj, &CryptoKey::Impl::importChaCha20Poly1305, + &CryptoKey::Impl::generateChaCha20Poly1305}, }; auto iter = ALGORITHMS.find(CryptoAlgorithm{name}); @@ -176,6 +191,7 @@ kj::Maybe getKeyLength(const SubtleCrypto::ImportKeyAlgorithm& derived {"AES-CBC"}, {"AES-GCM"}, {"AES-KW"}, + {"ChaCha20-Poly1305"}, {"HMAC"}, {"HKDF"}, {"PBKDF2"}, @@ -203,6 +219,8 @@ kj::Maybe getKeyLength(const SubtleCrypto::ImportKeyAlgorithm& derived "Derived AES key must be 128, 192, or 256 bits in length but provided ", length, "."); } return length; + } else if (*algIter == "ChaCha20-Poly1305") { + return 256; } else if (*algIter == "HMAC") { KJ_IF_SOME(length, derivedKeyAlgorithm.length) { // If the user requested a specific HMAC key length, honor it. @@ -226,6 +244,346 @@ 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 validateOmittedOrEmptyBufferSource(jsg::Lock& js, + const jsg::Optional>& value, + kj::StringPtr name) { + KJ_IF_SOME(ref, value) { + JSG_REQUIRE( + ref.getHandle(js).size() == 0, DOMNotSupportedError, name, " must be omitted or empty."); + } +} + +void validateSha3DerivedDigest(jsg::Lock& js, const SubtleCrypto::DigestAlgorithm& algorithm) { + if (strcasecmp(algorithm.name.cStr(), "cSHAKE128") == 0 || + strcasecmp(algorithm.name.cStr(), "cSHAKE256") == 0) { + auto outputLengthBits = JSG_REQUIRE_NONNULL( + algorithm.outputLength, DOMOperationError, "outputLength is required for cSHAKE"); + JSG_REQUIRE(outputLengthBits >= 0 && outputLengthBits % 8 == 0, DOMOperationError, + "outputLength must be a non-negative multiple of 8"); + + validateOmittedOrEmptyBufferSource(js, algorithm.functionName, "functionName"); + validateOmittedOrEmptyBufferSource(js, algorithm.customization, "customization"); + return; + } + + JSG_REQUIRE(strcasecmp(algorithm.name.cStr(), "SHA3-256") == 0 || + strcasecmp(algorithm.name.cStr(), "SHA3-384") == 0 || + strcasecmp(algorithm.name.cStr(), "SHA3-512") == 0, + DOMNotSupportedError, "Unrecognized or unimplemented digest algorithm requested."); +} + +bool supportsDigestAlgorithm(jsg::Lock& js, const SubtleCrypto::DigestAlgorithm& algorithm) { + KJ_IF_SOME(exception, kj::runCatchingExceptions([&] { lookupDigestAlgorithm(algorithm.name); })) { + (void)exception; + validateSha3DerivedDigest(js, algorithm); + } + return true; +} + +void validateAesKeyLength(int length) { + JSG_REQUIRE(length == 128 || length == 192 || length == 256, DOMOperationError, + "AES key length must be 128, 192, or 256 bits."); +} + +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, "."); + } +} + +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); + return 521; +} + +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, "ChaCha20-Poly1305"_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, "ChaCha20-Poly1305"_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."); + } else if (normalizedName == "ChaCha20-Poly1305") { + auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".") + .getHandle(js); + JSG_REQUIRE(iv.size() == 12, DOMOperationError, "ChaCha20-Poly1305 IV must be 12 bytes."); + KJ_IF_SOME(tagLength, algorithm.tagLength) { + JSG_REQUIRE(tagLength == 128, DOMOperationError, "ChaCha20-Poly1305 tag length must be 128."); + } + } +} + +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 @@ -394,19 +752,58 @@ jsg::Promise SubtleCrypto::verify(jsg::Lock& js, }); } -jsg::Promise> SubtleCrypto::digest( - jsg::Lock& js, kj::OneOf algorithmParam, jsg::JsBufferSource data) { +jsg::Promise> SubtleCrypto::digest(jsg::Lock& js, + kj::OneOf algorithmParam, + jsg::JsBufferSource data) { auto algorithm = interpretAlgorithmParam(kj::mv(algorithmParam)); auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, algorithm); return js.evalNow([&] { + auto ptr = nonNullBytes(data.asArrayPtr()); + + if (strcasecmp(algorithm.name.cStr(), "cSHAKE128") == 0 || + strcasecmp(algorithm.name.cStr(), "cSHAKE256") == 0) { + auto outputLengthBits = JSG_REQUIRE_NONNULL( + algorithm.outputLength, DOMOperationError, "outputLength is required for cSHAKE"); + JSG_REQUIRE(outputLengthBits >= 0 && outputLengthBits % 8 == 0, DOMOperationError, + "outputLength must be a non-negative multiple of 8"); + auto outputLengthBytes = static_cast(outputLengthBits / 8); + + validateOmittedOrEmptyBufferSource(js, algorithm.functionName, "functionName"); + validateOmittedOrEmptyBufferSource(js, algorithm.customization, "customization"); + + auto input = ::rust::Slice(ptr.begin(), ptr.size()); + auto result = strcasecmp(algorithm.name.cStr(), "cSHAKE128") == 0 + ? workerd::rust::sha3::cshake128(input, outputLengthBytes) + : workerd::rust::sha3::cshake256(input, outputLengthBytes); + auto buf = jsg::JsArrayBuffer::create(js, result.size()); + memcpy(buf.asArrayPtr().begin(), result.data(), result.size()); + return buf.addRef(js); + } + + auto sha3Fn = [&]() -> kj::Maybe<::rust::Vec (*)(::rust::Slice)> { + if (strcasecmp(algorithm.name.cStr(), "SHA3-256") == 0) { + return workerd::rust::sha3::sha3_256; + } else if (strcasecmp(algorithm.name.cStr(), "SHA3-384") == 0) { + return workerd::rust::sha3::sha3_384; + } else if (strcasecmp(algorithm.name.cStr(), "SHA3-512") == 0) { + return workerd::rust::sha3::sha3_512; + } + return kj::none; + }(); + KJ_IF_SOME(fn, sha3Fn) { + auto result = fn({ptr.begin(), ptr.size()}); + auto buf = jsg::JsArrayBuffer::create(js, result.size()); + memcpy(buf.asArrayPtr().begin(), result.data(), result.size()); + return buf.addRef(js); + } + auto type = lookupDigestAlgorithm(algorithm.name).second; auto digestCtx = kj::disposeWith(EVP_MD_CTX_new()); KJ_ASSERT(digestCtx.get() != nullptr); - auto ptr = nonNullBytes(data.asArrayPtr()); OSSLCALL(EVP_DigestInit_ex(digestCtx.get(), type, nullptr)); OSSLCALL(EVP_DigestUpdate(digestCtx.get(), ptr.begin(), ptr.size())); @@ -473,8 +870,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 +1006,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 +1064,309 @@ 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 sharedKeyRef = importKeySync(js, "raw-secret", sharedKey.copy(), + 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); + return importKeySync(js, "raw-secret", secret.copy(), 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)) { + 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); + return supportsDigestAlgorithm(js, normalizedAlgorithm); + } + + 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); + } + } + } + + kj::Maybe maybeLength = kj::none; + KJ_IF_SOME(thirdArgument, lengthOrAdditionalAlgorithm) { + maybeLength = readSupportsLength(js, thirdArgument); + } + return checkSupportForAlgorithm(parsedOperation, algorithm, maybeLength); + }, [](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..27035e1bd12 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; }; // ======================================================================================= @@ -350,6 +363,18 @@ class SubtleCrypto: public jsg::Object { JSG_STRUCT(name); }; + // Type of the `algorithm` parameter passed to `digest()` specifically. + // Extends HashAlgorithm's fields with optional parameters for XOF algorithms. + struct DigestAlgorithm { + kj::String name; + jsg::Optional outputLength; + jsg::Optional domainSeparation; + jsg::Optional> functionName; + jsg::Optional> customization; + + JSG_STRUCT(name, outputLength, domainSeparation, functionName, customization); + }; + // Type of the `algorithm` parameter passed to `encrypt()` and `decrypt()`. Different // algorithms call for different fields. struct EncryptAlgorithm { @@ -398,7 +423,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 +543,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 }; @@ -541,8 +574,9 @@ class SubtleCrypto: public jsg::Object { jsg::JsBufferSource signature, jsg::JsBufferSource data); - jsg::Promise> digest( - jsg::Lock& js, kj::OneOf algorithm, jsg::JsBufferSource data); + jsg::Promise> digest(jsg::Lock& js, + kj::OneOf algorithm, + jsg::JsBufferSource data); jsg::Promise, CryptoKeyPair>> generateKey(jsg::Lock& js, kj::OneOf algorithm, @@ -598,10 +632,67 @@ 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_STATIC_METHOD(supports); + JSG_METHOD(encrypt); JSG_METHOD(decrypt); JSG_METHOD(sign); @@ -614,9 +705,20 @@ class SubtleCrypto: public jsg::Object { JSG_METHOD(exportKey); JSG_METHOD(wrapKey); JSG_METHOD(unwrapKey); + JSG_METHOD(encapsulateKey); + JSG_METHOD(encapsulateBits); + JSG_METHOD(decapsulateKey); + JSG_METHOD(decapsulateBits); + JSG_METHOD(getPublicKey); JSG_METHOD(timingSafeEqual); JSG_TS_OVERRIDE({ + static supports(operation: string, + algorithm: string | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm | SubtleCryptoDigestAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, + length?: number | null): boolean; + static supports(operation: string, + algorithm: string | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm | SubtleCryptoDigestAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, + additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm): boolean; wrapKey(format: string, key: CryptoKey, wrappingKey: CryptoKey, @@ -626,9 +728,11 @@ class SubtleCrypto: public jsg::Object { baseKey : CryptoKey, length? : number | null) : Promise; - digest(algorithm: string | SubtleCryptoHashAlgorithm, + digest(algorithm: string | SubtleCryptoDigestAlgorithm, data: ArrayBuffer | ArrayBufferView) : Promise; + // SubtleCryptoDigestAlgorithm.functionName: ArrayBuffer | ArrayBufferView + // SubtleCryptoDigestAlgorithm.customization: ArrayBuffer | ArrayBufferView sign(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, data: ArrayBuffer | ArrayBufferView) @@ -642,6 +746,28 @@ class SubtleCrypto: public jsg::Object { 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; }); } }; @@ -776,13 +902,15 @@ 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, \ - api::CryptoKey::KeyAlgorithm, api::CryptoKey::AesKeyAlgorithm, \ - api::CryptoKey::HmacKeyAlgorithm, api::CryptoKey::RsaKeyAlgorithm, \ - api::CryptoKey::EllipticKeyAlgorithm, api::CryptoKey::ArbitraryKeyAlgorithm, \ - api::CryptoKey::AsymmetricKeyDetails, api::DigestStream + api::SubtleCrypto::DigestAlgorithm, api::SubtleCrypto::ImportKeyAlgorithm, \ + api::SubtleCrypto::SignAlgorithm, api::CryptoKey::KeyAlgorithm, \ + api::CryptoKey::AesKeyAlgorithm, api::CryptoKey::HmacKeyAlgorithm, \ + api::CryptoKey::RsaKeyAlgorithm, api::CryptoKey::EllipticKeyAlgorithm, \ + api::CryptoKey::ArbitraryKeyAlgorithm, api::CryptoKey::AsymmetricKeyDetails, \ + api::DigestStream } // namespace workerd::api 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..cef8cb9915e 100644 --- a/src/workerd/api/crypto/impl.h +++ b/src/workerd/api/crypto/impl.h @@ -122,6 +122,9 @@ class CryptoKey::Impl { static ImportFunc importEcdh; static ImportFunc importEddsa; static ImportFunc importRsaRaw; + static ImportFunc importMlDsa; + static ImportFunc importMlKem; + static ImportFunc importChaCha20Poly1305; using GenerateFunc = kj::OneOf, CryptoKeyPair>(jsg::Lock& js, kj::StringPtr normalizedName, @@ -135,6 +138,9 @@ class CryptoKey::Impl { static GenerateFunc generateEcdsa; static GenerateFunc generateEcdh; static GenerateFunc generateEddsa; + static GenerateFunc generateMlDsa; + static GenerateFunc generateMlKem; + static GenerateFunc generateChaCha20Poly1305; Impl(bool extractable, CryptoKeyUsageSet usages): extractable(extractable), usages(usages) {} @@ -182,6 +188,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..063139720ff --- /dev/null +++ b/src/workerd/api/crypto/mldsa.c++ @@ -0,0 +1,796 @@ +#include "impl.h" + +#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()); + } + } + + 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 publicKeyBytes.size() == otherMlDsa->publicKeyBytes.size() && + 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 thisSeed.size() == otherSeed.size() && + 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."); + + // Parse the stored public key bytes into a PublicKey struct + typename P::PublicKey pk; + CBS cbs; + CBS_init(&cbs, publicKeyBytes.begin(), publicKeyBytes.size()); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), InternalDOMOperationError, + "Failed to parse public key from private 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))); + + 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); + + // Parse public key for the public CryptoKey + typename P::PublicKey pk; + CBS cbs; + CBS_init(&cbs, encodedPublicKey, P::PUBLIC_KEY_BYTES); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), InternalDOMOperationError, + "Failed to parse generated ML-DSA 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"); + + // 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."); + + // 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, "."); + + KJ_IF_SOME(alg, jwk.alg) { + JSG_REQUIRE(alg == normalizedName, DOMDataError, "JWK \"alg\" field \"", alg, + "\" does not match \"", normalizedName, "\"."); + } + + KJ_IF_SOME(use, jwk.use) { + JSG_REQUIRE(use == "sig", DOMDataError, "JWK \"use\" field must be \"sig\" for ", + normalizedName, "."); + } + + 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."); + + // 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 (present for both public and private keys) + 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& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, + "Import data for raw-public must be a buffer."); + return MlDsaKey

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

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

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

::importPkcs8(keyBytes, 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..c5a27985ad6 --- /dev/null +++ b/src/workerd/api/crypto/mlkem.c++ @@ -0,0 +1,733 @@ +#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()); + } + } + + 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 publicKeyBytes.size() == otherMlKem->publicKeyBytes.size() && + 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 thisSeed.size() == otherSeed.size() && + 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."); + + // Parse the stored public key bytes into a PublicKey struct + typename P::PublicKey pk; + CBS cbs; + CBS_init(&cbs, publicKeyBytes.begin(), publicKeyBytes.size()); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), InternalDOMOperationError, + "Failed to parse public key from private 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[MLKEM_SEED_BYTES]; + typename P::PrivateKey sk; + P::generateKey(encodedPublicKey, seedBuf, &sk); + KJ_DEFER(OPENSSL_cleanse(seedBuf, sizeof(seedBuf))); + + 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); + + // Parse public key for the public CryptoKey + typename P::PublicKey pk; + CBS cbs; + CBS_init(&cbs, encodedPublicKey, P::PUBLIC_KEY_BYTES); + JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), InternalDOMOperationError, + "Failed to parse generated ML-KEM 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() == 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"); + + // 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."); + + // 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."); + + 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 (present for both public and private keys) + 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& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, + "Import data for raw-public must be a buffer."); + return MlKemKey

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

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

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

::importPkcs8(keyBytes, 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/crypto-extras-test.js b/src/workerd/api/tests/crypto-extras-test.js index 4b700bece06..ec4f0519686 100644 --- a/src/workerd/api/tests/crypto-extras-test.js +++ b/src/workerd/api/tests/crypto-extras-test.js @@ -216,6 +216,96 @@ export const deriveBitsNullLength = { }, }; +export const subtleCryptoSupportsSha3DerivedDigests = { + async test() { + async function rejectsNotSupported(algorithm) { + let threw = false; + try { + await crypto.subtle.digest(algorithm, new Uint8Array(0)); + } catch (err) { + threw = true; + strictEqual(err.name, 'NotSupportedError'); + } + ok(threw, 'Expected digest operation to reject'); + } + + ok(SubtleCrypto.supports('digest', 'SHA3-256')); + ok( + SubtleCrypto.supports('digest', { + name: 'cSHAKE128', + outputLength: 256, + }) + ); + ok( + SubtleCrypto.supports('digest', { + name: 'cSHAKE128', + outputLength: 256, + functionName: new Uint8Array(0), + }) + ); + ok( + SubtleCrypto.supports('digest', { + name: 'cSHAKE128', + outputLength: 256, + customization: new Uint8Array(0), + }) + ); + + strictEqual( + SubtleCrypto.supports('digest', { + name: 'cSHAKE128', + outputLength: 256, + functionName: new Uint8Array([1]), + }), + false + ); + strictEqual( + SubtleCrypto.supports('digest', { + name: 'cSHAKE128', + outputLength: 256, + customization: new Uint8Array([1]), + }), + false + ); + strictEqual( + SubtleCrypto.supports('digest', { + name: 'TurboSHAKE128', + outputLength: 256, + }), + false + ); + + strictEqual( + ( + await crypto.subtle.digest( + { + name: 'cSHAKE128', + outputLength: 256, + functionName: new Uint8Array(0), + customization: new Uint8Array(0), + }, + new Uint8Array(0) + ) + ).byteLength, + 32 + ); + await rejectsNotSupported({ + name: 'cSHAKE128', + outputLength: 256, + functionName: new Uint8Array([1]), + }); + await rejectsNotSupported({ + name: 'cSHAKE128', + outputLength: 256, + customization: new Uint8Array([1]), + }); + await rejectsNotSupported({ + name: 'TurboSHAKE128', + outputLength: 256, + }); + }, +}; + export const aesCounterOverflowTest = { async test() { // Regression test: Check that the input counter is not modified when it overflows in the diff --git a/src/workerd/io/BUILD.bazel b/src/workerd/io/BUILD.bazel index 41dd81da890..d7f1dca1be6 100644 --- a/src/workerd/io/BUILD.bazel +++ b/src/workerd/io/BUILD.bazel @@ -69,6 +69,7 @@ wd_cc_library( implementation_deps = [ "//src/per_isolate", "//src/rust/jsg", + "//src/rust/sha3", "//src/workerd/api:crypto-crc-impl", "//src/workerd/api:data-url", "//src/workerd/api/node:exceptions", diff --git a/src/wpt/BUILD.bazel b/src/wpt/BUILD.bazel index 14dd77e2b02..d562389a5b0 100644 --- a/src/wpt/BUILD.bazel +++ b/src/wpt/BUILD.bazel @@ -100,6 +100,7 @@ wpt_test( size = "enormous", compat_flags = ["disable_fast_jsg_struct"], 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..2f86daccf32 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': {}, 'digest/digest.https.any.js': { comment: 'They expect TypeError, we have NotSupportedError', expectedFailures: [ @@ -66,35 +68,36 @@ 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': {}, + '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': {}, + '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 +114,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 +127,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 +156,37 @@ 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': { + comment: 'Wrong type of error returned', + expectedFailures: [/^(Empty|Bad) algorithm:/], }, - + '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': {}, + '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,49 @@ 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': {}, + '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 +286,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 +314,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 +329,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 +343,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 +369,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 +382,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..7a617c399bd 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -1324,6 +1324,30 @@ declare abstract class Crypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ declare abstract class SubtleCrypto { + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoDigestAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + length?: number | null, + ): boolean; + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoDigestAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + ): boolean; /** * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. * @@ -1371,7 +1395,7 @@ declare abstract class SubtleCrypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoHashAlgorithm, + algorithm: string | SubtleCryptoDigestAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1449,6 +1473,31 @@ declare abstract class SubtleCrypto { extractable: boolean, keyUsages: string[], ): 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; 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; @@ -1549,6 +1608,13 @@ interface SubtleCryptoGenerateKeyAlgorithm { interface SubtleCryptoHashAlgorithm { name: string; } +interface SubtleCryptoDigestAlgorithm { + name: string; + outputLength?: number; + domainSeparation?: number; + functionName?: ArrayBuffer | ArrayBufferView; + customization?: ArrayBuffer | ArrayBufferView; +} interface SubtleCryptoImportKeyAlgorithm { name: string; hash?: string | SubtleCryptoHashAlgorithm; @@ -1561,6 +1627,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..d1c7c76b510 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -1330,6 +1330,30 @@ export declare abstract class Crypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ export declare abstract class SubtleCrypto { + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoDigestAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + length?: number | null, + ): boolean; + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoDigestAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + ): boolean; /** * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. * @@ -1377,7 +1401,7 @@ export declare abstract class SubtleCrypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoHashAlgorithm, + algorithm: string | SubtleCryptoDigestAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1455,6 +1479,31 @@ export declare abstract class SubtleCrypto { extractable: boolean, keyUsages: string[], ): 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; 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; @@ -1555,6 +1614,13 @@ export interface SubtleCryptoGenerateKeyAlgorithm { export interface SubtleCryptoHashAlgorithm { name: string; } +export interface SubtleCryptoDigestAlgorithm { + name: string; + outputLength?: number; + domainSeparation?: number; + functionName?: ArrayBuffer | ArrayBufferView; + customization?: ArrayBuffer | ArrayBufferView; +} export interface SubtleCryptoImportKeyAlgorithm { name: string; hash?: string | SubtleCryptoHashAlgorithm; @@ -1567,6 +1633,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..6257231eb2c 100755 --- a/types/generated-snapshot/index.d.ts +++ b/types/generated-snapshot/index.d.ts @@ -1271,6 +1271,30 @@ declare abstract class Crypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ declare abstract class SubtleCrypto { + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoDigestAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + length?: number | null, + ): boolean; + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoDigestAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + ): boolean; /** * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. * @@ -1318,7 +1342,7 @@ declare abstract class SubtleCrypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoHashAlgorithm, + algorithm: string | SubtleCryptoDigestAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1396,6 +1420,31 @@ declare abstract class SubtleCrypto { extractable: boolean, keyUsages: string[], ): 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; timingSafeEqual( a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView, @@ -1462,12 +1511,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; @@ -1496,6 +1555,13 @@ interface SubtleCryptoGenerateKeyAlgorithm { interface SubtleCryptoHashAlgorithm { name: string; } +interface SubtleCryptoDigestAlgorithm { + name: string; + outputLength?: number; + domainSeparation?: number; + functionName?: ArrayBuffer | ArrayBufferView; + customization?: ArrayBuffer | ArrayBufferView; +} interface SubtleCryptoImportKeyAlgorithm { name: string; hash?: string | SubtleCryptoHashAlgorithm; @@ -1508,6 +1574,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..6d76b84ef69 100755 --- a/types/generated-snapshot/index.ts +++ b/types/generated-snapshot/index.ts @@ -1277,6 +1277,30 @@ export declare abstract class Crypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ export declare abstract class SubtleCrypto { + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoDigestAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + length?: number | null, + ): boolean; + static supports( + operation: string, + algorithm: + | string + | SubtleCryptoGenerateKeyAlgorithm + | SubtleCryptoImportKeyAlgorithm + | SubtleCryptoDeriveKeyAlgorithm + | SubtleCryptoDigestAlgorithm + | SubtleCryptoEncryptAlgorithm + | SubtleCryptoSignAlgorithm, + additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, + ): boolean; /** * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. * @@ -1324,7 +1348,7 @@ export declare abstract class SubtleCrypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoHashAlgorithm, + algorithm: string | SubtleCryptoDigestAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1402,6 +1426,31 @@ export declare abstract class SubtleCrypto { extractable: boolean, keyUsages: string[], ): 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; timingSafeEqual( a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView, @@ -1468,12 +1517,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; @@ -1502,6 +1561,13 @@ export interface SubtleCryptoGenerateKeyAlgorithm { export interface SubtleCryptoHashAlgorithm { name: string; } +export interface SubtleCryptoDigestAlgorithm { + name: string; + outputLength?: number; + domainSeparation?: number; + functionName?: ArrayBuffer | ArrayBufferView; + customization?: ArrayBuffer | ArrayBufferView; +} export interface SubtleCryptoImportKeyAlgorithm { name: string; hash?: string | SubtleCryptoHashAlgorithm; @@ -1514,6 +1580,7 @@ export interface SubtleCryptoSignAlgorithm { hash?: string | SubtleCryptoHashAlgorithm; dataLength?: number; saltLength?: number; + context?: ArrayBuffer | ArrayBufferView; } export interface CryptoKeyKeyAlgorithm { name: string; From 7bcba9348380ff4becf5b16edd455e11a9dd3b6d Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Thu, 9 Jul 2026 14:33:16 +0200 Subject: [PATCH 02/11] Scope modern crypto subset Keep ML-KEM, ML-DSA, helper APIs, and JWK support. Defer SHA3, ChaCha20-Poly1305, and aws-lc Rust deps. Co-authored-by: Filip Skokan --- deps/rust/Cargo.lock | 51 ---- deps/rust/Cargo.toml | 2 - src/rust/sha3/BUILD.bazel | 11 - src/rust/sha3/lib.rs | 99 -------- src/workerd/api/crypto/chacha20.c++ | 240 ------------------ src/workerd/api/crypto/crypto.c++ | 107 +------- src/workerd/api/crypto/crypto.h | 38 +-- src/workerd/api/crypto/impl.h | 2 - src/workerd/api/tests/crypto-extras-test.js | 90 ------- src/workerd/io/BUILD.bazel | 1 - src/wpt/WebCryptoAPI-test.ts | 19 +- .../experimental/index.d.ts | 13 +- .../generated-snapshot/experimental/index.ts | 13 +- types/generated-snapshot/index.d.ts | 13 +- types/generated-snapshot/index.ts | 13 +- 15 files changed, 45 insertions(+), 667 deletions(-) delete mode 100644 src/rust/sha3/BUILD.bazel delete mode 100644 src/rust/sha3/lib.rs delete mode 100644 src/workerd/api/crypto/chacha20.c++ diff --git a/deps/rust/Cargo.lock b/deps/rust/Cargo.lock index 059c4dad254..1be4329dbda 100644 --- a/deps/rust/Cargo.lock +++ b/deps/rust/Cargo.lock @@ -92,28 +92,6 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" -[[package]] -name = "aws-lc-rs" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00" -dependencies = [ - "aws-lc-sys", - "zeroize", -] - -[[package]] -name = "aws-lc-sys" -version = "0.41.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4" -dependencies = [ - "cc", - "cmake", - "dunce", - "fs_extra", -] - [[package]] name = "base64" version = "0.22.1" @@ -323,15 +301,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" -[[package]] -name = "cmake" -version = "0.1.58" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] - [[package]] name = "codespan-reporting" version = "0.13.1" @@ -475,8 +444,6 @@ dependencies = [ "ada-url", "anyhow", "async-trait", - "aws-lc-rs", - "aws-lc-sys", "capnp", "capnp-rpc", "capnpc", @@ -540,12 +507,6 @@ dependencies = [ "dtoa", ] -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - [[package]] name = "either" version = "1.16.0" @@ -620,12 +581,6 @@ dependencies = [ "syn", ] -[[package]] -name = "fs_extra" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" - [[package]] name = "funty" version = "2.0.0" @@ -2387,12 +2342,6 @@ dependencies = [ "synstructure", ] -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" - [[package]] name = "zerotrie" version = "0.2.4" diff --git a/deps/rust/Cargo.toml b/deps/rust/Cargo.toml index 9b51eeda8fa..91ff8b6657b 100644 --- a/deps/rust/Cargo.toml +++ b/deps/rust/Cargo.toml @@ -28,8 +28,6 @@ syn = { version = "2", features = ["full"] } ada-url = "3" anyhow = "1" async-trait = { version = "0", default-features = false } -aws-lc-rs = { version = "1", default-features = false, features = ["non-fips"] } -aws-lc-sys = { version = "0", default-features = false } capnp = "0" capnpc = "0" capnp-rpc = "0" diff --git a/src/rust/sha3/BUILD.bazel b/src/rust/sha3/BUILD.bazel deleted file mode 100644 index 384f922ba4f..00000000000 --- a/src/rust/sha3/BUILD.bazel +++ /dev/null @@ -1,11 +0,0 @@ -load("//:build/wd_rust_crate.bzl", "wd_rust_crate") - -wd_rust_crate( - name = "sha3", - cxx_bridge_src = "lib.rs", - visibility = ["//visibility:public"], - deps = [ - "@crates_vendor//:aws-lc-rs", - "@crates_vendor//:aws-lc-sys", - ], -) diff --git a/src/rust/sha3/lib.rs b/src/rust/sha3/lib.rs deleted file mode 100644 index 83d8ad262df..00000000000 --- a/src/rust/sha3/lib.rs +++ /dev/null @@ -1,99 +0,0 @@ -use std::ptr::null_mut; - -use aws_lc_rs::digest; -use aws_lc_sys::EVP_DigestFinalXOF; -use aws_lc_sys::EVP_DigestInit_ex; -use aws_lc_sys::EVP_DigestUpdate; -use aws_lc_sys::EVP_MD; -use aws_lc_sys::EVP_MD_CTX; -use aws_lc_sys::EVP_MD_CTX_free; -use aws_lc_sys::EVP_MD_CTX_new; -use aws_lc_sys::EVP_shake128; -use aws_lc_sys::EVP_shake256; - -#[cxx::bridge(namespace = "workerd::rust::sha3")] -mod ffi { - extern "Rust" { - fn sha3_256(data: &[u8]) -> Vec; - fn sha3_384(data: &[u8]) -> Vec; - fn sha3_512(data: &[u8]) -> Vec; - fn cshake128(data: &[u8], output_len: usize) -> Vec; - fn cshake256(data: &[u8], output_len: usize) -> Vec; - } -} - -#[must_use] -pub fn sha3_256(data: &[u8]) -> Vec { - digest::digest(&digest::SHA3_256, data).as_ref().to_vec() -} - -#[must_use] -pub fn sha3_384(data: &[u8]) -> Vec { - digest::digest(&digest::SHA3_384, data).as_ref().to_vec() -} - -#[must_use] -pub fn sha3_512(data: &[u8]) -> Vec { - digest::digest(&digest::SHA3_512, data).as_ref().to_vec() -} - -struct EvpMdCtx(*mut EVP_MD_CTX); - -impl EvpMdCtx { - fn new(md: *const EVP_MD) -> Self { - assert!(!md.is_null()); - - // SAFETY: EVP_MD_CTX_new does not take arguments and returns an owned context pointer or null. - let ctx = unsafe { EVP_MD_CTX_new() }; - assert!(!ctx.is_null()); - - // SAFETY: ctx is a non-null context created above, md is a non-null AWS-LC digest descriptor, - // and a null ENGINE pointer is accepted by EVP_DigestInit_ex. - let ok = unsafe { EVP_DigestInit_ex(ctx, md, null_mut()) }; - assert_eq!(ok, 1); - Self(ctx) - } -} - -impl Drop for EvpMdCtx { - fn drop(&mut self) { - // SAFETY: self.0 is exclusively owned by this wrapper and was returned by EVP_MD_CTX_new. - unsafe { - EVP_MD_CTX_free(self.0); - } - } -} - -type EvpMdFactory = unsafe extern "C" fn() -> *const EVP_MD; - -fn shake(data: &[u8], output_len: usize, md_factory: EvpMdFactory) -> Vec { - if output_len == 0 { - return Vec::new(); - } - - // SAFETY: md_factory is one of AWS-LC's EVP_shake* functions and returns a static descriptor. - let md = unsafe { md_factory() }; - let ctx = EvpMdCtx::new(md); - if !data.is_empty() { - // SAFETY: ctx owns a valid initialized EVP_MD_CTX, and data.as_ptr() is valid for data.len() - // bytes because data is non-empty. - let ok = unsafe { EVP_DigestUpdate(ctx.0, data.as_ptr().cast(), data.len()) }; - assert_eq!(ok, 1); - } - - let mut output = vec![0u8; output_len]; - // SAFETY: ctx owns a valid SHAKE EVP_MD_CTX, and output is non-empty and writable for output.len(). - let ok = unsafe { EVP_DigestFinalXOF(ctx.0, output.as_mut_ptr(), output.len()) }; - assert_eq!(ok, 1); - output -} - -#[must_use] -pub fn cshake128(data: &[u8], output_len: usize) -> Vec { - shake(data, output_len, EVP_shake128) -} - -#[must_use] -pub fn cshake256(data: &[u8], output_len: usize) -> Vec { - shake(data, output_len, EVP_shake256) -} diff --git a/src/workerd/api/crypto/chacha20.c++ b/src/workerd/api/crypto/chacha20.c++ deleted file mode 100644 index b8353d4715e..00000000000 --- a/src/workerd/api/crypto/chacha20.c++ +++ /dev/null @@ -1,240 +0,0 @@ -#include "impl.h" - -#include -#include -#include - -#include -#include - -namespace workerd::api { -namespace { - -// ChaCha20-Poly1305 constants -constexpr size_t CHACHA20_POLY1305_KEY_SIZE = 32; // 256 bits -constexpr size_t CHACHA20_POLY1305_NONCE_SIZE = 12; // 96 bits -constexpr size_t CHACHA20_POLY1305_TAG_SIZE = 16; // 128 bits - -class ChaCha20Poly1305Key final: public CryptoKey::Impl { - public: - explicit ChaCha20Poly1305Key(kj::Array keyData, - kj::StringPtr algorithmName, - bool extractable, - CryptoKeyUsageSet usages) - : CryptoKey::Impl(extractable, usages), - keyData(kj::mv(keyData)), - algorithmName(algorithmName) {} - - private: - kj::StringPtr getAlgorithmName() const override { - return algorithmName; - } - - CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override { - return CryptoKey::KeyAlgorithm{algorithmName}; - } - - bool equals(const CryptoKey::Impl& other) const override { - return this == &other || (other.getType() == "secret"_kj && other.equals(keyData)); - } - - bool equals(const kj::Array& other) const override { - return keyData.size() == other.size() && - CRYPTO_memcmp(keyData.begin(), other.begin(), keyData.size()) == 0; - } - - SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override { - JSG_REQUIRE(format == "raw-secret" || format == "jwk", DOMNotSupportedError, - "ChaCha20-Poly1305 key only supports exporting \"raw-secret\" & \"jwk\", not \"", format, - "\"."); - - if (format == "jwk") { - SubtleCrypto::JsonWebKey jwk; - jwk.kty = kj::str("oct"); - jwk.k = fastEncodeBase64Url(keyData); - jwk.alg = kj::str("C20P"); - jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); }); - jwk.ext = true; - return jwk; - } - - return jsg::JsArrayBuffer::create(js, keyData).addRef(js); - } - - jsg::JsArrayBuffer encrypt(jsg::Lock& js, - SubtleCrypto::EncryptAlgorithm&& algorithm, - kj::ArrayPtr plainText) const override { - auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".") - .getHandle(js); - JSG_REQUIRE(iv.size() == CHACHA20_POLY1305_NONCE_SIZE, DOMOperationError, - "ChaCha20-Poly1305 IV must be 12 bytes (provided ", iv.size(), ")."); - - KJ_IF_SOME(tagLength, algorithm.tagLength) { - JSG_REQUIRE(tagLength == 128, DOMOperationError, - "ChaCha20-Poly1305 tag length must be 128 (provided ", tagLength, ")."); - } - - kj::ArrayPtr additionalData = nullptr; - KJ_IF_SOME(sourceRef, algorithm.additionalData) { - auto source = sourceRef.getHandle(js); - additionalData = source.asArrayPtr(); - } - - auto aeadCtx = kj::disposeWith(EVP_AEAD_CTX_new( - EVP_aead_chacha20_poly1305(), keyData.begin(), keyData.size(), CHACHA20_POLY1305_TAG_SIZE)); - KJ_ASSERT(aeadCtx.get() != nullptr); - - auto maxOutLen = plainText.size() + CHACHA20_POLY1305_TAG_SIZE; - auto cipherText = jsg::JsArrayBuffer::create(js, maxOutLen); - - size_t outLen = 0; - JSG_REQUIRE(EVP_AEAD_CTX_seal(aeadCtx.get(), cipherText.asArrayPtr().begin(), &outLen, - maxOutLen, iv.asArrayPtr().begin(), iv.size(), plainText.begin(), - plainText.size(), additionalData.begin(), additionalData.size()), - DOMOperationError, "ChaCha20-Poly1305 encryption failed", internalDescribeOpensslErrors()); - KJ_ASSERT(outLen == maxOutLen); - - return cipherText; - } - - jsg::JsArrayBuffer decrypt(jsg::Lock& js, - SubtleCrypto::EncryptAlgorithm&& algorithm, - kj::ArrayPtr cipherText) const override { - auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".") - .getHandle(js); - JSG_REQUIRE(iv.size() == CHACHA20_POLY1305_NONCE_SIZE, DOMOperationError, - "ChaCha20-Poly1305 IV must be 12 bytes (provided ", iv.size(), ")."); - - KJ_IF_SOME(tagLength, algorithm.tagLength) { - JSG_REQUIRE(tagLength == 128, DOMOperationError, - "ChaCha20-Poly1305 tag length must be 128 (provided ", tagLength, ")."); - } - - JSG_REQUIRE(cipherText.size() >= CHACHA20_POLY1305_TAG_SIZE, DOMOperationError, - "Ciphertext is too short to contain a valid ChaCha20-Poly1305 authentication tag."); - - kj::ArrayPtr additionalData = nullptr; - KJ_IF_SOME(sourceRef, algorithm.additionalData) { - auto source = sourceRef.getHandle(js); - additionalData = source.asArrayPtr(); - } - - auto aeadCtx = kj::disposeWith(EVP_AEAD_CTX_new( - EVP_aead_chacha20_poly1305(), keyData.begin(), keyData.size(), CHACHA20_POLY1305_TAG_SIZE)); - KJ_ASSERT(aeadCtx.get() != nullptr); - - auto maxOutLen = cipherText.size(); - auto plainText = jsg::JsArrayBuffer::create(js, maxOutLen); - - size_t outLen = 0; - JSG_REQUIRE(EVP_AEAD_CTX_open(aeadCtx.get(), plainText.asArrayPtr().begin(), &outLen, maxOutLen, - iv.asArrayPtr().begin(), iv.size(), cipherText.begin(), cipherText.size(), - additionalData.begin(), additionalData.size()), - DOMOperationError, "ChaCha20-Poly1305 decryption failed."); - - return jsg::JsArrayBuffer::create(js, plainText.asArrayPtr().first(outLen)); - } - - kj::StringPtr jsgGetMemoryName() const override { - return "ChaCha20Poly1305Key"; - } - size_t jsgGetMemorySelfSize() const override { - return sizeof(ChaCha20Poly1305Key); - } - void jsgGetMemoryInfo(jsg::MemoryTracker& tracker) const override { - tracker.trackFieldWithSize("keyData", keyData.size()); - } - - ZeroOnFree keyData; - kj::StringPtr algorithmName; -}; - -} // namespace - -kj::OneOf, CryptoKeyPair> CryptoKey::Impl::generateChaCha20Poly1305( - jsg::Lock& js, - kj::StringPtr normalizedName, - SubtleCrypto::GenerateKeyAlgorithm&& algorithm, - bool extractable, - kj::ArrayPtr keyUsages) { - CryptoKeyUsageSet validUsages = CryptoKeyUsageSet::encrypt() | CryptoKeyUsageSet::decrypt() | - CryptoKeyUsageSet::wrapKey() | CryptoKeyUsageSet::unwrapKey(); - auto usages = CryptoKeyUsageSet::validate( - normalizedName, CryptoKeyUsageSet::Context::generate, keyUsages, validUsages); - - auto keyDataArray = kj::heapArray(CHACHA20_POLY1305_KEY_SIZE); - IoContext::current().getEntropySource().generate(keyDataArray); - - return js.alloc( - kj::heap(kj::mv(keyDataArray), normalizedName, extractable, usages)); -} - -kj::Own CryptoKey::Impl::importChaCha20Poly1305(jsg::Lock& js, - kj::StringPtr normalizedName, - kj::StringPtr format, - SubtleCrypto::ImportKeyData keyData, - SubtleCrypto::ImportKeyAlgorithm&& algorithm, - bool extractable, - kj::ArrayPtr keyUsages) { - CryptoKeyUsageSet validUsages = CryptoKeyUsageSet::encrypt() | CryptoKeyUsageSet::decrypt() | - CryptoKeyUsageSet::wrapKey() | CryptoKeyUsageSet::unwrapKey(); - auto usages = CryptoKeyUsageSet::validate( - normalizedName, CryptoKeyUsageSet::Context::importSecret, keyUsages, validUsages); - - kj::Array keyDataArray; - - if (format == "raw-secret") { - keyDataArray = kj::mv(keyData.get>()); - JSG_REQUIRE(keyDataArray.size() == CHACHA20_POLY1305_KEY_SIZE, DOMDataError, - "ChaCha20-Poly1305 key must be 256 bits (provided ", keyDataArray.size() * 8, ")."); - } else if (format == "jwk") { - auto& keyDataJwk = keyData.get(); - JSG_REQUIRE(keyDataJwk.kty == "oct", DOMDataError, - "Symmetric \"jwk\" key import requires a JSON Web Key with Key Type parameter " - "\"kty\" equal to \"oct\" (encountered \"", - keyDataJwk.kty, "\")."); - - keyDataArray = UNWRAP_JWK_BIGNUM(kj::mv(keyDataJwk.k), DOMDataError, - "Symmetric \"jwk\" key import requires a base64Url encoding of the key."); - - JSG_REQUIRE(keyDataArray.size() == CHACHA20_POLY1305_KEY_SIZE, DOMDataError, - "ChaCha20-Poly1305 key must be 256 bits (provided ", keyDataArray.size() * 8, ")."); - - KJ_IF_SOME(alg, keyDataJwk.alg) { - JSG_REQUIRE(alg == "C20P", DOMDataError, - "Symmetric \"jwk\" key contains invalid \"alg\" value \"", alg, "\", expected \"C20P\"."); - } - - if (keyUsages.size() != 0) { - KJ_IF_SOME(u, keyDataJwk.use) { - JSG_REQUIRE(u == "enc", DOMDataError, - "Symmetric \"jwk\" key must have a \"use\" of \"enc\", not \"", u, "\"."); - } - } - - KJ_IF_SOME(ops, keyDataJwk.key_ops) { - std::sort(ops.begin(), ops.end()); - auto duplicate = std::adjacent_find(ops.begin(), ops.end()); - JSG_REQUIRE(duplicate == ops.end(), DOMDataError, - "Symmetric \"jwk\" key contains duplicate value \"", *duplicate, "\", in \"key_op\"."); - - for (const auto& usage: keyUsages) { - JSG_REQUIRE(std::binary_search(ops.begin(), ops.end(), usage), DOMDataError, - "\"jwk\" key missing usage \"", usage, "\", in \"key_ops\"."); - } - } - - KJ_IF_SOME(e, keyDataJwk.ext) { - JSG_REQUIRE(e || !extractable, DOMDataError, "\"jwk\" key has value \"", e ? "true" : "false", - "\", for \"ext\" that is incompatible " - "with import extractability value \"", - extractable ? "true" : "false", "\"."); - } - } else { - JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unrecognized key import format \"", format, "\"."); - } - - return kj::heap(kj::mv(keyDataArray), normalizedName, extractable, usages); -} - -} // namespace workerd::api diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index a850c58e625..a5d227e3026 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -13,7 +13,6 @@ #include #include #include -#include #include #include @@ -144,8 +143,6 @@ static kj::Maybe lookupAlgorithm(kj::StringPtr name) { {"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}, - {"ChaCha20-Poly1305"_kj, &CryptoKey::Impl::importChaCha20Poly1305, - &CryptoKey::Impl::generateChaCha20Poly1305}, }; auto iter = ALGORITHMS.find(CryptoAlgorithm{name}); @@ -191,7 +188,6 @@ kj::Maybe getKeyLength(const SubtleCrypto::ImportKeyAlgorithm& derived {"AES-CBC"}, {"AES-GCM"}, {"AES-KW"}, - {"ChaCha20-Poly1305"}, {"HMAC"}, {"HKDF"}, {"PBKDF2"}, @@ -219,8 +215,6 @@ kj::Maybe getKeyLength(const SubtleCrypto::ImportKeyAlgorithm& derived "Derived AES key must be 128, 192, or 256 bits in length but provided ", length, "."); } return length; - } else if (*algIter == "ChaCha20-Poly1305") { - return 256; } else if (*algIter == "HMAC") { KJ_IF_SOME(length, derivedKeyAlgorithm.length) { // If the user requested a specific HMAC key length, honor it. @@ -311,42 +305,6 @@ bool isOneOf(kj::StringPtr value, std::initializer_list names) { return std::find(names.begin(), names.end(), value) != names.end(); } -void validateOmittedOrEmptyBufferSource(jsg::Lock& js, - const jsg::Optional>& value, - kj::StringPtr name) { - KJ_IF_SOME(ref, value) { - JSG_REQUIRE( - ref.getHandle(js).size() == 0, DOMNotSupportedError, name, " must be omitted or empty."); - } -} - -void validateSha3DerivedDigest(jsg::Lock& js, const SubtleCrypto::DigestAlgorithm& algorithm) { - if (strcasecmp(algorithm.name.cStr(), "cSHAKE128") == 0 || - strcasecmp(algorithm.name.cStr(), "cSHAKE256") == 0) { - auto outputLengthBits = JSG_REQUIRE_NONNULL( - algorithm.outputLength, DOMOperationError, "outputLength is required for cSHAKE"); - JSG_REQUIRE(outputLengthBits >= 0 && outputLengthBits % 8 == 0, DOMOperationError, - "outputLength must be a non-negative multiple of 8"); - - validateOmittedOrEmptyBufferSource(js, algorithm.functionName, "functionName"); - validateOmittedOrEmptyBufferSource(js, algorithm.customization, "customization"); - return; - } - - JSG_REQUIRE(strcasecmp(algorithm.name.cStr(), "SHA3-256") == 0 || - strcasecmp(algorithm.name.cStr(), "SHA3-384") == 0 || - strcasecmp(algorithm.name.cStr(), "SHA3-512") == 0, - DOMNotSupportedError, "Unrecognized or unimplemented digest algorithm requested."); -} - -bool supportsDigestAlgorithm(jsg::Lock& js, const SubtleCrypto::DigestAlgorithm& algorithm) { - KJ_IF_SOME(exception, kj::runCatchingExceptions([&] { lookupDigestAlgorithm(algorithm.name); })) { - (void)exception; - validateSha3DerivedDigest(js, algorithm); - } - return true; -} - void validateAesKeyLength(int length) { JSG_REQUIRE(length == 128 || length == 192 || length == 256, DOMOperationError, "AES key length must be 128, 192, or 256 bits."); @@ -452,8 +410,7 @@ void validateImportKeyAlgorithm( } bool supportsEncryptDecrypt(kj::StringPtr normalizedName) { - return isOneOf(normalizedName, - {"AES-CTR"_kj, "AES-CBC"_kj, "AES-GCM"_kj, "RSA-OAEP"_kj, "ChaCha20-Poly1305"_kj}); + return isOneOf(normalizedName, {"AES-CTR"_kj, "AES-CBC"_kj, "AES-GCM"_kj, "RSA-OAEP"_kj}); } bool supportsSign(kj::StringPtr normalizedName) { @@ -477,7 +434,7 @@ bool supportsExportKey(kj::StringPtr 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, "ChaCha20-Poly1305"_kj}); + "ML-KEM-1024"_kj}); } bool supportsEncapsulate(kj::StringPtr normalizedName) { @@ -511,13 +468,6 @@ void validateEncryptAlgorithm( algorithm.length, TypeError, "Missing \"length\" member in \"algorithm\"."); JSG_REQUIRE(counterLength > 0 && counterLength <= 128, DOMOperationError, "Invalid counter of ", counterLength, " bits length provided."); - } else if (normalizedName == "ChaCha20-Poly1305") { - auto iv = JSG_REQUIRE_NONNULL(algorithm.iv, TypeError, "Missing field \"iv\" in \"algorithm\".") - .getHandle(js); - JSG_REQUIRE(iv.size() == 12, DOMOperationError, "ChaCha20-Poly1305 IV must be 12 bytes."); - KJ_IF_SOME(tagLength, algorithm.tagLength) { - JSG_REQUIRE(tagLength == 128, DOMOperationError, "ChaCha20-Poly1305 tag length must be 128."); - } } } @@ -752,53 +702,14 @@ jsg::Promise SubtleCrypto::verify(jsg::Lock& js, }); } -jsg::Promise> SubtleCrypto::digest(jsg::Lock& js, - kj::OneOf algorithmParam, - jsg::JsBufferSource data) { +jsg::Promise> SubtleCrypto::digest( + jsg::Lock& js, kj::OneOf algorithmParam, jsg::JsBufferSource data) { auto algorithm = interpretAlgorithmParam(kj::mv(algorithmParam)); auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, algorithm); return js.evalNow([&] { auto ptr = nonNullBytes(data.asArrayPtr()); - - if (strcasecmp(algorithm.name.cStr(), "cSHAKE128") == 0 || - strcasecmp(algorithm.name.cStr(), "cSHAKE256") == 0) { - auto outputLengthBits = JSG_REQUIRE_NONNULL( - algorithm.outputLength, DOMOperationError, "outputLength is required for cSHAKE"); - JSG_REQUIRE(outputLengthBits >= 0 && outputLengthBits % 8 == 0, DOMOperationError, - "outputLength must be a non-negative multiple of 8"); - auto outputLengthBytes = static_cast(outputLengthBits / 8); - - validateOmittedOrEmptyBufferSource(js, algorithm.functionName, "functionName"); - validateOmittedOrEmptyBufferSource(js, algorithm.customization, "customization"); - - auto input = ::rust::Slice(ptr.begin(), ptr.size()); - auto result = strcasecmp(algorithm.name.cStr(), "cSHAKE128") == 0 - ? workerd::rust::sha3::cshake128(input, outputLengthBytes) - : workerd::rust::sha3::cshake256(input, outputLengthBytes); - auto buf = jsg::JsArrayBuffer::create(js, result.size()); - memcpy(buf.asArrayPtr().begin(), result.data(), result.size()); - return buf.addRef(js); - } - - auto sha3Fn = [&]() -> kj::Maybe<::rust::Vec (*)(::rust::Slice)> { - if (strcasecmp(algorithm.name.cStr(), "SHA3-256") == 0) { - return workerd::rust::sha3::sha3_256; - } else if (strcasecmp(algorithm.name.cStr(), "SHA3-384") == 0) { - return workerd::rust::sha3::sha3_384; - } else if (strcasecmp(algorithm.name.cStr(), "SHA3-512") == 0) { - return workerd::rust::sha3::sha3_512; - } - return kj::none; - }(); - KJ_IF_SOME(fn, sha3Fn) { - auto result = fn({ptr.begin(), ptr.size()}); - auto buf = jsg::JsArrayBuffer::create(js, result.size()); - memcpy(buf.asArrayPtr().begin(), result.data(), result.size()); - return buf.addRef(js); - } - auto type = lookupDigestAlgorithm(algorithm.name).second; auto digestCtx = kj::disposeWith(EVP_MD_CTX_new()); @@ -1181,7 +1092,7 @@ bool SubtleCrypto::supports(jsg::Lock& js, jsg::Optional> lengthOrAdditionalAlgorithm, const jsg::TypeHandler>& encryptAlgorithmHandler, const jsg::TypeHandler>& signAlgorithmHandler, - const jsg::TypeHandler>& digestAlgorithmHandler, + const jsg::TypeHandler>& digestAlgorithmHandler, const jsg::TypeHandler>& generateKeyAlgorithmHandler, const jsg::TypeHandler>& deriveKeyAlgorithmHandler, @@ -1230,7 +1141,13 @@ bool SubtleCrypto::supports(jsg::Lock& js, case SubtleOperation::DIGEST: { auto normalizedAlgorithm = normalizeSupportAlgorithm(js, algorithmValue, digestAlgorithmHandler); - return supportsDigestAlgorithm(js, normalizedAlgorithm); + KJ_IF_SOME(exception, kj::runCatchingExceptions([&] { + lookupDigestAlgorithm(normalizedAlgorithm.name); + })) { + (void)exception; + return false; + } + return true; } case SubtleOperation::GENERATE_KEY: { diff --git a/src/workerd/api/crypto/crypto.h b/src/workerd/api/crypto/crypto.h index 27035e1bd12..5a69549fab7 100644 --- a/src/workerd/api/crypto/crypto.h +++ b/src/workerd/api/crypto/crypto.h @@ -363,18 +363,6 @@ class SubtleCrypto: public jsg::Object { JSG_STRUCT(name); }; - // Type of the `algorithm` parameter passed to `digest()` specifically. - // Extends HashAlgorithm's fields with optional parameters for XOF algorithms. - struct DigestAlgorithm { - kj::String name; - jsg::Optional outputLength; - jsg::Optional domainSeparation; - jsg::Optional> functionName; - jsg::Optional> customization; - - JSG_STRUCT(name, outputLength, domainSeparation, functionName, customization); - }; - // Type of the `algorithm` parameter passed to `encrypt()` and `decrypt()`. Different // algorithms call for different fields. struct EncryptAlgorithm { @@ -574,9 +562,8 @@ class SubtleCrypto: public jsg::Object { jsg::JsBufferSource signature, jsg::JsBufferSource data); - jsg::Promise> digest(jsg::Lock& js, - kj::OneOf algorithm, - jsg::JsBufferSource data); + jsg::Promise> digest( + jsg::Lock& js, kj::OneOf algorithm, jsg::JsBufferSource data); jsg::Promise, CryptoKeyPair>> generateKey(jsg::Lock& js, kj::OneOf algorithm, @@ -681,7 +668,7 @@ class SubtleCrypto: public jsg::Object { jsg::Optional> lengthOrAdditionalAlgorithm, const jsg::TypeHandler>& encryptAlgorithmHandler, const jsg::TypeHandler>& signAlgorithmHandler, - const jsg::TypeHandler>& digestAlgorithmHandler, + const jsg::TypeHandler>& digestAlgorithmHandler, const jsg::TypeHandler>& generateKeyAlgorithmHandler, const jsg::TypeHandler>& deriveKeyAlgorithmHandler, @@ -714,10 +701,10 @@ class SubtleCrypto: public jsg::Object { JSG_TS_OVERRIDE({ static supports(operation: string, - algorithm: string | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm | SubtleCryptoDigestAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, + algorithm: string | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, length?: number | null): boolean; static supports(operation: string, - algorithm: string | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm | SubtleCryptoDigestAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, + algorithm: string | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm): boolean; wrapKey(format: string, key: CryptoKey, @@ -728,11 +715,9 @@ class SubtleCrypto: public jsg::Object { baseKey : CryptoKey, length? : number | null) : Promise; - digest(algorithm: string | SubtleCryptoDigestAlgorithm, + digest(algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView) : Promise; - // SubtleCryptoDigestAlgorithm.functionName: ArrayBuffer | ArrayBufferView - // SubtleCryptoDigestAlgorithm.customization: ArrayBuffer | ArrayBufferView sign(algorithm: string | SubtleCryptoSignAlgorithm, key: CryptoKey, data: ArrayBuffer | ArrayBufferView) @@ -905,12 +890,11 @@ class Crypto: public jsg::Object { api::SubtleCrypto::EncapsulatedBits, api::SubtleCrypto::EncapsulatedKey, \ api::SubtleCrypto::DeriveKeyAlgorithm, api::SubtleCrypto::EncryptAlgorithm, \ api::SubtleCrypto::GenerateKeyAlgorithm, api::SubtleCrypto::HashAlgorithm, \ - api::SubtleCrypto::DigestAlgorithm, api::SubtleCrypto::ImportKeyAlgorithm, \ - api::SubtleCrypto::SignAlgorithm, api::CryptoKey::KeyAlgorithm, \ - api::CryptoKey::AesKeyAlgorithm, api::CryptoKey::HmacKeyAlgorithm, \ - api::CryptoKey::RsaKeyAlgorithm, api::CryptoKey::EllipticKeyAlgorithm, \ - api::CryptoKey::ArbitraryKeyAlgorithm, api::CryptoKey::AsymmetricKeyDetails, \ - api::DigestStream + api::SubtleCrypto::ImportKeyAlgorithm, api::SubtleCrypto::SignAlgorithm, \ + api::CryptoKey::KeyAlgorithm, api::CryptoKey::AesKeyAlgorithm, \ + api::CryptoKey::HmacKeyAlgorithm, api::CryptoKey::RsaKeyAlgorithm, \ + api::CryptoKey::EllipticKeyAlgorithm, api::CryptoKey::ArbitraryKeyAlgorithm, \ + api::CryptoKey::AsymmetricKeyDetails, api::DigestStream } // namespace workerd::api diff --git a/src/workerd/api/crypto/impl.h b/src/workerd/api/crypto/impl.h index cef8cb9915e..02ba0c68318 100644 --- a/src/workerd/api/crypto/impl.h +++ b/src/workerd/api/crypto/impl.h @@ -124,7 +124,6 @@ class CryptoKey::Impl { static ImportFunc importRsaRaw; static ImportFunc importMlDsa; static ImportFunc importMlKem; - static ImportFunc importChaCha20Poly1305; using GenerateFunc = kj::OneOf, CryptoKeyPair>(jsg::Lock& js, kj::StringPtr normalizedName, @@ -140,7 +139,6 @@ class CryptoKey::Impl { static GenerateFunc generateEddsa; static GenerateFunc generateMlDsa; static GenerateFunc generateMlKem; - static GenerateFunc generateChaCha20Poly1305; Impl(bool extractable, CryptoKeyUsageSet usages): extractable(extractable), usages(usages) {} diff --git a/src/workerd/api/tests/crypto-extras-test.js b/src/workerd/api/tests/crypto-extras-test.js index ec4f0519686..4b700bece06 100644 --- a/src/workerd/api/tests/crypto-extras-test.js +++ b/src/workerd/api/tests/crypto-extras-test.js @@ -216,96 +216,6 @@ export const deriveBitsNullLength = { }, }; -export const subtleCryptoSupportsSha3DerivedDigests = { - async test() { - async function rejectsNotSupported(algorithm) { - let threw = false; - try { - await crypto.subtle.digest(algorithm, new Uint8Array(0)); - } catch (err) { - threw = true; - strictEqual(err.name, 'NotSupportedError'); - } - ok(threw, 'Expected digest operation to reject'); - } - - ok(SubtleCrypto.supports('digest', 'SHA3-256')); - ok( - SubtleCrypto.supports('digest', { - name: 'cSHAKE128', - outputLength: 256, - }) - ); - ok( - SubtleCrypto.supports('digest', { - name: 'cSHAKE128', - outputLength: 256, - functionName: new Uint8Array(0), - }) - ); - ok( - SubtleCrypto.supports('digest', { - name: 'cSHAKE128', - outputLength: 256, - customization: new Uint8Array(0), - }) - ); - - strictEqual( - SubtleCrypto.supports('digest', { - name: 'cSHAKE128', - outputLength: 256, - functionName: new Uint8Array([1]), - }), - false - ); - strictEqual( - SubtleCrypto.supports('digest', { - name: 'cSHAKE128', - outputLength: 256, - customization: new Uint8Array([1]), - }), - false - ); - strictEqual( - SubtleCrypto.supports('digest', { - name: 'TurboSHAKE128', - outputLength: 256, - }), - false - ); - - strictEqual( - ( - await crypto.subtle.digest( - { - name: 'cSHAKE128', - outputLength: 256, - functionName: new Uint8Array(0), - customization: new Uint8Array(0), - }, - new Uint8Array(0) - ) - ).byteLength, - 32 - ); - await rejectsNotSupported({ - name: 'cSHAKE128', - outputLength: 256, - functionName: new Uint8Array([1]), - }); - await rejectsNotSupported({ - name: 'cSHAKE128', - outputLength: 256, - customization: new Uint8Array([1]), - }); - await rejectsNotSupported({ - name: 'TurboSHAKE128', - outputLength: 256, - }); - }, -}; - export const aesCounterOverflowTest = { async test() { // Regression test: Check that the input counter is not modified when it overflows in the diff --git a/src/workerd/io/BUILD.bazel b/src/workerd/io/BUILD.bazel index d7f1dca1be6..41dd81da890 100644 --- a/src/workerd/io/BUILD.bazel +++ b/src/workerd/io/BUILD.bazel @@ -69,7 +69,6 @@ wd_cc_library( implementation_deps = [ "//src/per_isolate", "//src/rust/jsg", - "//src/rust/sha3", "//src/workerd/api:crypto-crc-impl", "//src/workerd/api:data-url", "//src/workerd/api/node:exceptions", diff --git a/src/wpt/WebCryptoAPI-test.ts b/src/wpt/WebCryptoAPI-test.ts index 2f86daccf32..d96131af30b 100644 --- a/src/wpt/WebCryptoAPI-test.ts +++ b/src/wpt/WebCryptoAPI-test.ts @@ -58,7 +58,7 @@ export default { }, 'derive_bits_keys/pbkdf2.js': supportFile, 'derive_bits_keys/pbkdf2_vectors.js': supportFile, - 'digest/cshake.tentative.https.any.js': {}, + 'digest/cshake.tentative.https.any.js': unsupported('cSHAKE'), 'digest/digest.https.any.js': { comment: 'They expect TypeError, we have NotSupportedError', expectedFailures: [ @@ -69,7 +69,7 @@ export default { ], }, 'digest/kangarootwelve.tentative.https.any.js': unsupported('KangarooTwelve'), - 'digest/sha3.tentative.https.any.js': {}, + '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', @@ -93,7 +93,8 @@ export default { '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': {}, + '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': supportFile, @@ -157,10 +158,8 @@ export default { expectedFailures: [/^(Empty|Bad) algorithm:/], }, 'generateKey/failures_X448.tentative.https.any.js': unsupported('X448'), - 'generateKey/failures_chacha20_poly1305.tentative.https.any.js': { - comment: 'Wrong type of error returned', - expectedFailures: [/^(Empty|Bad) algorithm:/], - }, + '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': {}, @@ -184,7 +183,8 @@ export default { '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': {}, + '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': {}, @@ -251,7 +251,8 @@ export default { unsupported('AES-OCB'), 'import_export/Argon2_importKey.tentative.https.any.js': unsupported('Argon2'), - 'import_export/ChaCha20-Poly1305_importKey.tentative.https.any.js': {}, + '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': {}, diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index 7a617c399bd..978bfea1120 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -1331,7 +1331,7 @@ declare abstract class SubtleCrypto { | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm - | SubtleCryptoDigestAlgorithm + | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, length?: number | null, @@ -1343,7 +1343,7 @@ declare abstract class SubtleCrypto { | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm - | SubtleCryptoDigestAlgorithm + | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, @@ -1395,7 +1395,7 @@ declare abstract class SubtleCrypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoDigestAlgorithm, + algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1608,13 +1608,6 @@ interface SubtleCryptoGenerateKeyAlgorithm { interface SubtleCryptoHashAlgorithm { name: string; } -interface SubtleCryptoDigestAlgorithm { - name: string; - outputLength?: number; - domainSeparation?: number; - functionName?: ArrayBuffer | ArrayBufferView; - customization?: ArrayBuffer | ArrayBufferView; -} interface SubtleCryptoImportKeyAlgorithm { name: string; hash?: string | SubtleCryptoHashAlgorithm; diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index d1c7c76b510..fb115ccca8b 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -1337,7 +1337,7 @@ export declare abstract class SubtleCrypto { | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm - | SubtleCryptoDigestAlgorithm + | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, length?: number | null, @@ -1349,7 +1349,7 @@ export declare abstract class SubtleCrypto { | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm - | SubtleCryptoDigestAlgorithm + | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, @@ -1401,7 +1401,7 @@ export declare abstract class SubtleCrypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoDigestAlgorithm, + algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1614,13 +1614,6 @@ export interface SubtleCryptoGenerateKeyAlgorithm { export interface SubtleCryptoHashAlgorithm { name: string; } -export interface SubtleCryptoDigestAlgorithm { - name: string; - outputLength?: number; - domainSeparation?: number; - functionName?: ArrayBuffer | ArrayBufferView; - customization?: ArrayBuffer | ArrayBufferView; -} export interface SubtleCryptoImportKeyAlgorithm { name: string; hash?: string | SubtleCryptoHashAlgorithm; diff --git a/types/generated-snapshot/index.d.ts b/types/generated-snapshot/index.d.ts index 6257231eb2c..5385b1749cb 100755 --- a/types/generated-snapshot/index.d.ts +++ b/types/generated-snapshot/index.d.ts @@ -1278,7 +1278,7 @@ declare abstract class SubtleCrypto { | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm - | SubtleCryptoDigestAlgorithm + | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, length?: number | null, @@ -1290,7 +1290,7 @@ declare abstract class SubtleCrypto { | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm - | SubtleCryptoDigestAlgorithm + | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, @@ -1342,7 +1342,7 @@ declare abstract class SubtleCrypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoDigestAlgorithm, + algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1555,13 +1555,6 @@ interface SubtleCryptoGenerateKeyAlgorithm { interface SubtleCryptoHashAlgorithm { name: string; } -interface SubtleCryptoDigestAlgorithm { - name: string; - outputLength?: number; - domainSeparation?: number; - functionName?: ArrayBuffer | ArrayBufferView; - customization?: ArrayBuffer | ArrayBufferView; -} interface SubtleCryptoImportKeyAlgorithm { name: string; hash?: string | SubtleCryptoHashAlgorithm; diff --git a/types/generated-snapshot/index.ts b/types/generated-snapshot/index.ts index 6d76b84ef69..65ce95a9594 100755 --- a/types/generated-snapshot/index.ts +++ b/types/generated-snapshot/index.ts @@ -1284,7 +1284,7 @@ export declare abstract class SubtleCrypto { | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm - | SubtleCryptoDigestAlgorithm + | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, length?: number | null, @@ -1296,7 +1296,7 @@ export declare abstract class SubtleCrypto { | SubtleCryptoGenerateKeyAlgorithm | SubtleCryptoImportKeyAlgorithm | SubtleCryptoDeriveKeyAlgorithm - | SubtleCryptoDigestAlgorithm + | SubtleCryptoHashAlgorithm | SubtleCryptoEncryptAlgorithm | SubtleCryptoSignAlgorithm, additionalAlgorithm: string | SubtleCryptoImportKeyAlgorithm, @@ -1348,7 +1348,7 @@ export declare abstract class SubtleCrypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoDigestAlgorithm, + algorithm: string | SubtleCryptoHashAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1561,13 +1561,6 @@ export interface SubtleCryptoGenerateKeyAlgorithm { export interface SubtleCryptoHashAlgorithm { name: string; } -export interface SubtleCryptoDigestAlgorithm { - name: string; - outputLength?: number; - domainSeparation?: number; - functionName?: ArrayBuffer | ArrayBufferView; - customization?: ArrayBuffer | ArrayBufferView; -} export interface SubtleCryptoImportKeyAlgorithm { name: string; hash?: string | SubtleCryptoHashAlgorithm; From 3ca1c5f052de512e0f975efa4da68c75ff07e391 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Thu, 9 Jul 2026 15:41:00 +0200 Subject: [PATCH 03/11] Gate modern WebCrypto algorithms --- src/workerd/api/crypto/crypto.c++ | 4 + src/workerd/api/crypto/crypto.h | 153 ++++++++++++-------- src/workerd/api/tests/crypto-extras-test.js | 20 +++ src/workerd/io/compatibility-date.capnp | 9 ++ src/wpt/BUILD.bazel | 5 +- types/generated-snapshot/index.d.ts | 57 -------- types/generated-snapshot/index.ts | 57 -------- 7 files changed, 129 insertions(+), 176 deletions(-) diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index a5d227e3026..987312e9d04 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -151,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; } } diff --git a/src/workerd/api/crypto/crypto.h b/src/workerd/api/crypto/crypto.h index 5a69549fab7..14e213ef869 100644 --- a/src/workerd/api/crypto/crypto.h +++ b/src/workerd/api/crypto/crypto.h @@ -677,9 +677,7 @@ class SubtleCrypto: public jsg::Object { // 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_STATIC_METHOD(supports); - + JSG_RESOURCE_TYPE(SubtleCrypto, CompatibilityFlags::Reader flags) { JSG_METHOD(encrypt); JSG_METHOD(decrypt); JSG_METHOD(sign); @@ -692,68 +690,101 @@ class SubtleCrypto: public jsg::Object { JSG_METHOD(exportKey); JSG_METHOD(wrapKey); JSG_METHOD(unwrapKey); - JSG_METHOD(encapsulateKey); - JSG_METHOD(encapsulateBits); - JSG_METHOD(decapsulateKey); - JSG_METHOD(decapsulateBits); - JSG_METHOD(getPublicKey); + 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({ - 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, + 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; - 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; + 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; - 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; + }); + } } }; diff --git a/src/workerd/api/tests/crypto-extras-test.js b/src/workerd/api/tests/crypto-extras-test.js index 4b700bece06..b283edfb8d3 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 SubtleCrypto, 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/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 d562389a5b0..6bf2563c04f 100644 --- a/src/wpt/BUILD.bazel +++ b/src/wpt/BUILD.bazel @@ -98,7 +98,10 @@ 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({ diff --git a/types/generated-snapshot/index.d.ts b/types/generated-snapshot/index.d.ts index 5385b1749cb..904b5daad88 100755 --- a/types/generated-snapshot/index.d.ts +++ b/types/generated-snapshot/index.d.ts @@ -1271,30 +1271,6 @@ declare abstract class Crypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ declare abstract class SubtleCrypto { - 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; /** * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. * @@ -1420,31 +1396,6 @@ declare abstract class SubtleCrypto { extractable: boolean, keyUsages: string[], ): 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; timingSafeEqual( a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView, @@ -1519,14 +1470,6 @@ interface RsaOtherPrimesInfo { d?: string; t?: string; } -interface SubtleCryptoEncapsulatedBits { - sharedKey: ArrayBuffer; - ciphertext: ArrayBuffer; -} -interface SubtleCryptoEncapsulatedKey { - sharedKey: CryptoKey; - ciphertext: ArrayBuffer; -} interface SubtleCryptoDeriveKeyAlgorithm { name: string; salt?: ArrayBuffer | ArrayBufferView; diff --git a/types/generated-snapshot/index.ts b/types/generated-snapshot/index.ts index 65ce95a9594..ab439dcda62 100755 --- a/types/generated-snapshot/index.ts +++ b/types/generated-snapshot/index.ts @@ -1277,30 +1277,6 @@ export declare abstract class Crypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ export declare abstract class SubtleCrypto { - 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; /** * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. * @@ -1426,31 +1402,6 @@ export declare abstract class SubtleCrypto { extractable: boolean, keyUsages: string[], ): 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; timingSafeEqual( a: ArrayBuffer | ArrayBufferView, b: ArrayBuffer | ArrayBufferView, @@ -1525,14 +1476,6 @@ export interface RsaOtherPrimesInfo { 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; From aa10a91840028463d21c162938bdbaeb4d42bb53 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Thu, 9 Jul 2026 16:11:40 +0200 Subject: [PATCH 04/11] Fix modern crypto buffer imports --- src/workerd/api/crypto/mldsa.c++ | 28 ++++++++++++++++------------ src/workerd/api/crypto/mlkem.c++ | 28 ++++++++++++++++------------ 2 files changed, 32 insertions(+), 24 deletions(-) diff --git a/src/workerd/api/crypto/mldsa.c++ b/src/workerd/api/crypto/mldsa.c++ index 063139720ff..9b2ed507913 100644 --- a/src/workerd/api/crypto/mldsa.c++ +++ b/src/workerd/api/crypto/mldsa.c++ @@ -752,27 +752,31 @@ kj::Own CryptoKey::Impl::importMlDsa(jsg::Lock& js, if (format == "raw-public") { auto usages = CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::importPublic, keyUsages, CryptoKeyUsageSet::verify()); - auto& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, - "Import data for raw-public must be a buffer."); - return MlDsaKey

::importRawPublic(keyBytes, normalizedName, extractable, usages); + 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& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, - "Import data for raw-seed must be a buffer."); - return MlDsaKey

::importRawSeed(keyBytes, normalizedName, extractable, usages); + 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& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, - "Import data for spki must be a buffer."); - return MlDsaKey

::importSpki(keyBytes, normalizedName, extractable, usages); + 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& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, - "Import data for pkcs8 must be a buffer."); - return MlDsaKey

::importPkcs8(keyBytes, normalizedName, extractable, usages); + 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."); diff --git a/src/workerd/api/crypto/mlkem.c++ b/src/workerd/api/crypto/mlkem.c++ index c5a27985ad6..8c850023fb0 100644 --- a/src/workerd/api/crypto/mlkem.c++ +++ b/src/workerd/api/crypto/mlkem.c++ @@ -690,27 +690,31 @@ kj::Own CryptoKey::Impl::importMlKem(jsg::Lock& js, if (format == "raw-public") { auto usages = CryptoKeyUsageSet::validate(normalizedName, CryptoKeyUsageSet::Context::importPublic, keyUsages, ML_KEM_PUBLIC_USAGES); - auto& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, - "Import data for raw-public must be a buffer."); - return MlKemKey

::importRawPublic(keyBytes, normalizedName, extractable, 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& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, - "Import data for raw-seed must be a buffer."); - return MlKemKey

::importRawSeed(keyBytes, normalizedName, extractable, 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& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, - "Import data for spki must be a buffer."); - return MlKemKey

::importSpki(keyBytes, normalizedName, extractable, 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& keyBytes = JSG_REQUIRE_NONNULL(keyData.tryGet>(), DOMDataError, - "Import data for pkcs8 must be a buffer."); - return MlKemKey

::importPkcs8(keyBytes, normalizedName, extractable, 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."); From 1c2cb5ff750c597ae7423e3baf98e199ba0b4317 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Thu, 9 Jul 2026 16:46:03 +0200 Subject: [PATCH 05/11] Trigger CI From df831b4a99950403cf4b21a00e6e55caca806fe4 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Thu, 9 Jul 2026 17:06:21 +0200 Subject: [PATCH 06/11] Fix encapsulated key imports --- src/workerd/api/crypto/crypto.c++ | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index 987312e9d04..f9aae88391f 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -1010,8 +1010,10 @@ jsg::Promise SubtleCrypto::encapsulateKey(jsg::Lo return js.evalNow([&]() -> EncapsulatedKey { validateOperation(encapsulationKey, algorithm.name, CryptoKeyUsageSet::encapsulateKey()); auto [sharedKey, ciphertext] = encapsulationKey.impl->encapsulate(js); - auto sharedKeyRef = importKeySync(js, "raw-secret", sharedKey.copy(), - kj::mv(sharedKeyAlgorithm), extractable, kj::mv(keyUsages)); + 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), @@ -1048,8 +1050,9 @@ jsg::Promise> SubtleCrypto::decapsulateKey(jsg::Lock& js, return js.evalNow([&] { validateOperation(decapsulationKey, algorithm.name, CryptoKeyUsageSet::decapsulateKey()); auto secret = decapsulationKey.impl->decapsulate(js, ciphertext); - return importKeySync(js, "raw-secret", secret.copy(), kj::mv(sharedKeyAlgorithm), extractable, - kj::mv(keyUsages)); + 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)); }); } From f4e298e1320b405abd3527620de5668934201bcb Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Thu, 9 Jul 2026 17:22:05 +0200 Subject: [PATCH 07/11] Update experimental crypto types --- .../experimental/index.d.ts | 48 +++++++++---------- .../generated-snapshot/experimental/index.ts | 48 +++++++++---------- 2 files changed, 48 insertions(+), 48 deletions(-) diff --git a/types/generated-snapshot/experimental/index.d.ts b/types/generated-snapshot/experimental/index.d.ts index 978bfea1120..d64383ea5e6 100755 --- a/types/generated-snapshot/experimental/index.d.ts +++ b/types/generated-snapshot/experimental/index.d.ts @@ -1324,30 +1324,6 @@ declare abstract class Crypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ declare abstract class SubtleCrypto { - 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; /** * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. * @@ -1473,6 +1449,30 @@ 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, diff --git a/types/generated-snapshot/experimental/index.ts b/types/generated-snapshot/experimental/index.ts index fb115ccca8b..3f430572275 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -1330,30 +1330,6 @@ export declare abstract class Crypto { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto) */ export declare abstract class SubtleCrypto { - 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; /** * The **`encrypt()`** method of the SubtleCrypto interface encrypts data. * @@ -1479,6 +1455,30 @@ 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, From 139a8daf0af1ca4879046c123131650fb1165728 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Thu, 9 Jul 2026 17:38:51 +0200 Subject: [PATCH 08/11] Fix crypto extras lint --- src/workerd/api/tests/crypto-extras-test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workerd/api/tests/crypto-extras-test.js b/src/workerd/api/tests/crypto-extras-test.js index b283edfb8d3..e8bbbe693fc 100644 --- a/src/workerd/api/tests/crypto-extras-test.js +++ b/src/workerd/api/tests/crypto-extras-test.js @@ -423,7 +423,7 @@ export const ecJwkKeyConsistencyCheck = { export const webCryptoModernAlgorithmsRequireCompatFlag = { async test() { - strictEqual('supports' in SubtleCrypto, false); + 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); From 6dffb0200bc8bee566342e24a9d964013159d5e0 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Fri, 10 Jul 2026 15:55:37 +0200 Subject: [PATCH 09/11] Address @bwesterb comments * throw if alg is not normalised * Validate JWK key_ops. * Use publicFromPrivate() in getPublicKey() * Zeroize * Added missing-alg ML-DSA JWK rejection test * ECDH P-521 supports("deriveBits", ..., 528) behavior --- src/workerd/api/crypto/crypto.c++ | 7 +- src/workerd/api/crypto/mldsa.c++ | 55 +++++++++----- src/workerd/api/crypto/mlkem.c++ | 22 +++--- src/workerd/api/tests/BUILD.bazel | 6 ++ .../tests/crypto-modern-algorithms-test.js | 71 +++++++++++++++++++ .../crypto-modern-algorithms-test.wd-test | 14 ++++ 6 files changed, 146 insertions(+), 29 deletions(-) create mode 100644 src/workerd/api/tests/crypto-modern-algorithms-test.js create mode 100644 src/workerd/api/tests/crypto-modern-algorithms-test.wd-test diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index f9aae88391f..7a07a7adc92 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -352,7 +352,12 @@ uint32_t getEcdhMaxDeriveBits(jsg::Lock& js, const CryptoKey& publicKey) { if (namedCurve == "P-256") return 256; if (namedCurve == "P-384") return 384; KJ_ASSERT(namedCurve == "P-521", namedCurve); - return 521; + + // 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, diff --git a/src/workerd/api/crypto/mldsa.c++ b/src/workerd/api/crypto/mldsa.c++ index 9b2ed507913..d5b8cc0304b 100644 --- a/src/workerd/api/crypto/mldsa.c++ +++ b/src/workerd/api/crypto/mldsa.c++ @@ -3,6 +3,8 @@ #include #include +#include + namespace workerd::api { namespace { @@ -191,6 +193,9 @@ class MlDsaKey final: public CryptoKey::Impl { 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, @@ -294,12 +299,9 @@ class MlDsaKey final: public CryptoKey::Impl { JSG_REQUIRE( keyType == KeyType::PRIVATE, DOMInvalidAccessError, "getPublicKey requires a private key."); - // Parse the stored public key bytes into a PublicKey struct typename P::PublicKey pk; - CBS cbs; - CBS_init(&cbs, publicKeyBytes.begin(), publicKeyBytes.size()); - JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), InternalDOMOperationError, - "Failed to parse public key from private key."); + 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); @@ -317,17 +319,15 @@ class MlDsaKey final: public CryptoKey::Impl { 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); - // Parse public key for the public CryptoKey typename P::PublicKey pk; - CBS cbs; - CBS_init(&cbs, encodedPublicKey, P::PUBLIC_KEY_BYTES); - JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), InternalDOMOperationError, - "Failed to parse generated ML-DSA public key"); + 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)); @@ -366,6 +366,7 @@ class MlDsaKey final: public CryptoKey::Impl { 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; @@ -495,6 +496,7 @@ class MlDsaKey final: public CryptoKey::Impl { 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; @@ -519,14 +521,31 @@ class MlDsaKey final: public CryptoKey::Impl { JSG_REQUIRE(jwk.kty == "AKP", DOMDataError, "JWK \"kty\" field must be \"AKP\" for ", normalizedName, "."); - KJ_IF_SOME(alg, jwk.alg) { - JSG_REQUIRE(alg == normalizedName, DOMDataError, "JWK \"alg\" field \"", alg, - "\" does not match \"", 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(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) { @@ -548,6 +567,7 @@ class MlDsaKey final: public CryptoKey::Impl { 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; @@ -600,7 +620,8 @@ class MlDsaKey final: public CryptoKey::Impl { KeyType keyType; kj::StringPtr algorithmName; - // Public key data (present for both public and private keys) + // 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; diff --git a/src/workerd/api/crypto/mlkem.c++ b/src/workerd/api/crypto/mlkem.c++ index 8c850023fb0..95433e5d52a 100644 --- a/src/workerd/api/crypto/mlkem.c++ +++ b/src/workerd/api/crypto/mlkem.c++ @@ -128,6 +128,9 @@ class MlKemKey final: public CryptoKey::Impl { 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 { @@ -230,12 +233,8 @@ class MlKemKey final: public CryptoKey::Impl { JSG_REQUIRE( keyType == KeyType::PRIVATE, DOMInvalidAccessError, "getPublicKey requires a private key."); - // Parse the stored public key bytes into a PublicKey struct typename P::PublicKey pk; - CBS cbs; - CBS_init(&cbs, publicKeyBytes.begin(), publicKeyBytes.size()); - JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), InternalDOMOperationError, - "Failed to parse public key from private key."); + P::publicFromPrivate(&pk, &KJ_ASSERT_NONNULL(privateKey)); return kj::heap>( kj::mv(pk), kj::heapArray(publicKeyBytes), algorithmName, true, usages); @@ -252,17 +251,14 @@ class MlKemKey final: public CryptoKey::Impl { 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); - // Parse public key for the public CryptoKey typename P::PublicKey pk; - CBS cbs; - CBS_init(&cbs, encodedPublicKey, P::PUBLIC_KEY_BYTES); - JSG_REQUIRE(1 == P::parsePublicKey(&pk, &cbs), InternalDOMOperationError, - "Failed to parse generated ML-KEM public key"); + P::publicFromPrivate(&pk, &sk); auto privateKey = js.alloc(kj::heap>(kj::mv(seedArray), kj::mv(sk), kj::mv(pkBytesCopy), normalizedName, extractable, privateKeyUsages)); @@ -301,6 +297,7 @@ class MlKemKey final: public CryptoKey::Impl { 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; @@ -427,6 +424,7 @@ class MlKemKey final: public CryptoKey::Impl { 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; @@ -494,6 +492,7 @@ class MlKemKey final: public CryptoKey::Impl { 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); @@ -542,7 +541,8 @@ class MlKemKey final: public CryptoKey::Impl { KeyType keyType; kj::StringPtr algorithmName; - // Public key data (present for both public and private keys) + // 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; 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-modern-algorithms-test.js b/src/workerd/api/tests/crypto-modern-algorithms-test.js new file mode 100644 index 00000000000..16ba4b972fe --- /dev/null +++ b/src/workerd/api/tests/crypto-modern-algorithms-test.js @@ -0,0 +1,71 @@ +// 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 } 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 + ); + }, +}; + +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"], + ) + ), + ], +); From 89fa6e762c43dccb0f4f37fe876df7847c591dd6 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Fri, 10 Jul 2026 17:10:03 +0200 Subject: [PATCH 10/11] Tighten modern crypto supports --- src/workerd/api/crypto/aes.c++ | 16 ----------- src/workerd/api/crypto/crypto.c++ | 28 ++++++------------- src/workerd/api/crypto/impl.h | 15 ++++++++++ src/workerd/api/crypto/mldsa.c++ | 8 ++---- src/workerd/api/crypto/mlkem.c++ | 8 ++---- .../tests/crypto-modern-algorithms-test.js | 12 +++++++- 6 files changed, 40 insertions(+), 47 deletions(-) diff --git a/src/workerd/api/crypto/aes.c++ b/src/workerd/api/crypto/aes.c++ index 09587ff2bdb..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, diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index 7a07a7adc92..368c51e0a3a 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -314,21 +314,6 @@ void validateAesKeyLength(int length) { "AES key length must be 128, 192, or 256 bits."); } -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, "."); - } -} - void validateNamedCurve(kj::StringPtr namedCurve) { JSG_REQUIRE(namedCurve == "P-256" || namedCurve == "P-384" || namedCurve == "P-521", DOMNotSupportedError, "Unsupported namedCurve \"", namedCurve, "\"."); @@ -1110,6 +1095,13 @@ bool SubtleCrypto::supports(jsg::Lock& js, 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, @@ -1285,11 +1277,7 @@ bool SubtleCrypto::supports(jsg::Lock& js, } } - kj::Maybe maybeLength = kj::none; - KJ_IF_SOME(thirdArgument, lengthOrAdditionalAlgorithm) { - maybeLength = readSupportsLength(js, thirdArgument); - } - return checkSupportForAlgorithm(parsedOperation, algorithm, maybeLength); + return checkSupportForAlgorithm(parsedOperation, algorithm, parsedLength); }, [](jsg::Value&&) { return false; }); } diff --git a/src/workerd/api/crypto/impl.h b/src/workerd/api/crypto/impl.h index 02ba0c68318..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 diff --git a/src/workerd/api/crypto/mldsa.c++ b/src/workerd/api/crypto/mldsa.c++ index d5b8cc0304b..3079ebb1f84 100644 --- a/src/workerd/api/crypto/mldsa.c++ +++ b/src/workerd/api/crypto/mldsa.c++ @@ -271,14 +271,12 @@ class MlDsaKey final: public CryptoKey::Impl { if (otherMlDsa == nullptr) return false; if (keyType != otherMlDsa->keyType) return false; if (keyType == KeyType::PUBLIC) { - return publicKeyBytes.size() == otherMlDsa->publicKeyBytes.size() && - CRYPTO_memcmp(publicKeyBytes.begin(), otherMlDsa->publicKeyBytes.begin(), - publicKeyBytes.size()) == 0; + 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 thisSeed.size() == otherSeed.size() && - CRYPTO_memcmp(thisSeed.begin(), otherSeed.begin(), thisSeed.size()) == 0; + return CRYPTO_memcmp(thisSeed.begin(), otherSeed.begin(), thisSeed.size()) == 0; } } diff --git a/src/workerd/api/crypto/mlkem.c++ b/src/workerd/api/crypto/mlkem.c++ index 95433e5d52a..66fd6d511ea 100644 --- a/src/workerd/api/crypto/mlkem.c++ +++ b/src/workerd/api/crypto/mlkem.c++ @@ -205,14 +205,12 @@ class MlKemKey final: public CryptoKey::Impl { if (otherMlKem == nullptr) return false; if (keyType != otherMlKem->keyType) return false; if (keyType == KeyType::PUBLIC) { - return publicKeyBytes.size() == otherMlKem->publicKeyBytes.size() && - CRYPTO_memcmp(publicKeyBytes.begin(), otherMlKem->publicKeyBytes.begin(), - publicKeyBytes.size()) == 0; + 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 thisSeed.size() == otherSeed.size() && - CRYPTO_memcmp(thisSeed.begin(), otherSeed.begin(), thisSeed.size()) == 0; + return CRYPTO_memcmp(thisSeed.begin(), otherSeed.begin(), thisSeed.size()) == 0; } } diff --git a/src/workerd/api/tests/crypto-modern-algorithms-test.js b/src/workerd/api/tests/crypto-modern-algorithms-test.js index 16ba4b972fe..491d24f76ac 100644 --- a/src/workerd/api/tests/crypto-modern-algorithms-test.js +++ b/src/workerd/api/tests/crypto-modern-algorithms-test.js @@ -1,7 +1,7 @@ // 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 } from 'node:assert'; +import { rejects, strictEqual, throws } from 'node:assert'; export const supportsEcdhP521ByteRoundedDeriveBits = { async test() { @@ -27,6 +27,16 @@ export const supportsEcdhP521ByteRoundedDeriveBits = { 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 + ); }, }; From 0e1b3c45ea29a9ad53668e100435e06e927b6a6c Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Fri, 10 Jul 2026 17:27:17 +0200 Subject: [PATCH 11/11] Restore digest input order --- src/workerd/api/crypto/crypto.c++ | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workerd/api/crypto/crypto.c++ b/src/workerd/api/crypto/crypto.c++ index 368c51e0a3a..99df74f5e02 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -703,12 +703,12 @@ jsg::Promise> SubtleCrypto::digest( auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, algorithm); return js.evalNow([&] { - auto ptr = nonNullBytes(data.asArrayPtr()); auto type = lookupDigestAlgorithm(algorithm.name).second; auto digestCtx = kj::disposeWith(EVP_MD_CTX_new()); KJ_ASSERT(digestCtx.get() != nullptr); + auto ptr = nonNullBytes(data.asArrayPtr()); OSSLCALL(EVP_DigestInit_ex(digestCtx.get(), type, nullptr)); OSSLCALL(EVP_DigestUpdate(digestCtx.get(), ptr.begin(), ptr.size()));