From 5cfa03ceca396b1c21a826cb44a984329cf35078 Mon Sep 17 00:00:00 2001 From: Asher Gomez Date: Wed, 13 Mar 2024 19:17:23 +1100 Subject: [PATCH] fix(ext/node): initial `crypto.createPublicKey()` support (#22509) Closes #21807 Co-authored-by: Divy Srivastava --- Cargo.lock | 1 + Cargo.toml | 1 + ext/crypto/Cargo.toml | 2 +- ext/node/Cargo.toml | 1 + ext/node/lib.rs | 1 + ext/node/ops/crypto/mod.rs | 108 ++++++++++++++++++ ext/node/polyfills/internal/crypto/keys.ts | 24 +++- tests/unit_node/crypto/crypto_key_test.ts | 28 ++++- .../testdata/ec_prime256v1_public.pem | 4 + 9 files changed, 164 insertions(+), 6 deletions(-) create mode 100644 tests/unit_node/testdata/ec_prime256v1_public.pem diff --git a/Cargo.lock b/Cargo.lock index 001649627e..bedb88752b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1687,6 +1687,7 @@ dependencies = [ "sha2", "signature", "simd-json", + "spki", "tokio", "typenum", "url", diff --git a/Cargo.toml b/Cargo.toml index 0562fd919f..38f04c5ae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -163,6 +163,7 @@ signature = "2.1" slab = "0.4" smallvec = "1.8" socket2 = { version = "0.5.3", features = ["all"] } +spki = "0.7.2" tar = "=0.4.40" tempfile = "3.4.0" termcolor = "1.1.3" diff --git a/ext/crypto/Cargo.toml b/ext/crypto/Cargo.toml index 89447cb8af..7369e0a845 100644 --- a/ext/crypto/Cargo.toml +++ b/ext/crypto/Cargo.toml @@ -39,7 +39,7 @@ serde_bytes.workspace = true sha1 = { version = "0.10.6", features = ["oid"] } sha2.workspace = true signature.workspace = true -spki = "0.7.2" +spki.workspace = true tokio.workspace = true uuid.workspace = true x25519-dalek = "2.0.0" diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 053286053a..8e3e695d61 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -68,6 +68,7 @@ sha-1 = "0.10.0" sha2.workspace = true signature.workspace = true simd-json = "0.13.4" +spki.workspace = true tokio.workspace = true typenum = "1.15.0" url.workspace = true diff --git a/ext/node/lib.rs b/ext/node/lib.rs index f9553a0383..f8c9dfc883 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -329,6 +329,7 @@ deno_core::extension!(deno_node, ops::require::op_require_break_on_next_statement, ops::util::op_node_guess_handle_type, ops::crypto::op_node_create_private_key, + ops::crypto::op_node_create_public_key, ops::ipc::op_node_child_ipc_pipe, ops::ipc::op_node_ipc_write, ops::ipc::op_node_ipc_read, diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index 8db562eef1..c2a9b1ab79 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -20,6 +20,7 @@ use rand::distributions::Uniform; use rand::thread_rng; use rand::Rng; use rsa::pkcs1::DecodeRsaPrivateKey; +use rsa::pkcs1::DecodeRsaPublicKey; use rsa::pkcs8; use rsa::pkcs8::der::asn1; use rsa::pkcs8::der::Decode; @@ -1459,3 +1460,110 @@ pub fn op_node_create_private_key( _ => Err(type_error("Unsupported algorithm")), } } + +fn parse_public_key( + key: &[u8], + format: &str, + type_: &str, +) -> Result { + match format { + "pem" => { + let (label, doc) = + pkcs8::Document::from_pem(std::str::from_utf8(key).unwrap())?; + if label != "PUBLIC KEY" { + return Err(type_error("Invalid PEM label")); + } + Ok(doc) + } + "der" => { + match type_ { + "pkcs1" => pkcs8::Document::from_pkcs1_der(key) + .map_err(|_| type_error("Invalid PKCS1 public key")), + // TODO(@iuioiua): spki type + _ => Err(type_error(format!("Unsupported key type: {}", type_))), + } + } + _ => Err(type_error(format!("Unsupported key format: {}", format))), + } +} + +#[op2] +#[serde] +pub fn op_node_create_public_key( + #[buffer] key: &[u8], + #[string] format: &str, + #[string] type_: &str, +) -> Result { + let doc = parse_public_key(key, format, type_)?; + let pk_info = spki::SubjectPublicKeyInfoRef::try_from(doc.as_bytes())?; + + let alg = pk_info.algorithm.oid; + + match alg { + RSA_ENCRYPTION_OID => { + let public_key = rsa::pkcs1::RsaPublicKey::from_der( + pk_info.subject_public_key.raw_bytes(), + )?; + let modulus_length = public_key.modulus.as_bytes().len() * 8; + + Ok(AsymmetricKeyDetails::Rsa { + modulus_length, + public_exponent: BigInt::from_bytes_be( + num_bigint::Sign::Plus, + public_key.public_exponent.as_bytes(), + ) + .into(), + }) + } + RSASSA_PSS_OID => { + let params = PssPrivateKeyParameters::try_from( + pk_info + .algorithm + .parameters + .ok_or_else(|| type_error("Malformed parameters".to_string()))?, + ) + .map_err(|_| type_error("Malformed parameters".to_string()))?; + + let hash_alg = params.hash_algorithm; + let hash_algorithm = match hash_alg.oid { + ID_SHA1_OID => "sha1", + ID_SHA256_OID => "sha256", + ID_SHA384_OID => "sha384", + ID_SHA512_OID => "sha512", + _ => return Err(type_error("Unsupported hash algorithm")), + }; + + let public_key = rsa::pkcs1::RsaPublicKey::from_der( + pk_info.subject_public_key.raw_bytes(), + )?; + let modulus_length = public_key.modulus.as_bytes().len() * 8; + Ok(AsymmetricKeyDetails::RsaPss { + modulus_length, + public_exponent: BigInt::from_bytes_be( + num_bigint::Sign::Plus, + public_key.public_exponent.as_bytes(), + ) + .into(), + hash_algorithm: hash_algorithm.to_string(), + salt_length: params.salt_length, + }) + } + EC_OID => { + let named_curve = pk_info + .algorithm + .parameters_oid() + .map_err(|_| type_error("malformed parameters"))?; + let named_curve = match named_curve { + ID_SECP256R1_OID => "p256", + ID_SECP384R1_OID => "p384", + ID_SECP521R1_OID => "p521", + _ => return Err(type_error("Unsupported named curve")), + }; + + Ok(AsymmetricKeyDetails::Ec { + named_curve: named_curve.to_string(), + }) + } + _ => Err(type_error("Unsupported algorithm")), + } +} diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index ab753582f7..33034d824c 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -4,7 +4,10 @@ // TODO(petamoriken): enable prefer-primordials for node polyfills // deno-lint-ignore-file prefer-primordials -import { op_node_create_private_key } from "ext:core/ops"; +import { + op_node_create_private_key, + op_node_create_public_key, +} from "ext:core/ops"; import { kHandle, @@ -239,9 +242,12 @@ export function createPrivateKey( } export function createPublicKey( - _key: PublicKeyInput | string | Buffer | KeyObject | JsonWebKeyInput, -): KeyObject { - notImplemented("crypto.createPublicKey"); + key: PublicKeyInput | string | Buffer | JsonWebKeyInput, +): PublicKeyObject { + const { data, format, type } = prepareAsymmetricKey(key); + const details = op_node_create_public_key(data, format, type); + const handle = setOwnedKey(copyBuffer(data)); + return new PublicKeyObject(handle, details); } function getKeyTypes(allowKeyObject: boolean, bufferOnly = false) { @@ -358,6 +364,16 @@ class PrivateKeyObject extends AsymmetricKeyObject { } } +class PublicKeyObject extends AsymmetricKeyObject { + constructor(handle: unknown, details: unknown) { + super("public", handle, details); + } + + export(_options: unknown) { + notImplemented("crypto.PublicKeyObject.prototype.export"); + } +} + export function setOwnedKey(key: Uint8Array): unknown { const handle = {}; KEY_STORE.set(handle, key); diff --git a/tests/unit_node/crypto/crypto_key_test.ts b/tests/unit_node/crypto/crypto_key_test.ts index 6dae793369..9b8224cd19 100644 --- a/tests/unit_node/crypto/crypto_key_test.ts +++ b/tests/unit_node/crypto/crypto_key_test.ts @@ -2,7 +2,9 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. import { + createHmac, createPrivateKey, + createPublicKey, createSecretKey, generateKeyPair, generateKeyPairSync, @@ -12,7 +14,6 @@ import { import { promisify } from "node:util"; import { Buffer } from "node:buffer"; import { assertEquals, assertThrows } from "@std/assert/mod.ts"; -import { createHmac } from "node:crypto"; const RUN_SLOW_TESTS = Deno.env.get("SLOW_TESTS") === "1"; @@ -240,3 +241,28 @@ Deno.test("createPrivateKey ec", function () { assertEquals(key.asymmetricKeyType, "ec"); assertEquals(key.asymmetricKeyDetails?.namedCurve, "p256"); }); + +const rsaPublicKey = Deno.readTextFileSync( + new URL("../testdata/rsa_public.pem", import.meta.url), +); + +Deno.test("createPublicKey() RSA", () => { + const key = createPublicKey(rsaPublicKey); + assertEquals(key.type, "public"); + assertEquals(key.asymmetricKeyType, "rsa"); + assertEquals(key.asymmetricKeyDetails?.modulusLength, 2048); + assertEquals(key.asymmetricKeyDetails?.publicExponent, 65537n); +}); + +// openssl ecparam -name prime256v1 -genkey -noout -out a.pem +// openssl ec -in a.pem -pubout -out b.pem +const ecPublicKey = Deno.readTextFileSync( + new URL("../testdata/ec_prime256v1_public.pem", import.meta.url), +); + +Deno.test("createPublicKey() EC", function () { + const key = createPublicKey(ecPublicKey); + assertEquals(key.type, "public"); + assertEquals(key.asymmetricKeyType, "ec"); + assertEquals(key.asymmetricKeyDetails?.namedCurve, "p256"); +}); diff --git a/tests/unit_node/testdata/ec_prime256v1_public.pem b/tests/unit_node/testdata/ec_prime256v1_public.pem new file mode 100644 index 0000000000..0b8b66dbed --- /dev/null +++ b/tests/unit_node/testdata/ec_prime256v1_public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEvk2xDvFKR/q/jqE5pjFk0afU5Ybe +83GsRx0PBXXFVE4yO1vE7ftaOp9Jqt3edpVyXIEyyrilnonNKITGxkB2Uw== +-----END PUBLIC KEY-----