@algovoi/rfc9421-verifier 0.1.1 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/parse.d.ts CHANGED
@@ -11,6 +11,14 @@ export interface ParsedSignatureInput {
11
11
  covered_components: string[];
12
12
  parameters: Record<string, string | number>;
13
13
  raw: string;
14
+ /**
15
+ * The post-label portion of the Signature-Input header value, i.e.
16
+ * the Inner List + parameters block exactly as it appeared on the
17
+ * wire. This is the value that the @signature-params line in the
18
+ * RFC 9421 §2.5 signing base must carry. Empty string if the input
19
+ * was the unlabelled form.
20
+ */
21
+ params_block: string;
14
22
  }
15
23
  export declare function parseSignatureInput(headerValue: string): ParsedSignatureInput;
16
24
  export declare function parseSignatureValue(headerValue: string): {
package/dist/parse.js CHANGED
@@ -35,6 +35,9 @@ export function parseSignatureInput(headerValue) {
35
35
  else {
36
36
  throw new SignatureInputParseError(`no label or covered-components list found at start: ${JSON.stringify(trimmed.slice(0, 40))}`);
37
37
  }
38
+ // Capture the post-label portion verbatim. This is what the
39
+ // @signature-params line of the RFC 9421 signing base must contain.
40
+ const paramsBlock = rest;
38
41
  const coveredMatch = COVERED_RE.exec(rest);
39
42
  if (!coveredMatch) {
40
43
  throw new SignatureInputParseError("no covered-components list found");
@@ -68,6 +71,7 @@ export function parseSignatureInput(headerValue) {
68
71
  covered_components: covered,
69
72
  parameters,
70
73
  raw: trimmed,
74
+ params_block: paramsBlock,
71
75
  };
72
76
  }
73
77
  export function parseSignatureValue(headerValue) {
@@ -6,6 +6,7 @@
6
6
  export declare class SigningBaseError extends Error {
7
7
  constructor(message: string);
8
8
  }
9
+ export type SigningBaseMode = "algovoi-v0" | "rfc9421";
9
10
  export interface SigningBaseInput {
10
11
  coveredComponents: string[];
11
12
  method?: string;
@@ -16,5 +17,26 @@ export interface SigningBaseInput {
16
17
  status?: number;
17
18
  headers?: Record<string, string>;
18
19
  parameters?: Record<string, string | number>;
20
+ /**
21
+ * Signing-base mode.
22
+ * - "algovoi-v0" (default): preserves the v0.1.0 behaviour for
23
+ * backward compatibility with the AlgoVoi internal fixture and
24
+ * the rfc9421_proxy_chain_v0 conformance set. @method is
25
+ * lowercased and no @signature-params line is appended.
26
+ * - "rfc9421": full RFC 9421 §2.5 compliance. @method is preserved
27
+ * as-supplied (HTTP convention is uppercase), and a final
28
+ * "@signature-params" line is appended carrying
29
+ * signatureParamsRaw verbatim. This is the shape required to
30
+ * verify external fixtures (Envoys envoys-rfc9421, Hippo
31
+ * hippo-rfc9421, RFC 9421 §B test vectors, and any other
32
+ * RFC-compliant implementation).
33
+ */
34
+ mode?: SigningBaseMode;
35
+ /**
36
+ * The post-label portion of the Signature-Input header value, i.e.
37
+ * the Inner List + parameters block exactly as it appeared on the
38
+ * wire. Required when mode is "rfc9421".
39
+ */
40
+ signatureParamsRaw?: string;
19
41
  }
20
42
  export declare function buildSigningBase(input: SigningBaseInput): string;
@@ -10,6 +10,13 @@ export class SigningBaseError extends Error {
10
10
  }
11
11
  }
12
12
  export function buildSigningBase(input) {
13
+ const mode = input.mode ?? "algovoi-v0";
14
+ if (mode !== "algovoi-v0" && mode !== "rfc9421") {
15
+ throw new SigningBaseError(`mode must be "algovoi-v0" or "rfc9421", got ${JSON.stringify(mode)}`);
16
+ }
17
+ if (mode === "rfc9421" && input.signatureParamsRaw === undefined) {
18
+ throw new SigningBaseError("rfc9421 mode requires signatureParamsRaw (the post-label portion of the Signature-Input header)");
19
+ }
13
20
  const normHeaders = {};
14
21
  for (const [k, v] of Object.entries(input.headers ?? {})) {
15
22
  normHeaders[k.toLowerCase()] = v;
@@ -23,7 +30,7 @@ export function buildSigningBase(input) {
23
30
  case "@method":
24
31
  if (input.method === undefined)
25
32
  throw new SigningBaseError("@method covered but method not supplied");
26
- value = input.method.toLowerCase();
33
+ value = mode === "rfc9421" ? input.method : input.method.toLowerCase();
27
34
  break;
28
35
  case "@authority":
29
36
  if (input.authority === undefined)
@@ -72,5 +79,8 @@ export function buildSigningBase(input) {
72
79
  }
73
80
  lines.push(`"${c}": ${value}`);
74
81
  }
82
+ if (mode === "rfc9421") {
83
+ lines.push(`"@signature-params": ${input.signatureParamsRaw}`);
84
+ }
75
85
  return lines.join("\n");
76
86
  }
package/dist/verify.d.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  * Mirror of the Python algovoi_rfc9421_verifier.verify module.
5
5
  * Uses @noble/ed25519 for Ed25519 verification.
6
6
  */
7
+ import { type SigningBaseMode } from "./signing-base.js";
7
8
  export declare class VerifyError extends Error {
8
9
  constructor(message: string);
9
10
  }
@@ -27,7 +28,15 @@ export interface VerifyRequestInput {
27
28
  publicKey: PublicKey;
28
29
  scheme?: string;
29
30
  requireContentDigest?: boolean;
30
- requireAlgorithm?: string;
31
+ requireAlgorithm?: string | null;
32
+ /**
33
+ * Signing-base mode. Default "algovoi-v0" preserves backward
34
+ * compatibility with the v0.1.0 internal fixture and the
35
+ * rfc9421_proxy_chain_v0 conformance set. Set to "rfc9421" to
36
+ * verify external RFC 9421-compliant fixtures (Envoys, Hippo,
37
+ * RFC 9421 §B test vectors).
38
+ */
39
+ mode?: SigningBaseMode;
31
40
  }
32
41
  export declare function verifySignature(signingBase: string, signatureBytes: Uint8Array, publicKey: PublicKey, algorithm?: string): Promise<boolean>;
33
42
  export declare function verifyRequest(input: VerifyRequestInput): Promise<VerifyResult>;
package/dist/verify.js CHANGED
@@ -111,7 +111,10 @@ export async function verifyRequest(input) {
111
111
  return fail(result, `Signature label ${sigParsed.label} does not match Signature-Input label ${parsedSi.label}`);
112
112
  }
113
113
  const requireCd = input.requireContentDigest ?? true;
114
- const requireAlg = input.requireAlgorithm ?? "sha-256";
114
+ // string = enforce that specific algorithm; null or undefined = accept any
115
+ // supported algorithm present in the Content-Digest header (SHA-256 or
116
+ // SHA-512 in this v0.2.x). Default is permissive.
117
+ const requireAlg = input.requireAlgorithm == null ? undefined : input.requireAlgorithm;
115
118
  if (requireCd) {
116
119
  const cdHeader = normHeaders["content-digest"];
117
120
  if (!cdHeader)
@@ -130,6 +133,7 @@ export async function verifyRequest(input) {
130
133
  else {
131
134
  result.content_digest_valid = true;
132
135
  }
136
+ const mode = input.mode ?? "algovoi-v0";
133
137
  let signingBase;
134
138
  try {
135
139
  signingBase = buildSigningBase({
@@ -140,6 +144,8 @@ export async function verifyRequest(input) {
140
144
  scheme: input.scheme ?? "https",
141
145
  headers: normHeaders,
142
146
  parameters: parsedSi.parameters,
147
+ mode,
148
+ signatureParamsRaw: mode === "rfc9421" ? parsedSi.params_block : undefined,
143
149
  });
144
150
  }
145
151
  catch (e) {
package/package.json CHANGED
@@ -1,62 +1,62 @@
1
- {
2
- "name": "@algovoi/rfc9421-verifier",
3
- "version": "0.1.1",
4
- "description": "AlgoVoi RFC 9421 HTTP Message Signatures + RFC 9530 Content-Digest reference verifier",
5
- "keywords": [
6
- "rfc9421",
7
- "rfc9530",
8
- "http-signatures",
9
- "ed25519",
10
- "agentic-payments",
11
- "compliance"
12
- ],
13
- "homepage": "https://github.com/chopmob-cloud/algovoi-rfc9421-verifier",
14
- "repository": {
15
- "type": "git",
16
- "url": "https://github.com/chopmob-cloud/algovoi-rfc9421-verifier.git",
17
- "directory": "typescript"
18
- },
19
- "bugs": {
20
- "url": "https://github.com/chopmob-cloud/algovoi-rfc9421-verifier/issues"
21
- },
22
- "license": "Apache-2.0",
23
- "author": "AlgoVoi <chopmob@gmail.com>",
24
- "type": "module",
25
- "main": "./dist/index.js",
26
- "module": "./dist/index.js",
27
- "types": "./dist/index.d.ts",
28
- "exports": {
29
- ".": {
30
- "import": "./dist/index.js",
31
- "types": "./dist/index.d.ts"
32
- }
33
- },
34
- "files": [
35
- "dist/**/*",
36
- "src/**/*",
37
- "README.md",
38
- "LICENSE"
39
- ],
40
- "scripts": {
41
- "build": "tsc",
42
- "test": "vitest run",
43
- "test:watch": "vitest",
44
- "clean": "rimraf dist",
45
- "prepublishOnly": "npm run clean && npm run build && npm test"
46
- },
47
- "engines": {
48
- "node": ">=18"
49
- },
50
- "dependencies": {
51
- "@noble/ed25519": "^2.1.0"
52
- },
53
- "devDependencies": {
54
- "@types/node": "^20.0.0",
55
- "rimraf": "^5.0.0",
56
- "typescript": "^5.4.0",
57
- "vitest": "^1.6.0"
58
- },
59
- "publishConfig": {
60
- "access": "public"
61
- }
62
- }
1
+ {
2
+ "name": "@algovoi/rfc9421-verifier",
3
+ "version": "0.2.1",
4
+ "description": "AlgoVoi RFC 9421 HTTP Message Signatures + RFC 9530 Content-Digest reference verifier",
5
+ "keywords": [
6
+ "rfc9421",
7
+ "rfc9530",
8
+ "http-signatures",
9
+ "ed25519",
10
+ "agentic-payments",
11
+ "compliance"
12
+ ],
13
+ "homepage": "https://github.com/chopmob-cloud/algovoi-rfc9421-verifier",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/chopmob-cloud/algovoi-rfc9421-verifier.git",
17
+ "directory": "typescript"
18
+ },
19
+ "bugs": {
20
+ "url": "https://github.com/chopmob-cloud/algovoi-rfc9421-verifier/issues"
21
+ },
22
+ "license": "Apache-2.0",
23
+ "author": "AlgoVoi <chopmob@gmail.com>",
24
+ "type": "module",
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "import": "./dist/index.js",
31
+ "types": "./dist/index.d.ts"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist/**/*",
36
+ "src/**/*",
37
+ "README.md",
38
+ "LICENSE"
39
+ ],
40
+ "scripts": {
41
+ "build": "tsc",
42
+ "test": "vitest run",
43
+ "test:watch": "vitest",
44
+ "clean": "rimraf dist",
45
+ "prepublishOnly": "npm run clean && npm run build && npm test"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "dependencies": {
51
+ "@noble/ed25519": "^2.1.0"
52
+ },
53
+ "devDependencies": {
54
+ "@types/node": "^20.0.0",
55
+ "rimraf": "^5.0.0",
56
+ "typescript": "^5.4.0",
57
+ "vitest": "^1.6.0"
58
+ },
59
+ "publishConfig": {
60
+ "access": "public"
61
+ }
62
+ }
package/src/parse.ts CHANGED
@@ -1,135 +1,148 @@
1
- /**
2
- * RFC 9421 Signature-Input and Signature header parsers.
3
- *
4
- * Mirror of the Python algovoi_rfc9421_verifier.parse module.
5
- */
6
-
7
- export class SignatureInputParseError extends Error {
8
- constructor(message: string) {
9
- super(message);
10
- this.name = "SignatureInputParseError";
11
- }
12
- }
13
-
14
- export interface ParsedSignatureInput {
15
- label: string;
16
- covered_components: string[];
17
- parameters: Record<string, string | number>;
18
- raw: string;
19
- }
20
-
21
- const LABEL_RE = /^\s*([A-Za-z][A-Za-z0-9_-]*)\s*=\s*/;
22
- const COVERED_RE = /\(\s*(?:"[^"]*"\s*)*\)/;
23
- const QUOTED_RE = /"([^"]*)"/g;
24
- const PARAM_RE = /([A-Za-z][A-Za-z0-9_-]*)=([^;,\s]+|"[^"]*")/g;
25
-
26
- export function parseSignatureInput(headerValue: string): ParsedSignatureInput {
27
- if (typeof headerValue !== "string") {
28
- throw new SignatureInputParseError(
29
- `header must be string, got ${typeof headerValue}`,
30
- );
31
- }
32
- const trimmed = headerValue.trim();
33
- if (trimmed.length === 0) {
34
- throw new SignatureInputParseError("empty header value");
35
- }
36
-
37
- let label: string;
38
- let rest: string;
39
-
40
- const labelMatch = LABEL_RE.exec(trimmed);
41
- if (labelMatch) {
42
- label = labelMatch[1];
43
- rest = trimmed.slice(labelMatch[0].length);
44
- } else if (trimmed.startsWith("(")) {
45
- label = "";
46
- rest = trimmed;
47
- } else {
48
- throw new SignatureInputParseError(
49
- `no label or covered-components list found at start: ${JSON.stringify(trimmed.slice(0, 40))}`,
50
- );
51
- }
52
-
53
- const coveredMatch = COVERED_RE.exec(rest);
54
- if (!coveredMatch) {
55
- throw new SignatureInputParseError("no covered-components list found");
56
- }
57
- const coveredRaw = coveredMatch[0];
58
- const covered: string[] = [];
59
- let qm: RegExpExecArray | null;
60
- const localQuoted = /"([^"]*)"/g;
61
- while ((qm = localQuoted.exec(coveredRaw)) !== null) {
62
- covered.push(qm[1]);
63
- }
64
- rest = rest.slice(coveredMatch.index + coveredRaw.length);
65
-
66
- const parameters: Record<string, string | number> = {};
67
- const localParam = new RegExp(PARAM_RE.source, "g");
68
- let pm: RegExpExecArray | null;
69
- while ((pm = localParam.exec(rest)) !== null) {
70
- const key = pm[1];
71
- const raw = pm[2];
72
- if (raw.startsWith('"') && raw.endsWith('"')) {
73
- parameters[key] = raw.slice(1, -1);
74
- } else if (/^-?\d+$/.test(raw)) {
75
- parameters[key] = parseInt(raw, 10);
76
- } else {
77
- parameters[key] = raw;
78
- }
79
- }
80
-
81
- return {
82
- label,
83
- covered_components: covered,
84
- parameters,
85
- raw: trimmed,
86
- };
87
- }
88
-
89
- export function parseSignatureValue(headerValue: string): {
90
- label: string;
91
- signature: Uint8Array;
92
- } {
93
- if (typeof headerValue !== "string") {
94
- throw new SignatureInputParseError(
95
- `header must be string, got ${typeof headerValue}`,
96
- );
97
- }
98
- const trimmed = headerValue.trim();
99
- if (trimmed.length === 0) {
100
- throw new SignatureInputParseError("empty Signature header value");
101
- }
102
-
103
- let label: string;
104
- let rest: string;
105
-
106
- const labelMatch = LABEL_RE.exec(trimmed);
107
- if (labelMatch) {
108
- label = labelMatch[1];
109
- rest = trimmed.slice(labelMatch[0].length).trim();
110
- } else if (trimmed.startsWith(":")) {
111
- label = "";
112
- rest = trimmed;
113
- } else {
114
- throw new SignatureInputParseError(
115
- `no label or signature-value prefix found at start: ${JSON.stringify(trimmed.slice(0, 40))}`,
116
- );
117
- }
118
-
119
- if (!rest.startsWith(":") || !rest.endsWith(":")) {
120
- throw new SignatureInputParseError(
121
- "signature value must be wrapped in colons (RFC 8941 byte-sequence form)",
122
- );
123
- }
124
-
125
- const sigB64 = rest.slice(1, -1);
126
- let sigBytes: Uint8Array;
127
- try {
128
- sigBytes = new Uint8Array(Buffer.from(sigB64, "base64"));
129
- } catch (e) {
130
- throw new SignatureInputParseError(
131
- `signature value is not valid base64: ${(e as Error).message}`,
132
- );
133
- }
134
- return { label, signature: sigBytes };
135
- }
1
+ /**
2
+ * RFC 9421 Signature-Input and Signature header parsers.
3
+ *
4
+ * Mirror of the Python algovoi_rfc9421_verifier.parse module.
5
+ */
6
+
7
+ export class SignatureInputParseError extends Error {
8
+ constructor(message: string) {
9
+ super(message);
10
+ this.name = "SignatureInputParseError";
11
+ }
12
+ }
13
+
14
+ export interface ParsedSignatureInput {
15
+ label: string;
16
+ covered_components: string[];
17
+ parameters: Record<string, string | number>;
18
+ raw: string;
19
+ /**
20
+ * The post-label portion of the Signature-Input header value, i.e.
21
+ * the Inner List + parameters block exactly as it appeared on the
22
+ * wire. This is the value that the @signature-params line in the
23
+ * RFC 9421 §2.5 signing base must carry. Empty string if the input
24
+ * was the unlabelled form.
25
+ */
26
+ params_block: string;
27
+ }
28
+
29
+ const LABEL_RE = /^\s*([A-Za-z][A-Za-z0-9_-]*)\s*=\s*/;
30
+ const COVERED_RE = /\(\s*(?:"[^"]*"\s*)*\)/;
31
+ const QUOTED_RE = /"([^"]*)"/g;
32
+ const PARAM_RE = /([A-Za-z][A-Za-z0-9_-]*)=([^;,\s]+|"[^"]*")/g;
33
+
34
+ export function parseSignatureInput(headerValue: string): ParsedSignatureInput {
35
+ if (typeof headerValue !== "string") {
36
+ throw new SignatureInputParseError(
37
+ `header must be string, got ${typeof headerValue}`,
38
+ );
39
+ }
40
+ const trimmed = headerValue.trim();
41
+ if (trimmed.length === 0) {
42
+ throw new SignatureInputParseError("empty header value");
43
+ }
44
+
45
+ let label: string;
46
+ let rest: string;
47
+
48
+ const labelMatch = LABEL_RE.exec(trimmed);
49
+ if (labelMatch) {
50
+ label = labelMatch[1];
51
+ rest = trimmed.slice(labelMatch[0].length);
52
+ } else if (trimmed.startsWith("(")) {
53
+ label = "";
54
+ rest = trimmed;
55
+ } else {
56
+ throw new SignatureInputParseError(
57
+ `no label or covered-components list found at start: ${JSON.stringify(trimmed.slice(0, 40))}`,
58
+ );
59
+ }
60
+
61
+ // Capture the post-label portion verbatim. This is what the
62
+ // @signature-params line of the RFC 9421 signing base must contain.
63
+ const paramsBlock = rest;
64
+
65
+ const coveredMatch = COVERED_RE.exec(rest);
66
+ if (!coveredMatch) {
67
+ throw new SignatureInputParseError("no covered-components list found");
68
+ }
69
+ const coveredRaw = coveredMatch[0];
70
+ const covered: string[] = [];
71
+ let qm: RegExpExecArray | null;
72
+ const localQuoted = /"([^"]*)"/g;
73
+ while ((qm = localQuoted.exec(coveredRaw)) !== null) {
74
+ covered.push(qm[1]);
75
+ }
76
+ rest = rest.slice(coveredMatch.index + coveredRaw.length);
77
+
78
+ const parameters: Record<string, string | number> = {};
79
+ const localParam = new RegExp(PARAM_RE.source, "g");
80
+ let pm: RegExpExecArray | null;
81
+ while ((pm = localParam.exec(rest)) !== null) {
82
+ const key = pm[1];
83
+ const raw = pm[2];
84
+ if (raw.startsWith('"') && raw.endsWith('"')) {
85
+ parameters[key] = raw.slice(1, -1);
86
+ } else if (/^-?\d+$/.test(raw)) {
87
+ parameters[key] = parseInt(raw, 10);
88
+ } else {
89
+ parameters[key] = raw;
90
+ }
91
+ }
92
+
93
+ return {
94
+ label,
95
+ covered_components: covered,
96
+ parameters,
97
+ raw: trimmed,
98
+ params_block: paramsBlock,
99
+ };
100
+ }
101
+
102
+ export function parseSignatureValue(headerValue: string): {
103
+ label: string;
104
+ signature: Uint8Array;
105
+ } {
106
+ if (typeof headerValue !== "string") {
107
+ throw new SignatureInputParseError(
108
+ `header must be string, got ${typeof headerValue}`,
109
+ );
110
+ }
111
+ const trimmed = headerValue.trim();
112
+ if (trimmed.length === 0) {
113
+ throw new SignatureInputParseError("empty Signature header value");
114
+ }
115
+
116
+ let label: string;
117
+ let rest: string;
118
+
119
+ const labelMatch = LABEL_RE.exec(trimmed);
120
+ if (labelMatch) {
121
+ label = labelMatch[1];
122
+ rest = trimmed.slice(labelMatch[0].length).trim();
123
+ } else if (trimmed.startsWith(":")) {
124
+ label = "";
125
+ rest = trimmed;
126
+ } else {
127
+ throw new SignatureInputParseError(
128
+ `no label or signature-value prefix found at start: ${JSON.stringify(trimmed.slice(0, 40))}`,
129
+ );
130
+ }
131
+
132
+ if (!rest.startsWith(":") || !rest.endsWith(":")) {
133
+ throw new SignatureInputParseError(
134
+ "signature value must be wrapped in colons (RFC 8941 byte-sequence form)",
135
+ );
136
+ }
137
+
138
+ const sigB64 = rest.slice(1, -1);
139
+ let sigBytes: Uint8Array;
140
+ try {
141
+ sigBytes = new Uint8Array(Buffer.from(sigB64, "base64"));
142
+ } catch (e) {
143
+ throw new SignatureInputParseError(
144
+ `signature value is not valid base64: ${(e as Error).message}`,
145
+ );
146
+ }
147
+ return { label, signature: sigBytes };
148
+ }