Skip to content

Commit 36b5495

Browse files
authored
Add /v0/api/verify (#57)
It allows to get a shorthand to validate a signature, instead of parsing the HTML page that is targetted at browsers. In addition, this commit adds support to read signature-agent
1 parent ad610e5 commit 36b5495

File tree

3 files changed

+130
-44
lines changed

3 files changed

+130
-44
lines changed

examples/browser-extension/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ You can confirm the policy is installed by navigating to `chrome://policy` and m
6868
## Debugging the Extension
6969

7070
Extensions installed by an Enterprise policy do not enable the DevTools by default. To enable the DevTools, open the system's policy file and add the following entry:
71+
7172
```
7273
"DeveloperToolsAvailability": 1
7374
```

examples/verification-workers/src/html.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,7 @@ footer {
253253
"x":"JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
254254
"nbf": 1743465600000
255255
}
256-
],
257-
"purpose": "rag"
256+
]
258257
}
259258
</pre>
260259

examples/verification-workers/src/index.ts

Lines changed: 128 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { invalidHTML, neutralHTML, validHTML } from "./html";
2828
import jwk from "../../rfc9421-keys/ed25519.json" assert { type: "json" };
2929
import { Ed25519Signer } from "web-bot-auth/crypto";
3030

31-
const getDirectory = async (): Promise<Directory> => {
31+
async function getExampleDirectory(): Promise<Directory> {
3232
const key = {
3333
kid: await jwkToKeyID(
3434
jwk,
@@ -44,40 +44,123 @@ const getDirectory = async (): Promise<Directory> => {
4444
keys: [key],
4545
purpose: "rag",
4646
};
47-
};
47+
}
48+
49+
async function fetchDirectory(signatureAgent: string): Promise<Directory> {
50+
// make "some" validatation of the Signature-Agent header before making a request
51+
let parsed: string;
52+
try {
53+
parsed = JSON.parse(signatureAgent);
54+
} catch (_e) {
55+
const e = new Error(
56+
`Failed to validate Signature-Agent header: ${signatureAgent}`
57+
);
58+
console.error(e.message);
59+
throw e;
60+
}
61+
62+
try {
63+
const url = new URL(parsed);
64+
if (url.protocol !== "https:") {
65+
throw new Error(
66+
'The demo only supports "https:" scheme for Signature-Agent header'
67+
);
68+
}
69+
if (url.pathname !== "/") {
70+
throw new Error(
71+
`Only support signature-agent at the root, got "${url.pathname}"`
72+
);
73+
}
74+
} catch (e) {
75+
console.error(
76+
`Failed to validate Signature-Agent header: ${signatureAgent}`
77+
);
78+
throw e;
79+
}
80+
if (parsed.endsWith("/")) {
81+
parsed = parsed.slice(0, -1);
82+
}
83+
console.log(
84+
`Fetching \`Signature-Agent\` directory from: "${parsed}${HTTP_MESSAGE_SIGNATURES_DIRECTORY}"`
85+
);
86+
const response = await fetch(`${parsed}${HTTP_MESSAGE_SIGNATURES_DIRECTORY}`);
87+
return response.json();
88+
}
4889

49-
const getSigner = async (): Promise<Signer> => {
90+
async function getSigner(): Promise<Signer> {
5091
return Ed25519Signer.fromJWK(jwk);
51-
};
92+
}
5293

53-
async function verifyEd25519(
94+
function verifyEd25519(
95+
directory: Directory
96+
): (
5497
data: string,
5598
signature: Uint8Array,
5699
params: VerificationParams
57-
) {
58-
// note that here we use getDirectory, but this is as simple as a fetch
59-
const directory = await getDirectory();
60-
61-
const key = await crypto.subtle.importKey(
62-
"jwk",
63-
directory.keys[0],
64-
{ name: "Ed25519" },
65-
true,
66-
["verify"]
67-
);
100+
) => Promise<void> {
101+
return async (data, signature, _params) => {
102+
const key = await crypto.subtle.importKey(
103+
"jwk",
104+
directory.keys[0],
105+
{ name: "Ed25519" },
106+
true,
107+
["verify"]
108+
);
68109

69-
const encodedData = new TextEncoder().encode(data);
110+
const encodedData = new TextEncoder().encode(data);
70111

71-
const isValid = await crypto.subtle.verify(
72-
{ name: "Ed25519" },
73-
key,
74-
signature,
75-
encodedData
76-
);
112+
const isValid = await crypto.subtle.verify(
113+
{ name: "Ed25519" },
114+
key,
115+
signature,
116+
encodedData
117+
);
77118

78-
if (!isValid) {
79-
throw new Error("invalid signature");
119+
if (!isValid) {
120+
throw new Error("invalid signature");
121+
}
122+
};
123+
}
124+
125+
const SignatureValidationStatus = {
126+
NEUTRAL: "neutral",
127+
INVALID: (message?: string) => `invalid${message ? `: ${message}` : ""}`,
128+
VALID: "valid",
129+
} as const;
130+
type SignatureValidationStatus = string;
131+
132+
async function verifySignature(
133+
env: Env,
134+
request: Request
135+
): Promise<SignatureValidationStatus> {
136+
if (request.headers.get("Signature") === null) {
137+
return SignatureValidationStatus.NEUTRAL;
138+
}
139+
140+
const signatureAgent = request.headers.get("Signature-Agent");
141+
let directory: Directory;
142+
try {
143+
if (signatureAgent && !signatureAgent.includes(env.SIGNATURE_AGENT)) {
144+
directory = await fetchDirectory(signatureAgent);
145+
} else {
146+
directory = await getExampleDirectory();
147+
}
148+
} catch (e) {
149+
return SignatureValidationStatus.INVALID((e as Error).message);
80150
}
151+
152+
try {
153+
await verify(request, verifyEd25519(directory));
154+
} catch (e) {
155+
return SignatureValidationStatus.INVALID((e as Error).message);
156+
}
157+
158+
console.log("Signature verified successfully");
159+
if (signatureAgent) {
160+
console.log(`Signature-Agent: "${signatureAgent}"`);
161+
}
162+
163+
return SignatureValidationStatus.VALID;
81164
}
82165

83166
export default {
@@ -92,8 +175,13 @@ export default {
92175
);
93176
}
94177

178+
if (url.pathname.startsWith("/v0/api/verify")) {
179+
const status = await verifySignature(env, request);
180+
return new Response(status);
181+
}
182+
95183
if (url.pathname.startsWith(HTTP_MESSAGE_SIGNATURES_DIRECTORY)) {
96-
const directory = await getDirectory();
184+
const directory = await getExampleDirectory();
97185

98186
const signedHeaders = await directoryResponseHeaders(
99187
request,
@@ -108,23 +196,21 @@ export default {
108196
});
109197
}
110198

111-
if (request.headers.get("Signature") === null) {
112-
return new Response(neutralHTML, {
113-
headers: { "content-type": "text/html" },
114-
});
199+
const status = await verifySignature(env, request);
200+
switch (status) {
201+
case SignatureValidationStatus.NEUTRAL:
202+
return new Response(neutralHTML, {
203+
headers: { "content-type": "text/html; charset=utf-8" },
204+
});
205+
case SignatureValidationStatus.VALID:
206+
return new Response(validHTML, {
207+
headers: { "content-type": "text/html; charset=utf-8" },
208+
});
209+
default:
210+
return new Response(invalidHTML, {
211+
headers: { "content-type": "text/html; charset=utf-8" },
212+
});
115213
}
116-
117-
try {
118-
await verify(request, verifyEd25519);
119-
} catch (e) {
120-
console.error(e);
121-
return new Response(invalidHTML, {
122-
headers: { "content-type": "text/html" },
123-
});
124-
}
125-
return new Response(validHTML, {
126-
headers: { "content-type": "text/html" },
127-
});
128214
},
129215
// On a schedule, send a web-bot-auth signed request to a target endpoint
130216
async scheduled(ctx, env, ectx) {

0 commit comments

Comments
 (0)