Repository navigation
Claim Gate v0.1: ClaimBundle + ClaimReceipt — portable claim verification (signatures, expiry, supersession, verify()) - #34
Conversation
|
@codex review Exact-head review requested per the approved Claim Gate plan (W-B: Codex review then founder YES). Context: the five-minute-read portability test has passed round 2 — a clean-room implementer using ONLY docs/CLAIM_GATE.md + fixtures achieved 9/9 golden parity and judged the spec sufficient for fully independent implementation (issuer-side determinism included). Review focus: schema soundness, signing/verification code paths, the normative check orders vs implementation, fixture coverage. Merge follows only on clean review + named founder YES; v0.1 tag is a separate founder act. |
jamesfgibbons
left a comment
There was a problem hiding this comment.
Field Marshal truth-review (COMMENT, not approve-to-merge). Head 571fcfe.
HOLDS. Independent run in /tmp: tests.test_claim_gate 24/24; full suite 83/83 OK with constitutional-cms[claims] on PATH.
Confirmed on the branch, not from the PR body:
- prose + schemas + goldens are the external law
receipt_hashomitssuperseded/superseded_bybundle_mismatchrecomputes the presented bundle core and ignores the stated field- expiry is inclusive (
as_of >= valid_until) with a boundary golden as_ofis an input;verified_atis that clock- all-UNMEASURED is never a green receipt
- HTTP
GET /claims/POST /verifyremains spec-only - fixture PEMs are TEST ONLY and documented as protecting nothing
Chair still owns YES + tag. Codex review is still empty on this PR. Do not publish as 0.5.0 (pyproject.toml is still that number; CHANGELOG correctly says v0.6.0-dev).
Packet: /tmp/field-marshal-claim-gate-34-20260822.md.
…tion (DRAFT) Two new portable objects behind one primitive: a ClaimBundleV0_1 is the frozen, hashed, Ed25519-signed core (volatile facts forbidden inside); a ClaimReceiptV0_1 is the volatile verification record that quotes bundle_hash. Receipts attest integrity + policy conformance, never truth. - schemas/claim_bundle_v0_1.schema.json + claim_receipt_v0_1.schema.json (additionalProperties: false; sha256:-prefixed outer hashes; house timestamps generated_at/valid_until, RFC 3339 UTC Z) - constitutional_cms/claims.py: build_bundle / sign_bundle / verify_bundle / verify_receipt (+ generate_keypair, supersede_receipt); canonicalization reused from evaluator (one code path); cryptography behind the new optional extra "claims" with a clear import-guard error - CLI: claim-bundle / claim-sign / claim-verify, fully offline against a local keys file; exit 0 verified, 1 refused (typed), 2 operational - docs/CLAIM_GATE.md: five-minute boundary spec — signing, expiry law (earliest-expiry-governs, verifier-enforced), never-overwrite supersession, keys.json convention (spec only), normative-future HTTP surface, strict-consumer contract - Technical constitution: tests/test_claim_gate.py + byte-stable golden pairs in tests/golden-claims/ signed by TEST ONLY fixture keys, including an independent-reproduction path - CI installs the claims extra so the constitution runs; lean installs skip crypto clauses honestly DRAFT until founder ratification (v0.6.0-dev in CHANGELOG; no tag). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…text
Clean-room implementer (CLAIM_GATE.md + CANONICAL_JSON.md + fixtures only)
achieved 8/8 golden parity but recorded 8 spec gaps, 4 divergence-capable.
All closed in docs/CLAIM_GATE.md; reference implementation aligned:
- GAP-1: receipt_id derivation written exactly (cr_ + sha256(canonical
{"bundle_hash","verified_at","evaluator_version"})[:16])
- GAP-2: as_of is an explicit input; verified_at IS the reported clock
- GAP-3: normative unmeasured-value encoding (object with "verdict" in
{UNMEASURED, NOT_APPLICABLE}; everything else is measured)
- GAP-4: normative refusal order 1-8; first failing check's code; a
different order is non-conformant
- GAP-5: both directions of valid_until inequality are expiry_mismatch
- GAP-6: inclusive expiry boundary (expired when as_of >= valid_until);
code changed > to >= for bundles and receipts; new boundary golden
pair expired_boundary.* + named inclusivity test
- GAP-7: refused-receipt valid_until rule written as prose (min() with
parseable bundle valid_until, clamped to verified_at)
- GAP-8: bundle_mismatch trigger = recomputed hash of the presented
bundle's core bytes; stated bundle_hash field ignored (code changed
from stated-field comparison to recompute; test extended)
83/83 tests OK; 14 pre-existing goldens byte-identical after regen.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…+ law-source inversion - Normative receipt-verification order written (what the code does): receipt_malformed -> receipt_hash_mismatch -> bundle_mismatch (when a bundle is presented) -> receipt_superseded -> receipt_expired -> receipt_verified; first-failure single code; steps 4-5 skipped for the historical question. Same conformance language as the bundle order. Code already matched; no code change. - Law source inverted for external implementers: the PROSE + schemas + golden fixtures are the law; tests/test_claim_gate.py is the reference implementation's internal enforcement, not a normative source. 24/24 claim-gate tests OK; full suite 83/83 OK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…round 1
Three independent reviewers ran live exploits against this branch. Every
finding below was corroborated by at least two of them, and every fix carries
a test that fails against the pre-fix code (verified by stashing the fix).
BLOCKING-1 — earliest-expiry-governs compared STRINGS, not instants.
min() and != ran over RFC 3339 text. The schema permits fractional seconds and
'.' (0x2E) sorts before 'Z' (0x5A), so lexical order is not chronological.
Two wrong verdicts, both demonstrated: a bundle whose valid_until was LATER
than its earliest claim VERIFIED, and a law-correct bundle spelling one instant
two ways was REFUSED. Now parsed to instants and compared as instants in
build_bundle and verify_bundle alike; the spec says so normatively.
BLOCKING-2 — the issuer was never bound to the signing key.
verify_bundle looked up the key by key_id alone and never checked
bundle["issuer"] against anything, so example.com's own key signed a bundle
claiming issuer victim-bank.example and it verified green against example.com's
keys.json. The keys document now declares its issuer, is validated against a
new ClaimKeysV0_1 schema, and a mismatch is refused with the new typed code
issuer_mismatch — normative order step 4, right after key lookup.
BLOCKING-3 — duplicate key_id let document ORDER pick the verdict.
First-match lookup meant an appended impostor entry sharing a legitimate key_id
flipped every legitimate bundle (placed first) or got the attacker's accepted
(placed last). A keys document containing a duplicate key_id is now refused
fail-closed as an operational error (exit 2) — documented as operational
because the keys file is verifier configuration, not the artifact under test.
BLOCKING-4 — duplicate/contradictory claim_id verified green.
One claim_id carrying both {"verdict":"PASS"} and {"verdict":"FAIL"} passed;
consumers key by claim_id and silently got last-wins. Now bundle_malformed,
enforced in code because JSON Schema cannot express it.
BLOCKING-5 — uncaught exceptions escaped as exit 1, the documented code for a
TYPED REFUSAL, with tracebacks. verify_bundle and verify_receipt are now TOTAL
over artifacts: a malformed keys entry (AttributeError), a bare JSON string
presented as a bundle (AttributeError), --horizon-seconds 999999999999
(OverflowError), an uppercase-hex bundle_hash (ValueError from receipt schema
validation), an unhashable claim value (TypeError), and impossible or
leap-second timestamps (bare assert) all become typed refusals. The bare assert
is gone; format: date-time is not enforced here so timestamps are really
parse-validated; horizon_seconds is bounded to 10 years; the CLI exits 2 with
one line for anything unexpected and never prints a traceback.
Honesty fixes (v0.1 design limits, now stated rather than implied):
- Receipts are UNAUTHENTICATED. Receipt-only verification no longer prints
VERIFIED; it prints RECEIPT INTEGRITY OK (unauthenticated — supply --bundle
and --keys to verify the claim). New spec section "What a v0.1 receipt does
NOT prove".
- Supersession is an ISSUER-SIDE assertion, not revocation: the annotation sits
outside the hash by design, so a holder can strip it and the receipt verifies
as current. A consumer needing current state MUST consult the issuer's
receipt endpoint. Authenticated supersession/revocation deferred until earned.
- Key `status` was inert — a "retired" key signed fresh long-lived bundles.
Removed from the fixture, the generator, and the spec; a keys entry carrying
it is refused rather than silently ignored. Revocation in v0.1 is removal.
- Portability MUST-rejects: duplicate JSON object members (undefined in RFC
8259, resolved differently by different parsers) refused at parse time; JSON
floats and integers beyond 2^53-1 refused, because canonicalization collapses
1.0 to 1 and emits huge literals no double-parsing implementation reproduces.
Also added tests for seven checks a deletion experiment showed had NO failing
test: bundle schema step, receipt schema step, receipt_expired, receipt expiry
inclusivity, verify_receipt's default current-state question, NOT_APPLICABLE in
the unmeasured set, and the quoted-hash guard. Each was re-verified by deleting
the check and confirming the new test fails.
sign_bundle now refuses to emit a bundle no conformant verifier will accept —
defence in depth, not a security control: the adversarial specimens are signed
with raw Ed25519, which is how an attacker signs.
Tests 83 -> 118, all passing. Goldens regenerate byte-identical; package-data
copies stay byte-identical to schemas/.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
GAP-9 (blocking) — computed-timestamp spelling was unspecified. The code already normalized zero-fraction spellings (the exploit predates 9c19e68), but nothing wrote the rule down, so an independent issuer could spell one instant two ways and mint two receipt_ids. Normative rule written: UTC Z always; fraction OMITTED when the microsecond field is zero, otherwise EXACTLY six digits; normalization happens before the value enters receipt_id or receipt_hash. Pinned by a test over five equivalent as_of spellings (plus the six-digit fractional case). MINOR-1 — "compared at full fractional precision" was not implementable against a reference that arithmetics at microseconds. Bounded fractional seconds to six digits in both schemas and the verifier grammar; a seventh digit is now REFUSED, never truncated, so comparison resolution and arithmetic resolution are the same microsecond and schema, prose, and code agree. MINOR-2 — float conflict with CANONICAL_JSON.md scoped explicitly: claim bundles/receipts REFUSE a float; the older conformance artifacts still COLLAPSE integral floats. Both statements stand; neither artifact reaches the other's branch. MINOR-3 — keys-document prose aligned to the schema: an optional free-text comment is permitted at document level and never consulted; key ENTRIES carry exactly key_id, alg, public_key. MINOR-4 — duplicate key_id refusal labeled: operational, CLI exit 2, no receipt, never a typed verdict about the bundle. COVERAGE — new goldens pin issuer_mismatch, signature_invalid, and bundle_malformed (signed attacker-style with raw Ed25519, since sign_bundle refuses to emit them), plus a result-level receipt_hash_mismatch artifact. A new test asserts every code in CLAIM_REASON_CODES appears in some golden. Tests 118 -> 123, all passing. All 23 goldens regenerate byte-identical; no pre-existing golden changed; package-data copies stay byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Round 4 returned YES/YES (independent implementation of both verifier and issuer, 23/23 golden parity). One residual remained: the six-digit cap sentence named artifact codes, and an as_of is not an artifact, so the rule did not reach verifier input. Measured behavior of the reference today (pinned, not changed): an as_of with more than six fractional digits fails the RFC 3339 grammar, so it is an OPERATIONAL refusal — ClaimGateError, CLI exit 2, no receipt. It is neither truncated nor rounded. The reviewer's measured truncation was in their own implementation; this code already refuses. Refusal is the only self-consistent choice: truncation maps ...00.1234567Z to ...00.123456Z and ...00.0000006Z to ...00Z, rounding maps the latter to ...00.000001Z, and each mints a different verified_at -> receipt_id -> receipt_hash. A caller holding a finer clock decides its own rounding before calling, and owns that decision visibly. Tests 123 -> 125. Goldens unchanged (23/23 byte-identical). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ecycle Add normative section to CLAIM_GATE.md clarifying the boundary between Claim Gate verification (integrity + policy conformance) and entity lifecycle authority (subject existence). Key points: - verify() / verify_bundle / verify_receipt attest that a claim MAY be expressed, never whether the subject still exists - A green bundle/receipt about a retired/unknown/seasonal_inactive subject is ontology drift, not a verified lie - Lifecycle authority is a different court; companion law is contracts/entity_lifecycle.yaml and docs/CONSUMING_LAYER.md - Claim Gate MUST NOT consult entity_lifecycle.yaml - Entity lifecycle MUST NOT become a claim reason code No behavior change, no schema change, no golden rewrite. This is a spec join documenting what Claim Gate does NOT prove. Co-authored-by: James F Gibbons <james@jamesfgibbons.com>
e932e1e to
63a3793
Compare
Portability round 4: YES/YES — independent implementation of verifier AND issuer confirmed; 23/23 golden parity; independent issuer reproduced every receipt's hashed core byte-for-byte. No further review rounds will run. Last residual closed in ccdbfd9: an
as_ofcarrying more than six fractional digits is REFUSED as an operational error (CLI exit 2), never truncated and never rounded — truncation would map…00.1234567Zto…00.123456Zand…00.0000006Zto…00Z, rounding would map the latter to…00.000001Z, and each choice mints a differentverified_at→receipt_id→receipt_hash. Pinned bytest_round4_as_of_finer_than_microsecond_is_refused_not_truncatedandtest_round4_pinned_as_of_precision_rule_is_deterministic. Final count: 125 tests, all passing; 23/23 goldens byte-identical.Residual / deferred at ratification time
ensure_ascii=Falsehashes the exact code points given, so an NFC-normalizing producer and a non-normalizing one hash the same logical content to different digests. Affects producers on both sides; no verifier-direction ambiguity.bindingsissuer policy is unspecified: nothing says when an issuer SHOULD attachdecision_hash/signal_id, so two issuers with different binding policy mint different receipts from the same bundle, keys, andas_of. Issuer-direction only — a verifier handed either receipt behaves identically.Draft — merges only on a named founder YES after Codex review + the five-minute-read portability test.
Portability test round 2: PASS (8/8 gaps closed by prose, 9/9 parity, independent-implementation verdict YES). Round-2 residuals closed in 571fcfe: normative receipt-verification check order (first-failure single code, same conformance language as the bundle order) and the law-source clause inverted — prose + schemas + golden fixtures are the law for external implementers; the test file is internal enforcement only.
Portability round 3 — issuer side (verifier side: YES on first run)
Round 3 returned verifier-side YES (golden parity first run, all 8 prior gaps still closed, both round-2 residuals closed) and issuer-side NO: one blocking defect, four minor conflicts, plus a coverage note. All closed in 689615f.
test_gap9_equivalent_as_of_spellings_produce_byte_identical_receiptsover five equivalentas_ofspellings.bundle_malformed), never truncated. Comparison resolution and arithmetic resolution are now the same microsecond, so schema, prose, and code agree and no host language's clock precision can change a verdict.CheckCatalogV1,EvidenceBundleV1,ConformanceReceiptV1) still COLLAPSE integral floats (1.0→1) and may legitimately carry aratio. Neither artifact class reaches the other's branch.commentschema_version,issuer,keys, plus an OPTIONAL free-textcommentthat carries no semantics and is never consulted; key ENTRIES carry exactlykey_id,alg,public_key. Test proves a keys file with and without the comment yields byte-identical receipts.key_idexit code unlabeledissuer_mismatch,signature_invalid,bundle_malformedbundle/receipt goldens (signed attacker-style with raw Ed25519, sincesign_bundlerefuses to emit them) and a result-levelreceipt_hash_mismatchreceipt golden. New testtest_goldens_pin_every_bundle_reason_codeassertsCLAIM_REASON_CODESis fully covered by shipped bytes.The normative timestamp rule, verbatim from
docs/CLAIM_GATE.md:Tests 118 → 123, all passing (64 claim-gate). All 23 golden files regenerate byte-identical; no pre-existing golden changed; package-data schema copies stay byte-identical to
schemas/.Five-minute-read portability test: PASSED (yes-with-asterisk), asterisk now closed
A clean-room agent given ONLY
docs/CLAIM_GATE.md+docs/CANONICAL_JSON.md+ the fixtures achieved 8/8 golden parity — byte-exact hashes, verdicts, reason codes,valid_until. It recorded 8 spec gaps (4 divergence-capable while still passing goldens); all are now closed as normative spec text (commit e2810f2):cr_+ sha256(canonical JSON of{"bundle_hash","verified_at","evaluator_version"})[:16]as_ofis an explicit INPUT, never implicit wall clock;verified_atIS the reported clock"verdict"isUNMEASURED/NOT_APPLICABLE; all else measuredexpiry_mismatch; no second codeas_of >= valid_until; code aligned (>→>=for bundles AND receipts); new boundary golden pairexpired_boundary.*+ named inclusivity testmin(bundle valid_until, verified_at + horizon)clamped toverified_at; unparseable bundle expiry →verified_at + horizonbundle_mismatchtriggerbundle_hashfield ignored (code changed from stated-field comparison to recompute)Code changes required by the written rules: GAP-6 (two comparisons
>→>=) and GAP-8 (recompute instead of stated-field read). Everything else already matched. After changes: 83/83 tests OK (24 claim-gate tests), all 14 pre-existing goldens byte-identical on regeneration, 2 new boundary golden files.What existed vs what was invented
Existed in this repo and is REUSED, not reinvented:
docs/CANONICAL_JSON.mdandevaluator.canonical_json/digest—claims.pyimports the same code path; no copy-paste drift.PASS|FAIL|UNMEASURED|NOT_APPLICABLEand the existing reason_code vocabulary (extended, documented below).tests/golden-receipts/+ conformance fixtures) — copied for bundles/receipts astests/golden-claims/.decision_envelope_v1): the Bundle is the frozen signed core, volatile facts FORBIDDEN inside; the Receipt quotesbundle_hash.certified: false-style honesty: a receipt verifies INTEGRITY + policy conformance, never truth.ABSENT from the entire workspace and INVENTED here: issuer identity, Ed25519 signatures, receipt expiry, supersession, and an exported
verify(). Nothing in any repo previously let a third party verify a quoted claim offline.Technical-constitution test table (all run in CI via the
claimsextra)test_a_determinism_same_input_identical_canonical_bytes_and_hash_twicetest_b_one_changed_byte_fails_verificationtest_c_expired_bundle_refused_with_typed_reasontest_d_unknown_signing_key_fails_closedtest_e_superseded_receipt_cannot_masquerade_as_current+test_e_supersession_never_mutates_the_hashed_coretest_f_all_unmeasured_evidence_is_never_a_green_receipttest_g_independent_reproduction_from_schema_docs_and_fixture_bytestest_h_key_rotation_done_correctly_keeps_old_bundles_verifyingtest_i_golden_bundles_and_receipts_are_byte_stabletest_j_earliest_expiry_governs_is_enforced_not_proseSuite totals: 24 new tests in
tests/test_claim_gate.py; full repo suite 83/83 OK with the extra installed, 63 OK + 20 honest skips on a lean install (skips are labeled, never silent passes).validate_contractsandvalidate_web_conformanceboth pass. Goldens regenerate byte-identically (16/16 files) viascripts/generate_claim_goldens.py. The constitution is fixture-proven (functionality is new — there was no prior behavior to be red against).House-style rulings applied (deviations from the advisor sketch)
generated_at(neverissued_at),valid_until(neverexpires_at), RFC 3339 UTCZ.sha256:<64 hex>; quoting a legacy serpradiodecision_hashaccepts^(sha256:)?[0-9a-f]{64}$inbindingsonly.schema_version= const type name (ClaimBundleV0_1,ClaimReceiptV0_1);receipt_id=^cr_[a-z0-9]{6,}$(deterministically derived — no randomness in identity); claim ids use the existingclaim:prefix style.receipt_hashomitssuperseded/superseded_byas well as itself. A pure self-omitted hash would make marking a receipt superseded destroy its own historical verifiability, contradicting the never-overwrite law. Tested explicitly.docs/CLAIM_GATE.md):bundle_verified,bundle_malformed,hash_mismatch,signature_invalid,key_unknown,bundle_expired,expiry_mismatch,claims_unmeasured.Dependency ruling
cryptographylives in the optional extraconstitutional-cms[claims]— the base install stays PyYAML + jsonschema. Missing extra raises a clear error naming the install command; CI installs the extra so the constitution runs there. Fixture keys are TEST ONLY, clearly labeled, and protect nothing; no private key material is ever printed by any tool (keys load from file paths /CONSTITUTIONAL_CMS_CLAIM_KEYpath env).Deferred until earned
GET keys.json / GET /claims/{id} / GET /receipts/{id} / POST /verify) — spec'd as NORMATIVE FUTURE indocs/CLAIM_GATE.md, explicitly not served by anything in this PR..well-known/constitutional-cms/keys.jsonserving (format is spec-only; the CLI reads the same shape from a local--keysfile, fully offline).tests/golden-receipts/).DRAFT status recorded as
v0.6.0-dev (pre-ratification)in CHANGELOG. No tag will be cut from this branch (signal-contract RELEASING.md discipline).🤖 Generated with Claude Code
Adversarial review round 1 — five BLOCKING defects found and fixed (9c19e68)
Three independent reviewers ran live exploits against this branch. Every finding below was corroborated by at least two of them independently. Each fix carries a test that fails against the pre-fix code — verified by stashing the fix and re-running (29 of the new tests fail on the pre-fix tree).
Test counts: 83 → 118, all passing. Goldens regenerate byte-identically; package-data copies stay byte-identical to
schemas/.Fixed — blocking
min()and!=ran over RFC 3339 text atclaims.py:224and:445. The schema permits fractional seconds and.(0x2E) sorts beforeZ(0x5A), so lexical order ≠ chronological order.["…12:00:00Z", "…12:00:00.999999Z"]with bundlevalid_until= the.999999one → PASS; the bundle outlived its earliest claim. (b) bundle…12:00:00Zvs sole claim…12:00:00.000Z— same instant — →expiry_mismatch; a law-correct bundle refused.parse_instant/instant_key(exact,Decimalfraction, offset-aware). Allmin()and equality on instants in bothbuild_bundleandverify_bundle. New normative spec section "Timestamps are INSTANTS, never strings".test_bundle_later_than_its_earliest_claim_is_refused,test_same_instant_in_a_different_lexical_form_verifies,test_instant_key_orders_chronologically_where_strings_do_not,test_build_bundle_valid_until_is_the_earliest_instantkey_idalone;bundle["issuer"]checked against nothing. The spec's "the issuer's published key" had no enforcement behind the possessive.issuer: victim-bank.example→ PASS against example.com's ownkeys.json.issuer(newClaimKeysV0_1schema, validated). New typed codeissuer_mismatchat normative order step 4, right after key lookup, before signature. Added to the receipt schema enum andCLAIM_REASON_CODES. New spec section "Issuer binding — a key proves possession, never identity".test_cross_issuer_forgery_is_refused,test_matching_issuer_still_verifies,test_keys_document_without_an_issuer_is_refused_fail_closed,test_cli_cross_issuer_forgery_is_refused_with_a_typed_reasonkey_id: first match wins. Verdict decided by document ORDER.test-2026-aplaced first → every legitimate bundlesignature_invalid; placed last → passes.key_idrefuses the whole document, fail closed. Classified and documented as operational (ClaimGateError→ CLI exit 2), because the keys file is verifier configuration, not the artifact under test. Keys doc also validated against a real schema (schema_version,issuer,keys[]) instead of only "keys[]is non-empty".test_duplicate_key_id_is_refused_in_either_order(both orderings),test_malformed_keys_entry_is_an_operational_refusal_not_a_crash,test_empty_keys_array_is_refusedclaim_idverified green. Consumers key byclaim_idand silently get last-wins.claim_id, one{"verdict":"PASS"}and one{"verdict":"FAIL"}→ PASS.bundle_malformedin code (JSON Schema cannot express it), folded into normative step 1 alongside timestamp parse-validation and the portability rejects.test_duplicate_claim_id_is_bundle_malformed{"keys": ["oops"]}→AttributeError; bare JSON string as--bundle→AttributeError;--horizon-seconds 999999999999→OverflowError; uppercase-hexbundle_hash→ValueErrorfrom receipt schema validation;{"verdict": ["UNMEASURED"]}→TypeError(unhashable);2026-13-45T99:99:99Zand2026-06-30T23:59:60Z→ bareassertfailure.verify_bundle/verify_receiptare now TOTAL over artifacts — every malformed input is a typed refusal, never an exception. Bareasserts replaced. Unmeasured check is safe against non-string verdicts (spec: they count as MEASURED). Quotedbundle_hashvalidated against the full^sha256:[0-9a-f]{64}$before use or receipt-quoting. Real RFC 3339 parse-validation in code (format: date-timeis not enforced — norfc3339-validatorin the dependency set).horizon_secondsbounded to[0, 315360000](10 years). CLI: exit 1 only for typed refusals; anything unexpected exits 2 with one line, never a traceback.test_uppercase_hex_bundle_hash_is_bundle_malformed,test_unhashable_claim_value_counts_as_measured,test_impossible_and_leap_second_timestamps_are_bundle_malformed,test_horizon_seconds_is_bounded,test_verify_bundle_is_total_over_arbitrary_json,test_verify_receipt_is_total_over_arbitrary_json,test_verify_receipt_refuses_a_non_object_bundle_without_crashing,test_unusable_as_of_is_operational_not_a_verdict,test_cli_never_exits_1_for_an_unexpected_conditionDocumented as a v0.1 limit (spec text — receipts stay non-cryptographic in v0.1)
receipt_hashis self-consistency only.VERIFIED— it printsRECEIPT INTEGRITY OK (unauthenticated — supply --bundle and --keys to verify the claim).test_cli_receipt_only_mode_never_claims_VERIFIED,test_cli_bundle_mode_still_says_VERIFIEDsuperseded/superseded_bysit outside the hash by design (so history stays verifiable), which means a holder can strip the annotation and it verifies as current, hash intact.statuswas inert.keys.jsonshipped"status": "retired"and the code ignored it; a retired key signed fresh long-lived bundles fine.statusis refused rather than silently ignored, so the removal is enforceable and nothing advertises a control that does not exist. Key lifecycle semantics added to deferred-until-earned; revocation in v0.1 is removal from the document. Rotation still keeps old bundles verifying (theverifiedgolden is signed by the older key).test_h2_keys_entries_advertise_no_inert_status_control,test_h_key_rotation_done_correctly_keeps_old_bundles_verifying1.0→1), huge integers become 300-digit literals, duplicate object members are last-wins.object_pairs_hook); JSON floats and integers beyond2^53-1refused asbundle_malformed/receipt_malformed. Unicode normalization explicitly named as NOT yet specified.test_duplicate_json_object_members_are_refused,test_non_interoperable_numbers_are_bundle_malformed,test_cli_never_exits_1_for_an_unexpected_condition[duplicate JSON object member]Tests added for previously unguarded checks
A deletion experiment showed these checks had no failing test — you could delete them and the suite stayed green. Each now has one, re-verified by deleting the check and confirming the named test fails:
test_bundle_schema_validation_step_is_guardedhash_mismatch→ FAILStest_receipt_schema_validation_step_is_guardedreceipt_hash_mismatch→ FAILSreceipt_expiredtest_receipt_expired_is_refused_for_current_statetest_receipt_expiry_boundary_is_inclusiveverify_receiptdefault current-statetest_verify_receipt_defaults_to_the_current_state_questioncurrent=False→ FAILSNOT_APPLICABLEin the unmeasured settest_not_applicable_is_in_the_unmeasured_settest_uppercase_hex_bundle_hash_is_bundle_malformedTwo further tests retire the "dead constants" debt by making
CLAIM_REASON_CODES/RECEIPT_RESULT_CODESload-bearing:test_reason_code_constants_match_the_published_schema(every code the verifier can emit is in the receipt schema enum) andtest_every_emitted_code_is_a_declared_code(sweep every golden; no undeclared code escapes).sign_bundlenow also refuses to emit a bundle no conformant verifier will accept (test_sign_bundle_refuses_to_emit_what_no_verifier_accepts). This is defence in depth, not a security control — the adversarial specimens in the test suite are signed with raw Ed25519, which is exactly how an attacker signs.Named debt — non-blocking findings, carried forward
Recorded here rather than fixed; none changes a verdict in v0.1:
ensure_ascii=Falsehashes exact code points; a producer that NFC-normalizes and one that does not will disagree. Named in the spec as unspecified; no rule, no test.sigpattern (^[A-Za-z0-9+/]{86}==$) and the newpublic_keypattern (^[A-Za-z0-9+/]{43}=$) pin the canonical form, but there is no test that a non-canonically-padded or whitespace-bearing encoding is refused.reason_codeshas nomaxItems/uniqueItems— a receipt could carry["bundle_verified"] * 10_000and validate. v0.1 emits exactly one code, so nothing exercises it.observed_at <= valid_until, orgenerated_atto relate sanely to either. A bundle generated after it expires validates.valueis accepted; canonicalization will recurse over it.bindings/generation_digesthave no golden coverage — both are optional and neither appears in any golden pair, so their canonical placement is untested.docs/CLAIM_GATE.md.Not judged a defect
None. Every corroborated finding reproduced exactly as reported; the reproduction script output is in the commit's review trail. Two of the reported B5 symptoms needed a more specific trigger than stated but were real: a bare JSON string passed to
--bundlein bundle mode already returnedbundle_malformedcorrectly — theAttributeErrorfires in receipt mode (--receipt … --bundle <bare string>) and inclaim-sign. Both are fixed.