Skip to content

Add "web-bot-auth/crypto" to ease signing #7

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
May 12, 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
39 changes: 1 addition & 38 deletions packages/web-bot-auth/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ More concrete examples are provided on [cloudflareresearch/web-bot-auth/examples

```typescript
import { Algorithm, signatureHeaders } from "web-bot-auth";
import { Ed25519Signer } from "web-bot-auth/crypto";

// The following simple request ios going to be signed
const request = new Request("https://example.com");
Expand All @@ -42,44 +43,6 @@ const RFC_9421_ED25519_TEST_KEY = {
x: "JrQLj5P_89iXES9-vFgrIy29clF9CC_oPPsw3c5D0bs",
};

// Declare a signer for HTTP Message Signature
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);
}
}

const headers = signatureHeaders(
request,
Ed25519Signer.fromJWK(RFC_9421_ED25519_TEST_KEY),
Expand Down
7 changes: 6 additions & 1 deletion packages/web-bot-auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
"types": "./dist/index.d.ts",
"require": "./dist/index.js",
"import": "./dist/index.mjs"
},
"./crypto": {
"types": "./dist/crypto.d.ts",
"require": "./dist/crypto.js",
"import": "./dist/crypto.mjs"
}
},
"scripts": {
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
"build": "tsup src/index.ts src/crypto.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",
Expand Down
43 changes: 3 additions & 40 deletions packages/web-bot-auth/scripts/test-vectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
///
/// 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 { generateNonce, signatureHeaders } = await import("../src/index.ts");

const { Ed25519Signer } = await import("../src/crypto.ts");

const fs = await import("fs");
const jwk = JSON.parse(
Expand All @@ -15,43 +15,6 @@ const jwk = JSON.parse(
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;
Expand Down
46 changes: 46 additions & 0 deletions packages/web-bot-auth/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { type Algorithm } from "http-message-sig";
import { jwkThumbprint as jwkToKeyID } from "jsonwebkey-thumbprint";
import { b64ToB64NoPadding, b64ToB64URL, u8ToB64 } from "./base64";

export const helpers = {
WEBCRYPTO_SHA256: (b: BufferSource) => crypto.subtle.digest("SHA-256", b),
BASE64URL_DECODE: (u: ArrayBuffer) =>
b64ToB64URL(b64ToB64NoPadding(u8ToB64(new Uint8Array(u)))),
};

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

import { b64ToB64URL, b64ToB64NoPadding, b64Tou8, u8ToB64 } from "./base64";
import { b64Tou8, u8ToB64 } from "./base64";
export { helpers } from "./crypto";

export const HTTP_MESSAGE_SIGNAGURE_TAG = "web-bot-auth";
export const SIGNATURE_AGENT_HEADER = "signature-agent";
Expand Down Expand Up @@ -163,9 +164,3 @@ export function verify<T>(
export interface Directory extends httpsig.Directory {
purpose: string;
}

export const helpers = {
WEBCRYPTO_SHA256: (b: BufferSource) => crypto.subtle.digest("SHA-256", b),
BASE64URL_DECODE: (u: ArrayBuffer) =>
b64ToB64URL(b64ToB64NoPadding(u8ToB64(new Uint8Array(u)))),
};
40 changes: 1 addition & 39 deletions packages/web-bot-auth/test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,17 @@
import { describe, it, expect } from "vitest";
import {
generateNonce,
helpers,
jwkToKeyID,
signatureHeaders,
validateNonce,
NONCE_LENGTH_IN_BYTES,
SIGNATURE_AGENT_HEADER,
} from "../src/index";
import { Ed25519Signer } from "../src/crypto";
import { b64Tou8, u8ToB64 } from "../src/base64";

import vectors from "./test_data/web_bot_auth_architecture_v1.json";
type Vectors = (typeof vectors)[number];

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);
}
}

describe.each(vectors)("Web-bot-auth-ed25519-Vector-%#", (v: Vectors) => {
it("should pass IETF draft test vectors", async () => {
const signer = await Ed25519Signer.fromJWK(v.key);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@
"signature_input": "sig2=(\"@authority\" \"signature-agent\");created=1735689600;keyid=\"poqkLGiymh_W0uP6PZFw-dvez3QJT5SolqXBCW38r0U\";alg=\"ed25519\";expires=1735693200;nonce=\"ZO3/XMEZjrvSnLtAP9M7jK0WGQf3J+pbmQRUpKDhF9/jsNCWqUh2sq+TH4WTX3/GpNoSZUa8eNWMKqxWp2/c2g==\";tag=\"web-bot-auth\"",
"signature_agent": "signature-agent.test"
}
]
]