From 567b4967a9f6271395f9e24183ae6f64f2a1cb9f Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Wed, 28 Aug 2024 19:54:49 +0530 Subject: [PATCH] fix(ext/node): import EC JWK keys (#25266) --- Cargo.lock | 14 ++++++ Cargo.toml | 6 +-- ext/node/lib.rs | 1 + ext/node/ops/crypto/keys.rs | 40 +++++++++++++++++ ext/node/polyfills/internal/crypto/keys.ts | 11 ++++- tests/unit_node/crypto/crypto_key_test.ts | 51 ++++++++++++++++++++-- 6 files changed, 116 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 330e08638e..aad78e21bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2694,6 +2694,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", + "base64ct", "crypto-bigint", "digest", "ff", @@ -2704,6 +2705,8 @@ dependencies = [ "pkcs8", "rand_core", "sec1", + "serde_json", + "serdect", "subtle", "zeroize", ] @@ -6169,6 +6172,7 @@ dependencies = [ "der", "generic-array", "pkcs8", + "serdect", "subtle", "zeroize", ] @@ -6314,6 +6318,16 @@ dependencies = [ "v8", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/Cargo.toml b/Cargo.toml index 6e250bc926..d3bdf14e4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -106,7 +106,7 @@ deno_cache_dir = "=0.10.2" deno_package_json = { version = "=0.1.1", default-features = false } dlopen2 = "0.6.1" ecb = "=0.1.2" -elliptic-curve = { version = "0.13.4", features = ["alloc", "arithmetic", "ecdh", "std", "pem"] } +elliptic-curve = { version = "0.13.4", features = ["alloc", "arithmetic", "ecdh", "std", "pem", "jwk"] } encoding_rs = "=0.8.33" fast-socks5 = "0.9.6" faster-hex = "0.9" @@ -141,8 +141,8 @@ num-bigint = { version = "0.4", features = ["rand"] } once_cell = "1.17.1" os_pipe = { version = "=1.1.5", features = ["io_safety"] } p224 = { version = "0.13.0", features = ["ecdh"] } -p256 = { version = "0.13.2", features = ["ecdh"] } -p384 = { version = "0.13.0", features = ["ecdh"] } +p256 = { version = "0.13.2", features = ["ecdh", "jwk"] } +p384 = { version = "0.13.0", features = ["ecdh", "jwk"] } parking_lot = "0.12.0" percent-encoding = "2.3.0" phf = { version = "0.11", features = ["macros"] } diff --git a/ext/node/lib.rs b/ext/node/lib.rs index f2c1576cf5..1023fced39 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -233,6 +233,7 @@ deno_core::extension!(deno_node, ops::crypto::op_node_verify_ed25519, ops::crypto::keys::op_node_create_private_key, ops::crypto::keys::op_node_create_ed_raw, + ops::crypto::keys::op_node_create_ec_jwk, ops::crypto::keys::op_node_create_public_key, ops::crypto::keys::op_node_create_secret_key, ops::crypto::keys::op_node_derive_public_key_from_private_key, diff --git a/ext/node/ops/crypto/keys.rs b/ext/node/ops/crypto/keys.rs index 7d7ec140e7..eccd08564e 100644 --- a/ext/node/ops/crypto/keys.rs +++ b/ext/node/ops/crypto/keys.rs @@ -13,6 +13,7 @@ use deno_core::unsync::spawn_blocking; use deno_core::GarbageCollected; use deno_core::ToJsBuffer; use ed25519_dalek::pkcs8::BitStringRef; +use elliptic_curve::JwkEcKey; use num_bigint::BigInt; use num_traits::FromPrimitive as _; use pkcs8::DecodePrivateKey as _; @@ -571,6 +572,36 @@ impl KeyObjectHandle { Ok(KeyObjectHandle::AsymmetricPublic(key)) } + pub fn new_ec_jwk( + jwk: &JwkEcKey, + is_public: bool, + ) -> Result { + // https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.1 + let handle = match jwk.crv() { + "P-256" if is_public => { + KeyObjectHandle::AsymmetricPublic(AsymmetricPublicKey::Ec( + EcPublicKey::P256(p256::PublicKey::from_jwk(jwk)?), + )) + } + "P-256" => KeyObjectHandle::AsymmetricPrivate(AsymmetricPrivateKey::Ec( + EcPrivateKey::P256(p256::SecretKey::from_jwk(jwk)?), + )), + "P-384" if is_public => { + KeyObjectHandle::AsymmetricPublic(AsymmetricPublicKey::Ec( + EcPublicKey::P384(p384::PublicKey::from_jwk(jwk)?), + )) + } + "P-384" => KeyObjectHandle::AsymmetricPrivate(AsymmetricPrivateKey::Ec( + EcPrivateKey::P384(p384::SecretKey::from_jwk(jwk)?), + )), + _ => { + return Err(type_error(format!("unsupported curve: {}", jwk.crv()))); + } + }; + + Ok(handle) + } + pub fn new_ed_raw( curve: &str, data: &[u8], @@ -1081,6 +1112,15 @@ pub fn op_node_create_ed_raw( KeyObjectHandle::new_ed_raw(curve, key, is_public) } +#[op2] +#[cppgc] +pub fn op_node_create_ec_jwk( + #[serde] jwk: elliptic_curve::JwkEcKey, + is_public: bool, +) -> Result { + KeyObjectHandle::new_ec_jwk(&jwk, is_public) +} + #[op2] #[cppgc] pub fn op_node_create_public_key( diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index 97e565023a..c2e9d95ee0 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -12,6 +12,7 @@ const { } = primordials; import { + op_node_create_ec_jwk, op_node_create_ed_raw, op_node_create_private_key, op_node_create_public_key, @@ -311,7 +312,15 @@ function getKeyObjectHandleFromJwk(key, ctx) { } if (key.kty === "EC") { - throw new TypeError("ec jwk imports not implemented"); + validateString(key.crv, "key.crv"); + validateString(key.x, "key.x"); + validateString(key.y, "key.y"); + + if (!isPublic) { + validateString(key.d, "key.d"); + } + + return op_node_create_ec_jwk(key, isPublic); } throw new TypeError("rsa jwk imports not implemented"); diff --git a/tests/unit_node/crypto/crypto_key_test.ts b/tests/unit_node/crypto/crypto_key_test.ts index 6c2c9f8519..dba9ba0626 100644 --- a/tests/unit_node/crypto/crypto_key_test.ts +++ b/tests/unit_node/crypto/crypto_key_test.ts @@ -440,7 +440,7 @@ Deno.test("create private key with invalid utf-8 string", function () { ); }); -Deno.test("Ed25519 jwk public key #1", function () { +Deno.test("Ed25519 import jwk public key #1", function () { const key = { "kty": "OKP", "crv": "Ed25519", @@ -460,7 +460,7 @@ MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo= assertEquals(spkiActual, spkiExpected); }); -Deno.test("Ed25519 jwk public key #2", function () { +Deno.test("Ed25519 import jwk public key #2", function () { const key = { "kty": "OKP", "crv": "Ed25519", @@ -478,7 +478,7 @@ MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo= assertEquals(spki, spkiExpected); }); -Deno.test("Ed25519 jwk private key", function () { +Deno.test("Ed25519 import jwk private key", function () { const key = { "kty": "OKP", "crv": "Ed25519", @@ -497,3 +497,48 @@ MC4CAQAwBQYDK2VwBCIEIJ1hsZ3v/VpguoRK9JLsLMREScVpezJpGXA7rAMcrn9g assertEquals(pkcs8Actual, pkcs8Expected); }); + +Deno.test("EC import jwk public key", function () { + const publicKey = createPublicKey({ + key: { + kty: "EC", + x: "_GGuz19zab5J70zyiUK6sAM5mHqUbsY8H6U2TnVlt-k", + y: "TcZG5efXZDIhNGDp6XuujoJqOEJU2D2ckjG9nOnSPIQ", + crv: "P-256", + }, + format: "jwk", + }); + + const publicSpki = publicKey.export({ type: "spki", format: "pem" }); + const spkiExpected = `-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE/GGuz19zab5J70zyiUK6sAM5mHqU +bsY8H6U2TnVlt+lNxkbl59dkMiE0YOnpe66Ogmo4QlTYPZySMb2c6dI8hA== +-----END PUBLIC KEY----- +`; + + assertEquals(publicSpki, spkiExpected); +}); + +Deno.test("EC import jwk private key", function () { + const privateKey = createPrivateKey({ + key: { + kty: "EC", + x: "_GGuz19zab5J70zyiUK6sAM5mHqUbsY8H6U2TnVlt-k", + y: "TcZG5efXZDIhNGDp6XuujoJqOEJU2D2ckjG9nOnSPIQ", + crv: "P-256", + d: "Wobjne0GqlB_1NynKu19rsw7zBHa94tKcWIxwIb88m8", + }, + format: "jwk", + }); + + const privatePkcs8 = privateKey.export({ type: "pkcs8", format: "pem" }); + + const pkcs8Expected = `-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgWobjne0GqlB/1Nyn +Ku19rsw7zBHa94tKcWIxwIb88m+hRANCAAT8Ya7PX3NpvknvTPKJQrqwAzmYepRu +xjwfpTZOdWW36U3GRuXn12QyITRg6el7ro6CajhCVNg9nJIxvZzp0jyE +-----END PRIVATE KEY----- +`; + + assertEquals(privatePkcs8, pkcs8Expected); +});