package release import ( "crypto/sha256" "encoding/hex" "encoding/json" "errors" "os" "reflect" "strings" "github.com/carlosframework/platform/internal/rootstate" ) type HistoryRecovery struct { Binding HistoryBinding `json:"binding"` Attempt HistoryAttempt `json:"attempt"` Settled bool `json:"settled"` Failed bool `json:"failed"` Recovering bool `json:"recovering"` Verified bool `json:"verified"` } type historyRecoveryRecord struct { Schema int `json:"schema"` HistoryRecovery } func historyRecoveryKey(r HistoryRecovery) string { sum := sha256.Sum256([]byte(r.Binding.Account + "\x00" + r.Binding.App + "\x00" + r.Binding.Host + "\x00" + r.Attempt.ID)) return hex.EncodeToString(sum[:]) + ".outcome" } func validHistoryRecovery(r HistoryRecovery) bool { return r.Binding.valid() && r.Attempt.ID != "" && r.Attempt.Source != "" && r.Attempt.Version != "" && r.Attempt.SHA256 != "" && r.Attempt.Platform != "" && !r.Attempt.Started.IsZero() && !r.Attempt.PromotedAt.IsZero() } func (h *RootHistory) recoveryOutcomes() ([]HistoryRecovery, error) { if h == nil || h.Store == nil { return nil, os.ErrPermission } names, err := h.Store.Names() if err != nil { return nil, err } var out []HistoryRecovery for _, name := range names { if !strings.HasSuffix(name, ".outcome") { continue } raw, err := h.Store.Read(name) if err != nil { return nil, err } var r historyRecoveryRecord if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &r) != nil || r.Schema != 1 || !validHistoryRecovery(r.HistoryRecovery) || historyRecoveryKey(r.HistoryRecovery) != name { return nil, os.ErrPermission } if err := h.Store.Write(name, raw); err != nil { return nil, err } out = append(out, r.HistoryRecovery) } return out, nil } func (h *RootHistory) RetainRecovery(r HistoryRecovery) error { if h == nil || h.Store == nil || !validHistoryRecovery(r) { return os.ErrPermission } r.Attempt.Started, r.Attempt.PromotedAt = r.Attempt.Started.UTC(), r.Attempt.PromotedAt.UTC() key := historyRecoveryKey(r) raw, err := h.Store.Read(key) if err == nil { var old historyRecoveryRecord if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &old) != nil || old.Schema != 1 || !validHistoryRecovery(old.HistoryRecovery) || old.Binding != r.Binding || !reflect.DeepEqual(old.Attempt, r.Attempt) { return os.ErrPermission } r.Settled = r.Settled || old.Settled r.Failed = r.Failed || old.Failed r.Recovering = r.Recovering || old.Recovering r.Verified = r.Verified || old.Verified } else if !errors.Is(err, os.ErrNotExist) { return err } raw, err = json.Marshal(historyRecoveryRecord{Schema: 1, HistoryRecovery: r}) if err != nil { return err } return h.Store.Write(key, raw) } func (h *RootHistory) recoveryRefusals(outcomes []HistoryRecovery, b HistoryBinding) ([]Refusal, error) { var refs []Refusal count := 0 for _, r := range outcomes { if r.Binding != b { continue } count++ if count > historyLimit { return nil, os.ErrPermission } retired, err := h.recoveryRetired(r, outcomes) if err != nil { return nil, err } if retired { continue } if !r.Settled { return nil, os.ErrPermission } if r.Failed || r.Recovering { refs = append(refs, Refusal{Schema: 1, Host: b.Host, App: b.App, Version: r.Attempt.Version, SHA256: r.Attempt.SHA256, Platform: r.Attempt.Platform, PointerPromotedAt: r.Attempt.PromotedAt, RefusedAt: r.Attempt.Started, Kind: RefusalNotUp}) } else if !r.Verified { return nil, os.ErrPermission } } return refs, nil }