Unverified Commit ac63f73a authored by Hong Minhee's avatar Hong Minhee
Browse files

CryptoKey utilities

parent 44c4e9d3
Loading
Loading
Loading
Loading
+8 −2
Original line number Diff line number Diff line
@@ -13,16 +13,22 @@ Version 0.3.0

To be released.

 -  Utility functions for responding with an ActivityPub object.
 -  Added utility functions for responding with an ActivityPub object:

     -  Added `respondWithObject()` function.
     -  Added `respondWithObjectIfAcceptable()` function.
     -  Added `RespondWithObjectOptions` interface.

 -  Added utility functions for generating and exporting cryptographic keys
    which are compatible with popular ActivityPub software:

     -  Added `generateCryptoKeyPair()` function.
     -  Added `exportJwk()` function.
     -  Added `importJwk()` function.

 -  The following functions and methods now throw `TypeError` if the specified
    `CryptoKey` is not `extractable`:

     -  `validateCryptoKey()` function
     -  `Context.getActorKey()` method
     -  `Context.sendActivity()` method
     -  `Federation.sendActivity()` method
+6 −26
Original line number Diff line number Diff line
/// <reference lib="deno.unstable" />
import { Temporal } from "npm:@js-temporal/polyfill@^0.4.4";
import { exportJwk, generateCryptoKeyPair, importJwk } from "fedify/httpsig";
import { hash, verify } from "scrypt";
import { openKv } from "./kv.ts";

@@ -22,24 +23,15 @@ export interface Blog extends BlogBase {

export async function setBlog(blog: BlogInput): Promise<void> {
  const kv = await openKv();
  const { privateKey, publicKey } = await crypto.subtle.generateKey(
    {
      name: "RSASSA-PKCS1-v1_5",
      modulusLength: 4096,
      publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
      hash: "SHA-256",
    },
    true,
    ["sign", "verify"],
  );
  const { privateKey, publicKey } = await generateCryptoKeyPair();
  await kv.set(["blog"], {
    handle: blog.handle,
    title: blog.title,
    description: blog.description,
    published: new Date().toISOString(),
    passwordHash: hash(blog.password, undefined, "scrypt"),
    privateKey: await crypto.subtle.exportKey("jwk", privateKey),
    publicKey: await crypto.subtle.exportKey("jwk", publicKey),
    privateKey: await exportJwk(privateKey),
    publicKey: await exportJwk(publicKey),
  });
}

@@ -56,20 +48,8 @@ export async function getBlog(): Promise<Blog | null> {
  if (entry == null || entry.value == null) return null;
  return {
    ...entry.value,
    privateKey: await crypto.subtle.importKey(
      "jwk",
      entry.value.privateKey,
      { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
      true,
      ["sign"],
    ),
    publicKey: await crypto.subtle.importKey(
      "jwk",
      entry.value.publicKey,
      { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
      true,
      ["verify"],
    ),
    privateKey: await importJwk(entry.value.privateKey, "private"),
    publicKey: await importJwk(entry.value.publicKey, "public"),
    published: Temporal.Instant.from(entry.value.published),
  };
}
+80 −2
Original line number Diff line number Diff line
import { assertThrows } from "jsr:@std/assert@^0.218.2";
import { validateCryptoKey } from "./key.ts";
import {
  assertEquals,
  assertRejects,
  assertThrows,
} from "jsr:@std/assert@^0.218.2";
import {
  exportJwk,
  generateCryptoKeyPair,
  importJwk,
  validateCryptoKey,
} from "./key.ts";
import { privateKey2, publicKey2 } from "../testing/keys.ts";

Deno.test("validateCryptoKey()", async () => {
  const pkcs1v15 = await crypto.subtle.generateKey(
@@ -58,3 +68,71 @@ Deno.test("validateCryptoKey()", async () => {
    "hash algorithm must be SHA-256",
  );
});

Deno.test("generateCryptoKeyPair()", async () => {
  const { privateKey, publicKey } = await generateCryptoKeyPair();
  validateCryptoKey(privateKey, "private");
  validateCryptoKey(publicKey, "public");
});

const publicJwk: JsonWebKey = {
  alg: "RS256",
  kty: "RSA",
  // cSpell: disable
  e: "AQAB",
  n: "oRmBtnxbdFutoRd1GLGwwGTrsqlRRWUe11hHQaoRLGf5LwQ0tIc6I9q-dynliw-2kxYsL" +
    "n9SH2je6HcTYOolgW7F_cOWXZQN04b-OiYcU1ConAhLjmn4k1uKawJ614y0ScPNd8PQ-Cl" +
    "jsnlPxbq9ofaCMe2BV3B6y09aCuGFJ0nxn1_ubjmIBIWWFTAznoz1J9BhJDGyt3IO3ABy3" +
    "f9zDVlR32L_n5VIkXnxkjUKdzMAOzYb62kuKOp1iznRTPrV71SNtivJMwSh_LVgBrmZjtI" +
    "n_oim-KyX_fdLU3tQ7VClyqmJzyAjccOH6Qj6nFTPh-vX07gqN8IlLT2uye4waw",
  // cSpell: enable
  key_ops: ["verify"],
  ext: true,
};

const privateJwk: JsonWebKey = {
  alg: "RS256",
  kty: "RSA",
  // cSpell: disable
  d: "f-Pa2L7Sb4YUSa1wlSEC-0li35uQ3DFRkY0QTG2xYnpMFGoXWTV9D1epGrqU8pePzias" +
    "_mCvFiZPx2Y4aRiYm68P2Mu7hCBz9XfWPN1iYTXIFM51BOLVpk3mjdsTICkgOusJI0m9j" +
    "DR3ZAjwLj14K6qhYvd0VbECmoItLjQoW64Sc9iDgD3CvGoTqv71oTfW70cy-Ve1xQ9CTh" +
    "AmMOTKe6rYCUTA8tMZcPszifZ4iOasOjgvRxyel86LqGNtyslY8k86gQlMtFpR3VeZV_8" +
    "otAWZn0mDc4vVU8HUO-DzYiIFdAcVxfPJh6tx7snCTsdzze_98OEAK4EWYBn7vsGFeQ",
  dp: "lrXReSkZQXSmSxQ1TimV5kMt96gSu4_r-OGIabVmoG5irhjMyN08Jjc3qK9oZS3uNM-Lx" +
    "AOg4OdzefjsF9IMfZJl6wuLd85g_l4BHSaEk5zC8l3QugX1IU9XZ7wDxXUrutMoNtZXDt" +
    "dbveAMtHNZlIu-qmEBDWzkqJiz2WpW-AE",
  dq: "TCLoYcX0ywuNA9DSU6v94KmBh1e_IELEFVbJb5vvLKlAK-ycMK0rfzC1co9Hhkski1Lsk" +
    "TnxnoqwZ5oF-7X10eZvy3Te_FHSl0IsTar8ST2-MRtGh2UjTdvP_nnygj4GcXvKfngjPE" +
    "fthDzVfVMeR38oDhDxMFD5AaY_v9aMH_U",
  e: "AQAB",
  n: "oRmBtnxbdFutoRd1GLGwwGTrsqlRRWUe11hHQaoRLGf5LwQ0tIc6I9q-dynliw-2kxYs" +
    "Ln9SH2je6HcTYOolgW7F_cOWXZQN04b-OiYcU1ConAhLjmn4k1uKawJ614y0ScPNd8PQ-" +
    "CljsnlPxbq9ofaCMe2BV3B6y09aCuGFJ0nxn1_ubjmIBIWWFTAznoz1J9BhJDGyt3IO3A" +
    "By3f9zDVlR32L_n5VIkXnxkjUKdzMAOzYb62kuKOp1iznRTPrV71SNtivJMwSh_LVgBrm" +
    "ZjtIn_oim-KyX_fdLU3tQ7VClyqmJzyAjccOH6Qj6nFTPh-vX07gqN8IlLT2uye4waw",
  p: "xuDd7tE_47NWwvDTpB403X13EPA3768MlNpl_v_BGiuP-1uvWUnsOVZB0F3HXSVg1sBV" +
    "Ntec46v7OU0P693gvYUhouTmSQpayY_VFqMklprWgs7cfneqbeDzv3C4Fw5waY-vjoIND" +
    "sE1jYELUnl5cVjXXyxuGFG-IaLJKmHmHX0",
  q: "z17X2t9zO6WcMp6W04gXdKmniJlxekOrOmWnrX9AwaM8NYCLN3y23r59nqNP9aUAWG1eo" +
    "GFmav2rYQitWhz_VsEu2pQUsfsYKZYHchu5p_jCYwuM3rIg7aCbhtGv_tBoWAf1NvKMhtp" +
    "2es0ZaHZCzKDGSOkIYDOB-ZDmNigWigc",
  qi: "KC6gWhVM_x7iQgl-gEoSh_iM1Jf314ZLJKAAz1DsTHMi5yuCkCMmmY7h6jlkAJVngK3KI" +
    "f5LPoAeUoGJ26E1kocbRU_nZBftMDVXHCYICz8qMQXR5euN_5SeJnu_VWXH-CY83MKhPY" +
    "AorWSZ1-G9gh-C16LlRMzJwoE6h5QNeNo",
  // cSpell: enable
  key_ops: ["sign"],
  ext: true,
};

Deno.test("exportJwk()", async () => {
  assertEquals(await exportJwk(privateKey2), privateJwk);
  assertEquals(await exportJwk(publicKey2.publicKey!), publicJwk);
});

Deno.test("importJwk()", async () => {
  assertEquals(await importJwk(privateJwk, "private"), privateKey2);
  assertEquals(await importJwk(publicJwk, "public"), publicKey2.publicKey!);
  assertRejects(() => importJwk(publicJwk, "private"));
  assertRejects(() => importJwk(privateJwk, "public"));
});
+51 −0
Original line number Diff line number Diff line
@@ -30,3 +30,54 @@ export function validateCryptoKey(
    );
  }
}

/**
 * Generates a key pair which is appropriate for Fedify.
 * @returns The generated key pair.
 */
export function generateCryptoKeyPair(): Promise<CryptoKeyPair> {
  return crypto.subtle.generateKey(
    {
      name: "RSASSA-PKCS1-v1_5",
      modulusLength: 4096,
      publicExponent: new Uint8Array([0x01, 0x00, 0x01]),
      hash: "SHA-256",
    },
    true,
    ["sign", "verify"],
  );
}

/**
 * Exports a key in JWK format.
 * @param key The key to export.  Either public or private key.
 * @returns The exported key in JWK format.  The key is suitable for
 *          serialization and storage.
 * @throws {TypeError} If the key is invalid or unsupported.
 */
export async function exportJwk(key: CryptoKey): Promise<JsonWebKey> {
  validateCryptoKey(key);
  return await crypto.subtle.exportKey("jwk", key);
}

/**
 * Imports a key from JWK format.
 * @param jwk The key in JWK format.
 * @param type Which type of key to import, either `"public"`" or `"private"`".
 * @returns The imported key.
 * @throws {TypeError} If the key is invalid or unsupported.
 */
export async function importJwk(
  jwk: JsonWebKey,
  type: "public" | "private",
): Promise<CryptoKey> {
  const key = await crypto.subtle.importKey(
    "jwk",
    jwk,
    { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
    true,
    type === "public" ? ["verify"] : ["sign"],
  );
  validateCryptoKey(key, type);
  return key;
}
+1 −0
Original line number Diff line number Diff line
@@ -15,6 +15,7 @@ import {
  Object as ASObject,
} from "../vocab/vocab.ts";
import { validateCryptoKey } from "./key.ts";
export { exportJwk, generateCryptoKeyPair, importJwk } from "./key.ts";

/**
 * Signs a request using the given private key.
Loading