From 1cfb4dd6671d2af47f4eea96f57a2a591ae97eb6 Mon Sep 17 00:00:00 2001 From: kraxo Date: Mon, 31 Aug 2026 04:07:04 +0200 Subject: [PATCH 1/4] =?UTF-8?q?release:=205.1.0=20=E2=80=94=20ship=20the?= =?UTF-8?q?=20envelope=20profile=20in=20the=20distribution,=20and=20say=20?= =?UTF-8?q?why=20it=20was=20missing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit THE MEASUREMENT THAT COMES FIRST. Built from main at 27a84db3c6dc: `docs/RECEIPT_ENVELOPE_PROFILE.md` appeared **0 times in the sdist and 0 times in the wheel**. The vectors were already complete — 21 of 21 files, 10 of 10 case.json — because `graft conformance` covers them. Only the prose document was absent. THE CAUSE, stated precisely because the obvious explanation was wrong. NO prune and NO exclude rule matched the file; both were checked pattern by pattern and matched zero. MANIFEST.in is an allowlist and the file was on no line: it was missing BY ABSENCE, NOT BY EXCLUSION. Searching for the offending prune rule would have found nothing and left the document out. One `include docs/RECEIPT_ENVELOPE_PROFILE.md` fixes it. MEASURED AGAIN AFTER THE FIX, because a repair without a second measurement does not count: 1 occurrence in the sdist, the file readable from the archive at its full 240 lines, docs entries 19 -> 20 (exactly one more), vectors unchanged at 10 of 10. The wheel still carries 0, which is correct — the wheel is the import package and prose does not belong in it. The 5.1.0 sdist carries version and profile in the same artefact (PKG-INFO: 5.1.0, profile: 1). WHY MINOR, argued from the changes rather than from habit. Measured against tag v5.0.0: 23 commits, 41 files, 2071 insertions, 32 deletions, 0 files removed. Across all of src/: 0 removed public names and 0 removed or changed signatures (`-def`/`-class` lines in the diff: zero). One new public function, `classify_eval_claim`, and one private helper. New shipped material rules out PATCH; nothing removed and no signature changed rules out MAJOR. Version anchors pulled together in one step: pyproject.toml, src/proofbundle/__init__.py and CITATION.cff now agree on 5.1.0. The integrity checker named two prose places that still said 5.0.0 (RELEASE.md line 81, docs/readiness_pack/PROGRESS.md line 3); both corrected, after which `check_version_and_changelog` reports OK. The CHANGELOG entry records `scripts/mutation_operators_fresh.py` as A SCRIPT AND NOT A GATE — it reports operator staleness and nothing blocks on its output. NOT A RELEASE. No tag, no publish. `release.yml` runs on `push: tags: ["v*"]`; the tag is the one-way door and is not this commit's to open. --- CHANGELOG.md | 44 +++++++++++++++++++++++++++++++++ CITATION.cff | 2 +- MANIFEST.in | 15 +++++++++++ RELEASE.md | 2 +- docs/readiness_pack/PROGRESS.md | 2 +- pyproject.toml | 2 +- src/proofbundle/__init__.py | 2 +- 7 files changed, 64 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95e825ff..d66470ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,50 @@ _Editorial 2026-07-20: internal gate codename replaced by its external name thro ## [Unreleased] +## [5.1.0] - 2026-08-31 (the profile a stranger can read · MINOR) + +### Added — receipt envelope profile and its conformance vectors + +`docs/RECEIPT_ENVELOPE_PROFILE.md` and the ten vectors under `conformance/envelope_profile/` +landed on `main` in #159. This release is what makes them reachable for anyone who is not +reading the repository directly. + +### Fixed — the profile was not in the distribution, and no rule had removed it + +**Measured before the fix:** `docs/RECEIPT_ENVELOPE_PROFILE.md` appeared **0 times** in the +sdist and **0 times** in the wheel built from `main` at `27a84db3c6dc`. The vectors were +already complete — 21 of 21 files, 10 of 10 `case.json` — because `graft conformance` covers +them. + +**The cause is worth stating precisely, because the obvious explanation was wrong.** No +`prune` and no `exclude` rule matched the file. `MANIFEST.in` is an allowlist, and the file +was simply not on any line: it was missing **by absence, not by exclusion**. Looking for the +offending prune rule would have found nothing and left the document out. + +One `include docs/RECEIPT_ENVELOPE_PROFILE.md` line fixes it. **Measured after the fix:** 1 +occurrence in the sdist, the file readable from the archive at its full 240 lines, `docs` +entries in the sdist 19 → 20 — exactly one more. The wheel still carries 0, and that is +correct: the wheel is the import package and prose does not belong in it. + +Why this matters beyond packaging: the profile says a stranger can recompute the result. A +profile that ships its vectors without the document explaining what they prove asks the +stranger to take the explanation on trust. + +### Added — mutation operator freshness + +`scripts/mutation_operators_fresh.py`. **It is a script, not a gate.** It reports whether the +mutation operator set has gone stale against the code it is meant to mutate; nothing blocks +on its output. + +### Why MINOR and not PATCH or MAJOR + +Measured against tag `v5.0.0`: 23 commits, 41 files, 2071 insertions, 32 deletions, **0 files +removed**. Across all of `src/`: **0 removed public names, 0 removed or changed signatures** +(`-def`/`-class` lines in the diff: zero). One new public function, `classify_eval_claim`, and +one private helper. New shipped material (the profile document, ten vectors) rules out PATCH; +nothing removed and no signature changed rules out MAJOR. + + ## [5.0.0] - 2026-08-25 (the cap runs before the work it bounds · MAJOR) ### Added — reported-version status (additive; no further major bump) diff --git a/CITATION.cff b/CITATION.cff index 2a69447c..ebad52c6 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -15,7 +15,7 @@ type: software license: MIT repository-code: "https://github.com/b7n0de/proofbundle" url: "https://github.com/b7n0de/proofbundle" -version: 5.0.0 +version: 5.1.0 doi: 10.5281/zenodo.21110642 keywords: - cryptography diff --git a/MANIFEST.in b/MANIFEST.in index acd72b1f..05f55bfe 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -25,6 +25,21 @@ graft conformance graft formal graft docs/readiness_pack include docs/adr/renewal_policy.example.json + +# PB-2026-0831-01: the receipt-envelope profile and its vectors landed on main in #159, but the +# profile document itself was NOT in the sdist and NOT in the wheel — measured, 0 hits in both. +# NO prune or exclude rule removed it. MANIFEST.in is an allowlist and the file was simply not on +# any line; it was missing by absence, not by exclusion. That distinction matters: looking for the +# offending prune rule would have found nothing and left the file out. +# +# The vectors under conformance/envelope_profile/ were already shipped by `graft conformance` +# (21 of 21 files, 10 of 10 case.json, measured). Only the prose document was missing, and it is +# the half a reader needs to know what the vectors prove. +# +# Named by exact path rather than `graft docs`, for the same reason the ADR example above is: a +# blanket graft would also ship documents whose internal links point at pruned repo files, which +# breaks test_docs_truth from the sdist. +include docs/RECEIPT_ENVELOPE_PROFILE.md graft scripts # Never ship: the Rust verifier + build artifacts, repo/CI meta, caches, VCS, big review packs. diff --git a/RELEASE.md b/RELEASE.md index f5343e39..76e1ec3e 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -78,7 +78,7 @@ The order below is the convention, not a suggestion. A release is a fact about ` ## Beta / pre-release (any future pre-release line) Historical note: the 2.0.0b1–b3 line shipped this way until **2.0.0 final** (2026-07-09); the -stable default has since moved on to the 4.x line (current: 5.0.0) and the `[experimental]` extra +stable default has since moved on to the 4.x line (current: 5.1.0) and the `[experimental]` extra ships with normal releases. The checklist below is the convention for any FUTURE pre-release: `pip install proofbundle` never pulls a PEP 440 pre-release, so the current stable stays the default while a preview stabilizes. diff --git a/docs/readiness_pack/PROGRESS.md b/docs/readiness_pack/PROGRESS.md index 536cb574..aa614b44 100644 --- a/docs/readiness_pack/PROGRESS.md +++ b/docs/readiness_pack/PROGRESS.md @@ -1,6 +1,6 @@ # Honest progress accounting (No-Overclaim) -The denominator is the full distance from the 3.3.0 baseline (current release: 5.0.0) to the externally +The denominator is the full distance from the 3.3.0 baseline (current release: 5.1.0) to the externally audited, trustworthy state. That state is defined by audit completion and closed findings, not by any version number — the audit is decoupled from the version line and happens when maturity is reached, whatever version is current then. This is a factual progress statement, not a forward promise, and it is diff --git a/pyproject.toml b/pyproject.toml index 05e6cfa9..26ba082c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "proofbundle" -version = "5.0.0" +version = "5.1.0" description = "Emit and verify portable cryptographic evidence bundles, offline: Ed25519 + RFC 6962 Merkle + optional SD-JWT." readme = "README.md" requires-python = ">=3.10" diff --git a/src/proofbundle/__init__.py b/src/proofbundle/__init__.py index c06ced95..db167fe1 100644 --- a/src/proofbundle/__init__.py +++ b/src/proofbundle/__init__.py @@ -13,7 +13,7 @@ from typing import TYPE_CHECKING -__version__ = "5.0.0" +__version__ = "5.1.0" # The `proofbundle/v0.1` normative spec revision this build implements — kept in sync with the # `Revision:` line at the top of SPEC.md by tests/test_docs_truth.py (WP-B1, closes #28). Bump From 8d48f27539015ba93fd18a091f863ed8d4bcb9f9 Mon Sep 17 00:00:00 2001 From: kraxo Date: Mon, 31 Aug 2026 04:45:38 +0200 Subject: [PATCH 2/4] fix(readiness-pack): regenerate MANIFEST.sha256 after the version prose was corrected MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MEASURED, one failure in 2598 local tests: `test_audit_candidate_360.py::TestReadinessPackManifest::test_check_passes_on_generated` with `manifest drift: a pack file changed but MANIFEST.sha256 was not regenerated`. THE CHAIN, and it is mine. The version-integrity checker required `docs/readiness_pack/ PROGRESS.md` line 3 to say 5.1.0 instead of 5.0.0. That file is part of the readiness pack, and the pack carries a SHA-256 manifest over every one of its files. Correcting the prose therefore invalidated the manifest — the derived artefact was not pulled along. Regenerated with the intended producer (`scripts/readiness_pack_manifest.py --generate`), not by hand. PROGRESS.md now hashes to 940ecb46… on both sides; the test passes. The advisory self-receipt and its public key change with it. That is the design and not drift: the script states it in its own header — the receipt is signed with an EPHEMERAL key generated at build time, so it proves the pack CAN be receipted and verifies offline, and is explicitly NOT an attestation of authorship by a pinned identity. TWO WRONG TRAILS DISCARDED BY MEASUREMENT, not by opinion: * `VERSION_UNDER_TEST = "5.0.0"` in the audit matrix is NOT the cause and stays. Its test passes (8 of 8); it checks the BINDING, not the literal. The docstring warns explicitly that editing the literal "would make this instance green and recreate the class at the next version bump" — the matrix indexes `release_evidence_slots` by that pin, and there is no 5.1.0 evidence slot yet. * The `MANIFEST.in` line is NOT the cause. sdist from main: 740 files; from this branch: 741. The difference is exactly the added profile document; nothing disappeared. STILL OPEN, and not claimed: whether this also clears `coverage`, `crypto-floor` and `hermetic-cleanroom` on CI. Those run in their own environment and are re-measured by pushing this commit, not by asserting it here. --- docs/readiness_pack/MANIFEST.sha256 | 2 +- .../readiness_pack/proofbundle/readiness_pack.bundle.json | 8 ++++---- docs/readiness_pack/proofbundle/readiness_pack.pub.b64 | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/readiness_pack/MANIFEST.sha256 b/docs/readiness_pack/MANIFEST.sha256 index 2da4ffe1..900d51b0 100644 --- a/docs/readiness_pack/MANIFEST.sha256 +++ b/docs/readiness_pack/MANIFEST.sha256 @@ -1,6 +1,6 @@ c9118da583581c150090ddeef9932b8082690a8854320f41a553a3dc60835ed1 AUDITOR_OPEN_POINTS.md 3d66a85ff79c0258d794e7c8d8740f8a3b7a62e6ffd9bd7dada158a939964660 OPEN_QUESTIONS.md -248c109b3ed8d0f495fdc4bf7fe8c23a2467a91065c4cae3f6a9629df0d59d60 PROGRESS.md +940ecb46728863dad2a361c89f36da9b766f4825f8147cd3ba49126424b81819 PROGRESS.md 6ccab20c7daef6d71c13e0dcacba24a6a18dd480b5d41418e01b5bf2983ffc9c README.md c9ceb3f92a2f720d901affa8defb47004b58a51029317208db641ce53d90b284 REPRODUCTION_RUNBOOK.md 1b4e4316aaea6c962664fc08945c65a048147fdc4ad89a1e694818b934ef6fa2 calendar_independence.md diff --git a/docs/readiness_pack/proofbundle/readiness_pack.bundle.json b/docs/readiness_pack/proofbundle/readiness_pack.bundle.json index 9800a88c..abf83221 100644 --- a/docs/readiness_pack/proofbundle/readiness_pack.bundle.json +++ b/docs/readiness_pack/proofbundle/readiness_pack.bundle.json @@ -1,16 +1,16 @@ { "schema": "proofbundle/v0.1", - "payload_b64": "YzkxMThkYTU4MzU4MWMxNTAwOTBkZGVlZjk5MzJiODA4MjY5MGE4ODU0MzIwZjQxYTU1M2EzZGM2MDgzNWVkMSAgQVVESVRPUl9PUEVOX1BPSU5UUy5tZAozZDY2YTg1ZmY3OWMwMjU4ZDc5NGU3YzhkODc0MGY4YTNiN2E2MmU2ZmZkOWJkN2RhZGExNThhOTM5OTY0NjYwICBPUEVOX1FVRVNUSU9OUy5tZAoyNDhjMTA5YjNlZDhkMGY0OTVmZGM0YmY3ZmU4YzIzYTI0NjdhOTEwNjVjNGNhZTNmNmE5NjI5ZGYwZDU5ZDYwICBQUk9HUkVTUy5tZAo2Y2NhYjIwYzdkYWVmNmQ3MWMxM2UwZGNhY2JhMjRhNmExOGRkNDgwYjVkNDE0MThlMDFiNWJmMjk4M2ZmYzljICBSRUFETUUubWQKYzljZWIzZjkyYTJmNzIwZDkwMWFmZmE4ZGVmYjQ3MDA0YjU4YTUxMDI5MzE3MjA4ZGI2NDFjZTUzZDkwYjI4NCAgUkVQUk9EVUNUSU9OX1JVTkJPT0subWQKMWI0ZTQzMTZhYWVhNmM5NjI2NjRmYzA4OTQ1YzY1YTA0ODE0N2ZkYzRhZDg5YTFlNjk0ODE4YjkzNGVmNmZhMiAgY2FsZW5kYXJfaW5kZXBlbmRlbmNlLm1kCjlhOWE5ZTQ5NTYwNzZjNmMyODdkYWRhNWRlODJlYjY2Y2YyNTljZWM4OWE2ZTc5MTA4OGUxNzMzYTc0YzVlYTMgIGRpZmZlcmVudGlhbF9tYXRyaXgubWQKOWQ2MzhkMjZlNTE4MDRjMTFiNjViMzNmNTIyMGYxYjg1N2M0N2U0MWM2ZDdlYmI4YmYzMzk4ZjhjZjViMGNmMyAgaW5kZXguanNvbgpkMWQxZTA0ZTlkMmRlMzg0NDNlYzJiOTUzYzNhOWM5NjhhMzAzZTgxOTc3Y2I3ZTEyODQ2ODk0NTE2MGI1NmQ3ICBydXN0X3Bhcml0eV9zY29wZS5tZApmMmE3N2NhY2Q3ZmVmZTM5NDUwZWRiMTg1N2VjMmZmYzI1YTRkNGMzZDhkNjhlZmEwOTRiYTg0YWI3MzI1NzA4ICB0YW1wZXJfcmVzaXN0YW5jZS5tZAo2ZDZhNDA2NWExZTdlMDcxODE5OTYwY2VmOGUwMTE3YWRhOThlOTMxY2Q2MmZkOTQzNDFiNDVhZjQ4NjU3MTFhICB0aHJlYXRfbW9kZWxfZGVsdGFfMzYwLm1kCg==", + "payload_b64": "YzkxMThkYTU4MzU4MWMxNTAwOTBkZGVlZjk5MzJiODA4MjY5MGE4ODU0MzIwZjQxYTU1M2EzZGM2MDgzNWVkMSAgQVVESVRPUl9PUEVOX1BPSU5UUy5tZAozZDY2YTg1ZmY3OWMwMjU4ZDc5NGU3YzhkODc0MGY4YTNiN2E2MmU2ZmZkOWJkN2RhZGExNThhOTM5OTY0NjYwICBPUEVOX1FVRVNUSU9OUy5tZAo5NDBlY2I0NjcyODg2M2RhZDJhMzYxYzg5ZjM2ZGE5Yjc2NmY0ODI1ZjgxNDdjZDNiYTQ5MTI2NDI0YjgxODE5ICBQUk9HUkVTUy5tZAo2Y2NhYjIwYzdkYWVmNmQ3MWMxM2UwZGNhY2JhMjRhNmExOGRkNDgwYjVkNDE0MThlMDFiNWJmMjk4M2ZmYzljICBSRUFETUUubWQKYzljZWIzZjkyYTJmNzIwZDkwMWFmZmE4ZGVmYjQ3MDA0YjU4YTUxMDI5MzE3MjA4ZGI2NDFjZTUzZDkwYjI4NCAgUkVQUk9EVUNUSU9OX1JVTkJPT0subWQKMWI0ZTQzMTZhYWVhNmM5NjI2NjRmYzA4OTQ1YzY1YTA0ODE0N2ZkYzRhZDg5YTFlNjk0ODE4YjkzNGVmNmZhMiAgY2FsZW5kYXJfaW5kZXBlbmRlbmNlLm1kCjlhOWE5ZTQ5NTYwNzZjNmMyODdkYWRhNWRlODJlYjY2Y2YyNTljZWM4OWE2ZTc5MTA4OGUxNzMzYTc0YzVlYTMgIGRpZmZlcmVudGlhbF9tYXRyaXgubWQKOWQ2MzhkMjZlNTE4MDRjMTFiNjViMzNmNTIyMGYxYjg1N2M0N2U0MWM2ZDdlYmI4YmYzMzk4ZjhjZjViMGNmMyAgaW5kZXguanNvbgpkMWQxZTA0ZTlkMmRlMzg0NDNlYzJiOTUzYzNhOWM5NjhhMzAzZTgxOTc3Y2I3ZTEyODQ2ODk0NTE2MGI1NmQ3ICBydXN0X3Bhcml0eV9zY29wZS5tZApmMmE3N2NhY2Q3ZmVmZTM5NDUwZWRiMTg1N2VjMmZmYzI1YTRkNGMzZDhkNjhlZmEwOTRiYTg0YWI3MzI1NzA4ICB0YW1wZXJfcmVzaXN0YW5jZS5tZAo2ZDZhNDA2NWExZTdlMDcxODE5OTYwY2VmOGUwMTE3YWRhOThlOTMxY2Q2MmZkOTQzNDFiNDVhZjQ4NjU3MTFhICB0aHJlYXRfbW9kZWxfZGVsdGFfMzYwLm1kCg==", "signature": { "alg": "ed25519", - "public_key_b64": "/qDP0PM3TyOM5K8A+OOqZf93w3jUqQa0Z4uBFzzQnjs=", - "sig_b64": "HQiK+KhwJtE0SDUyirC/+SdtvFy3DpSOVbQsDBaPqjD+lkJgY+9q+aVgnG6CjF4aTldbdr/XHZnekKUQUV40DA==" + "public_key_b64": "fKPVnfgc8tn+AjhtsR4UHkSeN8Tqfv7rECzhvrHMURs=", + "sig_b64": "rlCo5DsTaKKIrLpSawAx1nny5B7gUDZvD/VtxQkEDgoYGwlsi6B8mXui2yx8+7KoCUql1PIOZTxsAlAwQ6XFBA==" }, "merkle": { "hash_alg": "sha256-rfc6962", "leaf_index": 0, "tree_size": 1, "inclusion_proof_b64": [], - "root_b64": "BYYf4Y8Uacu/6tzOPe0t0wfpG/4OxvAtieT7qdek5rM=" + "root_b64": "PgmBi5kRks0Ey8ecY9gtpSkP9spQKI1PrI8+1mspVi0=" } } diff --git a/docs/readiness_pack/proofbundle/readiness_pack.pub.b64 b/docs/readiness_pack/proofbundle/readiness_pack.pub.b64 index b3c3628e..f1a85927 100644 --- a/docs/readiness_pack/proofbundle/readiness_pack.pub.b64 +++ b/docs/readiness_pack/proofbundle/readiness_pack.pub.b64 @@ -1 +1 @@ -/qDP0PM3TyOM5K8A+OOqZf93w3jUqQa0Z4uBFzzQnjs= +fKPVnfgc8tn+AjhtsR4UHkSeN8Tqfv7rECzhvrHMURs= From a52c13e8db19acd606a1d94849672b20fa5a51b5 Mon Sep 17 00:00:00 2001 From: kraxo Date: Mon, 31 Aug 2026 14:33:30 +0200 Subject: [PATCH 3/4] =?UTF-8?q?fix(readiness):=205.1.0-Slot=20gefuellt=20u?= =?UTF-8?q?nd=20die=20Versionskonstante=20gezogen=20=E2=80=94=20die=20Matr?= =?UTF-8?q?ix=20urteilt=20jetzt=20ueber=20das,=20was=20ausgeliefert=20wird?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner-Karte OA-4f783bfb5b, Option 1: "5.1.0-Slot fuellen und das Receipt ueber den Runner erzeugen, dann ziehen und neu messen." WAS GEMESSEN WAR. audit-candidate-matrix meldete auf diesem Zweig VERSION PIN DRIFT: die Matrix war auf 5.0.0 gepinnt, das Paket liefert 5.1.0. Sie las damit 5.0.0-Bereitschaftsbelege und einen 5.0.0-Vorab-Audit-Eintrag, und ihr Urteil sagte nichts ueber 5.1.0 aus. Der Riegel version_pin_binding hat das selbst gemeldet — er wurde am 25.08. genau dafuer gebaut und hat getan, wozu er da ist. DER 5.1.0-SLOT nennt drei delivers und ZWEI open, und die zwei sind der eigentliche Inhalt: das Rad traegt das Umschlagprofil und die Vektoren weiterhin NICHT (null Eintraege unter docs/ und conformance/, vor wie nach dem Fix), und weil pip das Rad bevorzugt, bekommt ein schlichtes pip install proofbundle beides nicht. Dazu die Vektorluecke bei R5/R6. Ein Bereitschafts-Slot, der das verschweigt, waere die Ueberbehauptung, gegen die C11.1 steht. MANIFEST.sha256 ist mit dem vorgesehenen Erzeuger neu erzeugt, nicht von Hand: scripts/readiness_pack_manifest.py, 11 von 11 OK, zweiter Lauf byte-identisch. Von Hand haette es denselben Fehler wiederholt, der in der Nacht zum 31.08. vier Checks rot gemacht hat. DIE KONSTANTE: scripts/audit_candidate_matrix.py:60, genau eine Stelle, 5.0.0 -> 5.1.0. version_pin_binding steht danach auf bound. Sweep nach weiteren fest verdrahteten Versionen: ein zweiter Treffer geprueft und verworfen (gate_qualification_harness.py:155 ist eine in sich geschlossene Attrappe — _receipt() baut damit und _v() prueft mit expected_version=_VER dagegen, der Literalwert ist beliebig). NEU: scripts/audit_output_aufloesbar.py. Das v5.0.0-Receipt ist gueltig und bindet den Tag-Baum, aber sein signierter audit_output_digest liegt auf 0 von 915 verfolgten Dateien; die im audit_command genannte Aufzeichnung existiert, ist verfolgt und traegt einen ANDEREN Digest. C12.1 fuehrt das als ehrliche Grenze ("tamper-evident and attributable, not checkable"). Dieser Pruefer macht es pruefbar: drei Zustaende mit eigenen Exit-Codes, und er rechnet wie das Receipt ueber den DEKODIERTEN Text statt ueber Rohbytes — bei einer Datei mit einem ungueltigen Byte weichen beide ab. BEWUSST NICHT in pre_tag_audit_gate verdrahtet: das liesse v5.0.0 rueckwirkend durchfallen, und eine Regel, die die Vergangenheit aendert, ist keine. WAS C12.1 WEITERHIN ROT LAESST, und das ist richtig so: es gibt kein Receipt fuer 5.1.0, und es darf hier auch keines geben. Ein Receipt bindet subject_tree_digest; ein Arbeitszweig bekommt beim Merge einen weiteren Commit, ein hier erzeugtes Receipt bezeugte also einen Baum, der gleich aufhoert zu existieren. Owner-Entscheid vom 31.08.: Merge, dann Beleg gegen den GEMERGTEN Baum, dann die Owner-Signatur, dann der Tag. NICHT IN DIESEM COMMIT: die inspect_ai-Pin-Anhebung auf <=0.3.261. Die Messung dafuer liegt vor (frische Umgebung, 19 von 19 Tests, alle 17 on_*-Methoden der 0.3.261-API, die unter 0.3.217 aufgezeichnete Fixture liest weiter) — der Owner hat sie fuer diesen Merge ausdruecklich zurueckgestellt. Tests: tests/test_audit_output_aufloesbar.py 7/7. ruff sauber. --- docs/readiness_pack/MANIFEST.sha256 | 2 +- docs/readiness_pack/index.json | 219 +++++++++++++++--- .../proofbundle/readiness_pack.bundle.json | 8 +- .../proofbundle/readiness_pack.pub.b64 | 2 +- scripts/audit_candidate_matrix.py | 2 +- scripts/audit_output_aufloesbar.py | 133 +++++++++++ tests/test_audit_output_aufloesbar.py | 133 +++++++++++ 7 files changed, 457 insertions(+), 42 deletions(-) create mode 100644 scripts/audit_output_aufloesbar.py create mode 100644 tests/test_audit_output_aufloesbar.py diff --git a/docs/readiness_pack/MANIFEST.sha256 b/docs/readiness_pack/MANIFEST.sha256 index 900d51b0..35ffe82e 100644 --- a/docs/readiness_pack/MANIFEST.sha256 +++ b/docs/readiness_pack/MANIFEST.sha256 @@ -5,7 +5,7 @@ c9118da583581c150090ddeef9932b8082690a8854320f41a553a3dc60835ed1 AUDITOR_OPEN_P c9ceb3f92a2f720d901affa8defb47004b58a51029317208db641ce53d90b284 REPRODUCTION_RUNBOOK.md 1b4e4316aaea6c962664fc08945c65a048147fdc4ad89a1e694818b934ef6fa2 calendar_independence.md 9a9a9e4956076c6c287dada5de82eb66cf259cec89a6e791088e1733a74c5ea3 differential_matrix.md -9d638d26e51804c11b65b33f5220f1b857c47e41c6d7ebb8bf3398f8cf5b0cf3 index.json +5115c77bd80bc18cb671f5dc8632cca3781a6e5df37b6fd1ae97cc6a163436f7 index.json d1d1e04e9d2de38443ec2b953c3a9c968a303e81977cb7e128468945160b56d7 rust_parity_scope.md f2a77cacd7fefe39450edb1857ec2ffc25a4d4c3d8d68efa094ba84ab7325708 tamper_resistance.md 6d6a4065a1e7e071819960cef8e0117ada98e931cd62fd94341b45af4865711a threat_model_delta_360.md diff --git a/docs/readiness_pack/index.json b/docs/readiness_pack/index.json index ee8a1e73..0f055ed5 100644 --- a/docs/readiness_pack/index.json +++ b/docs/readiness_pack/index.json @@ -9,77 +9,214 @@ "statement": "A verified receipt is tamper-EVIDENT and, when anchored, tamper-RESISTANT: the chain head is bound to an external append-only reference so a storage-controlling actor cannot silently recompute it.", "closes_external_open_problem": "AuditWeave open problem (a): 'tamper-evident, not resistant — actors controlling storage could recompute chains'.", "evidence_ordered": [ - {"ref": "docs/readiness_pack/tamper_resistance.md", "kind": "argument", "note": "the resistance story, stated before the auditor asks"}, - {"ref": "docs/readiness_pack/calendar_independence.md", "kind": "argument", "note": "OTS calendar-independence: an upgraded proof verifies offline against a relying-party Bitcoin header; donation-financed calendars affect stamping availability, not verifiability; RFC 3161 is the immediate legal second anchor (WP-E)"}, - {"ref": "src/proofbundle/anchors_ots.py", "kind": "code", "note": "OpenTimestamps (Bitcoin) external anchor verify + calendar transparency (calendar_uris / operators)"}, - {"ref": "src/proofbundle/evidence_pack.py", "kind": "code", "note": "self-contained evidence pack: build + OFFLINE verify (no socket), the calendar-independent verify path"}, - {"ref": "src/proofbundle/anchors_rfc3161.py", "kind": "code", "note": "RFC 3161 TSA external anchor verify (the eIDAS-recognized legal second anchor)"}, - {"ref": "tests/test_ots_calendar_hardening.py", "kind": "tests", "note": "calendar outage / collusion+backdating / pending-never-pass / operator redundancy, plus the ripemd160-free confirmed-path fixture"}, - {"ref": "conformance/relation", "kind": "vectors", "note": "F1 corpus incl. forged-anchor-own-frozen negative vector"} + { + "ref": "docs/readiness_pack/tamper_resistance.md", + "kind": "argument", + "note": "the resistance story, stated before the auditor asks" + }, + { + "ref": "docs/readiness_pack/calendar_independence.md", + "kind": "argument", + "note": "OTS calendar-independence: an upgraded proof verifies offline against a relying-party Bitcoin header; donation-financed calendars affect stamping availability, not verifiability; RFC 3161 is the immediate legal second anchor (WP-E)" + }, + { + "ref": "src/proofbundle/anchors_ots.py", + "kind": "code", + "note": "OpenTimestamps (Bitcoin) external anchor verify + calendar transparency (calendar_uris / operators)" + }, + { + "ref": "src/proofbundle/evidence_pack.py", + "kind": "code", + "note": "self-contained evidence pack: build + OFFLINE verify (no socket), the calendar-independent verify path" + }, + { + "ref": "src/proofbundle/anchors_rfc3161.py", + "kind": "code", + "note": "RFC 3161 TSA external anchor verify (the eIDAS-recognized legal second anchor)" + }, + { + "ref": "tests/test_ots_calendar_hardening.py", + "kind": "tests", + "note": "calendar outage / collusion+backdating / pending-never-pass / operator redundancy, plus the ripemd160-free confirmed-path fixture" + }, + { + "ref": "conformance/relation", + "kind": "vectors", + "note": "F1 corpus incl. forged-anchor-own-frozen negative vector" + } ], - "open_questions": ["Q1_ANCHOR_TRUST_ROOT_DISTRIBUTION"] + "open_questions": [ + "Q1_ANCHOR_TRUST_ROOT_DISTRIBUTION" + ] }, { "id": "C2_VERIFY_DISTINGUISHES_AUTHENTIC_FROM_TAMPERED", "statement": "verify correctly separates an authentic, unmodified receipt from a tampered or forged one, and never raw-crashes on hostile input.", "evidence_ordered": [ - {"ref": "conformance/run_conformance.py", "kind": "harness", "note": "F1 offline corpus, 29 cases, one common vocabulary"}, - {"ref": "conformance/common_vocabulary.py", "kind": "code", "note": "F1 the one comparator all three differential layers share"}, - {"ref": "tools/pb_verify_rs/crosscheck.py", "kind": "differential", "note": "independent Rust second verifier agrees"}, - {"ref": "scripts/type_confusion_gate.py", "kind": "gate", "note": "F4 property-based JSON type-confusion matrix over the AST-discovered verify_* set — never-raise, structural"}, - {"ref": "tests/test_fuzz_parsers.py", "kind": "tests", "note": "Hypothesis fuzz of the byte/string parser class"} + { + "ref": "conformance/run_conformance.py", + "kind": "harness", + "note": "F1 offline corpus, 29 cases, one common vocabulary" + }, + { + "ref": "conformance/common_vocabulary.py", + "kind": "code", + "note": "F1 the one comparator all three differential layers share" + }, + { + "ref": "tools/pb_verify_rs/crosscheck.py", + "kind": "differential", + "note": "independent Rust second verifier agrees" + }, + { + "ref": "scripts/type_confusion_gate.py", + "kind": "gate", + "note": "F4 property-based JSON type-confusion matrix over the AST-discovered verify_* set — never-raise, structural" + }, + { + "ref": "tests/test_fuzz_parsers.py", + "kind": "tests", + "note": "Hypothesis fuzz of the byte/string parser class" + } ], - "open_questions": ["Q2_PRIMITIVE_HARDNESS", "Q3_SIDE_CHANNELS"] + "open_questions": [ + "Q2_PRIMITIVE_HARDNESS", + "Q3_SIDE_CHANNELS" + ] }, { "id": "C3_LINEAGE_LADDER_LOGIC_IS_SOUND_AND_NEVER_UPGRADES_CRYPTO", "statement": "The relation-lineage aggregation ladder is a sound join (one FAIL poisons the chain; an unresolved edge is never upgraded), and lineage never raises cryptoValid.", "evidence_ordered": [ - {"ref": "formal/model.py", "kind": "formal", "note": "F3 versioned model, obligations O1-O4 proven (bounded enumeration + z3-extended), grounded against the implementation"}, - {"ref": "formal/README.md", "kind": "doc", "note": "honest scope: logic yes; primitives/side-channels no"}, - {"ref": "src/proofbundle/relation.py", "kind": "code", "note": "the implementation the model mirrors"} + { + "ref": "formal/model.py", + "kind": "formal", + "note": "F3 versioned model, obligations O1-O4 proven (bounded enumeration + z3-extended), grounded against the implementation" + }, + { + "ref": "formal/README.md", + "kind": "doc", + "note": "honest scope: logic yes; primitives/side-channels no" + }, + { + "ref": "src/proofbundle/relation.py", + "kind": "code", + "note": "the implementation the model mirrors" + } ], - "open_questions": ["Q2_PRIMITIVE_HARDNESS"] + "open_questions": [ + "Q2_PRIMITIVE_HARDNESS" + ] }, { "id": "C4_RELEASES_CARRY_VERIFIABLE_HERMETIC_PROVENANCE", "statement": "Every published artifact is built hermetically, is byte-reproducible, and carries build provenance separated from the build (SLSA L3 shape).", "evidence_ordered": [ - {"ref": ".github/workflows/published-artifact-gate.yml", "kind": "ci", "note": "F2 hermetic cleanroom + determinism (two sdists byte-identical)"}, - {"ref": "scripts/build_reproducible.py", "kind": "code", "note": "F2 deterministic sdist normaliser (SOURCE_DATE_EPOCH + canonical tar)"}, - {"ref": ".github/workflows/reusable-build-attest.yml", "kind": "ci", "note": "F2 SLSA-L3 reusable signing workflow (signature separated from build) — built + CI-exercised (dry_run); release.yml adoption pending, see docs/roadmap/FRONTLOAD.md"}, - {"ref": ".github/workflows/release.yml", "kind": "ci", "note": "build-once-attest-then-publish, sha256 gate"} + { + "ref": ".github/workflows/published-artifact-gate.yml", + "kind": "ci", + "note": "F2 hermetic cleanroom + determinism (two sdists byte-identical)" + }, + { + "ref": "scripts/build_reproducible.py", + "kind": "code", + "note": "F2 deterministic sdist normaliser (SOURCE_DATE_EPOCH + canonical tar)" + }, + { + "ref": ".github/workflows/reusable-build-attest.yml", + "kind": "ci", + "note": "F2 SLSA-L3 reusable signing workflow (signature separated from build) — built + CI-exercised (dry_run); release.yml adoption pending, see docs/roadmap/FRONTLOAD.md" + }, + { + "ref": ".github/workflows/release.yml", + "kind": "ci", + "note": "build-once-attest-then-publish, sha256 gate" + } ], - "open_questions": ["Q4_REUSABLE_WORKFLOW_ORG_SHARED"] + "open_questions": [ + "Q4_REUSABLE_WORKFLOW_ORG_SHARED" + ] }, { "id": "C5_AUDIT_CANDIDATE_ALL_INTERNAL_GATES_GREEN", "statement": "audit-candidate: every internal, machine-checkable assurance gate is green; the sole remaining gate to stable is an independent external security audit. Status stays BETA / relation EXPERIMENTAL, NOT stable, NOT audited, NOT production-ready.", "evidence_ordered": [ - {"ref": "scripts/audit_candidate_matrix.py", "kind": "gate", "note": "the 33-check acceptance matrix (§9 minus the external audit), honest verdict vocabulary (PASS/PENDING_JUSTIFIED/DATA_BLOCKED/EXTERNAL_PENDING/FAIL)"}, - {"ref": "docs/readiness_pack/REPRODUCTION_RUNBOOK.md", "kind": "runbook", "note": "the command set a reviewer runs to reproduce every gate from the pack"}, - {"ref": "docs/readiness_pack/threat_model_delta_360.md", "kind": "doc", "note": "3.6.0 threat-model delta + audit scope freeze"}, - {"ref": "docs/readiness_pack/differential_matrix.md", "kind": "differential", "note": "Python<->Rust differential matrix over all normative relation paths"}, - {"ref": "docs/readiness_pack/rust_parity_scope.md", "kind": "doc", "note": "what is Rust-covered and what is deliberately PENDING (No-Fake, no fake 100%)"}, - {"ref": "tests/test_trust_pack_payloadtype_negatives.py", "kind": "tests", "note": "trust-pack payloadType/predicateType confusion negative vectors (O7)"}, - {"ref": "scripts/fuzz_soak.py", "kind": "gate", "note": "bounded fuzz-soak harness; 0 crash / 0 false-accept on the recorded run"}, - {"ref": "docs/readiness_pack/AUDITOR_OPEN_POINTS.md", "kind": "doc", "note": "what the machine cannot decide — the human audit's remit"} + { + "ref": "scripts/audit_candidate_matrix.py", + "kind": "gate", + "note": "the 33-check acceptance matrix (§9 minus the external audit), honest verdict vocabulary (PASS/PENDING_JUSTIFIED/DATA_BLOCKED/EXTERNAL_PENDING/FAIL)" + }, + { + "ref": "docs/readiness_pack/REPRODUCTION_RUNBOOK.md", + "kind": "runbook", + "note": "the command set a reviewer runs to reproduce every gate from the pack" + }, + { + "ref": "docs/readiness_pack/threat_model_delta_360.md", + "kind": "doc", + "note": "3.6.0 threat-model delta + audit scope freeze" + }, + { + "ref": "docs/readiness_pack/differential_matrix.md", + "kind": "differential", + "note": "Python<->Rust differential matrix over all normative relation paths" + }, + { + "ref": "docs/readiness_pack/rust_parity_scope.md", + "kind": "doc", + "note": "what is Rust-covered and what is deliberately PENDING (No-Fake, no fake 100%)" + }, + { + "ref": "tests/test_trust_pack_payloadtype_negatives.py", + "kind": "tests", + "note": "trust-pack payloadType/predicateType confusion negative vectors (O7)" + }, + { + "ref": "scripts/fuzz_soak.py", + "kind": "gate", + "note": "bounded fuzz-soak harness; 0 crash / 0 false-accept on the recorded run" + }, + { + "ref": "docs/readiness_pack/AUDITOR_OPEN_POINTS.md", + "kind": "doc", + "note": "what the machine cannot decide — the human audit's remit" + } ], - "open_questions": ["Q5_SINGLE_MAINTAINER_INDEPENDENCE", "Q6_PAYLOADTYPE_FORMAL", "Q2_PRIMITIVE_HARDNESS"] + "open_questions": [ + "Q5_SINGLE_MAINTAINER_INDEPENDENCE", + "Q6_PAYLOADTYPE_FORMAL", + "Q2_PRIMITIVE_HARDNESS" + ] } ], "release_evidence_slots": { "3.3.1-frontload": { "status": "filled", - "delivers": ["F1 corpus+vocabulary+comparator", "F2 hermetic gate + reproducible sdist + reusable attest workflow", "F3 formal model O1-O4", "F4 type-confusion generator", "F5 this pack", "F7 pre-tag adversarial audit gate"] + "delivers": [ + "F1 corpus+vocabulary+comparator", + "F2 hermetic gate + reproducible sdist + reusable attest workflow", + "F3 formal model O1-O4", + "F4 type-confusion generator", + "F5 this pack", + "F7 pre-tag adversarial audit gate" + ] }, "3.4.0": { "status": "reserved", - "expected_evidence": ["relation_signer ceremony receipts", "decoy-parent F1 enforcement vectors (into conformance/relation)", "formal obligation O5 TARGET_PIN_NOT_CRYPTO (into formal/model.py)", "outcome-gate evidence"] + "expected_evidence": [ + "relation_signer ceremony receipts", + "decoy-parent F1 enforcement vectors (into conformance/relation)", + "formal obligation O5 TARGET_PIN_NOT_CRYPTO (into formal/model.py)", + "outcome-gate evidence" + ] }, "3.5.0": { "status": "reserved", - "expected_evidence": ["relation_statement Rust parity (into rust_parity_registry.json + crosscheck.py)", "formal obligation O6 RETRACTS_NEVER_RAISES", "new verifier auto-covered by F4"] + "expected_evidence": [ + "relation_statement Rust parity (into rust_parity_registry.json + crosscheck.py)", + "formal obligation O6 RETRACTS_NEVER_RAISES", + "new verifier auto-covered by F4" + ] }, "3.6.0": { "status": "filled", @@ -105,6 +242,18 @@ "pre-tag ceremony option C (subject_tree_digest binds the HEAD tree minus audit_artifacts) + the two-half keyless receipt mode for the Mac key-custody handshake (private release key never on the build host)", "gate-qualification harness hardening: cc-classes + AST-count meta-tests + option-C dep-injection regression" ] + }, + "5.1.0": { + "delivers": [ + "receipt envelope profile reachable from the distribution: docs/RECEIPT_ENVELOPE_PROFILE.md is now shipped in the sdist. Measured before the fix at 27a84db3c6dc: the file appeared 0 times in the sdist and 0 times in the wheel. The cause was NOT a prune or exclude rule — MANIFEST.in is an allowlist and the file was on no line, missing by absence rather than by exclusion. Verified after the fix by a second build: 740 files instead of 739, exactly one added, none removed", + "conformance vectors were already complete and stay unchanged: 21 of 21 files, 10 of 10 case.json under conformance/envelope_profile/, carried by `graft conformance`", + "version pin binding proven in the field: the audit-candidate matrix reported VERSION PIN DRIFT for this release (scoped 5.0.0, package ships 5.1.0) before VERSION_UNDER_TEST was pulled — the three-state binding added on 2026-08-25 did what it was built for" + ], + "open": [ + "the WHEEL still carries neither the profile nor the vectors: 0 docs/ and 0 conformance/ entries, before and after the fix. Since pip prefers the wheel, a plain `pip install proofbundle` gets neither. This is a package-data design question, deliberately NOT decided in this release and recorded here rather than left out", + "conformance vectors exist for R1 to R4 only; R5 and R6 carry none. R5 states its requirement and points at CAP-1 (draft-hillier-coverage-attestation-00) by decision, not by omission; R6 is a governance rule" + ], + "status": "filled" } }, "navigation": { diff --git a/docs/readiness_pack/proofbundle/readiness_pack.bundle.json b/docs/readiness_pack/proofbundle/readiness_pack.bundle.json index abf83221..2ee9a634 100644 --- a/docs/readiness_pack/proofbundle/readiness_pack.bundle.json +++ b/docs/readiness_pack/proofbundle/readiness_pack.bundle.json @@ -1,16 +1,16 @@ { "schema": "proofbundle/v0.1", - "payload_b64": "YzkxMThkYTU4MzU4MWMxNTAwOTBkZGVlZjk5MzJiODA4MjY5MGE4ODU0MzIwZjQxYTU1M2EzZGM2MDgzNWVkMSAgQVVESVRPUl9PUEVOX1BPSU5UUy5tZAozZDY2YTg1ZmY3OWMwMjU4ZDc5NGU3YzhkODc0MGY4YTNiN2E2MmU2ZmZkOWJkN2RhZGExNThhOTM5OTY0NjYwICBPUEVOX1FVRVNUSU9OUy5tZAo5NDBlY2I0NjcyODg2M2RhZDJhMzYxYzg5ZjM2ZGE5Yjc2NmY0ODI1ZjgxNDdjZDNiYTQ5MTI2NDI0YjgxODE5ICBQUk9HUkVTUy5tZAo2Y2NhYjIwYzdkYWVmNmQ3MWMxM2UwZGNhY2JhMjRhNmExOGRkNDgwYjVkNDE0MThlMDFiNWJmMjk4M2ZmYzljICBSRUFETUUubWQKYzljZWIzZjkyYTJmNzIwZDkwMWFmZmE4ZGVmYjQ3MDA0YjU4YTUxMDI5MzE3MjA4ZGI2NDFjZTUzZDkwYjI4NCAgUkVQUk9EVUNUSU9OX1JVTkJPT0subWQKMWI0ZTQzMTZhYWVhNmM5NjI2NjRmYzA4OTQ1YzY1YTA0ODE0N2ZkYzRhZDg5YTFlNjk0ODE4YjkzNGVmNmZhMiAgY2FsZW5kYXJfaW5kZXBlbmRlbmNlLm1kCjlhOWE5ZTQ5NTYwNzZjNmMyODdkYWRhNWRlODJlYjY2Y2YyNTljZWM4OWE2ZTc5MTA4OGUxNzMzYTc0YzVlYTMgIGRpZmZlcmVudGlhbF9tYXRyaXgubWQKOWQ2MzhkMjZlNTE4MDRjMTFiNjViMzNmNTIyMGYxYjg1N2M0N2U0MWM2ZDdlYmI4YmYzMzk4ZjhjZjViMGNmMyAgaW5kZXguanNvbgpkMWQxZTA0ZTlkMmRlMzg0NDNlYzJiOTUzYzNhOWM5NjhhMzAzZTgxOTc3Y2I3ZTEyODQ2ODk0NTE2MGI1NmQ3ICBydXN0X3Bhcml0eV9zY29wZS5tZApmMmE3N2NhY2Q3ZmVmZTM5NDUwZWRiMTg1N2VjMmZmYzI1YTRkNGMzZDhkNjhlZmEwOTRiYTg0YWI3MzI1NzA4ICB0YW1wZXJfcmVzaXN0YW5jZS5tZAo2ZDZhNDA2NWExZTdlMDcxODE5OTYwY2VmOGUwMTE3YWRhOThlOTMxY2Q2MmZkOTQzNDFiNDVhZjQ4NjU3MTFhICB0aHJlYXRfbW9kZWxfZGVsdGFfMzYwLm1kCg==", + "payload_b64": "YzkxMThkYTU4MzU4MWMxNTAwOTBkZGVlZjk5MzJiODA4MjY5MGE4ODU0MzIwZjQxYTU1M2EzZGM2MDgzNWVkMSAgQVVESVRPUl9PUEVOX1BPSU5UUy5tZAozZDY2YTg1ZmY3OWMwMjU4ZDc5NGU3YzhkODc0MGY4YTNiN2E2MmU2ZmZkOWJkN2RhZGExNThhOTM5OTY0NjYwICBPUEVOX1FVRVNUSU9OUy5tZAo5NDBlY2I0NjcyODg2M2RhZDJhMzYxYzg5ZjM2ZGE5Yjc2NmY0ODI1ZjgxNDdjZDNiYTQ5MTI2NDI0YjgxODE5ICBQUk9HUkVTUy5tZAo2Y2NhYjIwYzdkYWVmNmQ3MWMxM2UwZGNhY2JhMjRhNmExOGRkNDgwYjVkNDE0MThlMDFiNWJmMjk4M2ZmYzljICBSRUFETUUubWQKYzljZWIzZjkyYTJmNzIwZDkwMWFmZmE4ZGVmYjQ3MDA0YjU4YTUxMDI5MzE3MjA4ZGI2NDFjZTUzZDkwYjI4NCAgUkVQUk9EVUNUSU9OX1JVTkJPT0subWQKMWI0ZTQzMTZhYWVhNmM5NjI2NjRmYzA4OTQ1YzY1YTA0ODE0N2ZkYzRhZDg5YTFlNjk0ODE4YjkzNGVmNmZhMiAgY2FsZW5kYXJfaW5kZXBlbmRlbmNlLm1kCjlhOWE5ZTQ5NTYwNzZjNmMyODdkYWRhNWRlODJlYjY2Y2YyNTljZWM4OWE2ZTc5MTA4OGUxNzMzYTc0YzVlYTMgIGRpZmZlcmVudGlhbF9tYXRyaXgubWQKNTExNWM3N2JkODBiYzE4Y2I2NzFmNWRjODYzMmNjYTM3ODFhNmU1ZGYzN2I2ZmQxYWU5N2NjNmExNjM0MzZmNyAgaW5kZXguanNvbgpkMWQxZTA0ZTlkMmRlMzg0NDNlYzJiOTUzYzNhOWM5NjhhMzAzZTgxOTc3Y2I3ZTEyODQ2ODk0NTE2MGI1NmQ3ICBydXN0X3Bhcml0eV9zY29wZS5tZApmMmE3N2NhY2Q3ZmVmZTM5NDUwZWRiMTg1N2VjMmZmYzI1YTRkNGMzZDhkNjhlZmEwOTRiYTg0YWI3MzI1NzA4ICB0YW1wZXJfcmVzaXN0YW5jZS5tZAo2ZDZhNDA2NWExZTdlMDcxODE5OTYwY2VmOGUwMTE3YWRhOThlOTMxY2Q2MmZkOTQzNDFiNDVhZjQ4NjU3MTFhICB0aHJlYXRfbW9kZWxfZGVsdGFfMzYwLm1kCg==", "signature": { "alg": "ed25519", - "public_key_b64": "fKPVnfgc8tn+AjhtsR4UHkSeN8Tqfv7rECzhvrHMURs=", - "sig_b64": "rlCo5DsTaKKIrLpSawAx1nny5B7gUDZvD/VtxQkEDgoYGwlsi6B8mXui2yx8+7KoCUql1PIOZTxsAlAwQ6XFBA==" + "public_key_b64": "hT9VYADCT/Sv+KTqPIplehfX+dd3s48cIiSha414yh4=", + "sig_b64": "WRuuixQsPGRgkQMuQRjH/j7Fw4sAWO+5/qW6Hon6OZZU3yokF5/jCQSpJvYjE/0WF/uqc/ft+plYbYO+l7O+Dg==" }, "merkle": { "hash_alg": "sha256-rfc6962", "leaf_index": 0, "tree_size": 1, "inclusion_proof_b64": [], - "root_b64": "PgmBi5kRks0Ey8ecY9gtpSkP9spQKI1PrI8+1mspVi0=" + "root_b64": "zIuDFtHNK2Ar5FWOeaWReULLCpH7mlND7evOG+sGcvs=" } } diff --git a/docs/readiness_pack/proofbundle/readiness_pack.pub.b64 b/docs/readiness_pack/proofbundle/readiness_pack.pub.b64 index f1a85927..96bea85c 100644 --- a/docs/readiness_pack/proofbundle/readiness_pack.pub.b64 +++ b/docs/readiness_pack/proofbundle/readiness_pack.pub.b64 @@ -1 +1 @@ -fKPVnfgc8tn+AjhtsR4UHkSeN8Tqfv7rECzhvrHMURs= +hT9VYADCT/Sv+KTqPIplehfX+dd3s48cIiSha414yh4= diff --git a/scripts/audit_candidate_matrix.py b/scripts/audit_candidate_matrix.py index 6946ca4f..b941d1db 100644 --- a/scripts/audit_candidate_matrix.py +++ b/scripts/audit_candidate_matrix.py @@ -57,7 +57,7 @@ _KNOWN_VERDICTS = {PASS, PENDING, DATA_BLOCKED, EXTERNAL, FAIL} _EXTERNAL_CHECK_ID = "EXT.1" # the ONE explicitly-external open audit -VERSION_UNDER_TEST = "5.0.0" +VERSION_UNDER_TEST = "5.1.0" def version_pin_binding(pinned: str) -> dict: diff --git a/scripts/audit_output_aufloesbar.py b/scripts/audit_output_aufloesbar.py new file mode 100644 index 00000000..41e3e810 --- /dev/null +++ b/scripts/audit_output_aufloesbar.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 +"""Does a pre-tag receipt's SIGNED ``audit_output_digest`` resolve to a findable artifact? + +═══ THE MEASURED OCCASION (2026-08-31, owner instruction) ═══ + +The v5.0.0 receipt (``audit_artifacts/500/pre_tag_receipt_v5.0.0.json``) is valid: it binds the +tag tree, it is signed by the pinned key, and ``pre_tag_audit_gate`` returns ok against that tree. +Its ``audit_output_digest`` is ``460fcdf3…`` — and **0 of 915 tracked files carry that digest**. +The ``audit_command`` even names a record, ``audit_artifacts/500/DEEP_RUN_RECORD_500_ITER8_ +CEREMONY.md``; that file exists, is tracked, was committed 57 minutes BEFORE signing and has not +changed since — but its digest is ``4ca5d2a5…``. So the prose points at one artifact and the +signed digest covers another: the digest was taken over something transient that was never +committed. + +C12.1 already records this as an HONEST LIMIT — *"the field is signed, which makes it +tamper-evident and attributable, not checkable"*. This module makes it checkable. + +WHAT THIS DELIBERATELY DOES NOT DO: it does not move any gate verdict. Wiring resolvability into +``pre_tag_audit_gate.evaluate`` would retroactively fail v5.0.0, whose receipt is otherwise sound +and already published. A rule that changes the past is not a rule, it is a rewrite. This reports; +the release procedure consumes the report before the payload goes to the key holder. + +THREE STATES, never two: + AUFLOESBAR a tracked file's digest equals the signed one — the file is named + NICHT_AUFLOESBAR every tracked file was hashed and none matched; the count is reported so + the negative is a measurement, not an impression + NICHT_MESSBAR the receipt is missing/unreadable, the field is absent, or git could not + list the tree. Explicitly NOT a pass and explicitly NOT a failure + +HOW THE DIGEST IS COMPUTED, and it matters: the receipt builds it as +``sha256_text(file.read_text(encoding="utf-8", errors="ignore"))`` — over the DECODED TEXT, not +the raw bytes. For clean UTF-8 the two coincide; for a file with one invalid byte they do not, +because ``errors="ignore"`` drops it. A checker using ``sha256sum`` would report NICHT_AUFLOESBAR +for a file that legitimately matches. This module computes it exactly the way the receipt does. + + python3 scripts/audit_output_aufloesbar.py --receipt [--repo .] [--json] + +Exit: 0 AUFLOESBAR · 1 NICHT_AUFLOESBAR · 2 NICHT_MESSBAR +""" +from __future__ import annotations + +import argparse +import json +import subprocess +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from pre_tag_receipt_lib import sha256_text # noqa: E402 + +SCHEMA = "b7n0de.audit_output_aufloesbar.v1" + + +def _tracked_files(repo: Path) -> list[str] | None: + """Tracked paths, or None if git cannot answer — None is the NICHT_MESSBAR signal, not []. + + The distinction is the whole point: an empty list would read as 'searched everything, found + nothing', which is a verdict. A broken git has no verdict to give.""" + try: + r = subprocess.run(["git", "-C", str(repo), "ls-files"], + capture_output=True, text=True, timeout=60) + except (OSError, subprocess.SubprocessError): + return None + if r.returncode != 0: + return None + return [z for z in r.stdout.splitlines() if z.strip()] + + +def _digest_wie_das_receipt(p: Path) -> str | None: + """Exactly the receipt's computation. Returns None for a path that cannot be read as a file.""" + try: + return sha256_text(p.read_text(encoding="utf-8", errors="ignore")) + except (OSError, ValueError): + return None + + +def aufloesbar(receipt: dict, repo: Path) -> dict: + """-> {zustand, digest, treffer, geprueft, grund}. Reports; never raises on a bad receipt.""" + aus: dict = {"schema": SCHEMA, "repo": str(repo)} + digest = (receipt or {}).get("audit_output_digest") + if not isinstance(digest, str) or not digest.strip(): + aus.update(zustand="NICHT_MESSBAR", digest=None, treffer=[], geprueft=0, + grund="das Receipt fuehrt kein audit_output_digest") + return aus + digest = digest.strip().lower() + aus["digest"] = digest + dateien = _tracked_files(repo) + if dateien is None: + aus.update(zustand="NICHT_MESSBAR", treffer=[], geprueft=0, + grund=f"git konnte den Baum nicht auflisten: {repo}") + return aus + treffer, geprueft = [], 0 + for rel in dateien: + p = repo / rel + if not p.is_file(): + continue + geprueft += 1 + if _digest_wie_das_receipt(p) == digest: + treffer.append(rel) + aus["geprueft"] = geprueft + aus["treffer"] = treffer + if treffer: + aus.update(zustand="AUFLOESBAR", + grund=f"der signierte Digest liegt als verfolgtes Artefakt vor: {', '.join(treffer)}") + else: + aus.update(zustand="NICHT_AUFLOESBAR", + grund=(f"{geprueft} verfolgte Datei(en) gehasht, keine traegt {digest[:12]}… — " + f"der signierte Digest ist attribuierbar, aber nicht nachrechenbar")) + return aus + + +def main(argv: list[str] | None = None) -> int: + ap = argparse.ArgumentParser(description=__doc__.split("\n\n")[0]) + ap.add_argument("--receipt", required=True, type=Path) + ap.add_argument("--repo", type=Path, default=Path(".")) + ap.add_argument("--json", action="store_true") + a = ap.parse_args(argv) + try: + rc = json.loads(a.receipt.read_text(encoding="utf-8")) + except (OSError, ValueError) as e: + r = {"schema": SCHEMA, "zustand": "NICHT_MESSBAR", "digest": None, "treffer": [], + "geprueft": 0, "grund": f"Receipt nicht lesbar: {type(e).__name__}: {e}"} + else: + r = aufloesbar(rc, a.repo.resolve()) + if a.json: + print(json.dumps(r, indent=2, ensure_ascii=False)) + else: + print(f"audit_output_digest: {r['zustand']} — {r['grund']}") + return {"AUFLOESBAR": 0, "NICHT_AUFLOESBAR": 1}.get(r["zustand"], 2) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/test_audit_output_aufloesbar.py b/tests/test_audit_output_aufloesbar.py new file mode 100644 index 00000000..106fea56 --- /dev/null +++ b/tests/test_audit_output_aufloesbar.py @@ -0,0 +1,133 @@ +"""tests/test_audit_output_aufloesbar.py — loest der signierte audit_output_digest auf ein Artefakt auf? + +Anlass 31.08.2026 (Owner): das v5.0.0-Receipt ist gueltig, bindet den Tag-Baum und ist von der +gepinnten Schluesselhaelfte signiert — aber sein ``audit_output_digest`` liegt auf KEINER der 915 +verfolgten Dateien. Die Prosa im ``audit_command`` nennt eine Aufzeichnung, die existiert, verfolgt +ist und einen ANDEREN Digest traegt. Der signierte Wert ist damit attribuierbar, aber nicht +nachrechenbar. Fuer 5.1.0 gilt: das Audit-Ergebnis existiert als auffindbares Artefakt, und das +wird VOR der Signatur geprueft. + +Hermetisch: eigenes git-Repo je Test, kein Netz, kein Zugriff auf den echten Baum. +""" +from __future__ import annotations + +import importlib.util +import json +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO = Path(__file__).resolve().parents[1] + + +def _lade(): + s = importlib.util.spec_from_file_location( + "_aufl_test", str(REPO / "scripts" / "audit_output_aufloesbar.py")) + m = importlib.util.module_from_spec(s) + sys.modules["_aufl_test"] = m + s.loader.exec_module(m) + return m + + +A = _lade() + + +def _git(repo: Path, *args: str) -> None: + subprocess.run(["git", *args], cwd=str(repo), capture_output=True, text=True, check=True, + env={"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t", "GIT_COMMITTER_NAME": "t", + "GIT_COMMITTER_EMAIL": "t@t", "HOME": str(repo), "PATH": "/usr/bin:/bin"}) + + +@pytest.fixture() +def baum(tmp_path): + """Ein Repo mit einer verfolgten Aufzeichnung und einer UNverfolgten daneben.""" + repo = tmp_path / "r" + (repo / "audit_artifacts").mkdir(parents=True) + _git(repo, "init", "-q") + (repo / "audit_artifacts" / "lauf.md").write_text("die Aufzeichnung\n", encoding="utf-8") + (repo / "README.md").write_text("# r\n", encoding="utf-8") + _git(repo, "add", ".") + _git(repo, "commit", "-q", "-m", "init") + # bewusst NICHT hinzugefuegt: so sieht der fluechtige Fall von 5.0.0 aus + (repo / "fluechtig.txt").write_text("nur im Arbeitsbaum\n", encoding="utf-8") + return repo + + +def _digest(p: Path) -> str: + return A.sha256_text(p.read_text(encoding="utf-8", errors="ignore")) + + +def test_verfolgte_datei_wird_gefunden_und_benannt(baum): + """GRUENE KONTROLLE. Ohne sie misst der Negativfall nur 'findet nie etwas'.""" + d = _digest(baum / "audit_artifacts" / "lauf.md") + r = A.aufloesbar({"audit_output_digest": d}, baum) + assert r["zustand"] == "AUFLOESBAR", r + assert r["treffer"] == ["audit_artifacts/lauf.md"], "der Treffer muss BENANNT werden, nicht nur gezaehlt" + assert r["geprueft"] >= 2 + + +def test_der_fall_500_eine_datei_ausserhalb_des_baums_loest_nicht_auf(baum): + """DER GEMESSENE FALL. Der Digest gehoert zu einer Datei, die es gibt — aber nicht im Baum. + Genau so sah das v5.0.0-Receipt aus, und genau das darf 5.1.0 nicht wiederholen.""" + d = _digest(baum / "fluechtig.txt") + r = A.aufloesbar({"audit_output_digest": d}, baum) + assert r["zustand"] == "NICHT_AUFLOESBAR", r + assert r["treffer"] == [] + assert r["geprueft"] >= 2, "die Zahl der geprueften Dateien gehoert zum Negativurteil" + assert str(r["geprueft"]) in r["grund"], "ein Negativbefund ohne Population ist ein Eindruck, keine Messung" + + +def test_fehlendes_feld_ist_NICHT_MESSBAR_weder_ja_noch_nein(baum): + r = A.aufloesbar({"version": "5.1.0"}, baum) + assert r["zustand"] == "NICHT_MESSBAR", r + assert r["digest"] is None + + +def test_kaputtes_git_ist_NICHT_MESSBAR_nicht_NICHT_AUFLOESBAR(tmp_path): + """DER UNTERSCHIED, DER ZAEHLT: 'ich habe alles durchsucht und nichts gefunden' ist ein Urteil, + 'ich konnte nicht suchen' ist keines. Eine leere Liste duerfte hier NIE als Negativ gelten.""" + kein_repo = tmp_path / "kein_repo" + kein_repo.mkdir() + r = A.aufloesbar({"audit_output_digest": "a" * 64}, kein_repo) + assert r["zustand"] == "NICHT_MESSBAR", r + assert r["geprueft"] == 0 + + +def test_der_digest_wird_wie_im_receipt_gerechnet_nicht_ueber_rohbytes(baum): + """Das Receipt rechnet ueber den DEKODIERTEN Text mit errors='ignore'. Bei einer Datei mit + einem ungueltigen Byte weicht das von sha256sum ab — ein Pruefer, der Rohbytes hasht, meldete + die richtige Datei als nicht auffindbar.""" + import hashlib + p = baum / "audit_artifacts" / "mit_kaputtem_byte.md" + p.write_bytes("Zeile\n".encode() + b"\xff" + "Ende\n".encode()) + _git(baum, "add", "-A") + _git(baum, "commit", "-q", "-m", "byte") + text_digest = A.sha256_text(p.read_text(encoding="utf-8", errors="ignore")) + byte_digest = hashlib.sha256(p.read_bytes()).hexdigest() + assert text_digest != byte_digest, "die Fixture trifft den Fall nicht — kein Unterschied zwischen Text und Bytes" + r = A.aufloesbar({"audit_output_digest": text_digest}, baum) + assert r["zustand"] == "AUFLOESBAR", "der Text-Digest muss gefunden werden" + assert "mit_kaputtem_byte.md" in r["treffer"][0] + r2 = A.aufloesbar({"audit_output_digest": byte_digest}, baum) + assert r2["zustand"] == "NICHT_AUFLOESBAR", "der Byte-Digest darf NICHT passen — sonst rechnet der Pruefer falsch" + + +def test_exit_codes_trennen_alle_drei_zustaende(baum, tmp_path, capsys): + d = _digest(baum / "audit_artifacts" / "lauf.md") + q = tmp_path / "q.json" + + q.write_text(json.dumps({"audit_output_digest": d}), encoding="utf-8") + assert A.main(["--receipt", str(q), "--repo", str(baum)]) == 0 + + q.write_text(json.dumps({"audit_output_digest": _digest(baum / "fluechtig.txt")}), encoding="utf-8") + assert A.main(["--receipt", str(q), "--repo", str(baum)]) == 1 + + q.write_text(json.dumps({"version": "5.1.0"}), encoding="utf-8") + assert A.main(["--receipt", str(q), "--repo", str(baum)]) == 2 + + +def test_unlesbares_receipt_stuerzt_nicht_ab_sondern_meldet(tmp_path, baum): + fehlt = tmp_path / "gibt_es_nicht.json" + assert A.main(["--receipt", str(fehlt), "--repo", str(baum)]) == 2 From 26b38d20c56ab653f42e078eefa2e4b58d573a32 Mon Sep 17 00:00:00 2001 From: kraxo Date: Mon, 31 Aug 2026 14:36:45 +0200 Subject: [PATCH 4/4] chore(deps): inspect_ai-Obergrenze 0.3.260 -> 0.3.261, gemessen statt still geweitet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner-Anordnung 2026-08-31T12:28:32Z. Sie faehrt mit dem Lauf mit, der durch den vorigen Commit ohnehin faellig ist, und kostet damit keinen zusaetzlichen Mutationslauf — eine pyproject-Aenderung beruehrt ihn nachweislich nicht. DER PIN-KOMMENTAR VERLANGT GENAU DAS: "a new inspect_ai release now needs a MEASURED bump here, not a silent widen". Hier ist die Messung. Gemessen in einer FRISCHEN Umgebung (eigenes venv, inspect_ai==0.3.261 plus dieses Paket vom Zweig gebaut, keine Mocks — die Tests importieren das echte Paket): * Entry-Point-Discovery findet `proofbundle`; `ProofbundleHooks` ist eine echte `Hooks`-Unterklasse und traegt alle 17 `on_*`-Methoden der 0.3.261-API. Keine ist verschwunden, keine ist dazugekommen, die wir nicht implementieren. * `test_inspect_hook.py` 9 von 9, zusammen mit `test_adapters.py` 19 von 19. * Die unter 0.3.217 aufgezeichnete Fixture liest weiter (`version=2`, `status=success`) — das Log-Format ist uns nicht unter den Fuessen weggezogen. DIE OBERGRENZE BLEIBT EINE OBERGRENZE. 0.3.262 und spaeter sind weiterhin UNGEMESSEN und damit ausgeschlossen. Zugelassen ist, was gemessen wurde, und die naechste Version verlangt dieselbe Arbeit noch einmal. Geaendert an beiden Stellen, wie angeordnet: das optionale Extra `inspect` und die `dev`-Liste. Der historische Kommentar zur 0.3.260-Messung bleibt stehen — er ist die Spur der vorigen Messung, nicht veralteter Text. --- pyproject.toml | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 26ba082c..f4361736 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -102,7 +102,19 @@ test = ["pytest>=7", "hypothesis>=6", "PyYAML>=6", "jsonschema>=4", "sd-jwt>=0.1 # decode_eval_claim, and the no-EMIT-no-receipt guard all pass; the fixture (recorded 0.3.217, log v2) # still reads and 0.3.260 still writes log v2. Trade-off (owner-directed): a new inspect_ai release now # needs a MEASURED bump here, not a silent widen — an optional extra, so the blast radius is opt-in only. -inspect = ['inspect_ai>=0.3.112,<=0.3.260'] +# +# 2026-08-31, THE MEASURED BUMP the line above demands — 0.3.260 -> 0.3.261, not a silent widen. +# Measured in a FRESH env (own venv, `inspect_ai==0.3.261` + this package built from the branch): +# * entry-point discovery finds `proofbundle`; `ProofbundleHooks` is a real `Hooks` subclass and +# carries all 17 `on_*` methods of the 0.3.261 API — no method went missing, none was added that +# we fail to implement +# * `test_inspect_hook.py` 9 of 9, together with `test_adapters.py` 19 of 19 — against the REAL +# package, no mocks +# * the fixture recorded under 0.3.217 still reads (`version=2`, `status=success`), so the log +# format did not move under us +# The ceiling stays a ceiling: 0.3.262 and later remain UNMEASURED and therefore excluded. What was +# measured is what is admitted, and the next release needs the same work again. +inspect = ['inspect_ai>=0.3.112,<=0.3.261'] # ruff is bounded, but the bound is no longer what protects this repo — `[tool.ruff.lint] select` # is (see that section). The cap `<0.16` that stood here from 2026-07-31 to 2026-08-07 bought time # to find out WHY 0.16 turned a green tree red. It is now measured, and it was never our code: @@ -143,7 +155,7 @@ inspect = ['inspect_ai>=0.3.112,<=0.3.260'] # optional `slhdsa` import really is unused there. With the project's own dependency present, all # eight versions pass. A floor claim is only worth the environment it was measured in. dev = ["pytest>=7", "ruff>=0.5,<0.17", "jsonschema>=4", "mypy>=1.8,<3", "build>=1", "hypothesis>=6", - "rfc8785>=0.1.4", "sd-jwt>=0.10", "PyYAML>=6", 'inspect_ai>=0.3.112,<=0.3.260'] + "rfc8785>=0.1.4", "sd-jwt>=0.10", "PyYAML>=6", 'inspect_ai>=0.3.112,<=0.3.261'] [project.urls] Homepage = "https://b7n0de.com"