Files
felhom.eu/hub/internal/store/r366_escrow_key_change_test.go
T
admin 74009014aa R-366: the hub retains the escrow when the whole-guest backup key changes
A reinstall mints a new PBS key K while R-241 keeps the restic password, so
the supersession rule (restic sha only) overwrote the only copy of the old K
and every pre-reinstall whole-guest archive became unopenable for good. The
current row is now also retained when the key fingerprint changes (case and
space ignored; an empty fingerprint is unknown, not a change).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
2026-10-06 20:45:00 +02:00

66 lines
2.8 KiB
Go

package store
import "testing"
// R-366 — a reinstall mints a NEW whole-guest backup key K (`felhom-pbs-apply`: `--encryption-key
// autogen`) while, since R-241's mint guard, the box keeps its restic password. The escrow PUT then
// carries the SAME restic sha and a NEW key fingerprint. Until this fix the supersession rule looked
// at the restic sha only, so the row holding the OLD K (wrapped under the customer's recovery code)
// was OVERWRITTEN — every whole-guest archive written before the reinstall became unopenable for
// good, not only for the new box. The old row must be RETAINED when K changes.
//
// COMPANION RED-PROOF (observed, `audits/night-burndown-2026-10-06/r366/red-key-change.txt`): on the
// pre-fix rule (`curSHA != resticPwSHA256` alone) this fails with "a new backup key with the same
// restic password must supersede (retain the old K)". Restored.
func TestSaveHostEscrow_R366_NewBackupKeySameResticPasswordRetainsOldKey(t *testing.T) {
st := newTestStore(t)
const h = "h1"
if _, _, err := st.SaveHostEscrow(h, []byte("K-old-wrapped"), "3f:4f:65:c0", "zk", "2026-08-18T00:00:00Z", "SHA_SAME"); err != nil {
t.Fatal(err)
}
sup, _, err := st.SaveHostEscrow(h, []byte("K-new-wrapped"), "dd:d1:d8:53", "zk", "2026-08-21T00:00:00Z", "SHA_SAME")
if err != nil {
t.Fatal(err)
}
if !sup {
t.Fatal("a new backup key with the same restic password must supersede (retain the old K)")
}
old, err := st.ListSupersededEscrow(h)
if err != nil {
t.Fatal(err)
}
if len(old) != 1 || string(old[0].Blob) != "K-old-wrapped" || old[0].KeyFingerprint != "3f:4f:65:c0" {
t.Fatalf("the old K is not retained: %+v", old)
}
cur, _ := st.GetHostEscrow(h)
if cur == nil || string(cur.Blob) != "K-new-wrapped" || cur.KeyFingerprint != "dd:d1:d8:53" {
t.Fatalf("the current row must be the new K: %+v", cur)
}
}
// The same key in another letter case, or a legacy row with no fingerprint, is NOT a key change:
// a re-upload of the same K must stay idempotent (no retained row per re-ceremony).
func TestSaveHostEscrow_R366_SameKeyOrUnknownFingerprintDoesNotSupersede(t *testing.T) {
st := newTestStore(t)
const h = "h1"
if _, _, err := st.SaveHostEscrow(h, []byte("a"), "AB:CD", "zk", "2026-08-18T00:00:00Z", "SHA"); err != nil {
t.Fatal(err)
}
for i, fp := range []string{"ab:cd", " AB:CD ", ""} {
sup, _, err := st.SaveHostEscrow(h, []byte("b"), fp, "zk", "2026-08-19T00:00:00Z", "SHA")
if err != nil {
t.Fatal(err)
}
if sup {
t.Fatalf("case %d (%q): the same or an unknown fingerprint must not supersede", i, fp)
}
// keep the stored fingerprint known for the next case
if _, _, err := st.SaveHostEscrow(h, []byte("a"), "AB:CD", "zk", "2026-08-18T00:00:00Z", "SHA"); err != nil {
t.Fatal(err)
}
}
if n, _ := st.CountSupersededEscrow(h); n != 0 {
t.Fatalf("no key change happened, yet %d rows were retained", n)
}
}