From 7e262c41af4a1f893bc191296a70592f576b03bc Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Thu, 7 May 2026 16:59:59 +0100 Subject: [PATCH 1/9] feat: protect directory validator --- examples/verification-workers/src/html.ts | 99 +++++- examples/verification-workers/src/index.ts | 336 +++++++++++++++++- .../verification-workers/test/index.spec.ts | 276 +++++++++++++- .../worker-configuration.d.ts | 2 + 4 files changed, 700 insertions(+), 13 deletions(-) diff --git a/examples/verification-workers/src/html.ts b/examples/verification-workers/src/html.ts index 7d2e850..a8a209a 100644 --- a/examples/verification-workers/src/html.ts +++ b/examples/verification-workers/src/html.ts @@ -12,12 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -const generateHTML = (status?: boolean) => ` +function escapeAttribute(value: string): string { + return value.replaceAll("&", "&").replaceAll('"', """); +} + +export const generateHTML = ( + status: boolean | undefined, + turnstileSiteKey: string +) => ` Identify Bots with HTTP Message Signatures + +@media (max-width: 640px) { + .header-grid { + grid-template-columns: 1fr; + gap: 0; + } + .header-grid label { + margin-top: 1rem; + } +} +`; + +export const generateHTML = (status: boolean | undefined) => ` + + + + + Identify Bots with HTTP Message Signatures + ${theme}
@@ -301,21 +342,9 @@ 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 + Use the debug page to validate key directories and signature headers.

I have comments and want to contribute. Where do I go?

@@ -329,9 +358,88 @@ 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.

+ +`; + +export const generateDebugHTML = (turnstileSiteKey: string) => ` + + + + + Debug HTTP Message Signatures + + ${theme} + + +
+

Debug HTTP Message Signatures

+

Validate key directories and inspect signature inputs.

+
+
+

+ This page collects debugging tools for Web Bot Auth implementations. Start by validating the key directory. Header verification is reserved for a later step. +

+ +
+

Validate key directory

+

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

+
+ + +
+ +
+
+
+ +
+

Verify request headers

+

+ Paste the signed request target and HTTP Message Signature headers here. +

+
+
+ + + + + + + + + + + + + + + + + +
+ +
+ + +
+
+
+
${theme} + ${debugStyle}
@@ -377,7 +117,7 @@ export const generateDebugHTML = (turnstileSiteKey: string) => `

- This page collects debugging tools for Web Bot Auth implementations. Start by validating the key directory. Header verification is reserved for a later step. + This page collects debugging tools for Web Bot Auth implementations. Start by validating the key directory.

@@ -551,8 +291,6 @@ export const generateDebugHTML = (turnstileSiteKey: string) => ` return { key: match[1], components, params, input: header.replace(/^[^=]+=/, "") }; }; - const requestTarget = (url) => url.pathname + url.search; - const componentValue = (component, request, headers) => { switch (component) { case "@method": @@ -567,7 +305,7 @@ export const generateDebugHTML = (turnstileSiteKey: string) => ` case "@scheme": return request.url.protocol.slice(0, -1); case "@request-target": - return requestTarget(request.url); + return request.url.pathname + request.url.search; case "@path": return request.url.pathname; case "@query": diff --git a/examples/verification-workers/src/index-html.ts b/examples/verification-workers/src/index-html.ts new file mode 100644 index 0000000..7e55316 --- /dev/null +++ b/examples/verification-workers/src/index-html.ts @@ -0,0 +1,299 @@ +// Copyright 2025 Cloudflare, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export const theme = ``; + +const generateHTML = (status?: boolean) => ` + + + + + Identify Bots with HTTP Message Signatures + ${theme} + + +
+

Identify Bots with HTTP Message Signatures

+

+ ${status === undefined ? "Your browser does not support HTTP Message Signatures" : ""} + ${status === false ? "The Signature you sent does not validate against test public key" : ""} + ${status === true ? "You successfully authenticated as owning the test public key" : ""} +

+
+
+

+ HTTP Message Signatures are a mechanism to create, encode, and verify signatures over components of an HTTP message. + They are standardised by the IETF in RFC 9421. + + This website validates the presence of such signature as defined in draft-meunier-web-bot-auth-architecture. +

+

+ This website checks for an Ed25519 signature on incoming request. They should be signed by a test public key defined in Appendix B.1.4 of RFC 9421. +

+ +

Why do platforms and websites need this?

+

+ As a platform provider, I would like to ensure websites are able to identify requests originating from my service. + At the moment, I share IP ranges, but this is long to deploy, cumbersome to maintain, and costly, especially with the multiplication of services, and the need to localise outgoing traffic with a forward proxy. + It's even more pressing as I onboard multiple companies on my platform that need to have their own identity. + And user agent headers do not have any integrity protection. +

+

+ It's time for websites to know who's calling, and for platforms to prove it. +

+ +

How to retrieve the public key used by this website

+

+ We define a key directory accessible under /.well-known/http-message-signatures-directory + + The directory looks as follow +

+
{
+  "keys": [
+    {
+      "kid":"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U",
+      "kty":"OKP",
+      "crv":"Ed25519",
+      "x":"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
+      "nbf": 1743465600000
+    }
+  ]
+}
+      
+ +

+ Parameters are defined as follow: + +

    +
  • keys: an array of serialised JSON Web Key defined by RFC 7517 +
      +
    • kid: JWK Thumbprint as defined in RFC 7638
    • +
    • nbf: start of the validity of the public key as a unix timestamp in milliseconds defined by RFC 7519
    • +
    • exp: end of the validity of the public key as a unix timestamp in milliseconds defined by RFC 7519
    • +
    • ...jwk: JWK public cryptographic material
    • +
    +
  • +
  • purpose: represents what a signature means. Examples could be the draft for Short usage preference proposed.
  • +
+

+ +

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

+

+ Use the debug page to validate key directories and signature headers. +

+ +

I have comments and want to contribute. Where do I go?

+

+ First off, this is fantastic news! +

+

+ To contribute to this website, you can go to cloudflareresearch/web-bot-auth. +

+

+ 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. +

+
+ +`; + +export const neutralHTML = generateHTML(); +export const invalidHTML = generateHTML(false); +export const validHTML = generateHTML(true); diff --git a/examples/verification-workers/src/index.ts b/examples/verification-workers/src/index.ts index 1a68bdf..7e0152b 100644 --- a/examples/verification-workers/src/index.ts +++ b/examples/verification-workers/src/index.ts @@ -24,7 +24,8 @@ import { signatureHeaders, verify, } from "web-bot-auth"; -import { generateDebugHTML, generateHTML } from "./html"; +import { generateDebugHTML } from "./debug-html"; +import { invalidHTML, neutralHTML, validHTML } from "./index-html"; import { proxyDirectoryRequest } from "./proxy-directory"; import jwk from "../../rfc9421-keys/ed25519.json" assert { type: "json" }; import { Ed25519Signer } from "web-bot-auth/crypto"; @@ -33,27 +34,6 @@ function errorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } -function getProperty(value: unknown, name: string): unknown { - if (value === null || typeof value !== "object") { - return undefined; - } - for (const [key, property] of Object.entries(value)) { - if (key === name) { - return property; - } - } - return undefined; -} - -function getStringProperty(value: unknown, name: string): string | undefined { - const property = getProperty(value, name); - return typeof property === "string" ? property : undefined; -} - -function getTurnstileSiteKey(env: Env): string { - return getStringProperty(env, "TURNSTILE_SITE_KEY") ?? ""; -} - async function getExampleDirectory(): Promise { const key = { kid: await jwkToKeyID( @@ -196,7 +176,7 @@ export default { const url = new URL(request.url); if (url.pathname.startsWith("/debug")) { - return new Response(generateDebugHTML(getTurnstileSiteKey(env)), { + return new Response(generateDebugHTML(env.TURNSTILE_SITE_KEY ?? ""), { headers: { "content-type": "text/html; charset=utf-8" }, }); } @@ -229,15 +209,15 @@ export default { const status = await verifySignature(env, request); switch (status) { case SignatureValidationStatus.NEUTRAL: - return new Response(generateHTML(undefined), { + return new Response(neutralHTML, { headers: { "content-type": "text/html; charset=utf-8" }, }); case SignatureValidationStatus.VALID: - return new Response(generateHTML(true), { + return new Response(validHTML, { headers: { "content-type": "text/html; charset=utf-8" }, }); default: - return new Response(generateHTML(false), { + return new Response(invalidHTML, { headers: { "content-type": "text/html; charset=utf-8" }, }); } From 5f675fc7cb72e3010b7e8a7f1087349a39a50805 Mon Sep 17 00:00:00 2001 From: Thibault Meunier Date: Tue, 26 May 2026 12:17:34 +0200 Subject: [PATCH 7/9] fix: repair debug validation --- .../verification-workers/src/debug-html.ts | 47 +++++++++++++++++-- examples/verification-workers/src/index.ts | 16 ++++--- .../verification-workers/test/index.spec.ts | 21 +++++++++ 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/examples/verification-workers/src/debug-html.ts b/examples/verification-workers/src/debug-html.ts index 65213e1..bbad7b6 100644 --- a/examples/verification-workers/src/debug-html.ts +++ b/examples/verification-workers/src/debug-html.ts @@ -18,6 +18,18 @@ function escapeAttribute(value: string): string { return value.replaceAll("&", "&").replaceAll('"', """); } +function turnstileScript(siteKey: string): string { + return siteKey.length > 0 + ? '' + : ""; +} + +function turnstileWidget(siteKey: string): string { + return siteKey.length > 0 + ? `
` + : '

Turnstile is not configured for this deployment.

'; +} + const debugStyle = `