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(),