Loading .vscode/settings.json +1 −0 Original line number Diff line number Diff line Loading @@ -98,6 +98,7 @@ "instanceof", "interoperating", "jsonld", "keycache", "keypair", "langstr", "Lemmy", Loading src/federation/handler.ts +3 −36 Original line number Diff line number Diff line Loading @@ -11,7 +11,6 @@ import { accepts } from "@std/http/negotiation"; import metadata from "../deno.json" with { type: "json" }; import type { DocumentLoader } from "../runtime/docloader.ts"; import { verifyRequest } from "../sig/http.ts"; import type { KeyCache } from "../sig/key.ts"; import { detachSignature, verifyJsonLd } from "../sig/ld.ts"; import { doesActorOwnKey } from "../sig/owner.ts"; import { verifyObject } from "../sig/proof.ts"; Loading @@ -19,9 +18,8 @@ import type { Recipient } from "../vocab/actor.ts"; import { getTypeId } from "../vocab/type.ts"; import { Activity, CryptographicKey, type CryptographicKey, Link, Multikey, Object, OrderedCollection, OrderedCollectionPage, Loading @@ -38,6 +36,7 @@ import type { } from "./callback.ts"; import type { Context, InboxContext, RequestContext } from "./context.ts"; import type { InboxListenerSet } from "./inbox.ts"; import { KvKeyCache } from "./keycache.ts"; import type { KvKey, KvStore } from "./kv.ts"; import type { MessageQueue } from "./mq.ts"; import type { InboxMessage } from "./queue.ts"; Loading Loading @@ -545,39 +544,7 @@ async function handleInboxInternal<TContextData>( headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } const keyCache: KeyCache & { nullKeys: Set<string> } = { nullKeys: new Set(), async get(keyId: URL) { if (this.nullKeys.has(keyId.href)) return null; const serialized = await kv.get([ ...kvPrefixes.publicKey, keyId.href, ]); if (serialized == null) return undefined; let object: Object; try { object = await Object.fromJsonLd(serialized, ctx); } catch { await kv.delete([...kvPrefixes.publicKey, keyId.href]); return undefined; } if (object instanceof CryptographicKey || object instanceof Multikey) { return object; } await kv.delete([...kvPrefixes.publicKey, keyId.href]); return undefined; }, async set(keyId: URL, key: CryptographicKey | Multikey | null) { if (key == null) { this.nullKeys.add(keyId.href); await kv.delete([...kvPrefixes.publicKey, keyId.href]); return; } this.nullKeys.delete(keyId.href); const serialized = await key.toJsonLd(ctx); await kv.set([...kvPrefixes.publicKey, keyId.href], serialized); }, }; const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx); const ldSigVerified = await verifyJsonLd(json, { contextLoader: ctx.contextLoader, documentLoader: ctx.documentLoader, Loading src/federation/keycache.test.ts 0 → 100644 +67 −0 Original line number Diff line number Diff line import { assert } from "@std/assert/assert"; import { assertEquals } from "@std/assert/assert-equals"; import { assertInstanceOf } from "@std/assert/assert-instance-of"; import { test } from "../testing/mod.ts"; import { CryptographicKey, Multikey } from "../vocab/vocab.ts"; import { KvKeyCache } from "./keycache.ts"; import { MemoryKvStore } from "./kv.ts"; test("KvKeyCache.set()", async () => { const kv = new MemoryKvStore(); const cache = new KvKeyCache(kv, ["pk"]); await cache.set( new URL("https://example.com/key"), new CryptographicKey({ id: new URL("https://example.com/key") }), ); assertEquals( await kv.get(["pk", "https://example.com/key"]), { "@context": "https://w3id.org/security/v1", id: "https://example.com/key", type: "CryptographicKey", }, ); await cache.set( new URL("https://example.com/key2"), new Multikey({ id: new URL("https://example.com/key2") }), ); assertEquals( await kv.get(["pk", "https://example.com/key2"]), { "@context": "https://w3id.org/security/multikey/v1", id: "https://example.com/key2", type: "Multikey", }, ); await cache.set(new URL("https://example.com/null"), null); assert(cache.nullKeys.has("https://example.com/null")); }); test("KvKeyCache.get()", async () => { const kv = new MemoryKvStore(); const cache = new KvKeyCache(kv, ["pk"]); await kv.set(["pk", "https://example.com/key"], { "@context": "https://w3id.org/security/v1", id: "https://example.com/key", type: "CryptographicKey", }); const cryptoKey = await cache.get(new URL("https://example.com/key")); assertInstanceOf(cryptoKey, CryptographicKey); assertEquals(cryptoKey?.id?.href, "https://example.com/key"); await kv.set(["pk", "https://example.com/key2"], { "@context": "https://w3id.org/security/multikey/v1", id: "https://example.com/key2", type: "Multikey", }); const multikey = await cache.get(new URL("https://example.com/key2")); assertInstanceOf(multikey, Multikey); assertEquals(multikey?.id?.href, "https://example.com/key2"); cache.nullKeys.add("https://example.com/null"); assertEquals(await cache.get(new URL("https://example.com/null")), null); }); src/federation/keycache.ts 0 → 100644 +55 −0 Original line number Diff line number Diff line import type { DocumentLoader } from "../runtime/docloader.ts"; import type { KeyCache } from "../sig/key.ts"; import { CryptographicKey, Multikey } from "../vocab/vocab.ts"; import type { KvKey, KvStore } from "./kv.ts"; export interface KvKeyCacheOptions { documentLoader?: DocumentLoader; contextLoader?: DocumentLoader; } export class KvKeyCache implements KeyCache { readonly kv: KvStore; readonly prefix: KvKey; readonly options: KvKeyCacheOptions; readonly nullKeys: Set<string>; constructor(kv: KvStore, prefix: KvKey, options: KvKeyCacheOptions = {}) { this.kv = kv; this.prefix = prefix; this.nullKeys = new Set(); this.options = options; } async get( keyId: URL, ): Promise<CryptographicKey | Multikey | null | undefined> { if (this.nullKeys.has(keyId.href)) return null; const serialized = await this.kv.get([...this.prefix, keyId.href]); if (serialized == null) return undefined; try { return await CryptographicKey.fromJsonLd(serialized, this.options); } catch { try { return await Multikey.fromJsonLd(serialized, this.options); } catch { await this.kv.delete([...this.prefix, keyId.href]); return undefined; } } } async set( keyId: URL, key: CryptographicKey | Multikey | null, ): Promise<void> { if (key == null) { this.nullKeys.add(keyId.href); await this.kv.delete([...this.prefix, keyId.href]); return; } this.nullKeys.delete(keyId.href); const serialized = await key.toJsonLd(this.options); await this.kv.set([...this.prefix, keyId.href], serialized); } } Loading
.vscode/settings.json +1 −0 Original line number Diff line number Diff line Loading @@ -98,6 +98,7 @@ "instanceof", "interoperating", "jsonld", "keycache", "keypair", "langstr", "Lemmy", Loading
src/federation/handler.ts +3 −36 Original line number Diff line number Diff line Loading @@ -11,7 +11,6 @@ import { accepts } from "@std/http/negotiation"; import metadata from "../deno.json" with { type: "json" }; import type { DocumentLoader } from "../runtime/docloader.ts"; import { verifyRequest } from "../sig/http.ts"; import type { KeyCache } from "../sig/key.ts"; import { detachSignature, verifyJsonLd } from "../sig/ld.ts"; import { doesActorOwnKey } from "../sig/owner.ts"; import { verifyObject } from "../sig/proof.ts"; Loading @@ -19,9 +18,8 @@ import type { Recipient } from "../vocab/actor.ts"; import { getTypeId } from "../vocab/type.ts"; import { Activity, CryptographicKey, type CryptographicKey, Link, Multikey, Object, OrderedCollection, OrderedCollectionPage, Loading @@ -38,6 +36,7 @@ import type { } from "./callback.ts"; import type { Context, InboxContext, RequestContext } from "./context.ts"; import type { InboxListenerSet } from "./inbox.ts"; import { KvKeyCache } from "./keycache.ts"; import type { KvKey, KvStore } from "./kv.ts"; import type { MessageQueue } from "./mq.ts"; import type { InboxMessage } from "./queue.ts"; Loading Loading @@ -545,39 +544,7 @@ async function handleInboxInternal<TContextData>( headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } const keyCache: KeyCache & { nullKeys: Set<string> } = { nullKeys: new Set(), async get(keyId: URL) { if (this.nullKeys.has(keyId.href)) return null; const serialized = await kv.get([ ...kvPrefixes.publicKey, keyId.href, ]); if (serialized == null) return undefined; let object: Object; try { object = await Object.fromJsonLd(serialized, ctx); } catch { await kv.delete([...kvPrefixes.publicKey, keyId.href]); return undefined; } if (object instanceof CryptographicKey || object instanceof Multikey) { return object; } await kv.delete([...kvPrefixes.publicKey, keyId.href]); return undefined; }, async set(keyId: URL, key: CryptographicKey | Multikey | null) { if (key == null) { this.nullKeys.add(keyId.href); await kv.delete([...kvPrefixes.publicKey, keyId.href]); return; } this.nullKeys.delete(keyId.href); const serialized = await key.toJsonLd(ctx); await kv.set([...kvPrefixes.publicKey, keyId.href], serialized); }, }; const keyCache = new KvKeyCache(kv, kvPrefixes.publicKey, ctx); const ldSigVerified = await verifyJsonLd(json, { contextLoader: ctx.contextLoader, documentLoader: ctx.documentLoader, Loading
src/federation/keycache.test.ts 0 → 100644 +67 −0 Original line number Diff line number Diff line import { assert } from "@std/assert/assert"; import { assertEquals } from "@std/assert/assert-equals"; import { assertInstanceOf } from "@std/assert/assert-instance-of"; import { test } from "../testing/mod.ts"; import { CryptographicKey, Multikey } from "../vocab/vocab.ts"; import { KvKeyCache } from "./keycache.ts"; import { MemoryKvStore } from "./kv.ts"; test("KvKeyCache.set()", async () => { const kv = new MemoryKvStore(); const cache = new KvKeyCache(kv, ["pk"]); await cache.set( new URL("https://example.com/key"), new CryptographicKey({ id: new URL("https://example.com/key") }), ); assertEquals( await kv.get(["pk", "https://example.com/key"]), { "@context": "https://w3id.org/security/v1", id: "https://example.com/key", type: "CryptographicKey", }, ); await cache.set( new URL("https://example.com/key2"), new Multikey({ id: new URL("https://example.com/key2") }), ); assertEquals( await kv.get(["pk", "https://example.com/key2"]), { "@context": "https://w3id.org/security/multikey/v1", id: "https://example.com/key2", type: "Multikey", }, ); await cache.set(new URL("https://example.com/null"), null); assert(cache.nullKeys.has("https://example.com/null")); }); test("KvKeyCache.get()", async () => { const kv = new MemoryKvStore(); const cache = new KvKeyCache(kv, ["pk"]); await kv.set(["pk", "https://example.com/key"], { "@context": "https://w3id.org/security/v1", id: "https://example.com/key", type: "CryptographicKey", }); const cryptoKey = await cache.get(new URL("https://example.com/key")); assertInstanceOf(cryptoKey, CryptographicKey); assertEquals(cryptoKey?.id?.href, "https://example.com/key"); await kv.set(["pk", "https://example.com/key2"], { "@context": "https://w3id.org/security/multikey/v1", id: "https://example.com/key2", type: "Multikey", }); const multikey = await cache.get(new URL("https://example.com/key2")); assertInstanceOf(multikey, Multikey); assertEquals(multikey?.id?.href, "https://example.com/key2"); cache.nullKeys.add("https://example.com/null"); assertEquals(await cache.get(new URL("https://example.com/null")), null); });
src/federation/keycache.ts 0 → 100644 +55 −0 Original line number Diff line number Diff line import type { DocumentLoader } from "../runtime/docloader.ts"; import type { KeyCache } from "../sig/key.ts"; import { CryptographicKey, Multikey } from "../vocab/vocab.ts"; import type { KvKey, KvStore } from "./kv.ts"; export interface KvKeyCacheOptions { documentLoader?: DocumentLoader; contextLoader?: DocumentLoader; } export class KvKeyCache implements KeyCache { readonly kv: KvStore; readonly prefix: KvKey; readonly options: KvKeyCacheOptions; readonly nullKeys: Set<string>; constructor(kv: KvStore, prefix: KvKey, options: KvKeyCacheOptions = {}) { this.kv = kv; this.prefix = prefix; this.nullKeys = new Set(); this.options = options; } async get( keyId: URL, ): Promise<CryptographicKey | Multikey | null | undefined> { if (this.nullKeys.has(keyId.href)) return null; const serialized = await this.kv.get([...this.prefix, keyId.href]); if (serialized == null) return undefined; try { return await CryptographicKey.fromJsonLd(serialized, this.options); } catch { try { return await Multikey.fromJsonLd(serialized, this.options); } catch { await this.kv.delete([...this.prefix, keyId.href]); return undefined; } } } async set( keyId: URL, key: CryptographicKey | Multikey | null, ): Promise<void> { if (key == null) { this.nullKeys.add(keyId.href); await this.kv.delete([...this.prefix, keyId.href]); return; } this.nullKeys.delete(keyId.href); const serialized = await key.toJsonLd(this.options); await this.kv.set([...this.prefix, keyId.href], serialized); } }