From 6a5af364cb95b8df9c6b1e272547b179557aa04a Mon Sep 17 00:00:00 2001 From: Filip Skokan Date: Tue, 24 Mar 2026 23:12:08 +0100 Subject: [PATCH] implement a subset of "Modern Algorithms in the Web Cryptography API" Co-authored-by: GitHub Copilot --- build/wpt_test.bzl | 11 +- deps/rust/Cargo.lock | 20 + deps/rust/cargo.bzl | 1 + deps/rust/crates/BUILD.bazel | 12 + deps/rust/crates/BUILD.keccak-0.1.6.bazel | 53 ++ deps/rust/crates/BUILD.sha3-0.10.8.bazel | 48 ++ deps/rust/crates/defs.bzl | 23 + src/rust/sha3/BUILD.bazel | 10 + src/rust/sha3/lib.rs | 101 +++ src/workerd/api/crypto/aes.c++ | 7 +- src/workerd/api/crypto/chacha20.c++ | 240 ++++++ src/workerd/api/crypto/crypto.c++ | 231 ++++- src/workerd/api/crypto/crypto.h | 134 ++- 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++ | 609 ++++++++++++++ src/workerd/api/crypto/pbkdf2.c++ | 2 +- src/workerd/api/crypto/rsa.c++ | 20 + src/workerd/io/BUILD.bazel | 1 + src/wpt/BUILD.bazel | 1 + src/wpt/WebCryptoAPI-test.ts | 349 ++++---- src/wpt/harness/utils.ts | 48 +- .../experimental/index.d.ts | 45 +- .../generated-snapshot/experimental/index.ts | 45 +- types/generated-snapshot/latest/index.d.ts | 45 +- types/generated-snapshot/latest/index.ts | 45 +- 31 files changed, 2742 insertions(+), 235 deletions(-) create mode 100644 deps/rust/crates/BUILD.keccak-0.1.6.bazel create mode 100644 deps/rust/crates/BUILD.sha3-0.10.8.bazel 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 a92e392663f..e990a527b4c 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) @@ -144,7 +145,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, @@ -171,6 +172,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), }, ) @@ -187,7 +190,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 @@ -198,7 +201,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 d52f63dd269..b5a89291234 100644 --- a/deps/rust/Cargo.lock +++ b/deps/rust/Cargo.lock @@ -466,6 +466,7 @@ dependencies = [ "scratch", "serde", "serde_json", + "sha3", "static_assertions", "swc_common", "swc_ts_fast_strip", @@ -929,6 +930,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + [[package]] name = "libc" version = "0.2.183" @@ -1524,6 +1534,16 @@ dependencies = [ "digest", ] +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/deps/rust/cargo.bzl b/deps/rust/cargo.bzl index befe48f70f5..252ff0e4dc3 100644 --- a/deps/rust/cargo.bzl +++ b/deps/rust/cargo.bzl @@ -29,6 +29,7 @@ PACKAGES = WORKERD_CXX_PACKAGES | { # param_extractor depends on unbounded_depth feature "serde_json": crate.spec(version = "1", features = ["unbounded_depth"]), "serde": crate.spec(version = "1", features = ["derive"]), + "sha3": crate.spec(version = "0", default_features = False), "thiserror": crate.spec(version = "2"), # tokio is huge, let's enable only features when we actually need them. "tokio": crate.spec(version = "1", default_features = False, features = ["net", "rt", "rt-multi-thread", "time"]), diff --git a/deps/rust/crates/BUILD.bazel b/deps/rust/crates/BUILD.bazel index 8e694fd2a83..2f2608f0496 100644 --- a/deps/rust/crates/BUILD.bazel +++ b/deps/rust/crates/BUILD.bazel @@ -337,6 +337,18 @@ alias( tags = ["manual"], ) +alias( + name = "sha3-0.10.8", + actual = "@crates_vendor__sha3-0.10.8//:sha3", + tags = ["manual"], +) + +alias( + name = "sha3", + actual = "@crates_vendor__sha3-0.10.8//:sha3", + tags = ["manual"], +) + alias( name = "static_assertions-1.1.0", actual = "@crates_vendor__static_assertions-1.1.0//:static_assertions", diff --git a/deps/rust/crates/BUILD.keccak-0.1.6.bazel b/deps/rust/crates/BUILD.keccak-0.1.6.bazel new file mode 100644 index 00000000000..1054efd7e96 --- /dev/null +++ b/deps/rust/crates/BUILD.keccak-0.1.6.bazel @@ -0,0 +1,53 @@ +############################################################################### +# @generated +# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To +# regenerate this file, run the following: +# +# bazel run @@//deps/rust:crates_vendor +############################################################################### + +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "keccak", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_root = "src/lib.rs", + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=keccak", + "manual", + "noclippy", + "norustfmt", + ], + version = "0.1.6", + deps = select({ + "@rules_rust//rust/platform:aarch64-apple-darwin": [ + "@crates_vendor__cpufeatures-0.2.17//:cpufeatures", # cfg(target_arch = "aarch64") + ], + "@rules_rust//rust/platform:aarch64-unknown-linux-gnu": [ + "@crates_vendor__cpufeatures-0.2.17//:cpufeatures", # cfg(target_arch = "aarch64") + ], + "//conditions:default": [], + }), +) diff --git a/deps/rust/crates/BUILD.sha3-0.10.8.bazel b/deps/rust/crates/BUILD.sha3-0.10.8.bazel new file mode 100644 index 00000000000..44110c3c1c9 --- /dev/null +++ b/deps/rust/crates/BUILD.sha3-0.10.8.bazel @@ -0,0 +1,48 @@ +############################################################################### +# @generated +# DO NOT MODIFY: This file is auto-generated by a crate_universe tool. To +# regenerate this file, run the following: +# +# bazel run @@//deps/rust:crates_vendor +############################################################################### + +load("@rules_rust//rust:defs.bzl", "rust_library") + +package(default_visibility = ["//visibility:public"]) + +rust_library( + name = "sha3", + srcs = glob( + include = ["**/*.rs"], + allow_empty = True, + ), + compile_data = glob( + include = ["**"], + allow_empty = True, + exclude = [ + "**/* *", + ".tmp_git_root/**/*", + "BUILD", + "BUILD.bazel", + "WORKSPACE", + "WORKSPACE.bazel", + ], + ), + crate_root = "src/lib.rs", + edition = "2018", + rustc_flags = [ + "--cap-lints=allow", + ], + tags = [ + "cargo-bazel", + "crate-name=sha3", + "manual", + "noclippy", + "norustfmt", + ], + version = "0.10.8", + deps = [ + "@crates_vendor__digest-0.10.7//:digest", + "@crates_vendor__keccak-0.1.6//:keccak", + ], +) diff --git a/deps/rust/crates/defs.bzl b/deps/rust/crates/defs.bzl index 7eb63e7c39a..fe88962e405 100644 --- a/deps/rust/crates/defs.bzl +++ b/deps/rust/crates/defs.bzl @@ -319,6 +319,7 @@ _NORMAL_DEPENDENCIES = { "scratch": Label("@crates_vendor//:scratch-1.0.9"), "serde": Label("@crates_vendor//:serde-1.0.228"), "serde_json": Label("@crates_vendor//:serde_json-1.0.149"), + "sha3": Label("@crates_vendor//:sha3-0.10.8"), "static_assertions": Label("@crates_vendor//:static_assertions-1.1.0"), "swc_common": Label("@crates_vendor//:swc_common-18.0.1"), "swc_ts_fast_strip": Label("@crates_vendor//:swc_ts_fast_strip-43.0.0"), @@ -410,6 +411,7 @@ _CONDITIONS = { "cfg(any(unix, target_os = \"wasi\"))": ["@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-darwin", "@rules_rust//rust/platform:x86_64-unknown-linux-gnu"], "cfg(not(all(target_arch = \"arm\", target_os = \"none\")))": ["@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-darwin", "@rules_rust//rust/platform:x86_64-pc-windows-msvc", "@rules_rust//rust/platform:x86_64-unknown-linux-gnu"], "cfg(not(windows))": ["@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu", "@rules_rust//rust/platform:x86_64-apple-darwin", "@rules_rust//rust/platform:x86_64-unknown-linux-gnu"], + "cfg(target_arch = \"aarch64\")": ["@rules_rust//rust/platform:aarch64-apple-darwin", "@rules_rust//rust/platform:aarch64-unknown-linux-gnu"], "cfg(target_os = \"hermit\")": [], "cfg(target_os = \"netbsd\")": [], "cfg(target_os = \"solaris\")": [], @@ -1390,6 +1392,16 @@ def crate_repositories(): build_file = Label("//deps/rust/crates:BUILD.js-sys-0.3.91.bazel"), ) + maybe( + http_archive, + name = "crates_vendor__keccak-0.1.6", + sha256 = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653", + type = "tar.gz", + urls = ["https://static.crates.io/crates/keccak/0.1.6/download"], + strip_prefix = "keccak-0.1.6", + build_file = Label("//deps/rust/crates:BUILD.keccak-0.1.6.bazel"), + ) + maybe( http_archive, name = "crates_vendor__libc-0.2.183", @@ -2039,6 +2051,16 @@ def crate_repositories(): build_file = Label("//deps/rust/crates:BUILD.sha1-0.10.6.bazel"), ) + maybe( + http_archive, + name = "crates_vendor__sha3-0.10.8", + sha256 = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60", + type = "tar.gz", + urls = ["https://static.crates.io/crates/sha3/0.10.8/download"], + strip_prefix = "sha3-0.10.8", + build_file = Label("//deps/rust/crates:BUILD.sha3-0.10.8.bazel"), + ) + maybe( http_archive, name = "crates_vendor__shlex-1.3.0", @@ -2855,6 +2877,7 @@ def crate_repositories(): struct(repo = "crates_vendor__scratch-1.0.9", is_dev_dep = False), struct(repo = "crates_vendor__serde-1.0.228", is_dev_dep = False), struct(repo = "crates_vendor__serde_json-1.0.149", is_dev_dep = False), + struct(repo = "crates_vendor__sha3-0.10.8", is_dev_dep = False), struct(repo = "crates_vendor__static_assertions-1.1.0", is_dev_dep = False), struct(repo = "crates_vendor__swc_common-18.0.1", is_dev_dep = False), struct(repo = "crates_vendor__swc_ts_fast_strip-43.0.0", is_dev_dep = False), diff --git a/src/rust/sha3/BUILD.bazel b/src/rust/sha3/BUILD.bazel new file mode 100644 index 00000000000..ddbdd5c5c58 --- /dev/null +++ b/src/rust/sha3/BUILD.bazel @@ -0,0 +1,10 @@ +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//:sha3", + ], +) diff --git a/src/rust/sha3/lib.rs b/src/rust/sha3/lib.rs new file mode 100644 index 00000000000..305a31b2589 --- /dev/null +++ b/src/rust/sha3/lib.rs @@ -0,0 +1,101 @@ +use sha3::Digest; +use sha3::digest::ExtendableOutput; +use sha3::digest::Update; +use sha3::digest::XofReader; +use sha3::digest::core_api::CoreWrapper; + +#[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 turboshake128(data: &[u8], output_len: usize, domain_separation: u8) -> Vec; + fn turboshake256(data: &[u8], output_len: usize, domain_separation: u8) -> Vec; + fn cshake128( + data: &[u8], + output_len: usize, + function_name: &[u8], + customization: &[u8], + ) -> Vec; + fn cshake256( + data: &[u8], + output_len: usize, + function_name: &[u8], + customization: &[u8], + ) -> Vec; + } +} + +#[must_use] +pub fn sha3_256(data: &[u8]) -> Vec { + sha3::Sha3_256::digest(data).to_vec() +} + +#[must_use] +pub fn sha3_384(data: &[u8]) -> Vec { + sha3::Sha3_384::digest(data).to_vec() +} + +#[must_use] +pub fn sha3_512(data: &[u8]) -> Vec { + sha3::Sha3_512::digest(data).to_vec() +} + +#[must_use] +pub fn turboshake128(data: &[u8], output_len: usize, domain_separation: u8) -> Vec { + debug_assert!((0x01..=0x7F).contains(&domain_separation)); + let mut hasher = CoreWrapper::from_core(sha3::TurboShake128Core::new(domain_separation)); + hasher.update(data); + let mut reader = hasher.finalize_xof(); + let mut output = vec![0u8; output_len]; + reader.read(&mut output); + output +} + +#[must_use] +pub fn turboshake256(data: &[u8], output_len: usize, domain_separation: u8) -> Vec { + debug_assert!((0x01..=0x7F).contains(&domain_separation)); + let mut hasher = CoreWrapper::from_core(sha3::TurboShake256Core::new(domain_separation)); + hasher.update(data); + let mut reader = hasher.finalize_xof(); + let mut output = vec![0u8; output_len]; + reader.read(&mut output); + output +} + +#[must_use] +pub fn cshake128( + data: &[u8], + output_len: usize, + function_name: &[u8], + customization: &[u8], +) -> Vec { + let mut hasher = CoreWrapper::from_core(sha3::CShake128Core::new_with_function_name( + function_name, + customization, + )); + hasher.update(data); + let mut reader = hasher.finalize_xof(); + let mut output = vec![0u8; output_len]; + reader.read(&mut output); + output +} + +#[must_use] +pub fn cshake256( + data: &[u8], + output_len: usize, + function_name: &[u8], + customization: &[u8], +) -> Vec { + let mut hasher = CoreWrapper::from_core(sha3::CShake256Core::new_with_function_name( + function_name, + customization, + )); + hasher.update(data); + let mut reader = hasher.finalize_xof(); + let mut output = vec![0u8; output_len]; + reader.read(&mut output); + output +} diff --git a/src/workerd/api/crypto/aes.c++ b/src/workerd/api/crypto/aes.c++ index 200ba57f6d6..5c535ea6b05 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(). keyDataArray = kj::mv(keyData.get>()); switch (keyDataArray.size() * 8) { 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 6cf3456554f..5a3574ecde0 100644 --- a/src/workerd/api/crypto/crypto.c++ +++ b/src/workerd/api/crypto/crypto.c++ @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -35,6 +36,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"); } @@ -46,8 +51,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; } @@ -122,6 +128,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}); @@ -167,6 +180,7 @@ kj::Maybe getKeyLength(const SubtleCrypto::ImportKeyAlgorithm& derived {"AES-CBC"}, {"AES-GCM"}, {"AES-KW"}, + {"ChaCha20-Poly1305"}, {"HMAC"}, {"HKDF"}, {"PBKDF2"}, @@ -194,6 +208,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. @@ -383,13 +399,101 @@ jsg::Promise SubtleCrypto::verify(jsg::Lock& js, } jsg::Promise> SubtleCrypto::digest(jsg::Lock& js, - kj::OneOf algorithmParam, + kj::OneOf algorithmParam, kj::Array data) { auto algorithm = interpretAlgorithmParam(kj::mv(algorithmParam)); auto checkErrorsOnFinish = webCryptoOperationBegin(__func__, algorithm); return js.evalNow([&] { + // TurboSHAKE is not available via BoringSSL, so we use Rust (RustCrypto sha3 crate). + // TurboSHAKE is an XOF with variable output length and domain separation byte. + if (strcasecmp(algorithm.name.cStr(), "TurboSHAKE128") == 0 || + strcasecmp(algorithm.name.cStr(), "TurboSHAKE256") == 0) { + auto outputLengthBits = JSG_REQUIRE_NONNULL( + algorithm.outputLength, DOMOperationError, "outputLength is required for TurboSHAKE"); + JSG_REQUIRE(outputLengthBits > 0 && outputLengthBits % 8 == 0, DOMOperationError, + "outputLength must be a positive multiple of 8"); + auto outputLengthBytes = static_cast(outputLengthBits / 8); + + uint8_t domainSep = 0x1F; + KJ_IF_SOME(ds, algorithm.domainSeparation) { + JSG_REQUIRE(ds >= 0x01 && ds <= 0x7F, DOMOperationError, + "domainSeparation must be between 0x01 and 0x7F"); + domainSep = static_cast(ds); + } + + auto input = ::rust::Slice(data.begin(), data.size()); + auto result = strcasecmp(algorithm.name.cStr(), "TurboSHAKE128") == 0 + ? workerd::rust::sha3::turboshake128(input, outputLengthBytes, domainSep) + : workerd::rust::sha3::turboshake256(input, outputLengthBytes, domainSep); + auto buf = jsg::JsArrayBuffer::create(js, result.size()); + memcpy(buf.asArrayPtr().begin(), result.data(), result.size()); + return buf.addRef(js); + } + + // cSHAKE is not available via BoringSSL, so we use Rust (RustCrypto sha3 crate). + // cSHAKE is an XOF with variable output length, function name (N), and customization (S). + 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); + + kj::Array fnNameArr; + KJ_IF_SOME(fnNameRef, algorithm.functionName) { + auto fnNameHandle = fnNameRef.getHandle(js); + auto fnNameBytes = fnNameHandle.asArrayPtr(); + // functionName, when non-empty, must be a NIST-defined value. + if (fnNameBytes.size() > 0) { + auto fnNameStr = kj::str(fnNameBytes.asChars()); + JSG_REQUIRE( + fnNameStr == "TupleHash" || fnNameStr == "ParallelHash" || fnNameStr == "KMAC", + DOMNotSupportedError, + "Unsupported functionName value. Must be empty, \"TupleHash\", \"ParallelHash\", " + "or \"KMAC\"."); + } + fnNameArr = kj::heapArray(fnNameBytes); + } + + kj::Array custArr; + KJ_IF_SOME(custRef, algorithm.customization) { + auto custHandle = custRef.getHandle(js); + custArr = kj::heapArray(custHandle.asArrayPtr()); + } + + auto input = ::rust::Slice(data.begin(), data.size()); + auto fnName = ::rust::Slice(fnNameArr.begin(), fnNameArr.size()); + auto cust = ::rust::Slice(custArr.begin(), custArr.size()); + auto result = strcasecmp(algorithm.name.cStr(), "cSHAKE128") == 0 + ? workerd::rust::sha3::cshake128(input, outputLengthBytes, fnName, cust) + : workerd::rust::sha3::cshake256(input, outputLengthBytes, fnName, cust); + auto buf = jsg::JsArrayBuffer::create(js, result.size()); + memcpy(buf.asArrayPtr().begin(), result.data(), result.size()); + return buf.addRef(js); + } + + // SHA-3 is not available via BoringSSL's EVP_MD interface, so we use a + // Rust implementation from the RustCrypto sha3 crate. + 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({data.begin(), data.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()); @@ -461,8 +565,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.copy(), kj::mv(derivedKeyAlgorithm), extractable, kj::mv(keyUsages)); + return importKeySync(js, "raw-secret", secret.copy(), kj::mv(derivedKeyAlgorithm), extractable, + kj::mv(keyUsages)); }); } @@ -596,10 +700,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."); // Make a copy of the key import data. keyData = kj::heapArray(key.asPtr()); @@ -652,6 +756,117 @@ 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::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 ad2fff86e83..6d5138ceb3b 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,19 @@ 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 + // like TurboSHAKE and cSHAKE. + 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 +424,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 +544,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 }; @@ -542,7 +576,7 @@ class SubtleCrypto: public jsg::Object { kj::Array data); jsg::Promise> digest(jsg::Lock& js, - kj::OneOf algorithm, + kj::OneOf algorithm, kj::Array data); jsg::Promise, CryptoKeyPair>> generateKey(jsg::Lock& js, @@ -599,6 +633,49 @@ 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); + // This is a non-standard extension based off Node.js' implementation of crypto.timingSafeEqual. bool timingSafeEqual(jsg::JsBufferSource a, jsg::JsBufferSource b); @@ -615,6 +692,11 @@ 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({ @@ -627,9 +709,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) @@ -643,6 +727,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; }); } }; @@ -777,13 +883,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 79e85e54907..25b23c72176 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(). keyDataArray = kj::mv(keyData.get>()); } else if (format == "jwk") { diff --git a/src/workerd/api/crypto/ec.c++ b/src/workerd/api/crypto/ec.c++ index a81a337bafc..83ce4b7cb94 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 @@ -703,7 +708,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 @@ -764,7 +769,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 @@ -838,6 +843,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 { @@ -1169,7 +1179,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 1e169c070a5..54cc5d4dd7e 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 b9477be2b20..8916ac8f9e8 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 03a20891e6d..37fcbd500d2 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..641fb89ea4f --- /dev/null +++ b/src/workerd/api/crypto/mlkem.c++ @@ -0,0 +1,609 @@ +#include "impl.h" + +#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 { + 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 is intentionally not supported for ML-KEM. + // TODO(conform): Add JWK format support once it is standardized. + + 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); + } +}; + +// 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 { + 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 845696e552f..a9cbe50f902 100644 --- a/src/workerd/api/crypto/pbkdf2.c++ +++ b/src/workerd/api/crypto/pbkdf2.c++ @@ -129,7 +129,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/io/BUILD.bazel b/src/workerd/io/BUILD.bazel index 669d3e827f7..41166707f27 100644 --- a/src/workerd/io/BUILD.bazel +++ b/src/workerd/io/BUILD.bazel @@ -61,6 +61,7 @@ wd_cc_library( ] + ["//src/workerd/api:hdrs"], implementation_deps = [ "//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 289f0897bdb..0984ad2ea23 100644 --- a/src/wpt/BUILD.bazel +++ b/src/wpt/BUILD.bazel @@ -99,6 +99,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..6e140fc8655 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': {}, + '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,53 @@ 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)', + 'SubtleCrypto interface: operation supports(DOMString, AlgorithmIdentifier, optional unsigned long?)', + 'SubtleCrypto interface: operation supports(DOMString, AlgorithmIdentifier, AlgorithmIdentifier)', + 'SubtleCrypto interface: calling supports(DOMString, AlgorithmIdentifier, optional unsigned long?) on crypto.subtle with too few arguments must throw TypeError', + 'SubtleCrypto interface: calling supports(DOMString, AlgorithmIdentifier, AlgorithmIdentifier) on crypto.subtle with too few arguments must throw TypeError', + '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 +290,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 +318,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 +333,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 +347,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 +373,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 +386,28 @@ 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': { + comment: 'SubtleCrypto.supports is not implemented', + omittedTests: true, }, - + '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 7f0efa943d8..2a1a21af689 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 { * [MDN Reference](https://developer.mozilla.org/docs/Web/API/SubtleCrypto/digest) */ digest( - algorithm: string | SubtleCryptoHashAlgorithm, + algorithm: string | SubtleCryptoDigestAlgorithm, data: ArrayBuffer | ArrayBufferView, ): Promise; /** @@ -1409,6 +1409,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, @@ -1475,12 +1500,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; @@ -1509,6 +1544,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; @@ -1521,6 +1563,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 b8c0bd5c81a..d1f6917ef4c 100755 --- a/types/generated-snapshot/experimental/index.ts +++ b/types/generated-snapshot/experimental/index.ts @@ -1333,7 +1333,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; /** @@ -1411,6 +1411,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, @@ -1477,12 +1502,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; @@ -1511,6 +1546,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; @@ -1523,6 +1565,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/latest/index.d.ts b/types/generated-snapshot/latest/index.d.ts index 827ab0a143c..2e08b76697a 100755 --- a/types/generated-snapshot/latest/index.d.ts +++ b/types/generated-snapshot/latest/index.d.ts @@ -1270,7 +1270,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; /** @@ -1348,6 +1348,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, @@ -1414,12 +1439,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; @@ -1448,6 +1483,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; @@ -1460,6 +1502,7 @@ interface SubtleCryptoSignAlgorithm { hash?: string | SubtleCryptoHashAlgorithm; dataLength?: number; saltLength?: number; + context?: ArrayBuffer | ArrayBufferView; } interface CryptoKeyKeyAlgorithm { name: string; diff --git a/types/generated-snapshot/latest/index.ts b/types/generated-snapshot/latest/index.ts index 29b0eb905cc..688416d9dda 100755 --- a/types/generated-snapshot/latest/index.ts +++ b/types/generated-snapshot/latest/index.ts @@ -1272,7 +1272,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; /** @@ -1350,6 +1350,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, @@ -1416,12 +1441,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; @@ -1450,6 +1485,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; @@ -1462,6 +1504,7 @@ export interface SubtleCryptoSignAlgorithm { hash?: string | SubtleCryptoHashAlgorithm; dataLength?: number; saltLength?: number; + context?: ArrayBuffer | ArrayBufferView; } export interface CryptoKeyKeyAlgorithm { name: string;