R-196 / R-204 item 2: a re-issue no longer marks a healthy escrow stale (hub v0.95.0)
ReissueCredentials marked the escrow stale on every re-issue, on precautionary grounds — the box's re-apply MIGHT mint a fresh repository password. It usually does not. A stale flag withholds restic_pw_sha256 from the ACK, which stops the controller's auto-confirm, which leaves EscrowState pending, which makes OffboxRunnable false: every off-site backup refused on a box whose key was never in doubt — and the customer told to re-run the one ceremony that would have superseded the key just recovered. The case it guessed at is measured elsewhere: the controller's Scenario-F re-check compares the sealed hash against the live repo password on every ACK (and the mark was BLINDING it by emptying that hash), and R-197's offsite_repo_key_changed fires on a proven difference across a supersession. offsite_reissued is unchanged. MarkEscrowStale is kept without a caller so a future EVIDENTIAL writer has the mechanism, with a test pinning it live. TestReissue_InvalidatesEscrow is replaced by its exact inverse.
This commit is contained in:
@@ -211,12 +211,27 @@ func TestReissue_RefusesAmbiguousLookup(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// v0.57.0 (2.3, escrow honesty) — re-issuing offsite credentials INVALIDATES the key-escrow blob:
|
||||
// the blob sealed the OLD repo password, so a recovery code minted against it would decrypt a
|
||||
// password that no longer opens the repo. RED-PROOF (Scenario C): on pre-fix code (no MarkEscrowStale
|
||||
// in ReissueCredentials + no stale plumbing) the hub keeps advertising the escrow as current after a
|
||||
// re-issue and keeps serving its restic-hash for auto-confirm — this test asserts it does NEITHER.
|
||||
func TestReissue_InvalidatesEscrow(t *testing.T) {
|
||||
// SCENARIO C (R-196 / R-204 item 2, hub v0.95.0) — re-issuing the offsite PROVIDER credential must
|
||||
// LEAVE A HEALTHY ESCROW ALONE.
|
||||
//
|
||||
// THIS TEST REPLACES `TestReissue_InvalidatesEscrow`, which asserted the opposite and was wrong on a
|
||||
// false premise: a re-issue resets the storage-provider account password and cannot touch the restic
|
||||
// REPOSITORY password, which is generated on the box and never leaves it except sealed under R. The
|
||||
// old assertion looked right because the EFFECT it checked (a stale escrow) was real — nobody checked
|
||||
// the CAUSE. Kept as one test rather than deleted so the inversion is visible in `git log -L`.
|
||||
//
|
||||
// WHAT IT ASSERTS IS THE CONSEQUENCE, not the mechanism: not "MarkEscrowStale was not called" but
|
||||
// "the ACK still carries the hash", because the hash is what the controller's auto-confirm needs and
|
||||
// its absence is what blocked every off-site backup during the 2026-08-04 drill.
|
||||
//
|
||||
// RED-PROOF: restore the marking — put back
|
||||
//
|
||||
// if host, _ := p.Store.GetHostByCustomer(customerID); host != nil {
|
||||
// if esc, _ := p.Store.GetHostEscrow(host.HostID); esc != nil { p.Store.MarkEscrowStale(host.HostID) }
|
||||
// }
|
||||
//
|
||||
// in ReissueCredentials → es.Stale becomes true, the hash is withheld, and this test fails on both.
|
||||
func TestReissue_DoesNotMarkAHealthyEscrowStale(t *testing.T) {
|
||||
p, _, st := newTestProvisioner(t)
|
||||
const cust = "cust-esc"
|
||||
if _, err := p.ProvisionOffsite(context.Background(), cust, Input{Enabled: true, Type: "shared", QuotaGB: 10}); err != nil {
|
||||
@@ -229,7 +244,6 @@ func TestReissue_InvalidatesEscrow(t *testing.T) {
|
||||
if _, _, err := st.SaveHostEscrow(cust+"-01", []byte("opaque-blob"), "SHA256:fp", "zero_knowledge", "2026-07-16T00:00:00Z", "OLDHASH"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Before re-issue: current escrow — the hub serves the sealed hash and is NOT stale.
|
||||
es, err := st.GetEscrowStatusForCustomer(cust)
|
||||
if err != nil || es == nil {
|
||||
t.Fatalf("escrow status (before): %v", err)
|
||||
@@ -238,25 +252,56 @@ func TestReissue_InvalidatesEscrow(t *testing.T) {
|
||||
t.Fatalf("pre-reissue escrow must be current: stale=%v hash=%q", es.Stale, es.ResticPwSHA256)
|
||||
}
|
||||
|
||||
// Re-issue the offsite credential — the repo password just changed under the sealed blob.
|
||||
// Re-issue the offsite PROVIDER credential. The box still holds its repo_password file, so the
|
||||
// repository password does NOT change — this is the ordinary re-issue shape.
|
||||
if err := p.ReissueCredentials(context.Background(), cust, "shared"); err != nil {
|
||||
t.Fatalf("reissue: %v", err)
|
||||
}
|
||||
|
||||
// After: the escrow is STALE and the restic-hash is WITHHELD (no auto-confirm against a dead key).
|
||||
es, err = st.GetEscrowStatusForCustomer(cust)
|
||||
if err != nil || es == nil {
|
||||
t.Fatalf("escrow status (after): %v", err)
|
||||
}
|
||||
if !es.Stale {
|
||||
t.Fatal("RED-PROOF: escrow must be STALE after an offsite re-issue (the hub was advertising ceremony-done against a key the repo no longer accepts)")
|
||||
if es.Stale {
|
||||
t.Fatal("a re-issue marked a HEALTHY escrow stale — this blocks every off-site backup and asks the customer for a ceremony that would supersede a good key (R-196)")
|
||||
}
|
||||
if es.ResticPwSHA256 != "" {
|
||||
t.Fatalf("a stale escrow must WITHHOLD the restic hash to inhibit auto-confirm, got %q", es.ResticPwSHA256)
|
||||
if es.ResticPwSHA256 != "OLDHASH" {
|
||||
t.Fatalf("the sealed hash must keep flowing to the ACK so the controller can auto-confirm; got %q", es.ResticPwSHA256)
|
||||
}
|
||||
|
||||
// The customer is still TOLD the credential moved — removing the false alarm must not remove the
|
||||
// true notice. `offsite_reissued` is untouched by this change.
|
||||
ev, eerr := st.GetLatestEventByType(cust, "offsite_reissued")
|
||||
if eerr != nil || ev == nil {
|
||||
t.Fatalf("offsite_reissued must still fire on every re-issue (err=%v ev=%+v)", eerr, ev)
|
||||
}
|
||||
// …and the false alarm is gone.
|
||||
if bad, _ := st.GetLatestEventByType(cust, "escrow_stale"); bad != nil {
|
||||
t.Fatalf("a re-issue must not raise escrow_stale on a healthy escrow: %+v", bad)
|
||||
}
|
||||
}
|
||||
|
||||
// The other half of Scenario C: the stale MECHANISM is intact and still does its job when something
|
||||
// legitimately sets it. Without this, "nothing marks stale any more" could decay into "stale no longer
|
||||
// works", and the next evidential caller would ship inert (the seam-built-but-never-wired shape).
|
||||
func TestEscrowStaleMechanism_StillWithholdsAndClears(t *testing.T) {
|
||||
_, _, st := newTestProvisioner(t)
|
||||
const cust = "cust-stale-mech"
|
||||
if err := st.UpsertHost(&store.Host{HostID: cust + "-01", CustomerID: cust, APIKey: "k"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, _, err := st.SaveHostEscrow(cust+"-01", []byte("blob"), "SHA256:fp", "zero_knowledge", "2026-07-16T00:00:00Z", "OLDHASH"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.MarkEscrowStale(cust + "-01"); err != nil {
|
||||
t.Fatalf("MarkEscrowStale: %v", err)
|
||||
}
|
||||
es, _ := st.GetEscrowStatusForCustomer(cust)
|
||||
if es == nil || !es.Stale || es.ResticPwSHA256 != "" {
|
||||
t.Fatalf("a marked escrow must read stale AND withhold the hash: %+v", es)
|
||||
}
|
||||
// A fresh ceremony (new blob sealing the new password) clears stale + serves the new hash.
|
||||
if _, _, err := st.SaveHostEscrow(cust+"-01", []byte("opaque-blob-2"), "SHA256:fp", "zero_knowledge", "2026-07-16T01:00:00Z", "NEWHASH"); err != nil {
|
||||
if _, _, err := st.SaveHostEscrow(cust+"-01", []byte("blob-2"), "SHA256:fp", "zero_knowledge", "2026-07-16T01:00:00Z", "NEWHASH"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
es, _ = st.GetEscrowStatusForCustomer(cust)
|
||||
|
||||
Reference in New Issue
Block a user