From b00f076017ca82e1f5f29de0a58dfaea8cdddeb2 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Thu, 14 Mar 2024 06:53:50 -0700 Subject: [PATCH] fix(ext/node): Support private EC key signing (#22914) Fixes https://github.com/denoland/deno/issues/18972 Support for web-push VAPID keys & jws signing - Fixes EC keygen to return raw private key and uncompressed public key point. - Support for `EC PRIVATE KEY` --- Cargo.lock | 1 + ext/node/Cargo.toml | 1 + ext/node/ops/crypto/mod.rs | 92 +++++++++++++++------- tests/unit_node/crypto/crypto_sign_test.ts | 11 +++ 4 files changed, 76 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9eb1c5d2e7..50005a04b4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1683,6 +1683,7 @@ dependencies = [ "ripemd", "rsa", "scrypt", + "sec1", "serde", "sha-1", "sha2", diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 8e3e695d61..8569a0491a 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -63,6 +63,7 @@ ring.workspace = true ripemd = "0.1.3" rsa.workspace = true scrypt = "0.11.0" +sec1 = "0.7" serde = "1.0.149" sha-1 = "0.10.0" sha2.workspace = true diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index 7f593520b4..6b1ca9a38b 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -373,20 +373,36 @@ pub fn op_node_sign( let oid; let pkey = match format { - "pem" => { - if label == "PRIVATE KEY" { + "pem" => match label { + "PRIVATE KEY" => { let pk_info = pkcs8::PrivateKeyInfo::try_from(doc.as_bytes())?; oid = pk_info.algorithm.oid; pk_info.private_key - } else if label == "RSA PRIVATE KEY" { + } + "RSA PRIVATE KEY" => { oid = RSA_ENCRYPTION_OID; doc.as_bytes() - } else { - return Err(type_error("Invalid PEM label")); } - } + "EC PRIVATE KEY" => { + let ec_pk = sec1::EcPrivateKey::from_der(doc.as_bytes())?; + match ec_pk.parameters { + Some(sec1::EcParameters::NamedCurve(o)) => { + oid = o; + ec_pk.private_key + } + // https://datatracker.ietf.org/doc/html/rfc5915#section-3 + // + // Though the ASN.1 indicates that + // the parameters field is OPTIONAL, implementations that conform to + // this document MUST always include the parameters field. + _ => return Err(type_error("invalid ECPrivateKey params")), + } + } + _ => return Err(type_error("Invalid PEM label")), + }, _ => return Err(type_error("Unsupported key format")), }; + match oid { RSA_ENCRYPTION_OID => { use rsa::pkcs1v15::SigningKey; @@ -419,6 +435,25 @@ pub fn op_node_sign( .into(), ) } + // signature structure encoding is DER by default for DSA and ECDSA. + // + // TODO(@littledivy): Validate public_key if present + ID_SECP256R1_OID => { + let key = p256::ecdsa::SigningKey::from_slice(pkey)?; + Ok( + key + .sign_prehash(digest) + .map(|sig: p256::ecdsa::Signature| sig.to_der().to_vec().into())?, + ) + } + ID_SECP384R1_OID => { + let key = p384::ecdsa::SigningKey::from_slice(pkey)?; + Ok( + key + .sign_prehash(digest) + .map(|sig: p384::ecdsa::Signature| sig.to_der().to_vec().into())?, + ) + } _ => Err(type_error("Unsupported signing key")), } } @@ -704,30 +739,32 @@ pub async fn op_node_dsa_generate_async( fn ec_generate( named_curve: &str, ) -> Result<(ToJsBuffer, ToJsBuffer), AnyError> { - use ring::signature::EcdsaKeyPair; - use ring::signature::KeyPair; + use elliptic_curve::sec1::ToEncodedPoint; - let curve = match named_curve { + let mut rng = rand::thread_rng(); + // TODO(@littledivy): Support public key point encoding. + // Default is uncompressed. + match named_curve { "P-256" | "prime256v1" | "secp256r1" => { - &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING + let key = p256::SecretKey::random(&mut rng); + let public_key = key.public_key(); + + Ok(( + key.to_bytes().to_vec().into(), + public_key.to_encoded_point(false).as_ref().to_vec().into(), + )) } "P-384" | "prime384v1" | "secp384r1" => { - &ring::signature::ECDSA_P384_SHA384_FIXED_SIGNING + let key = p384::SecretKey::random(&mut rng); + let public_key = key.public_key(); + + Ok(( + key.to_bytes().to_vec().into(), + public_key.to_encoded_point(false).as_ref().to_vec().into(), + )) } - _ => return Err(type_error("Unsupported named curve")), - }; - - let rng = ring::rand::SystemRandom::new(); - - let pkcs8 = EcdsaKeyPair::generate_pkcs8(curve, &rng) - .map_err(|_| type_error("Failed to generate EC key"))?; - - let public_key = EcdsaKeyPair::from_pkcs8(curve, pkcs8.as_ref(), &rng) - .map_err(|_| type_error("Failed to generate EC key"))? - .public_key() - .as_ref() - .to_vec(); - Ok((pkcs8.as_ref().to_vec().into(), public_key.into())) + _ => Err(type_error("Unsupported named curve")), + } } #[op2] @@ -1363,11 +1400,8 @@ fn parse_private_key( ) -> Result { match format { "pem" => { - let (label, doc) = + let (_, doc) = pkcs8::SecretDocument::from_pem(std::str::from_utf8(key).unwrap())?; - if label != "PRIVATE KEY" { - return Err(type_error("Invalid PEM label")); - } Ok(doc) } "der" => { diff --git a/tests/unit_node/crypto/crypto_sign_test.ts b/tests/unit_node/crypto/crypto_sign_test.ts index be459bf520..2ca0af9438 100644 --- a/tests/unit_node/crypto/crypto_sign_test.ts +++ b/tests/unit_node/crypto/crypto_sign_test.ts @@ -127,3 +127,14 @@ Deno.test({ } }, }); + +Deno.test({ + name: "crypto.createSign|sign - EC PRIVATE KEY", + fn() { + const pem = `-----BEGIN EC PRIVATE KEY----- +MDECAQEEIIThPSZ00CNW1UD5Ju9mhplv6SSs3T5objYjlx11gHW9oAoGCCqGSM49 +AwEH +-----END EC PRIVATE KEY-----`; + createSign("SHA256").update("test").sign(pem, "base64"); + }, +});