Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(node): implement createPrivateKey #20981

Merged
merged 14 commits into from
Nov 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

26 changes: 26 additions & 0 deletions cli/tests/unit_node/crypto/crypto_key_test.ts
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Discussed offline, we'll enable more native tests once KeyObject support in sign and verify is added (in a follow up).

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
import {
createPrivateKey,
createSecretKey,
generateKeyPair,
generateKeyPairSync,
Expand Down Expand Up @@ -202,3 +203,28 @@ for (const primeLength of [1024, 2048, 4096]) {
},
});
}

const rsaPrivateKey = Deno.readTextFileSync(
new URL("../testdata/rsa_private.pem", import.meta.url),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be import.meta.resolve("../testdata/rsa_private.pem");

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deno.readTextFileSync does not support the file:// scheme

);

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),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

);

Deno.test("createPrivateKey ec", function () {
const key = createPrivateKey(ecPrivateKey);
assertEquals(key.type, "private");
assertEquals(key.asymmetricKeyType, "ec");
assertEquals(key.asymmetricKeyDetails?.namedCurve, "p256");
});
5 changes: 5 additions & 0 deletions cli/tests/unit_node/crypto/ec_private_secp256r1.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgbT/dwqGGyRs19qy8
XPyAZVluvTE9N6hIbyVuFyZobrahRANCAATeWMJqmunAZ6o2lumC79MklBB3Z7ZF
ToryVl8HXevci1d/R+OZ6FjYnoICxw3rMXiKMDtUTAFi2ikL20O4+62M
-----END PRIVATE KEY-----
1 change: 1 addition & 0 deletions ext/node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ deno_core::extension!(deno_node,
ops::require::op_require_package_imports_resolve<P>,
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 = [
Expand Down
274 changes: 274 additions & 0 deletions ext/node/ops/crypto/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -1173,3 +1180,270 @@ pub async fn op_node_gen_prime_async(
) -> Result<ToJsBuffer, AnyError> {
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");
littledivy marked this conversation as resolved.
Show resolved Hide resolved

// Default HashAlgorithm for RSASSA-PSS-params (sha1)
//
// sha1 HashAlgorithm ::= {
// algorithm id-sha1,
// parameters SHA1Parameters : NULL
// }
//
// SHA1Parameters ::= NULL
static SHA1_HASH_ALGORITHM: Lazy<rsa::pkcs8::AlgorithmIdentifierRef<'static>> =
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<Vec<u8>> =
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<rsa::pkcs8::der::asn1::AnyRef<'a>>
for PssPrivateKeyParameters<'a>
{
type Error = rsa::pkcs8::der::Error;

fn try_from(
any: rsa::pkcs8::der::asn1::AnyRef<'a>,
) -> rsa::pkcs8::der::Result<PssPrivateKeyParameters> {
any.sequence(|decoder| {
let hash_algorithm = decoder
.context_specific::<rsa::pkcs8::AlgorithmIdentifierRef>(
HASH_ALGORITHM_TAG,
pkcs8::der::TagMode::Explicit,
)?
.map(TryInto::try_into)
.transpose()?
.unwrap_or(*SHA1_HASH_ALGORITHM);

let mask_gen_algorithm = decoder
.context_specific::<rsa::pkcs8::AlgorithmIdentifierRef>(
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::<u32>(
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<pkcs8::SecretDocument, AnyError> {
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<AsymmetricKeyDetails, AnyError> {
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")),
}
}
5 changes: 3 additions & 2 deletions ext/node/polyfills/internal/crypto/cipher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -485,5 +485,6 @@ export default {
publicEncrypt,
Cipheriv,
Decipheriv,
prepareKey,
getCipherInfo,
};
Loading