summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDivy Srivastava <dj.srivastava23@gmail.com>2021-09-13 15:03:28 +0530
committerGitHub <noreply@github.com>2021-09-13 11:33:28 +0200
commit2199bdaf64c59c69f53079362e902355325cfa37 (patch)
treebaeca465179956e1a34d9501ab4e872d58db2488
parent84f874715763df71bb3bbf77f0714f8afdc17bb3 (diff)
feat(ext/crypto): export RSA keys as pkcs#8 (#11880)
-rw-r--r--cli/tests/unit/webcrypto_test.ts44
-rw-r--r--ext/crypto/00_crypto.js95
-rw-r--r--ext/crypto/lib.rs151
3 files changed, 276 insertions, 14 deletions
diff --git a/cli/tests/unit/webcrypto_test.ts b/cli/tests/unit/webcrypto_test.ts
index 57ab051d1..7e0f132e0 100644
--- a/cli/tests/unit/webcrypto_test.ts
+++ b/cli/tests/unit/webcrypto_test.ts
@@ -357,6 +357,50 @@ unitTest(async function subtleCryptoHmacImportExport() {
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,
+ );
+ }
+});
+
unitTest(async function testHkdfDeriveBits() {
const rawKey = await crypto.getRandomValues(new Uint8Array(16));
const key = await crypto.subtle.importKey(
diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js
index 2cb7a3bb2..68a8e4f9f 100644
--- a/ext/crypto/00_crypto.js
+++ b/ext/crypto/00_crypto.js
@@ -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'";
@@ -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");
@@ -1339,7 +1422,8 @@
);
const handle = {};
WeakMapPrototypeSet(KEY_STORE, handle, {
- type: "pkcs8",
+ // PKCS#1 for RSA
+ type: "raw",
data: keyData,
});
@@ -1399,7 +1483,8 @@
);
const handle = {};
WeakMapPrototypeSet(KEY_STORE, handle, {
- type: "pkcs8",
+ // PKCS#1 for RSA
+ type: "raw",
data: keyData,
});
diff --git a/ext/crypto/lib.rs b/ext/crypto/lib.rs
index 319f26c22..cf2c379a0 100644
--- a/ext/crypto/lib.rs
+++ b/ext/crypto/lib.rs
@@ -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;
@@ -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)),
@@ -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 =
@@ -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()))?
@@ -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
@@ -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()))?
@@ -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
@@ -554,6 +556,137 @@ pub async fn op_crypto_verify_key(
#[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"),
+ // 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 {
key: KeyData,
algorithm: Algorithm,
@@ -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
@@ -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