@optitransfer/proofline 0.2.0-next.0 → 0.2.0-next.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/CHANGELOG.md +21 -1
- package/README.md +95 -2
- package/bin/proofline.mjs +206 -11
- package/docs/receipt.md +25 -0
- package/package.json +4 -1
- package/src/checks.mjs +12 -7
- package/src/contract.mjs +242 -1
- package/src/engine.mjs +77 -28
- package/src/guided.mjs +240 -0
- package/src/hook-stop.mjs +105 -0
- package/src/index.mjs +243 -15
- package/src/infer.mjs +173 -0
- package/src/inference.json +20 -0
- package/src/isolate.mjs +123 -0
- package/src/mcp.mjs +20 -5
- package/src/receipt.mjs +17 -0
- package/src/signature.mjs +110 -0
- package/src/vault.mjs +109 -0
- package/src/verify-receipt.mjs +109 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// Verify a receipt envelope. Offline unless --sigstore is asked for, and that import is lazy.
|
|
2
|
+
import crypto from 'node:crypto';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { receiptZeroConfig } from './infer.mjs';
|
|
6
|
+
|
|
7
|
+
const STATEMENT = 'https://in-toto.io/Statement/v1';
|
|
8
|
+
const PAYLOAD_TYPE = 'application/vnd.in-toto+json';
|
|
9
|
+
const PREDICATE_TYPE = 'https://proofline.dev/receipt/v1';
|
|
10
|
+
|
|
11
|
+
export function pae(payloadType, payload) {
|
|
12
|
+
const t = Buffer.from(payloadType);
|
|
13
|
+
const b = Buffer.isBuffer(payload) ? payload : Buffer.from(payload);
|
|
14
|
+
return Buffer.concat([Buffer.from(`DSSEv1 ${t.length} `), t, Buffer.from(` ${b.length} `), b]);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function fromB64(s) {
|
|
18
|
+
if (typeof s !== 'string' || !/^[A-Za-z0-9+/_=-]+$/.test(s)) return null;
|
|
19
|
+
return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadKeys(dir) {
|
|
23
|
+
const out = new Map();
|
|
24
|
+
if (!dir || !fs.existsSync(dir)) return out;
|
|
25
|
+
for (const name of fs.readdirSync(dir)) {
|
|
26
|
+
if (!name.endsWith('.pem')) continue;
|
|
27
|
+
const pem = fs.readFileSync(path.join(dir, name), 'utf8');
|
|
28
|
+
out.set(name.slice(0, -4), crypto.createPublicKey(pem));
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function schemaProblem(statement) {
|
|
34
|
+
if (!statement || statement._type !== STATEMENT) return 'statement _type';
|
|
35
|
+
if (statement.predicateType !== PREDICATE_TYPE) return 'predicateType';
|
|
36
|
+
const p = statement.predicate;
|
|
37
|
+
if (!p || typeof p !== 'object') return 'predicate';
|
|
38
|
+
if (!['L0', 'L1', 'L2'].includes(p.level)) return 'predicate.level';
|
|
39
|
+
if (p.agent != null && typeof p.agent !== 'object') return 'predicate.agent';
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function result(exitCode, message, extra = {}) {
|
|
44
|
+
return { exitCode, message, differences: extra.differences || [], level: extra.level || null, ...extra };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function verifySignatures(envelope, keys) {
|
|
48
|
+
if (!envelope || typeof envelope.payloadType !== 'string' || typeof envelope.payload !== 'string' || !Array.isArray(envelope.signatures) || !envelope.signatures.length) {
|
|
49
|
+
return { problem: 'schema', detail: 'a DSSE envelope needs payloadType, payload and one signature' };
|
|
50
|
+
}
|
|
51
|
+
const body = fromB64(envelope.payload);
|
|
52
|
+
if (!body) return { problem: 'schema', detail: 'payload is not base64' };
|
|
53
|
+
const data = pae(envelope.payloadType, body);
|
|
54
|
+
let verified = 0;
|
|
55
|
+
let unknown = 0;
|
|
56
|
+
let failed = 0;
|
|
57
|
+
for (const s of envelope.signatures) {
|
|
58
|
+
const id = typeof s?.keyid === 'string' ? s.keyid : '';
|
|
59
|
+
const sig = fromB64(s?.sig || '');
|
|
60
|
+
const key = keys.get(id);
|
|
61
|
+
if (!key || !sig) { unknown++; continue; }
|
|
62
|
+
const ok = crypto.verify(null, data, key, sig);
|
|
63
|
+
if (ok) verified++;
|
|
64
|
+
else failed++;
|
|
65
|
+
}
|
|
66
|
+
return { body, verified, unknown, failed };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* @param {string} envelopePath
|
|
71
|
+
* @param {{ root?: string, keys?: string, sigstore?: boolean, recompute?: boolean, base?: string, suite?: boolean }} [opts]
|
|
72
|
+
*/
|
|
73
|
+
export async function verifyReceipt(envelopePath, opts = {}) {
|
|
74
|
+
if (!envelopePath) return result(3, 'usage: verify-receipt <envelope>');
|
|
75
|
+
let raw;
|
|
76
|
+
try { raw = JSON.parse(fs.readFileSync(envelopePath, 'utf8')); }
|
|
77
|
+
catch { return result(1, 'envelope is not JSON'); }
|
|
78
|
+
if (opts.sigstore) return verifySigstore(raw);
|
|
79
|
+
const keys = loadKeys(opts.keys || (process.env.AGENTKEY_HOME ? path.join(process.env.AGENTKEY_HOME, 'keys') : ''));
|
|
80
|
+
const sig = verifySignatures(raw, keys);
|
|
81
|
+
if (sig.problem) return result(1, sig.detail);
|
|
82
|
+
if (sig.failed) return result(1, 'signature failed');
|
|
83
|
+
if (!sig.verified) return result(2, 'unknown key');
|
|
84
|
+
let statement;
|
|
85
|
+
try { statement = JSON.parse(sig.body.toString('utf8')); }
|
|
86
|
+
catch { return result(1, 'payload is not a statement'); }
|
|
87
|
+
if (raw.payloadType !== PAYLOAD_TYPE) return result(1, 'payloadType is not an in-toto statement');
|
|
88
|
+
const schema = schemaProblem(statement);
|
|
89
|
+
if (schema) return result(1, `schema: ${schema}`);
|
|
90
|
+
const level = statement.predicate.level;
|
|
91
|
+
if (!opts.recompute) return result(0, `verified\nlevel ${level}`, { level });
|
|
92
|
+
if (!opts.base) return result(3, '--recompute needs --base');
|
|
93
|
+
const live = receiptZeroConfig(opts.root || process.cwd(), { base: opts.base });
|
|
94
|
+
const recomputed = live.exitCode === 0 && live.level === 'L1' && live.suites.every((s) => s.status === 'pass');
|
|
95
|
+
const differences = [];
|
|
96
|
+
if (Boolean(statement.predicate.green_on_head) !== recomputed) {
|
|
97
|
+
differences.push(`green_on_head: envelope ${Boolean(statement.predicate.green_on_head)} recomputed ${recomputed}`);
|
|
98
|
+
}
|
|
99
|
+
if (differences.length) return result(1, differences.join('\n'), { level, differences });
|
|
100
|
+
return result(0, `verified\nlevel ${level}`, { level });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function verifySigstore(bundle) {
|
|
104
|
+
const sigs = bundle?.verificationMaterial?.tlogEntries || bundle?.signatures || [];
|
|
105
|
+
if (sigs.length !== 1) return result(1, 'a sigstore bundle carries exactly one signature');
|
|
106
|
+
try { await import('sigstore'); }
|
|
107
|
+
catch { return result(2, 'sigstore verifier is not available'); }
|
|
108
|
+
return result(2, 'sigstore verification is not confirmed offline');
|
|
109
|
+
}
|