From 737e08099e11abbeb2bc7ab6b56a12ae00826845 Mon Sep 17 00:00:00 2001 From: Hong Minhee Date: Thu, 11 Apr 2024 23:04:54 +0900 Subject: [PATCH] getKeyOwner() function --- CHANGES.md | 1 + httpsig/mod.test.ts | 39 +++++++++++++++++++++++++++++++++++- httpsig/mod.ts | 49 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 2 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b2b14420..4648bf41 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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. diff --git a/httpsig/mod.test.ts b/httpsig/mod.test.ts index d167df5b..78d72d76 100644 --- a/httpsig/mod.test.ts +++ b/httpsig/mod.test.ts @@ -1,8 +1,9 @@ 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 () => { @@ -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); +}); diff --git a/httpsig/mod.ts b/httpsig/mod.ts index 30084a1e..f28f3c43 100644 --- a/httpsig/mod.ts +++ b/httpsig/mod.ts @@ -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, @@ -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 { + 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; +}