Skip to content

Commit

Permalink
fix(ext/node): support private key export in JWK format (#27325)
Browse files Browse the repository at this point in the history
Closes #26643

---------

Co-authored-by: Divy Srivastava <[email protected]>
  • Loading branch information
bartlomieju and littledivy authored Dec 31, 2024
1 parent 7b491a2 commit 1cd3600
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 1 deletion.
1 change: 1 addition & 0 deletions ext/node/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
109 changes: 109 additions & 0 deletions ext/node/ops/crypto/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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/
Expand Down Expand Up @@ -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")]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion ext/node/polyfills/internal/crypto/keys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
16 changes: 16 additions & 0 deletions tests/unit_node/crypto/crypto_key_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});

0 comments on commit 1cd3600

Please sign in to comment.