@sanning/proof 0.18.0 → 0.19.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/README.md +2 -2
- package/dist/case-file.js +2 -1
- package/dist/case.js +11 -4
- package/dist/chain.d.ts +28 -0
- package/dist/chain.js +61 -4
- package/dist/cli.js +226 -33
- 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 +8 -4
- package/dist/index.js +20 -4
- 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 +11 -4
- package/src/chain.ts +81 -4
- package/src/cli.ts +259 -28
- 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 +30 -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 +3161 -0
package/src/cli.ts
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
|
//
|
|
@@ -50,12 +52,12 @@ import { fileURLToPath } from "node:url";
|
|
|
50
52
|
|
|
51
53
|
import { isAgentProofSpec, verifyAgentProof } from "./agent-proof.js";
|
|
52
54
|
import type { AgentProofResult } from "./agent-proof.js";
|
|
53
|
-
import { CHAIN_READING_SCOPE, readChains, renderChainFinding } from "./chain.js";
|
|
55
|
+
import { CHAIN_READING_SCOPE, readChainLinks, readChains, renderChainFinding } from "./chain.js";
|
|
54
56
|
|
|
55
57
|
// What this command holds after asking for a chain reading: the reading, or a
|
|
56
58
|
// named statement that one could not be produced. Three states, not two.
|
|
57
59
|
type ChainOutcome = ChainReading | { unreadable: string };
|
|
58
|
-
import type { ChainReading } from "./chain.js";
|
|
60
|
+
import type { ChainLink, ChainReading } from "./chain.js";
|
|
59
61
|
import {
|
|
60
62
|
VERDICT_DISPLAY_TITLES,
|
|
61
63
|
caseReliance,
|
|
@@ -78,6 +80,9 @@ import { composeExport } from "./compose.js";
|
|
|
78
80
|
import type { ComposeExportOptions } from "./compose.js";
|
|
79
81
|
import { isHexField, sha256Hex, utf8 } from "./crypto.js";
|
|
80
82
|
import { verifyEvidenceBundle } from "./evidence.js";
|
|
83
|
+
import { readRuns, renderAccountLine, renderEnclaveLines } from "./run.js";
|
|
84
|
+
import type { RunReading } from "./run.js";
|
|
85
|
+
import { loadInstalledAdapters } from "./run-adapters.js";
|
|
81
86
|
import { DuplicateKeyError, parseRefusingDuplicates } from "./verifier.js";
|
|
82
87
|
import {
|
|
83
88
|
fileMatchToJson,
|
|
@@ -107,10 +112,21 @@ import {
|
|
|
107
112
|
EXIT_CODES_HELP,
|
|
108
113
|
EXIT_FAILED,
|
|
109
114
|
EXIT_MALFORMED,
|
|
115
|
+
EXIT_SANDBOX,
|
|
110
116
|
EXIT_UNDETERMINED,
|
|
111
117
|
EXIT_VERIFIED,
|
|
112
118
|
exitCodeFor,
|
|
119
|
+
sandboxExit,
|
|
113
120
|
} from "./exit-codes.js";
|
|
121
|
+
import {
|
|
122
|
+
type EnvironmentMarker,
|
|
123
|
+
environmentMarker,
|
|
124
|
+
onlySimulatedUnsettled,
|
|
125
|
+
simulatedOnlyRun,
|
|
126
|
+
SANDBOX_HEADLINE,
|
|
127
|
+
SANDBOX_LINE,
|
|
128
|
+
SANDBOX_TITLE,
|
|
129
|
+
} from "./environment.js";
|
|
114
130
|
|
|
115
131
|
interface Cli {
|
|
116
132
|
out: (s: string) => void;
|
|
@@ -416,7 +432,7 @@ async function runVerify(rest: string[], io: Cli, sink: JsonSink | null): Promis
|
|
|
416
432
|
gateways,
|
|
417
433
|
...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}),
|
|
418
434
|
});
|
|
419
|
-
return reportAgentProof(result, bundlePath, gateways, io, sink);
|
|
435
|
+
return reportAgentProof(result, bundlePath, gateways, io, sink, parsed);
|
|
420
436
|
}
|
|
421
437
|
|
|
422
438
|
// Default: treat anything else as an evidence bundle. verifyEvidenceBundle
|
|
@@ -429,8 +445,11 @@ async function runVerify(rest: string[], io: Cli, sink: JsonSink | null): Promis
|
|
|
429
445
|
});
|
|
430
446
|
// Read the chains from the SAME parsed bundle the verifier saw. Findings are
|
|
431
447
|
// evidence about the records in front of us, not a verdict: they are printed
|
|
432
|
-
// and they
|
|
433
|
-
//
|
|
448
|
+
// and they move `exit` only through the pack's own declaration, where a v3
|
|
449
|
+
// declaration claims a chain complete and in order and a finding under that
|
|
450
|
+
// claim contradicts it (rule 11). Without a claim a chain is not a
|
|
451
|
+
// completeness proof, and an exit code is exactly the kind of thing people
|
|
452
|
+
// read as one.
|
|
434
453
|
//
|
|
435
454
|
// A READING THAT COULD NOT BE PRODUCED SAYS SO. This was an unguarded call:
|
|
436
455
|
// a throw out of `readChains` would have escaped the command entirely and
|
|
@@ -445,7 +464,16 @@ async function runVerify(rest: string[], io: Cli, sink: JsonSink | null): Promis
|
|
|
445
464
|
} catch (e) {
|
|
446
465
|
chains = { unreadable: errMsg(e) };
|
|
447
466
|
}
|
|
448
|
-
|
|
467
|
+
// The links rule 11 holds a claimed chain to (evidence-bundle.md 5.4.2), from
|
|
468
|
+
// the same parsed bundle. Links that cannot be read are none, which reads a
|
|
469
|
+
// claim UNKNOWN and never clean.
|
|
470
|
+
let links: ChainLink[] | null;
|
|
471
|
+
try {
|
|
472
|
+
links = "unreadable" in chains ? null : await readChainLinks(parsed);
|
|
473
|
+
} catch {
|
|
474
|
+
links = null;
|
|
475
|
+
}
|
|
476
|
+
const exit = reportEvidence(result, bundlePath, gateways, io, chains, links, parsed, logs, sink);
|
|
449
477
|
// Files handed in beside the pack (evidence-bundle.md 5.7, A29): findings
|
|
450
478
|
// beside the verdict, never inside it, so `exit` is untouched.
|
|
451
479
|
if (offered !== undefined) {
|
|
@@ -1397,6 +1425,7 @@ function reportEvidence(
|
|
|
1397
1425
|
gateways: string[],
|
|
1398
1426
|
io: Cli,
|
|
1399
1427
|
chains: ChainOutcome,
|
|
1428
|
+
links: ChainLink[] | null,
|
|
1400
1429
|
parsed: unknown,
|
|
1401
1430
|
logs: LoadedLogs | undefined,
|
|
1402
1431
|
sink: JsonSink | null,
|
|
@@ -1503,7 +1532,12 @@ function reportEvidence(
|
|
|
1503
1532
|
// their verdicts rather than a new check. A pack that DOES
|
|
1504
1533
|
// carry a declaration this reader cannot read is a different case and exits
|
|
1505
1534
|
// 3: the holder chose that field, so UNKNOWN is the honest answer.
|
|
1506
|
-
|
|
1535
|
+
//
|
|
1536
|
+
// A v3 declaration that claims a chain is checked against this command's
|
|
1537
|
+
// own chain links (rule 11, evidence-bundle.md 5.4.2 v1.36, A45). Links
|
|
1538
|
+
// that could not be read are passed as none, which reads the claim UNKNOWN
|
|
1539
|
+
// and never clean.
|
|
1540
|
+
const declaration = checkDeclaration(parsed, logs?.content, links);
|
|
1507
1541
|
const grade = gradePack(
|
|
1508
1542
|
result,
|
|
1509
1543
|
cliDisclosureAccount(result, logs),
|
|
@@ -1511,8 +1545,34 @@ function reportEvidence(
|
|
|
1511
1545
|
declaration,
|
|
1512
1546
|
);
|
|
1513
1547
|
|
|
1548
|
+
// Sandbox evidence: read from the signed envelopes the pack holds.
|
|
1549
|
+
const marker = environmentMarker([parsed]);
|
|
1550
|
+
// A pack whose records are all signed `dev`, undetermined only because a
|
|
1551
|
+
// simulated witness cannot be looked up, would grade `verified` with that
|
|
1552
|
+
// witness accepted: it is sandbox evidence, exit 5, never 3.
|
|
1553
|
+
const simulatedOnly =
|
|
1554
|
+
grade.verdict === "partial" &&
|
|
1555
|
+
onlySimulatedUnsettled(result) &&
|
|
1556
|
+
gradePack(
|
|
1557
|
+
{ ...result, status: "verified" },
|
|
1558
|
+
cliDisclosureAccount(result, logs),
|
|
1559
|
+
completenessNotes(parsed),
|
|
1560
|
+
declaration,
|
|
1561
|
+
).verdict === "verified";
|
|
1562
|
+
const exit = sandboxExit(mapGradeToExit(grade), marker, simulatedOnly);
|
|
1563
|
+
|
|
1514
1564
|
io.out("");
|
|
1515
|
-
printRollup(
|
|
1565
|
+
printRollup(
|
|
1566
|
+
grade.verdict,
|
|
1567
|
+
result.status,
|
|
1568
|
+
result.assertedStatus,
|
|
1569
|
+
gateways,
|
|
1570
|
+
result.onChainChecked,
|
|
1571
|
+
io,
|
|
1572
|
+
marker,
|
|
1573
|
+
exit === EXIT_SANDBOX,
|
|
1574
|
+
simulatedOnlyRun(result, gateways),
|
|
1575
|
+
);
|
|
1516
1576
|
io.out(
|
|
1517
1577
|
paint(
|
|
1518
1578
|
` declaration: ${renderDeclarationSummary(declaration)}`,
|
|
@@ -1539,8 +1599,8 @@ function reportEvidence(
|
|
|
1539
1599
|
);
|
|
1540
1600
|
}
|
|
1541
1601
|
|
|
1542
|
-
if (sink) sink.data = verifyJson(result, grade, declaration, chains);
|
|
1543
|
-
return
|
|
1602
|
+
if (sink) sink.data = verifyJson(result, grade, declaration, chains, marker);
|
|
1603
|
+
return exit;
|
|
1544
1604
|
}
|
|
1545
1605
|
|
|
1546
1606
|
/**
|
|
@@ -1553,6 +1613,7 @@ function verifyJson(
|
|
|
1553
1613
|
grade: PackGrade | null,
|
|
1554
1614
|
declaration: ReturnType<typeof checkDeclaration> | null,
|
|
1555
1615
|
chains: ChainOutcome | null,
|
|
1616
|
+
marker: EnvironmentMarker | null = null,
|
|
1556
1617
|
): Record<string, unknown> {
|
|
1557
1618
|
let chainBlock: Record<string, unknown> | null = null;
|
|
1558
1619
|
if (chains !== null) {
|
|
@@ -1579,6 +1640,9 @@ function verifyJson(
|
|
|
1579
1640
|
? { presented: result.events.length, content_verified: grade.recordsVerified }
|
|
1580
1641
|
: null,
|
|
1581
1642
|
chains: chainBlock,
|
|
1643
|
+
// Present only where the pack holds an envelope signed `dev`
|
|
1644
|
+
// (envelope-spec §2 MAY), so a production pack's JSON is unchanged.
|
|
1645
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
1582
1646
|
};
|
|
1583
1647
|
}
|
|
1584
1648
|
|
|
@@ -1661,7 +1725,9 @@ function reportAgentProof(
|
|
|
1661
1725
|
gateways: string[],
|
|
1662
1726
|
io: Cli,
|
|
1663
1727
|
sink: JsonSink | null,
|
|
1728
|
+
parsed: unknown,
|
|
1664
1729
|
): number {
|
|
1730
|
+
const marker = result.status === "malformed" ? null : environmentMarker([parsed]);
|
|
1665
1731
|
if (sink) {
|
|
1666
1732
|
sink.data = {
|
|
1667
1733
|
verdict: result.status,
|
|
@@ -1669,6 +1735,7 @@ function reportAgentProof(
|
|
|
1669
1735
|
spec_version: "sanning.agent.proof/v1",
|
|
1670
1736
|
on_chain_checked: result.onChainChecked,
|
|
1671
1737
|
notes: [...result.errors],
|
|
1738
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
1672
1739
|
};
|
|
1673
1740
|
}
|
|
1674
1741
|
if (result.status === "malformed") {
|
|
@@ -1687,10 +1754,11 @@ function reportAgentProof(
|
|
|
1687
1754
|
io.out("");
|
|
1688
1755
|
// An agent inclusion proof carries no records and no disclosed bytes, so
|
|
1689
1756
|
// there is no pack to grade: the bundle status IS the headline here.
|
|
1690
|
-
|
|
1757
|
+
const exit = sandboxExit(exitCodeFor(result.status), marker);
|
|
1758
|
+
printRollup(result.status, result.status, null, gateways, result.onChainChecked, io, marker, exit === EXIT_SANDBOX);
|
|
1691
1759
|
for (const e of result.errors) io.out(paint(` note: ${e}`, DIM));
|
|
1692
1760
|
|
|
1693
|
-
return
|
|
1761
|
+
return exit;
|
|
1694
1762
|
}
|
|
1695
1763
|
|
|
1696
1764
|
function printRollup(
|
|
@@ -1700,12 +1768,27 @@ function printRollup(
|
|
|
1700
1768
|
gateways: string[],
|
|
1701
1769
|
onChainChecked: boolean,
|
|
1702
1770
|
io: Cli,
|
|
1771
|
+
marker: EnvironmentMarker | null = null,
|
|
1772
|
+
sandbox = false,
|
|
1773
|
+
simulatedOnly = false,
|
|
1703
1774
|
): void {
|
|
1775
|
+
// Three scopes, not two. A run with gateways whose only witnesses are
|
|
1776
|
+
// simulated looked nothing up (`simulatedOnlyRun`): saying "on-chain" there
|
|
1777
|
+
// claims a check that never happened, and saying "no gateway re-fetch
|
|
1778
|
+
// requested" denies the flag the reader passed. Every other run that is not
|
|
1779
|
+
// counted as checked on a chain, a run that stopped early among them, keeps
|
|
1780
|
+
// the second scope as before.
|
|
1704
1781
|
const scope = onChainChecked
|
|
1705
1782
|
? `verified offline + on-chain against ${gateways.length} gateway(s)`
|
|
1706
|
-
:
|
|
1783
|
+
: simulatedOnly
|
|
1784
|
+
? "verified offline; a simulated witness is not on a chain, so nothing was looked up"
|
|
1785
|
+
: "verified offline (no gateway re-fetch requested)";
|
|
1707
1786
|
let head: string;
|
|
1708
|
-
switch (headline) {
|
|
1787
|
+
switch (sandbox ? "sandbox" : headline) {
|
|
1788
|
+
// Sandbox evidence (exit 5) never prints VERIFIED.
|
|
1789
|
+
case "sandbox":
|
|
1790
|
+
head = paint(SANDBOX_HEADLINE, Y);
|
|
1791
|
+
break;
|
|
1709
1792
|
case "verified":
|
|
1710
1793
|
head = paint("VERIFIED", G);
|
|
1711
1794
|
break;
|
|
@@ -1723,6 +1806,7 @@ function printRollup(
|
|
|
1723
1806
|
head = paint(headline.toUpperCase(), R);
|
|
1724
1807
|
}
|
|
1725
1808
|
io.out(`${head} ${paint(scope, DIM)}`);
|
|
1809
|
+
if (marker !== null) io.out(paint(` ${SANDBOX_LINE}`, Y));
|
|
1726
1810
|
// Both verdicts, whenever they differ, because they answer different
|
|
1727
1811
|
// questions: §3 grades the signed bundle, the pack verdict grades the bundle
|
|
1728
1812
|
// plus whatever bytes travelled beside it. A pack can be incomplete while the
|
|
@@ -2379,7 +2463,7 @@ async function unzip(
|
|
|
2379
2463
|
|
|
2380
2464
|
export const CASE_USAGE = `sanning-proof case -- a case reading: one verdict over one pack or many, offline
|
|
2381
2465
|
|
|
2382
|
-
sanning-proof case <folder or case file>... [--gateways <url,url>] [--files <path>...] [--json]
|
|
2466
|
+
sanning-proof case <folder or case file>... [--gateways <url,url>] [--files <path>...] [--enclave-policy <file>] [--enclave-rpc <url>] [--json]
|
|
2383
2467
|
|
|
2384
2468
|
<folder> a pack folder (bundle.json plus logs/), a folder that holds
|
|
2385
2469
|
several packs, or an unzipped case file. Packs are found by the
|
|
@@ -2417,6 +2501,17 @@ export const CASE_USAGE = `sanning-proof case -- a case reading: one verdict ove
|
|
|
2417
2501
|
contribution, the packs, each with its index, the files outside
|
|
2418
2502
|
them, the case file index comparison, the witness line and the
|
|
2419
2503
|
on-chain half.
|
|
2504
|
+
--enclave-policy
|
|
2505
|
+
a sanning.enclave-policy/v1 file: what this reader accepts of
|
|
2506
|
+
an enclave attestation. Where the packs hold a runner's run, an
|
|
2507
|
+
enclave attestation line and a run-account line print beside
|
|
2508
|
+
the witness line (DRAFT); neither moves the verdict or the exit
|
|
2509
|
+
code. Without a policy, or without an adapter for the provider
|
|
2510
|
+
installed, the line reads not_checked.
|
|
2511
|
+
--enclave-rpc
|
|
2512
|
+
a chain JSON-RPC endpoint: a flare-fcc run also gets the online
|
|
2513
|
+
line, read from the contracts the policy names. It moves
|
|
2514
|
+
nothing either.
|
|
2420
2515
|
|
|
2421
2516
|
Files outside every pack are matched too, always, so
|
|
2422
2517
|
sanning-proof case audit-file.zip checks a whole download in one command: each
|
|
@@ -2499,6 +2594,8 @@ interface CaseData {
|
|
|
2499
2594
|
reliance: string;
|
|
2500
2595
|
summary: string;
|
|
2501
2596
|
witness: { rendered: string };
|
|
2597
|
+
enclave_attestation?: { rendered: string[] }[];
|
|
2598
|
+
run_account?: { rendered: string }[];
|
|
2502
2599
|
exceptions: { rendered: string }[];
|
|
2503
2600
|
notes: { rendered: string }[];
|
|
2504
2601
|
packs: {
|
|
@@ -2508,8 +2605,10 @@ interface CaseData {
|
|
|
2508
2605
|
unaccounted: string[];
|
|
2509
2606
|
signing_key: string | null;
|
|
2510
2607
|
grade: string | null;
|
|
2608
|
+
environment_marker?: EnvironmentMarker;
|
|
2511
2609
|
}[];
|
|
2512
2610
|
records: { pack_index: number; status: string }[];
|
|
2611
|
+
environment_marker?: EnvironmentMarker;
|
|
2513
2612
|
outside: string[];
|
|
2514
2613
|
not_regular: string[];
|
|
2515
2614
|
case_file: CaseFileData[];
|
|
@@ -2528,18 +2627,30 @@ const titleOf = (word: string): string =>
|
|
|
2528
2627
|
* scope sentence and the witness line are printed with it every time.
|
|
2529
2628
|
*/
|
|
2530
2629
|
function caseReport(data: CaseData): string[] {
|
|
2531
|
-
const lines = [
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2630
|
+
const lines = [data.title];
|
|
2631
|
+
// Sandbox evidence: printed with every headline the marker is on.
|
|
2632
|
+
if (data.environment_marker !== undefined) lines.push(` ${SANDBOX_LINE}`);
|
|
2633
|
+
lines.push(` ${data.summary}`, ` ${data.witness.rendered}`);
|
|
2634
|
+
// Where the packs hold a runner's run: its enclave attestation line and its
|
|
2635
|
+
// run-account line, beside the witness line.
|
|
2636
|
+
const enclave = data.enclave_attestation ?? [];
|
|
2637
|
+
const account = data.run_account ?? [];
|
|
2638
|
+
for (let i = 0; i < Math.min(enclave.length, account.length); i++) {
|
|
2639
|
+
for (const line of enclave[i]!.rendered) lines.push(` ${line}`);
|
|
2640
|
+
lines.push(` ${account[i]!.rendered}`);
|
|
2641
|
+
}
|
|
2642
|
+
lines.push(` ${data.reliance}`);
|
|
2537
2643
|
for (const e of data.exceptions) lines.push(` - ${e.rendered}`);
|
|
2538
2644
|
for (const n of data.notes) lines.push(` note: ${n.rendered}`);
|
|
2539
2645
|
lines.push(`packs ${data.packs.length}`);
|
|
2540
2646
|
for (const p of data.packs) {
|
|
2541
2647
|
const key = p.signing_key !== null ? p.signing_key : "a key this reader cannot read";
|
|
2542
|
-
const grade =
|
|
2648
|
+
const grade =
|
|
2649
|
+
p.grade === null
|
|
2650
|
+
? "not graded"
|
|
2651
|
+
: p.grade === "verified" && p.environment_marker !== undefined
|
|
2652
|
+
? SANDBOX_TITLE
|
|
2653
|
+
: titleOf(p.grade);
|
|
2543
2654
|
// Each record's status, counted per pack (A43): the JSON lists every one.
|
|
2544
2655
|
const mine = data.records.filter((r) => r.pack_index === p.index);
|
|
2545
2656
|
const count = (status: string) => mine.filter((r) => r.status === status).length;
|
|
@@ -2581,16 +2692,27 @@ function caseReport(data: CaseData): string[] {
|
|
|
2581
2692
|
}
|
|
2582
2693
|
const oc = data.on_chain;
|
|
2583
2694
|
if (oc.checked) {
|
|
2695
|
+
// A drop whose only witnesses are simulated had nothing looked up on a
|
|
2696
|
+
// chain, and the header says so rather than "looked up against".
|
|
2697
|
+
const simulated = oc.packs.reduce((n, r) => n + (r.simulated ?? 0), 0);
|
|
2698
|
+
const onlySimulated =
|
|
2699
|
+
simulated > 0 &&
|
|
2700
|
+
oc.packs.every((r) => (r.simulated ?? 0) === r.witnesses && (r.witness_times ?? []).length === 0);
|
|
2584
2701
|
lines.push(
|
|
2585
|
-
|
|
2586
|
-
"
|
|
2587
|
-
|
|
2702
|
+
onlySimulated
|
|
2703
|
+
? "on-chain nothing looked up: every witness here is simulated, " +
|
|
2704
|
+
"which is not on a chain; the reading above is offline and this " +
|
|
2705
|
+
"does not change it"
|
|
2706
|
+
: `on-chain looked up against ${oc.gateways.length} gateway(s), ` +
|
|
2707
|
+
"pack by pack; the reading above is offline and this does not " +
|
|
2708
|
+
"change it",
|
|
2588
2709
|
);
|
|
2589
2710
|
for (const r of oc.packs) {
|
|
2711
|
+
const sim = r.simulated ? `, ${r.simulated} of them simulated (not on a chain)` : "";
|
|
2590
2712
|
lines.push(
|
|
2591
2713
|
` ${r.ref} ${r.witnesses} witness(es): ${r.confirmed} ` +
|
|
2592
2714
|
`confirmed, ${r.mismatched} mismatched, ${r.unreachable} ` +
|
|
2593
|
-
`unreachable, ${r.not_checked} not checked`,
|
|
2715
|
+
`unreachable, ${r.not_checked} not checked${sim}`,
|
|
2594
2716
|
);
|
|
2595
2717
|
}
|
|
2596
2718
|
}
|
|
@@ -2681,6 +2803,13 @@ export async function caseFileReadings(
|
|
|
2681
2803
|
return out as CaseFileData[];
|
|
2682
2804
|
}
|
|
2683
2805
|
|
|
2806
|
+
// One pack's environment marker, as the key its JSON row carries only where
|
|
2807
|
+
// the pack holds an envelope signed `dev`.
|
|
2808
|
+
function packMarker(bundle: unknown): { environment_marker?: EnvironmentMarker } {
|
|
2809
|
+
const m = environmentMarker([bundle]);
|
|
2810
|
+
return m !== null ? { environment_marker: m } : {};
|
|
2811
|
+
}
|
|
2812
|
+
|
|
2684
2813
|
function caseError(e: CaseInputError, asJson: boolean, io: Cli): number {
|
|
2685
2814
|
if (asJson) outJson(io, jsonText({ error: e.message, exit_code: EXIT_MALFORMED, hint: e.hint }));
|
|
2686
2815
|
io.err(noLoneSurrogates(`case: ${e.message}`));
|
|
@@ -2769,11 +2898,67 @@ export async function looseFiles(
|
|
|
2769
2898
|
return { offered, skipped };
|
|
2770
2899
|
}
|
|
2771
2900
|
|
|
2901
|
+
// A policy file that is not JSON: read, and unreadable (`policy_unreadable`).
|
|
2902
|
+
const POLICY_NOT_JSON = Symbol("policy not JSON");
|
|
2903
|
+
|
|
2904
|
+
async function readEnclavePolicy(path: string | null): Promise<unknown> {
|
|
2905
|
+
if (path === null) return null;
|
|
2906
|
+
let raw: Uint8Array;
|
|
2907
|
+
try {
|
|
2908
|
+
if (!(await stat(path)).isFile()) throw new CaseInputError(`--enclave-policy ${path} is not a readable file`);
|
|
2909
|
+
raw = new Uint8Array(await readFile(path));
|
|
2910
|
+
} catch (e) {
|
|
2911
|
+
if (e instanceof CaseInputError) throw e;
|
|
2912
|
+
throw new CaseInputError(`--enclave-policy ${path} is not a readable file`);
|
|
2913
|
+
}
|
|
2914
|
+
try {
|
|
2915
|
+
return parseRefusingDuplicates(new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(raw));
|
|
2916
|
+
} catch {
|
|
2917
|
+
return POLICY_NOT_JSON;
|
|
2918
|
+
}
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
/** The two line members of `--json`: one entry per run, with the lines it prints. */
|
|
2922
|
+
function runLines(runs: readonly RunReading[]): [Record<string, unknown>[], Record<string, unknown>[]] {
|
|
2923
|
+
const enclave = runs.map((r) => ({
|
|
2924
|
+
pack_ref: r.packRef,
|
|
2925
|
+
pack_index: r.packIndex,
|
|
2926
|
+
event_id: r.eventId,
|
|
2927
|
+
runner_public_key: r.runnerPublicKey,
|
|
2928
|
+
agent_public_key: r.agentPublicKey,
|
|
2929
|
+
state: r.enclave.state,
|
|
2930
|
+
kinds: r.enclave.kinds,
|
|
2931
|
+
notes: r.enclave.notes,
|
|
2932
|
+
reason: r.enclave.reason,
|
|
2933
|
+
provider: r.enclave.provider,
|
|
2934
|
+
evaluation: r.enclave.evaluation,
|
|
2935
|
+
evaluated_at: r.enclave.evaluatedAt,
|
|
2936
|
+
token_checked: r.enclave.tokenChecked,
|
|
2937
|
+
online: r.online !== null ? { state: r.online.state, kinds: r.online.kinds } : null,
|
|
2938
|
+
rendered: renderEnclaveLines(r).map(escapeControl),
|
|
2939
|
+
}));
|
|
2940
|
+
const account = runs.map((r) => ({
|
|
2941
|
+
pack_ref: r.packRef,
|
|
2942
|
+
pack_index: r.packIndex,
|
|
2943
|
+
event_id: r.eventId,
|
|
2944
|
+
runner_public_key: r.runnerPublicKey,
|
|
2945
|
+
state: r.account.state,
|
|
2946
|
+
findings: r.account.findings.map((f) => ({ kind: f.kind, event_id: f.eventId })),
|
|
2947
|
+
notes: r.account.notes.map((n) => ({ kind: n.kind, event_id: n.eventId })),
|
|
2948
|
+
closing_event_id: r.account.closingEventId,
|
|
2949
|
+
reason: r.account.reason,
|
|
2950
|
+
rendered: escapeControl(renderAccountLine(r)),
|
|
2951
|
+
}));
|
|
2952
|
+
return [enclave, account];
|
|
2953
|
+
}
|
|
2954
|
+
|
|
2772
2955
|
async function caseCommand(argv: readonly string[], io: Cli): Promise<number> {
|
|
2773
2956
|
let asJson = false;
|
|
2774
2957
|
let gateways: string[] = [];
|
|
2775
2958
|
const paths: string[] = [];
|
|
2776
2959
|
let filePaths: string[] | undefined;
|
|
2960
|
+
let enclavePolicy: string | null = null;
|
|
2961
|
+
let enclaveRpc: string | null = null;
|
|
2777
2962
|
for (let i = 0; i < argv.length; i++) {
|
|
2778
2963
|
const arg = argv[i]!;
|
|
2779
2964
|
if ((arg === "--help" || arg === "-h" || arg === "help") && paths.length === 0) {
|
|
@@ -2803,6 +2988,16 @@ async function caseCommand(argv: readonly string[], io: Cli): Promise<number> {
|
|
|
2803
2988
|
}
|
|
2804
2989
|
filePaths = [...(filePaths ?? []), ...taken];
|
|
2805
2990
|
i += taken.length;
|
|
2991
|
+
} else if (arg === "--enclave-policy" || arg === "--enclave-rpc") {
|
|
2992
|
+
const next = argv[i + 1];
|
|
2993
|
+
if (next === undefined) {
|
|
2994
|
+
io.err(arg === "--enclave-policy" ? "case: --enclave-policy requires a policy file" : "case: --enclave-rpc requires a URL");
|
|
2995
|
+
outBlock(io, CASE_USAGE);
|
|
2996
|
+
return EXIT_MALFORMED;
|
|
2997
|
+
}
|
|
2998
|
+
if (arg === "--enclave-policy") enclavePolicy = next;
|
|
2999
|
+
else enclaveRpc = next;
|
|
3000
|
+
i++;
|
|
2806
3001
|
} else if (arg.startsWith("--")) {
|
|
2807
3002
|
io.err(`case: unknown option ${arg}`);
|
|
2808
3003
|
outBlock(io, CASE_USAGE);
|
|
@@ -2825,6 +3020,13 @@ async function caseCommand(argv: readonly string[], io: Cli): Promise<number> {
|
|
|
2825
3020
|
offered = read.offered;
|
|
2826
3021
|
skipped = read.skipped;
|
|
2827
3022
|
}
|
|
3023
|
+
let policy: unknown;
|
|
3024
|
+
try {
|
|
3025
|
+
policy = await readEnclavePolicy(enclavePolicy);
|
|
3026
|
+
} catch (e) {
|
|
3027
|
+
if (e instanceof CaseInputError) return caseError(e, asJson, io);
|
|
3028
|
+
throw e;
|
|
3029
|
+
}
|
|
2828
3030
|
|
|
2829
3031
|
const whole = { n: MAX_FOLDER_BYTES };
|
|
2830
3032
|
let split;
|
|
@@ -2854,18 +3056,33 @@ async function caseCommand(argv: readonly string[], io: Cli): Promise<number> {
|
|
|
2854
3056
|
for (const [p, bundle] of bundles) {
|
|
2855
3057
|
caches.push(readWitnessCache(bundle, await witnessCacheBytes(tree, split, p, whole, cacheRefused)));
|
|
2856
3058
|
}
|
|
3059
|
+
const confirmedStamps: string[][] = [];
|
|
2857
3060
|
const onChain = await onChainHalf(
|
|
2858
3061
|
bundles.map(([p, bundle], i): [string, unknown, CachedWitnessTime[]] => [p.ref, bundle, caches[i]!]),
|
|
2859
3062
|
gateways,
|
|
2860
3063
|
{ ...(io.fetchImpl ? { fetchImpl: io.fetchImpl } : {}) },
|
|
3064
|
+
confirmedStamps,
|
|
2861
3065
|
);
|
|
2862
3066
|
const witness = witnessLine(onChain, caches.flat());
|
|
2863
|
-
|
|
3067
|
+
// Sandbox evidence: over every envelope every pack holds, and per
|
|
3068
|
+
// pack for its row. Only a case that would otherwise exit 0 exits 5.
|
|
3069
|
+
const marker = environmentMarker(bundles.map(([, b]) => b));
|
|
3070
|
+
// Undetermined only because a simulated witness cannot be looked up: every
|
|
3071
|
+
// other witness confirmed, none unreachable or mismatched, every pack names
|
|
3072
|
+
// one. With every record signed `dev` that is sandbox evidence, exit 5.
|
|
3073
|
+
const simulatedOnly =
|
|
3074
|
+
reading.verdict === "verified" &&
|
|
3075
|
+
onChain.verdict === "undetermined" &&
|
|
3076
|
+
onChain.packs.some((r) => (r.simulated ?? 0) > 0) &&
|
|
3077
|
+
onChain.packs.every(
|
|
3078
|
+
(r) => r.witnesses > 0 && r.unreachable === 0 && r.mismatched === 0 && r.not_checked === (r.simulated ?? 0),
|
|
3079
|
+
);
|
|
3080
|
+
const code = sandboxExit(caseExitCode(reading.verdict, onChain.verdict), marker, simulatedOnly);
|
|
2864
3081
|
const nil = <T>(v: T | undefined): T | null => (v === undefined ? null : v);
|
|
2865
3082
|
|
|
2866
3083
|
const data = {
|
|
2867
3084
|
verdict: reading.verdict,
|
|
2868
|
-
title: titleOf(reading.verdict),
|
|
3085
|
+
title: reading.verdict === "verified" && marker !== null ? SANDBOX_TITLE : titleOf(reading.verdict),
|
|
2869
3086
|
reliance: caseReliance(reading.verdict, reading.handoffRecords),
|
|
2870
3087
|
exit_code: code,
|
|
2871
3088
|
summary: renderCaseSummary(reading),
|
|
@@ -2925,13 +3142,27 @@ async function caseCommand(argv: readonly string[], io: Cli): Promise<number> {
|
|
|
2925
3142
|
grade: reading.packs[i]!.grade,
|
|
2926
3143
|
records_withheld: reading.packs[i]!.recordsWithheld,
|
|
2927
3144
|
declaration_version: reading.packs[i]!.declarationVersion,
|
|
3145
|
+
...packMarker(bundles[i]![1]),
|
|
2928
3146
|
})),
|
|
2929
3147
|
outside: split.outside,
|
|
2930
3148
|
not_regular: split.notRegular,
|
|
2931
3149
|
case_file: caseFile,
|
|
2932
3150
|
on_chain: onChain,
|
|
2933
3151
|
...(cacheRefused.length > 0 ? { witness_cache_not_read: cacheRefused } : {}),
|
|
3152
|
+
...(marker !== null ? { environment_marker: marker } : {}),
|
|
2934
3153
|
};
|
|
3154
|
+
// The runs in the drop (DRAFT, specs/enclave-attestation.md and
|
|
3155
|
+
// run-records.md): an enclave attestation line and a run-account line per
|
|
3156
|
+
// run, beside the witness line. Neither moves `code`, and a drop that holds
|
|
3157
|
+
// no run prints and writes exactly what it did before.
|
|
3158
|
+
const runs = (
|
|
3159
|
+
await readRuns(
|
|
3160
|
+
bundles.map(([p, bundle]) => ({ ref: p.ref, bundle, content: p.content })),
|
|
3161
|
+
{ policy, adapters: await loadInstalledAdapters(), rpc: enclaveRpc, witnessed: confirmedStamps },
|
|
3162
|
+
)
|
|
3163
|
+
).runs;
|
|
3164
|
+
const withRuns: Record<string, unknown> = data;
|
|
3165
|
+
if (runs.length > 0) [withRuns.enclave_attestation, withRuns.run_account] = runLines(runs);
|
|
2935
3166
|
// The files outside every pack (evidence-bundle.md 5.7, A29, A41), always,
|
|
2936
3167
|
// then the files after --files. A pack's logs/ is its side content and is
|
|
2937
3168
|
// never an offered file; only the paths after --files are. Both are matched
|