@@ -28,7 +28,7 @@ import { invalidHTML, neutralHTML, validHTML } from "./html";
2828import jwk from "../../rfc9421-keys/ed25519.json" assert { type : "json" } ;
2929import { 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
83166export 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