diff options
author | Sean Michael Wykes <8363933+SeanWykes@users.noreply.github.com> | 2022-01-19 00:38:35 -0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-01-19 09:08:35 +0530 |
commit | 77e58fe7f9fc20dabf77424efbd25ce332f8f59c (patch) | |
tree | e8cdf3b06661b209ea685b5d762f670e662923b7 /ext/crypto | |
parent | b3545dd447dcbab6629827dbe8d127ef82f8da69 (diff) |
feat(ext/crypto): implement pkcs8/spki/jwk exportKey for ECDSA and ECDH (#13104)
Diffstat (limited to 'ext/crypto')
-rw-r--r-- | ext/crypto/00_crypto.js | 126 | ||||
-rw-r--r-- | ext/crypto/ec_key.rs | 150 | ||||
-rw-r--r-- | ext/crypto/export_key.rs | 173 | ||||
-rw-r--r-- | ext/crypto/lib.rs | 1 | ||||
-rw-r--r-- | ext/crypto/shared.rs | 29 |
5 files changed, 479 insertions, 0 deletions
diff --git a/ext/crypto/00_crypto.js b/ext/crypto/00_crypto.js index 81c475ad7..2596bb052 100644 --- a/ext/crypto/00_crypto.js +++ b/ext/crypto/00_crypto.js @@ -975,6 +975,10 @@ case "RSA-OAEP": { return exportKeyRSA(format, key, innerKey); } + case "ECDH": + case "ECDSA": { + return exportKeyEC(format, key, innerKey); + } case "AES-CTR": case "AES-CBC": case "AES-GCM": @@ -3377,6 +3381,128 @@ } } + function exportKeyEC(format, key, innerKey) { + switch (format) { + case "pkcs8": { + // 1. + if (key[_type] !== "private") { + throw new DOMException( + "Key is not a private key", + "InvalidAccessError", + ); + } + + // 2. + const data = core.opSync("op_crypto_export_key", { + algorithm: key[_algorithm].name, + namedCurve: key[_algorithm].namedCurve, + format: "pkcs8", + }, innerKey); + + return data.buffer; + } + case "spki": { + // 1. + if (key[_type] !== "public") { + throw new DOMException( + "Key is not a public key", + "InvalidAccessError", + ); + } + + // 2. + const data = core.opSync("op_crypto_export_key", { + algorithm: key[_algorithm].name, + namedCurve: key[_algorithm].namedCurve, + format: "spki", + }, innerKey); + + return data.buffer; + } + case "jwk": { + if (key[_algorithm].name == "ECDSA") { + // 1-2. + const jwk = { + kty: "EC", + }; + + // 3.1 + jwk.crv = key[_algorithm].namedCurve; + + // Missing from spec + let algNamedCurve; + + switch (key[_algorithm].namedCurve) { + case "P-256": { + algNamedCurve = "ES256"; + break; + } + case "P-384": { + algNamedCurve = "ES384"; + break; + } + case "P-521": { + algNamedCurve = "ES512"; + break; + } + default: + throw new DOMException( + "Curve algorithm not supported", + "DataError", + ); + } + + jwk.alg = algNamedCurve; + + // 3.2 - 3.4. + const data = core.opSync("op_crypto_export_key", { + format: key[_type] === "private" ? "jwkprivate" : "jwkpublic", + algorithm: key[_algorithm].name, + namedCurve: key[_algorithm].namedCurve, + }, innerKey); + ObjectAssign(jwk, data); + + // 4. + jwk.key_ops = key.usages; + + // 5. + jwk.ext = key[_extractable]; + + return jwk; + } else { // ECDH + // 1-2. + const jwk = { + kty: "EC", + }; + + // missing step from spec + jwk.alg = "ECDH"; + + // 3.1 + jwk.crv = key[_algorithm].namedCurve; + + // 3.2 - 3.4 + const data = core.opSync("op_crypto_export_key", { + format: key[_type] === "private" ? "jwkprivate" : "jwkpublic", + algorithm: key[_algorithm].name, + namedCurve: key[_algorithm].namedCurve, + }, innerKey); + ObjectAssign(jwk, data); + + // 4. + jwk.key_ops = key.usages; + + // 5. + jwk.ext = key[_extractable]; + + return jwk; + } + } + default: + throw new DOMException("Not implemented", "NotSupportedError"); + } + } + async function generateKeyAES(normalizedAlgorithm, extractable, usages) { const algorithmName = normalizedAlgorithm.name; diff --git a/ext/crypto/ec_key.rs b/ext/crypto/ec_key.rs new file mode 100644 index 000000000..3509f0aef --- /dev/null +++ b/ext/crypto/ec_key.rs @@ -0,0 +1,150 @@ +use deno_core::error::AnyError; + +use elliptic_curve::AlgorithmParameters; + +use elliptic_curve::pkcs8; +use elliptic_curve::pkcs8::der; +use elliptic_curve::pkcs8::der::asn1::*; +use elliptic_curve::pkcs8::der::Decodable as Pkcs8Decodable; +use elliptic_curve::pkcs8::der::Encodable; +use elliptic_curve::pkcs8::der::TagNumber; +use elliptic_curve::pkcs8::AlgorithmIdentifier; +use elliptic_curve::pkcs8::ObjectIdentifier; +use elliptic_curve::pkcs8::PrivateKeyDocument; +use elliptic_curve::pkcs8::PrivateKeyInfo; +use elliptic_curve::zeroize::Zeroizing; + +use crate::shared::*; + +const VERSION: u8 = 1; + +const PUBLIC_KEY_TAG: TagNumber = TagNumber::new(1); + +pub struct ECPrivateKey<'a, C: elliptic_curve::Curve> { + pub algorithm: AlgorithmIdentifier<'a>, + + pub private_d: elliptic_curve::FieldBytes<C>, + + pub encoded_point: &'a [u8], +} + +#[allow(dead_code)] +///todo(@sean) - to be removed in #13154 +impl<'a, C> ECPrivateKey<'a, C> +where + C: elliptic_curve::Curve + AlgorithmParameters, +{ + /// Create a new ECPrivateKey from a serialized private scalar and encoded public key + pub fn from_private_and_public_bytes( + private_d: elliptic_curve::FieldBytes<C>, + encoded_point: &'a [u8], + ) -> Self { + Self { + private_d, + encoded_point, + algorithm: C::algorithm_identifier(), + } + } + + pub fn named_curve_oid(&self) -> Result<ObjectIdentifier, AnyError> { + let parameters = self + .algorithm + .parameters + .ok_or_else(|| data_error("malformed parameters"))?; + + Ok(parameters.oid().unwrap()) + } + + fn internal_to_pkcs8_der(&self) -> der::Result<Vec<u8>> { + // Shamelessly copied from pkcs8 crate and modified so as + // to not require Arithmetic trait currently missing from p384 + let secret_key_field = OctetString::new(&self.private_d)?; + let public_key_bytes = &self.encoded_point; + let public_key_field = ContextSpecific { + tag_number: PUBLIC_KEY_TAG, + value: BitString::new(public_key_bytes)?.into(), + }; + + let der_message_fields: &[&dyn Encodable] = + &[&VERSION, &secret_key_field, &public_key_field]; + + let encoded_len = + der::message::encoded_len(der_message_fields)?.try_into()?; + let mut der_message = Zeroizing::new(vec![0u8; encoded_len]); + let mut encoder = der::Encoder::new(&mut der_message); + encoder.message(der_message_fields)?; + encoder.finish()?; + + Ok(der_message.to_vec()) + } + + pub fn to_pkcs8_der(&self) -> Result<PrivateKeyDocument, AnyError> { + let pkcs8_der = self + .internal_to_pkcs8_der() + .map_err(|_| data_error("expected valid PKCS#8 data"))?; + + let pki = + pkcs8::PrivateKeyInfo::new(C::algorithm_identifier(), pkcs8_der.as_ref()); + + Ok(pki.to_der()) + } +} + +impl<'a, C: elliptic_curve::Curve> TryFrom<&'a [u8]> for ECPrivateKey<'a, C> { + type Error = AnyError; + + fn try_from(bytes: &'a [u8]) -> Result<ECPrivateKey<C>, AnyError> { + let pk_info = PrivateKeyInfo::from_der(bytes) + .map_err(|_| data_error("expected valid PKCS#8 data"))?; + + Self::try_from(pk_info) + } +} + +impl<'a, C: elliptic_curve::Curve> TryFrom<PrivateKeyInfo<'a>> + for ECPrivateKey<'a, C> +{ + type Error = AnyError; + + fn try_from( + pk_info: PrivateKeyInfo<'a>, + ) -> Result<ECPrivateKey<'a, C>, AnyError> { + let any = der::asn1::Any::from_der(pk_info.private_key).map_err(|_| { + data_error("expected valid PrivateKeyInfo private_key der") + })?; + + if pk_info.algorithm.oid != elliptic_curve::ALGORITHM_OID { + return Err(data_error("unsupported algorithm")); + } + + any + .sequence(|decoder| { + // ver + if decoder.uint8()? != VERSION { + return Err(der::Tag::Integer.value_error()); + } + + // private_key + let priv_key = decoder.octet_string()?.as_bytes(); + let mut private_d = elliptic_curve::FieldBytes::<C>::default(); + if priv_key.len() != private_d.len() { + return Err(der::Tag::Sequence.value_error()); + }; + private_d.copy_from_slice(priv_key); + + let public_key = decoder + .context_specific(PUBLIC_KEY_TAG)? + .ok_or_else(|| { + der::Tag::ContextSpecific(PUBLIC_KEY_TAG).value_error() + })? + .bit_string()?; + + Ok(Self { + private_d, + encoded_point: public_key.as_bytes(), + algorithm: pk_info.algorithm, + }) + }) + .map_err(|_| data_error("expected valid PrivateKeyInfo private_key der")) + } +} diff --git a/ext/crypto/export_key.rs b/ext/crypto/export_key.rs index 25faf1791..891aea92a 100644 --- a/ext/crypto/export_key.rs +++ b/ext/crypto/export_key.rs @@ -8,7 +8,10 @@ use serde::Serialize; use spki::der::asn1; use spki::der::Decodable; use spki::der::Encodable; +use spki::AlgorithmIdentifier; +use spki::ObjectIdentifier; +use crate::ec_key::ECPrivateKey; use crate::shared::*; #[derive(Deserialize)] @@ -38,6 +41,10 @@ pub enum ExportKeyAlgorithm { RsaPss {}, #[serde(rename = "RSA-OAEP")] RsaOaep {}, + #[serde(rename = "ECDSA", rename_all = "camelCase")] + Ecdsa { named_curve: EcNamedCurve }, + #[serde(rename = "ECDH", rename_all = "camelCase")] + Ecdh { named_curve: EcNamedCurve }, #[serde(rename = "AES")] Aes {}, #[serde(rename = "HMAC")] @@ -66,6 +73,15 @@ pub enum ExportKeyResult { dq: String, qi: String, }, + JwkPublicEc { + x: String, + y: String, + }, + JwkPrivateEc { + x: String, + y: String, + d: String, + }, } pub fn op_crypto_export_key( @@ -77,6 +93,10 @@ pub fn op_crypto_export_key( ExportKeyAlgorithm::RsassaPkcs1v15 {} | ExportKeyAlgorithm::RsaPss {} | ExportKeyAlgorithm::RsaOaep {} => export_key_rsa(opts.format, key_data), + ExportKeyAlgorithm::Ecdh { named_curve } + | ExportKeyAlgorithm::Ecdsa { named_curve } => { + export_key_ec(opts.format, key_data, opts.algorithm, named_curve) + } ExportKeyAlgorithm::Aes {} | ExportKeyAlgorithm::Hmac {} => { export_key_symmetric(opts.format, key_data) } @@ -200,3 +220,156 @@ fn export_key_symmetric( _ => Err(unsupported_format()), } } + +fn export_key_ec( + format: ExportKeyFormat, + key_data: RawKeyData, + algorithm: ExportKeyAlgorithm, + named_curve: EcNamedCurve, +) -> Result<ExportKeyResult, deno_core::anyhow::Error> { + match format { + ExportKeyFormat::Spki => { + let subject_public_key = match named_curve { + EcNamedCurve::P256 => { + let point = key_data.as_ec_public_key_p256()?; + + point.as_ref().to_vec() + } + EcNamedCurve::P384 => { + let point = key_data.as_ec_public_key_p384()?; + + point.as_ref().to_vec() + } + EcNamedCurve::P521 => { + return Err(data_error("Unsupported named curve")) + } + }; + + let alg_id = match named_curve { + EcNamedCurve::P256 => <p256::NistP256 as p256::elliptic_curve::AlgorithmParameters>::algorithm_identifier(), + EcNamedCurve::P384 => <p384::NistP384 as p384::elliptic_curve::AlgorithmParameters>::algorithm_identifier(), + EcNamedCurve::P521 => return Err(data_error("Unsupported named curve")) + }; + + let alg_id = match algorithm { + ExportKeyAlgorithm::Ecdh { .. } => AlgorithmIdentifier { + oid: ObjectIdentifier::new("1.3.132.1.12"), + parameters: alg_id.parameters, + }, + _ => alg_id, + }; + + // the SPKI structure + let key_info = spki::SubjectPublicKeyInfo { + algorithm: alg_id, + subject_public_key: &subject_public_key, + }; + + let spki_der = key_info.to_vec().unwrap(); + + Ok(ExportKeyResult::Spki(spki_der.into())) + } + ExportKeyFormat::Pkcs8 => { + // private_key is a PKCS#8 DER-encoded private key + let private_key = key_data.as_ec_private_key()?; + + Ok(ExportKeyResult::Pkcs8(private_key.to_vec().into())) + } + ExportKeyFormat::JwkPublic => match named_curve { + EcNamedCurve::P256 => { + let point = key_data.as_ec_public_key_p256()?; + let coords = point.coordinates(); + + if let p256::elliptic_curve::sec1::Coordinates::Uncompressed { x, y } = + coords + { + Ok(ExportKeyResult::JwkPublicEc { + x: bytes_to_b64(x), + y: bytes_to_b64(y), + }) + } else { + Err(custom_error( + "DOMExceptionOperationError", + "failed to decode public key", + )) + } + } + EcNamedCurve::P384 => { + let point = key_data.as_ec_public_key_p384()?; + let coords = point.coordinates(); + + if let p384::elliptic_curve::sec1::Coordinates::Uncompressed { x, y } = + coords + { + Ok(ExportKeyResult::JwkPublicEc { + x: bytes_to_b64(x), + y: bytes_to_b64(y), + }) + } else { + Err(custom_error( + "DOMExceptionOperationError", + "failed to decode public key", + )) + } + } + EcNamedCurve::P521 => Err(data_error("Unsupported named curve")), + }, + ExportKeyFormat::JwkPrivate => { + let private_key = key_data.as_ec_private_key()?; + + match named_curve { + EcNamedCurve::P256 => { + let ec_key = ECPrivateKey::<p256::NistP256>::try_from(private_key) + .map_err(|_| { + custom_error( + "DOMExceptionOperationError", + "failed to decode private key", + ) + })?; + + let point = p256::EncodedPoint::from_bytes(&ec_key.encoded_point) + .map_err(|_| data_error("expected valid public EC key"))?; + + if let elliptic_curve::sec1::Coordinates::Uncompressed { x, y } = + point.coordinates() + { + Ok(ExportKeyResult::JwkPrivateEc { + x: bytes_to_b64(x), + y: bytes_to_b64(y), + d: bytes_to_b64(&ec_key.private_d), + }) + } else { + Err(data_error("expected valid public EC key")) + } + } + + EcNamedCurve::P384 => { + let ec_key = ECPrivateKey::<p384::NistP384>::try_from(private_key) + .map_err(|_| { + custom_error( + "DOMExceptionOperationError", + "failed to decode private key", + ) + })?; + + let point = p384::EncodedPoint::from_bytes(&ec_key.encoded_point) + .map_err(|_| data_error("expected valid public EC key"))?; + + if let elliptic_curve::sec1::Coordinates::Uncompressed { x, y } = + point.coordinates() + { + Ok(ExportKeyResult::JwkPrivateEc { + x: bytes_to_b64(x), + y: bytes_to_b64(y), + d: bytes_to_b64(&ec_key.private_d), + }) + } else { + Err(data_error("expected valid public EC key")) + } + } + _ => Err(not_supported_error("Unsupported namedCurve")), + } + } + ExportKeyFormat::JwkSecret => Err(unsupported_format()), + } +} diff --git a/ext/crypto/lib.rs b/ext/crypto/lib.rs index 0331d0dc3..f33c25f00 100644 --- a/ext/crypto/lib.rs +++ b/ext/crypto/lib.rs @@ -58,6 +58,7 @@ use std::path::PathBuf; pub use rand; // Re-export rand mod decrypt; +mod ec_key; mod encrypt; mod export_key; mod generate_key; diff --git a/ext/crypto/shared.rs b/ext/crypto/shared.rs index 3b32bb2a2..de287efb0 100644 --- a/ext/crypto/shared.rs +++ b/ext/crypto/shared.rs @@ -106,6 +106,35 @@ impl RawKeyData { _ => Err(type_error("expected secret key")), } } + + pub fn as_ec_public_key_p256(&self) -> Result<p256::EncodedPoint, AnyError> { + match self { + RawKeyData::Public(data) => { + // public_key is a serialized EncodedPoint + p256::EncodedPoint::from_bytes(&data) + .map_err(|_| type_error("expected valid private EC key")) + } + _ => Err(type_error("expected private key")), + } + } + + pub fn as_ec_public_key_p384(&self) -> Result<p384::EncodedPoint, AnyError> { + match self { + RawKeyData::Public(data) => { + // public_key is a serialized EncodedPoint + p384::EncodedPoint::from_bytes(&data) + .map_err(|_| type_error("expected valid private EC key")) + } + _ => Err(type_error("expected private key")), + } + } + + pub fn as_ec_private_key(&self) -> Result<&[u8], AnyError> { + match self { + RawKeyData::Private(data) => Ok(data), + _ => Err(type_error("expected private key")), + } + } } pub fn data_error(msg: impl Into<Cow<'static, str>>) -> AnyError { |