@mcp-audit-gateway/core 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/.well-known/agent-governance.json +47 -0
- package/.well-known/security-insights-snippet.yml +9 -0
- package/CHANGELOG.md +32 -0
- package/dist/attestation/audit-log.d.ts +1 -0
- package/dist/attestation/audit-log.d.ts.map +1 -1
- package/dist/attestation/audit-log.js +19 -0
- package/dist/attestation/audit-log.js.map +1 -1
- package/dist/attestation/signer.d.ts.map +1 -1
- package/dist/attestation/signer.js +7 -0
- package/dist/attestation/signer.js.map +1 -1
- package/dist/attestation/signer.test.js +72 -0
- package/dist/attestation/signer.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/policy/engine.d.ts +12 -1
- package/dist/policy/engine.d.ts.map +1 -1
- package/dist/policy/engine.js +26 -4
- package/dist/policy/engine.js.map +1 -1
- package/dist/policy/engine.test.js +42 -1
- package/dist/policy/engine.test.js.map +1 -1
- package/dist/proxy/gateway.d.ts.map +1 -1
- package/dist/proxy/gateway.js +5 -1
- package/dist/proxy/gateway.js.map +1 -1
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/attestation/audit-log.ts +24 -1
- package/src/attestation/signer.test.ts +84 -0
- package/src/attestation/signer.ts +8 -1
- package/src/index.ts +1 -1
- package/src/policy/engine.test.ts +47 -1
- package/src/policy/engine.ts +38 -5
- package/src/proxy/gateway.ts +6 -1
- package/src/types.ts +8 -0
- package/test/vectors/README.md +44 -0
- package/test/vectors/canonicalization.json +582 -0
- package/test/vectors/generate.mjs +354 -0
- package/test/vectors/verify.mjs +273 -0
- package/test/vectors/verify.py +277 -0
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
|
|
8
|
+
// Exact replica of canonicalizeRecord from src/attestation/signer.ts
|
|
9
|
+
function canonicalizeRecord(record) {
|
|
10
|
+
const ordered = [
|
|
11
|
+
["id", record.id],
|
|
12
|
+
["timestamp", record.timestamp],
|
|
13
|
+
["method", record.method],
|
|
14
|
+
["toolName", record.toolName ?? null],
|
|
15
|
+
["namespace", record.namespace ?? null],
|
|
16
|
+
["upstream", record.upstream ?? null],
|
|
17
|
+
["principal", record.principal ?? null],
|
|
18
|
+
["durationMs", record.durationMs],
|
|
19
|
+
["success", record.success],
|
|
20
|
+
["errorCode", record.errorCode ?? null],
|
|
21
|
+
["previousHash", record.previousHash ?? null],
|
|
22
|
+
];
|
|
23
|
+
return JSON.stringify(ordered);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Exact replica of hashRecord from src/attestation/audit-log.ts
|
|
27
|
+
function hashRecord(record) {
|
|
28
|
+
const json = JSON.stringify(record);
|
|
29
|
+
return { hash: sha256Hex(json), json };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function sha256Hex(input) {
|
|
33
|
+
return createHash("sha256").update(input).digest("hex");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- Canonicalization test vectors ---
|
|
37
|
+
|
|
38
|
+
const vectorDefs = [
|
|
39
|
+
{
|
|
40
|
+
name: "genesis_all_fields",
|
|
41
|
+
record: {
|
|
42
|
+
id: "550e8400-e29b-41d4-a716-446655440001",
|
|
43
|
+
timestamp: "2026-08-21T12:00:00.000Z",
|
|
44
|
+
method: "tools/call",
|
|
45
|
+
toolName: "weather_lookup",
|
|
46
|
+
namespace: "external-apis",
|
|
47
|
+
upstream: "weather-server",
|
|
48
|
+
principal: "user:alice@example.com",
|
|
49
|
+
durationMs: 142,
|
|
50
|
+
success: true,
|
|
51
|
+
previousHash: "genesis",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: "genesis_null_optionals",
|
|
56
|
+
record: {
|
|
57
|
+
id: "550e8400-e29b-41d4-a716-446655440002",
|
|
58
|
+
timestamp: "2026-08-21T12:00:01.000Z",
|
|
59
|
+
method: "tools/list",
|
|
60
|
+
durationMs: 3,
|
|
61
|
+
success: true,
|
|
62
|
+
previousHash: "genesis",
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "error_with_code",
|
|
67
|
+
record: {
|
|
68
|
+
id: "550e8400-e29b-41d4-a716-446655440003",
|
|
69
|
+
timestamp: "2026-08-21T12:00:02.500Z",
|
|
70
|
+
method: "tools/call",
|
|
71
|
+
toolName: "database_query",
|
|
72
|
+
namespace: "internal",
|
|
73
|
+
upstream: "db-server",
|
|
74
|
+
principal: "service:backend-agent",
|
|
75
|
+
durationMs: 5021,
|
|
76
|
+
success: false,
|
|
77
|
+
errorCode: -32603,
|
|
78
|
+
previousHash: "genesis",
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "zero_duration",
|
|
83
|
+
record: {
|
|
84
|
+
id: "550e8400-e29b-41d4-a716-446655440004",
|
|
85
|
+
timestamp: "2026-08-21T12:00:03.000Z",
|
|
86
|
+
method: "tools/call",
|
|
87
|
+
toolName: "cache_get",
|
|
88
|
+
namespace: "infra",
|
|
89
|
+
upstream: "cache-server",
|
|
90
|
+
durationMs: 0,
|
|
91
|
+
success: true,
|
|
92
|
+
previousHash: "genesis",
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
name: "invalid_params_error",
|
|
97
|
+
record: {
|
|
98
|
+
id: "550e8400-e29b-41d4-a716-446655440005",
|
|
99
|
+
timestamp: "2026-08-21T12:00:04.000Z",
|
|
100
|
+
method: "tools/call",
|
|
101
|
+
toolName: "send_email",
|
|
102
|
+
namespace: "comms",
|
|
103
|
+
upstream: "email-server",
|
|
104
|
+
principal: "user:bob@example.com",
|
|
105
|
+
durationMs: 12,
|
|
106
|
+
success: false,
|
|
107
|
+
errorCode: -32602,
|
|
108
|
+
previousHash: "genesis",
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "unicode_in_fields",
|
|
113
|
+
record: {
|
|
114
|
+
id: "550e8400-e29b-41d4-a716-446655440006",
|
|
115
|
+
timestamp: "2026-08-21T12:00:05.000Z",
|
|
116
|
+
method: "tools/call",
|
|
117
|
+
toolName: "天気検索",
|
|
118
|
+
namespace: "external-apis",
|
|
119
|
+
upstream: "weather-server-⚡",
|
|
120
|
+
principal: "user:tanaka.太郎@example.jp",
|
|
121
|
+
durationMs: 88,
|
|
122
|
+
success: true,
|
|
123
|
+
previousHash: "genesis",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
name: "max_safe_integer_duration",
|
|
128
|
+
record: {
|
|
129
|
+
id: "550e8400-e29b-41d4-a716-446655440007",
|
|
130
|
+
timestamp: "2026-08-21T12:00:06.000Z",
|
|
131
|
+
method: "tools/call",
|
|
132
|
+
toolName: "long_running_task",
|
|
133
|
+
namespace: "compute",
|
|
134
|
+
upstream: "batch-server",
|
|
135
|
+
principal: "service:scheduler",
|
|
136
|
+
durationMs: 9007199254740991,
|
|
137
|
+
success: true,
|
|
138
|
+
previousHash: "genesis",
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
name: "empty_string_tool_name",
|
|
143
|
+
record: {
|
|
144
|
+
id: "550e8400-e29b-41d4-a716-446655440008",
|
|
145
|
+
timestamp: "2026-08-21T12:00:07.000Z",
|
|
146
|
+
method: "tools/call",
|
|
147
|
+
toolName: "",
|
|
148
|
+
namespace: "",
|
|
149
|
+
upstream: "legacy-server",
|
|
150
|
+
principal: "user:admin",
|
|
151
|
+
durationMs: 1,
|
|
152
|
+
success: true,
|
|
153
|
+
previousHash: "genesis",
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
|
|
158
|
+
const canonicalizationVectors = vectorDefs.map(({ name, record }) => {
|
|
159
|
+
const canonical = canonicalizeRecord(record);
|
|
160
|
+
return {
|
|
161
|
+
name,
|
|
162
|
+
record,
|
|
163
|
+
canonical,
|
|
164
|
+
sha256_canonical: sha256Hex(canonical),
|
|
165
|
+
};
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// --- Chain vectors: 3 linked records ---
|
|
169
|
+
|
|
170
|
+
const chain1 = {
|
|
171
|
+
id: "660e8400-e29b-41d4-a716-446655440001",
|
|
172
|
+
timestamp: "2026-08-21T13:00:00.000Z",
|
|
173
|
+
method: "tools/call",
|
|
174
|
+
toolName: "search",
|
|
175
|
+
namespace: "research",
|
|
176
|
+
upstream: "search-server",
|
|
177
|
+
principal: "user:carol@example.com",
|
|
178
|
+
durationMs: 230,
|
|
179
|
+
success: true,
|
|
180
|
+
previousHash: "genesis",
|
|
181
|
+
attestation: "abc123deadbeef",
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const { hash: chain1_hash, json: chain1_json } = hashRecord(chain1);
|
|
185
|
+
|
|
186
|
+
const chain2 = {
|
|
187
|
+
id: "660e8400-e29b-41d4-a716-446655440002",
|
|
188
|
+
timestamp: "2026-08-21T13:00:01.500Z",
|
|
189
|
+
method: "tools/call",
|
|
190
|
+
toolName: "summarize",
|
|
191
|
+
namespace: "research",
|
|
192
|
+
upstream: "llm-server",
|
|
193
|
+
principal: "user:carol@example.com",
|
|
194
|
+
durationMs: 1840,
|
|
195
|
+
success: true,
|
|
196
|
+
previousHash: chain1_hash,
|
|
197
|
+
attestation: "def456cafebabe",
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const { hash: chain2_hash, json: chain2_json } = hashRecord(chain2);
|
|
201
|
+
|
|
202
|
+
const chain3 = {
|
|
203
|
+
id: "660e8400-e29b-41d4-a716-446655440003",
|
|
204
|
+
timestamp: "2026-08-21T13:00:05.000Z",
|
|
205
|
+
method: "tools/call",
|
|
206
|
+
toolName: "store_result",
|
|
207
|
+
namespace: "research",
|
|
208
|
+
upstream: "storage-server",
|
|
209
|
+
principal: "user:carol@example.com",
|
|
210
|
+
durationMs: 45,
|
|
211
|
+
success: true,
|
|
212
|
+
previousHash: chain2_hash,
|
|
213
|
+
attestation: "789abcfeedface",
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const { hash: chain3_hash, json: chain3_json } = hashRecord(chain3);
|
|
217
|
+
|
|
218
|
+
const chainVectors = {
|
|
219
|
+
description:
|
|
220
|
+
"Three linked records demonstrating hash chain verification. The chain hash covers the FULL record including the attestation field. This means the attestation (signature) is bound into the chain sequence.",
|
|
221
|
+
chain_algorithm: "sha256(JSON.stringify(fullRecord))",
|
|
222
|
+
chain_key_order: [
|
|
223
|
+
"id",
|
|
224
|
+
"timestamp",
|
|
225
|
+
"method",
|
|
226
|
+
"toolName",
|
|
227
|
+
"namespace",
|
|
228
|
+
"upstream",
|
|
229
|
+
"principal",
|
|
230
|
+
"durationMs",
|
|
231
|
+
"success",
|
|
232
|
+
"previousHash",
|
|
233
|
+
"attestation",
|
|
234
|
+
],
|
|
235
|
+
chain_key_order_note:
|
|
236
|
+
"The chain hash uses JSON.stringify which preserves JavaScript insertion order. Verifiers in languages without ordered maps MUST serialize keys in this exact order to reproduce chain hashes. The canonical form (tuple-array) does NOT have this limitation.",
|
|
237
|
+
genesis_seed: "genesis",
|
|
238
|
+
records: [
|
|
239
|
+
{
|
|
240
|
+
record: chain1,
|
|
241
|
+
full_record_json: chain1_json,
|
|
242
|
+
record_hash: chain1_hash,
|
|
243
|
+
canonical: canonicalizeRecord(chain1),
|
|
244
|
+
sha256_canonical: sha256Hex(canonicalizeRecord(chain1)),
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
record: chain2,
|
|
248
|
+
full_record_json: chain2_json,
|
|
249
|
+
record_hash: chain2_hash,
|
|
250
|
+
canonical: canonicalizeRecord(chain2),
|
|
251
|
+
sha256_canonical: sha256Hex(canonicalizeRecord(chain2)),
|
|
252
|
+
previous_record_hash: chain1_hash,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
record: chain3,
|
|
256
|
+
full_record_json: chain3_json,
|
|
257
|
+
record_hash: chain3_hash,
|
|
258
|
+
canonical: canonicalizeRecord(chain3),
|
|
259
|
+
sha256_canonical: sha256Hex(canonicalizeRecord(chain3)),
|
|
260
|
+
previous_record_hash: chain2_hash,
|
|
261
|
+
},
|
|
262
|
+
],
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
// --- Dual-hash demonstration vector ---
|
|
266
|
+
// Same logical fields, different attestation → same canonical hash, different chain hash
|
|
267
|
+
|
|
268
|
+
const dualBase = {
|
|
269
|
+
id: "770e8400-e29b-41d4-a716-446655440001",
|
|
270
|
+
timestamp: "2026-08-21T14:00:00.000Z",
|
|
271
|
+
method: "tools/call",
|
|
272
|
+
toolName: "verify_identity",
|
|
273
|
+
namespace: "auth",
|
|
274
|
+
upstream: "identity-server",
|
|
275
|
+
principal: "user:dave@example.com",
|
|
276
|
+
durationMs: 55,
|
|
277
|
+
success: true,
|
|
278
|
+
previousHash: "genesis",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
const dualA = { ...dualBase, attestation: "sig_aaa111" };
|
|
282
|
+
const dualB = { ...dualBase, attestation: "sig_bbb222" };
|
|
283
|
+
|
|
284
|
+
const dualHashDemo = {
|
|
285
|
+
description:
|
|
286
|
+
"Two records with identical auditable fields but different attestation values. They produce the SAME canonical hash (attestation is excluded from signing) but DIFFERENT chain hashes (attestation is included in chain). This demonstrates the dual-hash design.",
|
|
287
|
+
record_a: {
|
|
288
|
+
record: dualA,
|
|
289
|
+
canonical: canonicalizeRecord(dualA),
|
|
290
|
+
sha256_canonical: sha256Hex(canonicalizeRecord(dualA)),
|
|
291
|
+
full_record_json: JSON.stringify(dualA),
|
|
292
|
+
record_hash: sha256Hex(JSON.stringify(dualA)),
|
|
293
|
+
},
|
|
294
|
+
record_b: {
|
|
295
|
+
record: dualB,
|
|
296
|
+
canonical: canonicalizeRecord(dualB),
|
|
297
|
+
sha256_canonical: sha256Hex(canonicalizeRecord(dualB)),
|
|
298
|
+
full_record_json: JSON.stringify(dualB),
|
|
299
|
+
record_hash: sha256Hex(JSON.stringify(dualB)),
|
|
300
|
+
},
|
|
301
|
+
assertions: {
|
|
302
|
+
canonical_hashes_match:
|
|
303
|
+
sha256Hex(canonicalizeRecord(dualA)) ===
|
|
304
|
+
sha256Hex(canonicalizeRecord(dualB)),
|
|
305
|
+
chain_hashes_differ:
|
|
306
|
+
sha256Hex(JSON.stringify(dualA)) !== sha256Hex(JSON.stringify(dualB)),
|
|
307
|
+
},
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
// --- Assemble output ---
|
|
311
|
+
|
|
312
|
+
const output = {
|
|
313
|
+
format_version: "1.1.0",
|
|
314
|
+
implementation: "mcp-audit-gateway",
|
|
315
|
+
canonical_form: "tuple-array",
|
|
316
|
+
description:
|
|
317
|
+
"Conformance vectors for mcp-audit-gateway's tuple-array canonicalization and SHA-256 hash chain.",
|
|
318
|
+
encoding: "utf-8",
|
|
319
|
+
hash_algorithm: "sha256",
|
|
320
|
+
hash_output: "hex-lowercase",
|
|
321
|
+
null_rule:
|
|
322
|
+
"Fields listed in field_order that are absent or undefined in the source record MUST be serialized as JSON null in the tuple-array canonical form.",
|
|
323
|
+
signing_exclusions: ["attestation"],
|
|
324
|
+
signing_note:
|
|
325
|
+
"The attestation field is excluded from the canonical form used for signing. It cannot be included because the signature IS the attestation value (including it would be circular). The canonical form contains exactly the 11 fields listed in field_order.",
|
|
326
|
+
chain_hash_note:
|
|
327
|
+
"The chain hash (record_hash) uses JSON.stringify(fullRecord) which INCLUDES the attestation field. This binds the signature into the chain sequence, preventing attestation stripping without breaking the chain. The chain hash depends on JavaScript insertion order; verifiers must serialize keys in the order specified by chain.chain_key_order.",
|
|
328
|
+
field_order: [
|
|
329
|
+
"id",
|
|
330
|
+
"timestamp",
|
|
331
|
+
"method",
|
|
332
|
+
"toolName",
|
|
333
|
+
"namespace",
|
|
334
|
+
"upstream",
|
|
335
|
+
"principal",
|
|
336
|
+
"durationMs",
|
|
337
|
+
"success",
|
|
338
|
+
"errorCode",
|
|
339
|
+
"previousHash",
|
|
340
|
+
],
|
|
341
|
+
canonicalization: canonicalizationVectors,
|
|
342
|
+
chain: chainVectors,
|
|
343
|
+
dual_hash_demo: dualHashDemo,
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
writeFileSync(
|
|
347
|
+
join(__dirname, "canonicalization.json"),
|
|
348
|
+
JSON.stringify(output, null, 2) + "\n",
|
|
349
|
+
);
|
|
350
|
+
|
|
351
|
+
console.log("Generated test/vectors/canonicalization.json");
|
|
352
|
+
console.log(` ${canonicalizationVectors.length} canonicalization vectors`);
|
|
353
|
+
console.log(` ${chainVectors.records.length} chain vectors`);
|
|
354
|
+
console.log(` 1 dual-hash demonstration`);
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const vectors = JSON.parse(
|
|
8
|
+
readFileSync(join(__dirname, "canonicalization.json"), "utf-8"),
|
|
9
|
+
);
|
|
10
|
+
|
|
11
|
+
function sha256Hex(input) {
|
|
12
|
+
return createHash("sha256").update(input).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function canonicalizeFromRecord(record, fieldOrder) {
|
|
16
|
+
const ordered = fieldOrder.map((key) => [key, record[key] ?? null]);
|
|
17
|
+
if (record.decisionContextDigest != null) {
|
|
18
|
+
ordered.splice(10, 0, ["decisionContextDigest", record.decisionContextDigest]);
|
|
19
|
+
}
|
|
20
|
+
if (record.parties != null) {
|
|
21
|
+
const insertAt = record.decisionContextDigest != null ? 12 : 11;
|
|
22
|
+
ordered.splice(insertAt, 0, ["parties", record.parties]);
|
|
23
|
+
}
|
|
24
|
+
return JSON.stringify(ordered);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let passed = 0;
|
|
28
|
+
let failed = 0;
|
|
29
|
+
|
|
30
|
+
console.log(`Format version: ${vectors.format_version}`);
|
|
31
|
+
console.log(`Encoding: ${vectors.encoding}`);
|
|
32
|
+
console.log(`Hash: ${vectors.hash_algorithm} (${vectors.hash_output})`);
|
|
33
|
+
console.log();
|
|
34
|
+
|
|
35
|
+
// --- Canonicalization vectors ---
|
|
36
|
+
console.log("=== Canonicalization Vectors ===\n");
|
|
37
|
+
|
|
38
|
+
for (const v of vectors.canonicalization) {
|
|
39
|
+
const canonical = canonicalizeFromRecord(v.record, vectors.field_order);
|
|
40
|
+
const hash = sha256Hex(canonical);
|
|
41
|
+
|
|
42
|
+
const canonMatch = canonical === v.canonical;
|
|
43
|
+
const hashMatch = hash === v.sha256_canonical;
|
|
44
|
+
|
|
45
|
+
if (canonMatch && hashMatch) {
|
|
46
|
+
console.log(` PASS: ${v.name}`);
|
|
47
|
+
passed++;
|
|
48
|
+
} else {
|
|
49
|
+
console.log(` FAIL: ${v.name}`);
|
|
50
|
+
if (!canonMatch) {
|
|
51
|
+
console.log(` canonical expected: ${v.canonical}`);
|
|
52
|
+
console.log(` canonical got: ${canonical}`);
|
|
53
|
+
}
|
|
54
|
+
if (!hashMatch) {
|
|
55
|
+
console.log(` hash expected: ${v.sha256_canonical}`);
|
|
56
|
+
console.log(` hash got: ${hash}`);
|
|
57
|
+
}
|
|
58
|
+
failed++;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Chain vectors ---
|
|
63
|
+
console.log("\n=== Chain Vectors ===\n");
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < vectors.chain.records.length; i++) {
|
|
66
|
+
const entry = vectors.chain.records[i];
|
|
67
|
+
const record = entry.record;
|
|
68
|
+
|
|
69
|
+
// Verify canonical form
|
|
70
|
+
const canonical = canonicalizeFromRecord(record, vectors.field_order);
|
|
71
|
+
const canonHash = sha256Hex(canonical);
|
|
72
|
+
const canonMatch = canonical === entry.canonical;
|
|
73
|
+
const canonHashMatch = canonHash === entry.sha256_canonical;
|
|
74
|
+
|
|
75
|
+
if (canonMatch && canonHashMatch) {
|
|
76
|
+
console.log(` PASS: chain[${i}] canonical (${record.toolName})`);
|
|
77
|
+
passed++;
|
|
78
|
+
} else {
|
|
79
|
+
console.log(` FAIL: chain[${i}] canonical (${record.toolName})`);
|
|
80
|
+
if (!canonMatch) console.log(` canonical mismatch`);
|
|
81
|
+
if (!canonHashMatch) console.log(` canonical hash mismatch`);
|
|
82
|
+
failed++;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Verify chain hash via full_record_json
|
|
86
|
+
const refHash = sha256Hex(entry.full_record_json);
|
|
87
|
+
if (refHash === entry.record_hash) {
|
|
88
|
+
console.log(` PASS: chain[${i}] record_hash (${record.toolName})`);
|
|
89
|
+
passed++;
|
|
90
|
+
} else {
|
|
91
|
+
console.log(` FAIL: chain[${i}] record_hash (${record.toolName})`);
|
|
92
|
+
console.log(` expected: ${entry.record_hash}`);
|
|
93
|
+
console.log(` got: ${refHash}`);
|
|
94
|
+
failed++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Verify native JSON.stringify produces same hash
|
|
98
|
+
const nativeJson = JSON.stringify(record);
|
|
99
|
+
const nativeHash = sha256Hex(nativeJson);
|
|
100
|
+
if (nativeHash === entry.record_hash) {
|
|
101
|
+
console.log(` PASS: chain[${i}] native stringify match (${record.toolName})`);
|
|
102
|
+
passed++;
|
|
103
|
+
} else {
|
|
104
|
+
console.log(` FAIL: chain[${i}] native stringify diverges (${record.toolName})`);
|
|
105
|
+
console.log(` reference: ${entry.full_record_json}`);
|
|
106
|
+
console.log(` native: ${nativeJson}`);
|
|
107
|
+
failed++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Verify linkage
|
|
111
|
+
let linkageOk;
|
|
112
|
+
if (i === 0) {
|
|
113
|
+
linkageOk = record.previousHash === vectors.chain.genesis_seed;
|
|
114
|
+
} else {
|
|
115
|
+
const prev = vectors.chain.records[i - 1];
|
|
116
|
+
linkageOk =
|
|
117
|
+
record.previousHash === prev.record_hash &&
|
|
118
|
+
entry.previous_record_hash === prev.record_hash;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (linkageOk) {
|
|
122
|
+
console.log(` PASS: chain[${i}] linkage (${record.toolName})`);
|
|
123
|
+
passed++;
|
|
124
|
+
} else {
|
|
125
|
+
console.log(` FAIL: chain[${i}] linkage (${record.toolName})`);
|
|
126
|
+
failed++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Dual-hash demonstration ---
|
|
131
|
+
console.log("\n=== Dual-Hash Demonstration ===\n");
|
|
132
|
+
|
|
133
|
+
const demo = vectors.dual_hash_demo;
|
|
134
|
+
|
|
135
|
+
const canonA = canonicalizeFromRecord(demo.record_a.record, vectors.field_order);
|
|
136
|
+
const canonB = canonicalizeFromRecord(demo.record_b.record, vectors.field_order);
|
|
137
|
+
const hashA = sha256Hex(canonA);
|
|
138
|
+
const hashB = sha256Hex(canonB);
|
|
139
|
+
|
|
140
|
+
if (hashA === hashB && hashA === demo.record_a.sha256_canonical) {
|
|
141
|
+
console.log(" PASS: canonical hashes match (attestation excluded)");
|
|
142
|
+
passed++;
|
|
143
|
+
} else {
|
|
144
|
+
console.log(" FAIL: canonical hashes should match");
|
|
145
|
+
failed++;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const chainA = sha256Hex(demo.record_a.full_record_json);
|
|
149
|
+
const chainB = sha256Hex(demo.record_b.full_record_json);
|
|
150
|
+
|
|
151
|
+
if (chainA !== chainB && chainA === demo.record_a.record_hash && chainB === demo.record_b.record_hash) {
|
|
152
|
+
console.log(" PASS: chain hashes differ (attestation included)");
|
|
153
|
+
passed++;
|
|
154
|
+
} else {
|
|
155
|
+
console.log(" FAIL: chain hashes should differ");
|
|
156
|
+
failed++;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (demo.assertions.canonical_hashes_match === true) {
|
|
160
|
+
console.log(" PASS: assertions.canonical_hashes_match");
|
|
161
|
+
passed++;
|
|
162
|
+
} else {
|
|
163
|
+
console.log(" FAIL: assertions.canonical_hashes_match should be true");
|
|
164
|
+
failed++;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (demo.assertions.chain_hashes_differ === true) {
|
|
168
|
+
console.log(" PASS: assertions.chain_hashes_differ");
|
|
169
|
+
passed++;
|
|
170
|
+
} else {
|
|
171
|
+
console.log(" FAIL: assertions.chain_hashes_differ should be true");
|
|
172
|
+
failed++;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// --- Party Attribution vectors ---
|
|
176
|
+
if (vectors.party_attribution) {
|
|
177
|
+
console.log("\n=== Party Attribution Vectors ===\n");
|
|
178
|
+
|
|
179
|
+
for (const v of vectors.party_attribution.vectors) {
|
|
180
|
+
const canonical = canonicalizeFromRecord(v.record, vectors.field_order);
|
|
181
|
+
const hash = sha256Hex(canonical);
|
|
182
|
+
|
|
183
|
+
const canonMatch = canonical === v.canonical;
|
|
184
|
+
const hashMatch = hash === v.sha256_canonical;
|
|
185
|
+
|
|
186
|
+
if (canonMatch && hashMatch) {
|
|
187
|
+
console.log(` PASS: ${v.name}`);
|
|
188
|
+
passed++;
|
|
189
|
+
} else {
|
|
190
|
+
console.log(` FAIL: ${v.name}`);
|
|
191
|
+
if (!canonMatch) {
|
|
192
|
+
console.log(` canonical expected: ${v.canonical}`);
|
|
193
|
+
console.log(` canonical got: ${canonical}`);
|
|
194
|
+
}
|
|
195
|
+
if (!hashMatch) {
|
|
196
|
+
console.log(` hash expected: ${v.sha256_canonical}`);
|
|
197
|
+
console.log(` hash got: ${hash}`);
|
|
198
|
+
}
|
|
199
|
+
failed++;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// --- Chain with Parties ---
|
|
205
|
+
if (vectors.party_attribution?.chain_with_parties) {
|
|
206
|
+
console.log("\n=== Chain with Parties ===\n");
|
|
207
|
+
|
|
208
|
+
const cwp = vectors.party_attribution.chain_with_parties;
|
|
209
|
+
for (let i = 0; i < cwp.records.length; i++) {
|
|
210
|
+
const entry = cwp.records[i];
|
|
211
|
+
const record = entry.record;
|
|
212
|
+
|
|
213
|
+
const canonical = canonicalizeFromRecord(record, vectors.field_order);
|
|
214
|
+
const canonHash = sha256Hex(canonical);
|
|
215
|
+
if (canonical === entry.canonical && canonHash === entry.sha256_canonical) {
|
|
216
|
+
console.log(` PASS: chain_with_parties[${i}] canonical (${record.toolName})`);
|
|
217
|
+
passed++;
|
|
218
|
+
} else {
|
|
219
|
+
console.log(` FAIL: chain_with_parties[${i}] canonical (${record.toolName})`);
|
|
220
|
+
failed++;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const refHash = sha256Hex(entry.full_record_json);
|
|
224
|
+
if (refHash === entry.record_hash) {
|
|
225
|
+
console.log(` PASS: chain_with_parties[${i}] record_hash (${record.toolName})`);
|
|
226
|
+
passed++;
|
|
227
|
+
} else {
|
|
228
|
+
console.log(` FAIL: chain_with_parties[${i}] record_hash (${record.toolName})`);
|
|
229
|
+
console.log(` expected: ${entry.record_hash}`);
|
|
230
|
+
console.log(` got: ${refHash}`);
|
|
231
|
+
failed++;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let linkageOk;
|
|
235
|
+
if (i === 0) {
|
|
236
|
+
linkageOk = record.previousHash === cwp.genesis_seed;
|
|
237
|
+
} else {
|
|
238
|
+
const prev = cwp.records[i - 1];
|
|
239
|
+
linkageOk =
|
|
240
|
+
record.previousHash === prev.record_hash &&
|
|
241
|
+
entry.previous_record_hash === prev.record_hash;
|
|
242
|
+
}
|
|
243
|
+
if (linkageOk) {
|
|
244
|
+
console.log(` PASS: chain_with_parties[${i}] linkage (${record.toolName})`);
|
|
245
|
+
passed++;
|
|
246
|
+
} else {
|
|
247
|
+
console.log(` FAIL: chain_with_parties[${i}] linkage (${record.toolName})`);
|
|
248
|
+
failed++;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// --- Scope Order Significance ---
|
|
254
|
+
if (vectors.party_attribution) {
|
|
255
|
+
const paVecs = vectors.party_attribution.vectors;
|
|
256
|
+
const origVec = paVecs.find((v) => v.name === "scope_order_original");
|
|
257
|
+
const sortVec = paVecs.find((v) => v.name === "scope_order_sorted");
|
|
258
|
+
if (origVec && sortVec) {
|
|
259
|
+
console.log("\n=== Scope Order Significance ===\n");
|
|
260
|
+
const hOrig = sha256Hex(canonicalizeFromRecord(origVec.record, vectors.field_order));
|
|
261
|
+
const hSort = sha256Hex(canonicalizeFromRecord(sortVec.record, vectors.field_order));
|
|
262
|
+
if (hOrig !== hSort) {
|
|
263
|
+
console.log(" PASS: different scope order produces different hash");
|
|
264
|
+
passed++;
|
|
265
|
+
} else {
|
|
266
|
+
console.log(" FAIL: scope order should produce different hashes");
|
|
267
|
+
failed++;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
|
273
|
+
process.exit(failed > 0 ? 1 : 0);
|