@algovoi/rfc9421-verifier 0.1.0 → 0.2.0

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/README.md CHANGED
@@ -137,6 +137,12 @@ originating gateway — exactly the property this verifier checks.
137
137
  | [`algovoi-audit-verifier`](https://pypi.org/project/algovoi-audit-verifier/) / [`@algovoi/audit-verifier`](https://www.npmjs.com/package/@algovoi/audit-verifier) | Selective-disclosure audit bundle verifier; consumes substrate output |
138
138
  | **`algovoi-rfc9421-verifier`** / `@algovoi/rfc9421-verifier` | **This package.** RFC 9421/9530 HTTP signature verifier |
139
139
 
140
+ ## Relationship to the canonicalisation discipline
141
+
142
+ This package verifies HTTP message signatures per RFC 9421 + RFC 9530 -- a different canonicalisation surface from the AlgoVoi JCS RFC 8785 receipt-body discipline at [docs.algovoi.co.uk/canonicalisation-substrate](https://docs.algovoi.co.uk/canonicalisation-substrate). HTTP signature verification (this package) and receipt-content verification (`@algovoi/audit-verifier` + the receipt-format packages) are complementary surfaces: this verifier confirms wire-level message integrity; the AlgoVoi JCS substrate confirms receipt-body canonical integrity. Both are AlgoVoi-authored under sole authorship.
143
+
144
+ Parties anchoring to the AlgoVoi canonicalisation discipline are recorded in the [Substrate Adopters Registry](https://docs.algovoi.co.uk/adopters); the registry's `canon_version` pin criterion applies to receipt-body artefacts, not to HTTP signatures as such.
145
+
140
146
  ## Licence
141
147
 
142
148
  Apache 2.0. See [`LICENSE`](./LICENSE).
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
+ // null = no algorithm requirement (accept any); undefined = default to sha-256
115
+ const requireAlg = input.requireAlgorithm === null
116
+ ? undefined
117
+ : (input.requireAlgorithm ?? "sha-256");
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,6 +1,6 @@
1
1
  {
2
2
  "name": "@algovoi/rfc9421-verifier",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AlgoVoi RFC 9421 HTTP Message Signatures + RFC 9530 Content-Digest reference verifier",
5
5
  "keywords": [
6
6
  "rfc9421",
package/src/parse.ts CHANGED
@@ -16,6 +16,14 @@ export interface ParsedSignatureInput {
16
16
  covered_components: string[];
17
17
  parameters: Record<string, string | number>;
18
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;
19
27
  }
20
28
 
21
29
  const LABEL_RE = /^\s*([A-Za-z][A-Za-z0-9_-]*)\s*=\s*/;
@@ -50,6 +58,10 @@ export function parseSignatureInput(headerValue: string): ParsedSignatureInput {
50
58
  );
51
59
  }
52
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
+
53
65
  const coveredMatch = COVERED_RE.exec(rest);
54
66
  if (!coveredMatch) {
55
67
  throw new SignatureInputParseError("no covered-components list found");
@@ -83,6 +95,7 @@ export function parseSignatureInput(headerValue: string): ParsedSignatureInput {
83
95
  covered_components: covered,
84
96
  parameters,
85
97
  raw: trimmed,
98
+ params_block: paramsBlock,
86
99
  };
87
100
  }
88
101
 
@@ -11,6 +11,8 @@ export class SigningBaseError extends Error {
11
11
  }
12
12
  }
13
13
 
14
+ export type SigningBaseMode = "algovoi-v0" | "rfc9421";
15
+
14
16
  export interface SigningBaseInput {
15
17
  coveredComponents: string[];
16
18
  method?: string;
@@ -21,9 +23,42 @@ export interface SigningBaseInput {
21
23
  status?: number;
22
24
  headers?: Record<string, string>;
23
25
  parameters?: Record<string, string | number>;
26
+ /**
27
+ * Signing-base mode.
28
+ * - "algovoi-v0" (default): preserves the v0.1.0 behaviour for
29
+ * backward compatibility with the AlgoVoi internal fixture and
30
+ * the rfc9421_proxy_chain_v0 conformance set. @method is
31
+ * lowercased and no @signature-params line is appended.
32
+ * - "rfc9421": full RFC 9421 §2.5 compliance. @method is preserved
33
+ * as-supplied (HTTP convention is uppercase), and a final
34
+ * "@signature-params" line is appended carrying
35
+ * signatureParamsRaw verbatim. This is the shape required to
36
+ * verify external fixtures (Envoys envoys-rfc9421, Hippo
37
+ * hippo-rfc9421, RFC 9421 §B test vectors, and any other
38
+ * RFC-compliant implementation).
39
+ */
40
+ mode?: SigningBaseMode;
41
+ /**
42
+ * The post-label portion of the Signature-Input header value, i.e.
43
+ * the Inner List + parameters block exactly as it appeared on the
44
+ * wire. Required when mode is "rfc9421".
45
+ */
46
+ signatureParamsRaw?: string;
24
47
  }
25
48
 
26
49
  export function buildSigningBase(input: SigningBaseInput): string {
50
+ const mode: SigningBaseMode = input.mode ?? "algovoi-v0";
51
+ if (mode !== "algovoi-v0" && mode !== "rfc9421") {
52
+ throw new SigningBaseError(
53
+ `mode must be "algovoi-v0" or "rfc9421", got ${JSON.stringify(mode)}`,
54
+ );
55
+ }
56
+ if (mode === "rfc9421" && input.signatureParamsRaw === undefined) {
57
+ throw new SigningBaseError(
58
+ "rfc9421 mode requires signatureParamsRaw (the post-label portion of the Signature-Input header)",
59
+ );
60
+ }
61
+
27
62
  const normHeaders: Record<string, string> = {};
28
63
  for (const [k, v] of Object.entries(input.headers ?? {})) {
29
64
  normHeaders[k.toLowerCase()] = v;
@@ -39,7 +74,7 @@ export function buildSigningBase(input: SigningBaseInput): string {
39
74
  case "@method":
40
75
  if (input.method === undefined)
41
76
  throw new SigningBaseError("@method covered but method not supplied");
42
- value = input.method.toLowerCase();
77
+ value = mode === "rfc9421" ? input.method : input.method.toLowerCase();
43
78
  break;
44
79
  case "@authority":
45
80
  if (input.authority === undefined)
@@ -96,5 +131,9 @@ export function buildSigningBase(input: SigningBaseInput): string {
96
131
  lines.push(`"${c}": ${value}`);
97
132
  }
98
133
 
134
+ if (mode === "rfc9421") {
135
+ lines.push(`"@signature-params": ${input.signatureParamsRaw}`);
136
+ }
137
+
99
138
  return lines.join("\n");
100
139
  }
package/src/verify.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  import {
16
16
  buildSigningBase,
17
17
  SigningBaseError,
18
+ type SigningBaseMode,
18
19
  } from "./signing-base.js";
19
20
  import {
20
21
  verifyContentDigest,
@@ -50,7 +51,15 @@ export interface VerifyRequestInput {
50
51
  publicKey: PublicKey;
51
52
  scheme?: string;
52
53
  requireContentDigest?: boolean;
53
- requireAlgorithm?: string;
54
+ requireAlgorithm?: string | null;
55
+ /**
56
+ * Signing-base mode. Default "algovoi-v0" preserves backward
57
+ * compatibility with the v0.1.0 internal fixture and the
58
+ * rfc9421_proxy_chain_v0 conformance set. Set to "rfc9421" to
59
+ * verify external RFC 9421-compliant fixtures (Envoys, Hippo,
60
+ * RFC 9421 §B test vectors).
61
+ */
62
+ mode?: SigningBaseMode;
54
63
  }
55
64
 
56
65
  function newResult(): VerifyResult {
@@ -173,7 +182,11 @@ export async function verifyRequest(
173
182
  }
174
183
 
175
184
  const requireCd = input.requireContentDigest ?? true;
176
- const requireAlg = input.requireAlgorithm ?? "sha-256";
185
+ // null = no algorithm requirement (accept any); undefined = default to sha-256
186
+ const requireAlg: string | undefined =
187
+ input.requireAlgorithm === null
188
+ ? undefined
189
+ : (input.requireAlgorithm ?? "sha-256");
177
190
 
178
191
  if (requireCd) {
179
192
  const cdHeader = normHeaders["content-digest"];
@@ -192,6 +205,8 @@ export async function verifyRequest(
192
205
  result.content_digest_valid = true;
193
206
  }
194
207
 
208
+ const mode: SigningBaseMode = input.mode ?? "algovoi-v0";
209
+
195
210
  let signingBase: string;
196
211
  try {
197
212
  signingBase = buildSigningBase({
@@ -202,6 +217,9 @@ export async function verifyRequest(
202
217
  scheme: input.scheme ?? "https",
203
218
  headers: normHeaders,
204
219
  parameters: parsedSi.parameters,
220
+ mode,
221
+ signatureParamsRaw:
222
+ mode === "rfc9421" ? parsedSi.params_block : undefined,
205
223
  });
206
224
  } catch (e) {
207
225
  if (e instanceof SigningBaseError) {