Repository: denoland/deno
Pull request: fix(node): support ECDSA with secp256k1 in `node:crypto`

## Summary

- Adds secp256k1 ECDSA support to `node:crypto` key generation, signing, verification, and key import/export (DER, PEM, JWK)
- Adds `Secp256k1` variant to `EcPrivateKey`/`EcPublicKey` enums using the existing `k256` crate
- Enables `ecdh`, `jwk`, `pem` features on `k256` workspace dependency

Closes #27523

## Details

The `k256` crate was already used for ECDH with secp256k1, but the ECDSA key generation/signing/verification paths only supported P-224, P-256, and P-384. This PR adds secp256k1 support across all EC code paths:

- **Key generation**: `generateKeyPair('ec', { namedCurve: 'secp256k1' })` now works
- **Signing/verification**: `sign()`, `verify()`, `createSign()`, `createVerify()` with secp256k1 keys
- **Key import/export**: PKCS#8, SEC1, SPKI (DER/PEM), and JWK formats
- **Key details**: Reports `namedCurve: "secp256k1"` in `asymmetricKeyDetails`
- **X.509**: Parsing secp256k1 public keys from certificates
- **DH**: `diffieHellman()` with secp256k1 KeyObject pairs

## Test plan

- [x] Added unit tests for key generation, sign/verify (one-shot and streaming), PEM export/import roundtrip, and async key generation
- [x] `cargo test unit_node::crypto::crypto_key_test` passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: Cargo.toml
patch: @@ -391,7 +391,7 @@ ed25519-dalek = "2.1.1"
 ed448-goldilocks = "0.8.3"
 elliptic-curve = { version = "0.13.4", features = ["alloc", "arithmetic", "ecdh", "std", "pem", "jwk"] }
 hkdf = "0.12.3"
-k256 = "0.13.1"
+k256 = { version = "0.13.1", features = ["ecdh", "jwk", "pem"] }
 md-5 = "0.10.5"
 md4 = "0.10.2"
 ocb3 = "0.1.0"

filename: ext/node_crypto/keys.rs
patch: @@ -136,6 +136,7 @@ pub enum EcPrivateKey {
   P224(p224::SecretKey),
   P256(p256::SecretKey),
   P384(p384::SecretKey),
+  Secp256k1(k256::SecretKey),
 }
 
 #[derive(Clone)]
@@ -166,6 +167,7 @@ pub enum EcPublicKey {
   P224(p224::PublicKey),
   P256(p256::PublicKey),
   P384(p384::PublicKey),
+  Secp256k1(k256::PublicKey),
 }
 
 #[derive(Clone)]
@@ -251,6 +253,7 @@ impl EcPublicKey {
       }
       EcPublicKey::P256(key) => Ok(key.to_jwk()),
       EcPublicKey::P384(key) => Ok(key.to_jwk()),
+      EcPublicKey::Secp256k1(key) => Ok(key.to_jwk()),
     }
   }
 }
@@ -262,6 +265,7 @@ impl EcPrivateKey {
       EcPrivateKey::P224(key) => EcPublicKey::P224(key.public_key()),
       EcPrivateKey::P256(key) => EcPublicKey::P256(key.public_key()),
       EcPrivateKey::P384(key) => EcPublicKey::P384(key.public_key()),
+      EcPrivateKey::Secp256k1(key) => EcPublicKey::Secp256k1(key.public_key()),
     }
   }
 
@@ -272,6 +276,7 @@ impl EcPrivateKey {
       }
       EcPrivateKey::P256(key) => Ok(key.to_jwk()),
       EcPrivateKey::P384(key) => Ok(key.to_jwk()),
+      EcPrivateKey::Secp256k1(key) => Ok(key.to_jwk()),
     }
   }
 }
@@ -300,6 +305,8 @@ pub const ID_SECP256R1_OID: const_oid::ObjectIdentifier =
   const_oid::ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7");
 pub const ID_SECP384R1_OID: const_oid::ObjectIdentifier =
   const_oid::ObjectIdentifier::new_unwrap("1.3.132.0.34");
+pub const ID_SECP256K1_OID: const_oid::ObjectIdentifier =
+  const_oid::ObjectIdentifier::new_unwrap("1.3.132.0.10");
 
 pub const RSA_ENCRYPTION_OID: const_oid::ObjectIdentifier =
   const_oid::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1");
@@ -732,6 +739,13 @@ impl KeyObjectHandle {
             .map_err(|_| AsymmetricPrivateKeyError::InvalidSec1PrivateKey)?;
             AsymmetricPrivateKey::Ec(EcPrivateKey::P384(secret_key))
           }
+          ID_SECP256K1_OID => {
+            let secret_key = k256::SecretKey::from_sec1_der(
+              pk_info.private_key,
+            )
+            .map_err(|_| AsymmetricPrivateKeyError::InvalidSec1PrivateKey)?;
+            AsymmetricPrivateKey::Ec(EcPrivateKey::Secp256k1(secret_key))
+          }
           _ => return Err(AsymmetricPrivateKeyError::UnsupportedEcNamedCurve),
         }
       }
@@ -789,6 +803,7 @@ impl KeyObjectHandle {
           const ID_SECP224R1: &[u8] = &oid!(raw 1.3.132.0.33);
           const ID_SECP256R1: &[u8] = &oid!(raw 1.2.840.10045.3.1.7);
           const ID_SECP384R1: &[u8] = &oid!(raw 1.3.132.0.34);
+          const ID_SECP256K1: &[u8] = &oid!(raw 1.3.132.0.10);
 
           match curve_oid.as_bytes() {
             ID_SECP224R1 => {
@@ -803,6 +818,10 @@ impl KeyObjectHandle {
               let public_key = p384::PublicKey::from_sec1_bytes(data)?;
               AsymmetricPublicKey::Ec(EcPublicKey::P384(public_key))
             }
+            ID_SECP256K1 => {
+              let public_key = k256::PublicKey::from_sec1_bytes(data)?;
+              AsymmetricPublicKey::Ec(EcPublicKey::Secp256k1(public_key))
+            }
             _ => return Err(X509PublicKeyError::UnsupportedEcNamedCurve),
           }
         } else {
@@ -897,6 +916,16 @@ impl KeyObjectHandle {
       "P-384" => KeyObjectHandle::AsymmetricPrivate(AsymmetricPrivateKey::Ec(
         EcPrivateKey::P384(p384::SecretKey::from_jwk(jwk)?),
       )),
+      "secp256k1" if is_public => {
+        KeyObjectHandle::AsymmetricPublic(AsymmetricPublicKey::Ec(
+          EcPublicKey::Secp256k1(k256::PublicKey::from_jwk(jwk)?),
+        ))
+      }
+      "secp256k1" => {
+        KeyObjectHandle::AsymmetricPrivate(AsymmetricPrivateKey::Ec(
+          EcPrivateKey::Secp256k1(k256::SecretKey::from_jwk(jwk)?),
+        ))
+      }
       _ => {
         return Err(EcJwkError::UnsupportedCurve(jwk.crv().to_string()));
       }
@@ -1067,6 +1096,10 @@ impl KeyObjectHandle {
             let public_key = p384::PublicKey::from_sec1_bytes(data)?;
             AsymmetricPublicKey::Ec(EcPublicKey::P384(public_key))
           }
+          ID_SECP256K1_OID => {
+            let public_key = k256::PublicKey::from_sec1_bytes(data)?;
+            AsymmetricPublicKey::Ec(EcPublicKey::Secp256k1(public_key))
+          }
           _ => return Err(AsymmetricPublicKeyError::UnsupportedEcNamedCurve),
         }
       }
@@ -1323,6 +1356,7 @@ impl AsymmetricPublicKey {
               EcPublicKey::P224(key) => (key.to_sec1_bytes(), ID_SECP224R1_OID),
               EcPublicKey::P256(key) => (key.to_sec1_bytes(), ID_SECP256R1_OID),
               EcPublicKey::P384(key) => (key.to_sec1_bytes(), ID_SECP384R1_OID),
+              EcPublicKey::Secp256k1(key) => (key.to_sec1_bytes(), ID_SECP256K1_OID),
             };
 
             let spki = SubjectPublicKeyInfoRef {
@@ -1523,6 +1557,7 @@ impl AsymmetricPrivateKey {
             EcPrivateKey::P224(key) => key.to_sec1_der(),
             EcPrivateKey::P256(key) => key.to_sec1_der(),
             EcPrivateKey::P384(key) => key.to_sec1_der(),
+            EcPrivateKey::Secp256k1(key) => key.to_sec1_der(),
           }
           .map_err(|_| AsymmetricPrivateKeyDerError::InvalidEcPrivateKey)?;
           Ok(sec1.to_vec().into_boxed_slice())
@@ -1551,6 +1586,7 @@ impl AsymmetricPrivateKey {
               EcPrivateKey::P224(key) => key.to_pkcs8_der(),
               EcPrivateKey::P256(key) => key.to_pkcs8_der(),
               EcPrivateKey::P384(key) => key.to_pkcs8_der(),
+              EcPrivateKey::Secp256k1(key) => key.to_pkcs8_der(),
             }
             .map_err(|_| AsymmetricPrivateKeyDerError::InvalidEcPrivateKey)?;
             document.to_bytes().to_vec().into_boxed_slice()
@@ -1813,9 +1849,10 @@ pub fn op_node_get_asymmetric_key_details(
       }
       AsymmetricPrivateKey::Ec(key) => {
         let named_curve = match key {
-          EcPrivateKey::P224(_) => "p224",
-          EcPrivateKey::P256(_) => "p256",
-          EcPrivateKey::P384(_) => "p384",
+          EcPrivateKey::P224(_) => "secp224r1",
+          EcPrivateKey::P256(_) => "prime256v1",
+          EcPrivateKey::P384(_) => "secp384r1",
+          EcPrivateKey::Secp256k1(_) => "secp256k1",
         };
         Ok(AsymmetricKeyDetails::Ec { named_curve })
       }
@@ -1866,9 +1903,10 @@ pub fn op_node_get_asymmetric_key_details(
       }
       AsymmetricPublicKey::Ec(key) => {
         let named_curve = match key {
-          EcPublicKey::P224(_) => "p224",
-          EcPublicKey::P256(_) => "p256",
-          EcPublicKey::P384(_) => "p384",
+          EcPublicKey::P224(_) => "secp224r1",
+          EcPublicKey::P256(_) => "prime256v1",
+          EcPublicKey::P384(_) => "secp384r1",
+          EcPublicKey::Secp256k1(_) => "secp256k1",
         };
         Ok(AsymmetricKeyDetails::Ec { named_curve })
       }
@@ -2169,6 +2207,10 @@ fn ec_generate(named_curve: &str) -> Result<KeyObjectHandlePair, JsErrorBox> {
       let key = p384::SecretKey::random(&mut rng);
       AsymmetricPrivateKey::Ec(EcPrivateKey::P384(key))
     }
+    "secp256k1" => {
+      let key = k256::SecretKey::random(&mut rng);
+      AsymmetricPrivateKey::Ec(EcPrivateKey::Secp256k1(key))
+    }
     _ => {
       return Err(JsErrorBox::type_error(format!(
         "unsupported named curve: {}",

filename: ext/node_crypto/lib.rs
patch: @@ -1149,6 +1149,16 @@ pub fn op_node_diffie_hellman(
       .raw_secret_bytes()
       .to_vec()
       .into_boxed_slice(),
+      (
+        AsymmetricPrivateKey::Ec(EcPrivateKey::Secp256k1(private)),
+        AsymmetricPublicKey::Ec(EcPublicKey::Secp256k1(public)),
+      ) => k256::ecdh::diffie_hellman(
+        private.to_nonzero_scalar(),
+        public.as_affine(),
+      )
+      .raw_secret_bytes()
+      .to_vec()
+      .into_boxed_slice(),
       (
         AsymmetricPrivateKey::X25519(private),
         AsymmetricPublicKey::X25519(public),

filename: ext/node_crypto/sign.rs
patch: @@ -197,6 +197,14 @@ impl KeyObjectHandle {
 
           dsa_signature(dsa_signature_encoding, signature)
         }
+        EcPrivateKey::Secp256k1(key) => {
+          let signing_key = k256::ecdsa::SigningKey::from(key);
+          let signature: k256::ecdsa::Signature = signing_key
+            .sign_prehash(digest)
+            .map_err(|_| KeyObjectHandlePrehashedSignAndVerifyError::FailedToSignDigest)?;
+
+          dsa_signature(dsa_signature_encoding, signature)
+        }
       },
       AsymmetricPrivateKey::X25519(_) => {
         Err(KeyObjectHandlePrehashedSignAndVerifyError::X25519KeyCannotBeUsedForSigning)
@@ -315,6 +323,18 @@ impl KeyObjectHandle {
           };
           Ok(verifying_key.verify_prehash(digest, &signature).is_ok())
         }
+        EcPublicKey::Secp256k1(key) => {
+          let verifying_key = k256::ecdsa::VerifyingKey::from(key);
+          let signature = if dsa_signature_encoding == 0 {
+            k256::ecdsa::Signature::from_der(signature)
+          } else {
+            k256::ecdsa::Signature::from_bytes(signature.into())
+          };
+          let Ok(signature) = signature else {
+            return Ok(false);
+          };
+          Ok(verifying_key.verify_prehash(digest, &signature).is_ok())
+        }
       },
       AsymmetricPublicKey::X25519(_) => {
         Err(KeyObjectHandlePrehashedSignAndVerifyError::X25519KeyCannotBeUsedForVerification)

filename: tests/unit_node/crypto/crypto_key_test.ts
patch: @@ -8,10 +8,13 @@ import {
   createPublicKey,
   createSecretKey,
   createSign,
+  createVerify,
   generateKeyPair,
   generateKeyPairSync,
   KeyObject,
   randomBytes,
+  sign,
+  verify,
   X509Certificate,
 } from "node:crypto";
 import { promisify } from "node:util";
@@ -290,7 +293,7 @@ Deno.test("createPrivateKey ec", function () {
   const key = createPrivateKey(ecPrivateKey);
   assertEquals(key.type, "private");
   assertEquals(key.asymmetricKeyType, "ec");
-  assertEquals(key.asymmetricKeyDetails?.namedCurve, "p256");
+  assertEquals(key.asymmetricKeyDetails?.namedCurve, "prime256v1");
 });
 
 const rsaPublicKey = Deno.readTextFileSync(
@@ -315,7 +318,7 @@ Deno.test("createPublicKey() EC", function () {
   const key = createPublicKey(ecPublicKey);
   assertEquals(key.type, "public");
   assertEquals(key.asymmetricKeyType, "ec");
-  assertEquals(key.asymmetricKeyDetails?.namedCurve, "p256");
+  assertEquals(key.asymmetricKeyDetails?.namedCurve, "prime256v1");
 });
 
 Deno.test("createPublicKey SPKI for DH", async function () {
@@ -825,3 +828,89 @@ Deno.test("curve25519 generate valid private jwk", function () {
   // @ts-ignore @types/node broken
   assert(privateKey.d);
 });
+
+Deno.test("generateKeyPairSync ec secp256k1", () => {
+  const { publicKey, privateKey } = generateKeyPairSync("ec", {
+    namedCurve: "secp256k1",
+  });
+
+  assertEquals(publicKey.type, "public");
+  assertEquals(privateKey.type, "private");
+  assertEquals(publicKey.asymmetricKeyType, "ec");
+  assertEquals(privateKey.asymmetricKeyType, "ec");
+
+  const pubDetails = publicKey.asymmetricKeyDetails as any;
+  assertEquals(pubDetails.namedCurve, "secp256k1");
+  const privDetails = privateKey.asymmetricKeyDetails as any;
+  assertEquals(privDetails.namedCurve, "secp256k1");
+});
+
+Deno.test("ec secp256k1 sign and verify", () => {
+  const { publicKey, privateKey } = generateKeyPairSync("ec", {
+    namedCurve: "secp256k1",
+  });
+
+  const data = Buffer.from("hello secp256k1");
+
+  const signature = sign("sha256", data, privateKey);
+  assert(signature.length > 0);
+
+  const ok = verify("sha256", data, publicKey, signature);
+  assert(ok);
+
+  const bad = verify("sha256", Buffer.from("wrong data"), publicKey, signature);
+  assert(!bad);
+});
+
+Deno.test("ec secp256k1 createSign and createVerify", () => {
+  const { publicKey, privateKey } = generateKeyPairSync("ec", {
+    namedCurve: "secp256k1",
+  });
+
+  const s = createSign("SHA256");
+  s.update("hello secp256k1");
+  const signature = s.sign(privateKey);
+
+  const v = createVerify("SHA256");
+  v.update("hello secp256k1");
+  assert(v.verify(publicKey, signature));
+});
+
+Deno.test("ec secp256k1 export and import PEM", () => {
+  const { publicKey, privateKey } = generateKeyPairSync("ec", {
+    namedCurve: "secp256k1",
+  });
+
+  const pubPem = publicKey.export({ type: "spki", format: "pem" });
+  const privPem = privateKey.export({ type: "pkcs8", format: "pem" });
+
+  assert(typeof pubPem === "string");
+  assert(typeof privPem === "string");
+  assert((pubPem as string).startsWith("-----BEGIN PUBLIC KEY-----"));
+  assert((privPem as string).startsWith("-----BEGIN PRIVATE KEY-----"));
+
+  const importedPub = createPublicKey(pubPem);
+  const importedPriv = createPrivateKey(privPem);
+
+  assertEquals(importedPub.asymmetricKeyType, "ec");
+  assertEquals(importedPriv.asymmetricKeyType, "ec");
+
+  // Sign with imported private key and verify with imported public key
+  const data = Buffer.from("roundtrip test");
+  const signature = sign("sha256", data, importedPriv);
+  assert(verify("sha256", data, importedPub, signature));
+});
+
+Deno.test("generateKeyPair async ec secp256k1", async () => {
+  const { publicKey, privateKey } = await generateKeyPairAsync(
+    "ec",
+    { namedCurve: "secp256k1" },
+  );
+
+  assertEquals(publicKey.type, "public");
+  assertEquals(privateKey.type, "private");
+
+  const data = Buffer.from("async test");
+  const signature = sign("sha256", data, privateKey);
+  assert(verify("sha256", data, publicKey, signature));
+});