Loading CHANGES.md +1 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ To be released. - Added `RequestContext.getSignedKey()` method. - Added `FederationFetchOptions.onUnauthorized` option for handling unauthorized fetches. - Added `getKeyOwner()` function. - The default implementation of `FederationFetchOptions.onNotAcceptable` option now responds with `Vary: Accept, Signature` header. Loading httpsig/mod.test.ts +38 −1 Original line number Diff line number Diff line import { Temporal } from "@js-temporal/polyfill"; import { assert, assertEquals, assertFalse } from "@std/assert"; import { doesActorOwnKey, sign, verify } from "../mod.ts"; import { doesActorOwnKey, getKeyOwner, sign, verify } from "../mod.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { privateKey2, publicKey1, publicKey2 } from "../testing/keys.ts"; import { lookupObject } from "../vocab/lookup.ts"; import { Create } from "../vocab/vocab.ts"; Deno.test("sign()", async () => { Loading Loading @@ -143,3 +144,39 @@ Deno.test("doesActorOwnKey()", async () => { assertFalse(await doesActorOwnKey(activity2, publicKey1, mockDocumentLoader)); assertFalse(await doesActorOwnKey(activity2, publicKey2, mockDocumentLoader)); }); Deno.test("getKeyOwner()", async () => { const owner = await getKeyOwner( new URL("https://example.com/users/handle#main-key"), mockDocumentLoader, ); assertEquals( owner, await lookupObject("https://example.com/users/handle", { documentLoader: mockDocumentLoader, }), ); const owner2 = await getKeyOwner( new URL("https://example.com/key"), mockDocumentLoader, ); assertEquals( owner2, await lookupObject("https://example.com/person", { documentLoader: mockDocumentLoader, }), ); const noOwner = await getKeyOwner( new URL("https://example.com/key2"), mockDocumentLoader, ); assertEquals(noOwner, null); const noOwner2 = await getKeyOwner( new URL("https://example.com/object"), mockDocumentLoader, ); assertEquals(noOwner2, null); }); httpsig/mod.ts +48 −1 Original line number Diff line number Diff line Loading @@ -8,7 +8,7 @@ import { Temporal } from "@js-temporal/polyfill"; import { equals } from "@std/bytes"; import { decodeBase64, encodeBase64 } from "@std/encoding/base64"; import type { DocumentLoader } from "../runtime/docloader.ts"; import { isActor } from "../vocab/actor.ts"; import { type Actor, isActor } from "../vocab/actor.ts"; import { type Activity, CryptographicKey, Loading Loading @@ -224,3 +224,50 @@ export async function doesActorOwnKey( } return false; } /** * Gets the actor that owns the specified key. Returns `null` if the key has no known owner. * * @param keyId The ID of the key to check. * @param documentLoader The document loader to use for fetching the key and its owner. * @returns The actor that owns the key, or `null` if the key has no known owner. * @sicne 0.7.0 */ export async function getKeyOwner( keyId: URL, documentLoader: DocumentLoader, ): Promise<Actor | null> { let keyDoc: unknown; try { const { document } = await documentLoader(keyId.href); keyDoc = document; } catch (_) { return null; } let object: ASObject | CryptographicKey; try { object = await ASObject.fromJsonLd(keyDoc, { documentLoader }); } catch (e) { if (!(e instanceof TypeError)) throw e; try { object = await CryptographicKey.fromJsonLd(keyDoc, { documentLoader }); } catch (e) { if (e instanceof TypeError) return null; throw e; } } let owner: Actor | null = null; if (object instanceof CryptographicKey) { if (object.ownerId == null) return null; owner = await object.getOwner({ documentLoader }); } else if (isActor(object)) { owner = object; } else { return null; } if (owner == null) return null; for (const kid of owner.publicKeyIds) { if (kid.href === keyId.href) return owner; } return null; } Loading
CHANGES.md +1 −0 Original line number Diff line number Diff line Loading @@ -19,6 +19,7 @@ To be released. - Added `RequestContext.getSignedKey()` method. - Added `FederationFetchOptions.onUnauthorized` option for handling unauthorized fetches. - Added `getKeyOwner()` function. - The default implementation of `FederationFetchOptions.onNotAcceptable` option now responds with `Vary: Accept, Signature` header. Loading
httpsig/mod.test.ts +38 −1 Original line number Diff line number Diff line import { Temporal } from "@js-temporal/polyfill"; import { assert, assertEquals, assertFalse } from "@std/assert"; import { doesActorOwnKey, sign, verify } from "../mod.ts"; import { doesActorOwnKey, getKeyOwner, sign, verify } from "../mod.ts"; import { mockDocumentLoader } from "../testing/docloader.ts"; import { privateKey2, publicKey1, publicKey2 } from "../testing/keys.ts"; import { lookupObject } from "../vocab/lookup.ts"; import { Create } from "../vocab/vocab.ts"; Deno.test("sign()", async () => { Loading Loading @@ -143,3 +144,39 @@ Deno.test("doesActorOwnKey()", async () => { assertFalse(await doesActorOwnKey(activity2, publicKey1, mockDocumentLoader)); assertFalse(await doesActorOwnKey(activity2, publicKey2, mockDocumentLoader)); }); Deno.test("getKeyOwner()", async () => { const owner = await getKeyOwner( new URL("https://example.com/users/handle#main-key"), mockDocumentLoader, ); assertEquals( owner, await lookupObject("https://example.com/users/handle", { documentLoader: mockDocumentLoader, }), ); const owner2 = await getKeyOwner( new URL("https://example.com/key"), mockDocumentLoader, ); assertEquals( owner2, await lookupObject("https://example.com/person", { documentLoader: mockDocumentLoader, }), ); const noOwner = await getKeyOwner( new URL("https://example.com/key2"), mockDocumentLoader, ); assertEquals(noOwner, null); const noOwner2 = await getKeyOwner( new URL("https://example.com/object"), mockDocumentLoader, ); assertEquals(noOwner2, null); });
httpsig/mod.ts +48 −1 Original line number Diff line number Diff line Loading @@ -8,7 +8,7 @@ import { Temporal } from "@js-temporal/polyfill"; import { equals } from "@std/bytes"; import { decodeBase64, encodeBase64 } from "@std/encoding/base64"; import type { DocumentLoader } from "../runtime/docloader.ts"; import { isActor } from "../vocab/actor.ts"; import { type Actor, isActor } from "../vocab/actor.ts"; import { type Activity, CryptographicKey, Loading Loading @@ -224,3 +224,50 @@ export async function doesActorOwnKey( } return false; } /** * Gets the actor that owns the specified key. Returns `null` if the key has no known owner. * * @param keyId The ID of the key to check. * @param documentLoader The document loader to use for fetching the key and its owner. * @returns The actor that owns the key, or `null` if the key has no known owner. * @sicne 0.7.0 */ export async function getKeyOwner( keyId: URL, documentLoader: DocumentLoader, ): Promise<Actor | null> { let keyDoc: unknown; try { const { document } = await documentLoader(keyId.href); keyDoc = document; } catch (_) { return null; } let object: ASObject | CryptographicKey; try { object = await ASObject.fromJsonLd(keyDoc, { documentLoader }); } catch (e) { if (!(e instanceof TypeError)) throw e; try { object = await CryptographicKey.fromJsonLd(keyDoc, { documentLoader }); } catch (e) { if (e instanceof TypeError) return null; throw e; } } let owner: Actor | null = null; if (object instanceof CryptographicKey) { if (object.ownerId == null) return null; owner = await object.getOwner({ documentLoader }); } else if (isActor(object)) { owner = object; } else { return null; } if (owner == null) return null; for (const kid of owner.publicKeyIds) { if (kid.href === keyId.href) return owner; } return null; }