Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions packages/fedify/src/federation/middleware.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { RouterError } from "@fedify/uri-template";
import * as vocab from "@fedify/vocab";
import {
Create,
CryptographicKey,
getTypeId,
lookupObject,
Note,
Expand Down Expand Up @@ -1731,6 +1732,157 @@ test({
assertStrictEquals(clone.federation, ctx.federation);
});

await t.step(
"getSignedKey() uses the key cache",
async () => {
const kv = new MemoryKvStore();
const keyId = rsaPublicKey2.id!;
let keyFetches = 0;

const documentLoader = async (url: string) => {
if (url === keyId.href) keyFetches++;
return await mockDocumentLoader(url);
};

const federation = createFederation<number>({
kv,
documentLoaderFactory: () => documentLoader,
contextLoaderFactory: () => mockDocumentLoader,
publicKeyTtl: { days: 7 },
});

const request1 = await signRequest(
new Request("https://example.com/"),
rsaPrivateKey2,
keyId,
);
const request2 = await signRequest(
new Request("https://example.com/"),
rsaPrivateKey2,
keyId,
);

const ctx1 = federation.createContext(request1, 1);
const ctx2 = federation.createContext(request2, 2);

assertEquals(await ctx1.getSignedKey(), rsaPublicKey2);
assertEquals(keyFetches, 1);

assertEquals(await ctx2.getSignedKey(), rsaPublicKey2);
assertEquals(keyFetches, 1);
},
);

await t.step(
"getSignedKey() caches unavailable keys",
async () => {
const kv = new MemoryKvStore();
const keyId = new URL("https://example.com/keys/missing");
let keyFetches = 0;

const documentLoader = async (url: string) => {
if (url === keyId.href) keyFetches++;
return await mockDocumentLoader(url);
};

const federation = createFederation<number>({
kv,
documentLoaderFactory: () => documentLoader,
contextLoaderFactory: () => mockDocumentLoader,
});

const request1 = await signRequest(
new Request("https://example.com/"),
rsaPrivateKey2,
keyId,
);
const request2 = await signRequest(
new Request("https://example.com/"),
rsaPrivateKey2,
keyId,
);

const ctx1 = federation.createContext(request1, 1);
const ctx2 = federation.createContext(request2, 2);

assertEquals(await ctx1.getSignedKey(), null);
assertEquals(keyFetches, 1);

assertEquals(await ctx2.getSignedKey(), null);
assertEquals(keyFetches, 1);
},
);

await t.step(
"getSignedKey() refetches a key that no longer verifies",
async () => {
const kv = new MemoryKvStore();
const keyId = rsaPublicKey2.id!;
let keyFetches = 0;

const rotatedPublicKey = new CryptographicKey({
id: keyId,
publicKey: rsaPublicKey3.publicKey,
});

const documentLoader = async (url: string) => {
if (url !== keyId.href) return await mockDocumentLoader(url);

keyFetches++;

if (keyFetches === 1) {
return await mockDocumentLoader(url);
}

return {
contextUrl: null,
documentUrl: url,
document: await rotatedPublicKey.toJsonLd({
contextLoader: mockDocumentLoader,
}),
};
};

const federation = createFederation<number>({
kv,
documentLoaderFactory: () => documentLoader,
contextLoaderFactory: () => mockDocumentLoader,
});

const request1 = await signRequest(
new Request("https://example.com/"),
rsaPrivateKey2,
keyId,
);
const request2 = await signRequest(
new Request("https://example.com/"),
rsaPrivateKey3,
keyId,
);

const request3 = await signRequest(
new Request("https://example.com/"),
rsaPrivateKey3,
keyId,
);

const ctx1 = federation.createContext(request1, 1);
const ctx2 = federation.createContext(request2, 2);

const ctx3 = federation.createContext(request3, 3);

assertEquals(await ctx1.getSignedKey(), rsaPublicKey2);
assertEquals(keyFetches, 1);

assertEquals(await ctx2.getSignedKey(), rotatedPublicKey);
assertEquals(keyFetches, 2);

assertEquals(await ctx3.getSignedKey(), rotatedPublicKey);
// The refreshed key was cached, so it is not fetched again:
assertEquals(keyFetches, 2);
},
);

fetchMock.hardReset();
},
});
Expand Down
13 changes: 13 additions & 0 deletions packages/fedify/src/federation/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6211,6 +6211,18 @@ class RequestContextImpl<TContextData> extends ContextImpl<TContextData>
options: GetSignedKeyOptions = {},
): Promise<CryptographicKey | null> {
if (this.#signedKey != null) return this.#signedKey;

const keyCache = new KvKeyCache(
this.federation.kv,
this.federation.kvPrefixes.publicKey,
{
documentLoader: options.documentLoader ?? this.documentLoader,
contextLoader: options.contextLoader ?? this.contextLoader,
tracerProvider: options.tracerProvider ?? this.tracerProvider,
keyTtl: this.federation.publicKeyTtl,
},
);

return this.#signedKey = await verifyRequest(this.request, {
...this,
contextLoader: options.contextLoader ?? this.contextLoader,
Expand All @@ -6219,6 +6231,7 @@ class RequestContextImpl<TContextData> extends ContextImpl<TContextData>
maxSignatures: this.federation.maxHttpSignatures,
meterProvider: this.meterProvider,
tracerProvider: options.tracerProvider ?? this.tracerProvider,
keyCache,
});
}

Expand Down
Loading