@sanning/proof 0.5.0 → 0.7.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 +31 -4
- package/dist/chain.d.ts +99 -0
- package/dist/chain.js +351 -0
- package/dist/cli.js +253 -38
- package/dist/compose.js +1 -1
- package/dist/evidence.d.ts +204 -7
- package/dist/evidence.js +621 -67
- package/dist/grading.d.ts +160 -0
- package/dist/grading.js +469 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +38 -1
- package/dist/side-content.d.ts +36 -0
- package/dist/side-content.js +98 -0
- package/dist/verifier.js +46 -7
- package/package.json +2 -1
- package/src/chain.ts +460 -0
- package/src/cli.ts +324 -35
- package/src/compose.ts +1 -1
- package/src/evidence.ts +875 -81
- package/src/grading.ts +623 -0
- package/src/index.ts +79 -0
- package/src/side-content.ts +119 -0
- package/src/verifier.ts +49 -7
- package/vectors/README.md +66 -0
- package/vectors/chain-reading.json +293 -0
- package/vectors/pack-grading.json +136 -0
package/dist/index.js
CHANGED
|
@@ -34,7 +34,44 @@ export { EMPTY_TREE_ROOT_HEX, auditPath, leafHash, merkleRoot, nodeHash, verifyI
|
|
|
34
34
|
// files that failed verification on genuine evidence. This hands back DECODED
|
|
35
35
|
// bytes, and only for events whose verification bound them to the committed
|
|
36
36
|
// content_hash. Additive; verifyEvidenceBundle's behavior is unchanged.
|
|
37
|
-
export { ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
|
|
37
|
+
export { ANCHOR_TRACE_BODY_TYPE, ANCHOR_TRACE_BODY_TYPE_V1, ANCHOR_TRACE_BODY_TYPES, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
|
|
38
|
+
// Pack grading (src/grading.ts): what a verification SURFACE may claim about a
|
|
39
|
+
// pack — the signed bundle PLUS whatever disclosed bytes travelled beside it.
|
|
40
|
+
//
|
|
41
|
+
// The rule is already normative in evidence-bundle.md §5.1 (absent disclosure is
|
|
42
|
+
// undetermined, not failed) and §6 (no status ever means "safe"); what the
|
|
43
|
+
// surfaces added on top was a sharper word for undetermined and an account of
|
|
44
|
+
// what the pack failed to disclose. It lived twice, in two repos and two
|
|
45
|
+
// languages, and the two derivations had already diverged once. It lives here.
|
|
46
|
+
//
|
|
47
|
+
// Pure — data in, data out, no I/O, so the kernel's scope boundary
|
|
48
|
+
// (specs/architecture.md §3) is unchanged. `vectors/pack-grading.json` is the
|
|
49
|
+
// conformance statement of the rule and ships in the published tarball, so a
|
|
50
|
+
// consumer that cannot yet import `gradePack` can vendor the table, hold its own
|
|
51
|
+
// derivation to it, and diff its copy against ours. Additive — no existing
|
|
52
|
+
// export changes.
|
|
53
|
+
export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
|
|
54
|
+
// Chain reading (src/chain.ts): following the pointer every sanning.events/v1
|
|
55
|
+
// record carries to the record before it, over the records ONE bundle presents.
|
|
56
|
+
//
|
|
57
|
+
// Every anchored event has carried that pointer since the profile was ratified
|
|
58
|
+
// and nothing we shipped read it, so a bundle with an event removed — and a
|
|
59
|
+
// chain that restarted after a crash and now asserts two beginnings — passed
|
|
60
|
+
// every check we had (proof#43). The verifier was right not to do this: it
|
|
61
|
+
// verifies a single envelope and says so. Nothing above it did it either.
|
|
62
|
+
//
|
|
63
|
+
// Pure — data in, data out, no I/O — so the kernel's scope boundary
|
|
64
|
+
// (specs/architecture.md §3) is unchanged, and the rule ships as
|
|
65
|
+
// `vectors/chain-reading.json` beside the pack-grading table for the same
|
|
66
|
+
// reason: a rule consumers must agree with needs a statement they can fetch.
|
|
67
|
+
//
|
|
68
|
+
// It returns FINDINGS, never a verdict, and it feeds no status. Chain links are
|
|
69
|
+
// deletion/reorder-evidence for the events present and NOT a completeness proof
|
|
70
|
+
// (`anchor/docs/profile-sanning.events-v1.md` §5) — drop the last N events of a
|
|
71
|
+
// chain and nothing dangles. `CHAIN_READING_SCOPE` is that sentence, exported so
|
|
72
|
+
// every surface prints the same limit rather than paraphrasing it into a claim.
|
|
73
|
+
// Additive — no existing export changes.
|
|
74
|
+
export { ADAPTER_CHAIN_NAMESPACES, CHAIN_READING_SCOPE, readChains, renderChainFinding, } from "./chain.js";
|
|
38
75
|
// The `proof export` composer (specs/evidence-export.md §5, phase 2): produce a
|
|
39
76
|
// signed, offline-verifiable sanning.evidence.export/v1 artifact from a source
|
|
40
77
|
// anchor-trace bundle + operator attestations + an exporter Ed25519 key. The
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** A caller-built map of disclosed bytes, in either shape it can take. */
|
|
2
|
+
export type SideContentMap = Readonly<Record<string, Uint8Array | string | undefined>> | ReadonlyMap<string, Uint8Array | string | undefined>;
|
|
3
|
+
/** One event's entry, and — separately — whether an entry existed at all. */
|
|
4
|
+
export interface SideEntry {
|
|
5
|
+
/** An entry EXISTS for this id. Never inferred from the value. */
|
|
6
|
+
offered: boolean;
|
|
7
|
+
/** What it held, read without ever invoking an accessor. */
|
|
8
|
+
value: Uint8Array | string | undefined;
|
|
9
|
+
}
|
|
10
|
+
/** Look up one event's out-of-band disclosure. The rule, stated once. */
|
|
11
|
+
export declare function sideContentEntry(content: SideContentMap | null | undefined, eventId: string): SideEntry;
|
|
12
|
+
/** Whether the map carries a usable entry for this id. The same rule. */
|
|
13
|
+
export declare function hasSideContent(content: SideContentMap | null | undefined, eventId: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* The ids the map actually carries — the same rule applied to every key, so a
|
|
16
|
+
* key that is not an entry cannot be reported as surplus disclosure either.
|
|
17
|
+
*/
|
|
18
|
+
export declare function sideContentKeys(content: SideContentMap | null | undefined): string[];
|
|
19
|
+
/**
|
|
20
|
+
* Whether an event's IN-BODY `events[].content` field is a disclosure.
|
|
21
|
+
*
|
|
22
|
+
* The other half of the same question, and it broke the same way. `content`
|
|
23
|
+
* is an OPTIONAL field, so `null` is how a producer's serializer spells its
|
|
24
|
+
* absence — Go's `json.Marshal` on a nil pointer without `omitempty`, Python's
|
|
25
|
+
* `json.dumps(None)`. A bare `!== undefined` marks such an event OFFERED while
|
|
26
|
+
* no bytes are selected (only a `string` decodes), and the kernel then splits
|
|
27
|
+
* against itself: `verifyEvidenceBundle` reports `contentOk: null` and rolls up
|
|
28
|
+
* VERIFIED, while `disclosedContent` reports FAILED — "content was disclosed
|
|
29
|
+
* for this event but is not a decodable lowercase-hex string". One kernel, one
|
|
30
|
+
* bundle, two verdicts, reachable from any conforming producer.
|
|
31
|
+
*
|
|
32
|
+
* Non-nullish and not a string — a number, an object, a hex string that is not
|
|
33
|
+
* hex — remains a real, unusable disclosure and stays a finding, exactly as on
|
|
34
|
+
* the side-input path.
|
|
35
|
+
*/
|
|
36
|
+
export declare function inBodyContentOffered(content: unknown): boolean;
|
|
@@ -0,0 +1,98 @@
|
|
|
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
|
+
const ABSENT = { offered: false, value: undefined };
|
|
51
|
+
/** Look up one event's out-of-band disclosure. The rule, stated once. */
|
|
52
|
+
export function sideContentEntry(content, eventId) {
|
|
53
|
+
if (content === undefined || content === null)
|
|
54
|
+
return ABSENT;
|
|
55
|
+
if (content instanceof Map) {
|
|
56
|
+
const value = content.get(eventId);
|
|
57
|
+
return value === undefined || value === null ? ABSENT : { offered: true, value };
|
|
58
|
+
}
|
|
59
|
+
const desc = Object.getOwnPropertyDescriptor(content, eventId);
|
|
60
|
+
if (desc === undefined || !desc.enumerable || !("value" in desc))
|
|
61
|
+
return ABSENT;
|
|
62
|
+
const value = desc.value;
|
|
63
|
+
return value === undefined || value === null ? ABSENT : { offered: true, value };
|
|
64
|
+
}
|
|
65
|
+
/** Whether the map carries a usable entry for this id. The same rule. */
|
|
66
|
+
export function hasSideContent(content, eventId) {
|
|
67
|
+
return sideContentEntry(content, eventId).offered;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The ids the map actually carries — the same rule applied to every key, so a
|
|
71
|
+
* key that is not an entry cannot be reported as surplus disclosure either.
|
|
72
|
+
*/
|
|
73
|
+
export function sideContentKeys(content) {
|
|
74
|
+
if (content === undefined || content === null)
|
|
75
|
+
return [];
|
|
76
|
+
const keys = content instanceof Map ? [...content.keys()] : Object.keys(content);
|
|
77
|
+
return keys.filter((id) => hasSideContent(content, id));
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Whether an event's IN-BODY `events[].content` field is a disclosure.
|
|
81
|
+
*
|
|
82
|
+
* The other half of the same question, and it broke the same way. `content`
|
|
83
|
+
* is an OPTIONAL field, so `null` is how a producer's serializer spells its
|
|
84
|
+
* absence — Go's `json.Marshal` on a nil pointer without `omitempty`, Python's
|
|
85
|
+
* `json.dumps(None)`. A bare `!== undefined` marks such an event OFFERED while
|
|
86
|
+
* no bytes are selected (only a `string` decodes), and the kernel then splits
|
|
87
|
+
* against itself: `verifyEvidenceBundle` reports `contentOk: null` and rolls up
|
|
88
|
+
* VERIFIED, while `disclosedContent` reports FAILED — "content was disclosed
|
|
89
|
+
* for this event but is not a decodable lowercase-hex string". One kernel, one
|
|
90
|
+
* bundle, two verdicts, reachable from any conforming producer.
|
|
91
|
+
*
|
|
92
|
+
* Non-nullish and not a string — a number, an object, a hex string that is not
|
|
93
|
+
* hex — remains a real, unusable disclosure and stays a finding, exactly as on
|
|
94
|
+
* the side-input path.
|
|
95
|
+
*/
|
|
96
|
+
export function inBodyContentOffered(content) {
|
|
97
|
+
return content !== undefined && content !== null;
|
|
98
|
+
}
|
package/dist/verifier.js
CHANGED
|
@@ -215,6 +215,18 @@ export async function verifyEnvelope(env, optionsOrContentHash) {
|
|
|
215
215
|
const opts = typeof optionsOrContentHash === "string"
|
|
216
216
|
? { expectedContentHash: optionsOrContentHash }
|
|
217
217
|
: (optionsOrContentHash ?? {});
|
|
218
|
+
// REFUSE an unknown option key rather than ignore it. `expected_content_hash`
|
|
219
|
+
// — the Python kernel's spelling — previously fell through as `undefined`, so
|
|
220
|
+
// the content bind silently never ran and the caller got a clean pass with no
|
|
221
|
+
// errors at all. A check that quietly does not happen is worse than one that
|
|
222
|
+
// fails: the caller believes they verified an artifact they never checked.
|
|
223
|
+
const KNOWN_VERIFY_OPTIONS = new Set(["payloadBytes", "expectedContentHash"]);
|
|
224
|
+
const unknownOptions = Object.keys(opts).filter((k) => !KNOWN_VERIFY_OPTIONS.has(k));
|
|
225
|
+
if (unknownOptions.length > 0) {
|
|
226
|
+
throw new TypeError(`verifyEnvelope: unknown option(s) ${unknownOptions.join(", ")}. ` +
|
|
227
|
+
`Did you mean ${[...KNOWN_VERIFY_OPTIONS].join(" / ")}? ` +
|
|
228
|
+
"Option keys are camelCase in the TypeScript kernel; the Python kernel uses snake_case.");
|
|
229
|
+
}
|
|
218
230
|
const expectedContentHash = opts.expectedContentHash;
|
|
219
231
|
const errors = [];
|
|
220
232
|
// Guard a malformed input (null / non-object / array) up front — every field
|
|
@@ -296,18 +308,45 @@ export async function verifyEnvelope(env, optionsOrContentHash) {
|
|
|
296
308
|
let contentHashOk = null;
|
|
297
309
|
let contentRole = null;
|
|
298
310
|
if (expectedContentHash !== undefined) {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
311
|
+
const committed = contentHashes(env);
|
|
312
|
+
if (committed.length === 0) {
|
|
313
|
+
// UNDETERMINED, not failed. An external-commitment envelope
|
|
314
|
+
// (`sanning.events/v1`) has no inline payload, so it commits to no
|
|
315
|
+
// envelope-readable content hash and there is nothing here to compare
|
|
316
|
+
// against. Reporting that as a failure would render "we cannot check
|
|
317
|
+
// this" as "it did not match" — what P-10 and I-5 exist to prevent — and
|
|
318
|
+
// would fail every envelope on the DL-52 path.
|
|
319
|
+
//
|
|
320
|
+
// The caller is still told the check did not happen; silence would be
|
|
321
|
+
// worse than either verdict.
|
|
322
|
+
contentHashOk = null;
|
|
323
|
+
errors.push("this envelope commits to no readable content hash (external-commitment " +
|
|
324
|
+
"profile), so the provided content hash could not be checked — " +
|
|
325
|
+
"undetermined, not failed");
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
const want = expectedContentHash.toLowerCase();
|
|
329
|
+
const match = committed.find((c) => c.hash.toLowerCase() === want);
|
|
330
|
+
contentHashOk = match !== undefined;
|
|
331
|
+
contentRole = match ? match.role : null;
|
|
332
|
+
if (!contentHashOk) {
|
|
333
|
+
errors.push("provided content hash does not match any hash this envelope commits to");
|
|
334
|
+
}
|
|
305
335
|
}
|
|
306
336
|
}
|
|
307
337
|
return {
|
|
308
338
|
// null (binding not checkable) does not fail the verdict — Python-kernel
|
|
309
339
|
// parity: ok = spec && sig && payloadHashOk is not False.
|
|
310
|
-
|
|
340
|
+
// A supplied `expectedContentHash` that matches nothing this envelope
|
|
341
|
+
// commits to is a FAILING verdict, not a side note (proof#42). This is the
|
|
342
|
+
// check an external auditor runs — "does this file match what was
|
|
343
|
+
// anchored" — and reporting it only in `contentHashOk` meant the top-level
|
|
344
|
+
// verdict, the field a caller naturally reads, said pass over a file that
|
|
345
|
+
// does not match.
|
|
346
|
+
//
|
|
347
|
+
// Absence stays undetermined: `contentHashOk` is null when no hash was
|
|
348
|
+
// supplied, and null must not fail (P-10 — absence is not failure).
|
|
349
|
+
ok: specVersionOk && signatureOk && payloadHashOk !== false && contentHashOk !== false,
|
|
311
350
|
specVersionOk,
|
|
312
351
|
payloadHashOk,
|
|
313
352
|
signatureOk,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanning/proof",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "TypeScript kernel of the Sanning verification stack: RFC 8785 (JCS) canonicalization, SHA-256, and Ed25519 verification for Verifiable Event Envelopes. Full-family verifier (inline + external commitment); accepts sanning.agent/v1, sanning.mlflow/v1, sanning.events/v1. Conformance-gated byte-for-byte against the test-vectors corpus.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
"files": [
|
|
20
20
|
"src",
|
|
21
21
|
"dist",
|
|
22
|
+
"vectors",
|
|
22
23
|
"README.md",
|
|
23
24
|
"LICENSE"
|
|
24
25
|
],
|