1
0
Fork 0
mirror of https://github.com/denoland/deno.git synced 2024-11-21 15:04:11 -05:00

fix(node): implement createPrivateKey (#20981)

Towards #18455
This commit is contained in:
Divy Srivastava 2023-11-09 09:56:59 -08:00 committed by GitHub
parent ee7fd0a212
commit c4029f6af2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 377 additions and 22 deletions

1
Cargo.lock generated
View file

@ -1437,6 +1437,7 @@ dependencies = [
"brotli",
"bytes",
"cbc",
"const-oid",
"data-encoding",
"deno_core",
"deno_fetch",

View file

@ -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");
});

View file

@ -0,0 +1,5 @@
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgbT/dwqGGyRs19qy8
XPyAZVluvTE9N6hIbyVuFyZobrahRANCAATeWMJqmunAZ6o2lumC79MklBB3Z7ZF
ToryVl8HXevci1d/R+OZ6FjYnoICxw3rMXiKMDtUTAFi2ikL20O4+62M
-----END PRIVATE KEY-----

View file

@ -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

View file

@ -274,6 +274,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 = [

View file

@ -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<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");
// 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")),
}
}

View file

@ -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,
};

View file

@ -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);

View file

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