Close nine adversarial-verification holes in signing and intake - #23
Merged
Conversation
The same reviewer that shaped the signing design attacked the merged implementation with live fixtures and confirmed nine holes. All nine are closed with regression tests. The highest-value one was not on the signing path: a case-variant intake key (outcome + Outcome) slipped the exact-match duplicate check, and Go's case-insensitive field matching read the second — turning a counterexample into a pass on every intake run. The strict decoder now rejects case-fold key collisions. - strictjson: reject case-fold key collisions (CRITICAL, live intake path) - verify -gate now requires -base: one head commit has many diffs - ValidateConsistency completed: evidence claim_id must match its claim, coverage file set must equal the change file set, tiers bounded to T0..T4, all counts non-negative and within the exact-integer range, negative intake counts rejected; GateBlocked blocks on Accepted < 1 - both renderers scrub every string field of control runes and strip Markdown-structural characters (scrubForDisplay) — a rendered receipt can no longer inject a terminal escape or a forged heading - checkAudience uses unicode.IsControl (C1 range, not ASCII only) - keygen opens the -out dir with O_NOFOLLOW and creates files via openat, so a symlinked output directory cannot redirect the write DESIGN.md records all nine and the lesson: a signature authenticates bytes, and byte-authenticity is worthless if the bytes are internally incoherent — the consistency validator is the load-bearing companion to the signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015GeUG1gboWiZSnFyzQghyp
correctful receipt33 claims — ✅ 33 verified · ❌ 0 refuted · Change:
|
| Tier | Claim | Statement |
|---|---|---|
| T1-assertion | TestContradictoryVerdictsFailLoudly |
Contradictory Verdicts Fail Loudly |
| T1-assertion | TestCrossSupplierCounterexampleSurvives |
Cross Supplier Counterexample Survives |
| T1-assertion | TestDuplicateJSONKeysRejected |
Duplicate JSONKeys Rejected |
| T1-assertion | TestParentSymlinkCannotSmuggleInTreeFiles |
Parent Symlink Cannot Smuggle In Tree Files |
| T1-assertion | TestRequiredNeedsUsableEvidence |
Required Needs Usable Evidence |
| T1-assertion | TestRejectedRowsAreScrubbed |
Rejected Rows Are Scrubbed |
| T1-assertion | TestCaseVariantOutcomeKeyRejected |
Case Variant Outcome Key Rejected |
| T1-assertion | TestRenderersStripControlRunes |
Renderers Strip Control Runes |
| T1-assertion | TestMarkdownRejectsInjectedHeading |
Markdown Rejects Injected Heading |
| T1-assertion | TestValidateConsistencyAcceptsAssembledReceipt |
Validate Consistency Accepts Assembled Receipt |
| T1-assertion | TestValidateConsistencyRejectsTampering |
Validate Consistency Rejects Tampering |
| T1-assertion | TestCanonicalGoldenVector |
Canonical Golden Vector |
| T1-assertion | TestKeygenAndLoadRoundTrip |
Keygen And Load Round Trip |
| T1-assertion | TestKeygenRefusesExistingAndLeavesNoPartialPair |
Keygen Refuses Existing And Leaves No Partial Pair |
| T1-assertion | TestKeygenRefusesPlantedSymlink |
Keygen Refuses Planted Symlink |
| T1-assertion | TestLoadKeyRejections |
Load Key Rejections |
| T1-assertion | TestKeygenRefusesSymlinkedParentDir |
Keygen Refuses Symlinked Parent Dir |
| T1-assertion | TestSignVerifyRoundTrip |
Sign Verify Round Trip |
| T1-assertion | TestVerifyWrongKeyFails |
Verify Wrong Key Fails |
| T1-assertion | TestVerifyAudienceBinding |
Verify Audience Binding |
| T1-assertion | TestVerifySubjectMatching |
Verify Subject Matching |
| T1-assertion | TestVerifyRejectsNonCanonicalArtifact |
Verify Rejects Non Canonical Artifact |
| T1-assertion | TestVerifyRejectsEveryByteMutation |
Verify Rejects Every Byte Mutation |
| T1-assertion | TestSignRefusesInconsistentReceipt |
Sign Refuses Inconsistent Receipt |
| T1-assertion | TestVerifyRejectsSignedInconsistency |
Verify Rejects Signed Inconsistency |
| T1-assertion | TestSignRefusesAlreadySigned |
Sign Refuses Already Signed |
| T1-assertion | TestSignRejectsControlAudience |
Sign Rejects Control Audience |
| T1-assertion | TestVerifyRejectsMalleableBase64 |
Verify Rejects Malleable Base64 |
| T1-assertion | TestGoldenSignature |
Golden Signature |
| T1-assertion | TestRFC8032Vector |
RFC8032 Vector |
| T1-assertion | TestSignRejectsC1Audience |
Sign Rejects C1 Audience |
| T1-assertion | TestCleanDocumentDecodes |
Clean Document Decodes |
| T1-assertion | TestStrictRejections |
Strict Rejections |
Harvest coverage: 17 files — 7 claimed · 10 scanned · 0 unread
3 spec-id mention(s) not minted as claims — the repo defines no spec-id corpus, so a reference has no possible referent
schema 0.0.14 · correctful v0.0.0-20260818034916-e37ce06b7125 e37ce06 · exit gate: refuted claims block; the remainder informs, never fails
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
After #22 merged, the reviewer that shaped the signing design attacked the implementation with live fixtures and confirmed nine holes — each reproduced, not argued. All nine are closed here with regression tests.
The highest-value finding was not on the signing path at all: a case-variant intake key (
{"outcome":"counterexample","Outcome":"verified"}) slipped the exact-match duplicate detector, and Go's case-insensitive struct-field matching then read the second key — turning a counterexample into a pass on every intake run, signed or not. The strict decoder now rejects any two keys in one object that are equal under case folding but not byte-equal.strictjson-gateaccepts the wrong diff-gaterequires-baseclaim_idmust match its claim; coverage file set must equal change file setAccepted < 1scrubForDisplaystrips control runes + Markdown structure in both renderersT0..T4before weighingunicode.IsControl-outdirO_NOFOLLOW, create viaopenatFindings 1, 3, 4, 6, 7 share one lesson: a signature authenticates bytes, and byte-authenticity is worthless if the bytes are internally incoherent. The consistency validator is the load-bearing companion to the signature and now re-derives and range-checks every field a reader or a gate trusts, not just the summary arithmetic.
Every attack was reproduced against the fixed binary (all now fail loudly) and the honest sign→verify→gate happy path still passes end to end.
🤖 Generated with Claude Code
https://claude.ai/code/session_015GeUG1gboWiZSnFyzQghyp