@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.
- package/dist/cjs/cas-attestation.d.ts +65 -0
- package/dist/cjs/cas-attestation.d.ts.map +1 -1
- package/dist/cjs/cas-attestation.js +87 -3
- package/dist/cjs/cas-attestation.js.map +1 -1
- package/dist/cjs/guard.d.ts.map +1 -1
- package/dist/cjs/guard.js +8 -0
- package/dist/cjs/guard.js.map +1 -1
- package/dist/cjs/vendor/VENDOR.sha256 +25 -2
- package/dist/cjs/vendor/keys/coderifts-keys.json +1 -0
- package/dist/cjs/vendor/verify-evidence.js +238 -0
- package/dist/cjs/vendor/verify-grant.js +522 -0
- package/dist/cjs/vendor/verify-prove-transcript.js +72 -0
- package/dist/esm/cas-attestation.d.ts +65 -0
- package/dist/esm/cas-attestation.d.ts.map +1 -1
- package/dist/esm/cas-attestation.js +86 -3
- package/dist/esm/cas-attestation.js.map +1 -1
- package/dist/esm/guard.d.ts.map +1 -1
- package/dist/esm/guard.js +8 -0
- package/dist/esm/guard.js.map +1 -1
- package/dist/esm/vendor/VENDOR.sha256 +25 -2
- package/dist/esm/vendor/keys/coderifts-keys.json +1 -0
- package/dist/esm/vendor/verify-evidence.js +238 -0
- package/dist/esm/vendor/verify-grant.js +522 -0
- package/dist/esm/vendor/verify-prove-transcript.js +72 -0
- package/package.json +1 -1
|
@@ -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
|
+
};
|