hub R-435: each clean-up window explains one fall only; a window stuck open past its deadline explains nothing (security review)

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-10-08 14:38:24 +02:00
parent b15061efb2
commit e1ff3210eb
4 changed files with 110 additions and 34 deletions
@@ -117,6 +117,38 @@ func TestR435_LyingWindowExplainsOnlyItsCap(t *testing.T) {
}
}
// Security review 2026-10-08: one window explains ONE fall. A window that removed 9 explains 69 → 60; a
// second fall 60 → 55 in the next report (inside the 2-h slack, no new window) alarms.
// RED-PROOF: stop marking windows as spent (usedWindows) → the second fall is explained again → 0 alarms → FAILS.
func TestR435_OneWindowExplainsOneFall(t *testing.T) {
st := newDiskStore(t)
cust := "p10"
if err := st.RecordOffsiteKeyInstalled(cust, "fp-1"); err != nil {
t.Fatal(err)
}
if ok, err := st.RecordOffsiteKeyConfirmed(cust, "fp-1"); err != nil || !ok {
t.Fatalf("confirm: %v %v", ok, err)
}
var msgs []string
saveOffsiteReport(t, st, cust, dropJSON(69, true, "", "ok"))
oc := NewOffsiteChecker(st, 48*time.Hour, func(_, et, _, msg, _, _ string) {
if et == "offsite_snapshots_dropped" {
msgs = append(msgs, msg)
}
}, quietLog())
closedWindow(t, cust, 69, 60)(st)
saveOffsiteReport(t, st, cust, dropJSON(60, true, "", "ok"))
oc.Check()
if len(msgs) != 0 {
t.Fatalf("the window explains 69 -> 60; got %d alarm(s)", len(msgs))
}
saveOffsiteReport(t, st, cust, dropJSON(55, true, "", "ok"))
oc.Check()
if len(msgs) != 1 {
t.Fatalf("a spent window must not explain the next fall 60 -> 55; got %d alarm(s)", len(msgs))
}
}
// Controls: the half-rule still governs a non-pinned tier, and an installed-but-unconfirmed key.
func TestR435_NotPinnedKeepsHalfRule(t *testing.T) {
if msgs := r435Run(t, "n1", r435Setup{next: dropJSON(60, true, "", "ok")}); len(msgs) != 0 {