@sanning/proof 0.9.1 → 0.10.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 +5 -5
- package/dist/agent-proof.js +5 -0
- package/dist/chain.js +4 -4
- package/dist/cli.js +13 -10
- package/dist/compose.js +3 -3
- package/dist/crypto.js +1 -1
- package/dist/evidence.d.ts +29 -54
- package/dist/evidence.js +349 -141
- package/dist/grading.d.ts +1 -1
- package/dist/grading.js +22 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +6 -6
- package/dist/verifier.d.ts +1 -0
- package/dist/verifier.js +66 -4
- package/package.json +2 -2
- package/src/agent-proof.ts +3 -0
- package/src/chain.ts +4 -4
- package/src/cli.ts +13 -10
- package/src/compose.ts +4 -4
- package/src/crypto.ts +1 -1
- package/src/evidence.ts +418 -196
- package/src/grading.ts +28 -5
- package/src/index.ts +5 -8
- package/src/verifier.ts +67 -4
- package/vectors/README.md +1 -1
- package/vectors/chain-reading.json +4 -4
- package/vectors/stamp-on-chain.json +3 -3
package/dist/grading.d.ts
CHANGED
|
@@ -113,7 +113,7 @@ export interface PackGrade {
|
|
|
113
113
|
export declare function accountDisclosure(bundle: unknown, content: Readonly<Record<string, Uint8Array>> | ReadonlyMap<string, Uint8Array>): DisclosureAccount;
|
|
114
114
|
/** The `leaf_count` a checkpoint's SIGNED record commits to, or null if unreadable. */
|
|
115
115
|
export declare function signedLeafCount(checkpoint: {
|
|
116
|
-
|
|
116
|
+
event_bytes?: unknown;
|
|
117
117
|
} | null | undefined): number | null;
|
|
118
118
|
/**
|
|
119
119
|
* Omission — the failure a per-record check structurally cannot see.
|
package/dist/grading.js
CHANGED
|
@@ -104,6 +104,18 @@ function inclusionKeyOf(incl) {
|
|
|
104
104
|
return null;
|
|
105
105
|
return (hasV2 ? incl.checkpoint_ref : incl.checkpoint_tx_id);
|
|
106
106
|
}
|
|
107
|
+
// The key a checkpoint and an inclusion are MATCHED on. A `checkpoint_ref` is
|
|
108
|
+
// folded to lower case, because the verifier lowercases it before comparing.
|
|
109
|
+
// A retired `tx_id` / `checkpoint_tx_id` is compared exactly as written, as the
|
|
110
|
+
// retired reader did, even when it happens to look like hex.
|
|
111
|
+
function checkpointMatchKey(o) {
|
|
112
|
+
const k = joinKeyOf(o);
|
|
113
|
+
return k !== null && typeof o?.checkpoint_ref === "string" ? k.toLowerCase() : k;
|
|
114
|
+
}
|
|
115
|
+
function inclusionMatchKey(incl) {
|
|
116
|
+
const k = inclusionKeyOf(incl);
|
|
117
|
+
return k !== null && typeof incl?.checkpoint_ref === "string" ? k.toLowerCase() : k;
|
|
118
|
+
}
|
|
107
119
|
const eventsOf = (bundle) => {
|
|
108
120
|
const e = bundle?.body?.events;
|
|
109
121
|
return Array.isArray(e) ? e : [];
|
|
@@ -272,7 +284,7 @@ export function accountDisclosure(bundle, content) {
|
|
|
272
284
|
}
|
|
273
285
|
/** The `leaf_count` a checkpoint's SIGNED record commits to, or null if unreadable. */
|
|
274
286
|
export function signedLeafCount(checkpoint) {
|
|
275
|
-
const hex = checkpoint?.
|
|
287
|
+
const hex = checkpoint?.event_bytes;
|
|
276
288
|
if (typeof hex !== "string" || hex.length === 0)
|
|
277
289
|
return null;
|
|
278
290
|
try {
|
|
@@ -310,7 +322,14 @@ export function completenessNotes(bundle) {
|
|
|
310
322
|
notes.push({ checkpointRef, signed: null, presented: null, kind: "unreadable" });
|
|
311
323
|
continue;
|
|
312
324
|
}
|
|
313
|
-
|
|
325
|
+
// Compared case-folded. The verifier recomputes `checkpoint_ref` and
|
|
326
|
+
// accepts it in any case (evidence-bundle.md §5.1 names lowercase hex as
|
|
327
|
+
// the form, and the kernel lowercases before comparing), so a pack whose
|
|
328
|
+
// checkpoint spells its ref in upper case still verifies. Grading it
|
|
329
|
+
// case-sensitively reported every record as omitted: a completeness
|
|
330
|
+
// accusation manufactured by letter case. The ref shown is the pack's own.
|
|
331
|
+
const want = checkpointMatchKey(cp);
|
|
332
|
+
const presented = events.filter((e) => inclusionMatchKey(e?.inclusion) === want).length;
|
|
314
333
|
if (presented < signed)
|
|
315
334
|
notes.push({ checkpointRef, signed, presented, kind: "omitted" });
|
|
316
335
|
else if (presented > signed)
|
|
@@ -455,7 +474,7 @@ export function renderCause(cause, subject = "this pack") {
|
|
|
455
474
|
case "leaf-count-unreadable":
|
|
456
475
|
return `checkpoint ${cause.checkpointRef} has no readable signed leaf_count — completeness is UNKNOWN`;
|
|
457
476
|
case "no-disclosure":
|
|
458
|
-
return (`no
|
|
477
|
+
return (`no event bytes were disclosed — content binding is UNKNOWN for all ${cause.count} ` +
|
|
459
478
|
`record(s), not verified`);
|
|
460
479
|
case "missing-disclosure":
|
|
461
480
|
return (`${cause.count} of ${cause.of} record(s) disclose no bytes (first: ${cause.firstEventId}) — ` +
|
package/dist/index.d.ts
CHANGED
|
@@ -2,8 +2,8 @@ export { ACCEPTED_SPEC_MAJORS, contentHashes, jcs, specVersionSupported, verifyE
|
|
|
2
2
|
export { bytesToHex, deriveOperatorAddress, ed25519Verify, hexToBytes, sha256Bytes, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
3
3
|
export { EMPTY_TREE_ROOT_HEX, auditPath, leafHash, merkleRoot, nodeHash, verifyInclusion, } from "./merkle.js";
|
|
4
4
|
export type { ContentRole, Envelope, Subject, VerificationResult, VerifyOptions } from "./types.js";
|
|
5
|
-
export { ANCHOR_TRACE_BODY_TYPE,
|
|
6
|
-
export type { AnchorTraceBody, AttestationPayload, AttestationRecord, AttestationResult, CheckpointResult, ComposeVerdictResult, DisclosedContentResult, DisclosedEventContent, DisclosureStatus, EvidenceBundle, EvidenceBundleResult, EvidenceIssuer, EvidenceStatus, EvidenceVerdict, EventResult, ExportBody, ExportResult, OnChainOutcome, OnChainResult, PerGatewayOutcome, AnchorRef, StampBody, StampEntry, StampResult, StampSubtree, StampSubtreeResult, StampTraceBody, StampEvidenceBody, StampEvidenceEvent, StampEvidenceInclusion, TraceCheckpoint,
|
|
5
|
+
export { ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, STAMP_EVIDENCE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
|
|
6
|
+
export type { AnchorTraceBody, AttestationPayload, AttestationRecord, AttestationResult, CheckpointResult, ComposeVerdictResult, DisclosedContentResult, DisclosedEventContent, DisclosureStatus, EvidenceBundle, EvidenceBundleResult, EvidenceIssuer, EvidenceStatus, EvidenceVerdict, EventResult, ExportBody, ExportResult, OnChainOutcome, OnChainResult, PerGatewayOutcome, AnchorRef, StampBody, StampEntry, StampResult, StampSubtree, StampSubtreeResult, StampTraceBody, StampEvidenceBody, StampEvidenceEvent, StampEvidenceInclusion, TraceCheckpoint, TraceEvent, TraceInclusion, VerdictAttestation, VerdictCheckpoint, VerdictEvent, VerdictObject, VerdictOnChain, VerdictOnChainGateway, VerifyEvidenceOptions, } from "./evidence.js";
|
|
7
7
|
export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
|
|
8
8
|
export type { CompletenessNote, DisclosureAccount, GradeInput, PackGrade, PackGradeCause, PackGradeCauseKind, PackVerdict, } from "./grading.js";
|
|
9
9
|
export { ADAPTER_CHAIN_NAMESPACES, CHAIN_READING_SCOPE, readChains, renderChainFinding, } from "./chain.js";
|
package/dist/index.js
CHANGED
|
@@ -21,9 +21,9 @@ export { ACCEPTED_SPEC_MAJORS, contentHashes, jcs, specVersionSupported, verifyE
|
|
|
21
21
|
export { bytesToHex, deriveOperatorAddress, ed25519Verify, hexToBytes, sha256Bytes, sha256Hex, utf8, verifyRsaPssSha256, } from "./crypto.js";
|
|
22
22
|
export { EMPTY_TREE_ROOT_HEX, auditPath, leafHash, merkleRoot, nodeHash, verifyInclusion, } from "./merkle.js";
|
|
23
23
|
// Evidence-bundle verification (specs/evidence-bundle.md): the signed
|
|
24
|
-
// sanning.evidence/v1 wrapper carrying the sanning.anchor.trace/
|
|
24
|
+
// sanning.evidence/v1 wrapper carrying the sanning.anchor.trace/v3 body. The CLI
|
|
25
25
|
// (`npx @sanning/proof verify`) is built on this. The same path dispatches on the
|
|
26
|
-
// issuer-composed sanning.evidence.export/
|
|
26
|
+
// issuer-composed sanning.evidence.export/v2 body (specs/evidence-export.md) to
|
|
27
27
|
// verifyExportBody (embedded RSA-PSS operator attestations + a recomputed §4
|
|
28
28
|
// verdict object with per-gateway on-chain outcomes).
|
|
29
29
|
//
|
|
@@ -34,7 +34,7 @@ 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,
|
|
37
|
+
export { ANCHOR_TRACE_BODY_TYPE, EXPORT_BODY_TYPE, STAMP_BODY_VERSION, STAMP_TRACE_BODY_TYPE, STAMP_EVIDENCE_BODY_TYPE, disclosedContent, recomputeExportVerdict, verifyEvidenceBundle, } from "./evidence.js";
|
|
38
38
|
// Pack grading (src/grading.ts): what a verification SURFACE may claim about a
|
|
39
39
|
// pack — the signed bundle PLUS whatever disclosed bytes travelled beside it.
|
|
40
40
|
//
|
|
@@ -51,7 +51,7 @@ export { ANCHOR_TRACE_BODY_TYPE, ANCHOR_TRACE_BODY_TYPE_V1, ANCHOR_TRACE_BODY_TY
|
|
|
51
51
|
// derivation to it, and diff its copy against ours. Additive — no existing
|
|
52
52
|
// export changes.
|
|
53
53
|
export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCause, signedLeafCount, } from "./grading.js";
|
|
54
|
-
// Chain reading (src/chain.ts): following the pointer every sanning.events/
|
|
54
|
+
// Chain reading (src/chain.ts): following the pointer every sanning.events/v2
|
|
55
55
|
// record carries to the record before it, over the records ONE bundle presents.
|
|
56
56
|
//
|
|
57
57
|
// Every anchored event has carried that pointer since the profile was ratified
|
|
@@ -67,13 +67,13 @@ export { accountDisclosure, completenessNotes, gradeCauses, gradePack, renderCau
|
|
|
67
67
|
//
|
|
68
68
|
// It returns FINDINGS, never a verdict, and it feeds no status. Chain links are
|
|
69
69
|
// deletion/reorder-evidence for the events present and NOT a completeness proof
|
|
70
|
-
// (`anchor/docs/profile-sanning.events-
|
|
70
|
+
// (`anchor/docs/profile-sanning.events-v2.md` §5), drop the last N events of a
|
|
71
71
|
// chain and nothing dangles. `CHAIN_READING_SCOPE` is that sentence, exported so
|
|
72
72
|
// every surface prints the same limit rather than paraphrasing it into a claim.
|
|
73
73
|
// Additive — no existing export changes.
|
|
74
74
|
export { ADAPTER_CHAIN_NAMESPACES, CHAIN_READING_SCOPE, readChains, renderChainFinding, } from "./chain.js";
|
|
75
75
|
// The `proof export` composer (specs/evidence-export.md §5, phase 2): produce a
|
|
76
|
-
// signed, offline-verifiable sanning.evidence.export/
|
|
76
|
+
// signed, offline-verifiable sanning.evidence.export/v2 artifact from a source
|
|
77
77
|
// anchor-trace bundle + operator attestations + an exporter Ed25519 key. The
|
|
78
78
|
// inverse of verifyEvidenceBundle's verifyExportBody dispatch.
|
|
79
79
|
export { attestationSignatureToHex, composeExport } from "./compose.js";
|
package/dist/verifier.d.ts
CHANGED
|
@@ -4,6 +4,7 @@ export declare const MAX_CANONICAL_DEPTH = 128;
|
|
|
4
4
|
export declare function exceedsDepth(value: unknown, max: number): boolean;
|
|
5
5
|
export declare function jcs(value: unknown): string;
|
|
6
6
|
export declare function specVersionSupported(specVersion: string): boolean;
|
|
7
|
+
export declare function isRfc3339UtcInstant(value: unknown): boolean;
|
|
7
8
|
export declare function contentHashes(env: Envelope): {
|
|
8
9
|
role: ContentRole;
|
|
9
10
|
hash: string;
|
package/dist/verifier.js
CHANGED
|
@@ -13,7 +13,7 @@ import { ed25519Verify, sha256Hex, utf8 } from "./crypto.js";
|
|
|
13
13
|
// major ("sanning.agent/v1.<minor>") are additive and tolerated — matching the Go
|
|
14
14
|
// reference kernel's semantics (pkg/proof isSupportedSpec) so the kernels do
|
|
15
15
|
// not diverge on minors. Accepting a new profile is a deliberate one-entry
|
|
16
|
-
// addition HERE and only here. sanning.events/
|
|
16
|
+
// addition HERE and only here. sanning.events/v2 (external commitment + Minimal
|
|
17
17
|
// disclosure; envelope-spec §4) is verified through the SAME primitives — the
|
|
18
18
|
// signed scope is still envelope-minus-signature-minus-co_signatures, with no
|
|
19
19
|
// profile-specific dialect (no underscore strip, that is mlflow-only).
|
|
@@ -35,9 +35,13 @@ import { ed25519Verify, sha256Hex, utf8 } from "./crypto.js";
|
|
|
35
35
|
// always strict mode) and the `readonly string[]` type rejects it at compile
|
|
36
36
|
// time. The elements are strings, so a shallow freeze is a deep one. Pinned by
|
|
37
37
|
// test/accept-set.test.ts "the exported registry cannot be widened at runtime".
|
|
38
|
+
//
|
|
39
|
+
// `sanning.events/v2` replaced `sanning.events/v1` in the producer-to-agent cut
|
|
40
|
+
// (envelope-spec A10, governance.md §8 W-3). The old major is rejected like any
|
|
41
|
+
// other unknown one: DL-73 keeps no reader for it.
|
|
38
42
|
export const ACCEPTED_SPEC_MAJORS = Object.freeze([
|
|
39
43
|
"sanning.agent/v1",
|
|
40
|
-
"sanning.events/
|
|
44
|
+
"sanning.events/v2",
|
|
41
45
|
]);
|
|
42
46
|
// Maximum JSON container-nesting depth a verifier will canonicalize. Inputs
|
|
43
47
|
// nested deeper are rejected as malformed BEFORE canonicalization runs. This is
|
|
@@ -228,6 +232,57 @@ export function specVersionSupported(specVersion) {
|
|
|
228
232
|
return minor.length > 0 && /^[0-9]+$/.test(minor);
|
|
229
233
|
});
|
|
230
234
|
}
|
|
235
|
+
// `signed_at` format (envelope-spec §2: "RFC 3339 UTC, `Z`"), proof#95. The
|
|
236
|
+
// accepted form is RFC 3339 `date-time` narrowed three ways, identically in
|
|
237
|
+
// every kernel:
|
|
238
|
+
// - the zone is an uppercase `Z`: never a numeric offset (not even
|
|
239
|
+
// `+00:00`), never a lowercase `z` or `t`;
|
|
240
|
+
// - the date is a real proleptic Gregorian date from 0001-01-01 to
|
|
241
|
+
// 9999-12-31, so February 29 only in a leap year;
|
|
242
|
+
// - second 60 is refused. RFC 3339 allows it at a real leap second, but
|
|
243
|
+
// which instants those were needs a table, and JavaScript's `Date`,
|
|
244
|
+
// Python's `datetime` and Go's `time` cannot hold one, so a verified `:60`
|
|
245
|
+
// would fail the first consumer that parses it.
|
|
246
|
+
// Fractional seconds of any length are allowed. Digits are ASCII `[0-9]`
|
|
247
|
+
// (Python's `\d` matches any Unicode digit, so the Python kernel spells it the
|
|
248
|
+
// same way).
|
|
249
|
+
const RFC3339_UTC = /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\.[0-9]+)?Z$/;
|
|
250
|
+
const DAYS_IN_MONTH = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
251
|
+
export function isRfc3339UtcInstant(value) {
|
|
252
|
+
if (typeof value !== "string")
|
|
253
|
+
return false;
|
|
254
|
+
const m = RFC3339_UTC.exec(value);
|
|
255
|
+
if (m === null)
|
|
256
|
+
return false;
|
|
257
|
+
const [year, month, day, hour, minute, second] = m.slice(1).map(Number);
|
|
258
|
+
if (year < 1 || month < 1 || month > 12 || day < 1)
|
|
259
|
+
return false;
|
|
260
|
+
const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
|
|
261
|
+
const days = month === 2 && leap ? 29 : DAYS_IN_MONTH[month - 1];
|
|
262
|
+
return day <= days && hour <= 23 && minute <= 59 && second <= 59;
|
|
263
|
+
}
|
|
264
|
+
// The skeleton fields whose FORMAT §2 fixes and no other check reaches: a
|
|
265
|
+
// correctly signed envelope with `signed_at: "2026-02-30T00:00:00Z"` or
|
|
266
|
+
// `environment: "prod\u0000uction"` passes the signature and the payload
|
|
267
|
+
// binding, so without this it verified in full. Each is checked only when
|
|
268
|
+
// present: §2 flags an absent `signed_at` as non-conformant rather than
|
|
269
|
+
// rejecting it, and `environment` is optional in the skeleton.
|
|
270
|
+
//
|
|
271
|
+
// Own properties only, because those are the members `jcs` signs over.
|
|
272
|
+
function skeletonFieldErrors(env) {
|
|
273
|
+
const fields = env;
|
|
274
|
+
const own = (k) => (Object.prototype.hasOwnProperty.call(fields, k) ? fields[k] : undefined);
|
|
275
|
+
const out = [];
|
|
276
|
+
const signedAt = own("signed_at");
|
|
277
|
+
if (signedAt !== undefined && !isRfc3339UtcInstant(signedAt)) {
|
|
278
|
+
out.push("malformed signed_at: not an RFC 3339 UTC date-time with Z naming a real date and time (envelope-spec §2)");
|
|
279
|
+
}
|
|
280
|
+
const environment = own("environment");
|
|
281
|
+
if (environment !== undefined && environment !== "dev" && environment !== "production") {
|
|
282
|
+
out.push('malformed environment: not "dev" or "production" (envelope-spec §2)');
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
231
286
|
// The content hash(es) an envelope commits to, by event type — the values a
|
|
232
287
|
// reverse lookup can match the user's in-browser file hash against.
|
|
233
288
|
// asset_registered -> payload.hash (the registered, known-good bytes)
|
|
@@ -339,6 +394,13 @@ export async function verifyEnvelope(env, optionsOrContentHash) {
|
|
|
339
394
|
const specVersionOk = specVersionSupported(env.spec_version);
|
|
340
395
|
if (!specVersionOk)
|
|
341
396
|
errors.push(`unsupported spec_version: ${JSON.stringify(env.spec_version)}`);
|
|
397
|
+
// Skeleton field format (envelope-spec §2, proof#95): a present `signed_at`
|
|
398
|
+
// or `environment` that is not what §2 says fails the verdict. There is no
|
|
399
|
+
// result field for it, so the verdict shape is unchanged for every envelope
|
|
400
|
+
// the spec allows; the reason is in `errors`.
|
|
401
|
+
const fieldErrors = skeletonFieldErrors(env);
|
|
402
|
+
errors.push(...fieldErrors);
|
|
403
|
+
const fieldsOk = fieldErrors.length === 0;
|
|
342
404
|
// Check 1 — Payload binding (both modes; see header).
|
|
343
405
|
//
|
|
344
406
|
// payload_hash MUST be present and a string in every mode — envelope-spec
|
|
@@ -404,7 +466,7 @@ export async function verifyEnvelope(env, optionsOrContentHash) {
|
|
|
404
466
|
const committed = contentHashes(env);
|
|
405
467
|
if (committed.length === 0) {
|
|
406
468
|
// UNDETERMINED, not failed. An external-commitment envelope
|
|
407
|
-
// (`sanning.events/
|
|
469
|
+
// (`sanning.events/v2`) has no inline payload, so it commits to no
|
|
408
470
|
// envelope-readable content hash and there is nothing here to compare
|
|
409
471
|
// against. Reporting that as a failure would render "we cannot check
|
|
410
472
|
// this" as "it did not match" — what P-10 and I-5 exist to prevent — and
|
|
@@ -439,7 +501,7 @@ export async function verifyEnvelope(env, optionsOrContentHash) {
|
|
|
439
501
|
//
|
|
440
502
|
// Absence stays undetermined: `contentHashOk` is null when no hash was
|
|
441
503
|
// supplied, and null must not fail (P-10 — absence is not failure).
|
|
442
|
-
ok: specVersionOk && signatureOk && payloadHashOk !== false && contentHashOk !== false,
|
|
504
|
+
ok: specVersionOk && fieldsOk && signatureOk && payloadHashOk !== false && contentHashOk !== false,
|
|
443
505
|
specVersionOk,
|
|
444
506
|
payloadHashOk,
|
|
445
507
|
signatureOk,
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanning/proof",
|
|
3
|
-
"version": "0.
|
|
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
|
|
3
|
+
"version": "0.10.0",
|
|
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 and sanning.events/v2. Conformance-gated byte-for-byte against the test-vectors corpus.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
package/src/agent-proof.ts
CHANGED
|
@@ -118,6 +118,9 @@ export async function verifyAgentProofBundle(
|
|
|
118
118
|
checkpointOk = res.ok && res.payloadHashOk === true;
|
|
119
119
|
if (!res.signatureOk) errors.push("checkpoint envelope signature failed");
|
|
120
120
|
if (res.payloadHashOk !== true) errors.push("checkpoint envelope payload binding failed");
|
|
121
|
+
// A malformed skeleton field (proof#95) fails the envelope with a valid
|
|
122
|
+
// signature and binding; name it rather than fail with no reason.
|
|
123
|
+
for (const m of res.errors) if (m.startsWith("malformed ")) errors.push(`checkpoint envelope ${m}`);
|
|
121
124
|
const payload = (b.checkpoint_envelope.payload ?? {}) as {
|
|
122
125
|
merkle_root?: unknown;
|
|
123
126
|
leaf_count?: unknown;
|
package/src/chain.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// event before it, and reporting what does not line up.
|
|
3
3
|
//
|
|
4
4
|
// WHY THIS LIVES IN THE KERNEL, BESIDE PACK GRADING.
|
|
5
|
-
// `sanning.events/
|
|
6
|
-
// shipped read it. The envelope verifier hashes each event's `
|
|
5
|
+
// `sanning.events/v2` records carry `previous_hash`, and until now nothing we
|
|
6
|
+
// shipped read it. The envelope verifier hashes each event's `event_bytes`
|
|
7
7
|
// and stops — `evidence.ts` says following the link is "consumer-layer logic
|
|
8
8
|
// above it", and it was right that a SINGLE-envelope verifier should not do
|
|
9
9
|
// it. But the consumer layer never did it either, so a bundle with an event
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
// ---- WHAT THIS IS NOT ------------------------------------------------------
|
|
19
19
|
//
|
|
20
20
|
// It is not a completeness statement and it must never be read as one. The
|
|
21
|
-
// profile says so in its own words (`anchor/docs/profile-sanning.events-
|
|
21
|
+
// profile says so in its own words (`anchor/docs/profile-sanning.events-v2.md`
|
|
22
22
|
// §5): chain semantics are "deletion/reorder-evidence for the events present
|
|
23
23
|
// ... not a completeness proof." Drop the last N events of a chain and nothing
|
|
24
24
|
// here dangles; the record that would have pointed at them was never written.
|
|
@@ -273,7 +273,7 @@ async function groupChains(bundle: unknown): Promise<Group[]> {
|
|
|
273
273
|
// event id an adapter pointer has nothing to resolve against.
|
|
274
274
|
if (eventId === null || publicKey === null) continue;
|
|
275
275
|
|
|
276
|
-
const decoded = decodeRecord(entry?.
|
|
276
|
+
const decoded = decodeRecord(entry?.event_bytes);
|
|
277
277
|
if (decoded === null) continue;
|
|
278
278
|
const { record, bytes } = decoded;
|
|
279
279
|
|
package/src/cli.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
// npx @sanning/proof verify <bundle.json> [comma,sep,gateways]
|
|
3
3
|
//
|
|
4
4
|
// One turnkey verifier for both producer bundles in the stack:
|
|
5
|
-
// - sanning.evidence/v1
|
|
6
|
-
//
|
|
5
|
+
// - sanning.evidence/v1: bodies sanning.anchor.trace/v3, sanning.stamp.trace/v1,
|
|
6
|
+
// sanning.stamp.evidence/v2 and sanning.evidence.export/v2
|
|
7
|
+
// - sanning.agent.proof/v1: the agent inclusion proof
|
|
7
8
|
// Dispatch is by sniffed spec_version; no flag tells the CLI which it is.
|
|
8
9
|
//
|
|
9
10
|
// Pinned exit codes (mirror `sanningd verify-status`):
|
|
@@ -184,8 +185,8 @@ export async function runCli(argv: string[], io: Cli): Promise<number> {
|
|
|
184
185
|
// `proof bundle <source-bundle.json> --attestations <f> --key <f> [-o <out>]`
|
|
185
186
|
// (`proof export` still reaches this, undocumented — see the dispatch above.)
|
|
186
187
|
//
|
|
187
|
-
// Compose a signed, offline-verifiable sanning.evidence.export/
|
|
188
|
-
// source sanning.anchor.trace/
|
|
188
|
+
// Compose a signed, offline-verifiable sanning.evidence.export/v2 artifact from a
|
|
189
|
+
// source sanning.anchor.trace/v3 bundle + operator attestation records + an exporter
|
|
189
190
|
// Ed25519 key (evidence-export.md §5, composer). Writes the export to `-o <out>`
|
|
190
191
|
// (or stdout when omitted, so it can be piped straight into `proof verify`).
|
|
191
192
|
//
|
|
@@ -307,8 +308,8 @@ function usageExport(io: Cli): number {
|
|
|
307
308
|
"Usage: npx @sanning/proof bundle <source-bundle.json> --attestations <file> --key <file> [-o <out.json>]",
|
|
308
309
|
);
|
|
309
310
|
io.out("");
|
|
310
|
-
io.out("Compose a signed, offline-verifiable sanning.evidence.export/
|
|
311
|
-
io.out("sanning.anchor.trace/
|
|
311
|
+
io.out("Compose a signed, offline-verifiable sanning.evidence.export/v2 artifact from an");
|
|
312
|
+
io.out("sanning.anchor.trace/v3 source bundle + operator attestation records + an exporter");
|
|
312
313
|
io.out("Ed25519 key. The output verifies with `proof verify <out.json>`.");
|
|
313
314
|
io.out("");
|
|
314
315
|
io.out("--attestations <file> JSON array of attestation records (or {attestations:[...]}).");
|
|
@@ -642,10 +643,12 @@ function printUsage(io: Cli): void {
|
|
|
642
643
|
io.out(" verify <bundle.json> [gateways] [--logs <path>] Verify a bundle");
|
|
643
644
|
io.out(" bundle <source.json> --attestations <f> --key <f> [-o] Create an attested bundle");
|
|
644
645
|
io.out("");
|
|
645
|
-
io.out("Verify a sanning.evidence/v1 (sanning.anchor.trace/
|
|
646
|
-
io.out("sanning.
|
|
647
|
-
io.out("
|
|
648
|
-
io.out("to also re-fetch
|
|
646
|
+
io.out("Verify a sanning.evidence/v1 bundle (body sanning.anchor.trace/v3,");
|
|
647
|
+
io.out("sanning.stamp.trace/v1, sanning.stamp.evidence/v2 or the attested export");
|
|
648
|
+
io.out("sanning.evidence.export/v2) or a sanning.agent.proof/v1 inclusion bundle,");
|
|
649
|
+
io.out("fully offline. Supply a comma-separated gateway list to also re-fetch");
|
|
650
|
+
io.out("each checkpoint and stamp on-chain. Only arweave witnesses are re-fetched;");
|
|
651
|
+
io.out("a witness of any other kind is not checked and the run exits 3.");
|
|
649
652
|
io.out("");
|
|
650
653
|
io.out("--logs <path> Bind disclosed raw logs to each event's committed");
|
|
651
654
|
io.out(" content_hash. Either shape works, and which one you");
|
package/src/compose.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// Attested-evidence-export composer (`proof export`, evidence-export.md §5,
|
|
2
2
|
// phase 2). The last-mile issuer/runtime job the kernel does NOT own (§5.1
|
|
3
|
-
// "Out of the kernel"): take a source `sanning.anchor.trace/
|
|
3
|
+
// "Out of the kernel"): take a source `sanning.anchor.trace/v3` bundle + a set of
|
|
4
4
|
// operator attestation records + an exporter Ed25519 key, and PRODUCE one signed,
|
|
5
|
-
// offline-verifiable `sanning.evidence.export/
|
|
5
|
+
// offline-verifiable `sanning.evidence.export/v2` artifact, exactly the bytes the
|
|
6
6
|
// kernel's verifyExportBody round-trip-verifies.
|
|
7
7
|
//
|
|
8
8
|
// This is the inverse of evidence.ts's verifyExportBody, and it is deliberately
|
|
@@ -64,7 +64,7 @@ export interface ComposeExportOptions {
|
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
export interface ComposeExportResult {
|
|
67
|
-
// The signed, offline-verifiable sanning.evidence.export/
|
|
67
|
+
// The signed, offline-verifiable sanning.evidence.export/v2 artifact.
|
|
68
68
|
bundle: EvidenceBundle;
|
|
69
69
|
// The export rollup the embedded kernel_verdict carries (verified|partial|failed).
|
|
70
70
|
status: EvidenceStatus;
|
|
@@ -75,7 +75,7 @@ export interface ComposeExportResult {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
const GENESIS = "GENESIS";
|
|
78
|
-
const EXPORT_SCHEMA =
|
|
78
|
+
const EXPORT_SCHEMA = EXPORT_BODY_TYPE;
|
|
79
79
|
|
|
80
80
|
// Transcode an attestation `signature` to the lowercase hex §2.2 mandates. The
|
|
81
81
|
// issuer emits either lowercase-hex or unpadded base64url; §2.2 stores hex, so
|
package/src/crypto.ts
CHANGED
|
@@ -85,7 +85,7 @@ export async function ed25519PublicKey(privateKeyHex: string): Promise<string> {
|
|
|
85
85
|
}
|
|
86
86
|
|
|
87
87
|
// ---------------------------------------------------------------------------
|
|
88
|
-
// RSA-PSS operator-attestation crypto (sanning.evidence.export/
|
|
88
|
+
// RSA-PSS operator-attestation crypto (sanning.evidence.export/v2)
|
|
89
89
|
//
|
|
90
90
|
// The attested-evidence-export body embeds operator attestations signed with
|
|
91
91
|
// RSA-PSS-SHA-256 over the JCS-canonical attestation payload; the operator key
|