From 1cd36009b0f5eeae0eb337415b35f7a27702bd2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bartek=20Iwa=C5=84czuk?= Date: Tue, 31 Dec 2024 12:49:02 +0000 Subject: [PATCH] fix(ext/node): support private key export in JWK format (#27325) Closes https://github.com/denoland/deno/issues/26643 --------- Co-authored-by: Divy Srivastava --- ext/node/lib.rs | 1 + ext/node/ops/crypto/keys.rs | 109 +++++++++++++++++++++ ext/node/polyfills/internal/crypto/keys.ts | 3 +- tests/unit_node/crypto/crypto_key_test.ts | 16 +++ 4 files changed, 128 insertions(+), 1 deletion(-) diff --git a/ext/node/lib.rs b/ext/node/lib.rs index b9b459efc135c4..0c4791eb72f358 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -262,6 +262,7 @@ deno_core::extension!(deno_node, ops::crypto::keys::op_node_derive_public_key_from_private_key, ops::crypto::keys::op_node_dh_keys_generate_and_export, ops::crypto::keys::op_node_export_private_key_der, + ops::crypto::keys::op_node_export_private_key_jwk, ops::crypto::keys::op_node_export_private_key_pem, ops::crypto::keys::op_node_export_public_key_der, ops::crypto::keys::op_node_export_public_key_pem, diff --git a/ext/node/ops/crypto/keys.rs b/ext/node/ops/crypto/keys.rs index f164972d48afc6..dfcd3d11bf0ad3 100644 --- a/ext/node/ops/crypto/keys.rs +++ b/ext/node/ops/crypto/keys.rs @@ -26,6 +26,7 @@ use rsa::pkcs1::DecodeRsaPrivateKey as _; use rsa::pkcs1::DecodeRsaPublicKey; use rsa::pkcs1::EncodeRsaPrivateKey as _; use rsa::pkcs1::EncodeRsaPublicKey; +use rsa::traits::PrivateKeyParts; use rsa::traits::PublicKeyParts; use rsa::RsaPrivateKey; use rsa::RsaPublicKey; @@ -255,6 +256,16 @@ impl EcPrivateKey { EcPrivateKey::P384(key) => EcPublicKey::P384(key.public_key()), } } + + pub fn to_jwk(&self) -> Result { + match self { + EcPrivateKey::P224(_) => { + Err(AsymmetricPrivateKeyJwkError::UnsupportedJwkEcCurveP224) + } + EcPrivateKey::P256(key) => Ok(key.to_jwk()), + EcPrivateKey::P384(key) => Ok(key.to_jwk()), + } + } } // https://oidref.com/ @@ -1107,6 +1118,16 @@ fn bytes_to_b64(bytes: &[u8]) -> String { BASE64_URL_SAFE_NO_PAD.encode(bytes) } +#[derive(Debug, thiserror::Error)] +pub enum AsymmetricPrivateKeyJwkError { + #[error("key is not an asymmetric private key")] + KeyIsNotAsymmetricPrivateKey, + #[error("Unsupported JWK EC curve: P224")] + UnsupportedJwkEcCurveP224, + #[error("jwk export not implemented for this key type")] + JwkExportNotImplementedForKeyType, +} + #[derive(Debug, thiserror::Error)] pub enum AsymmetricPublicKeyJwkError { #[error("key is not an asymmetric public key")] @@ -1328,7 +1349,73 @@ pub enum AsymmetricPrivateKeyDerError { UnsupportedKeyType(String), } +// https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2 +fn rsa_private_to_jwk(key: &RsaPrivateKey) -> deno_core::serde_json::Value { + let n = key.n(); + let e = key.e(); + let d = key.d(); + let p = &key.primes()[0]; + let q = &key.primes()[1]; + let dp = key.dp(); + let dq = key.dq(); + let qi = key.crt_coefficient(); + let oth = &key.primes()[2..]; + + let mut obj = deno_core::serde_json::json!({ + "kty": "RSA", + "n": bytes_to_b64(&n.to_bytes_be()), + "e": bytes_to_b64(&e.to_bytes_be()), + "d": bytes_to_b64(&d.to_bytes_be()), + "p": bytes_to_b64(&p.to_bytes_be()), + "q": bytes_to_b64(&q.to_bytes_be()), + "dp": dp.map(|dp| bytes_to_b64(&dp.to_bytes_be())), + "dq": dq.map(|dq| bytes_to_b64(&dq.to_bytes_be())), + "qi": qi.map(|qi| bytes_to_b64(&qi.to_bytes_be())), + }); + + if !oth.is_empty() { + obj["oth"] = deno_core::serde_json::json!(oth + .iter() + .map(|o| o.to_bytes_be()) + .collect::>()); + } + + obj +} + impl AsymmetricPrivateKey { + fn export_jwk( + &self, + ) -> Result { + match self { + AsymmetricPrivateKey::Rsa(key) => Ok(rsa_private_to_jwk(key)), + AsymmetricPrivateKey::RsaPss(key) => Ok(rsa_private_to_jwk(&key.key)), + AsymmetricPrivateKey::Ec(key) => { + let jwk = key.to_jwk()?; + Ok(deno_core::serde_json::json!(jwk)) + } + AsymmetricPrivateKey::X25519(static_secret) => { + let bytes = static_secret.to_bytes(); + + Ok(deno_core::serde_json::json!({ + "kty": "OKP", + "crv": "X25519", + "d": bytes_to_b64(&bytes), + })) + } + AsymmetricPrivateKey::Ed25519(key) => { + let bytes = key.to_bytes(); + + Ok(deno_core::serde_json::json!({ + "kty": "OKP", + "crv": "Ed25519", + "d": bytes_to_b64(&bytes), + })) + } + _ => Err(AsymmetricPrivateKeyJwkError::JwkExportNotImplementedForKeyType), + } + } + fn export_der( &self, typ: &str, @@ -2329,6 +2416,28 @@ pub fn op_node_export_private_key_pem( Ok(String::from_utf8(out).expect("invalid pem is not possible")) } +#[derive(Debug, thiserror::Error)] +pub enum ExportPrivateKeyJwkError { + #[error(transparent)] + AsymmetricPublicKeyJwk(#[from] AsymmetricPrivateKeyJwkError), + #[error("very large data")] + VeryLargeData, + #[error(transparent)] + Der(#[from] der::Error), +} + +#[op2] +#[serde] +pub fn op_node_export_private_key_jwk( + #[cppgc] handle: &KeyObjectHandle, +) -> Result { + let private_key = handle + .as_private_key() + .ok_or(AsymmetricPrivateKeyJwkError::KeyIsNotAsymmetricPrivateKey)?; + + Ok(private_key.export_jwk()?) +} + #[op2] #[buffer] pub fn op_node_export_private_key_der( diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index c91c23cc3dd931..932856df0e88d5 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -20,6 +20,7 @@ import { op_node_create_secret_key, op_node_derive_public_key_from_private_key, op_node_export_private_key_der, + op_node_export_private_key_jwk, op_node_export_private_key_pem, op_node_export_public_key_der, op_node_export_public_key_jwk, @@ -791,7 +792,7 @@ export class PrivateKeyObject extends AsymmetricKeyObject { export(options: JwkKeyExportOptions | KeyExportOptions) { if (options && options.format === "jwk") { - notImplemented("jwk private key export not implemented"); + return op_node_export_private_key_jwk(this[kHandle]); } const { format, diff --git a/tests/unit_node/crypto/crypto_key_test.ts b/tests/unit_node/crypto/crypto_key_test.ts index 5d206acc723fde..82306d02fef716 100644 --- a/tests/unit_node/crypto/crypto_key_test.ts +++ b/tests/unit_node/crypto/crypto_key_test.ts @@ -700,3 +700,19 @@ Deno.test("generateKeyPair promisify", async () => { assert(publicKey.startsWith("-----BEGIN PUBLIC KEY-----")); assert(privateKey.startsWith("-----BEGIN PRIVATE KEY-----")); }); + +Deno.test("RSA export private JWK", function () { + // @ts-ignore @types/node broken + const { privateKey, publicKey } = generateKeyPairSync("rsa", { + modulusLength: 4096, + publicKeyEncoding: { + format: "jwk", + }, + privateKeyEncoding: { + format: "jwk", + }, + }); + + assertEquals((privateKey as any).kty, "RSA"); + assertEquals((privateKey as any).n, (publicKey as any).n); +});