Loading CHANGES.md +1 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ To be released. - Added `CollectionCallbackSetters.authorize()` method. - Added `AuthorizedPredicate` type. - Added `RequestContext.getSignedKey()` method. - Added `RequestContext.getSignedKeyOwner()` method. - Added `FederationFetchOptions.onUnauthorized` option for handling unauthorized fetches. - Added `getKeyOwner()` function. Loading federation/context.ts +15 −0 Original line number Diff line number Diff line Loading @@ -155,6 +155,21 @@ export interface RequestContext<TContextData> extends Context<TContextData> { * @since 0.7.0 */ getSignedKey(): Promise<CryptographicKey | null>; /** * Gets the owner of the signed key, if any exists and it is verified. * Otherwise, `null` is returned. * * This can be used for implementing [authorized fetch] (also known as * secure mode) in ActivityPub. * * [authorized fetch]: https://swicg.github.io/activitypub-http-signature/#authorized-fetch * * @returns The owner of the signed key, or `null` if the key is not verified * or the owner is not found. * @since 0.7.0 */ getSignedKeyOwner(): Promise<Actor | null>; } /** Loading federation/middleware.test.ts +30 −1 Original line number Diff line number Diff line Loading @@ -13,12 +13,18 @@ import { getAuthenticatedDocumentLoader, } from "../runtime/docloader.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { privateKey2, publicKey2 } from "../testing/keys.ts"; import { privateKey2, privateKey3, publicKey2, publicKey3, } from "../testing/keys.ts"; import { Create, Person } from "../vocab/vocab.ts"; import type { Context } from "./context.ts"; import { MemoryKvStore } from "./kv.ts"; import { Federation } from "./middleware.ts"; import { RouterError } from "./router.ts"; import { lookupObject } from "@fedify/fedify/vocab"; Deno.test("Federation.createContext()", async (t) => { const kv = new MemoryKvStore(); Loading Loading @@ -179,8 +185,10 @@ Deno.test("Federation.createContext()", async (t) => { assertEquals(ctx.url, new URL("https://example.com/")); assertEquals(ctx.data, 123); assertEquals(await ctx.getSignedKey(), null); assertEquals(await ctx.getSignedKeyOwner(), null); // Multiple calls should return the same result: assertEquals(await ctx.getSignedKey(), null); assertEquals(await ctx.getSignedKeyOwner(), null); const signedReq = await sign( new Request("https://example.com/"), Loading @@ -192,8 +200,29 @@ Deno.test("Federation.createContext()", async (t) => { assertEquals(signedCtx.url, new URL("https://example.com/")); assertEquals(signedCtx.data, 456); assertEquals(await signedCtx.getSignedKey(), publicKey2); assertEquals(await signedCtx.getSignedKeyOwner(), null); // Multiple calls should return the same result: assertEquals(await signedCtx.getSignedKey(), publicKey2); assertEquals(await signedCtx.getSignedKeyOwner(), null); const signedReq2 = await sign( new Request("https://example.com/"), privateKey3, publicKey3.id!, ); const signedCtx2 = federation.createContext(signedReq2, 456); assertEquals(signedCtx2.request, signedReq2); assertEquals(signedCtx2.url, new URL("https://example.com/")); assertEquals(signedCtx2.data, 456); assertEquals(await signedCtx2.getSignedKey(), publicKey3); const expectedOwner = await lookupObject( "https://example.com/person2", { documentLoader: mockDocumentLoader }, ); assertEquals(await signedCtx2.getSignedKeyOwner(), expectedOwner); // Multiple calls should return the same result: assertEquals(await signedCtx2.getSignedKey(), publicKey3); assertEquals(await signedCtx2.getSignedKeyOwner(), expectedOwner); }); mf.uninstall(); Loading federation/middleware.ts +8 −1 Original line number Diff line number Diff line import { Temporal } from "@js-temporal/polyfill"; import { exportJwk, importJwk, validateCryptoKey } from "../httpsig/key.ts"; import { verify } from "../httpsig/mod.ts"; import { getKeyOwner, verify } from "../httpsig/mod.ts"; import { handleNodeInfo, handleNodeInfoJrd } from "../nodeinfo/handler.ts"; import { type AuthenticatedDocumentLoaderFactory, Loading Loading @@ -392,6 +392,7 @@ export class Federation<TContextData> { }; if (request == null) return context; let signedKey: CryptographicKey | null | undefined = undefined; let signedKeyOwner: Actor | null | undefined = undefined; const reqCtx: RequestContext<TContextData> = { ...context, request, Loading @@ -400,6 +401,12 @@ export class Federation<TContextData> { if (signedKey !== undefined) return signedKey; return signedKey = await verify(request, context.documentLoader); }, async getSignedKeyOwner() { if (signedKeyOwner !== undefined) return signedKeyOwner; const key = await this.getSignedKey(); if (key == null) return signedKeyOwner = null; return signedKeyOwner = await getKeyOwner(key, context.documentLoader); }, }; return reqCtx; } Loading httpsig/mod.test.ts +3 −0 Original line number Diff line number Diff line Loading @@ -168,6 +168,9 @@ Deno.test("getKeyOwner()", async () => { }), ); const owner3 = await getKeyOwner(publicKey1, mockDocumentLoader); assertEquals(owner3, owner2); const noOwner = await getKeyOwner( new URL("https://example.com/key2"), mockDocumentLoader, Loading Loading
CHANGES.md +1 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ To be released. - Added `CollectionCallbackSetters.authorize()` method. - Added `AuthorizedPredicate` type. - Added `RequestContext.getSignedKey()` method. - Added `RequestContext.getSignedKeyOwner()` method. - Added `FederationFetchOptions.onUnauthorized` option for handling unauthorized fetches. - Added `getKeyOwner()` function. Loading
federation/context.ts +15 −0 Original line number Diff line number Diff line Loading @@ -155,6 +155,21 @@ export interface RequestContext<TContextData> extends Context<TContextData> { * @since 0.7.0 */ getSignedKey(): Promise<CryptographicKey | null>; /** * Gets the owner of the signed key, if any exists and it is verified. * Otherwise, `null` is returned. * * This can be used for implementing [authorized fetch] (also known as * secure mode) in ActivityPub. * * [authorized fetch]: https://swicg.github.io/activitypub-http-signature/#authorized-fetch * * @returns The owner of the signed key, or `null` if the key is not verified * or the owner is not found. * @since 0.7.0 */ getSignedKeyOwner(): Promise<Actor | null>; } /** Loading
federation/middleware.test.ts +30 −1 Original line number Diff line number Diff line Loading @@ -13,12 +13,18 @@ import { getAuthenticatedDocumentLoader, } from "../runtime/docloader.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { privateKey2, publicKey2 } from "../testing/keys.ts"; import { privateKey2, privateKey3, publicKey2, publicKey3, } from "../testing/keys.ts"; import { Create, Person } from "../vocab/vocab.ts"; import type { Context } from "./context.ts"; import { MemoryKvStore } from "./kv.ts"; import { Federation } from "./middleware.ts"; import { RouterError } from "./router.ts"; import { lookupObject } from "@fedify/fedify/vocab"; Deno.test("Federation.createContext()", async (t) => { const kv = new MemoryKvStore(); Loading Loading @@ -179,8 +185,10 @@ Deno.test("Federation.createContext()", async (t) => { assertEquals(ctx.url, new URL("https://example.com/")); assertEquals(ctx.data, 123); assertEquals(await ctx.getSignedKey(), null); assertEquals(await ctx.getSignedKeyOwner(), null); // Multiple calls should return the same result: assertEquals(await ctx.getSignedKey(), null); assertEquals(await ctx.getSignedKeyOwner(), null); const signedReq = await sign( new Request("https://example.com/"), Loading @@ -192,8 +200,29 @@ Deno.test("Federation.createContext()", async (t) => { assertEquals(signedCtx.url, new URL("https://example.com/")); assertEquals(signedCtx.data, 456); assertEquals(await signedCtx.getSignedKey(), publicKey2); assertEquals(await signedCtx.getSignedKeyOwner(), null); // Multiple calls should return the same result: assertEquals(await signedCtx.getSignedKey(), publicKey2); assertEquals(await signedCtx.getSignedKeyOwner(), null); const signedReq2 = await sign( new Request("https://example.com/"), privateKey3, publicKey3.id!, ); const signedCtx2 = federation.createContext(signedReq2, 456); assertEquals(signedCtx2.request, signedReq2); assertEquals(signedCtx2.url, new URL("https://example.com/")); assertEquals(signedCtx2.data, 456); assertEquals(await signedCtx2.getSignedKey(), publicKey3); const expectedOwner = await lookupObject( "https://example.com/person2", { documentLoader: mockDocumentLoader }, ); assertEquals(await signedCtx2.getSignedKeyOwner(), expectedOwner); // Multiple calls should return the same result: assertEquals(await signedCtx2.getSignedKey(), publicKey3); assertEquals(await signedCtx2.getSignedKeyOwner(), expectedOwner); }); mf.uninstall(); Loading
federation/middleware.ts +8 −1 Original line number Diff line number Diff line import { Temporal } from "@js-temporal/polyfill"; import { exportJwk, importJwk, validateCryptoKey } from "../httpsig/key.ts"; import { verify } from "../httpsig/mod.ts"; import { getKeyOwner, verify } from "../httpsig/mod.ts"; import { handleNodeInfo, handleNodeInfoJrd } from "../nodeinfo/handler.ts"; import { type AuthenticatedDocumentLoaderFactory, Loading Loading @@ -392,6 +392,7 @@ export class Federation<TContextData> { }; if (request == null) return context; let signedKey: CryptographicKey | null | undefined = undefined; let signedKeyOwner: Actor | null | undefined = undefined; const reqCtx: RequestContext<TContextData> = { ...context, request, Loading @@ -400,6 +401,12 @@ export class Federation<TContextData> { if (signedKey !== undefined) return signedKey; return signedKey = await verify(request, context.documentLoader); }, async getSignedKeyOwner() { if (signedKeyOwner !== undefined) return signedKeyOwner; const key = await this.getSignedKey(); if (key == null) return signedKeyOwner = null; return signedKeyOwner = await getKeyOwner(key, context.documentLoader); }, }; return reqCtx; } Loading
httpsig/mod.test.ts +3 −0 Original line number Diff line number Diff line Loading @@ -168,6 +168,9 @@ Deno.test("getKeyOwner()", async () => { }), ); const owner3 = await getKeyOwner(publicKey1, mockDocumentLoader); assertEquals(owner3, owner2); const noOwner = await getKeyOwner( new URL("https://example.com/key2"), mockDocumentLoader, Loading