@sanning/proof 0.8.2 → 0.8.3
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 +1 -1
- package/dist/evidence.js +23 -2
- package/dist/verifier.js +16 -1
- package/package.json +1 -1
- package/src/evidence.ts +23 -2
- package/src/verifier.ts +16 -1
package/README.md
CHANGED
|
@@ -203,7 +203,7 @@ The primary signature covers `JCS(envelope minus signature minus co_signatures)`
|
|
|
203
203
|
|
|
204
204
|
## Why you can trust it (without trusting us)
|
|
205
205
|
|
|
206
|
-
Three **independent implementations** of this kernel exist: this package, the Python [`sanning-proof`](https://pypi.org/project/sanning-proof/) (PyPI), and and the MIT-licensed Go reference. The TypeScript and Python kernels are conformance-gated **byte-for-byte** in CI against one shared corpus, pinned per file by SHA-256 at cut tag `test-vectors-v3.
|
|
206
|
+
Three **independent implementations** of this kernel exist: this package, the Python [`sanning-proof`](https://pypi.org/project/sanning-proof/) (PyPI), and and the MIT-licensed Go reference. The TypeScript and Python kernels are conformance-gated **byte-for-byte** in CI against one shared corpus, pinned per file by SHA-256 at cut tag `test-vectors-v3.3`, and a cross-kernel agreement gate re-runs the corpus plus adversarial negatives through both, asserting identical verdicts on identical bytes. The Go reference runs as a third leg of that same gate, built at the commit its vendoring pins.
|
|
207
207
|
|
|
208
208
|
That mutual gate is the point: anyone can verify these provenance records with **no Sanning code in the trust path**, write your own verifier against the specs that ship inside the [`sanning-proof`](https://pypi.org/project/sanning-proof/) sdist, and the corpus will tell you if it is conformant.
|
|
209
209
|
|
package/dist/evidence.js
CHANGED
|
@@ -393,9 +393,20 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
393
393
|
}
|
|
394
394
|
const keyed = await resolveCheckpointKey(cp, recordBytes, isV1);
|
|
395
395
|
cpErrors.push(...keyed.errors);
|
|
396
|
+
// A duplicate key is a HARD failure, not a note. `byKey` below keeps the
|
|
397
|
+
// FIRST occurrence, so a second checkpoint claiming the same key is
|
|
398
|
+
// verified against the first one's envelope and its own stated identity is
|
|
399
|
+
// never independently checked. Recording the finding and leaving `ok` true
|
|
400
|
+
// let the bundle roll up to `verified` while carrying a checkpoint nothing
|
|
401
|
+
// had verified: a false pass, which is what `evidence-bundle.md` §3
|
|
402
|
+
// principle 3 exists to prevent (the displayed verdict must be the
|
|
403
|
+
// recomputed one, and here the recomputation quietly dropped a finding it
|
|
404
|
+
// had made).
|
|
405
|
+
let duplicateKey = false;
|
|
396
406
|
if (keyed.key !== null) {
|
|
397
407
|
if (byKey.has(keyed.key)) {
|
|
398
408
|
cpErrors.push(`duplicate checkpoint ${isV1 ? "tx_id" : "checkpoint_ref"} ${keyed.key}`);
|
|
409
|
+
duplicateKey = true;
|
|
399
410
|
}
|
|
400
411
|
else {
|
|
401
412
|
byKey.set(keyed.key, cp);
|
|
@@ -448,7 +459,7 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
448
459
|
// standing rule is that malformed is a failure while absent is
|
|
449
460
|
// undetermined (the same rule disclosed `content` follows). Without
|
|
450
461
|
// this term the vector `trace-v2-malformed-anchor-ref` verified GREEN.
|
|
451
|
-
ok: envelopeOk && merkleRootOk && keyed.errors.length === 0,
|
|
462
|
+
ok: envelopeOk && merkleRootOk && keyed.errors.length === 0 && !duplicateKey,
|
|
452
463
|
errors: cpErrors,
|
|
453
464
|
});
|
|
454
465
|
}
|
|
@@ -590,7 +601,17 @@ async function verifyAnchorTrace(b, bodyType, specVersionOk, assertedStatus, err
|
|
|
590
601
|
// gateway (null) leaves the offline-sound checkpoint ok; the run
|
|
591
602
|
// downgrades to "partial" (exit 3) via gatewayUnavailable, not a hard
|
|
592
603
|
// failure. A match (true) keeps it ok.
|
|
593
|
-
|
|
604
|
+
// AND into the verdict already computed, never rebuild it from a
|
|
605
|
+
// hand-listed set of dimensions. Rebuilding silently dropped every
|
|
606
|
+
// term not in that list: the duplicate-key gate (proof#28) and the
|
|
607
|
+
// `keyed.errors` term with it, so supplying a gateway turned a hard
|
|
608
|
+
// reject back into `verified` and an UNREACHABLE gateway turned it
|
|
609
|
+
// into `partial` (exit 3), which reads as a network problem an
|
|
610
|
+
// operator retries. Neither is conditioned on a gateway by the spec:
|
|
611
|
+
// `evidence-bundle.md` says two checkpoints carrying one key is a
|
|
612
|
+
// hard reject, full stop. Written this way, a term added at
|
|
613
|
+
// construction survives here by construction.
|
|
614
|
+
cpr.ok = cpr.ok && cpr.onChainOk !== false;
|
|
594
615
|
}
|
|
595
616
|
}
|
|
596
617
|
}
|
package/dist/verifier.js
CHANGED
|
@@ -236,7 +236,22 @@ export async function verifyEnvelope(env, optionsOrContentHash) {
|
|
|
236
236
|
return {
|
|
237
237
|
ok: false,
|
|
238
238
|
specVersionOk: false,
|
|
239
|
-
|
|
239
|
+
// `null`, not `false` (proof#62). This field carries "we cannot tell"
|
|
240
|
+
// against "it is wrong", and that distinction is what the whole
|
|
241
|
+
// minimum-disclosure posture rests on: a withheld record is `null` BY
|
|
242
|
+
// DESIGN and must never read as a failure (`evidence-bundle.md` §3.1,
|
|
243
|
+
// §6.2). There is no envelope here, so there is nothing to bind and
|
|
244
|
+
// nothing was determined. `false` asserted a binding had been checked
|
|
245
|
+
// and had failed, which is a claim this branch cannot support.
|
|
246
|
+
//
|
|
247
|
+
// The Python kernel already answered `None`, so the two disagreed on
|
|
248
|
+
// the dimension `verify-page` renders: an auditor on one CLI saw a
|
|
249
|
+
// dimension that could not be determined, and on the other one that
|
|
250
|
+
// failed, for the same bytes. `ok` is `false` either way, so no rollup
|
|
251
|
+
// verdict changes; what changes is what we tell the reader.
|
|
252
|
+
//
|
|
253
|
+
// `contentHashOk` on the next line already follows this rule.
|
|
254
|
+
payloadHashOk: null,
|
|
240
255
|
signatureOk: false,
|
|
241
256
|
contentHashOk: expectedContentHash === undefined ? null : false,
|
|
242
257
|
contentRole: null,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sanning/proof",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.3",
|
|
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",
|
package/src/evidence.ts
CHANGED
|
@@ -1007,9 +1007,20 @@ async function verifyAnchorTrace(
|
|
|
1007
1007
|
|
|
1008
1008
|
const keyed = await resolveCheckpointKey(cp, recordBytes, isV1);
|
|
1009
1009
|
cpErrors.push(...keyed.errors);
|
|
1010
|
+
// A duplicate key is a HARD failure, not a note. `byKey` below keeps the
|
|
1011
|
+
// FIRST occurrence, so a second checkpoint claiming the same key is
|
|
1012
|
+
// verified against the first one's envelope and its own stated identity is
|
|
1013
|
+
// never independently checked. Recording the finding and leaving `ok` true
|
|
1014
|
+
// let the bundle roll up to `verified` while carrying a checkpoint nothing
|
|
1015
|
+
// had verified: a false pass, which is what `evidence-bundle.md` §3
|
|
1016
|
+
// principle 3 exists to prevent (the displayed verdict must be the
|
|
1017
|
+
// recomputed one, and here the recomputation quietly dropped a finding it
|
|
1018
|
+
// had made).
|
|
1019
|
+
let duplicateKey = false;
|
|
1010
1020
|
if (keyed.key !== null) {
|
|
1011
1021
|
if (byKey.has(keyed.key)) {
|
|
1012
1022
|
cpErrors.push(`duplicate checkpoint ${isV1 ? "tx_id" : "checkpoint_ref"} ${keyed.key}`);
|
|
1023
|
+
duplicateKey = true;
|
|
1013
1024
|
} else {
|
|
1014
1025
|
byKey.set(keyed.key, cp);
|
|
1015
1026
|
anchorByKey.set(keyed.key, keyed.anchorRef);
|
|
@@ -1066,7 +1077,7 @@ async function verifyAnchorTrace(
|
|
|
1066
1077
|
// standing rule is that malformed is a failure while absent is
|
|
1067
1078
|
// undetermined (the same rule disclosed `content` follows). Without
|
|
1068
1079
|
// this term the vector `trace-v2-malformed-anchor-ref` verified GREEN.
|
|
1069
|
-
ok: envelopeOk && merkleRootOk && keyed.errors.length === 0,
|
|
1080
|
+
ok: envelopeOk && merkleRootOk && keyed.errors.length === 0 && !duplicateKey,
|
|
1070
1081
|
errors: cpErrors,
|
|
1071
1082
|
});
|
|
1072
1083
|
}
|
|
@@ -1217,7 +1228,17 @@ async function verifyAnchorTrace(
|
|
|
1217
1228
|
// gateway (null) leaves the offline-sound checkpoint ok; the run
|
|
1218
1229
|
// downgrades to "partial" (exit 3) via gatewayUnavailable, not a hard
|
|
1219
1230
|
// failure. A match (true) keeps it ok.
|
|
1220
|
-
|
|
1231
|
+
// AND into the verdict already computed, never rebuild it from a
|
|
1232
|
+
// hand-listed set of dimensions. Rebuilding silently dropped every
|
|
1233
|
+
// term not in that list: the duplicate-key gate (proof#28) and the
|
|
1234
|
+
// `keyed.errors` term with it, so supplying a gateway turned a hard
|
|
1235
|
+
// reject back into `verified` and an UNREACHABLE gateway turned it
|
|
1236
|
+
// into `partial` (exit 3), which reads as a network problem an
|
|
1237
|
+
// operator retries. Neither is conditioned on a gateway by the spec:
|
|
1238
|
+
// `evidence-bundle.md` says two checkpoints carrying one key is a
|
|
1239
|
+
// hard reject, full stop. Written this way, a term added at
|
|
1240
|
+
// construction survives here by construction.
|
|
1241
|
+
cpr.ok = cpr.ok && cpr.onChainOk !== false;
|
|
1221
1242
|
}
|
|
1222
1243
|
}
|
|
1223
1244
|
}
|
package/src/verifier.ts
CHANGED
|
@@ -252,7 +252,22 @@ export async function verifyEnvelope(
|
|
|
252
252
|
return {
|
|
253
253
|
ok: false,
|
|
254
254
|
specVersionOk: false,
|
|
255
|
-
|
|
255
|
+
// `null`, not `false` (proof#62). This field carries "we cannot tell"
|
|
256
|
+
// against "it is wrong", and that distinction is what the whole
|
|
257
|
+
// minimum-disclosure posture rests on: a withheld record is `null` BY
|
|
258
|
+
// DESIGN and must never read as a failure (`evidence-bundle.md` §3.1,
|
|
259
|
+
// §6.2). There is no envelope here, so there is nothing to bind and
|
|
260
|
+
// nothing was determined. `false` asserted a binding had been checked
|
|
261
|
+
// and had failed, which is a claim this branch cannot support.
|
|
262
|
+
//
|
|
263
|
+
// The Python kernel already answered `None`, so the two disagreed on
|
|
264
|
+
// the dimension `verify-page` renders: an auditor on one CLI saw a
|
|
265
|
+
// dimension that could not be determined, and on the other one that
|
|
266
|
+
// failed, for the same bytes. `ok` is `false` either way, so no rollup
|
|
267
|
+
// verdict changes; what changes is what we tell the reader.
|
|
268
|
+
//
|
|
269
|
+
// `contentHashOk` on the next line already follows this rule.
|
|
270
|
+
payloadHashOk: null,
|
|
256
271
|
signatureOk: false,
|
|
257
272
|
contentHashOk: expectedContentHash === undefined ? null : false,
|
|
258
273
|
contentRole: null,
|