package release import ( "crypto/ed25519" "encoding/json" "os" "strings" "testing" "time" "github.com/carlosframework/platform/internal/rootstate" ) func retirementFixture(t *testing.T) (*RootHistory, *healthyRetryStore, HistoryRecovery, HistoryRecovery, HistoryRecoveryRetirement, ed25519.PublicKey, ed25519.PrivateKey) { t.Helper() h, s, b := rootHistoryFixture(t) now := time.Date(2026, 10, 13, 0, 0, 0, 0, time.UTC) old := HistoryRecovery{Binding: b, Attempt: HistoryAttempt{ID: "failed", Source: "canonical", Version: "v2", SHA256: "failed-digest", Platform: AdoptPlatform(), Started: now.Add(-time.Hour), PromotedAt: now.Add(-2 * time.Hour)}, Settled: true, Failed: true} good := old good.Attempt.ID, good.Attempt.Version, good.Attempt.SHA256 = "verified", "v3", "good-digest" good.Attempt.Started, good.Attempt.PromotedAt = now, now good.Failed, good.Verified = false, true for _, r := range []HistoryRecovery{old, good} { if err := h.RetainRecovery(r); err != nil { t.Fatal(err) } } status, err := h.ReconstructionStatus() if err != nil { t.Fatal(err) } g := HistoryRecoveryRetirement{Schema: 1, Kind: "carlos-release-recovery-retirement", Identity: status.Identity, Operation: "retire-failed", IssuedAt: now, Binding: b, Source: "canonical", AttemptID: old.Attempt.ID, OutcomeDigest: historyRecoveryDigest(old), VerifiedID: good.Attempt.ID, VerifiedDigest: historyRecoveryDigest(good)} pub, key, err := ed25519.GenerateKey(nil) if err != nil { t.Fatal(err) } return h, s, old, good, g, pub, key } func retireForTest(h *RootHistory, g HistoryRecoveryRetirement, pub ed25519.PublicKey, key ed25519.PrivateKey) error { wire, err := SignRootEnvelope(RootSigner(key), g) if err != nil { return err } return h.RetireRecovery(pub, wire, g.Binding, g.IssuedAt, func(HistoryBinding) (string, error) { return "canonical", nil }) } func TestPlan14HistoryExactRecoveryRetirement(t *testing.T) { for _, ambiguous := range []bool{false, true} { t.Run(map[bool]string{false: "failed", true: "ambiguous"}[ambiguous], func(t *testing.T) { h, s, old, _, g, pub, key := retirementFixture(t) if ambiguous { old.Failed = false raw, _ := json.Marshal(historyRecoveryRecord{Schema: 1, HistoryRecovery: old}) if err := s.Write(historyRecoveryKey(old), raw); err != nil { t.Fatal(err) } g.OutcomeDigest = historyRecoveryDigest(old) } other := old other.Attempt.ID = "unrelated" other.Attempt.SHA256 = "unrelated-digest" other.Failed = true if err := h.RetainRecovery(other); err != nil { t.Fatal(err) } if err := retireForTest(h, g, pub, key); err != nil { t.Fatal(err) } h = &RootHistory{Store: s} refs, err := h.Refusals(old.Binding) if err != nil || len(refs) != 1 || refs[0].SHA256 != "unrelated-digest" { t.Fatalf("retirement not exact: %+v %v", refs, err) } if err := h.RetainRecovery(old); err != nil { t.Fatal(err) } refs, err = h.Refusals(old.Binding) if err != nil || len(refs) != 1 { t.Fatalf("source replay erased retirement: %+v %v", refs, err) } if _, err := s.Read(historyRecoveryKey(old)); err != nil { t.Fatal("retirement deleted evidence", err) } if err := h.AddRefusal(old.Binding, Refusal{Host: old.Binding.Host, App: old.Binding.App, Version: old.Attempt.Version, SHA256: old.Attempt.SHA256, Kind: RefusalWrongProgram, RefusedAt: g.IssuedAt, PointerPromotedAt: old.Attempt.PromotedAt}); err != nil { t.Fatal(err) } refs, err = h.Refusals(old.Binding) if err != nil || len(refs) != 2 { t.Fatalf("retirement cleared another category: %+v %v", refs, err) } }) } } func TestPlan14HistoryRecoveryRetirementAuthority(t *testing.T) { for _, attack := range []string{"signature", "identity", "binding", "source", "outcome", "witness", "pending", "unverified", "failed-witness", "older-witness", "canonical-change", "expired", "reconstruction-pending"} { t.Run(attack, func(t *testing.T) { h, s, old, good, g, pub, key := retirementFixture(t) resolve := func(HistoryBinding) (string, error) { return "canonical", nil } now := g.IssuedAt switch attack { case "signature": _, key, _ = ed25519.GenerateKey(nil) case "identity": g.Identity = strings.Repeat("0", 64) case "binding": g.Binding.Host = "other.example.com" case "source": g.Source = "forged" case "outcome": g.OutcomeDigest = strings.Repeat("0", 64) case "witness": g.VerifiedDigest = strings.Repeat("0", 64) case "pending": old.Settled = false case "unverified": good.Verified = false case "failed-witness": good.Failed = true case "older-witness": good.Attempt.Started = old.Attempt.Started case "canonical-change": calls := 0 resolve = func(HistoryBinding) (string, error) { calls++ if calls > 1 { return "changed", nil } return "canonical", nil } case "expired": now = now.Add(HostRetryGrantTTL + time.Second) case "reconstruction-pending": state, err := h.reconstructionState() if err != nil { t.Fatal(err) } state.Pending = &reconstructionIntent{} if err := h.writeReconstruction(state); err != nil { t.Fatal(err) } } if attack == "pending" || attack == "unverified" || attack == "failed-witness" || attack == "older-witness" { for _, r := range []HistoryRecovery{old, good} { raw, _ := json.Marshal(historyRecoveryRecord{Schema: 1, HistoryRecovery: r}) if err := s.Write(historyRecoveryKey(r), raw); err != nil { t.Fatal(err) } } g.OutcomeDigest, g.VerifiedDigest = historyRecoveryDigest(old), historyRecoveryDigest(good) } wire, err := SignRootEnvelope(RootSigner(key), g) if err != nil { t.Fatal(err) } if err := h.RetireRecovery(pub, wire, old.Binding, now, resolve); err == nil { t.Fatal("unauthorized retirement accepted") } refs, err := h.Refusals(old.Binding) if err == nil && len(refs) == 0 { t.Fatal("refusal disappeared") } }) } } type retirementSyncStore struct { rootstate.Store fail bool } func (s *retirementSyncStore) Write(name string, raw []byte) error { if err := s.Store.Write(name, raw); err != nil { return err } if s.fail && strings.HasSuffix(name, ".retired") { return os.ErrPermission } return nil } func TestPlan14HistoryRetirementVisibleSync(t *testing.T) { h, s, old, good, g, pub, key := retirementFixture(t) fault := &retirementSyncStore{Store: s, fail: true} h.Store = fault if err := retireForTest(h, g, pub, key); err == nil { t.Fatal("unsynced retirement reported success") } if refs, err := h.Refusals(old.Binding); err == nil && len(refs) == 0 { t.Fatal("unsynced retirement granted permission") } fault.fail = false if refs, err := h.Refusals(old.Binding); err != nil || len(refs) != 0 { t.Fatalf("durable retry: %+v %v", refs, err) } good.Failed = true if err := h.RetainRecovery(good); err != nil { t.Fatal(err) } if refs, err := h.Refusals(old.Binding); err == nil && len(refs) < 2 { t.Fatalf("changed witness kept retirement: %+v", refs) } if err := retireForTest(h, g, pub, key); err == nil { t.Fatal("old signature retired changed evidence") } } func TestPlan14HistoryRetirementDelivery(t *testing.T) { h, _, old, _, g, pub, key := retirementFixture(t) store := NewDirStore(t.TempDir()) if err := PublishHistoryRecoveryRetirement(t.Context(), store, RootSigner(key), g); err != nil { t.Fatal(err) } resolve := func(HistoryBinding) (string, error) { return "canonical", nil } if err := h.ReceiveRecoveryRetirements(t.Context(), store, pub, old.Binding, g.IssuedAt, resolve); err != nil { t.Fatal(err) } view, err := h.MigrationStatus(old.Binding, resolve) if err != nil || len(view.Hosts) != 1 || len(view.Hosts[0].RetiredRecoveries) != 1 || view.Hosts[0].RecoveryDigests[old.Attempt.ID] != g.OutcomeDigest { t.Fatalf("retirement status: %+v %v", view, err) } if err := h.ReceiveRecoveryRetirements(t.Context(), store, pub, old.Binding, g.IssuedAt.Add(2*HostRetryGrantTTL), resolve); err != nil { t.Fatal("committed retirement expired", err) } }