@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 +6 -0
- package/dist/parse.d.ts +8 -0
- package/dist/parse.js +4 -0
- package/dist/signing-base.d.ts +22 -0
- package/dist/signing-base.js +11 -1
- package/dist/verify.d.ts +10 -1
- package/dist/verify.js +7 -1
- package/package.json +1 -1
- package/src/parse.ts +13 -0
- package/src/signing-base.ts +40 -1
- package/src/verify.ts +20 -2
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) {
|
package/dist/signing-base.d.ts
CHANGED
|
@@ -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;
|
package/dist/signing-base.js
CHANGED
|
@@ -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
|
-
|
|
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
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
|
|
package/src/signing-base.ts
CHANGED
|
@@ -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
|
-
|
|
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) {
|