@attested-intelligence/aga-verify 1.0.0 → 2.0.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/README.md +44 -28
- package/dist/aga-verify.mjs +214 -126
- package/example-bundle.json +86 -258
- package/package.json +3 -6
- package/verify.ts +275 -198
package/README.md
CHANGED
|
@@ -1,56 +1,72 @@
|
|
|
1
1
|
# AGA Independent Verifier (`@attested-intelligence/aga-verify`)
|
|
2
2
|
|
|
3
|
-
Standalone verification of AGA
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
Standalone verification of canonical **AGA SEP Evidence Bundles**. **Zero AGA imports
|
|
4
|
+
and zero third-party dependencies** — it uses only Node's built-in `crypto` (Ed25519 +
|
|
5
|
+
SHA-256). The trust chain dead-ends at the Node runtime and the gateway public key you
|
|
6
|
+
pin; nothing else.
|
|
6
7
|
|
|
7
8
|
## Why this exists
|
|
8
9
|
|
|
9
|
-
AGA claims
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
AGA claims its Evidence Bundles are tamper-evident and offline-verifiable. This tool
|
|
11
|
+
proves that claim is checkable by **anyone**, with no trust in AGA's own code or in any
|
|
12
|
+
npm dependency: it re-implements the complete verification from scratch and runs against
|
|
13
|
+
a bundle you provide.
|
|
13
14
|
|
|
14
15
|
## Quickstart
|
|
15
16
|
|
|
16
17
|
```bash
|
|
17
|
-
#
|
|
18
|
+
# integrity only (proves the bundle is internally authentic + complete-as-presented):
|
|
18
19
|
npx @attested-intelligence/aga-verify <bundle.json>
|
|
19
20
|
|
|
20
|
-
#
|
|
21
|
-
npx @attested-intelligence/aga-verify
|
|
21
|
+
# integrity + PROVENANCE (proves it came from a specific gateway you trust out of band):
|
|
22
|
+
npx @attested-intelligence/aga-verify <bundle.json> --pubkey <64-hex-gateway-key>
|
|
23
|
+
|
|
24
|
+
# smoke-test against the bundled canonical example (a real signed bundle):
|
|
25
|
+
npx @attested-intelligence/aga-verify example-bundle.json \
|
|
26
|
+
--pubkey ea4a6c63e29c520abef5507b132ec5f9954776aebebe7b92421eea691446d22c
|
|
22
27
|
```
|
|
23
28
|
|
|
24
29
|
Exit code is `0` on `VERIFIED`, `1` on `FAILED` — usable directly in CI.
|
|
25
30
|
|
|
26
|
-
`example-bundle.json` is a real, signed evidence bundle produced by the
|
|
27
|
-
reference implementation (the AI-agent governance scenario); it is shipped so
|
|
28
|
-
a third party can confirm the verifier end-to-end in one command.
|
|
29
|
-
|
|
30
31
|
## What it verifies
|
|
31
32
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
Implements the canonical construction in
|
|
34
|
+
[`aga-receipt-spec/CANONICAL_CONSTRUCTION_v2.md`](https://attestedintelligence.com/technology) §6:
|
|
35
|
+
|
|
36
|
+
1. **Structural floor** — algorithm, well-formed (non-small-order) key, receipt/proof counts.
|
|
37
|
+
2. **Receipt signatures** — Ed25519 over the canonical receipt bytes, for every receipt.
|
|
38
|
+
3. **Chain + ordering** — each receipt links to the previous leaf; monotonic ids/timestamps.
|
|
39
|
+
4. **Merkle + bijection** — every leaf is **recomputed from receipt content**, walked to one root, and the proof set is the complete contiguous `0..N-1`.
|
|
40
|
+
5. **Signed checkpoint (mandatory)** — a gateway-signed checkpoint binds the root, the receipt count, and the chain head, so adding/dropping/reordering receipts fails.
|
|
41
|
+
6. **Provenance (only with `--pubkey`)** — the bundle key equals the key you pinned.
|
|
42
|
+
|
|
43
|
+
All steps are fully offline. No network calls, ever.
|
|
44
|
+
|
|
45
|
+
## What a PASS proves — and what it does not
|
|
46
|
+
|
|
47
|
+
A PASS proves every **present** receipt is authentic, correctly chained, Merkle-included
|
|
48
|
+
under a signed checkpoint, and (with `--pubkey`) issued by the pinned gateway — nothing
|
|
49
|
+
present was added, reordered, or truncated.
|
|
36
50
|
|
|
37
|
-
|
|
51
|
+
A PASS does **not** prove **non-omission**: it cannot establish that the signer recorded
|
|
52
|
+
*every* action it took. Completeness is bounded by the tamper-evidence of the interception
|
|
53
|
+
point, which is outside the bundle. Without `--pubkey`, a PASS proves integrity and
|
|
54
|
+
self-consistency under the bundle's own key, **not** provenance.
|
|
38
55
|
|
|
39
56
|
## Independence guarantee
|
|
40
57
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
the single source file, `verify.ts`.
|
|
58
|
+
`npm ls` shows **zero runtime dependencies**. The verifier is one source file
|
|
59
|
+
(`verify.ts`, bundled to `dist/aga-verify.mjs`) using only `node:crypto`. No AGA code, no
|
|
60
|
+
third-party packages, no network for verification.
|
|
45
61
|
|
|
46
62
|
## From source
|
|
47
63
|
|
|
48
64
|
```bash
|
|
49
|
-
npm install
|
|
50
|
-
npm test
|
|
51
|
-
npm run build
|
|
52
|
-
node dist/aga-verify.mjs example-bundle.json
|
|
65
|
+
npm install # devDeps only (esbuild, vitest, tsx) — zero runtime deps
|
|
66
|
+
npm test # vitest: genuine VERIFIES + every tamper/truncation/wrong-key FAILS
|
|
67
|
+
npm run build # bundles verify.ts -> dist/aga-verify.mjs (esbuild)
|
|
68
|
+
node dist/aga-verify.mjs example-bundle.json --pubkey <key>
|
|
53
69
|
```
|
|
54
70
|
|
|
55
71
|
---
|
|
56
|
-
Attested Intelligence Holdings LLC. Implements the AGA
|
|
72
|
+
Attested Intelligence Holdings LLC · MIT. Implements the canonical AGA SEP Evidence Bundle verification (`aga-receipt-spec` v2).
|
package/dist/aga-verify.mjs
CHANGED
|
@@ -1,163 +1,251 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// verify.ts
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
4
|
+
import { createHash, createPublicKey, verify as edVerify } from "node:crypto";
|
|
5
|
+
var ALGORITHM = "Ed25519-SHA256-JCS";
|
|
6
|
+
var SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
7
|
+
var MAX_CANON_DEPTH = 100;
|
|
8
|
+
var SEP_RECEIPT_FIELDS = [
|
|
9
|
+
"receipt_id",
|
|
10
|
+
"receipt_version",
|
|
11
|
+
"algorithm",
|
|
12
|
+
"timestamp",
|
|
13
|
+
"request_id",
|
|
14
|
+
"method",
|
|
15
|
+
"tool_name",
|
|
16
|
+
"decision",
|
|
17
|
+
"reason",
|
|
18
|
+
"policy_reference",
|
|
19
|
+
"arguments_hash",
|
|
20
|
+
"previous_receipt_hash",
|
|
21
|
+
"gateway_id",
|
|
22
|
+
"public_key",
|
|
23
|
+
"signature"
|
|
24
|
+
];
|
|
25
|
+
var SEP_CHECKPOINT_FIELDS = [
|
|
26
|
+
"algorithm",
|
|
27
|
+
"gateway_id",
|
|
28
|
+
"generated_at",
|
|
29
|
+
"head_leaf_hash",
|
|
30
|
+
"leaf_count",
|
|
31
|
+
"merkle_root",
|
|
32
|
+
"signature"
|
|
33
|
+
];
|
|
34
|
+
var LONE_SURROGATE = /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/;
|
|
35
|
+
function canon(o) {
|
|
36
|
+
const rec = (v, depth) => {
|
|
37
|
+
if (depth > MAX_CANON_DEPTH) throw new Error(`canon: input nesting exceeds ${MAX_CANON_DEPTH} levels`);
|
|
38
|
+
if (typeof v === "string" && LONE_SURROGATE.test(v)) throw new Error("canonicalize: lone surrogate");
|
|
39
|
+
if (v === null || typeof v !== "object") return JSON.stringify(v);
|
|
40
|
+
if (Array.isArray(v)) return "[" + v.map((x) => rec(x, depth + 1)).join(",") + "]";
|
|
41
|
+
const m = v;
|
|
42
|
+
return "{" + Object.keys(m).sort().map((k) => JSON.stringify(k) + ":" + rec(m[k], depth + 1)).join(",") + "}";
|
|
43
|
+
};
|
|
44
|
+
return rec(o, 0);
|
|
28
45
|
}
|
|
29
|
-
function
|
|
30
|
-
|
|
46
|
+
function hasExactKeys(o, fields) {
|
|
47
|
+
if (!o || typeof o !== "object" || Array.isArray(o)) return false;
|
|
48
|
+
const keys = Object.keys(o);
|
|
49
|
+
return keys.length === fields.length && fields.every((f) => Object.prototype.hasOwnProperty.call(o, f));
|
|
31
50
|
}
|
|
32
|
-
|
|
33
|
-
|
|
51
|
+
var sha = (b) => createHash("sha256").update(b).digest("hex");
|
|
52
|
+
var u8 = (s) => Buffer.from(s, "utf8");
|
|
53
|
+
var leafHash = (r) => sha(u8(canon(r)));
|
|
54
|
+
var nodeHash = (l, r) => sha(Buffer.concat([Buffer.from(l, "hex"), Buffer.from(r, "hex")]));
|
|
55
|
+
var stripField = (o, f) => Object.fromEntries(Object.entries(o).filter(([k]) => k !== f));
|
|
56
|
+
var isHex = (h, n) => typeof h === "string" && new RegExp(`^[0-9a-f]{${n}}$`).test(h);
|
|
57
|
+
var SMALL_ORDER_KEYS = /* @__PURE__ */ new Set([
|
|
58
|
+
"00".repeat(32),
|
|
59
|
+
"00".repeat(31) + "80",
|
|
60
|
+
"01" + "00".repeat(31),
|
|
61
|
+
"01" + "00".repeat(30) + "80",
|
|
62
|
+
"ec" + "ff".repeat(30) + "7f",
|
|
63
|
+
"ec" + "ff".repeat(31),
|
|
64
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc05",
|
|
65
|
+
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a",
|
|
66
|
+
"26e8958fc2b227b045c3f489f2ef98f0d5dfac05d3c63339b13802886d53fc85",
|
|
67
|
+
"c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa"
|
|
68
|
+
]);
|
|
69
|
+
var ED25519_P = (1n << 255n) - 19n;
|
|
70
|
+
function isCanonicalY(hex) {
|
|
71
|
+
const b = Buffer.from(hex, "hex");
|
|
72
|
+
let y = 0n;
|
|
73
|
+
for (let i = 0; i < 32; i++) y |= BigInt(i === 31 ? b[i] & 127 : b[i]) << BigInt(8 * i);
|
|
74
|
+
return y < ED25519_P;
|
|
34
75
|
}
|
|
35
|
-
|
|
76
|
+
var TS_CANONICAL = /^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]{3}Z$/;
|
|
77
|
+
var isLeap = (y) => y % 4 === 0 && (y % 100 !== 0 || y % 400 === 0);
|
|
78
|
+
var daysInMonth = (y, m) => [31, isLeap(y) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][m - 1];
|
|
79
|
+
function isCanonicalTimestamp(ts) {
|
|
80
|
+
if (typeof ts !== "string" || !TS_CANONICAL.test(ts)) return false;
|
|
81
|
+
const year = parseInt(ts.slice(0, 4), 10);
|
|
82
|
+
const month = parseInt(ts.slice(5, 7), 10);
|
|
83
|
+
const day = parseInt(ts.slice(8, 10), 10);
|
|
84
|
+
const hour = parseInt(ts.slice(11, 13), 10);
|
|
85
|
+
const minute = parseInt(ts.slice(14, 16), 10);
|
|
86
|
+
const second = parseInt(ts.slice(17, 19), 10);
|
|
87
|
+
if (month < 1 || month > 12) return false;
|
|
88
|
+
if (day < 1 || day > daysInMonth(year, month)) return false;
|
|
89
|
+
if (hour > 23) return false;
|
|
90
|
+
if (minute > 59) return false;
|
|
91
|
+
if (second > 59) return false;
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
function wellFormedKey(hex) {
|
|
95
|
+
if (!isHex(hex, 64)) return false;
|
|
96
|
+
if (SMALL_ORDER_KEYS.has(hex) || !isCanonicalY(hex)) return false;
|
|
36
97
|
try {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return ed.verify(sig, enc.encode(message), pk);
|
|
98
|
+
createPublicKey({ key: Buffer.concat([SPKI_PREFIX, Buffer.from(hex, "hex")]), format: "der", type: "spki" });
|
|
99
|
+
return true;
|
|
40
100
|
} catch {
|
|
41
101
|
return false;
|
|
42
102
|
}
|
|
43
103
|
}
|
|
44
|
-
function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
function verifyReceiptSignatures(receipts, portalPublicKey) {
|
|
53
|
-
return receipts.map((receipt) => {
|
|
54
|
-
const { portal_signature, ...unsigned } = receipt;
|
|
55
|
-
const canonical = canonicalize(unsigned);
|
|
56
|
-
return verifyEd25519(portal_signature, canonical, portalPublicKey);
|
|
57
|
-
});
|
|
58
|
-
}
|
|
59
|
-
function verifyMerkleProofs(proofs, checkpointRoot) {
|
|
60
|
-
return proofs.map((proof) => {
|
|
61
|
-
let hash = proof.leafHash;
|
|
62
|
-
for (const sibling of proof.siblings) {
|
|
63
|
-
hash = sibling.position === "left" ? merkleParentHash(sibling.hash, hash) : merkleParentHash(hash, sibling.hash);
|
|
64
|
-
}
|
|
65
|
-
return hash === checkpointRoot;
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
function verifyCheckpointAnchor(_checkpoint) {
|
|
69
|
-
return "SKIPPED";
|
|
104
|
+
function sigOk(pubHex, msg, sigHex) {
|
|
105
|
+
if (!wellFormedKey(pubHex) || !isHex(sigHex, 128) || /^0+$/.test(sigHex)) return false;
|
|
106
|
+
try {
|
|
107
|
+
const pk = createPublicKey({ key: Buffer.concat([SPKI_PREFIX, Buffer.from(pubHex, "hex")]), format: "der", type: "spki" });
|
|
108
|
+
return edVerify(null, u8(msg), pk, Buffer.from(sigHex, "hex"));
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
70
112
|
}
|
|
71
|
-
function
|
|
113
|
+
function validateShape(b) {
|
|
72
114
|
if (b === null || typeof b !== "object" || Array.isArray(b)) return "not a JSON object";
|
|
73
|
-
if (
|
|
74
|
-
if (typeof b.artifact.signature !== "string") return 'missing "artifact.signature"';
|
|
75
|
-
if (typeof b.artifact.issuer_identifier !== "string") return 'missing "artifact.issuer_identifier"';
|
|
76
|
-
if (!Array.isArray(b.receipts)) return 'missing "receipts" array';
|
|
115
|
+
if (b.algorithm !== ALGORITHM) return `algorithm must be "${ALGORITHM}"`;
|
|
77
116
|
if (typeof b.public_key !== "string") return 'missing "public_key"';
|
|
117
|
+
if (!Array.isArray(b.receipts)) return 'missing "receipts" array';
|
|
78
118
|
if (!Array.isArray(b.merkle_proofs)) return 'missing "merkle_proofs" array';
|
|
79
|
-
if (typeof b.
|
|
80
|
-
return 'missing "
|
|
81
|
-
}
|
|
119
|
+
if (typeof b.checkpoint !== "object" || b.checkpoint === null || typeof b.checkpoint.signature !== "string")
|
|
120
|
+
return 'missing signed "checkpoint"';
|
|
82
121
|
return null;
|
|
83
122
|
}
|
|
84
|
-
function
|
|
85
|
-
const
|
|
86
|
-
let bundle;
|
|
123
|
+
function verifySepBundle(bundle, expectedPublicKey) {
|
|
124
|
+
const pinned = isHex(expectedPublicKey, 64);
|
|
87
125
|
try {
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
step4_anchor: "SKIPPED",
|
|
95
|
-
overall: false,
|
|
96
|
-
errors: ["Failed to parse bundle JSON"],
|
|
97
|
-
details: { receipt_results: [], proof_results: [] }
|
|
126
|
+
const steps = [];
|
|
127
|
+
const errors = [];
|
|
128
|
+
const add = (name, ok, err) => {
|
|
129
|
+
steps.push({ name, ok });
|
|
130
|
+
if (!ok && err) errors.push(err);
|
|
131
|
+
return ok;
|
|
98
132
|
};
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
133
|
+
const receipts = Array.isArray(bundle?.receipts) ? bundle.receipts : [];
|
|
134
|
+
const proofs = Array.isArray(bundle?.merkle_proofs) ? bundle.merkle_proofs : [];
|
|
135
|
+
const pub = bundle?.public_key;
|
|
136
|
+
add(
|
|
137
|
+
"structural",
|
|
138
|
+
bundle?.algorithm === ALGORITHM && wellFormedKey(pub) && receipts.length > 0 && proofs.length === receipts.length && receipts.every((r) => hasExactKeys(r, SEP_RECEIPT_FIELDS)),
|
|
139
|
+
"structural floor failed (algorithm/key/receipt-count/receipt-schema)"
|
|
140
|
+
);
|
|
141
|
+
add(
|
|
142
|
+
"receipt_signatures",
|
|
143
|
+
receipts.length > 0 && receipts.every((r) => sigOk(pub, canon(stripField(r, "signature")), r.signature)),
|
|
144
|
+
"one or more receipt signatures invalid"
|
|
145
|
+
);
|
|
146
|
+
const leaves = receipts.map(leafHash);
|
|
147
|
+
let chain = receipts.length > 0;
|
|
148
|
+
let prevTs = "";
|
|
149
|
+
for (let i = 0; i < receipts.length; i++) {
|
|
150
|
+
if ((receipts[i].previous_receipt_hash || "") !== (i === 0 ? "" : leaves[i - 1])) chain = false;
|
|
151
|
+
const ts = receipts[i].timestamp;
|
|
152
|
+
if (!isCanonicalTimestamp(ts)) chain = false;
|
|
153
|
+
else {
|
|
154
|
+
if (i > 0 && ts < prevTs) chain = false;
|
|
155
|
+
prevTs = ts;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
add("chain_and_ordering", chain, "chain linkage, non-canonical timestamp, or ordering broken");
|
|
159
|
+
let root = null, merkle = proofs.length === receipts.length && proofs.length > 0;
|
|
160
|
+
const seen = /* @__PURE__ */ new Set();
|
|
161
|
+
for (const p of proofs) {
|
|
162
|
+
seen.add(p.leaf_index);
|
|
163
|
+
if (receipts[p.leaf_index] === void 0 || leaves[p.leaf_index] !== p.leaf_hash) merkle = false;
|
|
164
|
+
let cur = p.leaf_hash;
|
|
165
|
+
const sib = Array.isArray(p.siblings) ? p.siblings : [];
|
|
166
|
+
const dir = Array.isArray(p.directions) ? p.directions : [];
|
|
167
|
+
if (dir.length !== sib.length || !dir.every((d) => d === "left" || d === "right")) merkle = false;
|
|
168
|
+
for (let j = 0; j < sib.length; j++) cur = dir[j] === "left" ? nodeHash(sib[j], cur) : nodeHash(cur, sib[j]);
|
|
169
|
+
if (p.merkle_root !== cur) merkle = false;
|
|
170
|
+
if (root === null) root = cur;
|
|
171
|
+
else if (root !== cur) merkle = false;
|
|
172
|
+
}
|
|
173
|
+
const bijection = seen.size === receipts.length && [...seen].every((n) => Number.isInteger(n) && n >= 0 && n < receipts.length);
|
|
174
|
+
add("merkle_and_bijection", merkle && bijection, "Merkle proof, per-proof root, leaf recompute, or index bijection failed");
|
|
175
|
+
const cp = bundle.checkpoint;
|
|
176
|
+
let cpOk = false;
|
|
177
|
+
if (hasExactKeys(cp, SEP_CHECKPOINT_FIELDS)) {
|
|
178
|
+
cpOk = cp.algorithm === ALGORITHM && sigOk(pub, canon(stripField(cp, "signature")), cp.signature) && root !== null && cp.merkle_root === root && cp.leaf_count === receipts.length && cp.head_leaf_hash === (leaves.length ? leaves[leaves.length - 1] : "");
|
|
179
|
+
}
|
|
180
|
+
add("signed_checkpoint", cpOk, "signed checkpoint missing, mis-schema, wrong algorithm, or does not anchor the bundle");
|
|
181
|
+
const cpGatewayId = cp && typeof cp === "object" ? cp.gateway_id : void 0;
|
|
182
|
+
const cpGeneratedAt = cp && typeof cp === "object" ? cp.generated_at : void 0;
|
|
183
|
+
add(
|
|
184
|
+
"envelope_consistency",
|
|
185
|
+
receipts.length > 0 && receipts.every((r) => r.public_key === pub) && receipts.every((r) => r.gateway_id === bundle?.gateway_id) && cpGatewayId === bundle?.gateway_id && cpGeneratedAt === bundle?.generated_at && root !== null && bundle?.merkle_root === root,
|
|
186
|
+
// envelope merkle_root <-> recomputed
|
|
187
|
+
"envelope gateway_id/generated_at/merkle_root or a receipt public_key/gateway_id disagrees with the signed/recomputed values"
|
|
188
|
+
);
|
|
189
|
+
const issuerVerified = pinned && pub === expectedPublicKey;
|
|
190
|
+
if (pinned) add("gateway_key_match", issuerVerified, "bundle key does not match the pinned gateway key");
|
|
191
|
+
return { verdict: steps.every((s) => s.ok) ? "VERIFIED" : "FAILED", issuerVerified, pinned, steps, errors };
|
|
192
|
+
} catch (e) {
|
|
102
193
|
return {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
errors: [`unrecognized evidence-bundle format: ${shapeError}`],
|
|
109
|
-
details: { receipt_results: [], proof_results: [] }
|
|
194
|
+
verdict: "FAILED",
|
|
195
|
+
issuerVerified: false,
|
|
196
|
+
pinned,
|
|
197
|
+
steps: [{ name: "verifier_exception", ok: false }],
|
|
198
|
+
errors: [`verifier rejected a malformed bundle: ${String(e)}`]
|
|
110
199
|
};
|
|
111
200
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
});
|
|
124
|
-
const step4 = verifyCheckpointAnchor(bundle.checkpoint_reference);
|
|
125
|
-
return {
|
|
126
|
-
step1_artifact_sig: step1,
|
|
127
|
-
step2_receipt_sigs: step2,
|
|
128
|
-
step3_merkle_proofs: step3,
|
|
129
|
-
step4_anchor: step4,
|
|
130
|
-
overall: step1 && step2 && step3,
|
|
131
|
-
errors,
|
|
132
|
-
details: { receipt_results: receiptResults, proof_results: proofResults }
|
|
133
|
-
};
|
|
201
|
+
}
|
|
202
|
+
function verifyEvidenceBundle(bundleJson, expectedPublicKey) {
|
|
203
|
+
let parsed;
|
|
204
|
+
try {
|
|
205
|
+
parsed = JSON.parse(bundleJson);
|
|
206
|
+
} catch {
|
|
207
|
+
return { verdict: "FAILED", issuerVerified: false, pinned: false, steps: [], errors: ["failed to parse bundle JSON"] };
|
|
208
|
+
}
|
|
209
|
+
const shapeErr = validateShape(parsed);
|
|
210
|
+
if (shapeErr) return { verdict: "FAILED", issuerVerified: false, pinned: false, steps: [], errors: [`unrecognized evidence-bundle format: ${shapeErr}`] };
|
|
211
|
+
return verifySepBundle(parsed, expectedPublicKey);
|
|
134
212
|
}
|
|
135
213
|
if (typeof process !== "undefined" && process.argv[1]?.includes("verify")) {
|
|
136
214
|
const { readFileSync } = await import("node:fs");
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
215
|
+
const args = process.argv.slice(2);
|
|
216
|
+
const file = args.find((a) => !a.startsWith("--"));
|
|
217
|
+
const pk = args.includes("--pubkey") ? args[args.indexOf("--pubkey") + 1] : void 0;
|
|
218
|
+
if (!file) {
|
|
219
|
+
console.error("Usage: aga-verify <bundle.json> [--pubkey <64-hex-gateway-key>]");
|
|
220
|
+
process.exit(2);
|
|
221
|
+
}
|
|
222
|
+
let raw;
|
|
223
|
+
try {
|
|
224
|
+
raw = readFileSync(file, "utf-8");
|
|
225
|
+
} catch (e) {
|
|
226
|
+
console.log("\nAGA Independent Verifier\n");
|
|
227
|
+
console.log("OVERALL: FAILED (could not read bundle file)");
|
|
228
|
+
console.log(`
|
|
229
|
+
Errors:
|
|
230
|
+
- ${String(e)}`);
|
|
140
231
|
process.exit(1);
|
|
141
232
|
}
|
|
142
|
-
const
|
|
143
|
-
const result = verifyEvidenceBundle(bundleJson);
|
|
233
|
+
const result = verifyEvidenceBundle(raw, pk);
|
|
144
234
|
console.log("\nAGA Independent Verifier\n");
|
|
145
|
-
console.log(`
|
|
146
|
-
|
|
147
|
-
console.log(`Step 3 - Merkle inclusion proofs: ${result.step3_merkle_proofs ? "PASS" : "FAIL"} (${result.details.proof_results.filter((r) => r).length}/${result.details.proof_results.length})`);
|
|
148
|
-
console.log(`Step 4 - Checkpoint anchor: ${result.step4_anchor}`);
|
|
235
|
+
for (const s of result.steps) console.log(` ${s.ok ? "PASS" : "FAIL"} ${s.name}`);
|
|
236
|
+
const prov = result.verdict === "VERIFIED" ? result.pinned ? " (provenance verified)" : " (integrity only \u2014 no --pubkey given)" : "";
|
|
149
237
|
console.log(`
|
|
150
|
-
OVERALL: ${result.
|
|
238
|
+
OVERALL: ${result.verdict}${prov}`);
|
|
239
|
+
if (!result.pinned && result.verdict === "VERIFIED") {
|
|
240
|
+
console.log("NOTE: integrity + self-consistency proven, but NOT provenance. Re-run with --pubkey <gateway-key> to prove WHO issued it.");
|
|
241
|
+
}
|
|
151
242
|
if (result.errors.length) {
|
|
152
243
|
console.log("\nErrors:");
|
|
153
244
|
result.errors.forEach((e) => console.log(` - ${e}`));
|
|
154
245
|
}
|
|
155
|
-
process.exit(result.
|
|
246
|
+
process.exit(result.verdict === "VERIFIED" ? 0 : 1);
|
|
156
247
|
}
|
|
157
248
|
export {
|
|
158
|
-
verifyArtifactSignature,
|
|
159
|
-
verifyCheckpointAnchor,
|
|
160
249
|
verifyEvidenceBundle,
|
|
161
|
-
|
|
162
|
-
verifyReceiptSignatures
|
|
250
|
+
verifySepBundle
|
|
163
251
|
};
|