@sanning/proof 0.18.0 → 0.19.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/README.md +2 -2
- package/dist/case-file.js +2 -1
- package/dist/case.d.ts +20 -4
- package/dist/case.js +56 -14
- package/dist/chain.d.ts +28 -0
- package/dist/chain.js +61 -4
- package/dist/cli.js +227 -34
- package/dist/declaration.d.ts +43 -4
- package/dist/declaration.js +198 -14
- package/dist/environment.d.ts +42 -0
- package/dist/environment.js +136 -0
- package/dist/evidence.d.ts +6 -0
- package/dist/evidence.js +32 -1
- package/dist/exit-codes.d.ts +18 -0
- package/dist/exit-codes.js +25 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.js +21 -5
- package/dist/run-adapters.d.ts +10 -0
- package/dist/run-adapters.js +35 -0
- package/dist/run.d.ts +209 -0
- package/dist/run.js +1405 -0
- package/dist/verifier.d.ts +43 -0
- package/dist/verifier.js +104 -1
- package/dist/witness-lookup.d.ts +13 -1
- package/dist/witness-lookup.js +12 -2
- package/package.json +1 -1
- package/src/case-file.ts +2 -1
- package/src/case.ts +63 -16
- package/src/chain.ts +81 -4
- package/src/cli.ts +260 -29
- package/src/declaration.ts +242 -12
- package/src/environment.ts +145 -0
- package/src/evidence.ts +29 -1
- package/src/exit-codes.ts +28 -0
- package/src/index.ts +31 -1
- package/src/run-adapters.ts +44 -0
- package/src/run.ts +1598 -0
- package/src/verifier.ts +94 -1
- package/src/witness-lookup.ts +18 -1
- package/vectors/README.md +37 -0
- package/vectors/sandbox-evidence.json +3186 -0
package/dist/cli.js
CHANGED
|
@@ -17,6 +17,8 @@
|
|
|
17
17
|
// could not be reached reads `partial`; a pack whose records nobody hashed
|
|
18
18
|
// reads `incomplete` (see below)
|
|
19
19
|
// 4 mixed
|
|
20
|
+
// 5 sandbox: a record is signed environment dev; would be 0, or only a
|
|
21
|
+
// simulated witness is unchecked (`sandboxExit`)
|
|
20
22
|
// `sanningd verify-status` in sanning-io/agent mirrored the 0.12 table, which
|
|
21
23
|
// exited 0 on `partial`; it moves with this one.
|
|
22
24
|
//
|
|
@@ -47,13 +49,15 @@ import { createHash, randomBytes } from "node:crypto";
|
|
|
47
49
|
import { basename, resolve, sep } from "node:path";
|
|
48
50
|
import { fileURLToPath } from "node:url";
|
|
49
51
|
import { isAgentProofSpec, verifyAgentProof } from "./agent-proof.js";
|
|
50
|
-
import { CHAIN_READING_SCOPE, readChains, renderChainFinding } from "./chain.js";
|
|
52
|
+
import { CHAIN_READING_SCOPE, readChainLinks, readChains, renderChainFinding } from "./chain.js";
|
|
51
53
|
import { VERDICT_DISPLAY_TITLES, caseReliance, readCase, renderCaseNote, renderCaseSummary, renderException, renderWitnessLine, } from "./case.js";
|
|
52
54
|
import { CASE_FILE_INDEX_NAME, caseFileListedPaths, jsonDepthExceeds, readCaseFileIndex, renderCaseFileFinding, unreadableCaseFileIndex, } from "./case-file.js";
|
|
53
55
|
import { MAX_ZIP_ARCHIVE_BYTES, MAX_ZIP_BYTES, ZipReadError, readZip } from "./zip-reader.js";
|
|
54
56
|
import { composeExport } from "./compose.js";
|
|
55
57
|
import { isHexField, sha256Hex, utf8 } from "./crypto.js";
|
|
56
58
|
import { verifyEvidenceBundle } from "./evidence.js";
|
|
59
|
+
import { readRuns, renderAccountLine, renderEnclaveLines } from "./run.js";
|
|
60
|
+
import { loadInstalledAdapters } from "./run-adapters.js";
|
|
57
61
|
import { DuplicateKeyError, parseRefusingDuplicates } from "./verifier.js";
|
|
58
62
|
import { fileMatchToJson, matchCaseFiles, matchFiles, notAccountedFor, renderFileMatch, } from "./file-match.js";
|
|
59
63
|
import { WITNESS_CACHE_NAME, readWitnessCache, } from "./witness-cache.js";
|
|
@@ -65,7 +69,8 @@ import { WITNESS_CACHE_NAME, readWitnessCache, } from "./witness-cache.js";
|
|
|
65
69
|
import { onChainHalf, witnessLine } from "./witness-lookup.js";
|
|
66
70
|
import { checkDeclaration, renderDeclarationSummary, renderNote } from "./declaration.js";
|
|
67
71
|
import { completenessNotes, gradePack, renderCause } from "./grading.js";
|
|
68
|
-
import { EXIT_CODES_HELP, EXIT_FAILED, EXIT_MALFORMED, EXIT_UNDETERMINED, EXIT_VERIFIED, exitCodeFor, } from "./exit-codes.js";
|
|
72
|
+
import { EXIT_CODES_HELP, EXIT_FAILED, EXIT_MALFORMED, EXIT_SANDBOX, EXIT_UNDETERMINED, EXIT_VERIFIED, exitCodeFor, sandboxExit, } from "./exit-codes.js";
|
|
73
|
+
import { environmentMarker, onlySimulatedUnsettled, simulatedOnlyRun, SANDBOX_HEADLINE, SANDBOX_LINE, SANDBOX_TITLE, } from "./environment.js";
|
|
69
74
|
// ── What a person reads is escaped at one choke point ──
|
|
70
75
|
//
|
|
71
76
|
// Every human-readable line this command prints passes through `guard`, which
|
|
@@ -326,7 +331,7 @@ async function runVerify(rest, io, sink) {
|
|
|
326
331
|
gateways,
|
|
327
332
|
...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}),
|
|
328
333
|
});
|
|
329
|
-
return reportAgentProof(result, bundlePath, gateways, io, sink);
|
|
334
|
+
return reportAgentProof(result, bundlePath, gateways, io, sink, parsed);
|
|
330
335
|
}
|
|
331
336
|
// Default: treat anything else as an evidence bundle. verifyEvidenceBundle
|
|
332
337
|
// rejects an unknown major as "malformed" so a genuinely unknown shape still
|
|
@@ -338,8 +343,11 @@ async function runVerify(rest, io, sink) {
|
|
|
338
343
|
});
|
|
339
344
|
// Read the chains from the SAME parsed bundle the verifier saw. Findings are
|
|
340
345
|
// evidence about the records in front of us, not a verdict: they are printed
|
|
341
|
-
// and they
|
|
342
|
-
//
|
|
346
|
+
// and they move `exit` only through the pack's own declaration, where a v3
|
|
347
|
+
// declaration claims a chain complete and in order and a finding under that
|
|
348
|
+
// claim contradicts it (rule 11). Without a claim a chain is not a
|
|
349
|
+
// completeness proof, and an exit code is exactly the kind of thing people
|
|
350
|
+
// read as one.
|
|
343
351
|
//
|
|
344
352
|
// A READING THAT COULD NOT BE PRODUCED SAYS SO. This was an unguarded call:
|
|
345
353
|
// a throw out of `readChains` would have escaped the command entirely and
|
|
@@ -355,7 +363,17 @@ async function runVerify(rest, io, sink) {
|
|
|
355
363
|
catch (e) {
|
|
356
364
|
chains = { unreadable: errMsg(e) };
|
|
357
365
|
}
|
|
358
|
-
|
|
366
|
+
// The links rule 11 holds a claimed chain to (evidence-bundle.md 5.4.2), from
|
|
367
|
+
// the same parsed bundle. Links that cannot be read are none, which reads a
|
|
368
|
+
// claim UNKNOWN and never clean.
|
|
369
|
+
let links;
|
|
370
|
+
try {
|
|
371
|
+
links = "unreadable" in chains ? null : await readChainLinks(parsed);
|
|
372
|
+
}
|
|
373
|
+
catch {
|
|
374
|
+
links = null;
|
|
375
|
+
}
|
|
376
|
+
const exit = reportEvidence(result, bundlePath, gateways, io, chains, links, parsed, logs, sink);
|
|
359
377
|
// Files handed in beside the pack (evidence-bundle.md 5.7, A29): findings
|
|
360
378
|
// beside the verdict, never inside it, so `exit` is untouched.
|
|
361
379
|
if (offered !== undefined) {
|
|
@@ -1234,7 +1252,7 @@ function cliDisclosureAccount(result, logs) {
|
|
|
1234
1252
|
extra: (logs?.suppliedIds ?? []).filter((id) => !known.has(id)),
|
|
1235
1253
|
};
|
|
1236
1254
|
}
|
|
1237
|
-
function reportEvidence(result, path, gateways, io, chains, parsed, logs, sink) {
|
|
1255
|
+
function reportEvidence(result, path, gateways, io, chains, links, parsed, logs, sink) {
|
|
1238
1256
|
if (result.status === "malformed") {
|
|
1239
1257
|
if (sink)
|
|
1240
1258
|
sink.data = verifyJson(result, null, null, null);
|
|
@@ -1324,10 +1342,24 @@ function reportEvidence(result, path, gateways, io, chains, parsed, logs, sink)
|
|
|
1324
1342
|
// their verdicts rather than a new check. A pack that DOES
|
|
1325
1343
|
// carry a declaration this reader cannot read is a different case and exits
|
|
1326
1344
|
// 3: the holder chose that field, so UNKNOWN is the honest answer.
|
|
1327
|
-
|
|
1345
|
+
//
|
|
1346
|
+
// A v3 declaration that claims a chain is checked against this command's
|
|
1347
|
+
// own chain links (rule 11, evidence-bundle.md 5.4.2 v1.36, A45). Links
|
|
1348
|
+
// that could not be read are passed as none, which reads the claim UNKNOWN
|
|
1349
|
+
// and never clean.
|
|
1350
|
+
const declaration = checkDeclaration(parsed, logs?.content, links);
|
|
1328
1351
|
const grade = gradePack(result, cliDisclosureAccount(result, logs), completenessNotes(parsed), declaration);
|
|
1352
|
+
// Sandbox evidence: read from the signed envelopes the pack holds.
|
|
1353
|
+
const marker = environmentMarker([parsed]);
|
|
1354
|
+
// A pack whose records are all signed `dev`, undetermined only because a
|
|
1355
|
+
// simulated witness cannot be looked up, would grade `verified` with that
|
|
1356
|
+
// witness accepted: it is sandbox evidence, exit 5, never 3.
|
|
1357
|
+
const simulatedOnly = grade.verdict === "partial" &&
|
|
1358
|
+
onlySimulatedUnsettled(result) &&
|
|
1359
|
+
gradePack({ ...result, status: "verified" }, cliDisclosureAccount(result, logs), completenessNotes(parsed), declaration).verdict === "verified";
|
|
1360
|
+
const exit = sandboxExit(mapGradeToExit(grade), marker, simulatedOnly);
|
|
1329
1361
|
io.out("");
|
|
1330
|
-
printRollup(grade.verdict, result.status, result.assertedStatus, gateways, result.onChainChecked, io);
|
|
1362
|
+
printRollup(grade.verdict, result.status, result.assertedStatus, gateways, result.onChainChecked, io, marker, exit === EXIT_SANDBOX, simulatedOnlyRun(result, gateways));
|
|
1331
1363
|
io.out(paint(` declaration: ${renderDeclarationSummary(declaration)}`, declaration.verdict === "failed" ? R : declaration.verdict === "verified" ? DIM : Y));
|
|
1332
1364
|
for (const n of declaration.notes)
|
|
1333
1365
|
io.out(paint(` note: ${renderNote(n)}`, Y));
|
|
@@ -1346,15 +1378,15 @@ function reportEvidence(result, path, gateways, io, chains, parsed, logs, sink)
|
|
|
1346
1378
|
io.out(paint(` logs: ${grade.recordsVerified}/${result.events.length} record(s) content-verified`, DIM));
|
|
1347
1379
|
}
|
|
1348
1380
|
if (sink)
|
|
1349
|
-
sink.data = verifyJson(result, grade, declaration, chains);
|
|
1350
|
-
return
|
|
1381
|
+
sink.data = verifyJson(result, grade, declaration, chains, marker);
|
|
1382
|
+
return exit;
|
|
1351
1383
|
}
|
|
1352
1384
|
/**
|
|
1353
1385
|
* What `verify --json` prints for an evidence bundle, the same keys and the
|
|
1354
1386
|
* same words as `_verify_json` in the Python twin. `grade`, `declaration` and
|
|
1355
1387
|
* `chains` are null for a bundle no verdict could be rendered over.
|
|
1356
1388
|
*/
|
|
1357
|
-
function verifyJson(result, grade, declaration, chains) {
|
|
1389
|
+
function verifyJson(result, grade, declaration, chains, marker = null) {
|
|
1358
1390
|
let chainBlock = null;
|
|
1359
1391
|
if (chains !== null) {
|
|
1360
1392
|
chainBlock =
|
|
@@ -1380,6 +1412,9 @@ function verifyJson(result, grade, declaration, chains) {
|
|
|
1380
1412
|
? { presented: result.events.length, content_verified: grade.recordsVerified }
|
|
1381
1413
|
: null,
|
|
1382
1414
|
chains: chainBlock,
|
|
1415
|
+
// Present only where the pack holds an envelope signed `dev`
|
|
1416
|
+
// (envelope-spec §2 MAY), so a production pack's JSON is unchanged.
|
|
1417
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
1383
1418
|
};
|
|
1384
1419
|
}
|
|
1385
1420
|
/**
|
|
@@ -1440,7 +1475,8 @@ function reportExport(exp, io) {
|
|
|
1440
1475
|
}
|
|
1441
1476
|
}
|
|
1442
1477
|
}
|
|
1443
|
-
function reportAgentProof(result, path, gateways, io, sink) {
|
|
1478
|
+
function reportAgentProof(result, path, gateways, io, sink, parsed) {
|
|
1479
|
+
const marker = result.status === "malformed" ? null : environmentMarker([parsed]);
|
|
1444
1480
|
if (sink) {
|
|
1445
1481
|
sink.data = {
|
|
1446
1482
|
verdict: result.status,
|
|
@@ -1448,6 +1484,7 @@ function reportAgentProof(result, path, gateways, io, sink) {
|
|
|
1448
1484
|
spec_version: "sanning.agent.proof/v1",
|
|
1449
1485
|
on_chain_checked: result.onChainChecked,
|
|
1450
1486
|
notes: [...result.errors],
|
|
1487
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
1451
1488
|
};
|
|
1452
1489
|
}
|
|
1453
1490
|
if (result.status === "malformed") {
|
|
@@ -1464,17 +1501,30 @@ function reportAgentProof(result, path, gateways, io, sink) {
|
|
|
1464
1501
|
io.out("");
|
|
1465
1502
|
// An agent inclusion proof carries no records and no disclosed bytes, so
|
|
1466
1503
|
// there is no pack to grade: the bundle status IS the headline here.
|
|
1467
|
-
|
|
1504
|
+
const exit = sandboxExit(exitCodeFor(result.status), marker);
|
|
1505
|
+
printRollup(result.status, result.status, null, gateways, result.onChainChecked, io, marker, exit === EXIT_SANDBOX);
|
|
1468
1506
|
for (const e of result.errors)
|
|
1469
1507
|
io.out(paint(` note: ${e}`, DIM));
|
|
1470
|
-
return
|
|
1508
|
+
return exit;
|
|
1471
1509
|
}
|
|
1472
|
-
function printRollup(headline, bundleStatus, asserted, gateways, onChainChecked, io) {
|
|
1510
|
+
function printRollup(headline, bundleStatus, asserted, gateways, onChainChecked, io, marker = null, sandbox = false, simulatedOnly = false) {
|
|
1511
|
+
// Three scopes, not two. A run with gateways whose only witnesses are
|
|
1512
|
+
// simulated looked nothing up (`simulatedOnlyRun`): saying "on-chain" there
|
|
1513
|
+
// claims a check that never happened, and saying "no gateway re-fetch
|
|
1514
|
+
// requested" denies the flag the reader passed. Every other run that is not
|
|
1515
|
+
// counted as checked on a chain, a run that stopped early among them, keeps
|
|
1516
|
+
// the second scope as before.
|
|
1473
1517
|
const scope = onChainChecked
|
|
1474
1518
|
? `verified offline + on-chain against ${gateways.length} gateway(s)`
|
|
1475
|
-
:
|
|
1519
|
+
: simulatedOnly
|
|
1520
|
+
? "verified offline; a simulated witness is not on a chain, so nothing was looked up"
|
|
1521
|
+
: "verified offline (no gateway re-fetch requested)";
|
|
1476
1522
|
let head;
|
|
1477
|
-
switch (headline) {
|
|
1523
|
+
switch (sandbox ? "sandbox" : headline) {
|
|
1524
|
+
// Sandbox evidence (exit 5) never prints VERIFIED.
|
|
1525
|
+
case "sandbox":
|
|
1526
|
+
head = paint(SANDBOX_HEADLINE, Y);
|
|
1527
|
+
break;
|
|
1478
1528
|
case "verified":
|
|
1479
1529
|
head = paint("VERIFIED", G);
|
|
1480
1530
|
break;
|
|
@@ -1492,6 +1542,8 @@ function printRollup(headline, bundleStatus, asserted, gateways, onChainChecked,
|
|
|
1492
1542
|
head = paint(headline.toUpperCase(), R);
|
|
1493
1543
|
}
|
|
1494
1544
|
io.out(`${head} ${paint(scope, DIM)}`);
|
|
1545
|
+
if (marker !== null)
|
|
1546
|
+
io.out(paint(` ${SANDBOX_LINE}`, Y));
|
|
1495
1547
|
// Both verdicts, whenever they differ, because they answer different
|
|
1496
1548
|
// questions: §3 grades the signed bundle, the pack verdict grades the bundle
|
|
1497
1549
|
// plus whatever bytes travelled beside it. A pack can be incomplete while the
|
|
@@ -2072,7 +2124,7 @@ async function unzip(arg, prefix, tree, seen) {
|
|
|
2072
2124
|
// 5.5.4 v1.16, A17), so it reads `failed` and exits 1.
|
|
2073
2125
|
export const CASE_USAGE = `sanning-proof case -- a case reading: one verdict over one pack or many, offline
|
|
2074
2126
|
|
|
2075
|
-
sanning-proof case <folder or case file>... [--gateways <url,url>] [--files <path>...] [--json]
|
|
2127
|
+
sanning-proof case <folder or case file>... [--gateways <url,url>] [--files <path>...] [--enclave-policy <file>] [--enclave-rpc <url>] [--json]
|
|
2076
2128
|
|
|
2077
2129
|
<folder> a pack folder (bundle.json plus logs/), a folder that holds
|
|
2078
2130
|
several packs, or an unzipped case file. Packs are found by the
|
|
@@ -2110,6 +2162,17 @@ export const CASE_USAGE = `sanning-proof case -- a case reading: one verdict ove
|
|
|
2110
2162
|
contribution, the packs, each with its index, the files outside
|
|
2111
2163
|
them, the case file index comparison, the witness line and the
|
|
2112
2164
|
on-chain half.
|
|
2165
|
+
--enclave-policy
|
|
2166
|
+
a sanning.enclave-policy/v1 file: what this reader accepts of
|
|
2167
|
+
an enclave attestation. Where the packs hold a runner's run, an
|
|
2168
|
+
enclave attestation line and a run-account line print beside
|
|
2169
|
+
the witness line (DRAFT); neither moves the verdict or the exit
|
|
2170
|
+
code. Without a policy, or without an adapter for the provider
|
|
2171
|
+
installed, the line reads not_checked.
|
|
2172
|
+
--enclave-rpc
|
|
2173
|
+
a chain JSON-RPC endpoint: a flare-fcc run also gets the online
|
|
2174
|
+
line, read from the contracts the policy names. It moves
|
|
2175
|
+
nothing either.
|
|
2113
2176
|
|
|
2114
2177
|
Files outside every pack are matched too, always, so
|
|
2115
2178
|
sanning-proof case audit-file.zip checks a whole download in one command: each
|
|
@@ -2184,12 +2247,21 @@ const titleOf = (word) => Object.prototype.hasOwnProperty.call(VERDICT_DISPLAY_T
|
|
|
2184
2247
|
* scope sentence and the witness line are printed with it every time.
|
|
2185
2248
|
*/
|
|
2186
2249
|
function caseReport(data) {
|
|
2187
|
-
const lines = [
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
` ${
|
|
2191
|
-
|
|
2192
|
-
|
|
2250
|
+
const lines = [data.title];
|
|
2251
|
+
// Sandbox evidence: printed with every headline the marker is on.
|
|
2252
|
+
if (data.environment_marker !== undefined)
|
|
2253
|
+
lines.push(` ${SANDBOX_LINE}`);
|
|
2254
|
+
lines.push(` ${data.summary}`, ` ${data.witness.rendered}`);
|
|
2255
|
+
// Where the packs hold a runner's run: its enclave attestation line and its
|
|
2256
|
+
// run-account line, beside the witness line.
|
|
2257
|
+
const enclave = data.enclave_attestation ?? [];
|
|
2258
|
+
const account = data.run_account ?? [];
|
|
2259
|
+
for (let i = 0; i < Math.min(enclave.length, account.length); i++) {
|
|
2260
|
+
for (const line of enclave[i].rendered)
|
|
2261
|
+
lines.push(` ${line}`);
|
|
2262
|
+
lines.push(` ${account[i].rendered}`);
|
|
2263
|
+
}
|
|
2264
|
+
lines.push(` ${data.reliance}`);
|
|
2193
2265
|
for (const e of data.exceptions)
|
|
2194
2266
|
lines.push(` - ${e.rendered}`);
|
|
2195
2267
|
for (const n of data.notes)
|
|
@@ -2197,7 +2269,11 @@ function caseReport(data) {
|
|
|
2197
2269
|
lines.push(`packs ${data.packs.length}`);
|
|
2198
2270
|
for (const p of data.packs) {
|
|
2199
2271
|
const key = p.signing_key !== null ? p.signing_key : "a key this reader cannot read";
|
|
2200
|
-
const grade = p.grade
|
|
2272
|
+
const grade = p.grade === null
|
|
2273
|
+
? "not graded"
|
|
2274
|
+
: p.grade === "verified" && p.environment_marker !== undefined
|
|
2275
|
+
? SANDBOX_TITLE
|
|
2276
|
+
: titleOf(p.grade);
|
|
2201
2277
|
// Each record's status, counted per pack (A43): the JSON lists every one.
|
|
2202
2278
|
const mine = data.records.filter((r) => r.pack_index === p.index);
|
|
2203
2279
|
const count = (status) => mine.filter((r) => r.status === status).length;
|
|
@@ -2235,13 +2311,23 @@ function caseReport(data) {
|
|
|
2235
2311
|
}
|
|
2236
2312
|
const oc = data.on_chain;
|
|
2237
2313
|
if (oc.checked) {
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2314
|
+
// A drop whose only witnesses are simulated had nothing looked up on a
|
|
2315
|
+
// chain, and the header says so rather than "looked up against".
|
|
2316
|
+
const simulated = oc.packs.reduce((n, r) => n + (r.simulated ?? 0), 0);
|
|
2317
|
+
const onlySimulated = simulated > 0 &&
|
|
2318
|
+
oc.packs.every((r) => (r.simulated ?? 0) === r.witnesses && (r.witness_times ?? []).length === 0);
|
|
2319
|
+
lines.push(onlySimulated
|
|
2320
|
+
? "on-chain nothing looked up: every witness here is simulated, " +
|
|
2321
|
+
"which is not on a chain; the reading above is offline and this " +
|
|
2322
|
+
"does not change it"
|
|
2323
|
+
: `on-chain looked up against ${oc.gateways.length} gateway(s), ` +
|
|
2324
|
+
"pack by pack; the reading above is offline and this does not " +
|
|
2325
|
+
"change it");
|
|
2241
2326
|
for (const r of oc.packs) {
|
|
2327
|
+
const sim = r.simulated ? `, ${r.simulated} of them simulated (not on a chain)` : "";
|
|
2242
2328
|
lines.push(` ${r.ref} ${r.witnesses} witness(es): ${r.confirmed} ` +
|
|
2243
2329
|
`confirmed, ${r.mismatched} mismatched, ${r.unreachable} ` +
|
|
2244
|
-
`unreachable, ${r.not_checked} not checked`);
|
|
2330
|
+
`unreachable, ${r.not_checked} not checked${sim}`);
|
|
2245
2331
|
}
|
|
2246
2332
|
}
|
|
2247
2333
|
return lines;
|
|
@@ -2333,6 +2419,12 @@ export async function caseFileReadings(tree, whole, cap = MAX_FOLDER_BYTES) {
|
|
|
2333
2419
|
}
|
|
2334
2420
|
return out;
|
|
2335
2421
|
}
|
|
2422
|
+
// One pack's environment marker, as the key its JSON row carries only where
|
|
2423
|
+
// the pack holds an envelope signed `dev`.
|
|
2424
|
+
function packMarker(bundle) {
|
|
2425
|
+
const m = environmentMarker([bundle]);
|
|
2426
|
+
return m !== null ? { environment_marker: m } : {};
|
|
2427
|
+
}
|
|
2336
2428
|
function caseError(e, asJson, io) {
|
|
2337
2429
|
if (asJson)
|
|
2338
2430
|
outJson(io, jsonText({ error: e.message, exit_code: EXIT_MALFORMED, hint: e.hint }));
|
|
@@ -2417,11 +2509,69 @@ export async function looseFiles(tree, split, cap = MAX_FOLDER_BYTES) {
|
|
|
2417
2509
|
}
|
|
2418
2510
|
return { offered, skipped };
|
|
2419
2511
|
}
|
|
2512
|
+
// A policy file that is not JSON: read, and unreadable (`policy_unreadable`).
|
|
2513
|
+
const POLICY_NOT_JSON = Symbol("policy not JSON");
|
|
2514
|
+
async function readEnclavePolicy(path) {
|
|
2515
|
+
if (path === null)
|
|
2516
|
+
return null;
|
|
2517
|
+
let raw;
|
|
2518
|
+
try {
|
|
2519
|
+
if (!(await stat(path)).isFile())
|
|
2520
|
+
throw new CaseInputError(`--enclave-policy ${path} is not a readable file`);
|
|
2521
|
+
raw = new Uint8Array(await readFile(path));
|
|
2522
|
+
}
|
|
2523
|
+
catch (e) {
|
|
2524
|
+
if (e instanceof CaseInputError)
|
|
2525
|
+
throw e;
|
|
2526
|
+
throw new CaseInputError(`--enclave-policy ${path} is not a readable file`);
|
|
2527
|
+
}
|
|
2528
|
+
try {
|
|
2529
|
+
return parseRefusingDuplicates(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw));
|
|
2530
|
+
}
|
|
2531
|
+
catch {
|
|
2532
|
+
return POLICY_NOT_JSON;
|
|
2533
|
+
}
|
|
2534
|
+
}
|
|
2535
|
+
/** The two line members of `--json`: one entry per run, with the lines it prints. */
|
|
2536
|
+
function runLines(runs) {
|
|
2537
|
+
const enclave = runs.map((r) => ({
|
|
2538
|
+
pack_ref: r.packRef,
|
|
2539
|
+
pack_index: r.packIndex,
|
|
2540
|
+
event_id: r.eventId,
|
|
2541
|
+
runner_public_key: r.runnerPublicKey,
|
|
2542
|
+
agent_public_key: r.agentPublicKey,
|
|
2543
|
+
state: r.enclave.state,
|
|
2544
|
+
kinds: r.enclave.kinds,
|
|
2545
|
+
notes: r.enclave.notes,
|
|
2546
|
+
reason: r.enclave.reason,
|
|
2547
|
+
provider: r.enclave.provider,
|
|
2548
|
+
evaluation: r.enclave.evaluation,
|
|
2549
|
+
evaluated_at: r.enclave.evaluatedAt,
|
|
2550
|
+
token_checked: r.enclave.tokenChecked,
|
|
2551
|
+
online: r.online !== null ? { state: r.online.state, kinds: r.online.kinds } : null,
|
|
2552
|
+
rendered: renderEnclaveLines(r).map(escapeControl),
|
|
2553
|
+
}));
|
|
2554
|
+
const account = runs.map((r) => ({
|
|
2555
|
+
pack_ref: r.packRef,
|
|
2556
|
+
pack_index: r.packIndex,
|
|
2557
|
+
event_id: r.eventId,
|
|
2558
|
+
runner_public_key: r.runnerPublicKey,
|
|
2559
|
+
state: r.account.state,
|
|
2560
|
+
findings: r.account.findings.map((f) => ({ kind: f.kind, event_id: f.eventId })),
|
|
2561
|
+
notes: r.account.notes.map((n) => ({ kind: n.kind, event_id: n.eventId })),
|
|
2562
|
+
closing_event_id: r.account.closingEventId,
|
|
2563
|
+
reason: r.account.reason,
|
|
2564
|
+
rendered: escapeControl(renderAccountLine(r)),
|
|
2565
|
+
}));
|
|
2566
|
+
return [enclave, account];
|
|
2567
|
+
}
|
|
2420
2568
|
async function caseCommand(argv, io) {
|
|
2421
2569
|
let asJson = false;
|
|
2422
2570
|
let gateways = [];
|
|
2423
2571
|
const paths = [];
|
|
2424
2572
|
let filePaths;
|
|
2573
|
+
let enclavePolicy = null;
|
|
2574
|
+
let enclaveRpc = null;
|
|
2425
2575
|
for (let i = 0; i < argv.length; i++) {
|
|
2426
2576
|
const arg = argv[i];
|
|
2427
2577
|
if ((arg === "--help" || arg === "-h" || arg === "help") && paths.length === 0) {
|
|
@@ -2454,6 +2604,19 @@ async function caseCommand(argv, io) {
|
|
|
2454
2604
|
filePaths = [...(filePaths ?? []), ...taken];
|
|
2455
2605
|
i += taken.length;
|
|
2456
2606
|
}
|
|
2607
|
+
else if (arg === "--enclave-policy" || arg === "--enclave-rpc") {
|
|
2608
|
+
const next = argv[i + 1];
|
|
2609
|
+
if (next === undefined) {
|
|
2610
|
+
io.err(arg === "--enclave-policy" ? "case: --enclave-policy requires a policy file" : "case: --enclave-rpc requires a URL");
|
|
2611
|
+
outBlock(io, CASE_USAGE);
|
|
2612
|
+
return EXIT_MALFORMED;
|
|
2613
|
+
}
|
|
2614
|
+
if (arg === "--enclave-policy")
|
|
2615
|
+
enclavePolicy = next;
|
|
2616
|
+
else
|
|
2617
|
+
enclaveRpc = next;
|
|
2618
|
+
i++;
|
|
2619
|
+
}
|
|
2457
2620
|
else if (arg.startsWith("--")) {
|
|
2458
2621
|
io.err(`case: unknown option ${arg}`);
|
|
2459
2622
|
outBlock(io, CASE_USAGE);
|
|
@@ -2477,6 +2640,15 @@ async function caseCommand(argv, io) {
|
|
|
2477
2640
|
offered = read.offered;
|
|
2478
2641
|
skipped = read.skipped;
|
|
2479
2642
|
}
|
|
2643
|
+
let policy;
|
|
2644
|
+
try {
|
|
2645
|
+
policy = await readEnclavePolicy(enclavePolicy);
|
|
2646
|
+
}
|
|
2647
|
+
catch (e) {
|
|
2648
|
+
if (e instanceof CaseInputError)
|
|
2649
|
+
return caseError(e, asJson, io);
|
|
2650
|
+
throw e;
|
|
2651
|
+
}
|
|
2480
2652
|
const whole = { n: MAX_FOLDER_BYTES };
|
|
2481
2653
|
let split;
|
|
2482
2654
|
let tree;
|
|
@@ -2506,14 +2678,25 @@ async function caseCommand(argv, io) {
|
|
|
2506
2678
|
for (const [p, bundle] of bundles) {
|
|
2507
2679
|
caches.push(readWitnessCache(bundle, await witnessCacheBytes(tree, split, p, whole, cacheRefused)));
|
|
2508
2680
|
}
|
|
2509
|
-
const
|
|
2681
|
+
const confirmedStamps = [];
|
|
2682
|
+
const onChain = await onChainHalf(bundles.map(([p, bundle], i) => [p.ref, bundle, caches[i]]), gateways, { ...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}) }, confirmedStamps);
|
|
2510
2683
|
const witness = witnessLine(onChain, caches.flat());
|
|
2511
|
-
|
|
2684
|
+
// Sandbox evidence: over every envelope every pack holds, and per
|
|
2685
|
+
// pack for its row. Only a case that would otherwise exit 0 exits 5.
|
|
2686
|
+
const marker = environmentMarker(bundles.map(([, b]) => b));
|
|
2687
|
+
// Undetermined only because a simulated witness cannot be looked up: every
|
|
2688
|
+
// other witness confirmed, none unreachable or mismatched, every pack names
|
|
2689
|
+
// one. With every record signed `dev` that is sandbox evidence, exit 5.
|
|
2690
|
+
const simulatedOnly = reading.verdict === "verified" &&
|
|
2691
|
+
onChain.verdict === "undetermined" &&
|
|
2692
|
+
onChain.packs.some((r) => (r.simulated ?? 0) > 0) &&
|
|
2693
|
+
onChain.packs.every((r) => r.witnesses > 0 && r.unreachable === 0 && r.mismatched === 0 && r.not_checked === (r.simulated ?? 0));
|
|
2694
|
+
const code = sandboxExit(caseExitCode(reading.verdict, onChain.verdict), marker, simulatedOnly);
|
|
2512
2695
|
const nil = (v) => (v === undefined ? null : v);
|
|
2513
2696
|
const data = {
|
|
2514
2697
|
verdict: reading.verdict,
|
|
2515
|
-
title: titleOf(reading.verdict),
|
|
2516
|
-
reliance: caseReliance(reading.verdict, reading.handoffRecords),
|
|
2698
|
+
title: reading.verdict === "verified" && marker !== null ? SANDBOX_TITLE : titleOf(reading.verdict),
|
|
2699
|
+
reliance: caseReliance(reading.verdict, reading.handoffRecords, marker),
|
|
2517
2700
|
exit_code: code,
|
|
2518
2701
|
summary: renderCaseSummary(reading),
|
|
2519
2702
|
witness: {
|
|
@@ -2572,13 +2755,23 @@ async function caseCommand(argv, io) {
|
|
|
2572
2755
|
grade: reading.packs[i].grade,
|
|
2573
2756
|
records_withheld: reading.packs[i].recordsWithheld,
|
|
2574
2757
|
declaration_version: reading.packs[i].declarationVersion,
|
|
2758
|
+
...packMarker(bundles[i][1]),
|
|
2575
2759
|
})),
|
|
2576
2760
|
outside: split.outside,
|
|
2577
2761
|
not_regular: split.notRegular,
|
|
2578
2762
|
case_file: caseFile,
|
|
2579
2763
|
on_chain: onChain,
|
|
2580
2764
|
...(cacheRefused.length > 0 ? { witness_cache_not_read: cacheRefused } : {}),
|
|
2765
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
2581
2766
|
};
|
|
2767
|
+
// The runs in the drop (DRAFT, specs/enclave-attestation.md and
|
|
2768
|
+
// run-records.md): an enclave attestation line and a run-account line per
|
|
2769
|
+
// run, beside the witness line. Neither moves `code`, and a drop that holds
|
|
2770
|
+
// no run prints and writes exactly what it did before.
|
|
2771
|
+
const runs = (await readRuns(bundles.map(([p, bundle]) => ({ ref: p.ref, bundle, content: p.content })), { policy, adapters: await loadInstalledAdapters(), rpc: enclaveRpc, witnessed: confirmedStamps })).runs;
|
|
2772
|
+
const withRuns = data;
|
|
2773
|
+
if (runs.length > 0)
|
|
2774
|
+
[withRuns.enclave_attestation, withRuns.run_account] = runLines(runs);
|
|
2582
2775
|
// The files outside every pack (evidence-bundle.md 5.7, A29, A41), always,
|
|
2583
2776
|
// then the files after --files. A pack's logs/ is its side content and is
|
|
2584
2777
|
// never an offered file; only the paths after --files are. Both are matched
|
package/dist/declaration.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ChainLink } from "./chain.js";
|
|
1
2
|
import { type HandoffRecord } from "./handoff.js";
|
|
2
3
|
import { type SideContentMap } from "./side-content.js";
|
|
3
4
|
/** The first declaration schema, which every earlier reader knows. */
|
|
@@ -8,8 +9,15 @@ export declare const DECLARATION_VERSION = "sanning.declaration/v1";
|
|
|
8
9
|
* it cannot read, which is the safe direction.
|
|
9
10
|
*/
|
|
10
11
|
export declare const DECLARATION_VERSION_V2 = "sanning.declaration/v2";
|
|
12
|
+
/**
|
|
13
|
+
* v2 plus a signed `chains[]`: the `chain_key`s the holder claims are complete
|
|
14
|
+
* and in order (evidence-bundle.md 5.4.1 v1.36, A45). A reader that knows only
|
|
15
|
+
* v1 and v2 reads a v3 declaration as one it cannot read, which is the safe
|
|
16
|
+
* direction.
|
|
17
|
+
*/
|
|
18
|
+
export declare const DECLARATION_VERSION_V3 = "sanning.declaration/v3";
|
|
11
19
|
/** Every declaration schema this reader knows, in the order they were added. */
|
|
12
|
-
export declare const DECLARATION_VERSIONS: readonly ["sanning.declaration/v1", "sanning.declaration/v2"];
|
|
20
|
+
export declare const DECLARATION_VERSIONS: readonly ["sanning.declaration/v1", "sanning.declaration/v2", "sanning.declaration/v3"];
|
|
13
21
|
/**
|
|
14
22
|
* What a v2 declaration may sign a record IS, compared exactly: `handoff`, a
|
|
15
23
|
* record 4.1 governs that the pack claims as a side in `handoffs[]`, or
|
|
@@ -20,7 +28,7 @@ export type DeclarationRole = (typeof DECLARATION_ROLES)[number];
|
|
|
20
28
|
/** The three states a declaration check can reach, from the §3 vocabulary. */
|
|
21
29
|
export declare const DECLARATION_VERDICTS: readonly ["verified", "failed", "partial"];
|
|
22
30
|
export type DeclarationVerdict = (typeof DECLARATION_VERDICTS)[number];
|
|
23
|
-
export declare const DECLARATION_DEVIATION_KINDS: readonly ["declaration_malformed", "declared_absent", "undeclared_present", "withheld_but_disclosed", "duplicate_event_id", "pair_record_undeclared", "pair_record_not_a_handoff", "role_handoff_unclaimed", "role_step_claimed", "role_step_governed", "role_step_unreadable"];
|
|
31
|
+
export declare const DECLARATION_DEVIATION_KINDS: readonly ["declaration_malformed", "declared_absent", "undeclared_present", "withheld_but_disclosed", "duplicate_event_id", "pair_record_undeclared", "pair_record_not_a_handoff", "role_handoff_unclaimed", "role_step_claimed", "role_step_governed", "role_step_unreadable", "chain_second_root", "chain_fork", "chain_link_dangling", "chain_claimed_absent"];
|
|
24
32
|
export type DeclarationDeviationKind = (typeof DECLARATION_DEVIATION_KINDS)[number];
|
|
25
33
|
/**
|
|
26
34
|
* Every way a pack and the UNSIGNED content handed in beside it fail to line
|
|
@@ -60,7 +68,7 @@ export interface DeclarationContentFinding {
|
|
|
60
68
|
eventId?: string;
|
|
61
69
|
}
|
|
62
70
|
/** Everything this check reports that is NOT a deviation, as a closed set. */
|
|
63
|
-
export declare const DECLARATION_NOTE_KINDS: readonly ["disclosure_did_not_travel", "pair_record_withheld", "role_step_withheld", "other_declaration_held", "other_declaration_withheld"];
|
|
71
|
+
export declare const DECLARATION_NOTE_KINDS: readonly ["disclosure_did_not_travel", "pair_record_withheld", "role_step_withheld", "other_declaration_held", "other_declaration_withheld", "chain_claim_withheld"];
|
|
64
72
|
export type DeclarationNoteKind = (typeof DECLARATION_NOTE_KINDS)[number];
|
|
65
73
|
/**
|
|
66
74
|
* One named non-deviation finding, as data.
|
|
@@ -79,6 +87,8 @@ export interface DeclarationNote {
|
|
|
79
87
|
side?: "sent" | "received";
|
|
80
88
|
/** Which claimed hand-off, by position, for `pair_record_withheld`. */
|
|
81
89
|
pairIndex?: number;
|
|
90
|
+
/** The claimed chain, for `chain_claim_withheld`. */
|
|
91
|
+
chainKey?: string;
|
|
82
92
|
}
|
|
83
93
|
/** One deviation, as data. A surface renders its own prose or calls `renderDeviation`. */
|
|
84
94
|
export interface DeclarationDeviation {
|
|
@@ -97,6 +107,8 @@ export interface DeclarationDeviation {
|
|
|
97
107
|
* the first one this reader learned.
|
|
98
108
|
*/
|
|
99
109
|
version?: string;
|
|
110
|
+
/** The claimed chain, for the four rule 11 `chain_` deviations. */
|
|
111
|
+
chainKey?: string;
|
|
100
112
|
}
|
|
101
113
|
export interface DeclarationCheck {
|
|
102
114
|
verdict: DeclarationVerdict;
|
|
@@ -137,6 +149,12 @@ export interface DeclarationCheck {
|
|
|
137
149
|
eventId: string;
|
|
138
150
|
role: DeclarationRole;
|
|
139
151
|
}[];
|
|
152
|
+
/**
|
|
153
|
+
* Every `chain_key` a v3 declaration claims is complete and in order, in
|
|
154
|
+
* declaration order. Empty for v1 and v2, and for anything this reader could
|
|
155
|
+
* not read.
|
|
156
|
+
*/
|
|
157
|
+
chains?: string[];
|
|
140
158
|
}
|
|
141
159
|
/**
|
|
142
160
|
* One of the pack's own records: the id, and whether the pack supplied its
|
|
@@ -172,6 +190,19 @@ export interface PackRecord {
|
|
|
172
190
|
* two different readings and never one.
|
|
173
191
|
*/
|
|
174
192
|
bytesSupplied: boolean;
|
|
193
|
+
/**
|
|
194
|
+
* Whether this record's payload binding is UNDETERMINED: no `event_bytes`
|
|
195
|
+
* string and no inline envelope `payload`, the two things the envelope
|
|
196
|
+
* verifier binds `payload_hash` against. Rule 11 excuses a dangling link
|
|
197
|
+
* only for such a record's producing key.
|
|
198
|
+
*/
|
|
199
|
+
bindingUndetermined: boolean;
|
|
200
|
+
/**
|
|
201
|
+
* The envelope's signed `payload_hash` as written, or `""`. For a record
|
|
202
|
+
* whose binding is undetermined it is the identity a successor's pointer
|
|
203
|
+
* names: rule 11 excuses a dangling link only where the pointer equals it.
|
|
204
|
+
*/
|
|
205
|
+
payloadHash: string;
|
|
175
206
|
}
|
|
176
207
|
/**
|
|
177
208
|
* The pack's own records, in bundle order. Exported so the CASE layer
|
|
@@ -189,11 +220,19 @@ export declare function packRecords(bundle: unknown, content: SideContentMap | n
|
|
|
189
220
|
* and a pack that discloses them beside itself passes the map a consumer
|
|
190
221
|
* already built.
|
|
191
222
|
*
|
|
223
|
+
* `chains` is `readChains` over the SAME bundle. Only a v3 declaration that
|
|
224
|
+
* claims a chain reads it (rule 11). It is an argument rather than computed
|
|
225
|
+
* here because `readChains` is asynchronous and this check is not, and Python's
|
|
226
|
+
* `check_declaration` takes it the same way so the two kernels share one
|
|
227
|
+
* contract. A v3 declaration that claims a chain, checked without it, reads as
|
|
228
|
+
* a reader that does not know v3 reads it: `partial`, nothing named, and the
|
|
229
|
+
* pack `incomplete`. Leaving it out can never read `verified`.
|
|
230
|
+
*
|
|
192
231
|
* The verdict is this check's own. It does not restate the bundle verdict and
|
|
193
232
|
* it does not read it: a declaration can match perfectly over a pack whose
|
|
194
233
|
* signatures are forged, and saying so is `gradePack`'s job, which takes both.
|
|
195
234
|
*/
|
|
196
|
-
export declare function checkDeclaration(bundle: unknown, content?: SideContentMap | null): DeclarationCheck;
|
|
235
|
+
export declare function checkDeclaration(bundle: unknown, content?: SideContentMap | null, links?: readonly ChainLink[] | null): DeclarationCheck;
|
|
197
236
|
/**
|
|
198
237
|
* The house wording for one deviation.
|
|
199
238
|
*
|