Skip to content

Commit

Permalink
CryptoKey utilities
Browse files Browse the repository at this point in the history
  • Loading branch information
dahlia committed Mar 12, 2024
1 parent 44c4e9d commit ac63f73
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 43 deletions.
10 changes: 8 additions & 2 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 6 additions & 26 deletions examples/blog/models/blog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/// <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";

Expand All @@ -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),
});
}

Expand All @@ -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),
};
}
Expand Down
82 changes: 80 additions & 2 deletions httpsig/key.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
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(
Expand Down Expand Up @@ -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 changes: 51 additions & 0 deletions httpsig/key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 change: 1 addition & 0 deletions httpsig/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions vocab/__snapshots__/vocab.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ snapshot[`Deno.inspect(CryptographicKey) [auto] 1`] = `
extractable: true,
algorithm: {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 4096,
modulusLength: 2048,
publicExponent: Uint8Array(3) [ 1, 0, 1 ],
hash: { name: "SHA-256" }
},
Expand All @@ -26,7 +26,7 @@ snapshot[`Deno.inspect(CryptographicKey) [auto] 2`] = `
extractable: true,
algorithm: {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 4096,
modulusLength: 2048,
publicExponent: Uint8Array(3) [ 1, 0, 1 ],
hash: { name: "SHA-256" }
},
Expand All @@ -43,7 +43,7 @@ snapshot[`Deno.inspect(CryptographicKey) [auto] 3`] = `
extractable: true,
algorithm: {
name: "RSASSA-PKCS1-v1_5",
modulusLength: 4096,
modulusLength: 2048,
publicExponent: Uint8Array(3) [ 1, 0, 1 ],
hash: { name: "SHA-256" }
},
Expand Down
12 changes: 2 additions & 10 deletions vocab/vocab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
import { areAllScalarTypes } from "../codegen/type.ts";
import { LanguageString } from "../runtime/langstr.ts";
import { mockDocumentLoader } from "../testing/docloader.ts";
import { publicKey1 } from "../testing/keys.ts";
import {
Activity,
Create,
Expand Down Expand Up @@ -389,16 +390,7 @@ const sampleValues: Record<string, any> = {
hours: 1,
}),
"fedify:langTag": parseLanguageTag("en"),
"fedify:publicKey": (await crypto.subtle.generateKey(
{
name: "RSASSA-PKCS1-v1_5",
modulusLength: 4096,
publicExponent: new Uint8Array([1, 0, 1]),
hash: "SHA-256",
},
true,
["sign", "verify"],
)).publicKey,
"fedify:publicKey": publicKey1.publicKey,
"fedify:units": "m",
};

Expand Down

0 comments on commit ac63f73

Please sign in to comment.