@coderifts/agent-guard 17.1.0 → 17.3.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.
@@ -1,4 +1,4 @@
1
- # Vendored verify core — the OFFLINE receipt verifier (1307).
1
+ # Vendored verify core — the OFFLINE receipt verifier (1307) and the CANONICAL EVIDENCE core (1423).
2
2
  #
3
3
  # Byte copies from the public receipt-verifier, pinned. The guard used to verify over the
4
4
  # NETWORK via config.client.verifyReceipt(), which the other three enforcement points do not:
@@ -6,8 +6,33 @@
6
6
  # A verifier that needs the network fails differently from one that does not, and the sentence
7
7
  # "the same proof at all four points, offline" was false because of this one.
8
8
  #
9
+ # 1431 ADDS THE GRANT. Measured before it was changed: the guard authenticated the receipt (this
10
+ # verify.js) and the executor attestation (SDK verifyExecutionAttestation), and NEVER the
11
+ # execution grant. `parseGrantFields` in the SDK DECODES a grant to cross-check jti/scope_hash
12
+ # against the attestation, so a forged token whose payload copies those fields — with the word
13
+ # NEM-ALAIRAS where the signature belongs — was accepted as a kernel binding and upgraded
14
+ # ENFORCING_STRICT from authorized_not_committed to authorized_and_committed. A binding checked
15
+ # against an unauthenticated document is not a binding.
16
+ #
9
17
  # Do not edit these files here. Recopy from receipt-verifier and re-pin.
10
- # source: receipt-verifier e11d3f5d9f5444d8b2a07428c922d2040f4819ce
18
+ # PER-FILE PROVENANCE, mixed on purpose. Collapsing this to one revision would lie: verify.js is
19
+ # pinned at 6048195 and has NOT been recopied, because 1306's keyring-shape fix is the behaviour
20
+ # this guard was tested against and recopying HEAD would change a verifier nobody measured again.
21
+ # The 1423 evidence core is vendored at HEAD. test/vendor-parity.test.js checks each file against
22
+ # ITS OWN revision, not against HEAD, so an honest mixed pin does not read as drift.
23
+ #
24
+ # verify.js 6048195c0bcf7e2594a2d78e83cfc082ea615790
25
+ # arity.js 724aa9ae8171f1acf629c3c6b4879dc056575f20
26
+ # verify-grant.js a472bf9073781cba25d83c7d1406b1a7c4af0b94
27
+ # verify-prove-transcript.js 724aa9ae8171f1acf629c3c6b4879dc056575f20
28
+ # verified-execution-binding.js 1e41542296551139c41e2d4b4cff2e67bc42aa06
29
+ # verify-evidence.js 724aa9ae8171f1acf629c3c6b4879dc056575f20
30
+ # keys/coderifts-keys.json 724aa9ae8171f1acf629c3c6b4879dc056575f20
11
31
 
12
32
  verify.js 363e52c72ec4b913e1f3edc8e2029848feaeacb1efa6c07960fe5e5f195c23b3
13
33
  arity.js 721fcac58b960b5e2aa1af91edb6cfc87ebe828a6da73879967ef79a1ca474e8
34
+ verify-grant.js 127b53de3be77196862c10a8812221de0a5112cf1d3feae12d347a6021852df7
35
+ verify-prove-transcript.js 3bb17bd4f16d5ee3cf3fbb754b4df9aa570a615ca94ff2a97b751c4fabd3bd0d
36
+ verify-evidence.js 7ca498fad4b23e4ae8f28c42c30058bc3526a00d5cc6d6f36380335bf25d3288
37
+ verified-execution-binding.js e48ea29b9f8b0c5b1754187788cbaf78cdaf0b505848c05d1ae3ffebb30218f2
38
+ keys/coderifts-keys.json bfe0c898cda3c3a4c8141726d0adb4cdb7eb9762de6264e430eff6469b90422e
@@ -0,0 +1 @@
1
+ {"keys":[{"kid":"2026-07-k1","public_key_pem":"-----BEGIN PUBLIC KEY-----\nMCowBQYDK2VwAyEAJh8xDXLaOCpQ+bBC9d2I+zG1qVsCpeCuWbtH+aGjC7A=\n-----END PUBLIC KEY-----\n","status":"active","valid_from":null,"retired_at":null}]}
@@ -0,0 +1,316 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * verifiedExecutionBinding — the ONE predicate that answers "authorized and committed".
5
+ *
6
+ * ── WHY ONE (1459) ──────────────────────────────────────────────────────────────────────────
7
+ *
8
+ * The same question was being answered in several places with different rules, and the answers
9
+ * disagreed. Measured on the public agent-guard 17.2.0, with a forged-signature grant and a REAL
10
+ * attestation from a trusted executor bound to that grant's jti and scope:
11
+ *
12
+ * ENFORCING_STRICT authorized_and_committed = false
13
+ * ENFORCING_ATOMIC authorized_and_committed = true
14
+ *
15
+ * Two profiles of one product, looking at one set of bytes, reaching opposite conclusions — because
16
+ * the Atomic formula was `receipt_verified && committed && class === 'executor_attested'` and never
17
+ * asked whether the GRANT was signed by anyone. A predicate that is recomputed is a predicate that
18
+ * drifts; this file exists so callers QUOTE the answer instead.
19
+ *
20
+ * ── FOUR AUTHORITIES, AND ONLY THEIR INTERSECTION ───────────────────────────────────────────
21
+ *
22
+ * Each is independently true or not, and each answers a different question. None of them implies
23
+ * another, which is exactly why the intersection — not any one of them — is the success condition:
24
+ *
25
+ * issuer_grant did CodeRifts authorize THIS change? (a signature under a pinned key)
26
+ * executor_attestation did the executor commit THAT grant? (a signature over the grant's ids)
27
+ * one_run_root are these bytes from ONE run? (cr.evidence.root.v1 digests)
28
+ * provider_witness did the provider record it? (a readback — UNSIGNED by nature)
29
+ *
30
+ * ── SHORTFALLS ARE NAMED, NOT FOLDED INTO `false` ───────────────────────────────────────────
31
+ *
32
+ * "Not authorized" and "authorized but the commit is unproven" are different facts with different
33
+ * remedies, and a boolean loses that. Every shortfall gets a state a human can act on, and the
34
+ * states are ORDERED by severity so the returned one is the most serious thing that is wrong.
35
+ *
36
+ * WHAT THIS DOES NOT DO. It reaches no network and holds no keys: every input is supplied by the
37
+ * caller, already-verified or verifiable, and the four authorities are recomputed here from those
38
+ * inputs rather than taken as claims. It also cannot see a lying executor — the root closes
39
+ * third-party splicing, not an executor misreporting its own run.
40
+ */
41
+
42
+ const crypto = require('node:crypto');
43
+
44
+ const { verifyExecutionGrant } = require('./verify-grant.js');
45
+ const { verifyEvidenceRootBinding } = require('./verify-evidence.js');
46
+
47
+ const BINDING_V = 'cr.verified-execution-binding.v1';
48
+
49
+ /**
50
+ * Ordered most-severe first. The returned `state` is the FIRST unmet one, so a caller that renders
51
+ * a single line renders the thing that most needs fixing.
52
+ */
53
+ const STATE = Object.freeze({
54
+ UNAUTHORIZED: 'UNAUTHORIZED',
55
+ COMMIT_UNPROVEN: 'COMMIT_UNPROVEN',
56
+ ONE_RUN_UNPROVEN: 'ONE_RUN_UNPROVEN',
57
+ RECORDED_UNWITNESSED: 'RECORDED_UNWITNESSED',
58
+ NOT_COMMITTED: 'NOT_COMMITTED',
59
+ AUTHORIZED_AND_COMMITTED: 'AUTHORIZED_AND_COMMITTED',
60
+ });
61
+
62
+ /** What each authority is allowed to be missing for, so a caller can choose its own strictness. */
63
+ const AUTHORITY = Object.freeze({
64
+ ISSUER_GRANT: 'issuer_grant',
65
+ EXECUTOR_ATTESTATION: 'executor_attestation',
66
+ ONE_RUN_ROOT: 'one_run_root',
67
+ PROVIDER_WITNESS: 'provider_witness',
68
+ });
69
+
70
+ const sha256pref = (v) =>
71
+ `sha256:${crypto.createHash('sha256').update(String(v), 'utf8').digest('hex')}`;
72
+
73
+ const b64json = (seg) => {
74
+ try { return JSON.parse(Buffer.from(seg, 'base64url').toString('utf8')); } catch (_) { return null; }
75
+ };
76
+
77
+ /** The four fields an attestation binds, read from its own signed preimage. */
78
+ function attestationClaims(token) {
79
+ if (typeof token !== 'string') return null;
80
+ const seg = token.split('|');
81
+ if (seg.length !== 4 || !seg[2]) return null;
82
+ const body = b64json(seg[2]);
83
+ return body && typeof body === 'object' ? body : null;
84
+ }
85
+
86
+ /** The grant's identity, in one vocabulary across v1 and v2. */
87
+ function grantClaims(token) {
88
+ if (typeof token !== 'string') return null;
89
+ const body = b64json(String(token).split('.')[0]);
90
+ if (!body || typeof body !== 'object') return null;
91
+ return {
92
+ v: body.v,
93
+ jti: body.grant_id || body.jti || null,
94
+ scope_hash: body.after_payload_hash || body.scope_hash || null,
95
+ receipt_hash: body.receipt_hash || body.receipt_digest || null,
96
+ operation: body.operation || null,
97
+ // The rest of what a grant SAYS, so the attestation can be checked against all of it rather
98
+ // than against the two fields that happened to be compared first.
99
+ target: body.target_uri || body.target_id || null,
100
+ tenant_id: body.tenant_id || null,
101
+ executor_id: body.executor_id || null,
102
+ adapter_id: body.adapter_id || null,
103
+ audience: body.audience_hash || body.audience || null,
104
+ policy_hash: body.policy_hash || null,
105
+ state_token: body.expected_state_token || body.state_nonce || null,
106
+ };
107
+ }
108
+
109
+ /**
110
+ * @param {object} o
111
+ * @param {{verified: boolean, token?: string}} o.receipt
112
+ * @param {{token: string, publicKey?, keyring?, intended?: object, now?: number}} o.grant
113
+ * THE EXACT BYTES THE ISSUER SIGNED. Not a grant read back out of a tool result: a caller
114
+ * that lets the executed tool hand back its own authorization has already lost.
115
+ * @param {{token: string, registry?: object, verify?: Function}} [o.attestation]
116
+ * @param {{artifact: object, executorKey}} [o.evidenceRoot]
117
+ * @param {{signed: boolean}} [o.providerReadback]
118
+ * @param {boolean} o.committed
119
+ * @param {string[]} [o.required] authorities this caller demands; default: grant + attestation.
120
+ */
121
+ function verifiedExecutionBinding(o = {}) {
122
+ const required = new Set(Array.isArray(o.required) && o.required.length
123
+ ? o.required
124
+ : [AUTHORITY.ISSUER_GRANT, AUTHORITY.EXECUTOR_ATTESTATION]);
125
+ const shortfalls = [];
126
+ const authorities = {};
127
+ const note = (name, ok, detail) => {
128
+ authorities[name] = { ok, required: required.has(name), detail };
129
+ if (!ok && required.has(name)) shortfalls.push(`${name}: ${detail}`);
130
+ return ok;
131
+ };
132
+
133
+ // ── 1. THE ISSUER GRANT ────────────────────────────────────────────────────────────────
134
+ const g = o.grant || {};
135
+ let grantOk = false;
136
+ let gClaims = null;
137
+ if (typeof g.token !== 'string' || g.token.length === 0) {
138
+ note(AUTHORITY.ISSUER_GRANT, false, 'no execution grant was supplied');
139
+ } else if (!g.publicKey && !g.keyring) {
140
+ // FAIL-CLOSED, and named as its own thing: "we had no key" is not "the signature was bad".
141
+ note(AUTHORITY.ISSUER_GRANT, false,
142
+ 'no pinned issuer keyring was supplied, so the grant could not be authenticated');
143
+ } else {
144
+ const r = verifyExecutionGrant(g.token, {
145
+ ctx: { publicKey: g.publicKey, keyring: g.keyring, expectedKid: g.expectedKid ?? null },
146
+ ...(g.intended ? { intended: g.intended } : {}),
147
+ ...(Number.isFinite(g.now) ? { now: g.now } : {}),
148
+ });
149
+ gClaims = grantClaims(g.token);
150
+ grantOk = note(AUTHORITY.ISSUER_GRANT, r.valid === true,
151
+ `${r.status}${r.reason ? `/${r.reason}` : ''}`);
152
+ }
153
+
154
+ // ── 2. THE EXECUTOR ATTESTATION ────────────────────────────────────────────────────────
155
+ const a = o.attestation || {};
156
+ let attOk = false;
157
+ if (typeof a.token !== 'string' || a.token.length === 0) {
158
+ note(AUTHORITY.EXECUTOR_ATTESTATION, false, 'no executor attestation was supplied');
159
+ } else if (typeof a.verify !== 'function') {
160
+ note(AUTHORITY.EXECUTOR_ATTESTATION, false,
161
+ 'no attestation verifier was supplied, so the commit could not be checked');
162
+ } else {
163
+ const r = a.verify(a.token, { registry: a.registry });
164
+ const sigOk = r && r.valid === true;
165
+ // BOUND TO THE VERIFIED GRANT'S OWN IDS — not to ids the caller passed alongside. This is the
166
+ // join that makes the two signatures one statement instead of two unrelated true things.
167
+ const c = attestationClaims(a.token);
168
+ // ── THE ATTESTATION MUST BIND THE SAME EXECUTION, NOT MERELY THE SAME NAMES (1464) ────
169
+ //
170
+ // REPRODUCED before this was written. A correctly-signed grant bound to receipt R1, and a
171
+ // correctly-signed attestation from a trusted executor bound to receipt R2, sharing a
172
+ // grant_jti and a scope_hash — R1 != R2 — read AUTHORIZED_AND_COMMITTED. Both signatures are
173
+ // real; the two documents describe DIFFERENT executions and the join could not tell.
174
+ //
175
+ // `jti` and `scope_hash` are the two fields an attacker controls most cheaply: they are copied
176
+ // FROM the grant into the attestation by whoever assembles the pair. Comparing only those is
177
+ // comparing a value with its own copy. What binds is the receipt each side was issued against,
178
+ // and — the strongest available — the sha256 of the exact grant token bytes.
179
+ const attReceipt = c ? String(c.receipt_digest || '') : '';
180
+ const grantReceipt = gClaims ? String(gClaims.receipt_hash || '') : '';
181
+ const mismatch = (() => {
182
+ if (!sigOk) return `attestation ${r ? r.status : 'unverifiable'}`;
183
+ if (!gClaims) return 'there is no verified grant for the attestation to bind';
184
+ if (String(c.grant_jti || '') !== String(gClaims.jti || '')) {
185
+ return 'the attestation binds a different grant id than the verified grant';
186
+ }
187
+ if (String(c.scope_hash || '') !== String(gClaims.scope_hash || '')) {
188
+ return 'the attestation binds a different scope than the verified grant';
189
+ }
190
+ // THE CROSS-RECEIPT CHECK. Empty on either side is a mismatch: an attestation that names no
191
+ // receipt cannot be shown to be about this authorization, and "unstated" must not read as
192
+ // "the same".
193
+ if (!attReceipt || !grantReceipt || attReceipt !== grantReceipt) {
194
+ return `the grant was issued against receipt ${grantReceipt || '(none)'} and the `
195
+ + `attestation commits receipt ${attReceipt || '(none)'} — two different executions`;
196
+ }
197
+ // ── WHAT cr.exec.attest.v1 CAN AND CANNOT BE ASKED ──────────────────────────────
198
+ //
199
+ // MEASURED, and it bounds this check rather than the check bounding the format: the
200
+ // attestation body is a CLOSED set — executor_kid, grant_jti, receipt_digest, scope_hash,
201
+ // committed_at, state_nonce, result_digest, meta. Any other key is refused
202
+ // ATTEST_MALFORMED / unknown_field by its own verifier.
203
+ //
204
+ // So target, operation, tenant, executor, adapter, audience and policy CANNOT be
205
+ // cross-checked here: the attestation never states them, and a comparison against a field
206
+ // that cannot exist is not a check — it is a line that always passes. They are named in
207
+ // `does_not_prove` instead, which is the honest place for a binding the format cannot carry.
208
+ //
209
+ // The same is true of the exact grant-token digest: there is no field for it. Binding the
210
+ // grant BYTES rather than its claims would be the tightest join available and it needs a
211
+ // format change (a `grant_token_digest` slot in cr.exec.attest.v2), not a check here.
212
+ //
213
+ // What the format DOES let us bind is the state nonce, and it is bound below.
214
+ if (gClaims.state_token != null && c.state_nonce != null
215
+ && String(gClaims.state_token) !== String(c.state_nonce)) {
216
+ return 'the grant and the attestation disagree about the state nonce';
217
+ }
218
+ return null;
219
+ })();
220
+ attOk = note(AUTHORITY.EXECUTOR_ATTESTATION, mismatch === null, mismatch || 'bound');
221
+ }
222
+
223
+ // ── 3. ONE RUN ─────────────────────────────────────────────────────────────────────────
224
+ const er = o.evidenceRoot || null;
225
+ if (!er || !er.artifact) {
226
+ note(AUTHORITY.ONE_RUN_ROOT, false,
227
+ 'no cr.evidence.root.v1 was supplied, so these bytes are not shown to be one run');
228
+ } else {
229
+ const r = verifyEvidenceRootBinding(er.artifact, { executorKey: er.executorKey, sidecars: er.sidecars });
230
+ note(AUTHORITY.ONE_RUN_ROOT, r.ok === true, r.ok ? 'bound' : (r.failures[0] || 'unbound'));
231
+ }
232
+
233
+ // ── 4. THE PROVIDER WITNESS ────────────────────────────────────────────────────────────
234
+ // A readback is an UNSIGNED document by nature. It is carried, not verified, and this authority
235
+ // is false unless a caller states it was witnessed some stronger way — never true by default.
236
+ // ── A CALLER BOOLEAN IS NOT EVIDENCE (1465) ────────────────────────────────────────────
237
+ //
238
+ // REPRODUCED before this was written:
239
+ //
240
+ // required: ['provider_witness'], receipt: {verified: true},
241
+ // providerReadback: {signed: true}, committed: true, NO grant, NO attestation, NO root
242
+ // → AUTHORIZED_AND_COMMITTED, shortfalls: []
243
+ //
244
+ // `signed: true` was a bare boolean the caller wrote, and this function aggregated it into a
245
+ // global success. Nothing was verified; a field named `signed` was believed because it was set.
246
+ //
247
+ // A witness now requires a VERIFIED witness envelope: bytes plus a verifier plus a trust anchor.
248
+ // No such format exists yet (phase D measured that the readback is unsigned by nature), so this
249
+ // authority cannot currently be satisfied at all — and saying that plainly is the honest answer.
250
+ // Asking for it yields RECORDED_UNWITNESSED, which is exactly what it means.
251
+ //
252
+ // NOT a breaking change for the five consumers: none of them requires `provider_witness` today
253
+ // (guard and contract-gate ask for issuer_grant + executor_attestation; prove and conformance for
254
+ // issuer_grant + one_run_root). It removes a way to LIE, not a way anyone works.
255
+ const pw = o.providerReadback || null;
256
+ const witnessVerified = !!(pw && pw.verified === true && pw.envelope && pw.verifier);
257
+ note(AUTHORITY.PROVIDER_WITNESS, witnessVerified,
258
+ pw
259
+ ? (pw.signed === true && !witnessVerified
260
+ ? 'the caller asserted `signed: true` and supplied no verifiable witness envelope — a '
261
+ + 'boolean is not evidence, and no signed-witness format exists yet'
262
+ : 'the provider readback is an unsigned document (carried, not verified)')
263
+ : 'no provider readback was supplied');
264
+
265
+ // ── THE INTERSECTION ───────────────────────────────────────────────────────────────────
266
+ const committed = o.committed === true;
267
+ // THE RECEIPT, and what this function can honestly say about it.
268
+ //
269
+ // `verified` is the CALLER's determination: this core is not given the receipt token or a
270
+ // keyring, so it cannot re-establish it. That is recorded rather than hidden — a reader of the
271
+ // result can see whether the receipt was verified HERE or asserted by whoever called.
272
+ //
273
+ // Left as-is deliberately: making a bare boolean insufficient would change the input shape of
274
+ // all five consumers at once, and that belongs with the closed-profile work (1465), not
275
+ // half-done in a round that would leave them broken. The gap is named, not narrowed in silence.
276
+ const receiptOk = !!(o.receipt && o.receipt.verified === true);
277
+ const receiptAsserted = receiptOk && !(o.receipt.token && (o.receipt.keyring || o.receipt.publicKey));
278
+ if (!receiptOk) shortfalls.unshift('receipt: the decision receipt did not verify');
279
+
280
+ let state = STATE.AUTHORIZED_AND_COMMITTED;
281
+ if (!receiptOk || (required.has(AUTHORITY.ISSUER_GRANT) && !grantOk)) state = STATE.UNAUTHORIZED;
282
+ else if (required.has(AUTHORITY.EXECUTOR_ATTESTATION) && !attOk) state = STATE.COMMIT_UNPROVEN;
283
+ else if (required.has(AUTHORITY.ONE_RUN_ROOT) && !authorities[AUTHORITY.ONE_RUN_ROOT].ok) {
284
+ state = STATE.ONE_RUN_UNPROVEN;
285
+ } else if (required.has(AUTHORITY.PROVIDER_WITNESS) && !authorities[AUTHORITY.PROVIDER_WITNESS].ok) {
286
+ state = STATE.RECORDED_UNWITNESSED;
287
+ } else if (!committed) state = STATE.NOT_COMMITTED;
288
+
289
+ return {
290
+ v: BINDING_V,
291
+ authorized_and_committed: state === STATE.AUTHORIZED_AND_COMMITTED,
292
+ state,
293
+ authorities,
294
+ shortfalls,
295
+ /** True when `receipt.verified` was taken on the caller's word rather than established here. */
296
+ receipt_caller_asserted: receiptAsserted,
297
+ // Said out loud so a caller cannot read success as more than it is.
298
+ does_not_prove: [
299
+ 'that the executor told the truth about its own run — the evidence root closes third-party '
300
+ + 'splicing, not an executor misreporting itself',
301
+ 'that a provider merged anything; `provider_witness` is an unsigned readback unless a caller '
302
+ + 'states otherwise',
303
+ 'that the grant and the attestation agree about target, operation, tenant, executor, adapter, '
304
+ + 'audience or policy — cr.exec.attest.v1 is a closed field set that states none of them, so '
305
+ + 'those are UNCHECKED here rather than checked and equal (1464)',
306
+ 'that the attestation commits the exact grant BYTES — the format carries no grant-token '
307
+ + 'digest, so the join is over the grant id, scope, receipt and state nonce',
308
+ ...(receiptAsserted
309
+ ? ['that the decision receipt verifies — `receipt.verified` was asserted by the caller and '
310
+ + 'not established here; this core is given no receipt token or keyring to check it with']
311
+ : []),
312
+ ],
313
+ };
314
+ }
315
+
316
+ module.exports = { verifiedExecutionBinding, STATE, AUTHORITY, BINDING_V, grantClaims, attestationClaims };
@@ -0,0 +1,238 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * THE CANONICAL EVIDENCE VERIFIER — every signature in a prove envelope, not just one.
5
+ *
6
+ * ── WHAT 1423 MEASURED ──────────────────────────────────────────────────────────────────────
7
+ *
8
+ * A recorded artifact is protected by two different things, and they answer different questions:
9
+ *
10
+ * the PIN (sha256) are these the bytes we vendored? → tamper-EVIDENT
11
+ * the SIGNATURES did the named issuers produce them? → AUTHENTIC
12
+ *
13
+ * Conformance checked the pin and exactly one signature (the correlation). So an auditor could
14
+ * flip the last character of `issuance.execution_grant`, recompute the pin, and the profile still
15
+ * graded COVERED — reproduced, both mutations, before this file existed. The pin cannot catch that
16
+ * on its own: whoever edits the bytes also owns the file the hash is written in.
17
+ *
18
+ * capability-demo's `prove --check` already refused both mutations. That is the shape of the bug:
19
+ * not a missing capability anywhere, but TWO verifiers that disagreed about what checking means.
20
+ * So this is a shared core rather than a third implementation — one place to fix, one place to
21
+ * drift from, and consumers that can be tested against each other.
22
+ *
23
+ * ── WHAT IT REFUSES TO DO ───────────────────────────────────────────────────────────────────
24
+ *
25
+ * An ABSENT token is reported ABSENT and never as verified. This matters more than it sounds: a
26
+ * verifier that returns "ok" for an envelope carrying no grant at all would let the strongest
27
+ * possible tamper — deletion — read as a pass. The caller says which slots it requires
28
+ * (`required`), and absence of a required slot is a failure with its own reason.
29
+ *
30
+ * It authenticates SIGNATURES. It does not decide whether the run's claims are true: a correctly
31
+ * signed transcript of a failing run is authentic and still a failure. Callers keep grading.
32
+ */
33
+
34
+ const crypto = require('node:crypto');
35
+
36
+ const { verifyReceipt, keyringFromDocument } = require('./verify.js');
37
+ const { verifyExecutionGrant } = require('./verify-grant.js');
38
+ const { verifyProveTranscript } = require('./verify-prove-transcript.js');
39
+
40
+ const CORRELATION_V = 'cr.exec.correlation.v1';
41
+ const US = '\x1f';
42
+
43
+ /** The slots this verifier knows how to authenticate. */
44
+ const SLOT = Object.freeze({
45
+ CHAIN_RECEIPT: 'chain_receipt',
46
+ EXECUTION_GRANT: 'execution_grant',
47
+ TRANSCRIPT_TOKEN: 'transcript_token',
48
+ CORRELATION: 'correlation',
49
+ ATTESTATION: 'atomic_attestation',
50
+ });
51
+
52
+ /** Which keyring signs which slot. Stated as data so a caller can read it, not infer it. */
53
+ const SIGNER = Object.freeze({
54
+ [SLOT.CHAIN_RECEIPT]: 'issuer',
55
+ [SLOT.EXECUTION_GRANT]: 'issuer',
56
+ [SLOT.TRANSCRIPT_TOKEN]: 'executor',
57
+ [SLOT.CORRELATION]: 'executor',
58
+ [SLOT.ATTESTATION]: 'executor',
59
+ });
60
+
61
+ function toKeyring(doc) {
62
+ if (!doc) return null;
63
+ if (doc instanceof Map) return doc;
64
+ return keyringFromDocument(doc);
65
+ }
66
+
67
+ function keyFor(keyring, kid) {
68
+ if (!keyring) return null;
69
+ const entry = keyring instanceof Map ? keyring.get(kid) : keyring[kid];
70
+ if (!entry) return null;
71
+ return entry.publicKey || entry.public_key || entry;
72
+ }
73
+
74
+ /** Rebuilt from the fields, never read back from `correlation_hash`. */
75
+ function correlationPreimage(c) {
76
+ return [CORRELATION_V, c.scope_hash, c.contract_commit, c.contract_path, c.readback_commit]
77
+ .join(US);
78
+ }
79
+
80
+ /**
81
+ * The correlation is a bare Ed25519 signature over a field-joined preimage rather than a token,
82
+ * so it gets its own small verifier here instead of a shape it does not have.
83
+ */
84
+ function verifyCorrelation(c, publicKey) {
85
+ if (!c || c.v !== CORRELATION_V) {
86
+ return { valid: false, status: 'CORRELATION_MALFORMED', reason: 'not_a_correlation' };
87
+ }
88
+ if (!publicKey) {
89
+ return { valid: false, status: 'CORRELATION_UNKNOWN_KEY', reason: 'unknown_kid' };
90
+ }
91
+ const preimage = correlationPreimage(c);
92
+ const expected = `sha256:${crypto.createHash('sha256').update(preimage, 'utf8').digest('hex')}`;
93
+ // The hash is checked BEFORE the signature so a mutated binding field is named as what it is —
94
+ // a field that no longer matches its own digest — rather than as a generic bad signature.
95
+ if (c.correlation_hash && c.correlation_hash !== expected) {
96
+ return { valid: false, status: 'CORRELATION_UNBOUND', reason: 'correlation_hash_mismatch' };
97
+ }
98
+ let ok = false;
99
+ try {
100
+ ok = crypto.verify(
101
+ null, Buffer.from(preimage, 'utf8'), publicKey, Buffer.from(String(c.signature), 'base64url'),
102
+ );
103
+ } catch (_) {
104
+ return { valid: false, status: 'CORRELATION_INVALID_SIGNATURE', reason: 'signature_error' };
105
+ }
106
+ return ok
107
+ ? { valid: true, status: 'CORRELATION_VALID' }
108
+ : { valid: false, status: 'CORRELATION_INVALID_SIGNATURE', reason: 'signature_mismatch' };
109
+ }
110
+
111
+ /** cr.atomic.execution.attestation.v1 | <kid> | b64url(preimage) | <sig> */
112
+ function verifyAtomicAttestationToken(token, keyring) {
113
+ const seg = String(token).split('|');
114
+ if (seg.length !== 4 || seg[0] !== 'cr.atomic.execution.attestation.v1' || seg.some((s) => !s)) {
115
+ return { valid: false, status: 'ATTEST_MALFORMED', reason: 'malformed_structure' };
116
+ }
117
+ const publicKey = keyFor(keyring, seg[1]);
118
+ if (!publicKey) return { valid: false, status: 'ATTEST_UNKNOWN_KEY', reason: 'unknown_kid', kid: seg[1] };
119
+ let preimage;
120
+ try { preimage = Buffer.from(seg[2], 'base64url').toString('utf8'); } catch (_) {
121
+ return { valid: false, status: 'ATTEST_MALFORMED', reason: 'bad_preimage', kid: seg[1] };
122
+ }
123
+ let ok = false;
124
+ try {
125
+ ok = crypto.verify(null, Buffer.from(preimage, 'utf8'), publicKey, Buffer.from(seg[3], 'base64url'));
126
+ } catch (_) {
127
+ return { valid: false, status: 'ATTEST_INVALID_SIGNATURE', reason: 'signature_error', kid: seg[1] };
128
+ }
129
+ return ok
130
+ ? { valid: true, status: 'ATTEST_VALID', kid: seg[1], preimage }
131
+ : { valid: false, status: 'ATTEST_INVALID_SIGNATURE', reason: 'signature_mismatch', kid: seg[1] };
132
+ }
133
+
134
+ /**
135
+ * Authenticate every signed token an artifact carries.
136
+ *
137
+ * @param {object} artifact a cr.prove.artifact.v1 document
138
+ * @param {object} o
139
+ * @param {object|Map} [o.issuerKeys] registry document (or Map) for the CodeRifts issuer
140
+ * @param {object|Map} [o.executorKeys] registry document (or Map) for the executor
141
+ * @param {string[]} [o.required] slots that MUST be present; absence is a failure
142
+ * @param {number} [o.now] clock injection; defaults to each token's own issuance
143
+ * instant, so a recorded artifact is authenticated as of
144
+ * when it was made rather than expiring in the vendor tree
145
+ * @returns {{ok: boolean, slots: object[], failures: string[]}}
146
+ */
147
+ function verifyEvidenceEnvelope(artifact, o = {}) {
148
+ const issuer = toKeyring(o.issuerKeys);
149
+ const executor = toKeyring(o.executorKeys);
150
+ const required = new Set(Array.isArray(o.required) ? o.required : []);
151
+ const slots = [];
152
+ const failures = [];
153
+
154
+ const record = (name, present, result) => {
155
+ const entry = {
156
+ slot: name,
157
+ signer: SIGNER[name],
158
+ present,
159
+ verified: present ? result.valid === true : false,
160
+ status: present ? result.status : 'ABSENT',
161
+ reason: present ? (result.reason || null) : 'not_present_in_envelope',
162
+ kid: present ? (result.kid || null) : null,
163
+ };
164
+ slots.push(entry);
165
+ if (!present && required.has(name)) {
166
+ failures.push(`the ${name} is absent from the envelope, and this profile requires it`);
167
+ } else if (present && !entry.verified) {
168
+ failures.push(`the ${name} signature does not verify (${entry.status}${entry.reason ? `: ${entry.reason}` : ''})`);
169
+ }
170
+ return entry;
171
+ };
172
+
173
+ const iss = artifact && artifact.issuance ? artifact.issuance : null;
174
+
175
+ // ── issuer-signed ────────────────────────────────────────────────────────────────────────
176
+ const grantTok = iss && iss.execution_grant;
177
+ if (grantTok) {
178
+ // now = the grant's own not_before/iat. A RECORDED grant is short-lived by design; judging it
179
+ // against today's clock would report every vendored fixture as expired, which says something
180
+ // about the calendar and nothing about the signature.
181
+ const g = iss.grant || {};
182
+ const at = Number.isFinite(o.now) ? o.now : Date.parse(g.not_before || g.iat || artifact.started_at);
183
+ record(SLOT.EXECUTION_GRANT, true, verifyExecutionGrant(grantTok, {
184
+ ctx: { keyring: issuer, expectedKid: null },
185
+ now: Number.isFinite(at) ? at + 1000 : undefined,
186
+ }));
187
+ } else {
188
+ record(SLOT.EXECUTION_GRANT, false, {});
189
+ }
190
+
191
+ const receiptTok = iss && iss.chain_receipt;
192
+ if (receiptTok) {
193
+ const at = Number.isFinite(o.now) ? o.now : Date.parse(artifact.started_at);
194
+ record(SLOT.CHAIN_RECEIPT, true, verifyReceipt(receiptTok, {
195
+ ctx: { keyring: issuer, expectedKid: null },
196
+ now: Number.isFinite(at) ? at + 1000 : undefined,
197
+ }));
198
+ } else {
199
+ record(SLOT.CHAIN_RECEIPT, false, {});
200
+ }
201
+
202
+ // ── executor-signed ──────────────────────────────────────────────────────────────────────
203
+ if (artifact && artifact.transcript_token) {
204
+ record(SLOT.TRANSCRIPT_TOKEN, true,
205
+ verifyProveTranscript(artifact.transcript_token, { keyring: executor }));
206
+ } else {
207
+ record(SLOT.TRANSCRIPT_TOKEN, false, {});
208
+ }
209
+
210
+ if (artifact && artifact.correlation) {
211
+ const c = artifact.correlation;
212
+ const kid = executor && executor instanceof Map ? [...executor.keys()][0] : null;
213
+ record(SLOT.CORRELATION, true, verifyCorrelation(c, keyFor(executor, kid)));
214
+ } else {
215
+ record(SLOT.CORRELATION, false, {});
216
+ }
217
+
218
+ const att = artifact && artifact.atomic_execution_attestation
219
+ ? artifact.atomic_execution_attestation
220
+ : (artifact && artifact.attestation) || null;
221
+ if (typeof att === 'string' && att.length > 0) {
222
+ record(SLOT.ATTESTATION, true, verifyAtomicAttestationToken(att, executor));
223
+ } else {
224
+ record(SLOT.ATTESTATION, false, {});
225
+ }
226
+
227
+ return { ok: failures.length === 0, slots, failures };
228
+ }
229
+
230
+ module.exports = {
231
+ verifyEvidenceEnvelope,
232
+ verifyCorrelation,
233
+ verifyAtomicAttestationToken,
234
+ correlationPreimage,
235
+ SLOT,
236
+ SIGNER,
237
+ CORRELATION_V,
238
+ };