@algovoi/rfc9421-verifier 0.1.1 → 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/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/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) {
|