@sanning/proof 0.4.1 → 0.6.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 +59 -4
- package/dist/cli.js +222 -43
- package/dist/compose.js +1 -1
- package/dist/evidence.d.ts +106 -7
- package/dist/evidence.js +446 -81
- package/dist/grading.d.ts +160 -0
- package/dist/grading.js +469 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +25 -1
- package/dist/side-content.d.ts +36 -0
- package/dist/side-content.js +98 -0
- package/package.json +2 -1
- package/src/cli.ts +285 -41
- package/src/compose.ts +1 -1
- package/src/evidence.ts +647 -92
- package/src/grading.ts +623 -0
- package/src/index.ts +49 -0
- package/src/side-content.ts +119 -0
- package/vectors/README.md +66 -0
- package/vectors/pack-grading.json +136 -0
package/src/index.ts
CHANGED
|
@@ -51,9 +51,20 @@ export type { ContentRole, Envelope, Subject, VerificationResult, VerifyOptions
|
|
|
51
51
|
// issuer-composed sanning.evidence.export/v1 body (specs/evidence-export.md) to
|
|
52
52
|
// verifyExportBody (embedded RSA-PSS operator attestations + a recomputed §4
|
|
53
53
|
// verdict object with per-gateway on-chain outcomes).
|
|
54
|
+
//
|
|
55
|
+
// `disclosedContent` is the INGEST half of the same path (evidence-bundle
|
|
56
|
+
// §5.1). A received bundle carries its disclosed raw bytes as lowercase hex, so
|
|
57
|
+
// materializing them was, until now, every consumer re-implementing the
|
|
58
|
+
// encoding convention — and a consumer that copied the field VERBATIM wrote
|
|
59
|
+
// files that failed verification on genuine evidence. This hands back DECODED
|
|
60
|
+
// bytes, and only for events whose verification bound them to the committed
|
|
61
|
+
// content_hash. Additive; verifyEvidenceBundle's behavior is unchanged.
|
|
54
62
|
export {
|
|
55
63
|
ANCHOR_TRACE_BODY_TYPE,
|
|
64
|
+
ANCHOR_TRACE_BODY_TYPE_V1,
|
|
65
|
+
ANCHOR_TRACE_BODY_TYPES,
|
|
56
66
|
EXPORT_BODY_TYPE,
|
|
67
|
+
disclosedContent,
|
|
57
68
|
recomputeExportVerdict,
|
|
58
69
|
verifyEvidenceBundle,
|
|
59
70
|
} from "./evidence.js";
|
|
@@ -64,6 +75,9 @@ export type {
|
|
|
64
75
|
AttestationResult,
|
|
65
76
|
CheckpointResult,
|
|
66
77
|
ComposeVerdictResult,
|
|
78
|
+
DisclosedContentResult,
|
|
79
|
+
DisclosedEventContent,
|
|
80
|
+
DisclosureStatus,
|
|
67
81
|
EvidenceBundle,
|
|
68
82
|
EvidenceBundleResult,
|
|
69
83
|
EvidenceIssuer,
|
|
@@ -75,7 +89,9 @@ export type {
|
|
|
75
89
|
OnChainOutcome,
|
|
76
90
|
OnChainResult,
|
|
77
91
|
PerGatewayOutcome,
|
|
92
|
+
AnchorRef,
|
|
78
93
|
TraceCheckpoint,
|
|
94
|
+
TraceCheckpointV1,
|
|
79
95
|
TraceEvent,
|
|
80
96
|
TraceInclusion,
|
|
81
97
|
VerdictAttestation,
|
|
@@ -87,6 +103,39 @@ export type {
|
|
|
87
103
|
VerifyEvidenceOptions,
|
|
88
104
|
} from "./evidence.js";
|
|
89
105
|
|
|
106
|
+
// Pack grading (src/grading.ts): what a verification SURFACE may claim about a
|
|
107
|
+
// pack — the signed bundle PLUS whatever disclosed bytes travelled beside it.
|
|
108
|
+
//
|
|
109
|
+
// The rule is already normative in evidence-bundle.md §5.1 (absent disclosure is
|
|
110
|
+
// undetermined, not failed) and §6 (no status ever means "safe"); what the
|
|
111
|
+
// surfaces added on top was a sharper word for undetermined and an account of
|
|
112
|
+
// what the pack failed to disclose. It lived twice, in two repos and two
|
|
113
|
+
// languages, and the two derivations had already diverged once. It lives here.
|
|
114
|
+
//
|
|
115
|
+
// Pure — data in, data out, no I/O, so the kernel's scope boundary
|
|
116
|
+
// (specs/architecture.md §3) is unchanged. `vectors/pack-grading.json` is the
|
|
117
|
+
// conformance statement of the rule and ships in the published tarball, so a
|
|
118
|
+
// consumer that cannot yet import `gradePack` can vendor the table, hold its own
|
|
119
|
+
// derivation to it, and diff its copy against ours. Additive — no existing
|
|
120
|
+
// export changes.
|
|
121
|
+
export {
|
|
122
|
+
accountDisclosure,
|
|
123
|
+
completenessNotes,
|
|
124
|
+
gradeCauses,
|
|
125
|
+
gradePack,
|
|
126
|
+
renderCause,
|
|
127
|
+
signedLeafCount,
|
|
128
|
+
} from "./grading.js";
|
|
129
|
+
export type {
|
|
130
|
+
CompletenessNote,
|
|
131
|
+
DisclosureAccount,
|
|
132
|
+
GradeInput,
|
|
133
|
+
PackGrade,
|
|
134
|
+
PackGradeCause,
|
|
135
|
+
PackGradeCauseKind,
|
|
136
|
+
PackVerdict,
|
|
137
|
+
} from "./grading.js";
|
|
138
|
+
|
|
90
139
|
// The `proof export` composer (specs/evidence-export.md §5, phase 2): produce a
|
|
91
140
|
// signed, offline-verifiable sanning.evidence.export/v1 artifact from a source
|
|
92
141
|
// anchor-trace bundle + operator attestations + an exporter Ed25519 key. The
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// ONE answer to "did the holder actually supply bytes for this event_id?"
|
|
2
|
+
//
|
|
3
|
+
// WHY THIS IS A MODULE AND NOT TWO PRIVATE HELPERS.
|
|
4
|
+
// The kernel asks that question in two places — the verifier, deciding whether
|
|
5
|
+
// an event's content was offered at all (`evidence.ts`), and the grader,
|
|
6
|
+
// accounting which records a pack disclosed (`grading.ts`). It had two answers.
|
|
7
|
+
// On one and the same content map the grader said PRESENT while the verifier
|
|
8
|
+
// said nothing was offered, so `gradePack` returned `incomplete` with ZERO
|
|
9
|
+
// causes: a surface saying "incomplete" and unable to say why. That is exactly
|
|
10
|
+
// the outcome `grading.ts`'s own comment claims was removed, reappearing
|
|
11
|
+
// because the fix landed on one of the two definitions.
|
|
12
|
+
//
|
|
13
|
+
// Two implementations of one verification rule is the defect `grading.ts` was
|
|
14
|
+
// extracted into the kernel to end. A rule stated once cannot drift; a rule
|
|
15
|
+
// stated twice already has.
|
|
16
|
+
//
|
|
17
|
+
// ── THE RULE ────────────────────────────────────────────────────────────────
|
|
18
|
+
//
|
|
19
|
+
// An entry exists for `eventId` when the map carries an OWN, ENUMERABLE, DATA
|
|
20
|
+
// property (or `Map` key) holding something other than `null`/`undefined`.
|
|
21
|
+
// This is the STRICTER of the two definitions that were in the kernel — the
|
|
22
|
+
// verifier's — and it is stricter on purpose. Every clause was paid for:
|
|
23
|
+
//
|
|
24
|
+
// own — an `event_id` is PRODUCER-CHOSEN, and every key on
|
|
25
|
+
// `Object.prototype` is a legal one: `constructor`,
|
|
26
|
+
// `toString`, `valueOf`, `hasOwnProperty`, `isPrototypeOf`,
|
|
27
|
+
// `propertyIsEnumerable`, `toLocaleString`, `__proto__`. An
|
|
28
|
+
// indexed lookup walks the prototype chain and resolves every
|
|
29
|
+
// one of them, so absent evidence read as good evidence.
|
|
30
|
+
// enumerable — what a caller means by "the map I built" is what
|
|
31
|
+
// `Object.keys` shows; nothing hidden behind it counts.
|
|
32
|
+
// data — an accessor is NEVER invoked. A getter can throw, and this
|
|
33
|
+
// kernel's standing rule is that a verifier does not raise on
|
|
34
|
+
// adversarial input. Not hypothetical: a plain-object map that
|
|
35
|
+
// has taken a `__proto__` write carries `Uint8Array.prototype`
|
|
36
|
+
// as its prototype, and reading `byteLength`, `length`,
|
|
37
|
+
// `buffer` or `byteOffset` off a plain object through it throws
|
|
38
|
+
// a TypeError. Reading a descriptor cannot.
|
|
39
|
+
// non-nullish — `{ [id]: undefined }` is how `Object.fromEntries` and friends
|
|
40
|
+
// spell "no entry", and `new Map([[id, undefined]])` is the
|
|
41
|
+
// same statement in the other shape. Anything else non-nullish
|
|
42
|
+
// — a number, an object, a string that is not hex — IS a real,
|
|
43
|
+
// unusable disclosure and stays a finding.
|
|
44
|
+
//
|
|
45
|
+
// `offered` is what separates "nothing was disclosed" (undetermined) from
|
|
46
|
+
// "something was disclosed and is unusable" (a finding), and absent disclosure
|
|
47
|
+
// is undetermined, not failed (evidence-bundle.md §5.1). So it is returned
|
|
48
|
+
// SEPARATELY from the value and is never inferred from it: `value !== undefined`
|
|
49
|
+
// is the conflation this module exists to remove, in both directions.
|
|
50
|
+
|
|
51
|
+
/** A caller-built map of disclosed bytes, in either shape it can take. */
|
|
52
|
+
export type SideContentMap =
|
|
53
|
+
| Readonly<Record<string, Uint8Array | string | undefined>>
|
|
54
|
+
| ReadonlyMap<string, Uint8Array | string | undefined>;
|
|
55
|
+
|
|
56
|
+
/** One event's entry, and — separately — whether an entry existed at all. */
|
|
57
|
+
export interface SideEntry {
|
|
58
|
+
/** An entry EXISTS for this id. Never inferred from the value. */
|
|
59
|
+
offered: boolean;
|
|
60
|
+
/** What it held, read without ever invoking an accessor. */
|
|
61
|
+
value: Uint8Array | string | undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const ABSENT: SideEntry = { offered: false, value: undefined };
|
|
65
|
+
|
|
66
|
+
/** Look up one event's out-of-band disclosure. The rule, stated once. */
|
|
67
|
+
export function sideContentEntry(
|
|
68
|
+
content: SideContentMap | null | undefined,
|
|
69
|
+
eventId: string,
|
|
70
|
+
): SideEntry {
|
|
71
|
+
if (content === undefined || content === null) return ABSENT;
|
|
72
|
+
if (content instanceof Map) {
|
|
73
|
+
const value = content.get(eventId);
|
|
74
|
+
return value === undefined || value === null ? ABSENT : { offered: true, value };
|
|
75
|
+
}
|
|
76
|
+
const desc = Object.getOwnPropertyDescriptor(content, eventId);
|
|
77
|
+
if (desc === undefined || !desc.enumerable || !("value" in desc)) return ABSENT;
|
|
78
|
+
const value = desc.value as Uint8Array | string | undefined;
|
|
79
|
+
return value === undefined || value === null ? ABSENT : { offered: true, value };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Whether the map carries a usable entry for this id. The same rule. */
|
|
83
|
+
export function hasSideContent(
|
|
84
|
+
content: SideContentMap | null | undefined,
|
|
85
|
+
eventId: string,
|
|
86
|
+
): boolean {
|
|
87
|
+
return sideContentEntry(content, eventId).offered;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The ids the map actually carries — the same rule applied to every key, so a
|
|
92
|
+
* key that is not an entry cannot be reported as surplus disclosure either.
|
|
93
|
+
*/
|
|
94
|
+
export function sideContentKeys(content: SideContentMap | null | undefined): string[] {
|
|
95
|
+
if (content === undefined || content === null) return [];
|
|
96
|
+
const keys = content instanceof Map ? [...content.keys()] : Object.keys(content);
|
|
97
|
+
return keys.filter((id) => hasSideContent(content, id));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Whether an event's IN-BODY `events[].content` field is a disclosure.
|
|
102
|
+
*
|
|
103
|
+
* The other half of the same question, and it broke the same way. `content`
|
|
104
|
+
* is an OPTIONAL field, so `null` is how a producer's serializer spells its
|
|
105
|
+
* absence — Go's `json.Marshal` on a nil pointer without `omitempty`, Python's
|
|
106
|
+
* `json.dumps(None)`. A bare `!== undefined` marks such an event OFFERED while
|
|
107
|
+
* no bytes are selected (only a `string` decodes), and the kernel then splits
|
|
108
|
+
* against itself: `verifyEvidenceBundle` reports `contentOk: null` and rolls up
|
|
109
|
+
* VERIFIED, while `disclosedContent` reports FAILED — "content was disclosed
|
|
110
|
+
* for this event but is not a decodable lowercase-hex string". One kernel, one
|
|
111
|
+
* bundle, two verdicts, reachable from any conforming producer.
|
|
112
|
+
*
|
|
113
|
+
* Non-nullish and not a string — a number, an object, a hex string that is not
|
|
114
|
+
* hex — remains a real, unusable disclosure and stays a finding, exactly as on
|
|
115
|
+
* the side-input path.
|
|
116
|
+
*/
|
|
117
|
+
export function inBodyContentOffered(content: unknown): boolean {
|
|
118
|
+
return content !== undefined && content !== null;
|
|
119
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# `vectors/` — conformance tables that ship to consumers
|
|
2
|
+
|
|
3
|
+
Unlike [`../../test-vectors/`](../../test-vectors/) — the tagged, vendored corpus
|
|
4
|
+
for the kernel primitives — everything here is **inside the published package**
|
|
5
|
+
(`files: ["vectors", …]`) and reaches downstream surfaces through npm.
|
|
6
|
+
|
|
7
|
+
That distinction is the whole point. A rule that consumers must agree with needs
|
|
8
|
+
a statement they can *fetch*, not one they have to be trusted to have copied
|
|
9
|
+
correctly.
|
|
10
|
+
|
|
11
|
+
## `pack-grading.json`
|
|
12
|
+
|
|
13
|
+
The rule that decides whether a **pack** — a signed bundle plus whatever
|
|
14
|
+
disclosed bytes travelled beside it — is `verified`, `failed` or `incomplete`.
|
|
15
|
+
Implemented by [`../src/grading.ts`](../src/grading.ts) and asserted against this
|
|
16
|
+
table by [`../test/grading.test.ts`](../test/grading.test.ts).
|
|
17
|
+
|
|
18
|
+
**If an implementation disagrees with this table, the implementation is wrong.**
|
|
19
|
+
|
|
20
|
+
### For a consumer that can import the kernel
|
|
21
|
+
|
|
22
|
+
Don't vendor anything. Call `gradePack`:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { accountDisclosure, completenessNotes, gradePack, verifyEvidenceBundle } from "@sanning/proof"
|
|
26
|
+
|
|
27
|
+
const result = await verifyEvidenceBundle(bundle, { content })
|
|
28
|
+
const account = accountDisclosure(bundle, content)
|
|
29
|
+
const grade = gradePack(result, account, completenessNotes(bundle))
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
One implementation, no table to keep in step, no drift to guard against.
|
|
33
|
+
|
|
34
|
+
### For a consumer that cannot yet
|
|
35
|
+
|
|
36
|
+
An older pinned kernel, another language, a surface that adapts a result from
|
|
37
|
+
somewhere else. Then, and only then:
|
|
38
|
+
|
|
39
|
+
1. **Vendor this file byte-for-byte.** Not "equivalent" — identical. A vendored
|
|
40
|
+
copy that is not byte-identical to this one is a defect, not a variant.
|
|
41
|
+
2. **Assert your derivation against it** in your own suite, so an implementation
|
|
42
|
+
that drifts from the rule goes red in your repo.
|
|
43
|
+
3. **Run a drift check in CI** that compares your copy against the copy in
|
|
44
|
+
`node_modules/@sanning/proof/vectors/pack-grading.json` and fails on any
|
|
45
|
+
difference — *and fails when it cannot find a copy to compare against.* A
|
|
46
|
+
drift check that skips when its source is missing is not a check.
|
|
47
|
+
|
|
48
|
+
Step 3 is the one people skip, and it is the one that matters. Steps 1 and 2
|
|
49
|
+
catch an implementation drifting from its own table. Only step 3 catches the
|
|
50
|
+
table itself being edited on one side — which is the failure that actually
|
|
51
|
+
happened, and the reason this file moved here.
|
|
52
|
+
|
|
53
|
+
### Shape
|
|
54
|
+
|
|
55
|
+
| Key | Meaning |
|
|
56
|
+
|---|---|
|
|
57
|
+
| `ruleVersion` | Bumped when a case's expectation changes **or a case is added**. An implementation asserts the version it was written against, so a table it does not understand fails loudly rather than passing vacuously. Currently **2**. |
|
|
58
|
+
| `kernelEventClasses` | The four per-event outcome classes a verification result can carry, named so a case can be written as counts rather than as a hand-built bundle. |
|
|
59
|
+
| `cases[].kernel` | The verification result to grade, as class counts. |
|
|
60
|
+
| `cases[].logs` | The disclosure account: how many records the bundle presents, and how many disclosed usable bytes. |
|
|
61
|
+
| `cases[].omissions` | Completeness notes — a checkpoint's signed `leaf_count` against the records the pack presents for it. |
|
|
62
|
+
| `cases[].expect` | The verdict, the three counts, and how many causes the grade must carry. |
|
|
63
|
+
|
|
64
|
+
Nothing in a case names a transport. A directory read and a browser drop produce
|
|
65
|
+
the same account and must produce the same verdict; a table that encoded one
|
|
66
|
+
surface's mechanism would let the other one drift while staying green.
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
{
|
|
2
|
+
"rule": "pack-grading",
|
|
3
|
+
"ruleVersion": 2,
|
|
4
|
+
"statement": "What a verification surface may CLAIM about a pack it was handed. verified is reachable ONLY when every record's own bytes bound to its own commitment and nothing is missing, unreadable, surplus or omitted. Absent or unreadable evidence is incomplete (UNKNOWN), never failed and never verified. A hard kernel verdict wins: malformed > failed > incomplete > the kernel's own headline.",
|
|
5
|
+
"home": "@sanning/proof — ts/vectors/pack-grading.json. This file is the RULE, not a test fixture of one implementation. The kernel implements it in ts/src/grading.ts and is held to it by ts/test/grading.test.ts.",
|
|
6
|
+
"consumers": "A surface that grades packs SHOULD call the kernel's gradePack rather than derive the rule again. A surface that cannot yet (an older pinned kernel, another language) vendors this file byte-for-byte, asserts its own derivation against it, and runs a drift check that compares its copy against the copy shipped in the @sanning/proof tarball at vectors/pack-grading.json. A vendored copy that is not byte-identical to this one is a defect, not a variant.",
|
|
7
|
+
"howToUse": "Adapt a kernel verification result and a disclosure account into the shapes below, call gradePack, and assert the expectations. `kernel` gives the per-event outcome classes the kernel returns; `logs` gives the account of which records disclosed bytes; `omissions` gives the signed-leaf_count completeness notes. Nothing here depends on how the bytes were transported — a directory read and a browser drop produce the same account and must produce the same verdict.",
|
|
8
|
+
"kernelEventClasses": {
|
|
9
|
+
"okBound": "{ ok: true, contentOk: true } — the record's own bytes hashed to its own commitment",
|
|
10
|
+
"okUnknown": "{ ok: true, contentOk: null } — structurally fine, but no bytes were disclosed",
|
|
11
|
+
"mismatched": "{ ok: false, contentOk: false } — the bytes disagree with the commitment",
|
|
12
|
+
"brokenNoContent": "{ ok: false, contentOk: null } — the record itself does not verify"
|
|
13
|
+
},
|
|
14
|
+
"cases": [
|
|
15
|
+
{
|
|
16
|
+
"id": "clean-pack",
|
|
17
|
+
"why": "a real, untouched pack: every record's own bytes bound to its own commitment",
|
|
18
|
+
"kernel": { "status": "verified", "okBound": 26, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0 },
|
|
19
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
20
|
+
"omissions": [],
|
|
21
|
+
"expect": { "verdict": "verified", "recordsVerified": 26, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 0 }
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "one-log-tampered",
|
|
25
|
+
"why": "the scripted decision-word tamper — one record's bytes disagree",
|
|
26
|
+
"kernel": { "status": "failed", "okBound": 25, "okUnknown": 0, "mismatched": 1, "brokenNoContent": 0 },
|
|
27
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
28
|
+
"omissions": [],
|
|
29
|
+
"expect": { "verdict": "failed", "recordsVerified": 25, "recordsFailed": 1, "recordsUndetermined": 0, "causeCount": 0 }
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"id": "two-logs-swapped",
|
|
33
|
+
"why": "the case a set-membership check passes: both digests are still in the set, but each is now bound to the wrong record",
|
|
34
|
+
"kernel": { "status": "failed", "okBound": 24, "okUnknown": 0, "mismatched": 2, "brokenNoContent": 0 },
|
|
35
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
36
|
+
"omissions": [],
|
|
37
|
+
"expect": { "verdict": "failed", "recordsVerified": 24, "recordsFailed": 2, "recordsUndetermined": 0, "causeCount": 0 }
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"id": "one-log-deleted",
|
|
41
|
+
"why": "absent bytes are UNKNOWN — never verified, never failed (P-10)",
|
|
42
|
+
"kernel": { "status": "verified", "okBound": 25, "okUnknown": 1, "mismatched": 0, "brokenNoContent": 0 },
|
|
43
|
+
"logs": { "expected": 26, "present": 25, "missing": 1, "unreadable": 0, "extra": 0 },
|
|
44
|
+
"omissions": [],
|
|
45
|
+
"expect": { "verdict": "incomplete", "recordsVerified": 25, "recordsFailed": 0, "recordsUndetermined": 1, "causeCount": 1 }
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"id": "every-log-deleted",
|
|
49
|
+
"why": "the silent-catch regression: zero bytes hashed must never read as a verified pack",
|
|
50
|
+
"kernel": { "status": "verified", "okBound": 0, "okUnknown": 26, "mismatched": 0, "brokenNoContent": 0 },
|
|
51
|
+
"logs": { "expected": 26, "present": 0, "missing": 26, "unreadable": 0, "extra": 0 },
|
|
52
|
+
"omissions": [],
|
|
53
|
+
"expect": { "verdict": "incomplete", "recordsVerified": 0, "recordsFailed": 0, "recordsUndetermined": 26, "causeCount": 1 }
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"id": "one-log-unreadable",
|
|
57
|
+
"why": "a file we could not read is our fault, not the record's — UNKNOWN",
|
|
58
|
+
"kernel": { "status": "verified", "okBound": 25, "okUnknown": 1, "mismatched": 0, "brokenNoContent": 0 },
|
|
59
|
+
"logs": { "expected": 26, "present": 25, "missing": 0, "unreadable": 1, "extra": 0 },
|
|
60
|
+
"omissions": [],
|
|
61
|
+
"expect": { "verdict": "incomplete", "recordsVerified": 25, "recordsFailed": 0, "recordsUndetermined": 1, "causeCount": 1 }
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"id": "unaccounted-file",
|
|
65
|
+
"why": "a log no record in the bundle accounts for: the pack is not the pack it claims to be",
|
|
66
|
+
"kernel": { "status": "verified", "okBound": 26, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0 },
|
|
67
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 1 },
|
|
68
|
+
"omissions": [],
|
|
69
|
+
"expect": { "verdict": "incomplete", "recordsVerified": 26, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 1 }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "records-omitted",
|
|
73
|
+
"why": "every surviving record verifies perfectly; only the checkpoint's signed leaf_count sees the gap",
|
|
74
|
+
"kernel": { "status": "verified", "okBound": 24, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0 },
|
|
75
|
+
"logs": { "expected": 24, "present": 24, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
76
|
+
"omissions": [{ "kind": "omitted", "checkpointRef": "TX", "signed": 26, "presented": 24 }],
|
|
77
|
+
"expect": { "verdict": "incomplete", "recordsVerified": 24, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 1 }
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"id": "records-surplus",
|
|
81
|
+
"why": "more records than the signed tree accounts for is equally not-verified",
|
|
82
|
+
"kernel": { "status": "verified", "okBound": 26, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0 },
|
|
83
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
84
|
+
"omissions": [{ "kind": "surplus", "checkpointRef": "TX", "signed": 24, "presented": 26 }],
|
|
85
|
+
"expect": { "verdict": "incomplete", "recordsVerified": 26, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 1 }
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"id": "leaf-count-unreadable",
|
|
89
|
+
"why": "a completeness check we could not run is UNKNOWN, not a pass",
|
|
90
|
+
"kernel": { "status": "verified", "okBound": 26, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0 },
|
|
91
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
92
|
+
"omissions": [{ "kind": "unreadable", "checkpointRef": "TX", "signed": null, "presented": null }],
|
|
93
|
+
"expect": { "verdict": "incomplete", "recordsVerified": 26, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 1 }
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"id": "malformed-wins",
|
|
97
|
+
"why": "an unreadable bundle is named for what it is, whatever else is true",
|
|
98
|
+
"kernel": { "status": "malformed", "okBound": 0, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0 },
|
|
99
|
+
"logs": { "expected": 0, "present": 0, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
100
|
+
"omissions": [],
|
|
101
|
+
"expect": { "verdict": "malformed", "recordsVerified": 0, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 0 }
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
"id": "tamper-beats-absence",
|
|
105
|
+
"why": "a proven mismatch is still a failure even when other records were withheld — absence never masks tamper",
|
|
106
|
+
"kernel": { "status": "failed", "okBound": 24, "okUnknown": 1, "mismatched": 1, "brokenNoContent": 0 },
|
|
107
|
+
"logs": { "expected": 26, "present": 25, "missing": 1, "unreadable": 0, "extra": 0 },
|
|
108
|
+
"omissions": [],
|
|
109
|
+
"expect": { "verdict": "failed", "recordsVerified": 24, "recordsFailed": 1, "recordsUndetermined": 1, "causeCount": 1 }
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"id": "broken-record-is-failure-not-unknown",
|
|
113
|
+
"why": "a record that does not verify is a failure even with no content disclosed for it",
|
|
114
|
+
"kernel": { "status": "failed", "okBound": 25, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 1 },
|
|
115
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
116
|
+
"omissions": [],
|
|
117
|
+
"expect": { "verdict": "failed", "recordsVerified": 25, "recordsFailed": 1, "recordsUndetermined": 0, "causeCount": 0 }
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
"id": "kernel-headline-passes-through",
|
|
121
|
+
"why": "this rule downgrades a verdict; it never invents one",
|
|
122
|
+
"kernel": { "status": "partial", "okBound": 26, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0 },
|
|
123
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
124
|
+
"omissions": [],
|
|
125
|
+
"expect": { "verdict": "partial", "recordsVerified": 26, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 0 }
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"id": "unknown-headline-is-not-repeated",
|
|
129
|
+
"why": "a headline this rule cannot interpret is a check it could not run — UNKNOWN, never repeated verbatim. The status a surface prints must not be attacker-chosen: passing an uninterpreted string through is how a pack could be headlined with the one word the trust model forbids",
|
|
130
|
+
"kernel": { "status": "SAFE", "okBound": 26, "okUnknown": 0, "mismatched": 0, "brokenNoContent": 0 },
|
|
131
|
+
"logs": { "expected": 26, "present": 26, "missing": 0, "unreadable": 0, "extra": 0 },
|
|
132
|
+
"omissions": [],
|
|
133
|
+
"expect": { "verdict": "incomplete", "recordsVerified": 26, "recordsFailed": 0, "recordsUndetermined": 0, "causeCount": 0 }
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|