@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/src/verify.ts CHANGED
@@ -1,238 +1,256 @@
1
- /**
2
- * RFC 9421 + RFC 9530 verification top-level.
3
- *
4
- * Mirror of the Python algovoi_rfc9421_verifier.verify module.
5
- * Uses @noble/ed25519 for Ed25519 verification.
6
- */
7
-
8
- import * as ed25519 from "@noble/ed25519";
9
-
10
- import {
11
- parseSignatureInput,
12
- parseSignatureValue,
13
- SignatureInputParseError,
14
- } from "./parse.js";
15
- import {
16
- buildSigningBase,
17
- SigningBaseError,
18
- } from "./signing-base.js";
19
- import {
20
- verifyContentDigest,
21
- ContentDigestError,
22
- } from "./content-digest.js";
23
-
24
- export class VerifyError extends Error {
25
- constructor(message: string) {
26
- super(message);
27
- this.name = "VerifyError";
28
- }
29
- }
30
-
31
- export type PublicKey = string | Uint8Array;
32
-
33
- export interface VerifyResult {
34
- valid: boolean;
35
- signature_valid: boolean;
36
- content_digest_valid: boolean;
37
- signing_base: string;
38
- covered_components: string[];
39
- parameters: Record<string, string | number>;
40
- label: string;
41
- errors: string[];
42
- }
43
-
44
- export interface VerifyRequestInput {
45
- method: string;
46
- authority: string;
47
- path: string;
48
- headers: Record<string, string>;
49
- body: Uint8Array | Buffer | string;
50
- publicKey: PublicKey;
51
- scheme?: string;
52
- requireContentDigest?: boolean;
53
- requireAlgorithm?: string;
54
- }
55
-
56
- function newResult(): VerifyResult {
57
- return {
58
- valid: false,
59
- signature_valid: false,
60
- content_digest_valid: false,
61
- signing_base: "",
62
- covered_components: [],
63
- parameters: {},
64
- label: "",
65
- errors: [],
66
- };
67
- }
68
-
69
- function fail(result: VerifyResult, msg: string): VerifyResult {
70
- result.errors.push(msg);
71
- result.valid = false;
72
- return result;
73
- }
74
-
75
- function publicKeyBytes(pk: PublicKey): Uint8Array {
76
- if (pk instanceof Uint8Array) {
77
- if (pk.length !== 32) {
78
- throw new VerifyError(
79
- `Ed25519 public key must be 32 bytes, got ${pk.length}`,
80
- );
81
- }
82
- return pk;
83
- }
84
- if (typeof pk === "string") {
85
- const hex = pk.startsWith("0x") ? pk.slice(2) : pk;
86
- if (!/^[0-9a-fA-F]+$/.test(hex)) {
87
- throw new VerifyError("public key hex is invalid");
88
- }
89
- if (hex.length !== 64) {
90
- throw new VerifyError(
91
- `Ed25519 public key hex must decode to 32 bytes (64 hex chars), got ${hex.length}`,
92
- );
93
- }
94
- const bytes = new Uint8Array(32);
95
- for (let i = 0; i < 32; i++) {
96
- bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
97
- }
98
- return bytes;
99
- }
100
- throw new VerifyError("publicKey must be Uint8Array or hex string");
101
- }
102
-
103
- export async function verifySignature(
104
- signingBase: string,
105
- signatureBytes: Uint8Array,
106
- publicKey: PublicKey,
107
- algorithm: string = "ed25519",
108
- ): Promise<boolean> {
109
- if (algorithm.toLowerCase() !== "ed25519") {
110
- throw new VerifyError(`v0.1.0 supports ed25519 only; got ${algorithm}`);
111
- }
112
- if (signatureBytes.length !== 64) {
113
- throw new VerifyError(
114
- `Ed25519 signature must be 64 bytes, got ${signatureBytes.length}`,
115
- );
116
- }
117
- const pkBytes = publicKeyBytes(publicKey);
118
- const messageBytes = new TextEncoder().encode(signingBase);
119
- try {
120
- return await ed25519.verifyAsync(signatureBytes, messageBytes, pkBytes);
121
- } catch {
122
- return false;
123
- }
124
- }
125
-
126
- export async function verifyRequest(
127
- input: VerifyRequestInput,
128
- ): Promise<VerifyResult> {
129
- const result = newResult();
130
-
131
- const normHeaders: Record<string, string> = {};
132
- for (const [k, v] of Object.entries(input.headers)) {
133
- normHeaders[k.toLowerCase()] = v;
134
- }
135
-
136
- const siValue = normHeaders["signature-input"];
137
- if (!siValue) return fail(result, "Signature-Input header missing");
138
- const sValue = normHeaders["signature"];
139
- if (!sValue) return fail(result, "Signature header missing");
140
-
141
- let parsedSi;
142
- try {
143
- parsedSi = parseSignatureInput(siValue);
144
- } catch (e) {
145
- if (e instanceof SignatureInputParseError) {
146
- return fail(result, `Signature-Input parse error: ${e.message}`);
147
- }
148
- throw e;
149
- }
150
- result.label = parsedSi.label;
151
- result.covered_components = parsedSi.covered_components;
152
- result.parameters = parsedSi.parameters;
153
-
154
- let sigParsed;
155
- try {
156
- sigParsed = parseSignatureValue(sValue);
157
- } catch (e) {
158
- if (e instanceof SignatureInputParseError) {
159
- return fail(result, `Signature parse error: ${e.message}`);
160
- }
161
- throw e;
162
- }
163
-
164
- if (
165
- sigParsed.label &&
166
- parsedSi.label &&
167
- sigParsed.label !== parsedSi.label
168
- ) {
169
- return fail(
170
- result,
171
- `Signature label ${sigParsed.label} does not match Signature-Input label ${parsedSi.label}`,
172
- );
173
- }
174
-
175
- const requireCd = input.requireContentDigest ?? true;
176
- const requireAlg = input.requireAlgorithm ?? "sha-256";
177
-
178
- if (requireCd) {
179
- const cdHeader = normHeaders["content-digest"];
180
- if (!cdHeader)
181
- return fail(result, "Content-Digest header required but missing");
182
- try {
183
- verifyContentDigest(input.body, cdHeader, requireAlg);
184
- result.content_digest_valid = true;
185
- } catch (e) {
186
- if (e instanceof ContentDigestError) {
187
- return fail(result, `Content-Digest verification failed: ${e.message}`);
188
- }
189
- throw e;
190
- }
191
- } else {
192
- result.content_digest_valid = true;
193
- }
194
-
195
- let signingBase: string;
196
- try {
197
- signingBase = buildSigningBase({
198
- coveredComponents: parsedSi.covered_components,
199
- method: input.method,
200
- authority: input.authority,
201
- path: input.path,
202
- scheme: input.scheme ?? "https",
203
- headers: normHeaders,
204
- parameters: parsedSi.parameters,
205
- });
206
- } catch (e) {
207
- if (e instanceof SigningBaseError) {
208
- return fail(result, `Signing-base build error: ${e.message}`);
209
- }
210
- throw e;
211
- }
212
- result.signing_base = signingBase;
213
-
214
- const alg = parsedSi.parameters["alg"];
215
- const algStr = typeof alg === "string" ? alg : "ed25519";
216
-
217
- let sigOk: boolean;
218
- try {
219
- sigOk = await verifySignature(
220
- signingBase,
221
- sigParsed.signature,
222
- input.publicKey,
223
- algStr,
224
- );
225
- } catch (e) {
226
- if (e instanceof VerifyError) {
227
- return fail(result, `Signature verification setup error: ${e.message}`);
228
- }
229
- throw e;
230
- }
231
-
232
- if (!sigOk) {
233
- return fail(result, "Ed25519 signature does not verify against signing base");
234
- }
235
- result.signature_valid = true;
236
- result.valid = result.signature_valid && result.content_digest_valid;
237
- return result;
238
- }
1
+ /**
2
+ * RFC 9421 + RFC 9530 verification top-level.
3
+ *
4
+ * Mirror of the Python algovoi_rfc9421_verifier.verify module.
5
+ * Uses @noble/ed25519 for Ed25519 verification.
6
+ */
7
+
8
+ import * as ed25519 from "@noble/ed25519";
9
+
10
+ import {
11
+ parseSignatureInput,
12
+ parseSignatureValue,
13
+ SignatureInputParseError,
14
+ } from "./parse.js";
15
+ import {
16
+ buildSigningBase,
17
+ SigningBaseError,
18
+ type SigningBaseMode,
19
+ } from "./signing-base.js";
20
+ import {
21
+ verifyContentDigest,
22
+ ContentDigestError,
23
+ } from "./content-digest.js";
24
+
25
+ export class VerifyError extends Error {
26
+ constructor(message: string) {
27
+ super(message);
28
+ this.name = "VerifyError";
29
+ }
30
+ }
31
+
32
+ export type PublicKey = string | Uint8Array;
33
+
34
+ export interface VerifyResult {
35
+ valid: boolean;
36
+ signature_valid: boolean;
37
+ content_digest_valid: boolean;
38
+ signing_base: string;
39
+ covered_components: string[];
40
+ parameters: Record<string, string | number>;
41
+ label: string;
42
+ errors: string[];
43
+ }
44
+
45
+ export interface VerifyRequestInput {
46
+ method: string;
47
+ authority: string;
48
+ path: string;
49
+ headers: Record<string, string>;
50
+ body: Uint8Array | Buffer | string;
51
+ publicKey: PublicKey;
52
+ scheme?: string;
53
+ requireContentDigest?: boolean;
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;
63
+ }
64
+
65
+ function newResult(): VerifyResult {
66
+ return {
67
+ valid: false,
68
+ signature_valid: false,
69
+ content_digest_valid: false,
70
+ signing_base: "",
71
+ covered_components: [],
72
+ parameters: {},
73
+ label: "",
74
+ errors: [],
75
+ };
76
+ }
77
+
78
+ function fail(result: VerifyResult, msg: string): VerifyResult {
79
+ result.errors.push(msg);
80
+ result.valid = false;
81
+ return result;
82
+ }
83
+
84
+ function publicKeyBytes(pk: PublicKey): Uint8Array {
85
+ if (pk instanceof Uint8Array) {
86
+ if (pk.length !== 32) {
87
+ throw new VerifyError(
88
+ `Ed25519 public key must be 32 bytes, got ${pk.length}`,
89
+ );
90
+ }
91
+ return pk;
92
+ }
93
+ if (typeof pk === "string") {
94
+ const hex = pk.startsWith("0x") ? pk.slice(2) : pk;
95
+ if (!/^[0-9a-fA-F]+$/.test(hex)) {
96
+ throw new VerifyError("public key hex is invalid");
97
+ }
98
+ if (hex.length !== 64) {
99
+ throw new VerifyError(
100
+ `Ed25519 public key hex must decode to 32 bytes (64 hex chars), got ${hex.length}`,
101
+ );
102
+ }
103
+ const bytes = new Uint8Array(32);
104
+ for (let i = 0; i < 32; i++) {
105
+ bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
106
+ }
107
+ return bytes;
108
+ }
109
+ throw new VerifyError("publicKey must be Uint8Array or hex string");
110
+ }
111
+
112
+ export async function verifySignature(
113
+ signingBase: string,
114
+ signatureBytes: Uint8Array,
115
+ publicKey: PublicKey,
116
+ algorithm: string = "ed25519",
117
+ ): Promise<boolean> {
118
+ if (algorithm.toLowerCase() !== "ed25519") {
119
+ throw new VerifyError(`v0.1.0 supports ed25519 only; got ${algorithm}`);
120
+ }
121
+ if (signatureBytes.length !== 64) {
122
+ throw new VerifyError(
123
+ `Ed25519 signature must be 64 bytes, got ${signatureBytes.length}`,
124
+ );
125
+ }
126
+ const pkBytes = publicKeyBytes(publicKey);
127
+ const messageBytes = new TextEncoder().encode(signingBase);
128
+ try {
129
+ return await ed25519.verifyAsync(signatureBytes, messageBytes, pkBytes);
130
+ } catch {
131
+ return false;
132
+ }
133
+ }
134
+
135
+ export async function verifyRequest(
136
+ input: VerifyRequestInput,
137
+ ): Promise<VerifyResult> {
138
+ const result = newResult();
139
+
140
+ const normHeaders: Record<string, string> = {};
141
+ for (const [k, v] of Object.entries(input.headers)) {
142
+ normHeaders[k.toLowerCase()] = v;
143
+ }
144
+
145
+ const siValue = normHeaders["signature-input"];
146
+ if (!siValue) return fail(result, "Signature-Input header missing");
147
+ const sValue = normHeaders["signature"];
148
+ if (!sValue) return fail(result, "Signature header missing");
149
+
150
+ let parsedSi;
151
+ try {
152
+ parsedSi = parseSignatureInput(siValue);
153
+ } catch (e) {
154
+ if (e instanceof SignatureInputParseError) {
155
+ return fail(result, `Signature-Input parse error: ${e.message}`);
156
+ }
157
+ throw e;
158
+ }
159
+ result.label = parsedSi.label;
160
+ result.covered_components = parsedSi.covered_components;
161
+ result.parameters = parsedSi.parameters;
162
+
163
+ let sigParsed;
164
+ try {
165
+ sigParsed = parseSignatureValue(sValue);
166
+ } catch (e) {
167
+ if (e instanceof SignatureInputParseError) {
168
+ return fail(result, `Signature parse error: ${e.message}`);
169
+ }
170
+ throw e;
171
+ }
172
+
173
+ if (
174
+ sigParsed.label &&
175
+ parsedSi.label &&
176
+ sigParsed.label !== parsedSi.label
177
+ ) {
178
+ return fail(
179
+ result,
180
+ `Signature label ${sigParsed.label} does not match Signature-Input label ${parsedSi.label}`,
181
+ );
182
+ }
183
+
184
+ const requireCd = input.requireContentDigest ?? true;
185
+ // string = enforce that specific algorithm; null or undefined = accept any
186
+ // supported algorithm present in the Content-Digest header (SHA-256 or
187
+ // SHA-512 in this v0.2.x). Default is permissive.
188
+ const requireAlg: string | undefined =
189
+ input.requireAlgorithm == null ? undefined : input.requireAlgorithm;
190
+
191
+ if (requireCd) {
192
+ const cdHeader = normHeaders["content-digest"];
193
+ if (!cdHeader)
194
+ return fail(result, "Content-Digest header required but missing");
195
+ try {
196
+ verifyContentDigest(input.body, cdHeader, requireAlg);
197
+ result.content_digest_valid = true;
198
+ } catch (e) {
199
+ if (e instanceof ContentDigestError) {
200
+ return fail(result, `Content-Digest verification failed: ${e.message}`);
201
+ }
202
+ throw e;
203
+ }
204
+ } else {
205
+ result.content_digest_valid = true;
206
+ }
207
+
208
+ const mode: SigningBaseMode = input.mode ?? "algovoi-v0";
209
+
210
+ let signingBase: string;
211
+ try {
212
+ signingBase = buildSigningBase({
213
+ coveredComponents: parsedSi.covered_components,
214
+ method: input.method,
215
+ authority: input.authority,
216
+ path: input.path,
217
+ scheme: input.scheme ?? "https",
218
+ headers: normHeaders,
219
+ parameters: parsedSi.parameters,
220
+ mode,
221
+ signatureParamsRaw:
222
+ mode === "rfc9421" ? parsedSi.params_block : undefined,
223
+ });
224
+ } catch (e) {
225
+ if (e instanceof SigningBaseError) {
226
+ return fail(result, `Signing-base build error: ${e.message}`);
227
+ }
228
+ throw e;
229
+ }
230
+ result.signing_base = signingBase;
231
+
232
+ const alg = parsedSi.parameters["alg"];
233
+ const algStr = typeof alg === "string" ? alg : "ed25519";
234
+
235
+ let sigOk: boolean;
236
+ try {
237
+ sigOk = await verifySignature(
238
+ signingBase,
239
+ sigParsed.signature,
240
+ input.publicKey,
241
+ algStr,
242
+ );
243
+ } catch (e) {
244
+ if (e instanceof VerifyError) {
245
+ return fail(result, `Signature verification setup error: ${e.message}`);
246
+ }
247
+ throw e;
248
+ }
249
+
250
+ if (!sigOk) {
251
+ return fail(result, "Ed25519 signature does not verify against signing base");
252
+ }
253
+ result.signature_valid = true;
254
+ result.valid = result.signature_valid && result.content_digest_valid;
255
+ return result;
256
+ }