@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 +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 +62 -62
- package/src/parse.ts +148 -135
- package/src/signing-base.ts +139 -100
- package/src/verify.ts +256 -238
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
|
+
// 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.
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
+
}
|