diff --git a/examples/verification-workers/src/index.ts b/examples/verification-workers/src/index.ts index cca1482..16f9c7f 100644 --- a/examples/verification-workers/src/index.ts +++ b/examples/verification-workers/src/index.ts @@ -37,6 +37,7 @@ import jwk from "../../rfc9421-keys/ed25519.json" assert { type: "json" }; const DIRECTORY_MEDIA_TYPE = "application/http-message-signatures-directory+json"; +const DIRECTORY_MAX_KEYS = 100; interface Directory { readonly keys: readonly JsonWebKey[]; @@ -88,6 +89,9 @@ function directoryFromUnknown(value: unknown): Directory { if (!isRecord(value) || !Array.isArray(value.keys)) { throw new Error("directory must contain keys"); } + if (value.keys.length > DIRECTORY_MAX_KEYS) { + throw new Error("directory contains too many keys"); + } return { keys: value.keys.map(jsonWebKeyFromUnknown), purpose: typeof value.purpose === "string" ? value.purpose : "", @@ -167,7 +171,7 @@ async function fetchDirectory(entry: SignatureAgentEntry): Promise { const card = parseSignatureAgentCard(await fetchJSON(entry.uri), entry.uri); if (card.jwks !== undefined) { - return { keys: card.jwks.keys, purpose: "" }; + return directoryFromUnknown(card.jwks); } if (card.jwks_uri === undefined) { throw new Error("signature agent card must contain jwks or jwks_uri"); @@ -180,9 +184,15 @@ async function getSigner(): Promise { } async function resolveVerifier(directory: Directory, keyid: string) { - const key = directory.keys.find((candidate) => candidate.kid === keyid); - if (key === undefined) throw new Error(`unknown key ${keyid}`); - return verifierFromJWK(key); + for (const key of directory.keys) { + try { + const verifier = await verifierFromJWK(key); + if (verifier.keyid === keyid) return verifier; + } catch { + continue; + } + } + throw new Error(`unknown key ${keyid}`); } function fields(headers: Headers): FieldOccurrence[] { diff --git a/examples/verification-workers/test/index.spec.ts b/examples/verification-workers/test/index.spec.ts index 2e6389a..c6fa473 100644 --- a/examples/verification-workers/test/index.spec.ts +++ b/examples/verification-workers/test/index.spec.ts @@ -20,7 +20,10 @@ import { SELF, } from "cloudflare:test"; import { afterEach, describe, it, expect, vi } from "vitest"; +import { sign } from "web-bot-auth"; +import { signerFromJWK } from "web-bot-auth/crypto"; import worker from "../src/index"; +import jwk from "../../rfc9421-keys/ed25519.json" assert { type: "json" }; // For now, you'll need to do something like this to get a correctly-typed // `Request` to pass to `worker.fetch()`. @@ -191,6 +194,33 @@ describe("/.well-known/http-message-signatures-directory endpoint", () => { }); }); +describe("/v0/api/verify endpoint", () => { + it("selects a key by thumbprint when kid differs", async () => { + const request = new IncomingRequest(`${sampleURL}/v0/api/verify`, { + headers: { + "Signature-Agent": 'sig1="https://keys.example";type=directory', + }, + }); + const fields = await sign(request, { + expires: new Date(Date.now() + 300_000), + signer: await signerFromJWK(jwk), + }); + request.headers.set("Signature", fields.signature); + request.headers.set("Signature-Input", fields.signatureInput); + vi.stubGlobal( + "fetch", + vi.fn(() => + Promise.resolve( + Response.json({ keys: [{ ...jwk, d: undefined, kid: "other" }] }) + ) + ) + ); + + const response = await worker.fetch(request, env, createExecutionContext()); + expect(await response.text()).toBe("valid"); + }); +}); + describe("registry draft endpoints", () => { it("serves a signature-agent card", async () => { const response = await SELF.fetch(`${sampleURL}/signature-agent-card`);