Skip to content

Commit 1cd3600

Browse files
fix(ext/node): support private key export in JWK format (#27325)
Closes #26643 --------- Co-authored-by: Divy Srivastava <[email protected]>
1 parent 7b491a2 commit 1cd3600

File tree

4 files changed

+128
-1
lines changed

4 files changed

+128
-1
lines changed

ext/node/lib.rs

+1
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ deno_core::extension!(deno_node,
262262
ops::crypto::keys::op_node_derive_public_key_from_private_key,
263263
ops::crypto::keys::op_node_dh_keys_generate_and_export,
264264
ops::crypto::keys::op_node_export_private_key_der,
265+
ops::crypto::keys::op_node_export_private_key_jwk,
265266
ops::crypto::keys::op_node_export_private_key_pem,
266267
ops::crypto::keys::op_node_export_public_key_der,
267268
ops::crypto::keys::op_node_export_public_key_pem,

ext/node/ops/crypto/keys.rs

+109
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use rsa::pkcs1::DecodeRsaPrivateKey as _;
2626
use rsa::pkcs1::DecodeRsaPublicKey;
2727
use rsa::pkcs1::EncodeRsaPrivateKey as _;
2828
use rsa::pkcs1::EncodeRsaPublicKey;
29+
use rsa::traits::PrivateKeyParts;
2930
use rsa::traits::PublicKeyParts;
3031
use rsa::RsaPrivateKey;
3132
use rsa::RsaPublicKey;
@@ -255,6 +256,16 @@ impl EcPrivateKey {
255256
EcPrivateKey::P384(key) => EcPublicKey::P384(key.public_key()),
256257
}
257258
}
259+
260+
pub fn to_jwk(&self) -> Result<JwkEcKey, AsymmetricPrivateKeyJwkError> {
261+
match self {
262+
EcPrivateKey::P224(_) => {
263+
Err(AsymmetricPrivateKeyJwkError::UnsupportedJwkEcCurveP224)
264+
}
265+
EcPrivateKey::P256(key) => Ok(key.to_jwk()),
266+
EcPrivateKey::P384(key) => Ok(key.to_jwk()),
267+
}
268+
}
258269
}
259270

260271
// https://oidref.com/
@@ -1107,6 +1118,16 @@ fn bytes_to_b64(bytes: &[u8]) -> String {
11071118
BASE64_URL_SAFE_NO_PAD.encode(bytes)
11081119
}
11091120

1121+
#[derive(Debug, thiserror::Error)]
1122+
pub enum AsymmetricPrivateKeyJwkError {
1123+
#[error("key is not an asymmetric private key")]
1124+
KeyIsNotAsymmetricPrivateKey,
1125+
#[error("Unsupported JWK EC curve: P224")]
1126+
UnsupportedJwkEcCurveP224,
1127+
#[error("jwk export not implemented for this key type")]
1128+
JwkExportNotImplementedForKeyType,
1129+
}
1130+
11101131
#[derive(Debug, thiserror::Error)]
11111132
pub enum AsymmetricPublicKeyJwkError {
11121133
#[error("key is not an asymmetric public key")]
@@ -1328,7 +1349,73 @@ pub enum AsymmetricPrivateKeyDerError {
13281349
UnsupportedKeyType(String),
13291350
}
13301351

1352+
// https://datatracker.ietf.org/doc/html/rfc7518#section-6.3.2
1353+
fn rsa_private_to_jwk(key: &RsaPrivateKey) -> deno_core::serde_json::Value {
1354+
let n = key.n();
1355+
let e = key.e();
1356+
let d = key.d();
1357+
let p = &key.primes()[0];
1358+
let q = &key.primes()[1];
1359+
let dp = key.dp();
1360+
let dq = key.dq();
1361+
let qi = key.crt_coefficient();
1362+
let oth = &key.primes()[2..];
1363+
1364+
let mut obj = deno_core::serde_json::json!({
1365+
"kty": "RSA",
1366+
"n": bytes_to_b64(&n.to_bytes_be()),
1367+
"e": bytes_to_b64(&e.to_bytes_be()),
1368+
"d": bytes_to_b64(&d.to_bytes_be()),
1369+
"p": bytes_to_b64(&p.to_bytes_be()),
1370+
"q": bytes_to_b64(&q.to_bytes_be()),
1371+
"dp": dp.map(|dp| bytes_to_b64(&dp.to_bytes_be())),
1372+
"dq": dq.map(|dq| bytes_to_b64(&dq.to_bytes_be())),
1373+
"qi": qi.map(|qi| bytes_to_b64(&qi.to_bytes_be())),
1374+
});
1375+
1376+
if !oth.is_empty() {
1377+
obj["oth"] = deno_core::serde_json::json!(oth
1378+
.iter()
1379+
.map(|o| o.to_bytes_be())
1380+
.collect::<Vec<_>>());
1381+
}
1382+
1383+
obj
1384+
}
1385+
13311386
impl AsymmetricPrivateKey {
1387+
fn export_jwk(
1388+
&self,
1389+
) -> Result<deno_core::serde_json::Value, AsymmetricPrivateKeyJwkError> {
1390+
match self {
1391+
AsymmetricPrivateKey::Rsa(key) => Ok(rsa_private_to_jwk(key)),
1392+
AsymmetricPrivateKey::RsaPss(key) => Ok(rsa_private_to_jwk(&key.key)),
1393+
AsymmetricPrivateKey::Ec(key) => {
1394+
let jwk = key.to_jwk()?;
1395+
Ok(deno_core::serde_json::json!(jwk))
1396+
}
1397+
AsymmetricPrivateKey::X25519(static_secret) => {
1398+
let bytes = static_secret.to_bytes();
1399+
1400+
Ok(deno_core::serde_json::json!({
1401+
"kty": "OKP",
1402+
"crv": "X25519",
1403+
"d": bytes_to_b64(&bytes),
1404+
}))
1405+
}
1406+
AsymmetricPrivateKey::Ed25519(key) => {
1407+
let bytes = key.to_bytes();
1408+
1409+
Ok(deno_core::serde_json::json!({
1410+
"kty": "OKP",
1411+
"crv": "Ed25519",
1412+
"d": bytes_to_b64(&bytes),
1413+
}))
1414+
}
1415+
_ => Err(AsymmetricPrivateKeyJwkError::JwkExportNotImplementedForKeyType),
1416+
}
1417+
}
1418+
13321419
fn export_der(
13331420
&self,
13341421
typ: &str,
@@ -2329,6 +2416,28 @@ pub fn op_node_export_private_key_pem(
23292416
Ok(String::from_utf8(out).expect("invalid pem is not possible"))
23302417
}
23312418

2419+
#[derive(Debug, thiserror::Error)]
2420+
pub enum ExportPrivateKeyJwkError {
2421+
#[error(transparent)]
2422+
AsymmetricPublicKeyJwk(#[from] AsymmetricPrivateKeyJwkError),
2423+
#[error("very large data")]
2424+
VeryLargeData,
2425+
#[error(transparent)]
2426+
Der(#[from] der::Error),
2427+
}
2428+
2429+
#[op2]
2430+
#[serde]
2431+
pub fn op_node_export_private_key_jwk(
2432+
#[cppgc] handle: &KeyObjectHandle,
2433+
) -> Result<deno_core::serde_json::Value, ExportPrivateKeyJwkError> {
2434+
let private_key = handle
2435+
.as_private_key()
2436+
.ok_or(AsymmetricPrivateKeyJwkError::KeyIsNotAsymmetricPrivateKey)?;
2437+
2438+
Ok(private_key.export_jwk()?)
2439+
}
2440+
23322441
#[op2]
23332442
#[buffer]
23342443
pub fn op_node_export_private_key_der(

ext/node/polyfills/internal/crypto/keys.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
op_node_create_secret_key,
2121
op_node_derive_public_key_from_private_key,
2222
op_node_export_private_key_der,
23+
op_node_export_private_key_jwk,
2324
op_node_export_private_key_pem,
2425
op_node_export_public_key_der,
2526
op_node_export_public_key_jwk,
@@ -791,7 +792,7 @@ export class PrivateKeyObject extends AsymmetricKeyObject {
791792

792793
export(options: JwkKeyExportOptions | KeyExportOptions<KeyFormat>) {
793794
if (options && options.format === "jwk") {
794-
notImplemented("jwk private key export not implemented");
795+
return op_node_export_private_key_jwk(this[kHandle]);
795796
}
796797
const {
797798
format,

tests/unit_node/crypto/crypto_key_test.ts

+16
Original file line numberDiff line numberDiff line change
@@ -700,3 +700,19 @@ Deno.test("generateKeyPair promisify", async () => {
700700
assert(publicKey.startsWith("-----BEGIN PUBLIC KEY-----"));
701701
assert(privateKey.startsWith("-----BEGIN PRIVATE KEY-----"));
702702
});
703+
704+
Deno.test("RSA export private JWK", function () {
705+
// @ts-ignore @types/node broken
706+
const { privateKey, publicKey } = generateKeyPairSync("rsa", {
707+
modulusLength: 4096,
708+
publicKeyEncoding: {
709+
format: "jwk",
710+
},
711+
privateKeyEncoding: {
712+
format: "jwk",
713+
},
714+
});
715+
716+
assertEquals((privateKey as any).kty, "RSA");
717+
assertEquals((privateKey as any).n, (publicKey as any).n);
718+
});

0 commit comments

Comments
 (0)