Repository: denoland/deno
Pull request: fix(ext/node): exporting rsa public keys

Initial support for exporting rsa public KeyObject.

Current assumption is that RSA keys are stored in pkcs1 der format in key storage.

Ref https://github.com/denoland/deno/issues/23471 
Ref https://github.com/denoland/deno/issues/18928
Ref https://github.com/denoland/deno/issues/21124


Changed files:
filename: ext/node/lib.rs
patch: @@ -241,6 +241,8 @@ deno_core::extension!(deno_node,
     ops::crypto::op_node_ecdh_compute_secret,
     ops::crypto::op_node_ecdh_compute_public_key,
     ops::crypto::op_node_ecdh_encode_pubkey,
+    ops::crypto::op_node_export_rsa_public_pem,
+    ops::crypto::op_node_export_rsa_spki_der,
     ops::crypto::x509::op_node_x509_parse,
     ops::crypto::x509::op_node_x509_ca,
     ops::crypto::x509::op_node_x509_check_email,

filename: ext/node/ops/crypto/mod.rs
patch: @@ -42,6 +42,7 @@ use rsa::Oaep;
 use rsa::Pkcs1v15Encrypt;
 use rsa::RsaPrivateKey;
 use rsa::RsaPublicKey;
+use spki::EncodePublicKey;
 
 mod cipher;
 mod dh;
@@ -681,13 +682,32 @@ pub async fn op_node_generate_rsa_async(
   spawn_blocking(move || generate_rsa(modulus_length, public_exponent)).await?
 }
 
+#[op2]
+#[string]
+pub fn op_node_export_rsa_public_pem(
+  #[buffer] pkcs1_der: &[u8],
+) -> Result<String, AnyError> {
+  let public_key = RsaPublicKey::from_pkcs1_der(pkcs1_der)?;
+  let export = public_key.to_public_key_pem(Default::default())?;
+  Ok(export)
+}
+
+#[op2]
+#[serde]
+pub fn op_node_export_rsa_spki_der(
+  #[buffer] pkcs1_der: &[u8],
+) -> Result<ToJsBuffer, AnyError> {
+  let public_key = RsaPublicKey::from_pkcs1_der(pkcs1_der)?;
+  let export = public_key.to_public_key_der()?.to_vec();
+  Ok(export.into())
+}
+
 fn dsa_generate(
   modulus_length: usize,
   divisor_length: usize,
 ) -> Result<(ToJsBuffer, ToJsBuffer), AnyError> {
   let mut rng = rand::thread_rng();
   use dsa::pkcs8::EncodePrivateKey;
-  use dsa::pkcs8::EncodePublicKey;
   use dsa::Components;
   use dsa::KeySize;
   use dsa::SigningKey;

filename: ext/node/polyfills/internal/crypto/keygen.ts
patch: @@ -7,6 +7,8 @@
 import { KeyObject } from "ext:deno_node/internal/crypto/keys.ts";
 import { kAesKeyLengths } from "ext:deno_node/internal/crypto/util.ts";
 import {
+  PrivateKeyObject,
+  PublicKeyObject,
   SecretKeyObject,
   setOwnedKey,
 } from "ext:deno_node/internal/crypto/keys.ts";
@@ -564,8 +566,8 @@ export function generateKeyPair(
   ) => void,
 ) {
   createJob(kAsync, type, options).then(([privateKey, publicKey]) => {
-    privateKey = new KeyObject("private", setOwnedKey(privateKey));
-    publicKey = new KeyObject("public", setOwnedKey(publicKey));
+    privateKey = new PrivateKeyObject(setOwnedKey(privateKey), { type });
+    publicKey = new PublicKeyObject(setOwnedKey(publicKey), { type });
 
     if (typeof options === "object" && options !== null) {
       const { publicKeyEncoding, privateKeyEncoding } = options as any;
@@ -766,8 +768,8 @@ export function generateKeyPairSync(
   | KeyPairSyncResult<string | Buffer, string | Buffer> {
   let [privateKey, publicKey] = createJob(kSync, type, options);
 
-  privateKey = new KeyObject("private", setOwnedKey(privateKey));
-  publicKey = new KeyObject("public", setOwnedKey(publicKey));
+  privateKey = new PrivateKeyObject(setOwnedKey(privateKey), { type });
+  publicKey = new PublicKeyObject(setOwnedKey(publicKey), { type });
 
   if (typeof options === "object" && options !== null) {
     const { publicKeyEncoding, privateKeyEncoding } = options as any;

filename: ext/node/polyfills/internal/crypto/keys.ts
patch: @@ -7,6 +7,8 @@
 import {
   op_node_create_private_key,
   op_node_create_public_key,
+  op_node_export_rsa_public_pem,
+  op_node_export_rsa_spki_der,
 } from "ext:core/ops";
 
 import {
@@ -360,7 +362,7 @@ class AsymmetricKeyObject extends KeyObject {
   }
 }
 
-class PrivateKeyObject extends AsymmetricKeyObject {
+export class PrivateKeyObject extends AsymmetricKeyObject {
   constructor(handle: unknown, details: unknown) {
     super("private", handle, details);
   }
@@ -370,13 +372,35 @@ class PrivateKeyObject extends AsymmetricKeyObject {
   }
 }
 
-class PublicKeyObject extends AsymmetricKeyObject {
+export class PublicKeyObject extends AsymmetricKeyObject {
   constructor(handle: unknown, details: unknown) {
     super("public", handle, details);
   }
 
-  export(_options: unknown) {
-    notImplemented("crypto.PublicKeyObject.prototype.export");
+  export(options: unknown) {
+    const key = KEY_STORE.get(this[kHandle]);
+    switch (this.asymmetricKeyType) {
+      case "rsa":
+      case "rsa-pss": {
+        switch (options.format) {
+          case "pem":
+            return op_node_export_rsa_public_pem(key);
+          case "der": {
+            if (options.type == "pkcs1") {
+              return key;
+            } else {
+              return op_node_export_rsa_spki_der(key);
+            }
+          }
+          default:
+            throw new TypeError(`exporting ${options.type} is not implemented`);
+        }
+      }
+      default:
+        throw new TypeError(
+          `exporting ${this.asymmetricKeyType} is not implemented`,
+        );
+    }
   }
 }
 
@@ -414,4 +438,6 @@ export default {
   prepareSecretKey,
   setOwnedKey,
   SecretKeyObject,
+  PrivateKeyObject,
+  PublicKeyObject,
 };

filename: tests/unit_node/crypto/crypto_key_test.ts
patch: @@ -15,7 +15,7 @@ import {
 } from "node:crypto";
 import { promisify } from "node:util";
 import { Buffer } from "node:buffer";
-import { assertEquals, assertThrows } from "@std/assert/mod.ts";
+import { assert, assertEquals, assertThrows } from "@std/assert/mod.ts";
 
 const RUN_SLOW_TESTS = Deno.env.get("SLOW_TESTS") === "1";
 
@@ -402,3 +402,16 @@ SogaIHQjE81ZkmNtU5gM5Q==
     `jEwckJ/d5GkF/8TTm+wllq2JNghG/m2JYJIW7vS8Vms53zCTTNSSegTSoIVoxWymwTPw2dTtZi41Lg0O271/WvEmQhiWD2dnjz6D/0F4eyn+QUhcmGCadDFyfp7+8x1XOppSw2YB8vL5WCL0QDdp3TAa/rWI0Hn4OftHMa6HPvatkGs+8XlQOGCCfd3TLg+t1UROgpgmetjoAM67mlwxXMGGu/Tr/EbXnnINKeB0iuSmD1FCxlrgFuYWDKxd79n2jZ74FrS/zto+bqWSI5uUa4Ar7yvXtek1Cu1OFM6vgdN9Y6Po2UD9+IT04EhU03LUDY5paYOO8yohz7p7kqHvpA==`,
   );
 });
+
+Deno.test("generate rsa export public key", async function () {
+  const { publicKey } = await generateKeyPairAsync("rsa", {
+    modulusLength: 2048,
+  });
+
+  const spkiPem = publicKey.export({ format: "pem", type: "spki" });
+  assert(typeof spkiPem === "string");
+  assert(spkiPem.startsWith("-----BEGIN PUBLIC KEY-----"));
+
+  const der = publicKey.export({ format: "der", type: "spki" });
+  assert(der instanceof Uint8Array);
+});