@coderifts/agent-guard 17.1.0 → 17.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.
@@ -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
+ };