diff options
-rw-r--r-- | cli/tests/unit/webcrypto_test.ts | 44 | ||||
-rw-r--r-- | ext/crypto/00_crypto.js | 95 | ||||
-rw-r--r-- | ext/crypto/lib.rs | 151 |
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 |