@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.
Files changed (41) hide show
  1. package/.well-known/agent-governance.json +47 -0
  2. package/.well-known/security-insights-snippet.yml +9 -0
  3. package/CHANGELOG.md +32 -0
  4. package/dist/attestation/audit-log.d.ts +1 -0
  5. package/dist/attestation/audit-log.d.ts.map +1 -1
  6. package/dist/attestation/audit-log.js +19 -0
  7. package/dist/attestation/audit-log.js.map +1 -1
  8. package/dist/attestation/signer.d.ts.map +1 -1
  9. package/dist/attestation/signer.js +7 -0
  10. package/dist/attestation/signer.js.map +1 -1
  11. package/dist/attestation/signer.test.js +72 -0
  12. package/dist/attestation/signer.test.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.d.ts.map +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/policy/engine.d.ts +12 -1
  18. package/dist/policy/engine.d.ts.map +1 -1
  19. package/dist/policy/engine.js +26 -4
  20. package/dist/policy/engine.js.map +1 -1
  21. package/dist/policy/engine.test.js +42 -1
  22. package/dist/policy/engine.test.js.map +1 -1
  23. package/dist/proxy/gateway.d.ts.map +1 -1
  24. package/dist/proxy/gateway.js +5 -1
  25. package/dist/proxy/gateway.js.map +1 -1
  26. package/dist/types.d.ts +7 -0
  27. package/dist/types.d.ts.map +1 -1
  28. package/package.json +1 -1
  29. package/src/attestation/audit-log.ts +24 -1
  30. package/src/attestation/signer.test.ts +84 -0
  31. package/src/attestation/signer.ts +8 -1
  32. package/src/index.ts +1 -1
  33. package/src/policy/engine.test.ts +47 -1
  34. package/src/policy/engine.ts +38 -5
  35. package/src/proxy/gateway.ts +6 -1
  36. package/src/types.ts +8 -0
  37. package/test/vectors/README.md +44 -0
  38. package/test/vectors/canonicalization.json +582 -0
  39. package/test/vectors/generate.mjs +354 -0
  40. package/test/vectors/verify.mjs +273 -0
  41. package/test/vectors/verify.py +277 -0
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Cross-language verification of mcp-audit-gateway conformance vectors.
4
+
5
+ Proves the tuple-array canonical form is reproducible outside the
6
+ original JavaScript implementation. Chain hashes are verified against
7
+ the full_record_json reference string (avoiding insertion-order issues).
8
+ """
9
+
10
+ import hashlib
11
+ import json
12
+ import sys
13
+ from pathlib import Path
14
+
15
+ vectors_path = Path(__file__).parent / "canonicalization.json"
16
+ vectors = json.loads(vectors_path.read_text())
17
+
18
+ FIELD_ORDER = vectors["field_order"]
19
+
20
+
21
+ def canonicalize(record: dict) -> str:
22
+ """Reproduce the tuple-array canonical form in Python."""
23
+ ordered = []
24
+ for key in FIELD_ORDER:
25
+ value = record.get(key)
26
+ ordered.append([key, value])
27
+ if record.get("decisionContextDigest") is not None:
28
+ ordered.insert(10, ["decisionContextDigest", record["decisionContextDigest"]])
29
+ if record.get("parties") is not None:
30
+ insert_at = 12 if record.get("decisionContextDigest") is not None else 11
31
+ ordered.insert(insert_at, ["parties", record["parties"]])
32
+ return json.dumps(ordered, separators=(",", ":"), ensure_ascii=False)
33
+
34
+
35
+ def sha256_hex(data: str) -> str:
36
+ return hashlib.sha256(data.encode("utf-8")).hexdigest()
37
+
38
+
39
+ passed = 0
40
+ failed = 0
41
+
42
+ print(f"Format version: {vectors['format_version']}")
43
+ print(f"Encoding: {vectors['encoding']}")
44
+ print(f"Hash: {vectors['hash_algorithm']} ({vectors['hash_output']})")
45
+ print(f"Null rule: {vectors['null_rule']}")
46
+ print()
47
+
48
+ # --- Canonicalization vectors ---
49
+ print("=== Canonicalization Vectors ===\n")
50
+
51
+ for v in vectors["canonicalization"]:
52
+ canonical = canonicalize(v["record"])
53
+ h = sha256_hex(canonical)
54
+
55
+ canon_match = canonical == v["canonical"]
56
+ hash_match = h == v["sha256_canonical"]
57
+
58
+ if canon_match and hash_match:
59
+ print(f" PASS: {v['name']}")
60
+ passed += 1
61
+ else:
62
+ print(f" FAIL: {v['name']}")
63
+ if not canon_match:
64
+ print(f" canonical expected: {v['canonical']}")
65
+ print(f" canonical got: {canonical}")
66
+ if not hash_match:
67
+ print(f" hash expected: {v['sha256_canonical']}")
68
+ print(f" hash got: {h}")
69
+ failed += 1
70
+
71
+ # --- Chain vectors ---
72
+ print("\n=== Chain Vectors ===\n")
73
+
74
+ for i, entry in enumerate(vectors["chain"]["records"]):
75
+ record = entry["record"]
76
+
77
+ # Verify canonical form (cross-language safe)
78
+ canonical = canonicalize(record)
79
+ canon_hash = sha256_hex(canonical)
80
+ canon_match = canonical == entry["canonical"]
81
+ canon_hash_match = canon_hash == entry["sha256_canonical"]
82
+
83
+ if canon_match and canon_hash_match:
84
+ print(f" PASS: chain[{i}] canonical ({record['toolName']})")
85
+ passed += 1
86
+ else:
87
+ print(f" FAIL: chain[{i}] canonical ({record['toolName']})")
88
+ if not canon_match:
89
+ print(f" canonical mismatch")
90
+ if not canon_hash_match:
91
+ print(f" canonical hash mismatch")
92
+ failed += 1
93
+
94
+ # Verify chain hash against full_record_json reference
95
+ reference_json = entry["full_record_json"]
96
+ reference_hash = sha256_hex(reference_json)
97
+ chain_match = reference_hash == entry["record_hash"]
98
+
99
+ if chain_match:
100
+ print(f" PASS: chain[{i}] record_hash via reference JSON ({record['toolName']})")
101
+ passed += 1
102
+ else:
103
+ print(f" FAIL: chain[{i}] record_hash via reference JSON")
104
+ print(f" expected: {entry['record_hash']}")
105
+ print(f" got: {reference_hash}")
106
+ failed += 1
107
+
108
+ # Verify chain linkage
109
+ if i == 0:
110
+ linkage_ok = record["previousHash"] == vectors["chain"]["genesis_seed"]
111
+ else:
112
+ prev = vectors["chain"]["records"][i - 1]
113
+ linkage_ok = (
114
+ record["previousHash"] == prev["record_hash"]
115
+ and entry.get("previous_record_hash") == prev["record_hash"]
116
+ )
117
+
118
+ if linkage_ok:
119
+ print(f" PASS: chain[{i}] linkage ({record['toolName']})")
120
+ passed += 1
121
+ else:
122
+ print(f" FAIL: chain[{i}] linkage ({record['toolName']})")
123
+ failed += 1
124
+
125
+ # --- Dual-hash demonstration ---
126
+ print("\n=== Dual-Hash Demonstration ===\n")
127
+
128
+ demo = vectors["dual_hash_demo"]
129
+ a = demo["record_a"]
130
+ b = demo["record_b"]
131
+
132
+ # Canonical hashes should match (attestation excluded)
133
+ canon_a = canonicalize(a["record"])
134
+ canon_b = canonicalize(b["record"])
135
+ hash_a = sha256_hex(canon_a)
136
+ hash_b = sha256_hex(canon_b)
137
+
138
+ if hash_a == hash_b and hash_a == a["sha256_canonical"] and hash_b == b["sha256_canonical"]:
139
+ print(" PASS: canonical hashes match (attestation excluded from signing)")
140
+ passed += 1
141
+ else:
142
+ print(" FAIL: canonical hashes should match")
143
+ print(f" a: {hash_a}")
144
+ print(f" b: {hash_b}")
145
+ failed += 1
146
+
147
+ # Chain hashes should differ (attestation included)
148
+ chain_a = sha256_hex(a["full_record_json"])
149
+ chain_b = sha256_hex(b["full_record_json"])
150
+
151
+ if chain_a != chain_b and chain_a == a["record_hash"] and chain_b == b["record_hash"]:
152
+ print(" PASS: chain hashes differ (attestation included in chain)")
153
+ passed += 1
154
+ else:
155
+ print(" FAIL: chain hashes should differ")
156
+ print(f" a: {chain_a}")
157
+ print(f" b: {chain_b}")
158
+ failed += 1
159
+
160
+ # Verify assertions field
161
+ if demo["assertions"]["canonical_hashes_match"] is True:
162
+ print(" PASS: assertions.canonical_hashes_match confirmed")
163
+ passed += 1
164
+ else:
165
+ print(" FAIL: assertions.canonical_hashes_match should be true")
166
+ failed += 1
167
+
168
+ if demo["assertions"]["chain_hashes_differ"] is True:
169
+ print(" PASS: assertions.chain_hashes_differ confirmed")
170
+ passed += 1
171
+ else:
172
+ print(" FAIL: assertions.chain_hashes_differ should be true")
173
+ failed += 1
174
+
175
+ # --- Party Attribution vectors ---
176
+ if "party_attribution" in vectors:
177
+ print("\n=== Party Attribution Vectors ===\n")
178
+
179
+ for v in vectors["party_attribution"]["vectors"]:
180
+ canonical = canonicalize(v["record"])
181
+ h = sha256_hex(canonical)
182
+
183
+ canon_match = canonical == v["canonical"]
184
+ hash_match = h == v["sha256_canonical"]
185
+
186
+ if canon_match and hash_match:
187
+ print(f" PASS: {v['name']}")
188
+ passed += 1
189
+ else:
190
+ print(f" FAIL: {v['name']}")
191
+ if not canon_match:
192
+ print(f" canonical expected: {v['canonical']}")
193
+ print(f" canonical got: {canonical}")
194
+ if not hash_match:
195
+ print(f" hash expected: {v['sha256_canonical']}")
196
+ print(f" hash got: {h}")
197
+ failed += 1
198
+
199
+ # --- Chain with Parties vectors ---
200
+ if "party_attribution" in vectors and "chain_with_parties" in vectors["party_attribution"]:
201
+ print("\n=== Chain with Parties ===\n")
202
+
203
+ cwp = vectors["party_attribution"]["chain_with_parties"]
204
+ for i, entry in enumerate(cwp["records"]):
205
+ record = entry["record"]
206
+
207
+ canonical = canonicalize(record)
208
+ canon_hash = sha256_hex(canonical)
209
+ canon_match = canonical == entry["canonical"]
210
+ canon_hash_match = canon_hash == entry["sha256_canonical"]
211
+
212
+ if canon_match and canon_hash_match:
213
+ print(f" PASS: chain_with_parties[{i}] canonical ({record['toolName']})")
214
+ passed += 1
215
+ else:
216
+ print(f" FAIL: chain_with_parties[{i}] canonical ({record['toolName']})")
217
+ if not canon_match:
218
+ print(f" canonical mismatch")
219
+ if not canon_hash_match:
220
+ print(f" canonical hash mismatch")
221
+ failed += 1
222
+
223
+ reference_json = entry["full_record_json"]
224
+ reference_hash = sha256_hex(reference_json)
225
+ chain_match = reference_hash == entry["record_hash"]
226
+
227
+ if chain_match:
228
+ print(f" PASS: chain_with_parties[{i}] record_hash ({record['toolName']})")
229
+ passed += 1
230
+ else:
231
+ print(f" FAIL: chain_with_parties[{i}] record_hash ({record['toolName']})")
232
+ print(f" expected: {entry['record_hash']}")
233
+ print(f" got: {reference_hash}")
234
+ failed += 1
235
+
236
+ if i == 0:
237
+ linkage_ok = record["previousHash"] == cwp["genesis_seed"]
238
+ else:
239
+ prev = cwp["records"][i - 1]
240
+ linkage_ok = (
241
+ record["previousHash"] == prev["record_hash"]
242
+ and entry.get("previous_record_hash") == prev["record_hash"]
243
+ )
244
+
245
+ if linkage_ok:
246
+ print(f" PASS: chain_with_parties[{i}] linkage ({record['toolName']})")
247
+ passed += 1
248
+ else:
249
+ print(f" FAIL: chain_with_parties[{i}] linkage ({record['toolName']})")
250
+ failed += 1
251
+
252
+ # --- Scope Order Significance ---
253
+ if "party_attribution" in vectors:
254
+ pa_vectors = vectors["party_attribution"]["vectors"]
255
+ scope_orig = next((v for v in pa_vectors if v["name"] == "scope_order_original"), None)
256
+ scope_sort = next((v for v in pa_vectors if v["name"] == "scope_order_sorted"), None)
257
+ if scope_orig and scope_sort:
258
+ print("\n=== Scope Order Significance ===\n")
259
+ h_orig = sha256_hex(canonicalize(scope_orig["record"]))
260
+ h_sort = sha256_hex(canonicalize(scope_sort["record"]))
261
+ if h_orig != h_sort:
262
+ print(" PASS: different scope order produces different hash")
263
+ passed += 1
264
+ else:
265
+ print(" FAIL: scope order should produce different hashes")
266
+ failed += 1
267
+
268
+ # --- Summary ---
269
+ print(f"\n=== Results: {passed} passed, {failed} failed ===")
270
+
271
+ if failed > 0:
272
+ sys.exit(1)
273
+ else:
274
+ print("\nAll vectors verified cross-language (Python).")
275
+ print("Canonical form: tuple-array, cross-language safe.")
276
+ print("Chain hash: verified via full_record_json reference string.")
277
+ sys.exit(0)