@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 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.2`, 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.
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
- cpr.ok = cpr.envelopeOk && cpr.merkleRootOk && cpr.onChainOk !== false;
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
- payloadHashOk: false,
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.2",
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
- cpr.ok = cpr.envelopeOk && cpr.merkleRootOk && cpr.onChainOk !== false;
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
- payloadHashOk: false,
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,