diff --git a/examples/verification-workers/src/html.ts b/examples/verification-workers/src/html.ts index 7d2e850..c56d38c 100644 --- a/examples/verification-workers/src/html.ts +++ b/examples/verification-workers/src/html.ts @@ -189,6 +189,20 @@ button:focus { button:active { box-shadow: inset 0 1px 1px 1px rgba(0,0,0,.4); } +input[type="url"] { + border: 1px solid #999; + border-radius: 6px; + box-sizing: border-box; + display: block; + font: inherit; + margin: 0.5rem 0; + max-width: 720px; + padding: 8px 10px; + width: 100%; +} +.validation-result { + margin-top: 0.75rem; +} .question-list { margin-bottom: 2rem; @@ -273,6 +287,17 @@ footer {

+

Validate your key directory

+

+ Paste the full HTTPS URL for a /.well-known/http-message-signatures-directory endpoint to check whether it returns a usable directory. +

+
+ + + +

+
+

It's hard to debug. How can this website help?

This website expose an endpoint dropping incoming request headers on /debug @@ -289,6 +314,26 @@ footer { To contribute to the standard discussion, the current draft is hosted on thibmeu/http-message-signatures-directory, and is being discussed on web-bot-auth IETF mailing list.

+ `; diff --git a/examples/verification-workers/src/index.ts b/examples/verification-workers/src/index.ts index 1e683b0..02e3104 100644 --- a/examples/verification-workers/src/index.ts +++ b/examples/verification-workers/src/index.ts @@ -87,6 +87,103 @@ async function fetchDirectory(signatureAgent: string): Promise { return response.json(); } +function validateDirectory(directory: unknown): string[] { + const errors: string[] = []; + + if (directory === null || typeof directory !== "object") { + return ["Directory must be a JSON object"]; + } + + const value = directory as Partial; + + if (!Array.isArray(value.keys)) { + errors.push("Directory must include a keys array"); + } else if (value.keys.length === 0) { + errors.push("Directory keys array must not be empty"); + } else { + for (const [index, key] of value.keys.entries()) { + if (key === null || typeof key !== "object") { + errors.push(`keys[${index}] must be a JSON object`); + continue; + } + if (typeof key.kty !== "string") { + errors.push(`keys[${index}].kty must be a string`); + } + } + } + + if (typeof value.purpose !== "string" || value.purpose.length === 0) { + errors.push("Directory must include a non-empty purpose string"); + } + + return errors; +} + +async function validateDirectoryURL(request: Request): Promise { + const url = new URL(request.url); + const directoryURL = url.searchParams.get("url"); + + if (directoryURL === null || directoryURL.length === 0) { + return Response.json( + { ok: false, errors: ["Missing url query parameter"] }, + { status: 400 } + ); + } + + let parsed: URL; + try { + parsed = new URL(directoryURL); + } catch (_e) { + return Response.json( + { ok: false, errors: ["URL must be valid"] }, + { status: 400 } + ); + } + + if (parsed.protocol !== "https:") { + return Response.json( + { ok: false, errors: ['Directory URL must use "https:"'] }, + { status: 400 } + ); + } + + if (parsed.pathname !== HTTP_MESSAGE_SIGNATURES_DIRECTORY) { + return Response.json( + { + ok: false, + errors: [ + `Directory URL path must be ${HTTP_MESSAGE_SIGNATURES_DIRECTORY}`, + ], + }, + { status: 400 } + ); + } + + const response = await fetch(parsed); + if (!response.ok) { + return Response.json( + { + ok: false, + errors: [`Directory returned HTTP ${response.status}`], + }, + { status: 502 } + ); + } + + let directory: unknown; + try { + directory = await response.json(); + } catch (_e) { + return Response.json( + { ok: false, errors: ["Directory response must be valid JSON"] }, + { status: 502 } + ); + } + + const errors = validateDirectory(directory); + return Response.json({ ok: errors.length === 0, errors }); +} + async function getSigner(): Promise { return Ed25519Signer.fromJWK(jwk); } @@ -180,6 +277,10 @@ export default { return new Response(status); } + if (url.pathname.startsWith("/v0/api/validate-directory")) { + return validateDirectoryURL(request); + } + if (url.pathname.startsWith(HTTP_MESSAGE_SIGNATURES_DIRECTORY)) { const directory = await getExampleDirectory(); diff --git a/examples/verification-workers/test/index.spec.ts b/examples/verification-workers/test/index.spec.ts index 2e1950b..650a3eb 100644 --- a/examples/verification-workers/test/index.spec.ts +++ b/examples/verification-workers/test/index.spec.ts @@ -49,3 +49,51 @@ describe("/debug endpoint", () => { expect(await response.text()).toMatch(headersString); }); }); + +describe("/v0/api/validate-directory endpoint", () => { + it("requires a url query parameter", async () => { + const response = await SELF.fetch( + new Request(`${sampleURL}/v0/api/validate-directory`) + ); + + expect(response.status).toEqual(400); + expect(await response.json()).toEqual({ + ok: false, + errors: ["Missing url query parameter"], + }); + }); + + it("requires an https well-known directory URL", async () => { + const response = await SELF.fetch( + new Request( + `${sampleURL}/v0/api/validate-directory?url=${encodeURIComponent( + "http://example.com/" + )}` + ) + ); + + expect(response.status).toEqual(400); + expect(await response.json()).toEqual({ + ok: false, + errors: ['Directory URL must use "https:"'], + }); + }); + + it("requires the directory well-known path", async () => { + const response = await SELF.fetch( + new Request( + `${sampleURL}/v0/api/validate-directory?url=${encodeURIComponent( + "https://example.com/" + )}` + ) + ); + + expect(response.status).toEqual(400); + expect(await response.json()).toEqual({ + ok: false, + errors: [ + "Directory URL path must be /.well-known/http-message-signatures-directory", + ], + }); + }); +});