From 0b0d608889c84b90d824512515ef672d58c673d5 Mon Sep 17 00:00:00 2001 From: Isaac Bremseth Date: Tue, 28 Apr 2026 15:20:49 -0400 Subject: [PATCH 1/4] Add secp256k1 ECDSA support via aws-lc-rs --- deps/rust/Cargo.lock | 57 +++ deps/rust/Cargo.toml | 1 + src/rust/aws-lc/BUILD.bazel | 11 + src/rust/aws-lc/lib.rs | 74 ++++ src/workerd/api/crypto/ec.c++ | 38 +- src/workerd/api/crypto/secp256k1-key.c++ | 363 ++++++++++++++++ src/workerd/api/crypto/secp256k1-key.h | 65 +++ src/workerd/api/tests/BUILD.bazel | 9 + .../tests/crypto-secp256k1-no-flag-test.js | 50 +++ .../api/tests/crypto-secp256k1-test.wd-test | 29 ++ .../tests/crypto-secp256k1-with-flag-test.js | 411 ++++++++++++++++++ src/workerd/io/BUILD.bazel | 1 + src/workerd/io/compatibility-date.capnp | 6 + 13 files changed, 1110 insertions(+), 5 deletions(-) create mode 100644 src/rust/aws-lc/BUILD.bazel create mode 100644 src/rust/aws-lc/lib.rs create mode 100644 src/workerd/api/crypto/secp256k1-key.c++ create mode 100644 src/workerd/api/crypto/secp256k1-key.h create mode 100644 src/workerd/api/tests/crypto-secp256k1-no-flag-test.js create mode 100644 src/workerd/api/tests/crypto-secp256k1-test.wd-test create mode 100644 src/workerd/api/tests/crypto-secp256k1-with-flag-test.js diff --git a/deps/rust/Cargo.lock b/deps/rust/Cargo.lock index d69c3f0a666..47a4383abf9 100644 --- a/deps/rust/Cargo.lock +++ b/deps/rust/Cargo.lock @@ -92,6 +92,29 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-lc-rs" +version = "1.16.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" +dependencies = [ + "aws-lc-sys", + "untrusted", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", +] + [[package]] name = "base64" version = "0.22.1" @@ -301,6 +324,15 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + [[package]] name = "codespan-reporting" version = "0.13.1" @@ -444,6 +476,7 @@ dependencies = [ "ada-url", "anyhow", "async-trait", + "aws-lc-rs", "capnp", "capnp-rpc", "capnpc", @@ -507,6 +540,12 @@ dependencies = [ "dtoa", ] +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.15.0" @@ -581,6 +620,12 @@ dependencies = [ "syn", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "funty" version = "2.0.0" @@ -2132,6 +2177,12 @@ dependencies = [ "rand", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.5.8" @@ -2341,6 +2392,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + [[package]] name = "zerotrie" version = "0.2.4" diff --git a/deps/rust/Cargo.toml b/deps/rust/Cargo.toml index 91ff8b6657b..35676959979 100644 --- a/deps/rust/Cargo.toml +++ b/deps/rust/Cargo.toml @@ -28,6 +28,7 @@ syn = { version = "2", features = ["full"] } ada-url = "3" anyhow = "1" async-trait = { version = "0", default-features = false } +aws-lc-rs = "1" capnp = "0" capnpc = "0" capnp-rpc = "0" diff --git a/src/rust/aws-lc/BUILD.bazel b/src/rust/aws-lc/BUILD.bazel new file mode 100644 index 00000000000..b66ff107031 --- /dev/null +++ b/src/rust/aws-lc/BUILD.bazel @@ -0,0 +1,11 @@ +load("//:build/wd_rust_crate.bzl", "wd_rust_crate") + +wd_rust_crate( + name = "aws-lc", + cxx_bridge_src = "lib.rs", + visibility = ["//visibility:public"], + deps = [ + "//src/rust/cxx-integration", + "@crates_vendor//:aws-lc-rs", + ], +) diff --git a/src/rust/aws-lc/lib.rs b/src/rust/aws-lc/lib.rs new file mode 100644 index 00000000000..3eae8a7b762 --- /dev/null +++ b/src/rust/aws-lc/lib.rs @@ -0,0 +1,74 @@ +use aws_lc_rs::encoding::AsBigEndian; +use aws_lc_rs::rand::SystemRandom; +use aws_lc_rs::signature::ECDSA_P256K1_SHA256_FIXED; +use aws_lc_rs::signature::ECDSA_P256K1_SHA256_FIXED_SIGNING; +use aws_lc_rs::signature::EcdsaKeyPair; +use aws_lc_rs::signature::KeyPair; +use aws_lc_rs::signature::ParsedPublicKey; + +#[cxx::bridge(namespace = "workerd::rust::aws_lc")] +mod ffi { + extern "Rust" { + fn validate_public(input: &[u8]) -> bool; + fn validate_keypair(seckey: &[u8], pubkey: &[u8]) -> bool; + fn generate_keypair() -> Vec; + fn sign(seckey: &[u8], pubkey: &[u8], message: &[u8]) -> Vec; + fn verify(pubkey: &[u8], message: &[u8], signature: &[u8]) -> bool; + } +} + +#[must_use] +pub fn validate_public(input: &[u8]) -> bool { + ParsedPublicKey::new(&ECDSA_P256K1_SHA256_FIXED, input).is_ok() +} + +#[must_use] +pub fn validate_keypair(seckey: &[u8], pubkey: &[u8]) -> bool { + EcdsaKeyPair::from_private_key_and_public_key( + &ECDSA_P256K1_SHA256_FIXED_SIGNING, + seckey, + pubkey, + ) + .is_ok() +} + +#[must_use] +pub fn generate_keypair() -> Vec { + let Ok(keypair) = EcdsaKeyPair::generate(&ECDSA_P256K1_SHA256_FIXED_SIGNING) else { + return Vec::new(); + }; + let Ok(seckey_bytes) = keypair.private_key().as_be_bytes() else { + return Vec::new(); + }; + let pubkey_bytes = keypair.public_key().as_ref(); + let mut out = Vec::with_capacity(32 + 65); + out.extend_from_slice(seckey_bytes.as_ref()); + out.extend_from_slice(pubkey_bytes); + out +} + +#[must_use] +pub fn sign(seckey: &[u8], pubkey: &[u8], message: &[u8]) -> Vec { + let Ok(keypair) = EcdsaKeyPair::from_private_key_and_public_key( + &ECDSA_P256K1_SHA256_FIXED_SIGNING, + seckey, + pubkey, + ) else { + return Vec::new(); + }; + // aws-lc-rs ignores the SecureRandom parameter (kept for ring compatibility); nonces come + // from AWS-LC's internal CSPRNG. + let rng = SystemRandom::new(); + let Ok(signature) = keypair.sign(&rng, message) else { + return Vec::new(); + }; + signature.as_ref().to_vec() +} + +#[must_use] +pub fn verify(pubkey: &[u8], message: &[u8], signature: &[u8]) -> bool { + let Ok(parsed) = ParsedPublicKey::new(&ECDSA_P256K1_SHA256_FIXED, pubkey) else { + return false; + }; + parsed.verify_sig(message, signature).is_ok() +} diff --git a/src/workerd/api/crypto/ec.c++ b/src/workerd/api/crypto/ec.c++ index 9b5117c4c8e..6afc8ab9d6e 100644 --- a/src/workerd/api/crypto/ec.c++ +++ b/src/workerd/api/crypto/ec.c++ @@ -6,6 +6,7 @@ #include "impl.h" #include "keys.h" +#include "secp256k1-key.h" #include #include @@ -429,8 +430,8 @@ class EllipticKey final: public AsymmetricKeyCryptoKeyImpl { struct EllipticCurveInfo { kj::StringPtr normalizedName; - int opensslCurveId; - uint rsSize; // size of "r" and "s" in the signature + int opensslCurveId; // 0 for curves BoringSSL doesn't support (e.g. secp256k1). + uint rsSize; // size of "r" and "s" in the signature }; EllipticCurveInfo lookupEllipticCurve(kj::StringPtr curveName) { @@ -446,6 +447,15 @@ EllipticCurveInfo lookupEllipticCurve(kj::StringPtr curveName) { return iter->second; } +// Overload that adds secp256k1 to the table when the compat flag is set. +EllipticCurveInfo lookupEllipticCurve(jsg::Lock& js, kj::StringPtr curveName) { + if (FeatureFlags::get(js).getSecp256k1EcdsaCurve() && + strcasecmp(curveName.cStr(), "secp256k1") == 0) { + return {"secp256k1", 0, 32}; + } + return lookupEllipticCurve(curveName); +} + kj::OneOf, CryptoKeyPair> EllipticKey::generateElliptic(jsg::Lock& js, kj::StringPtr normalizedName, SubtleCrypto::GenerateKeyAlgorithm&& algorithm, @@ -455,7 +465,16 @@ kj::OneOf, CryptoKeyPair> EllipticKey::generateElliptic(jsg: kj::StringPtr namedCurve = JSG_REQUIRE_NONNULL( algorithm.namedCurve, TypeError, "Missing field \"namedCurve\" in \"algorithm\"."); - auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(namedCurve); + auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(js, namedCurve); + + // The compat flag scopes secp256k1 support to ECDSA; ECDH is rejected. + if (strcasecmp(normalizedNamedCurve.cStr(), "secp256k1") == 0) { + JSG_REQUIRE(normalizedName == "ECDSA", DOMNotSupportedError, "\"", normalizedName, + "\" is not supported for curve \"secp256k1\"."); + return Secp256k1Key::generatePair(js, + CryptoKey::EllipticKeyAlgorithm{normalizedName, normalizedNamedCurve}, extractable, + privateKeyUsages, publicKeyUsages); + } auto keyAlgorithm = CryptoKey::EllipticKeyAlgorithm{ normalizedName, @@ -700,7 +719,13 @@ kj::Own CryptoKey::Impl::importEcdsa(jsg::Lock& js, kj::StringPtr namedCurve = JSG_REQUIRE_NONNULL( algorithm.namedCurve, TypeError, "Missing field \"namedCurve\" in \"algorithm\"."); - auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(namedCurve); + auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(js, namedCurve); + + if (strcasecmp(normalizedNamedCurve.cStr(), "secp256k1") == 0) { + return Secp256k1Key::import(js, normalizedName, format, kj::mv(keyData), + CryptoKey::EllipticKeyAlgorithm{normalizedName, normalizedNamedCurve}, extractable, + keyUsages); + } auto importedKey = [&, curveId = curveId] { if (format != "raw") { @@ -758,7 +783,10 @@ kj::Own CryptoKey::Impl::importEcdh(jsg::Lock& js, kj::StringPtr namedCurve = JSG_REQUIRE_NONNULL( algorithm.namedCurve, TypeError, "Missing field \"namedCurve\" in \"algorithm\"."); - auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(namedCurve); + auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(js, namedCurve); + + JSG_REQUIRE(strcasecmp(normalizedNamedCurve.cStr(), "secp256k1") != 0, DOMNotSupportedError, + "ECDH is not supported for curve \"secp256k1\"."); auto importedKey = [&, curveId = curveId] { auto strictCrypto = FeatureFlags::get(js).getStrictCrypto(); diff --git a/src/workerd/api/crypto/secp256k1-key.c++ b/src/workerd/api/crypto/secp256k1-key.c++ new file mode 100644 index 00000000000..b6d299d3088 --- /dev/null +++ b/src/workerd/api/crypto/secp256k1-key.c++ @@ -0,0 +1,363 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +#include "secp256k1-key.h" + +#include "impl.h" + +#include +#include + +#include +#include + +#include +#include + +#include + +namespace workerd::api { + +namespace { + +constexpr size_t SECKEY_LEN = 32; +constexpr size_t PUBKEY_UNCOMPRESSED_LEN = 65; +constexpr size_t COORDINATE_LEN = 32; +constexpr size_t SIGNATURE_LEN = 64; + +kj::Own wrapSecret(kj::Array bytes) { + return kj::heap(kj::mv(bytes)); +} + +// Use on public keys only. Secret material must go through ZeroOnFree-wrapped storage. +kj::Array decodeJwkCoordinate( + kj::String&& encoded, size_t expectedLen, kj::StringPtr fieldName) { + auto bytes = JSG_REQUIRE_NONNULL(decodeBase64Url(kj::mv(encoded)), DOMDataError, + "Invalid base64url encoding in JSON Web Key \"", fieldName, "\" field."); + JSG_REQUIRE(bytes.size() <= expectedLen, DOMDataError, "JSON Web Key \"", fieldName, + "\" must be at most ", expectedLen, " bytes."); + auto out = kj::heapArray(expectedLen); + memset(out.begin(), 0, expectedLen); + out.slice(expectedLen - bytes.size(), expectedLen).copyFrom(bytes); + return out; +} + +void validateHash( + const kj::Maybe>& callTimeHash) { + auto hashName = api::getAlgorithmName(JSG_REQUIRE_NONNULL(callTimeHash, TypeError, + "Missing \"hash\" in AlgorithmIdentifier. (ECDSA requires that the hash algorithm be " + "specified at call time rather than on the key. This differs from other WebCrypto " + "algorithms for historical reasons.)")); + + JSG_REQUIRE(hashName == "SHA-256", DOMNotSupportedError, + "secp256k1 ECDSA only supports SHA-256 (got \"", hashName, "\")."); +} + +kj::Own importRaw(kj::ArrayPtr keyData, + CryptoKey::EllipticKeyAlgorithm keyAlgorithm, + bool extractable, + CryptoKeyUsageSet usages) { + JSG_REQUIRE(keyData.size() == PUBKEY_UNCOMPRESSED_LEN, DOMDataError, + "Invalid secp256k1 public key length (expected ", PUBKEY_UNCOMPRESSED_LEN, + " bytes uncompressed, got ", keyData.size(), ")."); + + JSG_REQUIRE(::workerd::rust::aws_lc::validate_public( + ::rust::Slice(keyData.begin(), keyData.size())), + DOMDataError, "Invalid secp256k1 public key (failed to parse)."); + + auto pub = kj::heapArray(PUBKEY_UNCOMPRESSED_LEN); + memcpy(pub.begin(), keyData.begin(), PUBKEY_UNCOMPRESSED_LEN); + return kj::heap( + KeyType::PUBLIC, kj::mv(pub), kj::none, kj::mv(keyAlgorithm), extractable, usages); +} + +kj::Own importJwk(SubtleCrypto::JsonWebKey&& jwk, + CryptoKey::EllipticKeyAlgorithm keyAlgorithm, + bool extractable, + CryptoKeyUsageSet usages, + kj::ArrayPtr keyUsages) { + JSG_REQUIRE(jwk.kty == "EC", DOMDataError, + "Elliptic curve \"jwk\" key import requires a JSON Web Key with Key Type parameter " + "\"kty\" (\"", + jwk.kty, "\") equal to \"EC\"."); + + auto& crv = + JSG_REQUIRE_NONNULL(jwk.crv, DOMDataError, "Missing field \"crv\" for secp256k1 key."); + JSG_REQUIRE(crv == "secp256k1", DOMDataError, "JSON Web Key Curve parameter \"crv\" (\"", crv, + "\") does not match expected \"secp256k1\"."); + + // alg is optional; if present it must be ES256K (RFC 8812). + KJ_IF_SOME(alg, jwk.alg) { + JSG_REQUIRE(alg == "ES256K", DOMDataError, "JSON Web Key Algorithm parameter \"alg\" (\"", alg, + "\") does not match expected \"ES256K\"."); + } + + if (keyUsages.size() > 0) { + KJ_IF_SOME(use, jwk.use) { + JSG_REQUIRE(use == "sig", DOMDataError, + "Asymmetric \"jwk\" key import with usages requires a JSON Web Key with " + "Public Key Use parameter \"use\" (\"", + use, "\") equal to \"sig\"."); + } + } + + KJ_IF_SOME(ops, jwk.key_ops) { + std::sort(ops.begin(), ops.end()); + JSG_REQUIRE(std::adjacent_find(ops.begin(), ops.end()) == ops.end(), DOMDataError, + "A JSON Web Key's Key Operations parameter (\"key_ops\") must not contain duplicates."); + + KJ_IF_SOME(use, jwk.use) { + JSG_REQUIRE(use == "sig", DOMDataError, + "Asymmetric \"jwk\" import requires a JSON Web Key with Public Key Use \"use\" (\"", use, + "\") equal to \"sig\"."); + } + + for (const auto& op: ops) { + JSG_REQUIRE(op == "sign" || op == "verify", DOMDataError, + "JSON Web Key Operations parameter (\"key_ops\") for ECDSA may only contain \"sign\" " + "and/or \"verify\" (got \"", + op, "\")."); + } + + for (const auto& requested: keyUsages) { + JSG_REQUIRE(std::find(ops.begin(), ops.end(), requested) != ops.end(), DOMDataError, + "All specified key usages must be present in the JSON Web Key's Key Operations " + "parameter (\"key_ops\")."); + } + } + + auto xBytes = decodeJwkCoordinate(JSG_REQUIRE_NONNULL(kj::mv(jwk.x), DOMDataError, + "Invalid EC key in JSON Web Key; missing \"x\"."), + COORDINATE_LEN, "x"); + auto yBytes = decodeJwkCoordinate(JSG_REQUIRE_NONNULL(kj::mv(jwk.y), DOMDataError, + "Invalid EC key in JSON Web Key; missing \"y\"."), + COORDINATE_LEN, "y"); + + auto pub = kj::heapArray(PUBKEY_UNCOMPRESSED_LEN); + pub[0] = 0x04; + memcpy(pub.begin() + 1, xBytes.begin(), COORDINATE_LEN); + memcpy(pub.begin() + 1 + COORDINATE_LEN, yBytes.begin(), COORDINATE_LEN); + + // Validate the public point is on the curve before storing. + JSG_REQUIRE(::workerd::rust::aws_lc::validate_public( + ::rust::Slice(pub.begin(), pub.size())), + DOMDataError, "Invalid secp256k1 public key in JSON Web Key (coordinates are not on curve)."); + + if (jwk.d == kj::none) { + return kj::heap( + KeyType::PUBLIC, kj::mv(pub), kj::none, kj::mv(keyAlgorithm), extractable, usages); + } + + // d is decoded into already-wrapped storage. The base64url intermediate is the only + // plaintext copy and gets scrubbed on the way out, including on error paths. + auto scrubbed = wrapSecret(kj::heapArray(SECKEY_LEN)); + memset(scrubbed->asPtr().begin(), 0, SECKEY_LEN); + + auto dRaw = JSG_REQUIRE_NONNULL(decodeBase64Url(kj::mv(KJ_ASSERT_NONNULL(jwk.d))), DOMDataError, + "Invalid EC key in JSON Web Key; missing or invalid private key component (\"d\")."); + KJ_DEFER(OPENSSL_cleanse(dRaw.begin(), dRaw.size())); + JSG_REQUIRE( + dRaw.size() <= SECKEY_LEN, DOMDataError, "Invalid length ", dRaw.size(), " for private key"); + scrubbed->asPtr().slice(SECKEY_LEN - dRaw.size(), SECKEY_LEN).copyFrom(dRaw); + + // aws-lc-rs's from_private_key_and_public_key validates that d derives the claimed (x, y) + // and that d is in range. Either failure surfaces here. + JSG_REQUIRE(::workerd::rust::aws_lc::validate_keypair( + ::rust::Slice(scrubbed->begin(), scrubbed->size()), + ::rust::Slice(pub.begin(), pub.size())), + DOMDataError, + "Invalid EC key; private key component \"d\" is out of range or does not match the " + "claimed public point."); + + return kj::heap( + KeyType::PRIVATE, kj::mv(pub), kj::mv(scrubbed), kj::mv(keyAlgorithm), extractable, usages); +} + +} // namespace + +Secp256k1Key::Secp256k1Key(KeyType keyType, + kj::Array publicKey, + kj::Maybe> privateKey, + CryptoKey::EllipticKeyAlgorithm keyAlgorithm, + bool extractable, + CryptoKeyUsageSet usages) + : CryptoKey::Impl(extractable, usages), + keyType(keyType), + publicKey(kj::mv(publicKey)), + privateKey(kj::mv(privateKey)), + keyAlgorithm(kj::mv(keyAlgorithm)) {} + +kj::StringPtr Secp256k1Key::getAlgorithmName() const { + return keyAlgorithm.name; +} + +CryptoKey::AlgorithmVariant Secp256k1Key::getAlgorithm(jsg::Lock& js) const { + return keyAlgorithm; +} + +kj::StringPtr Secp256k1Key::getType() const { + return toStringPtr(keyType); +} + +kj::StringPtr Secp256k1Key::jsgGetMemoryName() const { + return "Secp256k1Key"; +} + +size_t Secp256k1Key::jsgGetMemorySelfSize() const { + return sizeof(Secp256k1Key); +} + +kj::Own Secp256k1Key::import(jsg::Lock& js, + kj::StringPtr normalizedName, + kj::StringPtr format, + SubtleCrypto::ImportKeyData keyData, + CryptoKey::EllipticKeyAlgorithm keyAlgorithm, + bool extractable, + kj::ArrayPtr keyUsages) { + if (format == "raw"_kj) { + JSG_REQUIRE(keyData.is>(), DOMDataError, + "Expected raw secp256k1 key but instead got a JSON Web Key."); + auto usages = CryptoKeyUsageSet::validate(normalizedName, + CryptoKeyUsageSet::Context::importPublic, keyUsages, CryptoKeyUsageSet::verify()); + return importRaw( + keyData.get>().asPtr(), kj::mv(keyAlgorithm), extractable, usages); + } + + if (format == "jwk"_kj) { + JSG_REQUIRE(keyData.is(), DOMDataError, + "Expected JSON Web Key but instead got raw bytes."); + auto& jwk = keyData.get(); + bool isPrivate = jwk.d != kj::none; + auto usages = CryptoKeyUsageSet::validate(normalizedName, + isPrivate ? CryptoKeyUsageSet::Context::importPrivate + : CryptoKeyUsageSet::Context::importPublic, + keyUsages, isPrivate ? CryptoKeyUsageSet::sign() : CryptoKeyUsageSet::verify()); + return importJwk(kj::mv(jwk), kj::mv(keyAlgorithm), extractable, usages, keyUsages); + } + + JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unrecognized or unimplemented key import format \"", + format, "\" for secp256k1."); +} + +CryptoKeyPair Secp256k1Key::generatePair(jsg::Lock& js, + CryptoKey::EllipticKeyAlgorithm keyAlgorithm, + bool extractable, + CryptoKeyUsageSet privateKeyUsages, + CryptoKeyUsageSet publicKeyUsages) { + auto generated = ::workerd::rust::aws_lc::generate_keypair(); + JSG_REQUIRE(generated.size() == SECKEY_LEN + PUBKEY_UNCOMPRESSED_LEN, InternalDOMOperationError, + "Failed to generate secp256k1 keypair."); + + auto secret = kj::heapArray(SECKEY_LEN); + memcpy(secret.begin(), generated.data(), SECKEY_LEN); + auto wrappedSecret = wrapSecret(kj::mv(secret)); + OPENSSL_cleanse(generated.data(), SECKEY_LEN); + + const auto* pubSrc = generated.data() + SECKEY_LEN; + auto privatePub = kj::heapArray(PUBKEY_UNCOMPRESSED_LEN); + memcpy(privatePub.begin(), pubSrc, PUBKEY_UNCOMPRESSED_LEN); + auto publicPub = kj::heapArray(PUBKEY_UNCOMPRESSED_LEN); + memcpy(publicPub.begin(), pubSrc, PUBKEY_UNCOMPRESSED_LEN); + + auto privateKeyRef = js.alloc(kj::heap(KeyType::PRIVATE, + kj::mv(privatePub), kj::mv(wrappedSecret), keyAlgorithm, extractable, privateKeyUsages)); + auto publicKeyRef = js.alloc(kj::heap( + KeyType::PUBLIC, kj::mv(publicPub), kj::none, kj::mv(keyAlgorithm), true, publicKeyUsages)); + + return CryptoKeyPair{ + .publicKey = kj::mv(publicKeyRef), + .privateKey = kj::mv(privateKeyRef), + }; +} + +jsg::JsArrayBuffer Secp256k1Key::sign(jsg::Lock& js, + SubtleCrypto::SignAlgorithm&& algorithm, + kj::ArrayPtr data) const { + JSG_REQUIRE(keyType == KeyType::PRIVATE, DOMInvalidAccessError, + "Asymmetric signing requires a private key."); + auto& seckey = KJ_ASSERT_NONNULL(privateKey); + + validateHash(algorithm.hash); + + auto signature = + ::workerd::rust::aws_lc::sign(::rust::Slice(seckey->begin(), seckey->size()), + ::rust::Slice(publicKey.begin(), publicKey.size()), + ::rust::Slice(data.begin(), data.size())); + JSG_REQUIRE( + signature.size() == SIGNATURE_LEN, DOMOperationError, "secp256k1 ECDSA signing failed."); + + return jsg::JsArrayBuffer::create(js, kj::arrayPtr(signature.data(), signature.size())); +} + +bool Secp256k1Key::verify(jsg::Lock& js, + SubtleCrypto::SignAlgorithm&& algorithm, + kj::ArrayPtr signature, + kj::ArrayPtr data) const { + JSG_REQUIRE(keyType == KeyType::PUBLIC, DOMInvalidAccessError, + "Asymmetric verification requires a public key."); + + // A malformed signature is invalid, not an error. + if (signature.size() != SIGNATURE_LEN) return false; + + validateHash(algorithm.hash); + + return ::workerd::rust::aws_lc::verify( + ::rust::Slice(publicKey.begin(), publicKey.size()), + ::rust::Slice(data.begin(), data.size()), + ::rust::Slice(signature.begin(), signature.size())); +} + +SubtleCrypto::ExportKeyData Secp256k1Key::exportKey(jsg::Lock& js, kj::StringPtr format) const { + if (format == "raw"_kj) { + JSG_REQUIRE(keyType == KeyType::PUBLIC, DOMInvalidAccessError, + "Raw export of elliptic curve keys is only allowed for public keys."); + return jsg::JsArrayBuffer::create(js, publicKey.asPtr()).addRef(js); + } + + if (format == "jwk"_kj) { + KJ_ASSERT(publicKey.size() == PUBKEY_UNCOMPRESSED_LEN); + KJ_ASSERT(publicKey[0] == 0x04); + auto x = publicKey.slice(1, 1 + COORDINATE_LEN); + auto y = publicKey.slice(1 + COORDINATE_LEN, PUBKEY_UNCOMPRESSED_LEN); + + SubtleCrypto::JsonWebKey jwk; + jwk.kty = kj::str("EC"); + jwk.crv = kj::str("secp256k1"); + jwk.x = fastEncodeBase64Url(x); + jwk.y = fastEncodeBase64Url(y); + KJ_IF_SOME(seckey, privateKey) { + jwk.d = fastEncodeBase64Url(seckey->asPtr()); + } + jwk.ext = isExtractable(); + jwk.key_ops = getUsages().map([](auto usage) { return kj::str(usage.name()); }); + return jwk; + } + + JSG_FAIL_REQUIRE(DOMNotSupportedError, "Unrecognized or unimplemented key export format \"", + format, "\" for secp256k1."); +} + +bool Secp256k1Key::equals(const CryptoKey::Impl& other) const { + if (this == &other) return true; + + KJ_IF_SOME(that, kj::dynamicDowncastIfAvailable(other)) { + if (keyType != that.keyType) return false; + if (publicKey.size() != that.publicKey.size()) return false; + if (CRYPTO_memcmp(publicKey.begin(), that.publicKey.begin(), publicKey.size()) != 0) { + return false; + } + // Equal public halves already imply equal private scalars for valid keys, but compare the + // scalars directly and in constant time as defense in depth. + if (keyType == KeyType::PRIVATE) { + auto& a = KJ_ASSERT_NONNULL(privateKey); + auto& b = KJ_ASSERT_NONNULL(that.privateKey); + if (a->size() != b->size()) return false; + if (CRYPTO_memcmp(a->begin(), b->begin(), a->size()) != 0) return false; + } + return true; + } + return false; +} + +} // namespace workerd::api diff --git a/src/workerd/api/crypto/secp256k1-key.h b/src/workerd/api/crypto/secp256k1-key.h new file mode 100644 index 00000000000..78d7918108a --- /dev/null +++ b/src/workerd/api/crypto/secp256k1-key.h @@ -0,0 +1,65 @@ +#pragma once + +#include "crypto.h" +#include "impl.h" +#include "keys.h" + +namespace workerd::api { + +// BoringSSL doesn't include secp256k1, so curve operations are routed through aws-lc-rs. +// We inherit from CryptoKey::Impl directly because AsymmetricKeyCryptoKeyImpl is built +// around a BoringSSL EVP_PKEY. Each instance holds one key, public or private. +class Secp256k1Key final: public CryptoKey::Impl { + public: + Secp256k1Key(KeyType keyType, + kj::Array publicKey, + kj::Maybe> privateKey, + CryptoKey::EllipticKeyAlgorithm keyAlgorithm, + bool extractable, + CryptoKeyUsageSet usages); + + // Format is "raw" or "jwk". For "jwk", a `d` field selects the private-key path. + static kj::Own import(jsg::Lock& js, + kj::StringPtr normalizedName, + kj::StringPtr format, + SubtleCrypto::ImportKeyData keyData, + CryptoKey::EllipticKeyAlgorithm keyAlgorithm, + bool extractable, + kj::ArrayPtr keyUsages); + + static CryptoKeyPair generatePair(jsg::Lock& js, + CryptoKey::EllipticKeyAlgorithm keyAlgorithm, + bool extractable, + CryptoKeyUsageSet privateKeyUsages, + CryptoKeyUsageSet publicKeyUsages); + + kj::StringPtr getAlgorithmName() const override; + CryptoKey::AlgorithmVariant getAlgorithm(jsg::Lock& js) const override; + kj::StringPtr getType() const override; + + jsg::JsArrayBuffer sign(jsg::Lock& js, + SubtleCrypto::SignAlgorithm&& algorithm, + kj::ArrayPtr data) const override; + + bool verify(jsg::Lock& js, + SubtleCrypto::SignAlgorithm&& algorithm, + kj::ArrayPtr signature, + kj::ArrayPtr data) const override; + + SubtleCrypto::ExportKeyData exportKey(jsg::Lock& js, kj::StringPtr format) const override; + + bool equals(const CryptoKey::Impl& other) const override; + + kj::StringPtr jsgGetMemoryName() const override; + size_t jsgGetMemorySelfSize() const override; + + private: + KeyType keyType; + // 65-byte uncompressed SEC1 public key (04 || x || y). + kj::Array publicKey; + // ZeroOnFree's destructor suppresses its implicit moves, so it can't go in kj::Maybe directly. + kj::Maybe> privateKey; + CryptoKey::EllipticKeyAlgorithm keyAlgorithm; +}; + +} // namespace workerd::api diff --git a/src/workerd/api/tests/BUILD.bazel b/src/workerd/api/tests/BUILD.bazel index aec24a5ac49..d48a2e241f7 100644 --- a/src/workerd/api/tests/BUILD.bazel +++ b/src/workerd/api/tests/BUILD.bazel @@ -309,6 +309,15 @@ wd_test( data = ["crypto-impl-asymmetric-test.js"], ) +wd_test( + src = "crypto-secp256k1-test.wd-test", + args = ["--experimental"], + data = [ + "crypto-secp256k1-no-flag-test.js", + "crypto-secp256k1-with-flag-test.js", + ], +) + wd_test( src = "crypto-streams-test.wd-test", args = ["--experimental"], diff --git a/src/workerd/api/tests/crypto-secp256k1-no-flag-test.js b/src/workerd/api/tests/crypto-secp256k1-no-flag-test.js new file mode 100644 index 00000000000..fb23a04eba3 --- /dev/null +++ b/src/workerd/api/tests/crypto-secp256k1-no-flag-test.js @@ -0,0 +1,50 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +import { rejects } from 'node:assert'; + +const ECDSA = { name: 'ECDSA', namedCurve: 'secp256k1' }; +const expectedError = { + name: 'NotSupportedError', + message: /Unrecognized or unimplemented EC curve/, +}; + +export const generateKeyRejected = { + async test() { + await rejects( + crypto.subtle.generateKey(ECDSA, true, ['sign', 'verify']), + expectedError + ); + }, +}; + +export const importKeyRawRejected = { + async test() { + await rejects( + crypto.subtle.importKey('raw', new Uint8Array(33), ECDSA, true, [ + 'verify', + ]), + expectedError + ); + }, +}; + +export const importKeyJwkRejected = { + async test() { + await rejects( + crypto.subtle.importKey( + 'jwk', + { + kty: 'EC', + crv: 'secp256k1', + x: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + y: 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + }, + ECDSA, + true, + ['verify'] + ), + expectedError + ); + }, +}; diff --git a/src/workerd/api/tests/crypto-secp256k1-test.wd-test b/src/workerd/api/tests/crypto-secp256k1-test.wd-test new file mode 100644 index 00000000000..68a4b5f4817 --- /dev/null +++ b/src/workerd/api/tests/crypto-secp256k1-test.wd-test @@ -0,0 +1,29 @@ +using Workerd = import "/workerd/workerd.capnp"; + +# Two services so each side can run against a different setting of the secp256k1_ecdsa_curve +# compat flag. The flag is per-worker. + +const unitTests :Workerd.Config = ( + services = [ + ( name = "crypto-secp256k1-no-flag", + worker = ( + modules = [ + (name = "worker", esModule = embed "crypto-secp256k1-no-flag-test.js") + ], + compatibilityFlags = ["nodejs_compat"], + ) + ), + ( name = "crypto-secp256k1-with-flag", + worker = ( + modules = [ + (name = "worker", esModule = embed "crypto-secp256k1-with-flag-test.js") + ], + compatibilityFlags = [ + "nodejs_compat", + "experimental", + "secp256k1_ecdsa_curve", + ], + ) + ), + ], +); diff --git a/src/workerd/api/tests/crypto-secp256k1-with-flag-test.js b/src/workerd/api/tests/crypto-secp256k1-with-flag-test.js new file mode 100644 index 00000000000..2d76a7a9c0b --- /dev/null +++ b/src/workerd/api/tests/crypto-secp256k1-with-flag-test.js @@ -0,0 +1,411 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 +import { deepStrictEqual, ok, rejects, strictEqual } from 'node:assert'; + +// secp256k1 generator G (the public key for private key d = 1), defined in SEC 2 §2.4.1. +const GENERATOR_PUBKEY_COMPRESSED = new Uint8Array([ + 0x02, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, + 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, + 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98, +]); +const GENERATOR_PUBKEY_UNCOMPRESSED = new Uint8Array([ + 0x04, 0x79, 0xbe, 0x66, 0x7e, 0xf9, 0xdc, 0xbb, 0xac, 0x55, 0xa0, 0x62, 0x95, + 0xce, 0x87, 0x0b, 0x07, 0x02, 0x9b, 0xfc, 0xdb, 0x2d, 0xce, 0x28, 0xd9, 0x59, + 0xf2, 0x81, 0x5b, 0x16, 0xf8, 0x17, 0x98, 0x48, 0x3a, 0xda, 0x77, 0x26, 0xa3, + 0xc4, 0x65, 0x5d, 0xa4, 0xfb, 0xfc, 0x0e, 0x11, 0x08, 0xa8, 0xfd, 0x17, 0xb4, + 0x48, 0xa6, 0x85, 0x54, 0x19, 0x9c, 0x47, 0xd0, 0x8f, 0xfb, 0x10, 0xd4, 0xb8, +]); + +const ECDSA = { name: 'ECDSA', namedCurve: 'secp256k1' }; +const ECDSA_SHA256 = { name: 'ECDSA', hash: 'SHA-256' }; + +async function importGeneratorPublic() { + return crypto.subtle.importKey( + 'raw', + GENERATOR_PUBKEY_UNCOMPRESSED, + ECDSA, + true, + ['verify'] + ); +} + +async function generatePair() { + return crypto.subtle.generateKey(ECDSA, true, ['sign', 'verify']); +} + +export const importRawUncompressedPublicKey = { + async test() { + const key = await importGeneratorPublic(); + strictEqual(key.type, 'public'); + strictEqual(key.algorithm.name, 'ECDSA'); + strictEqual(key.algorithm.namedCurve, 'secp256k1'); + ok(key.usages.includes('verify')); + ok(!key.usages.includes('sign')); + }, +}; + +export const importUncompressedRoundTrip = { + // Raw export and import both use the 65-byte uncompressed SEC1 form (04 || x || y). + async test() { + const key = await importGeneratorPublic(); + const exported = new Uint8Array(await crypto.subtle.exportKey('raw', key)); + deepStrictEqual(exported, GENERATOR_PUBKEY_UNCOMPRESSED); + }, +}; + +export const importRejectsCompressed = { + // Only uncompressed (65-byte) raw input is accepted. Compressed (33-byte) is rejected. + async test() { + await rejects( + crypto.subtle.importKey('raw', GENERATOR_PUBKEY_COMPRESSED, ECDSA, true, [ + 'verify', + ]), + { name: 'DataError' } + ); + }, +}; + +export const importRejectsWrongLength = { + async test() { + await rejects( + crypto.subtle.importKey('raw', new Uint8Array(32), ECDSA, true, [ + 'verify', + ]), + { name: 'DataError' } + ); + }, +}; + +export const importRejectsInvalidPoint = { + async test() { + // 0x04 prefix claims uncompressed, but the (x, y) are not on the curve. + const bogus = new Uint8Array(65); + bogus[0] = 0x04; + bogus.fill(0xff, 1); + await rejects( + crypto.subtle.importKey('raw', bogus, ECDSA, true, ['verify']), + { name: 'DataError' } + ); + }, +}; + +export const generateKeyProducesPair = { + async test() { + const pair = await generatePair(); + strictEqual(pair.publicKey.type, 'public'); + strictEqual(pair.privateKey.type, 'private'); + strictEqual(pair.publicKey.algorithm.namedCurve, 'secp256k1'); + strictEqual(pair.privateKey.algorithm.namedCurve, 'secp256k1'); + ok(pair.privateKey.usages.includes('sign')); + ok(pair.publicKey.usages.includes('verify')); + ok(!pair.privateKey.usages.includes('verify')); + ok(!pair.publicKey.usages.includes('sign')); + }, +}; + +export const signVerifyRoundTrip = { + async test() { + const { privateKey, publicKey } = await generatePair(); + const message = new TextEncoder().encode('hello secp256k1'); + const signature = await crypto.subtle.sign( + ECDSA_SHA256, + privateKey, + message + ); + strictEqual(signature.byteLength, 64); + strictEqual( + await crypto.subtle.verify(ECDSA_SHA256, publicKey, signature, message), + true + ); + + const tampered = new Uint8Array(message); + tampered[0] ^= 0x01; + strictEqual( + await crypto.subtle.verify(ECDSA_SHA256, publicKey, signature, tampered), + false + ); + }, +}; + +export const signProducesIndependentSignatures = { + // aws-lc-rs uses random nonces (not RFC 6979 deterministic), so two signatures of the same + // message under the same key are different but both valid. + async test() { + const { privateKey, publicKey } = await generatePair(); + const message = new TextEncoder().encode('two signatures'); + const sigA = new Uint8Array( + await crypto.subtle.sign(ECDSA_SHA256, privateKey, message) + ); + const sigB = new Uint8Array( + await crypto.subtle.sign(ECDSA_SHA256, privateKey, message) + ); + // Different signatures, both valid. + ok(!sigA.every((b, i) => b === sigB[i]), 'expected different nonces'); + strictEqual( + await crypto.subtle.verify(ECDSA_SHA256, publicKey, sigA, message), + true + ); + strictEqual( + await crypto.subtle.verify(ECDSA_SHA256, publicKey, sigB, message), + true + ); + }, +}; + +export const signRejectsPublicKey = { + async test() { + const { publicKey } = await generatePair(); + await rejects( + crypto.subtle.sign(ECDSA_SHA256, publicKey, new Uint8Array([0])), + { name: 'InvalidAccessError' } + ); + }, +}; + +export const signRejectsUnsupportedHash = { + // Only SHA-256 is supported (per RFC 8812 ES256K). Other hashes are rejected. + async test() { + const { privateKey } = await generatePair(); + await rejects( + crypto.subtle.sign( + { name: 'ECDSA', hash: 'SHA-384' }, + privateKey, + new Uint8Array([0]) + ), + { name: 'NotSupportedError' } + ); + await rejects( + crypto.subtle.sign( + { name: 'ECDSA', hash: 'SHA-512' }, + privateKey, + new Uint8Array([0]) + ), + { name: 'NotSupportedError' } + ); + }, +}; + +export const verifyRejectsWrongLengthSignature = { + async test() { + const key = await importGeneratorPublic(); + strictEqual( + await crypto.subtle.verify( + ECDSA_SHA256, + key, + new Uint8Array(63), + new Uint8Array([0]) + ), + false + ); + }, +}; + +export const verifyRejectsAllZeroSignature = { + async test() { + const key = await importGeneratorPublic(); + strictEqual( + await crypto.subtle.verify( + ECDSA_SHA256, + key, + new Uint8Array(64), + new Uint8Array([0]) + ), + false + ); + }, +}; + +// JWK assertions derive known-good JWKs by round-tripping through raw/generate paths and mutate +// from there, rather than hardcoding base64url coordinate strings. + +async function makeGeneratorJwk() { + const key = await importGeneratorPublic(); + return crypto.subtle.exportKey('jwk', key); +} + +export const exportJwkShape = { + async test() { + const jwk = await makeGeneratorJwk(); + strictEqual(jwk.kty, 'EC'); + strictEqual(jwk.crv, 'secp256k1'); + strictEqual(typeof jwk.x, 'string'); + strictEqual(typeof jwk.y, 'string'); + strictEqual(jwk.d, undefined); + strictEqual(jwk.ext, true); + ok(jwk.key_ops.includes('verify')); + // base64url of 32 bytes is 43 chars, alphabet [A-Za-z0-9_-]. + strictEqual(jwk.x.length, 43); + strictEqual(jwk.y.length, 43); + ok(/^[A-Za-z0-9_-]+$/.test(jwk.x)); + ok(/^[A-Za-z0-9_-]+$/.test(jwk.y)); + }, +}; + +export const importJwkPublic = { + async test() { + const jwk = await makeGeneratorJwk(); + const key = await crypto.subtle.importKey('jwk', jwk, ECDSA, true, [ + 'verify', + ]); + strictEqual(key.type, 'public'); + strictEqual(key.algorithm.namedCurve, 'secp256k1'); + ok(key.usages.includes('verify')); + }, +}; + +export const jwkRoundTrip = { + async test() { + const fromRaw = await importGeneratorPublic(); + const jwk = await crypto.subtle.exportKey('jwk', fromRaw); + const fromJwk = await crypto.subtle.importKey('jwk', jwk, ECDSA, true, [ + 'verify', + ]); + const reExported = new Uint8Array( + await crypto.subtle.exportKey('raw', fromJwk) + ); + deepStrictEqual(reExported, GENERATOR_PUBKEY_UNCOMPRESSED); + }, +}; + +export const importJwkRejectsWrongKty = { + async test() { + const jwk = await makeGeneratorJwk(); + jwk.kty = 'OKP'; + await rejects( + crypto.subtle.importKey('jwk', jwk, ECDSA, true, ['verify']), + { name: 'DataError' } + ); + }, +}; + +export const importJwkRejectsWrongCrv = { + async test() { + const jwk = await makeGeneratorJwk(); + jwk.crv = 'P-256'; + await rejects( + crypto.subtle.importKey('jwk', jwk, ECDSA, true, ['verify']), + { name: 'DataError' } + ); + }, +}; + +export const importJwkRejectsWrongAlg = { + // RFC 8812: if `alg` is present on a secp256k1 JWK it must be "ES256K". + async test() { + const jwk = await makeGeneratorJwk(); + jwk.alg = 'ES256'; + await rejects( + crypto.subtle.importKey('jwk', jwk, ECDSA, true, ['verify']), + { name: 'DataError' } + ); + }, +}; + +export const importJwkRejectsExtractableFromUnextractable = { + // WebCrypto spec: if a JWK has ext: false, it can't be imported as extractable. + async test() { + const jwk = await makeGeneratorJwk(); + jwk.ext = false; + await rejects( + crypto.subtle.importKey('jwk', jwk, ECDSA, true, ['verify']), + { name: 'DataError' } + ); + }, +}; + +export const importJwkRejectsDuplicateKeyOps = { + // RFC 7517 §4.3: key_ops must not contain duplicates. + async test() { + const jwk = await makeGeneratorJwk(); + jwk.key_ops = ['verify', 'verify']; + await rejects( + crypto.subtle.importKey('jwk', jwk, ECDSA, true, ['verify']), + { name: 'DataError' } + ); + }, +}; + +export const importJwkRejectsKeyOpsMissingRequestedUsage = { + // WebCrypto spec: every requested key usage must appear in the JWK's key_ops. + async test() { + const jwk = await makeGeneratorJwk(); + jwk.key_ops = ['sign']; // doesn't include "verify" + await rejects( + crypto.subtle.importKey('jwk', jwk, ECDSA, true, ['verify']), + { name: 'DataError' } + ); + }, +}; + +export const jwkPrivateRoundTrip = { + async test() { + const { privateKey, publicKey } = await generatePair(); + const jwk = await crypto.subtle.exportKey('jwk', privateKey); + strictEqual(jwk.kty, 'EC'); + strictEqual(jwk.crv, 'secp256k1'); + strictEqual(typeof jwk.d, 'string'); + strictEqual(jwk.d.length, 43); + ok(jwk.key_ops.includes('sign')); + + const reimported = await crypto.subtle.importKey('jwk', jwk, ECDSA, true, [ + 'sign', + ]); + strictEqual(reimported.type, 'private'); + + const message = new TextEncoder().encode('round-trip via JWK'); + const sig = await crypto.subtle.sign(ECDSA_SHA256, reimported, message); + strictEqual( + await crypto.subtle.verify(ECDSA_SHA256, publicKey, sig, message), + true + ); + }, +}; + +export const jwkPrivateRejectsMismatchedHalves = { + // A JWK whose (x, y) doesn't match the point derived from `d` must be rejected, not silently + // produce a broken key. + async test() { + const a = await generatePair(); + const b = await generatePair(); + const jwkA = await crypto.subtle.exportKey('jwk', a.privateKey); + const jwkB = await crypto.subtle.exportKey('jwk', b.privateKey); + const mixed = { ...jwkA, d: jwkB.d }; + await rejects( + crypto.subtle.importKey('jwk', mixed, ECDSA, true, ['sign']), + { name: 'DataError' } + ); + }, +}; + +export const ecdhRejectedExplicitly = { + async test() { + await rejects( + crypto.subtle.importKey( + 'raw', + GENERATOR_PUBKEY_COMPRESSED, + { name: 'ECDH', namedCurve: 'secp256k1' }, + true, + ['deriveBits'] + ), + { + name: 'NotSupportedError', + message: /ECDH is not supported for curve "secp256k1"/, + } + ); + }, +}; + +export const ecdhGenerateRejectedExplicitly = { + // secp256k1 over ECDH must reject at generateKey too, not just at importKey. + async test() { + await rejects( + crypto.subtle.generateKey( + { name: 'ECDH', namedCurve: 'secp256k1' }, + true, + ['deriveBits'] + ), + { name: 'NotSupportedError' } + ); + }, +}; diff --git a/src/workerd/io/BUILD.bazel b/src/workerd/io/BUILD.bazel index 43ca28d5262..1e8bb824611 100644 --- a/src/workerd/io/BUILD.bazel +++ b/src/workerd/io/BUILD.bazel @@ -94,6 +94,7 @@ wd_cc_library( ":wasm-instantiate-shim", ":worker-interface", ":worker-source", + "//src/rust/aws-lc", "//src/rust/cxx-integration", "//src/workerd/api:analytics-engine_capnp", "//src/workerd/api:deferred-proxy", diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp index cb799f98688..b3d62616e10 100644 --- a/src/workerd/io/compatibility-date.capnp +++ b/src/workerd/io/compatibility-date.capnp @@ -1539,4 +1539,10 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { # startup. This allows packages to extend `sys.path` declaratively (e.g. to # add subdirectories or register import hooks). Without this flag, `.pth` # files in `python_modules/` are ignored. + + secp256k1EcdsaCurve @177 :Bool + $compatEnableFlag("secp256k1_ecdsa_curve") + $experimental; + # When enabled, the WebCrypto ECDSA algorithm accepts "secp256k1" as a + # namedCurve, with SHA-256 as the only supported hash. } From 06c03033796ebdf3e7a0f205532b38aefcfeff70 Mon Sep 17 00:00:00 2001 From: Isaac Bremseth Date: Wed, 29 Apr 2026 09:43:22 -0400 Subject: [PATCH 2/4] Use KJ helpers for memory ops and Rust conversions --- src/workerd/api/crypto/ec.c++ | 13 ++++--- src/workerd/api/crypto/secp256k1-key.c++ | 44 ++++++++++-------------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/src/workerd/api/crypto/ec.c++ b/src/workerd/api/crypto/ec.c++ index 6afc8ab9d6e..18510a91a03 100644 --- a/src/workerd/api/crypto/ec.c++ +++ b/src/workerd/api/crypto/ec.c++ @@ -434,6 +434,10 @@ struct EllipticCurveInfo { uint rsSize; // size of "r" and "s" in the signature }; +bool isSecp256k1(kj::StringPtr name) { + return strcasecmp(name.cStr(), "secp256k1") == 0; +} + EllipticCurveInfo lookupEllipticCurve(kj::StringPtr curveName) { static const std::map registeredCurves{ {"P-256", {"P-256", NID_X9_62_prime256v1, 32}}, @@ -449,8 +453,7 @@ EllipticCurveInfo lookupEllipticCurve(kj::StringPtr curveName) { // Overload that adds secp256k1 to the table when the compat flag is set. EllipticCurveInfo lookupEllipticCurve(jsg::Lock& js, kj::StringPtr curveName) { - if (FeatureFlags::get(js).getSecp256k1EcdsaCurve() && - strcasecmp(curveName.cStr(), "secp256k1") == 0) { + if (FeatureFlags::get(js).getSecp256k1EcdsaCurve() && isSecp256k1(curveName)) { return {"secp256k1", 0, 32}; } return lookupEllipticCurve(curveName); @@ -468,7 +471,7 @@ kj::OneOf, CryptoKeyPair> EllipticKey::generateElliptic(jsg: auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(js, namedCurve); // The compat flag scopes secp256k1 support to ECDSA; ECDH is rejected. - if (strcasecmp(normalizedNamedCurve.cStr(), "secp256k1") == 0) { + if (isSecp256k1(normalizedNamedCurve)) { JSG_REQUIRE(normalizedName == "ECDSA", DOMNotSupportedError, "\"", normalizedName, "\" is not supported for curve \"secp256k1\"."); return Secp256k1Key::generatePair(js, @@ -721,7 +724,7 @@ kj::Own CryptoKey::Impl::importEcdsa(jsg::Lock& js, auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(js, namedCurve); - if (strcasecmp(normalizedNamedCurve.cStr(), "secp256k1") == 0) { + if (isSecp256k1(normalizedNamedCurve)) { return Secp256k1Key::import(js, normalizedName, format, kj::mv(keyData), CryptoKey::EllipticKeyAlgorithm{normalizedName, normalizedNamedCurve}, extractable, keyUsages); @@ -785,7 +788,7 @@ kj::Own CryptoKey::Impl::importEcdh(jsg::Lock& js, auto [normalizedNamedCurve, curveId, rsSize] = lookupEllipticCurve(js, namedCurve); - JSG_REQUIRE(strcasecmp(normalizedNamedCurve.cStr(), "secp256k1") != 0, DOMNotSupportedError, + JSG_REQUIRE(!isSecp256k1(normalizedNamedCurve), DOMNotSupportedError, "ECDH is not supported for curve \"secp256k1\"."); auto importedKey = [&, curveId = curveId] { diff --git a/src/workerd/api/crypto/secp256k1-key.c++ b/src/workerd/api/crypto/secp256k1-key.c++ index b6d299d3088..014a8d6ca19 100644 --- a/src/workerd/api/crypto/secp256k1-key.c++ +++ b/src/workerd/api/crypto/secp256k1-key.c++ @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -38,7 +39,7 @@ kj::Array decodeJwkCoordinate( JSG_REQUIRE(bytes.size() <= expectedLen, DOMDataError, "JSON Web Key \"", fieldName, "\" must be at most ", expectedLen, " bytes."); auto out = kj::heapArray(expectedLen); - memset(out.begin(), 0, expectedLen); + out.asPtr().fill(0); out.slice(expectedLen - bytes.size(), expectedLen).copyFrom(bytes); return out; } @@ -62,12 +63,11 @@ kj::Own importRaw(kj::ArrayPtr keyData, "Invalid secp256k1 public key length (expected ", PUBKEY_UNCOMPRESSED_LEN, " bytes uncompressed, got ", keyData.size(), ")."); - JSG_REQUIRE(::workerd::rust::aws_lc::validate_public( - ::rust::Slice(keyData.begin(), keyData.size())), - DOMDataError, "Invalid secp256k1 public key (failed to parse)."); + JSG_REQUIRE(::workerd::rust::aws_lc::validate_public(keyData.as()), DOMDataError, + "Invalid secp256k1 public key (failed to parse)."); auto pub = kj::heapArray(PUBKEY_UNCOMPRESSED_LEN); - memcpy(pub.begin(), keyData.begin(), PUBKEY_UNCOMPRESSED_LEN); + pub.asPtr().copyFrom(keyData); return kj::heap( KeyType::PUBLIC, kj::mv(pub), kj::none, kj::mv(keyAlgorithm), extractable, usages); } @@ -121,7 +121,7 @@ kj::Own importJwk(SubtleCrypto::JsonWebKey&& jwk, } for (const auto& requested: keyUsages) { - JSG_REQUIRE(std::find(ops.begin(), ops.end(), requested) != ops.end(), DOMDataError, + JSG_REQUIRE(ops.asPtr().findFirst(requested) != kj::none, DOMDataError, "All specified key usages must be present in the JSON Web Key's Key Operations " "parameter (\"key_ops\")."); } @@ -136,13 +136,12 @@ kj::Own importJwk(SubtleCrypto::JsonWebKey&& jwk, auto pub = kj::heapArray(PUBKEY_UNCOMPRESSED_LEN); pub[0] = 0x04; - memcpy(pub.begin() + 1, xBytes.begin(), COORDINATE_LEN); - memcpy(pub.begin() + 1 + COORDINATE_LEN, yBytes.begin(), COORDINATE_LEN); + pub.slice(1, 1 + COORDINATE_LEN).copyFrom(xBytes); + pub.slice(1 + COORDINATE_LEN, PUBKEY_UNCOMPRESSED_LEN).copyFrom(yBytes); // Validate the public point is on the curve before storing. - JSG_REQUIRE(::workerd::rust::aws_lc::validate_public( - ::rust::Slice(pub.begin(), pub.size())), - DOMDataError, "Invalid secp256k1 public key in JSON Web Key (coordinates are not on curve)."); + JSG_REQUIRE(::workerd::rust::aws_lc::validate_public(pub.as()), DOMDataError, + "Invalid secp256k1 public key in JSON Web Key (coordinates are not on curve)."); if (jwk.d == kj::none) { return kj::heap( @@ -152,7 +151,7 @@ kj::Own importJwk(SubtleCrypto::JsonWebKey&& jwk, // d is decoded into already-wrapped storage. The base64url intermediate is the only // plaintext copy and gets scrubbed on the way out, including on error paths. auto scrubbed = wrapSecret(kj::heapArray(SECKEY_LEN)); - memset(scrubbed->asPtr().begin(), 0, SECKEY_LEN); + scrubbed->asPtr().fill(0); auto dRaw = JSG_REQUIRE_NONNULL(decodeBase64Url(kj::mv(KJ_ASSERT_NONNULL(jwk.d))), DOMDataError, "Invalid EC key in JSON Web Key; missing or invalid private key component (\"d\")."); @@ -164,8 +163,7 @@ kj::Own importJwk(SubtleCrypto::JsonWebKey&& jwk, // aws-lc-rs's from_private_key_and_public_key validates that d derives the claimed (x, y) // and that d is in range. Either failure surfaces here. JSG_REQUIRE(::workerd::rust::aws_lc::validate_keypair( - ::rust::Slice(scrubbed->begin(), scrubbed->size()), - ::rust::Slice(pub.begin(), pub.size())), + scrubbed->asPtr().as(), pub.as()), DOMDataError, "Invalid EC key; private key component \"d\" is out of range or does not match the " "claimed public point."); @@ -250,15 +248,15 @@ CryptoKeyPair Secp256k1Key::generatePair(jsg::Lock& js, "Failed to generate secp256k1 keypair."); auto secret = kj::heapArray(SECKEY_LEN); - memcpy(secret.begin(), generated.data(), SECKEY_LEN); + secret.asPtr().copyFrom(kj::arrayPtr(generated.data(), SECKEY_LEN)); auto wrappedSecret = wrapSecret(kj::mv(secret)); OPENSSL_cleanse(generated.data(), SECKEY_LEN); - const auto* pubSrc = generated.data() + SECKEY_LEN; + auto pubSrc = kj::arrayPtr(generated.data() + SECKEY_LEN, PUBKEY_UNCOMPRESSED_LEN); auto privatePub = kj::heapArray(PUBKEY_UNCOMPRESSED_LEN); - memcpy(privatePub.begin(), pubSrc, PUBKEY_UNCOMPRESSED_LEN); + privatePub.asPtr().copyFrom(pubSrc); auto publicPub = kj::heapArray(PUBKEY_UNCOMPRESSED_LEN); - memcpy(publicPub.begin(), pubSrc, PUBKEY_UNCOMPRESSED_LEN); + publicPub.asPtr().copyFrom(pubSrc); auto privateKeyRef = js.alloc(kj::heap(KeyType::PRIVATE, kj::mv(privatePub), kj::mv(wrappedSecret), keyAlgorithm, extractable, privateKeyUsages)); @@ -280,10 +278,8 @@ jsg::JsArrayBuffer Secp256k1Key::sign(jsg::Lock& js, validateHash(algorithm.hash); - auto signature = - ::workerd::rust::aws_lc::sign(::rust::Slice(seckey->begin(), seckey->size()), - ::rust::Slice(publicKey.begin(), publicKey.size()), - ::rust::Slice(data.begin(), data.size())); + auto signature = ::workerd::rust::aws_lc::sign( + seckey->asPtr().as(), publicKey.as(), data.as()); JSG_REQUIRE( signature.size() == SIGNATURE_LEN, DOMOperationError, "secp256k1 ECDSA signing failed."); @@ -303,9 +299,7 @@ bool Secp256k1Key::verify(jsg::Lock& js, validateHash(algorithm.hash); return ::workerd::rust::aws_lc::verify( - ::rust::Slice(publicKey.begin(), publicKey.size()), - ::rust::Slice(data.begin(), data.size()), - ::rust::Slice(signature.begin(), signature.size())); + publicKey.as(), data.as(), signature.as()); } SubtleCrypto::ExportKeyData Secp256k1Key::exportKey(jsg::Lock& js, kj::StringPtr format) const { From a99cb8b0ddfd19b4ffd83bf0bafca062f57ae778 Mon Sep 17 00:00:00 2001 From: Isaac Bremseth Date: Wed, 29 Apr 2026 10:09:53 -0400 Subject: [PATCH 3/4] Remove ::adjacent_find --- src/workerd/api/crypto/secp256k1-key.c++ | 13 ++++++------- .../api/tests/crypto-secp256k1-with-flag-test.js | 11 +++++++++++ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/workerd/api/crypto/secp256k1-key.c++ b/src/workerd/api/crypto/secp256k1-key.c++ index 014a8d6ca19..3e7ae44e736 100644 --- a/src/workerd/api/crypto/secp256k1-key.c++ +++ b/src/workerd/api/crypto/secp256k1-key.c++ @@ -103,21 +103,20 @@ kj::Own importJwk(SubtleCrypto::JsonWebKey&& jwk, } KJ_IF_SOME(ops, jwk.key_ops) { - std::sort(ops.begin(), ops.end()); - JSG_REQUIRE(std::adjacent_find(ops.begin(), ops.end()) == ops.end(), DOMDataError, - "A JSON Web Key's Key Operations parameter (\"key_ops\") must not contain duplicates."); - KJ_IF_SOME(use, jwk.use) { JSG_REQUIRE(use == "sig", DOMDataError, "Asymmetric \"jwk\" import requires a JSON Web Key with Public Key Use \"use\" (\"", use, "\") equal to \"sig\"."); } - for (const auto& op: ops) { - JSG_REQUIRE(op == "sign" || op == "verify", DOMDataError, + std::sort(ops.begin(), ops.end()); + for (size_t i = 0; i < ops.size(); ++i) { + JSG_REQUIRE(ops[i] == "sign" || ops[i] == "verify", DOMDataError, "JSON Web Key Operations parameter (\"key_ops\") for ECDSA may only contain \"sign\" " "and/or \"verify\" (got \"", - op, "\")."); + ops[i], "\")."); + JSG_REQUIRE(i == 0 || ops[i] != ops[i - 1], DOMDataError, + "A JSON Web Key's Key Operations parameter (\"key_ops\") must not contain duplicates."); } for (const auto& requested: keyUsages) { diff --git a/src/workerd/api/tests/crypto-secp256k1-with-flag-test.js b/src/workerd/api/tests/crypto-secp256k1-with-flag-test.js index 2d76a7a9c0b..fc8b0b6c91b 100644 --- a/src/workerd/api/tests/crypto-secp256k1-with-flag-test.js +++ b/src/workerd/api/tests/crypto-secp256k1-with-flag-test.js @@ -338,6 +338,17 @@ export const importJwkRejectsKeyOpsMissingRequestedUsage = { }, }; +export const importJwkRejectsInvalidKeyOp = { + // ECDSA key_ops may only contain "sign" and/or "verify". + async test() { + const jwk = await makeGeneratorJwk(); + jwk.key_ops = ['encrypt']; + await rejects(crypto.subtle.importKey('jwk', jwk, ECDSA, true, []), { + name: 'DataError', + }); + }, +}; + export const jwkPrivateRoundTrip = { async test() { const { privateKey, publicKey } = await generatePair(); From 60c901d626df38a25990ecab6cce5fe0b7a4cc54 Mon Sep 17 00:00:00 2001 From: Isaac Bremseth Date: Fri, 1 May 2026 13:00:09 -0500 Subject: [PATCH 4/4] Switch from compat flag to autogate --- deps/rust/Cargo.toml | 2 +- src/workerd/api/crypto/ec.c++ | 5 ++-- src/workerd/api/tests/BUILD.bazel | 18 ++++++++---- ...t.js => crypto-secp256k1-disabled-test.js} | 0 .../crypto-secp256k1-disabled-test.wd-test | 17 +++++++++++ ...st.js => crypto-secp256k1-enabled-test.js} | 0 .../crypto-secp256k1-enabled-test.wd-test | 17 +++++++++++ .../api/tests/crypto-secp256k1-test.wd-test | 29 ------------------- src/workerd/io/compatibility-date.capnp | 6 ---- src/workerd/util/autogate.c++ | 2 ++ src/workerd/util/autogate.h | 2 ++ 11 files changed, 55 insertions(+), 43 deletions(-) rename src/workerd/api/tests/{crypto-secp256k1-no-flag-test.js => crypto-secp256k1-disabled-test.js} (100%) create mode 100644 src/workerd/api/tests/crypto-secp256k1-disabled-test.wd-test rename src/workerd/api/tests/{crypto-secp256k1-with-flag-test.js => crypto-secp256k1-enabled-test.js} (100%) create mode 100644 src/workerd/api/tests/crypto-secp256k1-enabled-test.wd-test delete mode 100644 src/workerd/api/tests/crypto-secp256k1-test.wd-test diff --git a/deps/rust/Cargo.toml b/deps/rust/Cargo.toml index 35676959979..125e8927fbf 100644 --- a/deps/rust/Cargo.toml +++ b/deps/rust/Cargo.toml @@ -28,7 +28,7 @@ syn = { version = "2", features = ["full"] } ada-url = "3" anyhow = "1" async-trait = { version = "0", default-features = false } -aws-lc-rs = "1" +aws-lc-rs = { version = "1", features = ["prebuilt-nasm"] } capnp = "0" capnpc = "0" capnp-rpc = "0" diff --git a/src/workerd/api/crypto/ec.c++ b/src/workerd/api/crypto/ec.c++ index 18510a91a03..4112622610e 100644 --- a/src/workerd/api/crypto/ec.c++ +++ b/src/workerd/api/crypto/ec.c++ @@ -10,6 +10,7 @@ #include #include +#include #include #include @@ -451,9 +452,9 @@ EllipticCurveInfo lookupEllipticCurve(kj::StringPtr curveName) { return iter->second; } -// Overload that adds secp256k1 to the table when the compat flag is set. +// Overload that adds secp256k1 to the table when the autogate is enabled. EllipticCurveInfo lookupEllipticCurve(jsg::Lock& js, kj::StringPtr curveName) { - if (FeatureFlags::get(js).getSecp256k1EcdsaCurve() && isSecp256k1(curveName)) { + if (util::Autogate::isEnabled(util::AutogateKey::SECP256K1_ECDSA) && isSecp256k1(curveName)) { return {"secp256k1", 0, 32}; } return lookupEllipticCurve(curveName); diff --git a/src/workerd/api/tests/BUILD.bazel b/src/workerd/api/tests/BUILD.bazel index d48a2e241f7..ed31e7db0e6 100644 --- a/src/workerd/api/tests/BUILD.bazel +++ b/src/workerd/api/tests/BUILD.bazel @@ -309,13 +309,21 @@ wd_test( data = ["crypto-impl-asymmetric-test.js"], ) +# secp256k1 ECDSA when the SECP256K1_ECDSA autogate is off (default variant only). wd_test( - src = "crypto-secp256k1-test.wd-test", + src = "crypto-secp256k1-disabled-test.wd-test", args = ["--experimental"], - data = [ - "crypto-secp256k1-no-flag-test.js", - "crypto-secp256k1-with-flag-test.js", - ], + data = ["crypto-secp256k1-disabled-test.js"], + generate_all_autogates_variant = False, +) + +# secp256k1 ECDSA when the SECP256K1_ECDSA autogate is on (@all-autogates only). +wd_test( + src = "crypto-secp256k1-enabled-test.wd-test", + args = ["--experimental"], + data = ["crypto-secp256k1-enabled-test.js"], + generate_all_compat_flags_variant = False, + generate_default_variant = False, ) wd_test( diff --git a/src/workerd/api/tests/crypto-secp256k1-no-flag-test.js b/src/workerd/api/tests/crypto-secp256k1-disabled-test.js similarity index 100% rename from src/workerd/api/tests/crypto-secp256k1-no-flag-test.js rename to src/workerd/api/tests/crypto-secp256k1-disabled-test.js diff --git a/src/workerd/api/tests/crypto-secp256k1-disabled-test.wd-test b/src/workerd/api/tests/crypto-secp256k1-disabled-test.wd-test new file mode 100644 index 00000000000..a7e65bd9475 --- /dev/null +++ b/src/workerd/api/tests/crypto-secp256k1-disabled-test.wd-test @@ -0,0 +1,17 @@ +using Workerd = import "/workerd/workerd.capnp"; + +# Tests that secp256k1 ECDSA is rejected when the SECP256K1_ECDSA autogate is +# off. Only runs in the default variant; @all-autogates is skipped. + +const unitTests :Workerd.Config = ( + services = [ + ( name = "crypto-secp256k1-disabled", + worker = ( + modules = [ + (name = "worker", esModule = embed "crypto-secp256k1-disabled-test.js") + ], + compatibilityFlags = ["nodejs_compat"], + ) + ), + ], +); diff --git a/src/workerd/api/tests/crypto-secp256k1-with-flag-test.js b/src/workerd/api/tests/crypto-secp256k1-enabled-test.js similarity index 100% rename from src/workerd/api/tests/crypto-secp256k1-with-flag-test.js rename to src/workerd/api/tests/crypto-secp256k1-enabled-test.js diff --git a/src/workerd/api/tests/crypto-secp256k1-enabled-test.wd-test b/src/workerd/api/tests/crypto-secp256k1-enabled-test.wd-test new file mode 100644 index 00000000000..cda673cb1fe --- /dev/null +++ b/src/workerd/api/tests/crypto-secp256k1-enabled-test.wd-test @@ -0,0 +1,17 @@ +using Workerd = import "/workerd/workerd.capnp"; + +# Tests that secp256k1 ECDSA works when the SECP256K1_ECDSA autogate is on. +# Only runs in the @all-autogates variant. + +const unitTests :Workerd.Config = ( + services = [ + ( name = "crypto-secp256k1-enabled", + worker = ( + modules = [ + (name = "worker", esModule = embed "crypto-secp256k1-enabled-test.js") + ], + compatibilityFlags = ["nodejs_compat"], + ) + ), + ], +); diff --git a/src/workerd/api/tests/crypto-secp256k1-test.wd-test b/src/workerd/api/tests/crypto-secp256k1-test.wd-test deleted file mode 100644 index 68a4b5f4817..00000000000 --- a/src/workerd/api/tests/crypto-secp256k1-test.wd-test +++ /dev/null @@ -1,29 +0,0 @@ -using Workerd = import "/workerd/workerd.capnp"; - -# Two services so each side can run against a different setting of the secp256k1_ecdsa_curve -# compat flag. The flag is per-worker. - -const unitTests :Workerd.Config = ( - services = [ - ( name = "crypto-secp256k1-no-flag", - worker = ( - modules = [ - (name = "worker", esModule = embed "crypto-secp256k1-no-flag-test.js") - ], - compatibilityFlags = ["nodejs_compat"], - ) - ), - ( name = "crypto-secp256k1-with-flag", - worker = ( - modules = [ - (name = "worker", esModule = embed "crypto-secp256k1-with-flag-test.js") - ], - compatibilityFlags = [ - "nodejs_compat", - "experimental", - "secp256k1_ecdsa_curve", - ], - ) - ), - ], -); diff --git a/src/workerd/io/compatibility-date.capnp b/src/workerd/io/compatibility-date.capnp index b3d62616e10..cb799f98688 100644 --- a/src/workerd/io/compatibility-date.capnp +++ b/src/workerd/io/compatibility-date.capnp @@ -1539,10 +1539,4 @@ struct CompatibilityFlags @0x8f8c1b68151b6cef { # startup. This allows packages to extend `sys.path` declaratively (e.g. to # add subdirectories or register import hooks). Without this flag, `.pth` # files in `python_modules/` are ignored. - - secp256k1EcdsaCurve @177 :Bool - $compatEnableFlag("secp256k1_ecdsa_curve") - $experimental; - # When enabled, the WebCrypto ECDSA algorithm accepts "secp256k1" as a - # namedCurve, with SHA-256 as the only supported hash. } diff --git a/src/workerd/util/autogate.c++ b/src/workerd/util/autogate.c++ index 9d5d17db19f..bb34abed63f 100644 --- a/src/workerd/util/autogate.c++ +++ b/src/workerd/util/autogate.c++ @@ -47,6 +47,8 @@ kj::StringPtr KJ_STRINGIFY(AutogateKey key) { return "updated-auto-allocate-chunk-size"_kj; case AutogateKey::PYTHON_ABORT_ISOLATE_ON_FATAL_ERROR: return "python-abort-isolate-on-fatal-error"_kj; + case AutogateKey::SECP256K1_ECDSA: + return "secp256k1-ecdsa"_kj; case AutogateKey::NumOfKeys: KJ_FAIL_ASSERT("NumOfKeys should not be used in getName"); } diff --git a/src/workerd/util/autogate.h b/src/workerd/util/autogate.h index bbf16f70a73..b0fff05b121 100644 --- a/src/workerd/util/autogate.h +++ b/src/workerd/util/autogate.h @@ -52,6 +52,8 @@ enum class AutogateKey { UPDATED_AUTO_ALLOCATE_CHUNK_SIZE, // Call abortIsolate() when a Python worker encounters a fatal error. PYTHON_ABORT_ISOLATE_ON_FATAL_ERROR, + // Enable WebCrypto ECDSA support for the secp256k1 curve via aws-lc-rs. + SECP256K1_ECDSA, NumOfKeys // Reserved for iteration. };