Repository: denoland/deno
Pull request: fix(ext/node): import JWK octet key pairs

Ref https://github.com/denoland/deno/issues/24129

`kty: "okp"` is defined in [rfc8037](https://www.rfc-editor.org/rfc/rfc8037.html)


Changed files:
filename: ext/node/lib.rs
patch: @@ -232,6 +232,7 @@ deno_core::extension!(deno_node,
     ops::crypto::op_node_verify,
     ops::crypto::op_node_verify_ed25519,
     ops::crypto::keys::op_node_create_private_key,
+    ops::crypto::keys::op_node_create_ed_raw,
     ops::crypto::keys::op_node_create_public_key,
     ops::crypto::keys::op_node_create_secret_key,
     ops::crypto::keys::op_node_derive_public_key_from_private_key,

filename: ext/node/ops/crypto/keys.rs
patch: @@ -571,6 +571,50 @@ impl KeyObjectHandle {
     Ok(KeyObjectHandle::AsymmetricPublic(key))
   }
 
+  pub fn new_ed_raw(
+    curve: &str,
+    data: &[u8],
+    is_public: bool,
+  ) -> Result<KeyObjectHandle, AnyError> {
+    match curve {
+      "Ed25519" => {
+        let data = data
+          .try_into()
+          .map_err(|_| type_error("invalid Ed25519 key"))?;
+        if !is_public {
+          Ok(KeyObjectHandle::AsymmetricPrivate(
+            AsymmetricPrivateKey::Ed25519(
+              ed25519_dalek::SigningKey::from_bytes(data),
+            ),
+          ))
+        } else {
+          Ok(KeyObjectHandle::AsymmetricPublic(
+            AsymmetricPublicKey::Ed25519(
+              ed25519_dalek::VerifyingKey::from_bytes(data)?,
+            ),
+          ))
+        }
+      }
+      "X25519" => {
+        let data: [u8; 32] = data
+          .try_into()
+          .map_err(|_| type_error("invalid x25519 key"))?;
+        if !is_public {
+          Ok(KeyObjectHandle::AsymmetricPrivate(
+            AsymmetricPrivateKey::X25519(x25519_dalek::StaticSecret::from(
+              data,
+            )),
+          ))
+        } else {
+          Ok(KeyObjectHandle::AsymmetricPublic(
+            AsymmetricPublicKey::X25519(x25519_dalek::PublicKey::from(data)),
+          ))
+        }
+      }
+      _ => Err(type_error("unsupported curve")),
+    }
+  }
+
   pub fn new_asymmetric_public_key_from_js(
     key: &[u8],
     format: &str,
@@ -1027,6 +1071,16 @@ pub fn op_node_create_private_key(
   )
 }
 
+#[op2]
+#[cppgc]
+pub fn op_node_create_ed_raw(
+  #[string] curve: &str,
+  #[buffer] key: &[u8],
+  is_public: bool,
+) -> Result<KeyObjectHandle, AnyError> {
+  KeyObjectHandle::new_ed_raw(curve, key, is_public)
+}
+
 #[op2]
 #[cppgc]
 pub fn op_node_create_public_key(

filename: ext/node/polyfills/internal/crypto/keys.ts
patch: @@ -12,6 +12,7 @@ const {
 } = primordials;
 
 import {
+  op_node_create_ed_raw,
   op_node_create_private_key,
   op_node_create_public_key,
   op_node_create_secret_key,
@@ -32,6 +33,7 @@ import { kHandle } from "ext:deno_node/internal/crypto/constants.ts";
 import { isStringOrBuffer } from "ext:deno_node/internal/crypto/cipher.ts";
 import {
   ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
+  ERR_CRYPTO_INVALID_JWK,
   ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE,
   ERR_INVALID_ARG_TYPE,
   ERR_INVALID_ARG_VALUE,
@@ -56,6 +58,7 @@ import {
 import {
   validateObject,
   validateOneOf,
+  validateString,
 } from "ext:deno_node/internal/validators.mjs";
 import { BufferEncoding } from "ext:deno_node/_global.d.ts";
 
@@ -256,6 +259,64 @@ export function getKeyObjectHandle(key: KeyObject, ctx: KeyHandleContext) {
   return key[kHandle];
 }
 
+function getKeyObjectHandleFromJwk(key, ctx) {
+  validateObject(key, "key");
+  validateOneOf(
+    key.kty,
+    "key.kty",
+    ["RSA", "EC", "OKP"],
+  );
+  const isPublic = ctx === kConsumePublic || ctx === kCreatePublic;
+
+  if (key.kty === "OKP") {
+    validateString(key.crv, "key.crv");
+    validateOneOf(
+      key.crv,
+      "key.crv",
+      ["Ed25519", "Ed448", "X25519", "X448"],
+    );
+    validateString(key.x, "key.x");
+
+    if (!isPublic) {
+      validateString(key.d, "key.d");
+    }
+
+    let keyData;
+    if (isPublic) {
+      keyData = Buffer.from(key.x, "base64");
+    } else {
+      keyData = Buffer.from(key.d, "base64");
+    }
+
+    switch (key.crv) {
+      case "Ed25519":
+      case "X25519":
+        if (keyData.byteLength !== 32) {
+          throw new ERR_CRYPTO_INVALID_JWK();
+        }
+        break;
+      case "Ed448":
+        if (keyData.byteLength !== 57) {
+          throw new ERR_CRYPTO_INVALID_JWK();
+        }
+        break;
+      case "X448":
+        if (keyData.byteLength !== 56) {
+          throw new ERR_CRYPTO_INVALID_JWK();
+        }
+        break;
+    }
+
+    return op_node_create_ed_raw(key.crv, keyData, isPublic);
+  }
+
+  if (key.kty === "EC") {
+    throw new TypeError("ec jwk imports not implemented");
+  }
+
+  throw new TypeError("rsa jwk imports not implemented");
+}
+
 export function prepareAsymmetricKey(
   key:
     | string
@@ -306,7 +367,12 @@ export function prepareAsymmetricKey(
     } else if (isCryptoKey(data)) {
       notImplemented("using CryptoKey as input");
     } else if (isJwk(data) && format === "jwk") {
-      notImplemented("using JWK as input");
+      return {
+        // @ts-ignore __proto__ is magic
+        __proto__: null,
+        handle: getKeyObjectHandleFromJwk(data, ctx),
+        format,
+      };
     }
     // Either PEM or DER using PKCS#1 or SPKI.
     if (!isStringOrBuffer(data)) {

filename: ext/node/polyfills/internal/errors.ts
patch: @@ -927,6 +927,12 @@ export class ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE extends NodeTypeError {
   }
 }
 
+export class ERR_CRYPTO_INVALID_JWK extends NodeError {
+  constructor() {
+    super("ERR_CRYPTO_INVALID_JWK", "Invalid JWK");
+  }
+}
+
 export class ERR_CRYPTO_INVALID_STATE extends NodeError {
   constructor(x: string) {
     super("ERR_CRYPTO_INVALID_STATE", `Invalid state for operation ${x}`);
@@ -2733,6 +2739,7 @@ export default {
   ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS,
   ERR_CRYPTO_INVALID_DIGEST,
   ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE,
+  ERR_CRYPTO_INVALID_JWK,
   ERR_CRYPTO_INVALID_STATE,
   ERR_CRYPTO_PBKDF2_ERROR,
   ERR_CRYPTO_SCRYPT_INVALID_PARAMETER,

filename: tests/unit_node/crypto/crypto_key_test.ts
patch: @@ -439,3 +439,61 @@ Deno.test("create private key with invalid utf-8 string", function () {
     "not valid utf8",
   );
 });
+
+Deno.test("Ed25519 jwk public key #1", function () {
+  const key = {
+    "kty": "OKP",
+    "crv": "Ed25519",
+    "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
+    "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
+  };
+  const keyObject = createPublicKey({ key, format: "jwk" });
+
+  assertEquals(keyObject.type, "public");
+  const spkiActual = keyObject.export({ type: "spki", format: "pem" });
+
+  const spkiExpected = `-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
+-----END PUBLIC KEY-----
+`;
+
+  assertEquals(spkiActual, spkiExpected);
+});
+
+Deno.test("Ed25519 jwk public key #2", function () {
+  const key = {
+    "kty": "OKP",
+    "crv": "Ed25519",
+    "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
+  };
+
+  const keyObject = createPublicKey({ key, format: "jwk" });
+  assertEquals(keyObject.type, "public");
+
+  const spki = keyObject.export({ type: "spki", format: "pem" });
+  const spkiExpected = `-----BEGIN PUBLIC KEY-----
+MCowBQYDK2VwAyEA11qYAYKxCrfVS/7TyWQHOg7hcvPapiMlrwIaaPcHURo=
+-----END PUBLIC KEY-----
+`;
+  assertEquals(spki, spkiExpected);
+});
+
+Deno.test("Ed25519 jwk private key", function () {
+  const key = {
+    "kty": "OKP",
+    "crv": "Ed25519",
+    "d": "nWGxne_9WmC6hEr0kuwsxERJxWl7MmkZcDusAxyuf2A",
+    "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo",
+  };
+
+  const keyObject = createPrivateKey({ key, format: "jwk" });
+  assertEquals(keyObject.type, "private");
+
+  const pkcs8Actual = keyObject.export({ type: "pkcs8", format: "pem" });
+  const pkcs8Expected = `-----BEGIN PRIVATE KEY-----
+MC4CAQAwBQYDK2VwBCIEIJ1hsZ3v/VpguoRK9JLsLMREScVpezJpGXA7rAMcrn9g
+-----END PRIVATE KEY-----
+`;
+
+  assertEquals(pkcs8Actual, pkcs8Expected);
+});