Skip to content

Add Signature-Agent within Signature-Input and test vectors #6

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 2 commits into from
May 7, 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
5 changes: 3 additions & 2 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 { parseAcceptSignatureHeader as parseAcceptSignature } from "./parse";
export * from "./sign";
export * from "./types";
export * from "./verify";
export { parseAcceptSignatureHeader as parseAcceptSignature } from "./parse";
export * as base64 from "./base64";
66 changes: 43 additions & 23 deletions packages/web-bot-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,19 @@ More concrete examples are provided on [cloudflareresearch/web-bot-auth/examples
### Signing

```typescript
import { Algorithm, signatureHeaders } from 'web-bot-auth'
import { Algorithm, signatureHeaders } from "web-bot-auth";

// The following simple request ios going to be signed
const request = new Request('https://example.com')
const request = new Request("https://example.com");

// available at https://github.com/cloudflareresearch/web-bot-auth/blob/main/examples/rfc9421-keys/ed25519.json
const RFC_9421_ED25519_TEST_KEY = {
"kty": "OKP",
"crv": "Ed25519",
"kid": "test-key-ed25519",
"d": "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU",
"x": "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs"
}
kty: "OKP",
crv: "Ed25519",
kid: "test-key-ed25519",
d: "n4Ni-HpISpVObnQMW0wOhCKROaIKqKtW_2ZYb2p9KcU",
x: "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
};

// Declare a signer for HTTP Message Signature
class Ed25519Signer {
Expand All @@ -49,33 +49,53 @@ class Ed25519Signer {
private privateKey: CryptoKey;

constructor(keyid: string, privateKey: CryptoKey) {
this.keyid = keyid
this.privateKey = privateKey
this.keyid = keyid;
this.privateKey = privateKey;
}

static async fromJWK(public jwk: JsonWebKey): Promise<Ed25519Signer> {
const key = await crypto.subtle.import("jwk", jwk, { name: "Ed25519" }, true, ["sign"])
const keyid = await jwkToKeyID(jwk, helpers.WEBCRYPTO_SHA256, helpers.BASE64URL_DECODE)
return new Ed25519Signer(keyid, key)
static async fromJWK(jwk: JsonWebKey): Promise<Ed25519Signer> {
const key = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "Ed25519" },
true,
["sign"]
);
const keyid = await jwkToKeyID(
jwk,
helpers.WEBCRYPTO_SHA256,
helpers.BASE64URL_DECODE
);
return new Ed25519Signer(keyid, key);
}

sign(data: string): Promise<Uint8Array> {
async sign(data: string): Promise<Uint8Array> {
const message = new TextEncoder().encode(data);
return crypto.subtle.sign(message, this.privateKey);
const signature = await crypto.subtle.sign(
"ed25519",
this.privateKey,
message
);
return new Uint8Array(signature);
}
}

const headers = signatureHeaders(
request,
Ed25519Signer.fromJWK(RFC_9421_ED25519_TEST_KEY),
{
created: now,
expires: new Date(now.getTime() + 300_000), // now + 5 min
},
request,
Ed25519Signer.fromJWK(RFC_9421_ED25519_TEST_KEY),
{
created: now,
expires: new Date(now.getTime() + 300_000), // now + 5 min
}
);

// Et voila! Here is our signed request
const signedRequest = new Request('https://example.com', { headers: { 'Signature': headers['Signature'], 'Signature-Input': headers['Signature-Input'] } })
const signedRequest = new Request("https://example.com", {
headers: {
Signature: headers["Signature"],
"Signature-Input": headers["Signature-Input"],
},
});
```

### Verifying
Expand Down
1 change: 1 addition & 0 deletions packages/web-bot-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"generate-test-vectors": "node --experimental-transform-types scripts/test-vectors.ts test/test_data/web_bot_auth_architecture_v1.json",
"prepublishOnly": "npm run build",
"test": "vitest",
"watch": "npm run build -- --watch src"
Expand Down
157 changes: 157 additions & 0 deletions packages/web-bot-auth/scripts/test-vectors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/// This script generates test vectors for https://datatracker.ietf.org/doc/draft-meunier-http-message-signatures-directory/
/// The vectors are generated in JSON format
///
/// It takes one positional argument: [path] which is where the vectors should be written in JSON

const { generateNonce, helpers, jwkToKeyID, signatureHeaders } = await import(
"../src/index.ts"
);

const fs = await import("fs");
const jwk = JSON.parse(
await fs.promises.readFile("../../examples/rfc9421-keys/ed25519.json", "utf8")
);

const SIGNATURE_AGENT_DOMAIN = "signature-agent.test";
const ORIGIN_URL = "https://example.com/path/to/resource";

class Ed25519Signer {
public alg: Algorithm = "ed25519";
public keyid: string;
private privateKey: CryptoKey;

constructor(keyid: string, privateKey: CryptoKey) {
this.keyid = keyid;
this.privateKey = privateKey;
}

static async fromJWK(jwk: JsonWebKey): Promise<Ed25519Signer> {
const key = await crypto.subtle.importKey(
"jwk",
jwk,
{ name: "Ed25519" },
true,
["sign"]
);
const keyid = await jwkToKeyID(
jwk,
helpers.WEBCRYPTO_SHA256,
helpers.BASE64URL_DECODE
);
return new Ed25519Signer(keyid, key);
}

async sign(data: string): Promise<Uint8Array> {
const message = new TextEncoder().encode(data);
const signature = await crypto.subtle.sign(
"ed25519",
this.privateKey,
message
);
return new Uint8Array(signature);
}
}

interface TestVector {
key: JsonWebKey;
target_url: string;
created_ms: number;
expires_ms: number;
nonce: string;
label: string;
signature: string;
signature_input: string;
signature_agent?: string;
}

async function generateTestVectors(): Promise<TestVector[]> {
const now = new Date("2025-01-01T00:00:00Z");
const created = now;
const expires = new Date(now.getTime() + 3_600_000);
const signer = await Ed25519Signer.fromJWK(jwk);

const nonce = generateNonce();
const label = "sig1";
let request = new Request(ORIGIN_URL);
const signedHeaders = await signatureHeaders(request, signer, {
created,
expires,
nonce,
key: label,
});

const nonceWithAgent = generateNonce();
const labelWithAgent = "sig2";
request = new Request(ORIGIN_URL, {
headers: { "Signature-Agent": SIGNATURE_AGENT_DOMAIN },
});
const signedHeadersWithAgent = await signatureHeaders(request, signer, {
created,
expires,
nonce: nonceWithAgent,
key: labelWithAgent,
});

return [
{
key: jwk,
target_url: ORIGIN_URL,
created_ms: created.getTime(),
expires_ms: expires.getTime(),
nonce,
label,
signature: signedHeaders["Signature"],
signature_input: signedHeaders["Signature-Input"],
},
{
key: jwk,
target_url: ORIGIN_URL,
created_ms: created.getTime(),
expires_ms: expires.getTime(),
nonce: nonceWithAgent,
label: labelWithAgent,
signature: signedHeadersWithAgent["Signature"],
signature_input: signedHeadersWithAgent["Signature-Input"],
signature_agent: SIGNATURE_AGENT_DOMAIN,
},
];
}

const outputPath = process.argv[2];

if (!outputPath) {
console.error("Please provide a file path as the first argument.");
process.exit(1);
}

const vectors = await generateTestVectors();

for (const vector of vectors) {
console.log(`Signature base

NOTE: '\\' line wrapping per RFC 8792
`);
console.log(`"@authority": ${new URL(vector.target_url).host}`);
if (vector.signature_agent) {
console.log(`"signature-agent": ${vector.signature_agent}`);
}
console.log(
`"@signature-params": ${vector.signature_input.slice(`${vector.label}=`.length).replaceAll(";", "\\\n ;")}`
);
console.log("");

console.log(`Signature headers

NOTE: '\\' line wrapping per RFC 8792
`);
if (vector.signature_agent) {
console.log(`Signature-Agent: ${vector.signature_agent}`);
}
console.log(
`Signature-Input: ${vector.signature_input.replaceAll(";", "\\\n ;")}`
);
console.log(`Signature: ${vector.signature}`);
console.log("");
}

fs.writeFileSync(outputPath, JSON.stringify(vectors, null, 2), "utf-8");
27 changes: 24 additions & 3 deletions packages/web-bot-auth/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,21 @@ export { jwkThumbprint as jwkToKeyID } from "jsonwebkey-thumbprint";
import { b64ToB64URL, b64ToB64NoPadding, b64Tou8, u8ToB64 } from "./base64";

export const HTTP_MESSAGE_SIGNAGURE_TAG = "web-bot-auth";
export const REQUEST_COMPONENTS: httpsig.Component[] = ["@authority"];
export const SIGNATURE_AGENT_HEADER = "signature-agent";
export const REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT: httpsig.Component[] = [
"@authority",
];
export const REQUEST_COMPONENTS: httpsig.Component[] = [
"@authority",
SIGNATURE_AGENT_HEADER,
];
export const NONCE_LENGTH_IN_BYTES = 64;

export interface SignatureParams {
created: Date;
expires: Date;
nonce?: string;
key?: string;
}

export interface VerificationParams {
Expand Down Expand Up @@ -60,13 +68,20 @@ export function signatureHeaders<
throw new Error("nonce is not a valid uint32");
}
}
const signatureAgent = httpsig.extractHeader(message, SIGNATURE_AGENT_HEADER);
let components: string[] = REQUEST_COMPONENTS;
// not the ideal check, but extractHeader returns "" instead of throwing or null when the header does not exist
if (!signatureAgent) {
components = REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT;
}
return httpsig.signatureHeaders(message, {
signer,
components: REQUEST_COMPONENTS,
components,
created: params.created,
expires: params.expires,
nonce,
keyid: signer.keyid,
key: params.key,
tag: HTTP_MESSAGE_SIGNAGURE_TAG,
});
}
Expand All @@ -89,9 +104,15 @@ export function signatureHeadersSync<
throw new Error("nonce is not a valid uint32");
}
}
const signatureAgent = httpsig.extractHeader(message, SIGNATURE_AGENT_HEADER);
let components: string[] = REQUEST_COMPONENTS;
// not the ideal check, but extractHeader returns "" instead of throwing or null when the header does not exist
if (!signatureAgent) {
components = REQUEST_COMPONENTS_WITHOUT_SIGNATURE_AGENT;
}
return httpsig.signatureHeadersSync(message, {
signer,
components: REQUEST_COMPONENTS,
components,
created: params.created,
expires: params.expires,
nonce,
Expand Down
Loading