internal/auditpush at 5ea687f383, reviewed by codex, verified by claude-code.
VerifyLedgerDir accepts a signed adjudication with no finding ID, an invalid empty verdict, and no decision-maker as valid, so malformed imported ledger claims pass integrity verification.
#!/bin/sh
set -eu
p=internal/auditpush/repro_malformed_adjudication_test.go
trap 'rm -f "$p"' EXIT
cat >"$p" <<'EOF'
// SPDX-License-Identifier: Elastic-2.0
package auditpush
import (
"crypto/ed25519"
"fmt"
"testing"
"github.com/pdbethke/corralai/internal/review"
)
func TestReproVerifierAcceptsMalformedAdjudication(t *testing.T) {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil { t.Fatal(err) }
signer := Ed25519LedgerSigner{KeyID: "repro", Key: priv}
dir := t.TempDir()
if _, err := WriteReview(dir, review.Review{Commit: "abc", Scope: "internal/auditpush"}, signer); err != nil {
t.Fatal(err)
}
entries, err := ReadLedgerDir(dir)
if err != nil || len(entries) != 1 { t.Fatalf("read review: entries=%d err=%v", len(entries), err) }
malformed := LedgerEntry{
Kind: KindAdjudication,
Adjudication: &Adjudication{
Adjudicates: entries[0].Hash, // missing required #finding-id
Verdict: "", // not confirmed or refuted
By: "", // no decision-maker
Reason: "",
},
}
if _, err := AppendLedgerEntry(dir, malformed, signer); err != nil {
t.Fatal(err)
}
checks, err := VerifyLedgerDir(dir, pub)
if err != nil || len(checks) != 2 { t.Fatalf("verify: checks=%d err=%v", len(checks), err) }
got := checks[1]
fmt.Printf("malformed adjudication: hashOK=%v linkOK=%v sigOK=%v problem=%q note=%q\n", got.HashOK, got.LinkOK, got.SigOK, got.Problem, got.Note)
if !got.HashOK || !got.LinkOK || !got.SigOK || got.Problem != "" {
t.Fatalf("defect not demonstrated: %+v", got)
}
}
EOF
go test -run '^TestReproVerifierAcceptsMalformedAdjudication$' -v ./internal/auditpush=== RUN TestReproVerifierAcceptsMalformedAdjudication malformed adjudication: hashOK=true linkOK=true sigOK=true problem="" note=" 383c8112c277 by : " --- PASS: TestReproVerifierAcceptsMalformedAdjudication (0.00s) PASS ok github.com/pdbethke/corralai/internal/auditpush 0.009s
I attempted refutation on three fronts and none held. First, I looked for validation on the write path that would make the malformed entry unconstructible: AppendLedgerEntry (internal/auditpush/ledgerdir.go:279) rejects only KindCheckpoint and then calls placeEntry (:297), which performs no shape checking on Adjudication at all — it only sets Format, Prev, Pushed, the scan uid, then hashes and signs. So the entry the script builds really can enter the directory. Second, I looked for the checks elsewhere in the verifier: the switch at :459-488 is the whole semantic gate, and its only adjudication-specific arm (:484) checks nothing but that the substring before the first '#' of Adjudicates is a hash already seen in this chain. Because strings.SplitN on a value with no '#' returns the entire string as element 0, an Adjudicates that is a bare review hash — no finding ID — passes that predicate and falls through to the plain-Note arm at :486, which merely formats Verdict and By into prose (:487) without testing either. Third, I checked whether the claim is broader than what is demonstrated: it is not. The validation the finding says is absent from the verifier lives only in WriteAdjudication (:545-572), which enforces the '#<id>' suffix, the confirmed/refuted verdict enum, non-empty By and Reason, and the existence of the named finding on the referenced review — exactly the four properties the verifier never re-derives, which is the finding's point that the lower-level append path admits records WriteAdjudication would reject. The reviewer's script asserts HashOK, LinkOK, SigOK and an empty Problem on the malformed entry and its recorded run exits 0 with that output, so the claim is demonstrated as stated.
confirmed by pdbethke — real, reproduced: AppendLedgerEntry/placeEntry check no entry shape, so ledger append admits an adjudication WriteAdjudication would refuse and verify passes it — the rule at one door
ref 559719120ef093cd39100b0841eee944f6a33409115f9018dee7e13945ee034f#R1
Recorded so that "found nothing here" can be told apart from "never looked here".
The ledger verifier has a semantic-integrity hole: finding 1 demonstrates that a signed, hash-correct adjudication can omit the finding identifier, use an invalid verdict, and name no decision-maker, yet VerifyLedgerDir reports no problem. This lets the lower-level append path—and therefore `corral ledger append`—admit records that the validated WriteAdjudication path would reject.
One finding, R1. I tried to refute it three ways and failed on all three. (a) Semantic validation at append time: AppendLedgerEntry (internal/auditpush/ledgerdir.go:279-292) does exactly one kind check — it rejects KindCheckpoint — then hands off to placeEntry (:297), which sets Format/Prev/Pushed, recomputes scan_uid for scan entries, hashes and signs. There is no adjudication shape check anywhere on that path, so a caller-constructed Adjudication with an empty Verdict, empty By and an Adjudicates value carrying no '#finding-id' is written verbatim. The validation the finding says is missing from the verifier does exist, but only in WriteAdjudication (:544-575), which is a different, higher-level entry point: it requires the '#<id>' suffix (:545-548), requires Verdict be VerdictConfirmed or VerdictRefuted (:549-551), requires non-empty By and Reason (:552-555), and requires the named finding ID to actually exist on the referenced review (:564-572). (b) Verifier-side compensation: the switch in VerifyLedgerDir (:459-488) is the only semantic gate, and its sole adjudication arm (:484) tests `!seen[strings.SplitN(e.Adjudication.Adjudicates, "#", 2)[0]]` — SplitN on a string with no '#' yields the whole string as element 0, so an Adjudicates that is a bare review hash with no finding ID satisfies the predicate and falls through to the benign Note arm at :486. Verdict and By are only ever formatted into that Note (:487), never tested. (c) Narrowness: the claim's three components — no finding ID, invalid empty verdict, no decision-maker — are each precisely what lines 484-487 fail to check, and the reviewer's script asserts HashOK && LinkOK && SigOK && Problem == "" on the malformed entry, which is what 'passes integrity verification' means for this function; the recorded output confirms it, including the tell-tale degenerate note ' 383c8112c277 by : '. The reviewer's own 'checked and sound' list is consistent with what I read of the retraction arm (:474-477) and the checkpoint arms (:468-473), which do carry the referential checks the adjudication arm lacks — which makes the gap look like an omission rather than a deliberate policy. I have no refutation.