From 32438d25c337f9160c1c90d48680963654385e22 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Sun, 3 Dec 2023 09:58:13 +0530 Subject: [PATCH] fix(ext/node): sign with PEM private keys (#21287) Add support for signing with a RSA PEM private key: `pkcs8` and `pkcs1`. Fixes https://github.com/denoland/deno/issues/18972 Ref #21124 Verified fix with `npm:sshpk`. Unverfied but fixes `npm:google-auth-library`, `npm:web-push` & `oracle/oci-typescript-sdk` --------- Signed-off-by: Divy Srivastava --- .../unit_node/crypto/crypto_sign_test.ts | 46 +++++++++++++++++- .../unit_node/testdata/rsa_private_pkcs1.pem | 27 +++++++++++ ext/node/ops/crypto/mod.rs | 47 ++++++++++--------- ext/node/polyfills/internal/crypto/cipher.ts | 3 +- ext/node/polyfills/internal/crypto/keys.ts | 2 +- ext/node/polyfills/internal/crypto/sig.ts | 23 ++------- 6 files changed, 106 insertions(+), 42 deletions(-) create mode 100644 cli/tests/unit_node/testdata/rsa_private_pkcs1.pem diff --git a/cli/tests/unit_node/crypto/crypto_sign_test.ts b/cli/tests/unit_node/crypto/crypto_sign_test.ts index 58107b563a..b04cae4078 100644 --- a/cli/tests/unit_node/crypto/crypto_sign_test.ts +++ b/cli/tests/unit_node/crypto/crypto_sign_test.ts @@ -1,6 +1,9 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -import { assert, assertEquals } from "../../../../test_util/std/assert/mod.ts"; +import { + assert, + assertEquals, +} from "../../../../test_util/std/testing/asserts.ts"; import { createSign, createVerify, sign, verify } from "node:crypto"; import { Buffer } from "node:buffer"; @@ -9,6 +12,11 @@ const rsaPrivatePem = Buffer.from( new URL("../testdata/rsa_private.pem", import.meta.url), ), ); +const rsaPrivatePkcs1Pem = Buffer.from( + await Deno.readFile( + new URL("../testdata/rsa_private_pkcs1.pem", import.meta.url), + ), +); const rsaPublicPem = Buffer.from( await Deno.readFile( new URL("../testdata/rsa_public.pem", import.meta.url), @@ -86,3 +94,39 @@ Deno.test({ } }, }); + +Deno.test({ + name: "crypto.createPrivateKey|sign - RSA PEM", + fn() { + for (const testCase of table) { + for (const algorithm of testCase.algorithms) { + assertEquals( + createSign(algorithm).update(data).sign(rsaPrivatePem, "hex"), + testCase.signature, + ); + assertEquals( + sign(algorithm, data, rsaPrivatePem), + Buffer.from(testCase.signature, "hex"), + ); + } + } + }, +}); + +Deno.test({ + name: "crypto.createPrivateKey|sign - RSA PKCS1 PEM", + fn() { + for (const testCase of table) { + for (const algorithm of testCase.algorithms) { + assertEquals( + createSign(algorithm).update(data).sign(rsaPrivatePkcs1Pem, "hex"), + testCase.signature, + ); + assertEquals( + sign(algorithm, data, rsaPrivatePkcs1Pem), + Buffer.from(testCase.signature, "hex"), + ); + } + } + }, +}); diff --git a/cli/tests/unit_node/testdata/rsa_private_pkcs1.pem b/cli/tests/unit_node/testdata/rsa_private_pkcs1.pem new file mode 100644 index 0000000000..215e5cc513 --- /dev/null +++ b/cli/tests/unit_node/testdata/rsa_private_pkcs1.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAt9xYiIonscC3vz/A2ceR7KhZZlDu/5bye53nCVTcKnWd2seY +6UAdKersX6njr83Dd5OVe1BW/wJvp5EjWTAGYbFswlNmeD44edEGM939B6Lq+/8i +BkrTi8mGN4YCytivE24YI0D4XZMPfkLSpab2y/Hy4DjQKBq1ThZ0UBnK+9IhX37J +u/ZoGYSlTIGIhzyaiYBh7wrZBoPczIEu6et/kN2VnnbRUtkYTF97ggcv5h+hDpUQ +jQW0ZgOMcTc8n+RkGpIt0/iM/bTjI3Tz/gsFdi6hHcpZgbopPL630296iByyigQC +PJVzdusFrQN5DeC+zT/nGypQkZanLb4ZspSx9QIDAQABAoIBAQCS2erYu8gyoGPi +3E/zYgQ6ishFAZWzDWSFubwD5wSm4SSAzvViL/RbO6kqS25xR569DmLRiHzD17VI +mJMsNECUnPrqR2TL256OJZaXrNHh3I1lUwVhEzjeKMsL4/ys+d70XPXoiocVblVs +moDXEIGEqa48ywPvVE3Fngeuxrsq3/GCVBNiwtt0YjAOZxmKEh31UZdHO+YI+wNF +/Z8KQCPscN5HGlR0SIQOlqMANz49aKStrevdvjS1UcpabzDEkuK84g3saJhcpAhb +pGFmAf5GTjkkhE0rE1qDF15dSqrKGfCFtOjUeK17SIEN7E322ChmTReZ1hYGfoSV +cdFntUINAoGBAPFKL5QeJ6wZu8R/ru11wTG6sQA0Jub2hGccPXpbnPrT+3CACOLI +JTCLy/xTKW3dqRHj/wZEe+jUw88w7jwGb1BkWr4BI8tDvY9jQLP1jyuLWRfrxXbp +4Z0oeBBwBeCI/ZG7FIvdDTqWxn1aj3Tmh6s4ByqEdtwrrrJPcBUNl01fAoGBAMMR +3RGE/ca6X6xz6kgUD6TtHVhiiRJK1jm/u+q0n7i/MBkeDgTZkHYS7lPc0yIdtqaI +Plz5yzwHnAvuMrv8LSdkjwioig2yQa3tAij8kXxqs7wN5418DMV2s1OJBrPthYPs +bv4im2iI8V63JQS4ZMYQbckq8ABYccTpOnxXDy0rAoGBAKkvzHa+QjERhjB9GyoT +1FhLQIsVBmYSWrp1+cGO9V6HPxoeHJzvm+wTSf/uS/FmaINL6+j4Ii4a6gWgmJts +I6cqBtqNsAx5vjQJczf8KdxthBYa0sXTrsfktXNJKUXMqIgDtp9vazQ2vozs8AQX +FPAAhD3SzgkJdCBBRSTt97ZfAoGAWAziKpxLKL7LnL4dzDcx8JIPIuwnTxh0plCD +dCffyLaT8WJ9lXbXHFTjOvt8WfPrlDP/Ylxmfkw5BbGZOP1VLGjZn2DkH9aMiwNm +bDXFPdG0G3hzQovx/9fajiRV4DWghLHeT9wzJfZabRRiI0VQR472300AVEeX4vgb +rDBn600CgYEAk7czBCT9rHn/PNwCa17hlTy88C4vXkwbz83Oa+aX5L4e5gw5lhcR +2ZuZHLb2r6oMt9rlD7EIDItSs+u21LOXWPTAlazdnpYUyw/CzogM/PN+qNwMRXn5 +uXFFhmlP2mVg2EdELTahXch8kWqHaCSX53yvqCtRKu/j76V31TfQZGM= +-----END RSA PRIVATE KEY----- diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index 4aaa3f494b..97c3d76aac 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -19,6 +19,7 @@ use rand::distributions::Distribution; use rand::distributions::Uniform; use rand::thread_rng; use rand::Rng; +use rsa::pkcs1::DecodeRsaPrivateKey; use rsa::pkcs8; use rsa::pkcs8::der::asn1; use rsa::pkcs8::der::Decode; @@ -363,23 +364,32 @@ pub fn op_node_sign( #[buffer] digest: &[u8], #[string] digest_type: &str, #[serde] key: StringOrBuffer, - #[string] key_type: &str, - #[string] key_format: &str, + #[string] _type: &str, + #[string] format: &str, ) -> Result { - match key_type { - "rsa" => { + let (label, doc) = + pkcs8::SecretDocument::from_pem(std::str::from_utf8(&key).unwrap())?; + + let oid; + let pkey = match format { + "pem" => { + if 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" { + oid = RSA_ENCRYPTION_OID; + doc.as_bytes() + } else { + return Err(type_error("Invalid PEM label")); + } + } + _ => return Err(type_error("Unsupported key format")), + }; + match oid { + RSA_ENCRYPTION_OID => { use rsa::pkcs1v15::SigningKey; - let key = match key_format { - "pem" => RsaPrivateKey::from_pkcs8_pem((&key).try_into()?) - .map_err(|_| type_error("Invalid RSA private key"))?, - // TODO(kt3k): Support der and jwk formats - _ => { - return Err(type_error(format!( - "Unsupported key format: {}", - key_format - ))) - } - }; + let key = RsaPrivateKey::from_pkcs1_der(pkey)?; Ok( match digest_type { "sha224" => { @@ -408,10 +418,7 @@ pub fn op_node_sign( .into(), ) } - _ => Err(type_error(format!( - "Signing with {} keys is not supported yet", - key_type - ))), + _ => Err(type_error("Unsupported signing key")), } } @@ -1345,8 +1352,6 @@ fn parse_private_key( format: &str, type_: &str, ) -> Result { - use rsa::pkcs1::DecodeRsaPrivateKey; - match format { "pem" => { let (label, doc) = diff --git a/ext/node/polyfills/internal/crypto/cipher.ts b/ext/node/polyfills/internal/crypto/cipher.ts index 5fec98ff0c..9b63db5380 100644 --- a/ext/node/polyfills/internal/crypto/cipher.ts +++ b/ext/node/polyfills/internal/crypto/cipher.ts @@ -31,7 +31,8 @@ import { export function isStringOrBuffer(val) { return typeof val === "string" || isArrayBufferView(val) || - isAnyArrayBuffer(val); + isAnyArrayBuffer(val) || + Buffer.isBuffer(val); } const { ops, encode } = globalThis.__bootstrap.core; diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index e0c44cbf9c..6a4fb21497 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -210,7 +210,7 @@ export interface JsonWebKeyInput { format: "jwk"; } -function prepareAsymmetricKey(key) { +export function prepareAsymmetricKey(key) { if (isStringOrBuffer(key)) { return { format: "pem", data: getArrayBufferOrView(key, "key") }; } else if (typeof key == "object") { diff --git a/ext/node/polyfills/internal/crypto/sig.ts b/ext/node/polyfills/internal/crypto/sig.ts index c5eb34fae3..9e8af8d08a 100644 --- a/ext/node/polyfills/internal/crypto/sig.ts +++ b/ext/node/polyfills/internal/crypto/sig.ts @@ -20,8 +20,8 @@ import type { PublicKeyInput, } from "ext:deno_node/internal/crypto/types.ts"; import { - getKeyMaterial, KeyObject, + prepareAsymmetricKey, } from "ext:deno_node/internal/crypto/keys.ts"; import { createHash, Hash } from "ext:deno_node/internal/crypto/hash.ts"; import { KeyFormat, KeyType } from "ext:deno_node/internal/crypto/types.ts"; @@ -80,26 +80,13 @@ export class SignImpl extends Writable { privateKey: BinaryLike | SignKeyObjectInput | SignPrivateKeyInput, encoding?: BinaryToTextEncoding, ): Buffer | string { - let keyData: Uint8Array; - let keyType: KeyType; - let keyFormat: KeyFormat; - if (typeof privateKey === "string" || isArrayBufferView(privateKey)) { - // if the key is BinaryLike, interpret it as a PEM encoded RSA key - // deno-lint-ignore no-explicit-any - keyData = privateKey as any; - keyType = "rsa"; - keyFormat = "pem"; - } else { - keyData = getKeyMaterial(privateKey); - keyType = "rsa"; - keyFormat = "pem"; - } + const { data, format, type } = prepareAsymmetricKey(privateKey); const ret = Buffer.from(ops.op_node_sign( this.hash.digest(), this.#digestType, - keyData!, - keyType, - keyFormat, + data!, + type, + format, )); return encoding ? ret.toString(encoding) : ret; }