Skip to content

Add signature with each key for directory responses #30

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 21 additions & 9 deletions examples/browser-extension/scripts/build_web_artifacts.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand All @@ -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;
Expand Down Expand Up @@ -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();
33 changes: 23 additions & 10 deletions examples/verification-workers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@

import {
Directory,
HTTP_MESSAGE_SIGNATURES_DIRECTORY,
MediaType,
Signer,
VerificationParams,
directoryResponseHeaders,
helpers,
jwkToKeyID,
signatureHeaders,
Expand Down Expand Up @@ -42,6 +46,10 @@ const getDirectory = async (): Promise<Directory> => {
};
};

const getSigner = async (): Promise<Signer> => {
return Ed25519Signer.fromJWK(jwk);
};

async function verifyEd25519(
data: string,
signature: Uint8Array,
Expand Down Expand Up @@ -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,
},
});
}
Expand Down Expand Up @@ -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: {
Expand Down
6 changes: 5 additions & 1 deletion packages/http-message-sig/src/consts.ts
Original file line number Diff line number Diff line change
@@ -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",
}
59 changes: 59 additions & 0 deletions packages/http-message-sig/src/directory.ts
Original file line number Diff line number Diff line change
@@ -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<T1 extends RequestLike>(
request: T1, // request is used to derive @authority for the response
signers: Signer[],
params: SignatureParams
): Promise<SignatureHeaders> {
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<string, SignatureHeaders>();

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),
};
}
1 change: 1 addition & 0 deletions packages/http-message-sig/src/index.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 2 additions & 0 deletions packages/web-bot-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ export {
type SignatureHeaders,
type Signer,
type SignerSync,
Tag,
directoryResponseHeaders,
} from "http-message-sig";
export { jwkThumbprint as jwkToKeyID } from "jsonwebkey-thumbprint";

Expand Down