Repository: denoland/deno
Pull request: fix(ext/node): support private key export in JWK format

Closes https://github.com/denoland/deno/issues/26643

Changed files:
filename: ext/node/lib.rs
patch: @@ -262,6 +262,7 @@ deno_core::extension!(deno_node,
     ops::crypto::keys::op_node_derive_public_key_from_private_key,
     ops::crypto::keys::op_node_dh_keys_generate_and_export,
     ops::crypto::keys::op_node_export_private_key_der,
+    ops::crypto::keys::op_node_export_private_key_jwk,
     ops::crypto::keys::op_node_export_private_key_pem,
     ops::crypto::keys::op_node_export_public_key_der,
     ops::crypto::keys::op_node_export_public_key_pem,

filename: ext/node/ops/crypto/keys.rs
patch: @@ -26,6 +26,7 @@ use rsa::pkcs1::DecodeRsaPrivateKey as _;
 use rsa::pkcs1::DecodeRsaPublicKey;
 use rsa::pkcs1::EncodeRsaPrivateKey as _;
 use rsa::pkcs1::EncodeRsaPublicKey;
+use rsa::traits::PrivateKeyParts;
 use rsa::traits::PublicKeyParts;
 use rsa::RsaPrivateKey;
 use rsa::RsaPublicKey;
@@ -255,6 +256,16 @@ impl EcPrivateKey {
       EcPrivateKey::P384(key) => EcPublicKey::P384(key.public_key()),
     }
   }
+
+  pub fn to_jwk(&self) -> Result<JwkEcKey, AsymmetricPrivateKeyJwkError> {
+    match self {
+      EcPrivateKey::P224(_) => {
+        Err(AsymmetricPrivateKeyJwkError::UnsupportedJwkEcCurveP224)
+      }
+      EcPrivateKey::P256(key) => Ok(key.to_jwk()),
+      EcPrivateKey::P384(key) => Ok(key.to_jwk()),
+    }
+  }
 }
 
 // https://oidref.com/
@@ -1107,6 +1118,16 @@ fn bytes_to_b64(bytes: &[u8]) -> String {
   BASE64_URL_SAFE_NO_PAD.encode(bytes)
 }
 
+#[derive(Debug, thiserror::Error)]
+pub enum AsymmetricPrivateKeyJwkError {
+  #[error("key is not an asymmetric private key")]
+  KeyIsNotAsymmetricPrivateKey,
+  #[error("Unsupported JWK EC curve: P224")]
+  UnsupportedJwkEcCurveP224,
+  #[error("jwk export not implemented for this key type")]
+  JwkExportNotImplementedForKeyType,
+}
+
 #[derive(Debug, thiserror::Error)]
 pub enum AsymmetricPublicKeyJwkError {
   #[error("key is not an asymmetric public key")]
@@ -1328,7 +1349,73 @@ pub enum AsymmetricPrivateKeyDerError {
   UnsupportedKeyType(String),
 }
 
+// https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2
+fn rsa_private_to_jwk(key: &RsaPrivateKey) -> deno_core::serde_json::Value {
+  let n = key.n();
+  let e = key.e();
+  let d = key.d();
+  let p = &key.primes()[0];
+  let q = &key.primes()[1];
+  let dp = key.dp();
+  let dq = key.dq();
+  let qi = key.crt_coefficient();
+  let oth = &key.primes()[2..];
+
+  let mut obj = deno_core::serde_json::json!({
+      "kty": "RSA",
+      "n": bytes_to_b64(&n.to_bytes_be()),
+      "e": bytes_to_b64(&e.to_bytes_be()),
+      "d": bytes_to_b64(&d.to_bytes_be()),
+      "p": bytes_to_b64(&p.to_bytes_be()),
+      "q": bytes_to_b64(&q.to_bytes_be()),
+      "dp": dp.map(|dp| bytes_to_b64(&dp.to_bytes_be())),
+      "dq": dq.map(|dq| bytes_to_b64(&dq.to_bytes_be())),
+      "qi": qi.map(|qi| bytes_to_b64(&qi.to_bytes_be())),
+  });
+
+  if !oth.is_empty() {
+    obj["oth"] = deno_core::serde_json::json!(oth
+      .iter()
+      .map(|o| o.to_bytes_be())
+      .collect::<Vec<_>>());
+  }
+
+  obj
+}
+
 impl AsymmetricPrivateKey {
+  fn export_jwk(
+    &self,
+  ) -> Result<deno_core::serde_json::Value, AsymmetricPrivateKeyJwkError> {
+    match self {
+      AsymmetricPrivateKey::Rsa(key) => Ok(rsa_private_to_jwk(key)),
+      AsymmetricPrivateKey::RsaPss(key) => Ok(rsa_private_to_jwk(&key.key)),
+      AsymmetricPrivateKey::Ec(key) => {
+        let jwk = key.to_jwk()?;
+        Ok(deno_core::serde_json::json!(jwk))
+      }
+      AsymmetricPrivateKey::X25519(static_secret) => {
+        let bytes = static_secret.to_bytes();
+
+        Ok(deno_core::serde_json::json!({
+            "kty": "OKP",
+            "crv": "X25519",
+            "d": bytes_to_b64(&bytes),
+        }))
+      }
+      AsymmetricPrivateKey::Ed25519(key) => {
+        let bytes = key.to_bytes();
+
+        Ok(deno_core::serde_json::json!({
+            "kty": "OKP",
+            "crv": "Ed25519",
+            "d": bytes_to_b64(&bytes),
+        }))
+      }
+      _ => Err(AsymmetricPrivateKeyJwkError::JwkExportNotImplementedForKeyType),
+    }
+  }
+
   fn export_der(
     &self,
     typ: &str,
@@ -2329,6 +2416,28 @@ pub fn op_node_export_private_key_pem(
   Ok(String::from_utf8(out).expect("invalid pem is not possible"))
 }
 
+#[derive(Debug, thiserror::Error)]
+pub enum ExportPrivateKeyJwkError {
+  #[error(transparent)]
+  AsymmetricPublicKeyJwk(#[from] AsymmetricPrivateKeyJwkError),
+  #[error("very large data")]
+  VeryLargeData,
+  #[error(transparent)]
+  Der(#[from] der::Error),
+}
+
+#[op2]
+#[serde]
+pub fn op_node_export_private_key_jwk(
+  #[cppgc] handle: &KeyObjectHandle,
+) -> Result<deno_core::serde_json::Value, ExportPrivateKeyJwkError> {
+  let private_key = handle
+    .as_private_key()
+    .ok_or(AsymmetricPrivateKeyJwkError::KeyIsNotAsymmetricPrivateKey)?;
+
+  Ok(private_key.export_jwk()?)
+}
+
 #[op2]
 #[buffer]
 pub fn op_node_export_private_key_der(

filename: ext/node/polyfills/internal/crypto/keys.ts
patch: @@ -20,6 +20,7 @@ import {
   op_node_create_secret_key,
   op_node_derive_public_key_from_private_key,
   op_node_export_private_key_der,
+  op_node_export_private_key_jwk,
   op_node_export_private_key_pem,
   op_node_export_public_key_der,
   op_node_export_public_key_jwk,
@@ -791,7 +792,7 @@ export class PrivateKeyObject extends AsymmetricKeyObject {
 
   export(options: JwkKeyExportOptions | KeyExportOptions<KeyFormat>) {
     if (options && options.format === "jwk") {
-      notImplemented("jwk private key export not implemented");
+      return op_node_export_private_key_jwk(this[kHandle]);
     }
     const {
       format,

filename: tests/unit_node/crypto/crypto_key_test.ts
patch: @@ -700,3 +700,19 @@ Deno.test("generateKeyPair promisify", async () => {
   assert(publicKey.startsWith("-----BEGIN PUBLIC KEY-----"));
   assert(privateKey.startsWith("-----BEGIN PRIVATE KEY-----"));
 });
+
+Deno.test("RSA export private JWK", function () {
+  // @ts-ignore @types/node broken
+  const { privateKey, publicKey } = generateKeyPairSync("rsa", {
+    modulusLength: 4096,
+    publicKeyEncoding: {
+      format: "jwk",
+    },
+    privateKeyEncoding: {
+      format: "jwk",
+    },
+  });
+
+  assertEquals((privateKey as any).kty, "RSA");
+  assertEquals((privateKey as any).n, (publicKey as any).n);
+});