package release import ( "crypto/ed25519" "encoding/json" "errors" "os" "strings" "time" "github.com/carlosframework/platform/internal/rootstate" ) type HistoryRecoveryRetirement struct { Schema int `json:"schema"` Kind string `json:"kind"` Identity string `json:"identity"` Operation string `json:"operation"` IssuedAt time.Time `json:"issued_at"` Binding HistoryBinding `json:"binding"` Source string `json:"source"` AttemptID string `json:"attempt_id"` OutcomeDigest string `json:"outcome_digest"` VerifiedID string `json:"verified_id"` VerifiedDigest string `json:"verified_digest"` } func historyRecoveryDigest(r HistoryRecovery) string { r.Attempt.Started, r.Attempt.PromotedAt = r.Attempt.Started.UTC(), r.Attempt.PromotedAt.UTC() raw, err := json.Marshal(r) if err != nil { return "" } return reconstructionDigest(raw) } func historyRetirementKey(r HistoryRecovery) string { return strings.TrimSuffix(historyRecoveryKey(r), ".outcome") + ".retired" } func validHistoryRetirement(g HistoryRecoveryRetirement) bool { return g.Schema == 1 && g.Kind == "carlos-release-recovery-retirement" && len(g.Identity) == 64 && g.Operation != "" && len(g.Operation) <= 256 && !g.IssuedAt.IsZero() && g.Binding.valid() && g.Source != "" && g.AttemptID != "" && g.VerifiedID != "" && g.AttemptID != g.VerifiedID && len(g.OutcomeDigest) == 64 && len(g.VerifiedDigest) == 64 } func retirementEvidence(g HistoryRecoveryRetirement, outcomes []HistoryRecovery) bool { var old, good *HistoryRecovery for i := range outcomes { r := &outcomes[i] if r.Binding != g.Binding { continue } if r.Attempt.ID == g.AttemptID { old = r } if r.Attempt.ID == g.VerifiedID { good = r } } return old != nil && good != nil && old.Settled && good.Settled && good.Verified && !good.Failed && !good.Recovering && good.Attempt.Source == g.Source && good.Attempt.Started.After(old.Attempt.Started) && historyRecoveryDigest(*old) == g.OutcomeDigest && historyRecoveryDigest(*good) == g.VerifiedDigest } func (h *RootHistory) RetireRecovery(pub ed25519.PublicKey, wire []byte, b HistoryBinding, now time.Time, resolve func(HistoryBinding) (string, error)) error { if !b.valid() || resolve == nil || now.IsZero() || len(wire) > rootstate.MaxRecord { return os.ErrPermission } var g HistoryRecoveryRetirement if err := VerifyRootEnvelope(pub, wire, &g); err != nil { return err } if !validHistoryRetirement(g) || g.Binding != b || g.IssuedAt.Before(now.Add(-HostRetryGrantTTL)) || g.IssuedAt.After(now.Add(hostRetrySkew)) { return os.ErrPermission } if err := h.reconstructionReady(); err != nil { return err } state, err := h.reconstructionState() if err != nil { return err } if state.Identity != g.Identity { return os.ErrPermission } source, err := resolve(b) if err != nil { return err } if source != g.Source { return os.ErrPermission } outcomes, err := h.recoveryOutcomes() if err != nil { return err } if !retirementEvidence(g, outcomes) { return os.ErrPermission } source, err = resolve(b) if err != nil { return err } if source != g.Source { return os.ErrPermission } raw, err := json.Marshal(g) if err != nil { return err } return h.Store.Write(historyRetirementKey(HistoryRecovery{Binding: b, Attempt: HistoryAttempt{ID: g.AttemptID}}), raw) } func (h *RootHistory) recoveryRetirementDigest(r HistoryRecovery, outcomes []HistoryRecovery) (string, error) { raw, err := h.Store.Read(historyRetirementKey(r)) if errors.Is(err, os.ErrNotExist) { return "", nil } if err != nil { return "", err } var g HistoryRecoveryRetirement if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &g) != nil || !validHistoryRetirement(g) || g.Binding != r.Binding || g.AttemptID != r.Attempt.ID { return "", os.ErrPermission } state, err := h.reconstructionState() if err != nil { return "", err } if state.Pending != nil || state.Identity != g.Identity { return "", os.ErrPermission } if historyRecoveryDigest(r) != g.OutcomeDigest || !retirementEvidence(g, outcomes) { return "", nil } if err := h.Store.Write(historyRetirementKey(r), raw); err != nil { return "", err } return reconstructionDigest(raw), nil } func (h *RootHistory) recoveryRetired(r HistoryRecovery, outcomes []HistoryRecovery) (bool, error) { digest, err := h.recoveryRetirementDigest(r, outcomes) return digest != "", err } func (h *RootHistory) RecoveryRetired(r HistoryRecovery) (bool, error) { if h == nil || h.Store == nil || !validHistoryRecovery(r) { return false, os.ErrPermission } outcomes, err := h.recoveryOutcomes() if err != nil { return false, err } return h.recoveryRetired(r, outcomes) }