diff --git a/examples/browser-extension/scripts/build_web_artifacts.mjs b/examples/browser-extension/scripts/build_web_artifacts.mjs index f2d7177..4a9b5f9 100644 --- a/examples/browser-extension/scripts/build_web_artifacts.mjs +++ b/examples/browser-extension/scripts/build_web_artifacts.mjs @@ -17,10 +17,10 @@ import * as fs from "node:fs"; import path from "node:path"; const { KeyObject } = await import("node:crypto"); const { subtle } = globalThis.crypto; -import pkg from '../package.json' with { type: "json" }; +import pkg from "../package.json" with { type: "json" }; function makePolicy(extensionID) { - const MarkerString = "EXTENSION_ID_REPLACED_BY_NPM_RUN_BUNDLE_CHROME" + const MarkerString = "EXTENSION_ID_REPLACED_BY_NPM_RUN_BUNDLE_CHROME"; const policyPath = path.join(path.dirname("."), "policy"); if (!fs.existsSync(policyPath)) { fs.mkdirSync(policyPath, { recursive: true }); @@ -37,8 +37,20 @@ function makePolicy(extensionID) { } function setManifestVersion(version) { - const manifestInputPath = path.join(path.dirname("."), "platform", "mv3", "chromium", 'manifest.json'); - const manifestOutputPath = path.join(path.dirname("."), "dist", "mv3", "chromium", 'manifest.json'); + const manifestInputPath = path.join( + path.dirname("."), + "platform", + "mv3", + "chromium", + "manifest.json" + ); + const manifestOutputPath = path.join( + path.dirname("."), + "dist", + "mv3", + "chromium", + "manifest.json" + ); const manifestStr = fs.readFileSync(manifestInputPath, "utf8"); const manifest = JSON.parse(manifestStr); manifest.version = version; @@ -72,22 +84,22 @@ async function main() { }); const crx = new ChromeExtension({ - codebase: "http://localhost:8000/" + pkg.name + '.crx', + codebase: "http://localhost:8000/" + pkg.name + ".crx", privateKey: skPEM, publicKey: pkBytes, }); setManifestVersion(pkg.version); - await crx.load(path.join(path.dirname("."), "dist", "mv3", "chromium")) + await crx.load(path.join(path.dirname("."), "dist", "mv3", "chromium")); const extensionBytes = await crx.pack(); const extensionID = crx.generateAppId(); fs.writeFileSync("private_key.pem", skPEM); - fs.writeFileSync(path.join(distPath, pkg.name + '.crx'), extensionBytes); + fs.writeFileSync(path.join(distPath, pkg.name + ".crx"), extensionBytes); fs.writeFileSync(path.join(distPath, "update.xml"), crx.generateUpdateXML()); makePolicy(extensionID); - console.log(`Build Extension with ID: ${extensionID}`) -}; + console.log(`Build Extension with ID: ${extensionID}`); +} await main(); diff --git a/examples/verification-workers/src/index.ts b/examples/verification-workers/src/index.ts index a314339..b91a584 100644 --- a/examples/verification-workers/src/index.ts +++ b/examples/verification-workers/src/index.ts @@ -14,7 +14,11 @@ import { Directory, + HTTP_MESSAGE_SIGNATURES_DIRECTORY, + MediaType, + Signer, VerificationParams, + directoryResponseHeaders, helpers, jwkToKeyID, signatureHeaders, @@ -42,6 +46,10 @@ const getDirectory = async (): Promise => { }; }; +const getSigner = async (): Promise => { + return Ed25519Signer.fromJWK(jwk); +}; + async function verifyEd25519( data: string, signature: Uint8Array, @@ -84,12 +92,18 @@ export default { ); } - if ( - url.pathname.startsWith("/.well-known/http-message-signatures-directory") - ) { - return new Response(JSON.stringify(await getDirectory()), { + if (url.pathname.startsWith(HTTP_MESSAGE_SIGNATURES_DIRECTORY)) { + const directory = await getDirectory(); + + const signedHeaders = await directoryResponseHeaders( + request, + [await getSigner()], + { created: new Date(), expires: new Date(Date.now() + 300_000) } + ); + return new Response(JSON.stringify(directory), { headers: { - "content-type": "application/http-message-signatures-directory", + ...signedHeaders, + "content-type": MediaType.HTTP_MESSAGE_SIGNATURES_DIRECTORY, }, }); } @@ -118,11 +132,10 @@ export default { const request = new Request(env.TARGET_URL, { headers }); const created = new Date(ctx.scheduledTime); const expires = new Date(created.getTime() + 300_000); - const signedHeaders = await signatureHeaders( - request, - await Ed25519Signer.fromJWK(jwk), - { created, expires } - ); + const signedHeaders = await signatureHeaders(request, await getSigner(), { + created, + expires, + }); await fetch( new Request(request.url, { headers: { diff --git a/packages/http-message-sig/src/consts.ts b/packages/http-message-sig/src/consts.ts index bb98656..cf1f9ca 100644 --- a/packages/http-message-sig/src/consts.ts +++ b/packages/http-message-sig/src/consts.ts @@ -1,6 +1,10 @@ export const HTTP_MESSAGE_SIGNATURES_DIRECTORY = - "./well-known/http-message-signatures-directory"; + "/.well-known/http-message-signatures-directory"; export enum MediaType { HTTP_MESSAGE_SIGNATURES_DIRECTORY = "application/http-message-signatures-directory", } + +export enum Tag { + HTTP_MESSAGE_SIGNAGURES_DIRECTORY = "http-message-signatures-directory", +} diff --git a/packages/http-message-sig/src/directory.ts b/packages/http-message-sig/src/directory.ts new file mode 100644 index 0000000..ee463e6 --- /dev/null +++ b/packages/http-message-sig/src/directory.ts @@ -0,0 +1,59 @@ +import { Tag } from "./consts"; +import { signatureHeaders } from "./sign"; +import { Component, RequestLike, SignatureHeaders, Signer } from "./types"; + +export const RESPONSE_COMPONENTS: Component[] = ["@authority"]; + +export interface SignatureParams { + created: Date; + expires: Date; +} + +export async function directoryResponseHeaders( + request: T1, // request is used to derive @authority for the response + signers: Signer[], + params: SignatureParams +): Promise { + if (params.created.getTime() > params.expires.getTime()) { + throw new Error("created should happen before expires"); + } + + // TODO: consider validating the directory structure, and confirm we have one signer per key + + const components: string[] = RESPONSE_COMPONENTS; + + const headers = new Map(); + + for (let i = 0; i < signers.length; i += 1) { + // eslint-disable-next-line security/detect-object-injection + const signer = signers[i]; + if (headers.has(signer.keyid)) { + throw new Error(`Duplicated signer with keyid ${signer.keyid}`); + } + + headers.set( + signer.keyid, + await signatureHeaders(request, { + signer, + components, + created: params.created, + expires: params.expires, + keyid: signer.keyid, + key: `binding${i}`, + tag: Tag.HTTP_MESSAGE_SIGNAGURES_DIRECTORY, + }) + ); + } + + const SF_SEPARATOR = ", "; + // Providing multiple signature as described in Section 4.3 of RFC 9421 + // https://datatracker.ietf.org/doc/html/rfc9421#name-multiple-signatures + return { + Signature: Array.from(headers.values()) + .map((h) => h.Signature) + .join(SF_SEPARATOR), + "Signature-Input": Array.from(headers.values()) + .map((h) => h["Signature-Input"]) + .join(SF_SEPARATOR), + }; +} diff --git a/packages/http-message-sig/src/index.ts b/packages/http-message-sig/src/index.ts index 0abcc3f..57b6a0b 100644 --- a/packages/http-message-sig/src/index.ts +++ b/packages/http-message-sig/src/index.ts @@ -1,6 +1,7 @@ export * as base64 from "./base64"; export { extractHeader } from "./build"; export * from "./consts"; +export * from "./directory"; export { parseAcceptSignatureHeader as parseAcceptSignature } from "./parse"; export * from "./sign"; export * from "./types"; diff --git a/packages/web-bot-auth/src/index.ts b/packages/web-bot-auth/src/index.ts index 3b5b62a..301fac2 100644 --- a/packages/web-bot-auth/src/index.ts +++ b/packages/web-bot-auth/src/index.ts @@ -6,6 +6,8 @@ export { type SignatureHeaders, type Signer, type SignerSync, + Tag, + directoryResponseHeaders, } from "http-message-sig"; export { jwkThumbprint as jwkToKeyID } from "jsonwebkey-thumbprint";