diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 1023fced39..bf7db14751 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -242,6 +242,7 @@ deno_core::extension!(deno_node, 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, + ops::crypto::keys::op_node_export_public_key_jwk, ops::crypto::keys::op_node_export_secret_key_b64url, ops::crypto::keys::op_node_export_secret_key, ops::crypto::keys::op_node_generate_dh_group_key_async, diff --git a/ext/node/ops/crypto/keys.rs b/ext/node/ops/crypto/keys.rs index eccd08564e..7334fb8eba 100644 --- a/ext/node/ops/crypto/keys.rs +++ b/ext/node/ops/crypto/keys.rs @@ -235,6 +235,16 @@ impl RsaPssPrivateKey { } } +impl EcPublicKey { + pub fn to_jwk(&self) -> Result { + match self { + EcPublicKey::P224(_) => Err(type_error("Unsupported JWK EC curve: P224")), + EcPublicKey::P256(key) => Ok(key.to_jwk()), + EcPublicKey::P384(key) => Ok(key.to_jwk()), + } + } +} + impl EcPrivateKey { /// Derives the public key from the private key. pub fn to_public_key(&self) -> EcPublicKey { @@ -848,7 +858,63 @@ fn parse_rsa_pss_params( Ok(details) } +use base64::prelude::BASE64_URL_SAFE_NO_PAD; + +fn bytes_to_b64(bytes: &[u8]) -> String { + BASE64_URL_SAFE_NO_PAD.encode(bytes) +} + impl AsymmetricPublicKey { + fn export_jwk(&self) -> Result { + match self { + AsymmetricPublicKey::Ec(key) => { + let jwk = key.to_jwk()?; + Ok(deno_core::serde_json::json!(jwk)) + } + AsymmetricPublicKey::X25519(key) => { + let bytes = key.as_bytes(); + let jwk = deno_core::serde_json::json!({ + "kty": "OKP", + "crv": "X25519", + "x": bytes_to_b64(bytes), + }); + Ok(jwk) + } + AsymmetricPublicKey::Ed25519(key) => { + let bytes = key.to_bytes(); + let jwk = deno_core::serde_json::json!({ + "kty": "OKP", + "crv": "Ed25519", + "x": bytes_to_b64(&bytes), + }); + Ok(jwk) + } + AsymmetricPublicKey::Rsa(key) => { + let n = key.n(); + let e = key.e(); + + let jwk = deno_core::serde_json::json!({ + "kty": "RSA", + "n": bytes_to_b64(&n.to_bytes_be()), + "e": bytes_to_b64(&e.to_bytes_be()), + }); + Ok(jwk) + } + AsymmetricPublicKey::RsaPss(key) => { + let n = key.key.n(); + let e = key.key.e(); + + let jwk = deno_core::serde_json::json!({ + "kty": "RSA", + "n": bytes_to_b64(&n.to_bytes_be()), + "e": bytes_to_b64(&e.to_bytes_be()), + }); + Ok(jwk) + } + _ => Err(type_error("jwk export not implemented for this key type")), + } + } + fn export_der(&self, typ: &str) -> Result, AnyError> { match typ { "pkcs1" => match self { @@ -1848,6 +1914,18 @@ pub fn op_node_export_secret_key_b64url( Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(key)) } +#[op2] +#[serde] +pub fn op_node_export_public_key_jwk( + #[cppgc] handle: &KeyObjectHandle, +) -> Result { + let public_key = handle + .as_public_key() + .ok_or_else(|| type_error("key is not an asymmetric public key"))?; + + public_key.export_jwk() +} + #[op2] #[string] pub fn op_node_export_public_key_pem( diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index c2e9d95ee0..49a618b65b 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -21,6 +21,7 @@ import { op_node_export_private_key_der, op_node_export_private_key_pem, op_node_export_public_key_der, + op_node_export_public_key_jwk, op_node_export_public_key_pem, op_node_export_secret_key, op_node_export_secret_key_b64url, @@ -786,8 +787,9 @@ export class PublicKeyObject extends AsymmetricKeyObject { export(options: JwkKeyExportOptions | KeyExportOptions) { if (options && options.format === "jwk") { - notImplemented("jwk public key export not implemented"); + return op_node_export_public_key_jwk(this[kHandle]); } + const { format, type, diff --git a/tests/unit_node/crypto/crypto_key_test.ts b/tests/unit_node/crypto/crypto_key_test.ts index dba9ba0626..5dfab3ca42 100644 --- a/tests/unit_node/crypto/crypto_key_test.ts +++ b/tests/unit_node/crypto/crypto_key_test.ts @@ -498,6 +498,61 @@ MC4CAQAwBQYDK2VwBCIEIJ1hsZ3v/VpguoRK9JLsLMREScVpezJpGXA7rAMcrn9g assertEquals(pkcs8Actual, pkcs8Expected); }); +Deno.test("RSA export public JWK", async function () { + const importKey = "-----BEGIN PUBLIC KEY-----\n" + + "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAqF66soiDvuqUB7ufWtuV\n" + + "5a1nZIw90m9qHEl2MeNt66HeEjG2GeHDfF5a4uplutnAh3dwpFweHqGIyB16POTI\n" + + "YysJ/rMPKoWZFQ1LEcr23rSgmL49YpifDetl5V/UR+zEygL3UzzZmbdjuyZz+Sjt\n" + + "FY+SAoZ9XPCqIaNha9uVFcurW44MvAkhzQR/yy5NWPaJ/yv4oI/exvuZnUwwBHvH\n" + + "gwVchfr7Jh5LRmYTPeyuI1lUOovVzE+0Ty/2tFfrm2hpedqYXvEuVu+yJzfuNoLf\n" + + "TGfz15J76eoRdFTCTdaG/MQnrzxZnIlmIpdpTPl0xVOwjKRpeYK06GS7EAa7cS9D\n" + + "dnsHkF/Mr9Yys5jw/49fXqh9BH3Iy0p5YmeQIMep04CUDFj7MZ+3SK8b0mA4SscH\n" + + "dIraZZynLZ1crM0ECAJBldM4TKqIDACYGU7XyRV+419cPJvYybHys5m7thS3QI7E\n" + + "LTpMV+WoYtZ5xeBCm7z5i3iPY6eSh2JtTu6oa3ALwwnXPAaZqDIFer8SoQNyVb0v\n" + + "EU8bVDeGXm1ha5gcC5KxqqnadO/WDD6Jke79Ji04sBEKTTodSOARyTGpGFEcC3Nn\n" + + "xSSScGCxMrGJuTDtnz+Eh6l6ysT+Nei9ZRMxNu8sZKAR43XkVXxF/OdSCbftFOAs\n" + + "wyPJtyhQALGPcK5cWPQS2sUCAwEAAQ==\n" + + "-----END PUBLIC KEY-----\n"; + const publicKey = createPublicKey(importKey); + + const jwk = publicKey.export({ format: "jwk" }); + assertEquals(jwk, { + kty: "RSA", + n: "qF66soiDvuqUB7ufWtuV5a1nZIw90m9qHEl2MeNt66HeEjG2GeHDfF5a4uplutnAh3dwpFweHqGIyB16POTIYysJ_rMPKoWZFQ1LEcr23rSgmL49YpifDetl5V_UR-zEygL3UzzZmbdjuyZz-SjtFY-SAoZ9XPCqIaNha9uVFcurW44MvAkhzQR_yy5NWPaJ_yv4oI_exvuZnUwwBHvHgwVchfr7Jh5LRmYTPeyuI1lUOovVzE-0Ty_2tFfrm2hpedqYXvEuVu-yJzfuNoLfTGfz15J76eoRdFTCTdaG_MQnrzxZnIlmIpdpTPl0xVOwjKRpeYK06GS7EAa7cS9DdnsHkF_Mr9Yys5jw_49fXqh9BH3Iy0p5YmeQIMep04CUDFj7MZ-3SK8b0mA4SscHdIraZZynLZ1crM0ECAJBldM4TKqIDACYGU7XyRV-419cPJvYybHys5m7thS3QI7ELTpMV-WoYtZ5xeBCm7z5i3iPY6eSh2JtTu6oa3ALwwnXPAaZqDIFer8SoQNyVb0vEU8bVDeGXm1ha5gcC5KxqqnadO_WDD6Jke79Ji04sBEKTTodSOARyTGpGFEcC3NnxSSScGCxMrGJuTDtnz-Eh6l6ysT-Nei9ZRMxNu8sZKAR43XkVXxF_OdSCbftFOAswyPJtyhQALGPcK5cWPQS2sU", + e: "AQAB", + }); +}); + +Deno.test("EC export public jwk", async function () { + const key = "-----BEGIN PUBLIC KEY-----\n" + + "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEVEEIrFEZ+40Pk90LtKBQ3r7FGAPl\n" + + "v4bvX9grC8bNiNiVAcyEKs+QZKQj/0/CUPJV10AmavrUoPk/7Wy0sejopQ==\n" + + "-----END PUBLIC KEY-----\n"; + const publicKey = createPublicKey(key); + + const jwk = publicKey.export({ format: "jwk" }); + assertEquals(jwk, { + kty: "EC", + x: "VEEIrFEZ-40Pk90LtKBQ3r7FGAPlv4bvX9grC8bNiNg", + y: "lQHMhCrPkGSkI_9PwlDyVddAJmr61KD5P-1stLHo6KU", + crv: "P-256", + }); +}); + +Deno.test("Ed25519 export public jwk", async function () { + const key = "-----BEGIN PUBLIC KEY-----\n" + + "MCowBQYDK2VwAyEAKCVFOD6Le61XM7HbN/MB/N06mX5bti2p50qjLvT1mzE=\n" + + "-----END PUBLIC KEY-----\n"; + const publicKey = createPublicKey(key); + + const jwk = publicKey.export({ format: "jwk" }); + assertEquals(jwk, { + crv: "Ed25519", + x: "KCVFOD6Le61XM7HbN_MB_N06mX5bti2p50qjLvT1mzE", + kty: "OKP", + }); +}); + Deno.test("EC import jwk public key", function () { const publicKey = createPublicKey({ key: {