Loading .vscode/settings.json +4 −1 Original line number Diff line number Diff line Loading @@ -22,7 +22,10 @@ }, "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno", "editor.formatOnSave": true "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } }, "cSpell.words": [ "bccs", Loading federation/handler.ts +12 −26 Original line number Diff line number Diff line import { accepts } from "jsr:@std/http@^0.218.2"; import { doesActorOwnKey, verify } from "../httpsig/mod.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { Activity, Link, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/vocab.ts"; import { ActorDispatcher, CollectionCounter, Loading @@ -7,16 +16,6 @@ import { InboxListener, } from "./callback.ts"; import { RequestContext } from "./context.ts"; import { verify } from "../httpsig/mod.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { isActor } from "../vocab/actor.ts"; import { Activity, Link, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/mod.ts"; function acceptsJsonLd(request: Request): boolean { const types = accepts(request); Loading Loading @@ -250,8 +249,8 @@ export async function handleInbox<TContextData>( return response instanceof Promise ? await response : response; } } const keyId = await verify(request, documentLoader); if (keyId == null) { const key = await verify(request, documentLoader); if (key == null) { const response = new Response("Failed to verify the request signature.", { status: 401, headers: { "Content-Type": "text/plain; charset=utf-8" }, Loading Loading @@ -300,7 +299,7 @@ export async function handleInbox<TContextData>( }); return response; } if (!await doesActorOwnKey(activity, keyId)) { if (!await doesActorOwnKey(activity, key, documentLoader)) { const response = new Response("The signer and the actor do not match.", { status: 401, headers: { "Content-Type": "text/plain; charset=utf-8" }, Loading Loading @@ -341,16 +340,3 @@ export async function handleInbox<TContextData>( headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } async function doesActorOwnKey( activity: Activity, keyId: URL, ): Promise<boolean> { if (activity.actorId?.href === keyId.href.replace(/#.*$/, "")) return true; const actor = await activity.getActor(); if (actor == null || !isActor(actor)) return false; for (const publicKeyId of actor.publicKeyIds) { if (publicKeyId.href === keyId.href) return true; } return false; } federation/send.test.ts 0 → 100644 +54 −0 Original line number Diff line number Diff line import { assertEquals } from "jsr:@std/assert@^0.218.2"; import { Service } from "../mod.ts"; import { Actor } from "../vocab/actor.ts"; import { Application, Endpoints, Group, Person } from "../vocab/vocab.ts"; import { extractInboxes } from "./send.ts"; Deno.test("extractInboxes()", () => { const recipients: Actor[] = [ new Person({ id: new URL("https://example.com/alice"), inbox: new URL("https://example.com/alice/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.com/inbox"), }), }), new Application({ id: new URL("https://example.com/app"), inbox: new URL("https://example.com/app/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.com/inbox"), }), }), new Group({ id: new URL("https://example.org/group"), inbox: new URL("https://example.org/group/inbox"), }), new Service({ id: new URL("https://example.net/service"), inbox: new URL("https://example.net/service/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.net/inbox"), }), }), ]; let inboxes = extractInboxes({ recipients }); assertEquals( inboxes, new Set([ new URL("https://example.com/alice/inbox"), new URL("https://example.com/app/inbox"), new URL("https://example.org/group/inbox"), new URL("https://example.net/service/inbox"), ]), ); inboxes = extractInboxes({ recipients, preferSharedInbox: true }); assertEquals( inboxes, new Set([ new URL("https://example.com/inbox"), new URL("https://example.org/group/inbox"), new URL("https://example.net/inbox"), ]), ); }); federation/send.ts +3 −3 Original line number Diff line number Diff line Loading @@ -28,14 +28,14 @@ export interface ExtractInboxesParameters { export function extractInboxes( { recipients, preferSharedInbox }: ExtractInboxesParameters, ): Set<URL> { const inboxes = new Set<URL>(); const inboxes: Record<string, URL> = {}; for (const recipient of recipients) { const inbox = preferSharedInbox ? recipient.endpoints?.sharedInbox ?? recipient.inboxId : recipient.inboxId; if (inbox != null) inboxes.add(inbox); if (inbox != null) inboxes[inbox.href] = inbox; } return inboxes; return new Set(Object.values(inboxes)); } /** Loading httpsig/key.test.ts 0 → 100644 +60 −0 Original line number Diff line number Diff line import { assertThrows } from "jsr:@std/assert@^0.218.2"; import { validateCryptoKey } from "./key.ts"; Deno.test("validateCryptoKey()", async () => { const pkcs1v15 = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, true, ["sign", "verify"], ); validateCryptoKey(pkcs1v15.privateKey, "private"); validateCryptoKey(pkcs1v15.privateKey); validateCryptoKey(pkcs1v15.publicKey, "public"); validateCryptoKey(pkcs1v15.publicKey); assertThrows( () => validateCryptoKey(pkcs1v15.privateKey, "public"), TypeError, "The key is not a public key.", ); assertThrows( () => validateCryptoKey(pkcs1v15.publicKey, "private"), TypeError, "The key is not a private key.", ); const ecdsa = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256", }, true, ["sign", "verify"], ); assertThrows( () => validateCryptoKey(ecdsa.publicKey), TypeError, "only RSASSA-PKCS1-v1_5", ); const pkcs1v15Sha512 = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-512", }, true, ["sign", "verify"], ); assertThrows( () => validateCryptoKey(pkcs1v15Sha512.privateKey), TypeError, "hash algorithm must be SHA-256", ); }); Loading
.vscode/settings.json +4 −1 Original line number Diff line number Diff line Loading @@ -22,7 +22,10 @@ }, "[typescript]": { "editor.defaultFormatter": "denoland.vscode-deno", "editor.formatOnSave": true "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.organizeImports": "explicit" } }, "cSpell.words": [ "bccs", Loading
federation/handler.ts +12 −26 Original line number Diff line number Diff line import { accepts } from "jsr:@std/http@^0.218.2"; import { doesActorOwnKey, verify } from "../httpsig/mod.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { Activity, Link, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/vocab.ts"; import { ActorDispatcher, CollectionCounter, Loading @@ -7,16 +16,6 @@ import { InboxListener, } from "./callback.ts"; import { RequestContext } from "./context.ts"; import { verify } from "../httpsig/mod.ts"; import { DocumentLoader } from "../runtime/docloader.ts"; import { isActor } from "../vocab/actor.ts"; import { Activity, Link, Object, OrderedCollection, OrderedCollectionPage, } from "../vocab/mod.ts"; function acceptsJsonLd(request: Request): boolean { const types = accepts(request); Loading Loading @@ -250,8 +249,8 @@ export async function handleInbox<TContextData>( return response instanceof Promise ? await response : response; } } const keyId = await verify(request, documentLoader); if (keyId == null) { const key = await verify(request, documentLoader); if (key == null) { const response = new Response("Failed to verify the request signature.", { status: 401, headers: { "Content-Type": "text/plain; charset=utf-8" }, Loading Loading @@ -300,7 +299,7 @@ export async function handleInbox<TContextData>( }); return response; } if (!await doesActorOwnKey(activity, keyId)) { if (!await doesActorOwnKey(activity, key, documentLoader)) { const response = new Response("The signer and the actor do not match.", { status: 401, headers: { "Content-Type": "text/plain; charset=utf-8" }, Loading Loading @@ -341,16 +340,3 @@ export async function handleInbox<TContextData>( headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } async function doesActorOwnKey( activity: Activity, keyId: URL, ): Promise<boolean> { if (activity.actorId?.href === keyId.href.replace(/#.*$/, "")) return true; const actor = await activity.getActor(); if (actor == null || !isActor(actor)) return false; for (const publicKeyId of actor.publicKeyIds) { if (publicKeyId.href === keyId.href) return true; } return false; }
federation/send.test.ts 0 → 100644 +54 −0 Original line number Diff line number Diff line import { assertEquals } from "jsr:@std/assert@^0.218.2"; import { Service } from "../mod.ts"; import { Actor } from "../vocab/actor.ts"; import { Application, Endpoints, Group, Person } from "../vocab/vocab.ts"; import { extractInboxes } from "./send.ts"; Deno.test("extractInboxes()", () => { const recipients: Actor[] = [ new Person({ id: new URL("https://example.com/alice"), inbox: new URL("https://example.com/alice/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.com/inbox"), }), }), new Application({ id: new URL("https://example.com/app"), inbox: new URL("https://example.com/app/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.com/inbox"), }), }), new Group({ id: new URL("https://example.org/group"), inbox: new URL("https://example.org/group/inbox"), }), new Service({ id: new URL("https://example.net/service"), inbox: new URL("https://example.net/service/inbox"), endpoints: new Endpoints({ sharedInbox: new URL("https://example.net/inbox"), }), }), ]; let inboxes = extractInboxes({ recipients }); assertEquals( inboxes, new Set([ new URL("https://example.com/alice/inbox"), new URL("https://example.com/app/inbox"), new URL("https://example.org/group/inbox"), new URL("https://example.net/service/inbox"), ]), ); inboxes = extractInboxes({ recipients, preferSharedInbox: true }); assertEquals( inboxes, new Set([ new URL("https://example.com/inbox"), new URL("https://example.org/group/inbox"), new URL("https://example.net/inbox"), ]), ); });
federation/send.ts +3 −3 Original line number Diff line number Diff line Loading @@ -28,14 +28,14 @@ export interface ExtractInboxesParameters { export function extractInboxes( { recipients, preferSharedInbox }: ExtractInboxesParameters, ): Set<URL> { const inboxes = new Set<URL>(); const inboxes: Record<string, URL> = {}; for (const recipient of recipients) { const inbox = preferSharedInbox ? recipient.endpoints?.sharedInbox ?? recipient.inboxId : recipient.inboxId; if (inbox != null) inboxes.add(inbox); if (inbox != null) inboxes[inbox.href] = inbox; } return inboxes; return new Set(Object.values(inboxes)); } /** Loading
httpsig/key.test.ts 0 → 100644 +60 −0 Original line number Diff line number Diff line import { assertThrows } from "jsr:@std/assert@^0.218.2"; import { validateCryptoKey } from "./key.ts"; Deno.test("validateCryptoKey()", async () => { const pkcs1v15 = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256", }, true, ["sign", "verify"], ); validateCryptoKey(pkcs1v15.privateKey, "private"); validateCryptoKey(pkcs1v15.privateKey); validateCryptoKey(pkcs1v15.publicKey, "public"); validateCryptoKey(pkcs1v15.publicKey); assertThrows( () => validateCryptoKey(pkcs1v15.privateKey, "public"), TypeError, "The key is not a public key.", ); assertThrows( () => validateCryptoKey(pkcs1v15.publicKey, "private"), TypeError, "The key is not a private key.", ); const ecdsa = await crypto.subtle.generateKey( { name: "ECDSA", namedCurve: "P-256", }, true, ["sign", "verify"], ); assertThrows( () => validateCryptoKey(ecdsa.publicKey), TypeError, "only RSASSA-PKCS1-v1_5", ); const pkcs1v15Sha512 = await crypto.subtle.generateKey( { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-512", }, true, ["sign", "verify"], ); assertThrows( () => validateCryptoKey(pkcs1v15Sha512.privateKey), TypeError, "hash algorithm must be SHA-256", ); });