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

feat(ext/crypto): export RSA as pkcs#8 #11880

Merged
merged 14 commits into from
Sep 13, 2021
44 changes: 44 additions & 0 deletions cli/tests/unit/webcrypto_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,3 +356,47 @@ unitTest(async function subtleCryptoHmacImportExport() {
const exportedKey2 = await crypto.subtle.exportKey("jwk", key2);
assertEquals(exportedKey2, jwk);
});

// deno-fmt-ignore
const asn1AlgorithmIdentifier = new Uint8Array([
0x02, 0x01, 0x00, // INTEGER
0x30, 0x0d, // SEQUENCE (2 elements)
0x06, 0x09, // OBJECT IDENTIFIER
0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01, // 1.2.840.113549.1.1.1 (rsaEncryption)
0x05, 0x00, // NULL
]);

unitTest(async function rsaExportPkcs8() {
for (const algorithm of ["RSASSA-PKCS1-v1_5", "RSA-PSS", "RSA-OAEP"]) {
const keyPair = await crypto.subtle.generateKey(
{
name: algorithm,
modulusLength: 2048,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
algorithm !== "RSA-OAEP" ? ["sign", "verify"] : ["encrypt", "decrypt"],
);

assert(keyPair.privateKey);
assert(keyPair.publicKey);
assertEquals(keyPair.privateKey.extractable, true);

const exportedKey = await crypto.subtle.exportKey(
"pkcs8",
keyPair.privateKey,
);

assert(exportedKey);
assert(exportedKey instanceof ArrayBuffer);

const pkcs8 = new Uint8Array(exportedKey);
assert(pkcs8.length > 0);

assertEquals(
pkcs8.slice(4, asn1AlgorithmIdentifier.byteLength + 4),
asn1AlgorithmIdentifier,
);
}
});
95 changes: 90 additions & 5 deletions ext/crypto/00_crypto.js
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,6 @@
* @param {CryptoKey} key
* @returns {Promise<any>}
*/
// deno-lint-ignore require-await
async exportKey(format, key) {
webidl.assertBranded(this, SubtleCrypto);
const prefix = "Failed to execute 'exportKey' on 'SubtleCrypto'";
Expand Down Expand Up @@ -1077,8 +1076,92 @@
// TODO(@littledivy): Redundant break but deno_lint complains without it
break;
}
// TODO(@littledivy): RSASSA-PKCS1-v1_5
// TODO(@littledivy): RSA-PSS
case "RSASSA-PKCS1-v1_5": {
switch (format) {
case "pkcs8": {
// 1.
if (key[_type] !== "private") {
throw new DOMException(
"Key is not a private key",
"InvalidAccessError",
);
}

// 2.
const data = await core.opAsync(
"op_crypto_export_key",
{
key: innerKey,
format: "pkcs8",
algorithm: "RSASSA-PKCS1-v1_5",
},
);

// 3.
return data.buffer;
}
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
}
case "RSA-PSS": {
switch (format) {
case "pkcs8": {
// 1.
if (key[_type] !== "private") {
throw new DOMException(
"Key is not a private key",
"InvalidAccessError",
);
}

// 2.
const data = await core.opAsync(
"op_crypto_export_key",
{
key: innerKey,
format: "pkcs8",
algorithm: "RSA-PSS",
hash: key[_algorithm].hash.name,
},
);

// 3.
return data.buffer;
}
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
}
case "RSA-OAEP": {
switch (format) {
case "pkcs8": {
// 1.
if (key[_type] !== "private") {
throw new DOMException(
"Key is not a private key",
"InvalidAccessError",
);
}

// 2.
const data = await core.opAsync(
"op_crypto_export_key",
{
key: innerKey,
format: "pkcs8",
algorithm: "RSA-PSS",
hash: key[_algorithm].hash.name,
},
);

// 3.
return data.buffer;
}
default:
throw new DOMException("Not implemented", "NotSupportedError");
}
}
// TODO(@littledivy): ECDSA
default:
throw new DOMException("Not implemented", "NotSupportedError");
Expand Down Expand Up @@ -1339,7 +1422,8 @@
);
const handle = {};
WeakMapPrototypeSet(KEY_STORE, handle, {
type: "pkcs8",
// PKCS#1 for RSA
type: "raw",
data: keyData,
});

Expand Down Expand Up @@ -1399,7 +1483,8 @@
);
const handle = {};
WeakMapPrototypeSet(KEY_STORE, handle, {
type: "pkcs8",
// PKCS#1 for RSA
type: "raw",
data: keyData,
});

Expand Down
151 changes: 142 additions & 9 deletions ext/crypto/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,9 @@ use ring::signature::EcdsaSigningAlgorithm;
use ring::signature::EcdsaVerificationAlgorithm;
use ring::signature::KeyPair;
use rsa::padding::PaddingScheme;
use rsa::pkcs8::FromPrivateKey;
use rsa::pkcs8::ToPrivateKey;
use rsa::pkcs1::FromRsaPrivateKey;
use rsa::pkcs1::ToRsaPrivateKey;
use rsa::pkcs8::der::asn1;
use rsa::BigUint;
use rsa::PublicKey;
use rsa::RsaPrivateKey;
Expand Down Expand Up @@ -81,6 +82,7 @@ pub fn init(maybe_seed: Option<u64>) -> Extension {
("op_crypto_sign_key", op_async(op_crypto_sign_key)),
("op_crypto_verify_key", op_async(op_crypto_verify_key)),
("op_crypto_derive_bits", op_async(op_crypto_derive_bits)),
("op_crypto_export_key", op_async(op_crypto_export_key)),
("op_crypto_encrypt_key", op_async(op_crypto_encrypt_key)),
("op_crypto_decrypt_key", op_async(op_crypto_decrypt_key)),
("op_crypto_subtle_digest", op_async(op_crypto_subtle_digest)),
Expand Down Expand Up @@ -164,7 +166,7 @@ pub async fn op_crypto_generate_key(
.unwrap()
.map_err(|e| custom_error("DOMExceptionOperationError", e.to_string()))?;

private_key.to_pkcs8_der()?.as_ref().to_vec()
private_key.to_pkcs1_der()?.as_ref().to_vec()
}
Algorithm::Ecdsa => {
let curve: &EcdsaSigningAlgorithm =
Expand Down Expand Up @@ -270,7 +272,7 @@ pub async fn op_crypto_sign_key(

let signature = match algorithm {
Algorithm::RsassaPkcs1v15 => {
let private_key = RsaPrivateKey::from_pkcs8_der(&*args.key.data)?;
let private_key = RsaPrivateKey::from_pkcs1_der(&*args.key.data)?;
let (padding, hashed) = match args
.hash
.ok_or_else(|| type_error("Missing argument hash".to_string()))?
Expand Down Expand Up @@ -320,7 +322,7 @@ pub async fn op_crypto_sign_key(
private_key.sign(padding, &hashed)?
}
Algorithm::RsaPss => {
let private_key = RsaPrivateKey::from_pkcs8_der(&*args.key.data)?;
let private_key = RsaPrivateKey::from_pkcs1_der(&*args.key.data)?;

let salt_len = args
.salt_length
Expand Down Expand Up @@ -426,7 +428,7 @@ pub async fn op_crypto_verify_key(
let verification = match algorithm {
Algorithm::RsassaPkcs1v15 => {
let public_key: RsaPublicKey =
RsaPrivateKey::from_pkcs8_der(&*args.key.data)?.to_public_key();
RsaPrivateKey::from_pkcs1_der(&*args.key.data)?.to_public_key();
let (padding, hashed) = match args
.hash
.ok_or_else(|| type_error("Missing argument hash".to_string()))?
Expand Down Expand Up @@ -483,7 +485,7 @@ pub async fn op_crypto_verify_key(
.ok_or_else(|| type_error("Missing argument saltLength".to_string()))?
as usize;
let public_key: RsaPublicKey =
RsaPrivateKey::from_pkcs8_der(&*args.key.data)?.to_public_key();
RsaPrivateKey::from_pkcs1_der(&*args.key.data)?.to_public_key();

let rng = OsRng;
let (padding, hashed) = match args
Expand Down Expand Up @@ -552,6 +554,137 @@ pub async fn op_crypto_verify_key(
Ok(verification)
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ExportKeyArg {
key: KeyData,
algorithm: Algorithm,
format: KeyFormat,
// RSA-PSS
hash: Option<CryptoHash>,
}

pub async fn op_crypto_export_key(
_state: Rc<RefCell<OpState>>,
args: ExportKeyArg,
_zero_copy: Option<ZeroCopyBuf>,
) -> Result<ZeroCopyBuf, AnyError> {
let algorithm = args.algorithm;
match algorithm {
Algorithm::RsassaPkcs1v15 => {
match args.format {
KeyFormat::Pkcs8 => {
// private_key is a PKCS#1 DER-encoded private key

let private_key = &args.key.data;

// the PKCS#8 v1 structure
// PrivateKeyInfo ::= SEQUENCE {
// version Version,
// privateKeyAlgorithm PrivateKeyAlgorithmIdentifier,
// privateKey PrivateKey,
// attributes [0] IMPLICIT Attributes OPTIONAL }

// version is 0 when publickey is None

let pk_info = rsa::pkcs8::PrivateKeyInfo {
attributes: None,
public_key: None,
algorithm: rsa::pkcs8::AlgorithmIdentifier {
// rsaEncryption(1)
oid: rsa::pkcs8::ObjectIdentifier::new("1.2.840.113549.1.1.1"),
// parameters field should not be ommited (None).
// It MUST have ASN.1 type NULL as per defined in RFC 3279 Section 2.3.1
parameters: Some(asn1::Any::from(asn1::Null)),
},
private_key,
};

Ok(pk_info.to_der().as_ref().to_vec().into())
}
// TODO(@littledivy): spki
// TODO(@littledivy): jwk
_ => unreachable!(),
}
}
Algorithm::RsaPss => {
match args.format {
KeyFormat::Pkcs8 => {
// Intentionally unused but required. Not encoded into PKCS#8 (see below).
let _hash = args
.hash
.ok_or_else(|| type_error("Missing argument hash".to_string()))?;

// private_key is a PKCS#1 DER-encoded private key
let private_key = &args.key.data;

// version is 0 when publickey is None

let pk_info = rsa::pkcs8::PrivateKeyInfo {
attributes: None,
public_key: None,
algorithm: rsa::pkcs8::AlgorithmIdentifier {
// Spec wants the OID to be id-RSASSA-PSS (1.2.840.113549.1.1.10) but ring and RSA do not support it.
// Instead, we use rsaEncryption (1.2.840.113549.1.1.1) as specified in RFC 3447.
// Node, Chromium and Firefox also use rsaEncryption (1.2.840.113549.1.1.1) and do not support id-RSASSA-PSS.

// parameters are set to NULL opposed to what spec wants (see above)
oid: rsa::pkcs8::ObjectIdentifier::new("1.2.840.113549.1.1.1"),
Copy link
Member

Choose a reason for hiding this comment

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

So probably we shouldn't support it either...

Copy link
Member Author

Choose a reason for hiding this comment

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

yup, that is the current behavior 👍

// parameters field should not be ommited (None).
// It MUST have ASN.1 type NULL as per defined in RFC 3279 Section 2.3.1
parameters: Some(asn1::Any::from(asn1::Null)),
},
private_key,
};

Ok(pk_info.to_der().as_ref().to_vec().into())
}
// TODO(@littledivy): spki
// TODO(@littledivy): jwk
_ => unreachable!(),
}
}
Algorithm::RsaOaep => {
match args.format {
KeyFormat::Pkcs8 => {
// Intentionally unused but required. Not encoded into PKCS#8 (see below).
let _hash = args
.hash
.ok_or_else(|| type_error("Missing argument hash".to_string()))?;

// private_key is a PKCS#1 DER-encoded private key
let private_key = &args.key.data;

// version is 0 when publickey is None

let pk_info = rsa::pkcs8::PrivateKeyInfo {
attributes: None,
public_key: None,
algorithm: rsa::pkcs8::AlgorithmIdentifier {
// Spec wants the OID to be id-RSAES-OAEP (1.2.840.113549.1.1.10) but ring and RSA crate do not support it.
// Instead, we use rsaEncryption (1.2.840.113549.1.1.1) as specified in RFC 3447.
// Chromium and Firefox also use rsaEncryption (1.2.840.113549.1.1.1) and do not support id-RSAES-OAEP.

// parameters are set to NULL opposed to what spec wants (see above)
oid: rsa::pkcs8::ObjectIdentifier::new("1.2.840.113549.1.1.1"),
// parameters field should not be ommited (None).
// It MUST have ASN.1 type NULL as per defined in RFC 3279 Section 2.3.1
parameters: Some(asn1::Any::from(asn1::Null)),
},
private_key,
};

Ok(pk_info.to_der().as_ref().to_vec().into())
}
// TODO(@littledivy): spki
// TODO(@littledivy): jwk
_ => unreachable!(),
}
}
_ => Err(type_error("Unsupported algorithm".to_string())),
}
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeriveKeyArg {
Expand Down Expand Up @@ -642,7 +775,7 @@ pub async fn op_crypto_encrypt_key(
match algorithm {
Algorithm::RsaOaep => {
let public_key: RsaPublicKey =
RsaPrivateKey::from_pkcs8_der(&*args.key.data)?.to_public_key();
RsaPrivateKey::from_pkcs1_der(&*args.key.data)?.to_public_key();
let label = args.label.map(|l| String::from_utf8_lossy(&*l).to_string());
let mut rng = OsRng;
let padding = match args
Expand Down Expand Up @@ -705,7 +838,7 @@ pub async fn op_crypto_decrypt_key(
match algorithm {
Algorithm::RsaOaep => {
let private_key: RsaPrivateKey =
RsaPrivateKey::from_pkcs8_der(&*args.key.data)?;
RsaPrivateKey::from_pkcs1_der(&*args.key.data)?;
let label = args.label.map(|l| String::from_utf8_lossy(&*l).to_string());
let padding = match args
.hash
Expand Down