diff --git a/Cargo.lock b/Cargo.lock index 39e7f6c09b..ea60b516c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1437,6 +1437,7 @@ dependencies = [ "brotli", "bytes", "cbc", + "const-oid", "data-encoding", "deno_core", "deno_fetch", diff --git a/cli/tests/unit_node/crypto/crypto_key_test.ts b/cli/tests/unit_node/crypto/crypto_key_test.ts index 672c9fa7f0..0e56d82550 100644 --- a/cli/tests/unit_node/crypto/crypto_key_test.ts +++ b/cli/tests/unit_node/crypto/crypto_key_test.ts @@ -2,6 +2,7 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. import { + createPrivateKey, createSecretKey, generateKeyPair, generateKeyPairSync, @@ -202,3 +203,28 @@ for (const primeLength of [1024, 2048, 4096]) { }, }); } + +const rsaPrivateKey = Deno.readTextFileSync( + new URL("../testdata/rsa_private.pem", import.meta.url), +); + +Deno.test("createPrivateKey rsa", function () { + const key = createPrivateKey(rsaPrivateKey); + assertEquals(key.type, "private"); + assertEquals(key.asymmetricKeyType, "rsa"); + assertEquals(key.asymmetricKeyDetails?.modulusLength, 2048); + assertEquals(key.asymmetricKeyDetails?.publicExponent, 65537n); +}); + +// openssl ecparam -name secp256r1 -genkey -noout -out a.pem +// openssl pkcs8 -topk8 -nocrypt -in a.pem -out b.pem +const ecPrivateKey = Deno.readTextFileSync( + new URL("./ec_private_secp256r1.pem", import.meta.url), +); + +Deno.test("createPrivateKey ec", function () { + const key = createPrivateKey(ecPrivateKey); + assertEquals(key.type, "private"); + assertEquals(key.asymmetricKeyType, "ec"); + assertEquals(key.asymmetricKeyDetails?.namedCurve, "p256"); +}); diff --git a/cli/tests/unit_node/crypto/ec_private_secp256r1.pem b/cli/tests/unit_node/crypto/ec_private_secp256r1.pem new file mode 100644 index 0000000000..f1d5c5769f --- /dev/null +++ b/cli/tests/unit_node/crypto/ec_private_secp256r1.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgbT/dwqGGyRs19qy8 +XPyAZVluvTE9N6hIbyVuFyZobrahRANCAATeWMJqmunAZ6o2lumC79MklBB3Z7ZF +ToryVl8HXevci1d/R+OZ6FjYnoICxw3rMXiKMDtUTAFi2ikL20O4+62M +-----END PRIVATE KEY----- diff --git a/ext/node/Cargo.toml b/ext/node/Cargo.toml index 8877a9cb16..a96b225715 100644 --- a/ext/node/Cargo.toml +++ b/ext/node/Cargo.toml @@ -19,6 +19,7 @@ aes.workspace = true brotli.workspace = true bytes.workspace = true cbc.workspace = true +const-oid = "0.9.5" data-encoding.workspace = true deno_core.workspace = true deno_fetch.workspace = true diff --git a/ext/node/lib.rs b/ext/node/lib.rs index cb395ad9a7..bbb3f82f85 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -274,6 +274,7 @@ deno_core::extension!(deno_node, ops::require::op_require_package_imports_resolve

, ops::require::op_require_break_on_next_statement, ops::util::op_node_guess_handle_type, + ops::crypto::op_node_create_private_key, ], esm_entry_point = "ext:deno_node/02_init.js", esm = [ diff --git a/ext/node/ops/crypto/mod.rs b/ext/node/ops/crypto/mod.rs index b81eb97f1f..35940da77d 100644 --- a/ext/node/ops/crypto/mod.rs +++ b/ext/node/ops/crypto/mod.rs @@ -3,6 +3,7 @@ use deno_core::error::generic_error; use deno_core::error::type_error; use deno_core::error::AnyError; use deno_core::op2; +use deno_core::serde_v8::BigInt as V8BigInt; use deno_core::unsync::spawn_blocking; use deno_core::JsBuffer; use deno_core::OpState; @@ -13,10 +14,16 @@ use hkdf::Hkdf; use num_bigint::BigInt; use num_bigint_dig::BigUint; use num_traits::FromPrimitive; +use once_cell::sync::Lazy; use rand::distributions::Distribution; use rand::distributions::Uniform; use rand::thread_rng; use rand::Rng; +use rsa::pkcs8; +use rsa::pkcs8::der::asn1; +use rsa::pkcs8::der::Decode; +use rsa::pkcs8::der::Encode; +use rsa::pkcs8::der::Reader; use std::future::Future; use std::rc::Rc; @@ -1173,3 +1180,270 @@ pub async fn op_node_gen_prime_async( ) -> Result { Ok(spawn_blocking(move || gen_prime(size)).await?) } + +#[derive(serde::Serialize)] +#[serde(tag = "type")] +pub enum AsymmetricKeyDetails { + #[serde(rename = "rsa")] + #[serde(rename_all = "camelCase")] + Rsa { + modulus_length: usize, + public_exponent: V8BigInt, + }, + #[serde(rename = "rsa-pss")] + #[serde(rename_all = "camelCase")] + RsaPss { + modulus_length: usize, + public_exponent: V8BigInt, + hash_algorithm: String, + salt_length: u32, + }, + #[serde(rename = "ec")] + #[serde(rename_all = "camelCase")] + Ec { named_curve: String }, +} + +// https://oidref.com/ +const ID_SHA1_OID: rsa::pkcs8::ObjectIdentifier = + rsa::pkcs8::ObjectIdentifier::new_unwrap("1.3.14.3.2.26"); +const ID_SHA256_OID: rsa::pkcs8::ObjectIdentifier = + rsa::pkcs8::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.1"); +const ID_SHA384_OID: rsa::pkcs8::ObjectIdentifier = + rsa::pkcs8::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.2"); +const ID_SHA512_OID: rsa::pkcs8::ObjectIdentifier = + rsa::pkcs8::ObjectIdentifier::new_unwrap("2.16.840.1.101.3.4.2.3"); +const ID_MFG1: rsa::pkcs8::ObjectIdentifier = + rsa::pkcs8::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.8"); +pub const ID_SECP256R1_OID: const_oid::ObjectIdentifier = + const_oid::ObjectIdentifier::new_unwrap("1.2.840.10045.3.1.7"); +pub const ID_SECP384R1_OID: const_oid::ObjectIdentifier = + const_oid::ObjectIdentifier::new_unwrap("1.3.132.0.34"); +pub const ID_SECP521R1_OID: const_oid::ObjectIdentifier = + const_oid::ObjectIdentifier::new_unwrap("1.3.132.0.35"); + +// Default HashAlgorithm for RSASSA-PSS-params (sha1) +// +// sha1 HashAlgorithm ::= { +// algorithm id-sha1, +// parameters SHA1Parameters : NULL +// } +// +// SHA1Parameters ::= NULL +static SHA1_HASH_ALGORITHM: Lazy> = + Lazy::new(|| rsa::pkcs8::AlgorithmIdentifierRef { + // id-sha1 + oid: ID_SHA1_OID, + // NULL + parameters: Some(asn1::AnyRef::from(asn1::Null)), + }); + +// TODO(@littledivy): `pkcs8` should provide AlgorithmIdentifier to Any conversion. +static ENCODED_SHA1_HASH_ALGORITHM: Lazy> = + Lazy::new(|| SHA1_HASH_ALGORITHM.to_der().unwrap()); + +// Default MaskGenAlgrithm for RSASSA-PSS-params (mgf1SHA1) +// +// mgf1SHA1 MaskGenAlgorithm ::= { +// algorithm id-mgf1, +// parameters HashAlgorithm : sha1 +// } +static MGF1_SHA1_MASK_ALGORITHM: Lazy< + rsa::pkcs8::AlgorithmIdentifierRef<'static>, +> = Lazy::new(|| rsa::pkcs8::AlgorithmIdentifierRef { + // id-mgf1 + oid: ID_MFG1, + // sha1 + parameters: Some( + asn1::AnyRef::from_der(&ENCODED_SHA1_HASH_ALGORITHM).unwrap(), + ), +}); + +pub const RSA_ENCRYPTION_OID: const_oid::ObjectIdentifier = + const_oid::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.1"); +pub const RSASSA_PSS_OID: const_oid::ObjectIdentifier = + const_oid::ObjectIdentifier::new_unwrap("1.2.840.113549.1.1.10"); +pub const EC_OID: const_oid::ObjectIdentifier = + const_oid::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); + +// The parameters field associated with OID id-RSASSA-PSS +// Defined in RFC 3447, section A.2.3 +// +// RSASSA-PSS-params ::= SEQUENCE { +// hashAlgorithm [0] HashAlgorithm DEFAULT sha1, +// maskGenAlgorithm [1] MaskGenAlgorithm DEFAULT mgf1SHA1, +// saltLength [2] INTEGER DEFAULT 20, +// trailerField [3] TrailerField DEFAULT trailerFieldBC +// } +pub struct PssPrivateKeyParameters<'a> { + pub hash_algorithm: rsa::pkcs8::AlgorithmIdentifierRef<'a>, + pub mask_gen_algorithm: rsa::pkcs8::AlgorithmIdentifierRef<'a>, + pub salt_length: u32, +} + +// Context-specific tag number for hashAlgorithm. +const HASH_ALGORITHM_TAG: rsa::pkcs8::der::TagNumber = + rsa::pkcs8::der::TagNumber::new(0); + +// Context-specific tag number for maskGenAlgorithm. +const MASK_GEN_ALGORITHM_TAG: rsa::pkcs8::der::TagNumber = + rsa::pkcs8::der::TagNumber::new(1); + +// Context-specific tag number for saltLength. +const SALT_LENGTH_TAG: rsa::pkcs8::der::TagNumber = + rsa::pkcs8::der::TagNumber::new(2); + +impl<'a> TryFrom> + for PssPrivateKeyParameters<'a> +{ + type Error = rsa::pkcs8::der::Error; + + fn try_from( + any: rsa::pkcs8::der::asn1::AnyRef<'a>, + ) -> rsa::pkcs8::der::Result { + any.sequence(|decoder| { + let hash_algorithm = decoder + .context_specific::( + HASH_ALGORITHM_TAG, + pkcs8::der::TagMode::Explicit, + )? + .map(TryInto::try_into) + .transpose()? + .unwrap_or(*SHA1_HASH_ALGORITHM); + + let mask_gen_algorithm = decoder + .context_specific::( + MASK_GEN_ALGORITHM_TAG, + pkcs8::der::TagMode::Explicit, + )? + .map(TryInto::try_into) + .transpose()? + .unwrap_or(*MGF1_SHA1_MASK_ALGORITHM); + + let salt_length = decoder + .context_specific::( + SALT_LENGTH_TAG, + pkcs8::der::TagMode::Explicit, + )? + .map(TryInto::try_into) + .transpose()? + .unwrap_or(20); + + Ok(Self { + hash_algorithm, + mask_gen_algorithm, + salt_length, + }) + }) + } +} + +fn parse_private_key( + key: &[u8], + format: &str, + type_: &str, +) -> Result { + use rsa::pkcs1::DecodeRsaPrivateKey; + + match format { + "pem" => { + let (label, doc) = + pkcs8::SecretDocument::from_pem(std::str::from_utf8(key).unwrap())?; + if label != "PRIVATE KEY" { + return Err(type_error("Invalid PEM label")); + } + Ok(doc) + } + "der" => { + match type_ { + "pkcs8" => pkcs8::SecretDocument::from_pkcs8_der(key) + .map_err(|_| type_error("Invalid PKCS8 private key")), + "pkcs1" => pkcs8::SecretDocument::from_pkcs1_der(key) + .map_err(|_| type_error("Invalid PKCS1 private key")), + // TODO(@littledivy): sec1 type + _ => Err(type_error(format!("Unsupported key type: {}", type_))), + } + } + _ => Err(type_error(format!("Unsupported key format: {}", format))), + } +} + +#[op2] +#[serde] +pub fn op_node_create_private_key( + #[buffer] key: &[u8], + #[string] format: &str, + #[string] type_: &str, +) -> Result { + use rsa::pkcs1::der::Decode; + + let doc = parse_private_key(key, format, type_)?; + let pk_info = pkcs8::PrivateKeyInfo::try_from(doc.as_bytes())?; + + let alg = pk_info.algorithm.oid; + + match alg { + RSA_ENCRYPTION_OID => { + let private_key = + rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key)?; + let modulus_length = private_key.modulus.as_bytes().len() * 8; + + Ok(AsymmetricKeyDetails::Rsa { + modulus_length, + public_exponent: BigInt::from_bytes_be( + num_bigint::Sign::Plus, + private_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 private_key = + rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key)?; + let modulus_length = private_key.modulus.as_bytes().len() * 8; + Ok(AsymmetricKeyDetails::RsaPss { + modulus_length, + public_exponent: BigInt::from_bytes_be( + num_bigint::Sign::Plus, + private_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/cipher.ts b/ext/node/polyfills/internal/crypto/cipher.ts index cf1641326d..5fec98ff0c 100644 --- a/ext/node/polyfills/internal/crypto/cipher.ts +++ b/ext/node/polyfills/internal/crypto/cipher.ts @@ -28,7 +28,7 @@ import { isArrayBufferView, } from "ext:deno_node/internal/util/types.ts"; -function isStringOrBuffer(val) { +export function isStringOrBuffer(val) { return typeof val === "string" || isArrayBufferView(val) || isAnyArrayBuffer(val); @@ -456,7 +456,7 @@ export function publicEncrypt( return ops.op_node_public_encrypt(data, buffer, padding); } -function prepareKey(key) { +export function prepareKey(key) { // TODO(@littledivy): handle these cases // - node KeyObject // - web CryptoKey @@ -485,5 +485,6 @@ export default { publicEncrypt, Cipheriv, Decipheriv, + prepareKey, getCipherInfo, }; diff --git a/ext/node/polyfills/internal/crypto/keys.ts b/ext/node/polyfills/internal/crypto/keys.ts index be85b44a3e..e0c44cbf9c 100644 --- a/ext/node/polyfills/internal/crypto/keys.ts +++ b/ext/node/polyfills/internal/crypto/keys.ts @@ -8,6 +8,7 @@ import { kHandle, kKeyObject, } from "ext:deno_node/internal/crypto/constants.ts"; +import { isStringOrBuffer } from "ext:deno_node/internal/crypto/cipher.ts"; import { ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE, ERR_INVALID_ARG_TYPE, @@ -16,7 +17,6 @@ import { import { notImplemented } from "ext:deno_node/_utils.ts"; import type { KeyFormat, - KeyType, PrivateKeyInput, PublicKeyInput, } from "ext:deno_node/internal/crypto/types.ts"; @@ -39,6 +39,9 @@ import { forgivingBase64UrlEncode as encodeToBase64Url, } from "ext:deno_web/00_infra.js"; +const { core } = globalThis.__bootstrap; +const { ops } = core; + export const getArrayBufferOrView = hideStackFrames( ( buffer, @@ -168,18 +171,6 @@ export class KeyObject { return this[kKeyType]; } - get asymmetricKeyDetails(): AsymmetricKeyDetails | undefined { - notImplemented("crypto.KeyObject.prototype.asymmetricKeyDetails"); - - return undefined; - } - - get asymmetricKeyType(): KeyType | undefined { - notImplemented("crypto.KeyObject.prototype.asymmetricKeyType"); - - return undefined; - } - get symmetricKeySize(): number | undefined { notImplemented("crypto.KeyObject.prototype.symmetricKeySize"); @@ -219,10 +210,33 @@ export interface JsonWebKeyInput { format: "jwk"; } +function prepareAsymmetricKey(key) { + if (isStringOrBuffer(key)) { + return { format: "pem", data: getArrayBufferOrView(key, "key") }; + } else if (typeof key == "object") { + const { key: data, encoding, format, type } = key; + if (!isStringOrBuffer(data)) { + throw new TypeError("Invalid key type"); + } + + return { + data: getArrayBufferOrView(data, "key", encoding), + format: format ?? "pem", + encoding, + type, + }; + } + + throw new TypeError("Invalid key type"); +} + export function createPrivateKey( - _key: PrivateKeyInput | string | Buffer | JsonWebKeyInput, -): KeyObject { - notImplemented("crypto.createPrivateKey"); + key: PrivateKeyInput | string | Buffer | JsonWebKeyInput, +): PrivateKeyObject { + const { data, format, type } = prepareAsymmetricKey(key); + const details = ops.op_node_create_private_key(data, format, type); + const handle = setOwnedKey(copyBuffer(data)); + return new PrivateKeyObject(handle, details); } export function createPublicKey( @@ -316,6 +330,35 @@ export class SecretKeyObject extends KeyObject { } } +const kAsymmetricKeyType = Symbol("kAsymmetricKeyType"); +const kAsymmetricKeyDetails = Symbol("kAsymmetricKeyDetails"); + +class AsymmetricKeyObject extends KeyObject { + constructor(type: KeyObjectType, handle: unknown, details: unknown) { + super(type, handle); + this[kAsymmetricKeyType] = details.type; + this[kAsymmetricKeyDetails] = { ...details }; + } + + get asymmetricKeyType() { + return this[kAsymmetricKeyType]; + } + + get asymmetricKeyDetails() { + return this[kAsymmetricKeyDetails]; + } +} + +class PrivateKeyObject extends AsymmetricKeyObject { + constructor(handle: unknown, details: unknown) { + super("private", handle, details); + } + + export(_options: unknown) { + notImplemented("crypto.PrivateKeyObject.prototype.export"); + } +} + export function setOwnedKey(key: Uint8Array): unknown { const handle = {}; KEY_STORE.set(handle, key); diff --git a/ext/node/polyfills/internal/crypto/sig.ts b/ext/node/polyfills/internal/crypto/sig.ts index ebbd11dc6c..c5eb34fae3 100644 --- a/ext/node/polyfills/internal/crypto/sig.ts +++ b/ext/node/polyfills/internal/crypto/sig.ts @@ -19,7 +19,10 @@ import type { PrivateKeyInput, PublicKeyInput, } from "ext:deno_node/internal/crypto/types.ts"; -import { KeyObject } from "ext:deno_node/internal/crypto/keys.ts"; +import { + getKeyMaterial, + KeyObject, +} 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"; import { isArrayBufferView } from "ext:deno_node/internal/util/types.ts"; @@ -87,9 +90,9 @@ export class SignImpl extends Writable { keyType = "rsa"; keyFormat = "pem"; } else { - // TODO(kt3k): Add support for the case when privateKey is a KeyObject, - // CryptoKey, etc - notImplemented("crypto.Sign.prototype.sign with non BinaryLike input"); + keyData = getKeyMaterial(privateKey); + keyType = "rsa"; + keyFormat = "pem"; } const ret = Buffer.from(ops.op_node_sign( this.hash.digest(),