@coderifts/agent-guard 17.2.0 → 17.3.1
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 +15 -0
- package/dist/cjs/cas-attestation.d.ts.map +1 -1
- package/dist/cjs/cas-attestation.js +136 -6
- package/dist/cjs/cas-attestation.js.map +1 -1
- package/dist/cjs/vendor/VENDOR.sha256 +29 -9
- package/dist/cjs/vendor/evidence-root.js +188 -0
- package/dist/cjs/vendor/verified-execution-binding.js +507 -0
- package/dist/cjs/vendor/verify-evidence.js +172 -0
- package/dist/cjs/vendor/verify-grant.js +79 -4
- package/dist/cjs/vendor/verify.js +6 -6
- package/dist/esm/cas-attestation.d.ts +15 -0
- package/dist/esm/cas-attestation.d.ts.map +1 -1
- package/dist/esm/cas-attestation.js +136 -6
- package/dist/esm/cas-attestation.js.map +1 -1
- package/dist/esm/vendor/VENDOR.sha256 +29 -9
- package/dist/esm/vendor/evidence-root.js +188 -0
- package/dist/esm/vendor/verified-execution-binding.js +507 -0
- package/dist/esm/vendor/verify-evidence.js +172 -0
- package/dist/esm/vendor/verify-grant.js +79 -4
- package/dist/esm/vendor/verify.js +6 -6
- package/package.json +1 -1
|
@@ -15,22 +15,42 @@
|
|
|
15
15
|
# against an unauthenticated document is not a binding.
|
|
16
16
|
#
|
|
17
17
|
# Do not edit these files here. Recopy from receipt-verifier and re-pin.
|
|
18
|
-
# PER-FILE PROVENANCE,
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
18
|
+
# PER-FILE PROVENANCE, RETIRED. It said verify.js was held at 6048195 because "recopying HEAD
|
|
19
|
+
# would change a verifier nobody measured again" — true when written. MEASURED before retiring it:
|
|
20
|
+
# the 6048195 -> v1.0.0 delta on verify.js is ZERO non-comment lines (CLI usage docs), so the
|
|
21
|
+
# reason did not apply to this delta. verify-evidence.js was older still and did not export
|
|
22
|
+
# verifyEvidenceRootBinding at all, which is why the shared core needed a fail-closed guard against
|
|
23
|
+
# its own missing dependency. The core is now ONE released tag.
|
|
23
24
|
#
|
|
24
25
|
# verify.js 6048195c0bcf7e2594a2d78e83cfc082ea615790
|
|
25
26
|
# arity.js 724aa9ae8171f1acf629c3c6b4879dc056575f20
|
|
26
|
-
# verify-grant.js
|
|
27
|
+
# verify-grant.js a472bf9073781cba25d83c7d1406b1a7c4af0b94
|
|
27
28
|
# verify-prove-transcript.js 724aa9ae8171f1acf629c3c6b4879dc056575f20
|
|
28
29
|
# verify-evidence.js 724aa9ae8171f1acf629c3c6b4879dc056575f20
|
|
29
30
|
# keys/coderifts-keys.json 724aa9ae8171f1acf629c3c6b4879dc056575f20
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
# ── THE RECEIPT-VERIFIER CORE IS PINNED TO A RELEASED TAG ───────────────────────────────────
|
|
33
|
+
#
|
|
34
|
+
# The shared verification core here comes from receipt-verifier **v1.0.0**, peeled commit
|
|
35
|
+
# 51a8224439959a5b46c0b09e9a2cd67117f05d56.
|
|
36
|
+
#
|
|
37
|
+
# It was pinned to a WORKING TREE and to per-file commits. Both are honest about where the bytes
|
|
38
|
+
# came from and neither is a provenance a stranger can resolve: a working tree is not fetchable,
|
|
39
|
+
# so "the vendored bytes match upstream" reduced to "they match whatever is on this machine right
|
|
40
|
+
# now". A released tag is a name anyone can check out — that is the difference between a pin and
|
|
41
|
+
# a note.
|
|
42
|
+
#
|
|
43
|
+
# The hash lines below are UNCHANGED by the re-pin. Where a file already matched the tag this is
|
|
44
|
+
# prose only; where it did not, it was re-copied from the tag and that is recorded below.
|
|
45
|
+
#
|
|
46
|
+
# source_commit: 51a8224439959a5b46c0b09e9a2cd67117f05d56 (receipt-verifier v1.0.0)
|
|
47
|
+
#
|
|
48
|
+
|
|
49
|
+
verify.js 348d3f851cd79bf63298b51da422d11be980820576ab0bf0a2990b9f007d6249
|
|
32
50
|
arity.js 721fcac58b960b5e2aa1af91edb6cfc87ebe828a6da73879967ef79a1ca474e8
|
|
33
|
-
verify-grant.js
|
|
51
|
+
verify-grant.js 4a0ad5bd7bb8282e086bea99dcaf8ca278370d80019c5681d2942bf11bc87504
|
|
34
52
|
verify-prove-transcript.js 3bb17bd4f16d5ee3cf3fbb754b4df9aa570a615ca94ff2a97b751c4fabd3bd0d
|
|
35
|
-
verify-evidence.js
|
|
53
|
+
verify-evidence.js 98cd9b989a25095e0442f91f68a1adecd6e6a2a5b05902bf2580b8cba973c5e2
|
|
54
|
+
verified-execution-binding.js 9d0259e916f2bcd32029744c3e9ec81448bcefa46e6ad3f054c8a650dc0482b7
|
|
36
55
|
keys/coderifts-keys.json bfe0c898cda3c3a4c8141726d0adb4cdb7eb9762de6264e430eff6469b90422e
|
|
56
|
+
evidence-root.js 4d5476fc44bbfc2a847feeb313a1c6c08ef7fd832a2348ef3c244bd8d3f03fcd
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* cr.evidence.root.v1 — ONE signed manifest that binds a set of tokens to ONE run.
|
|
5
|
+
*
|
|
6
|
+
* ── THE ATTACK THIS EXISTS FOR (1432) ───────────────────────────────────────────────────────
|
|
7
|
+
*
|
|
8
|
+
* 1423 made every token in a prove artifact authenticate against its issuer's key. That closed
|
|
9
|
+
* forgery and left something open, which the second auditor found and which was reproduced three
|
|
10
|
+
* ways before this file existed: take a REAL second run of the same producer, at the same commit,
|
|
11
|
+
* and move one of ITS tokens into the first run's artifact. Recompute the pin. Everything still
|
|
12
|
+
* verifies — because nothing was forged. Each token really was issued, really is signed, and
|
|
13
|
+
* really says what it says.
|
|
14
|
+
*
|
|
15
|
+
* transcript_token from run B → graded COVERED
|
|
16
|
+
* execution_grant from run B → graded COVERED
|
|
17
|
+
* chain_receipt from run B → graded COVERED
|
|
18
|
+
*
|
|
19
|
+
* Authenticity is a property of a TOKEN. "These tokens are one run" is a property of a SET, and no
|
|
20
|
+
* per-token signature can carry it. So a set needs its own signature.
|
|
21
|
+
*
|
|
22
|
+
* ── WHAT THE ROOT IS, AND WHAT IT IS NOT ────────────────────────────────────────────────────
|
|
23
|
+
*
|
|
24
|
+
* It is the producer saying, under its own key: "this run emitted exactly these bytes". The
|
|
25
|
+
* binding is the sha256 of each token's EXACT bytes, not a claim copied out of it — a substituted
|
|
26
|
+
* token has different bytes, so its digest cannot match, whatever it says inside.
|
|
27
|
+
*
|
|
28
|
+
* It is NOT a second opinion on any token's validity. A root cannot make an expired grant current
|
|
29
|
+
* or a bad signature good; every per-token check still runs and still decides. The root adds one
|
|
30
|
+
* sentence the others could not say.
|
|
31
|
+
*
|
|
32
|
+
* ── WHY IT IS SIGNED BY THE EXECUTOR ────────────────────────────────────────────────────────
|
|
33
|
+
*
|
|
34
|
+
* The executor is the only party present for the whole run: it mints the challenge, consumes the
|
|
35
|
+
* grant, seals the attestation and signs the transcript. The issuer sees one authorize; the
|
|
36
|
+
* provider sees one merge. A manifest of the run has to be signed by whoever witnessed the run,
|
|
37
|
+
* and that is the executor — the same key the correlation already uses, deliberately, so a reader
|
|
38
|
+
* verifying one is verifying the other's signer too.
|
|
39
|
+
*
|
|
40
|
+
* The residual is stated rather than hidden: this is the EXECUTOR's account of its own run. It
|
|
41
|
+
* makes splicing detectable, not impossible for the executor itself. An executor that lies about
|
|
42
|
+
* its own run was never constrained by its own signature — what the root removes is the ability of
|
|
43
|
+
* a THIRD party (anyone who can edit a vendored artifact) to assemble two honest runs into one.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
const crypto = require('node:crypto');
|
|
47
|
+
|
|
48
|
+
const ROOT_V = 'cr.evidence.root.v1';
|
|
49
|
+
const ROOT_SIGNING_PREFIX = 'crevidenceroot.v1';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The slots a root accounts for. MANDATORY ones must be present and non-null: a root that simply
|
|
53
|
+
* omits a token would let deletion — the strongest tamper there is — read as "not applicable".
|
|
54
|
+
*/
|
|
55
|
+
const SLOTS = Object.freeze({
|
|
56
|
+
chain_receipt: { mandatory: true },
|
|
57
|
+
execution_grant: { mandatory: true },
|
|
58
|
+
transcript_token: { mandatory: true },
|
|
59
|
+
correlation: { mandatory: true },
|
|
60
|
+
atomic_attestation: { mandatory: false },
|
|
61
|
+
provider_readback: { mandatory: false },
|
|
62
|
+
});
|
|
63
|
+
const SLOT_NAMES = Object.freeze(Object.keys(SLOTS));
|
|
64
|
+
|
|
65
|
+
const sha256pref = (bytes) =>
|
|
66
|
+
`sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`;
|
|
67
|
+
|
|
68
|
+
/** RFC 8785-shaped canonical JSON: sorted keys, no whitespace, no invented values. */
|
|
69
|
+
function canonicalJson(value) {
|
|
70
|
+
if (value === null) return 'null';
|
|
71
|
+
const t = typeof value;
|
|
72
|
+
if (t === 'boolean' || t === 'string') return JSON.stringify(value);
|
|
73
|
+
if (t === 'number') {
|
|
74
|
+
if (!Number.isFinite(value)) throw new TypeError('canonicalJson: non-finite number');
|
|
75
|
+
return JSON.stringify(value);
|
|
76
|
+
}
|
|
77
|
+
if (t === 'undefined') throw new TypeError('canonicalJson: undefined');
|
|
78
|
+
if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
|
|
79
|
+
const keys = Object.keys(value).sort();
|
|
80
|
+
return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalJson(value[k])}`).join(',')}}`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The exact bytes signed. The WHOLE body under canonical JSON, so every field is covered and a
|
|
85
|
+
* future field cannot be added without breaking the signature.
|
|
86
|
+
*/
|
|
87
|
+
function rootSigningInput(body) {
|
|
88
|
+
return `${ROOT_SIGNING_PREFIX}|${canonicalJson(body)}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Digest a token exactly as it travels.
|
|
93
|
+
*
|
|
94
|
+
* A STRING is hashed as utf8 bytes. An OBJECT — the correlation is one — is hashed as canonical
|
|
95
|
+
* JSON, so key order in a re-serialised artifact cannot change the digest of a value nobody
|
|
96
|
+
* edited. Absent is null, never the empty-string hash: `sha256('')` is a real value and would
|
|
97
|
+
* make "no token" indistinguishable from "a token that happens to be empty".
|
|
98
|
+
*/
|
|
99
|
+
function digestToken(token) {
|
|
100
|
+
if (token == null) return null;
|
|
101
|
+
if (typeof token === 'string') return token.length === 0 ? null : sha256pref(Buffer.from(token, 'utf8'));
|
|
102
|
+
return sha256pref(Buffer.from(canonicalJson(token), 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Build and sign a root.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} o
|
|
109
|
+
* @param {string} o.run_id
|
|
110
|
+
* @param {{name: string, version: string, commit: string|null}} o.producer
|
|
111
|
+
* @param {string} o.operation
|
|
112
|
+
* @param {string} o.target_uri
|
|
113
|
+
* @param {string|null} o.contract_commit
|
|
114
|
+
* @param {object} o.tokens slot name → the token as it travels (string or object)
|
|
115
|
+
* @param {object} o.claims { grant_id, receipt_hash, scope_hash, policy_hash, state_token_hash }
|
|
116
|
+
* @param {import('crypto').KeyObject} o.privateKey
|
|
117
|
+
* @param {string} o.executor_kid
|
|
118
|
+
*/
|
|
119
|
+
function buildEvidenceRoot(o) {
|
|
120
|
+
const artifact_digests = {};
|
|
121
|
+
for (const name of SLOT_NAMES) {
|
|
122
|
+
artifact_digests[name] = digestToken(o.tokens ? o.tokens[name] : null);
|
|
123
|
+
}
|
|
124
|
+
const c = o.claims || {};
|
|
125
|
+
const body = {
|
|
126
|
+
v: ROOT_V,
|
|
127
|
+
run_id: String(o.run_id),
|
|
128
|
+
executor_kid: String(o.executor_kid),
|
|
129
|
+
producer: {
|
|
130
|
+
name: String(o.producer.name),
|
|
131
|
+
version: String(o.producer.version),
|
|
132
|
+
commit: o.producer.commit == null ? null : String(o.producer.commit),
|
|
133
|
+
},
|
|
134
|
+
operation: o.operation == null ? null : String(o.operation),
|
|
135
|
+
target_uri: o.target_uri == null ? null : String(o.target_uri),
|
|
136
|
+
contract_commit: o.contract_commit == null ? null : String(o.contract_commit),
|
|
137
|
+
artifact_digests,
|
|
138
|
+
// The CLAIMS the run asserts. They are ALSO inside the tokens; carrying them here is what
|
|
139
|
+
// lets a verifier compare the two and refuse a manifest that agrees with itself but not with
|
|
140
|
+
// its own evidence.
|
|
141
|
+
grant_id: c.grant_id == null ? null : String(c.grant_id),
|
|
142
|
+
receipt_hash: c.receipt_hash == null ? null : String(c.receipt_hash),
|
|
143
|
+
scope_hash: c.scope_hash == null ? null : String(c.scope_hash),
|
|
144
|
+
policy_hash: c.policy_hash == null ? null : String(c.policy_hash),
|
|
145
|
+
state_token_hash: c.state_token_hash == null ? null : String(c.state_token_hash),
|
|
146
|
+
};
|
|
147
|
+
const signature = crypto.sign(null, Buffer.from(rootSigningInput(body), 'utf8'), o.privateKey);
|
|
148
|
+
return { ...body, signature: signature.toString('base64url') };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Verify a root's own signature. Says nothing about the tokens — that is verifyEvidenceEnvelope. */
|
|
152
|
+
function verifyEvidenceRoot(root, publicKey) {
|
|
153
|
+
if (!root || root.v !== ROOT_V) {
|
|
154
|
+
return { valid: false, status: 'ROOT_MALFORMED', reason: 'not_an_evidence_root' };
|
|
155
|
+
}
|
|
156
|
+
if (typeof root.signature !== 'string' || root.signature.length === 0) {
|
|
157
|
+
return { valid: false, status: 'ROOT_MALFORMED', reason: 'no_signature' };
|
|
158
|
+
}
|
|
159
|
+
if (!publicKey) {
|
|
160
|
+
return { valid: false, status: 'ROOT_UNKNOWN_KEY', reason: 'unknown_kid' };
|
|
161
|
+
}
|
|
162
|
+
const { signature, ...body } = root;
|
|
163
|
+
let ok = false;
|
|
164
|
+
try {
|
|
165
|
+
ok = crypto.verify(
|
|
166
|
+
null, Buffer.from(rootSigningInput(body), 'utf8'), publicKey,
|
|
167
|
+
Buffer.from(signature, 'base64url'),
|
|
168
|
+
);
|
|
169
|
+
} catch (_) {
|
|
170
|
+
return { valid: false, status: 'ROOT_INVALID_SIGNATURE', reason: 'signature_error' };
|
|
171
|
+
}
|
|
172
|
+
return ok
|
|
173
|
+
? { valid: true, status: 'ROOT_VALID' }
|
|
174
|
+
: { valid: false, status: 'ROOT_INVALID_SIGNATURE', reason: 'signature_mismatch' };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
ROOT_V,
|
|
179
|
+
ROOT_SIGNING_PREFIX,
|
|
180
|
+
SLOTS,
|
|
181
|
+
SLOT_NAMES,
|
|
182
|
+
canonicalJson,
|
|
183
|
+
rootSigningInput,
|
|
184
|
+
digestToken,
|
|
185
|
+
buildEvidenceRoot,
|
|
186
|
+
verifyEvidenceRoot,
|
|
187
|
+
sha256pref,
|
|
188
|
+
};
|