carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package release
2
3import (
4 "crypto/ed25519"
5 "encoding/json"
6 "errors"
7 "os"
8 "strings"
9 "time"
10
11 "github.com/carlosframework/platform/internal/rootstate"
12)
13
14type HistoryRecoveryRetirement struct {
15 Schema int `json:"schema"`
16 Kind string `json:"kind"`
17 Identity string `json:"identity"`
18 Operation string `json:"operation"`
19 IssuedAt time.Time `json:"issued_at"`
20 Binding HistoryBinding `json:"binding"`
21 Source string `json:"source"`
22 AttemptID string `json:"attempt_id"`
23 OutcomeDigest string `json:"outcome_digest"`
24 VerifiedID string `json:"verified_id"`
25 VerifiedDigest string `json:"verified_digest"`
26}
27
28func historyRecoveryDigest(r HistoryRecovery) string {
29 r.Attempt.Started, r.Attempt.PromotedAt = r.Attempt.Started.UTC(), r.Attempt.PromotedAt.UTC()
30 raw, err := json.Marshal(r)
31 if err != nil {
32 return ""
33 }
34 return reconstructionDigest(raw)
35}
36
37func historyRetirementKey(r HistoryRecovery) string {
38 return strings.TrimSuffix(historyRecoveryKey(r), ".outcome") + ".retired"
39}
40
41func validHistoryRetirement(g HistoryRecoveryRetirement) bool {
42 return g.Schema == 1 && g.Kind == "carlos-release-recovery-retirement" && len(g.Identity) == 64 &&
43 g.Operation != "" && len(g.Operation) <= 256 && !g.IssuedAt.IsZero() && g.Binding.valid() && g.Source != "" &&
44 g.AttemptID != "" && g.VerifiedID != "" && g.AttemptID != g.VerifiedID && len(g.OutcomeDigest) == 64 && len(g.VerifiedDigest) == 64
45}
46
47func retirementEvidence(g HistoryRecoveryRetirement, outcomes []HistoryRecovery) bool {
48 var old, good *HistoryRecovery
49 for i := range outcomes {
50 r := &outcomes[i]
51 if r.Binding != g.Binding {
52 continue
53 }
54 if r.Attempt.ID == g.AttemptID {
55 old = r
56 }
57 if r.Attempt.ID == g.VerifiedID {
58 good = r
59 }
60 }
61 return old != nil && good != nil && old.Settled && good.Settled && good.Verified && !good.Failed && !good.Recovering &&
62 good.Attempt.Source == g.Source && good.Attempt.Started.After(old.Attempt.Started) &&
63 historyRecoveryDigest(*old) == g.OutcomeDigest && historyRecoveryDigest(*good) == g.VerifiedDigest
64}
65
66func (h *RootHistory) RetireRecovery(pub ed25519.PublicKey, wire []byte, b HistoryBinding, now time.Time, resolve func(HistoryBinding) (string, error)) error {
67 if !b.valid() || resolve == nil || now.IsZero() || len(wire) > rootstate.MaxRecord {
68 return os.ErrPermission
69 }
70 var g HistoryRecoveryRetirement
71 if err := VerifyRootEnvelope(pub, wire, &g); err != nil {
72 return err
73 }
74 if !validHistoryRetirement(g) || g.Binding != b || g.IssuedAt.Before(now.Add(-HostRetryGrantTTL)) || g.IssuedAt.After(now.Add(hostRetrySkew)) {
75 return os.ErrPermission
76 }
77 if err := h.reconstructionReady(); err != nil {
78 return err
79 }
80 state, err := h.reconstructionState()
81 if err != nil {
82 return err
83 }
84 if state.Identity != g.Identity {
85 return os.ErrPermission
86 }
87 source, err := resolve(b)
88 if err != nil {
89 return err
90 }
91 if source != g.Source {
92 return os.ErrPermission
93 }
94 outcomes, err := h.recoveryOutcomes()
95 if err != nil {
96 return err
97 }
98 if !retirementEvidence(g, outcomes) {
99 return os.ErrPermission
100 }
101 source, err = resolve(b)
102 if err != nil {
103 return err
104 }
105 if source != g.Source {
106 return os.ErrPermission
107 }
108 raw, err := json.Marshal(g)
109 if err != nil {
110 return err
111 }
112 return h.Store.Write(historyRetirementKey(HistoryRecovery{Binding: b, Attempt: HistoryAttempt{ID: g.AttemptID}}), raw)
113}
114
115func (h *RootHistory) recoveryRetirementDigest(r HistoryRecovery, outcomes []HistoryRecovery) (string, error) {
116 raw, err := h.Store.Read(historyRetirementKey(r))
117 if errors.Is(err, os.ErrNotExist) {
118 return "", nil
119 }
120 if err != nil {
121 return "", err
122 }
123 var g HistoryRecoveryRetirement
124 if len(raw) > rootstate.MaxRecord || strictCanonicalJSON(raw, &g) != nil || !validHistoryRetirement(g) || g.Binding != r.Binding || g.AttemptID != r.Attempt.ID {
125 return "", os.ErrPermission
126 }
127 state, err := h.reconstructionState()
128 if err != nil {
129 return "", err
130 }
131 if state.Pending != nil || state.Identity != g.Identity {
132 return "", os.ErrPermission
133 }
134 if historyRecoveryDigest(r) != g.OutcomeDigest || !retirementEvidence(g, outcomes) {
135 return "", nil
136 }
137 if err := h.Store.Write(historyRetirementKey(r), raw); err != nil {
138 return "", err
139 }
140 return reconstructionDigest(raw), nil
141}
142
143func (h *RootHistory) recoveryRetired(r HistoryRecovery, outcomes []HistoryRecovery) (bool, error) {
144 digest, err := h.recoveryRetirementDigest(r, outcomes)
145 return digest != "", err
146}
147
148func (h *RootHistory) RecoveryRetired(r HistoryRecovery) (bool, error) {
149 if h == nil || h.Store == nil || !validHistoryRecovery(r) {
150 return false, os.ErrPermission
151 }
152 outcomes, err := h.recoveryOutcomes()
153 if err != nil {
154 return false, err
155 }
156 return h.recoveryRetired(r, outcomes)
157}
158