summaryrefslogtreecommitdiff
path: root/ext/crypto
diff options
context:
space:
mode:
authorSean Michael Wykes <8363933+SeanWykes@users.noreply.github.com>2022-01-19 00:38:35 -0300
committerGitHub <noreply@github.com>2022-01-19 09:08:35 +0530
commit77e58fe7f9fc20dabf77424efbd25ce332f8f59c (patch)
treee8cdf3b06661b209ea685b5d762f670e662923b7 /ext/crypto
parentb3545dd447dcbab6629827dbe8d127ef82f8da69 (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.js126
-rw-r--r--ext/crypto/ec_key.rs150
-rw-r--r--ext/crypto/export_key.rs173
-rw-r--r--ext/crypto/lib.rs1
-rw-r--r--ext/crypto/shared.rs29
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 {