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/sha256"
5 "encoding/hex"
6 "encoding/json"
7 "errors"
8 "os"
9 "slices"
10 "strings"
11 "time"
12
13 "github.com/carlosframework/platform/internal/rootstate"
14)
15
16const RootHistoryDir = "/var/lib/carlos-release/private/history"
17const historyLimit = 256
18
19type HistoryBinding struct {
20 Host string `json:"host"`
21 Account string `json:"account"`
22 App string `json:"app"`
23}
24
25type HistoryAttempt struct {
26 ID string `json:"id"`
27 Version string `json:"version"`
28 Source string `json:"source"`
29 Started time.Time `json:"started"`
30 SHA256 string `json:"sha256,omitempty"`
31 Platform string `json:"platform,omitempty"`
32 PromotedAt time.Time `json:"promoted_at,omitempty"`
33}
34
35func (h *RootHistory) Pending(b HistoryBinding) (*HistoryAttempt, error) {
36 r, err := h.read(b)
37 if err != nil {
38 return nil, err
39 }
40 if r.Attempt == nil {
41 return nil, nil
42 }
43 a := *r.Attempt
44 return &a, nil
45}
46
47type historyPrograms struct {
48 Revision uint64 `json:"revision"`
49 Complete bool `json:"complete"`
50 Values []string `json:"values"`
51}
52
53type historyRefusals struct {
54 Revision uint64 `json:"revision"`
55 Complete bool `json:"complete"`
56 Values []Refusal `json:"values"`
57}
58
59type historyRecord struct {
60 Creation string `json:"creation,omitempty"`
61 Fresh bool `json:"fresh,omitempty"`
62 Schema int `json:"schema"`
63 Binding HistoryBinding `json:"binding"`
64 Revision uint64 `json:"revision"`
65 Programs historyPrograms `json:"programs"`
66 Refusals historyRefusals `json:"refusals"`
67 Attempt *HistoryAttempt `json:"attempt,omitempty"`
68}
69
70// Callers hold the root release exclusion across reads, decisions and effects.
71// A store is supplied only by root construction, never by a service request.
72type RootHistory struct {
73 Store rootstate.Store
74}
75
76func historyKey(b HistoryBinding) string {
77 sum := sha256.Sum256([]byte(b.Account + "\x00" + b.App + "\x00" + b.Host))
78 return hex.EncodeToString(sum[:]) + ".json"
79}
80
81func (b HistoryBinding) valid() bool {
82 return ValidAccount(b.Account) && ValidApp(b.App) && b.Host != "" && len(b.Host) <= 253 && !strings.ContainsAny(b.Host, "/\\\x00\n\r")
83}
84
85func validHistory(r historyRecord, b HistoryBinding) bool {
86 if len(r.Creation) > 4096 || (r.Fresh && (r.Creation == "" || !r.Programs.Complete || !r.Refusals.Complete || len(r.Programs.Values) != 0 || len(r.Refusals.Values) != 0 || r.Attempt != nil)) {
87 return false
88 }
89 if !b.valid() || r.Schema != 1 || r.Binding != b || r.Revision == 0 ||
90 r.Programs.Revision == 0 || r.Programs.Revision > r.Revision || r.Refusals.Revision == 0 || r.Refusals.Revision > r.Revision ||
91 len(r.Programs.Values) > historyLimit || len(r.Refusals.Values) > historyLimit {
92 return false
93 }
94 seen := map[string]bool{}
95 for _, p := range r.Programs.Values {
96 if p == "" || len(p) > 4096 || strings.ContainsAny(p, "\x00\n\r") || seen[p] {
97 return false
98 }
99 seen[p] = true
100 }
101 for _, v := range r.Refusals.Values {
102 if v.Schema != 1 || v.Host != b.Host || v.App != b.App || v.Version == "" || v.RefusedAt.IsZero() || v.MoveOp != "" {
103 return false
104 }
105 if v.Kind != RefusalNotUp && v.Kind != RefusalWrongProgram && v.Kind != RefusalNotProven {
106 return false
107 }
108 }
109 if a := r.Attempt; a != nil && (a.ID == "" || a.Source == "" || a.Version == "" || a.Started.IsZero()) {
110 return false
111 }
112 return true
113}
114
115func (h *RootHistory) read(b HistoryBinding) (historyRecord, error) {
116 var r historyRecord
117 if err := h.reconstructionReady(); err != nil {
118 return r, err
119 }
120 return h.readRecord(b)
121}
122
123func (h *RootHistory) readRecord(b HistoryBinding) (historyRecord, error) {
124 var r historyRecord
125 if h == nil || h.Store == nil || !b.valid() {
126 return r, os.ErrPermission
127 }
128 raw, err := h.Store.Read(historyKey(b))
129 if err != nil {
130 return r, err
131 }
132 if len(raw) > rootstate.MaxRecord {
133 return r, os.ErrPermission
134 }
135 if strictCanonicalJSON(raw, &r) != nil || !validHistory(r, b) {
136 return historyRecord{}, os.ErrPermission
137 }
138 // A preceding process may have seen rename succeed and directory sync fail.
139 // Recommit the exact bytes before using either absence or an exception.
140 if err := h.Store.Write(historyKey(b), raw); err != nil {
141 return historyRecord{}, err
142 }
143 return r, nil
144}
145
146func (h *RootHistory) write(r historyRecord) error {
147 if h == nil || h.Store == nil || !validHistory(r, r.Binding) {
148 return os.ErrPermission
149 }
150 raw, err := json.Marshal(r)
151 if err != nil {
152 return err
153 }
154 if len(raw) > rootstate.MaxRecord {
155 return os.ErrPermission
156 }
157 return h.Store.Write(historyKey(r.Binding), raw)
158}
159
160func (h *RootHistory) Programs(b HistoryBinding) ([]string, error) {
161 r, err := h.read(b)
162 if err != nil {
163 return nil, err
164 }
165 if !r.Programs.Complete {
166 return nil, os.ErrPermission
167 }
168 return slices.Clone(r.Programs.Values), nil
169}
170
171func (h *RootHistory) RecordInstalledProgram(b HistoryBinding, program string) error {
172 r, err := h.read(b)
173 if errors.Is(err, os.ErrNotExist) && h != nil && h.Store != nil && b.valid() {
174 r = historyRecord{Schema: 1, Binding: b, Revision: 1,
175 Programs: historyPrograms{Revision: 1}, Refusals: historyRefusals{Revision: 1}}
176 } else if err != nil {
177 return err
178 }
179 if !r.Fresh && r.Programs.Complete && (program == "" || slices.Contains(r.Programs.Values, program)) {
180 return nil
181 }
182 if r.Revision == ^uint64(0) {
183 return os.ErrPermission
184 }
185 if program != "" && !slices.Contains(r.Programs.Values, program) {
186 r.Programs.Values = append(r.Programs.Values, program)
187 }
188 r.Revision++
189 r.Fresh = false
190 r.Programs.Complete, r.Programs.Revision = true, r.Revision
191 return h.write(r)
192}
193
194func (h *RootHistory) Refusals(b HistoryBinding) ([]Refusal, error) {
195 r, err := h.read(b)
196 if err != nil {
197 return nil, err
198 }
199 if !r.Refusals.Complete || r.Attempt != nil {
200 return nil, os.ErrPermission
201 }
202 outcomes, err := h.recoveryOutcomes()
203 if err != nil {
204 return nil, err
205 }
206 refs, err := h.recoveryRefusals(outcomes, b)
207 if err != nil {
208 return nil, err
209 }
210 refs = append(slices.Clone(r.Refusals.Values), refs...)
211 if len(refs) > historyLimit {
212 return nil, os.ErrPermission
213 }
214 return refs, nil
215}
216
217func (h *RootHistory) AddRefusal(b HistoryBinding, v Refusal) error {
218 r, err := h.read(b)
219 if errors.Is(err, os.ErrNotExist) && h != nil && h.Store != nil && b.valid() {
220 r = historyRecord{Schema: 1, Binding: b, Revision: 1,
221 Programs: historyPrograms{Revision: 1}, Refusals: historyRefusals{Revision: 1}}
222 } else if err != nil {
223 return err
224 }
225 v.Schema = 1
226 for _, old := range r.Refusals.Values {
227 if old.RefusedAt.Equal(v.RefusedAt) && old.PointerPromotedAt.Equal(v.PointerPromotedAt) {
228 old.RefusedAt, old.PointerPromotedAt = v.RefusedAt, v.PointerPromotedAt
229 }
230 if old == v {
231 return nil
232 }
233 }
234 if r.Revision == ^uint64(0) {
235 return os.ErrPermission
236 }
237 r.Revision++
238 r.Fresh = false
239 r.Refusals.Revision = r.Revision
240 next := r
241 next.Refusals.Values = append(slices.Clone(r.Refusals.Values), v)
242 raw, marshalErr := json.Marshal(next)
243 if marshalErr != nil {
244 return marshalErr
245 }
246 if len(next.Refusals.Values) > historyLimit || len(raw) > rootstate.MaxRecord {
247 r.Refusals.Complete = false
248 return errors.Join(os.ErrPermission, h.write(r))
249 }
250 return h.write(next)
251}
252
253// The caller has independently verified the signed target. Missing or corrupt
254// history cannot grant minting, but it does not prevent authorized recovery.
255func (h *RootHistory) Blocks(b HistoryBinding, p *ChannelPointer) (bool, error) {
256 if !b.valid() || p == nil || p.Version == "" {
257 return true, os.ErrPermission
258 }
259 for _, v := range h.recoveryDenials(b) {
260 if v.Version == p.Version && (v.Kind != RefusalNotUp || v.Blocks(p)) {
261 return true, nil
262 }
263 }
264 return false, nil
265}
266
267func (h *RootHistory) Begin(b HistoryBinding, a HistoryAttempt) error {
268 r, err := h.read(b)
269 if err != nil {
270 return err
271 }
272 if !r.Refusals.Complete || !r.Programs.Complete {
273 return os.ErrPermission
274 }
275 if r.Attempt != nil {
276 return os.ErrPermission
277 }
278 if r.Revision == ^uint64(0) {
279 return os.ErrPermission
280 }
281 r.Revision++
282 r.Fresh = false
283 r.Attempt = &a
284 return h.write(r)
285}
286
287// Only independently checked completion may settle an intent. Callers cannot
288// infer completion from a live link or registry stamp.
289func (h *RootHistory) Settle(b HistoryBinding, id string, refusal *Refusal) error {
290 r, err := h.read(b)
291 if err != nil {
292 return err
293 }
294 if r.Attempt == nil || r.Attempt.ID != id {
295 return os.ErrPermission
296 }
297 if refusal != nil {
298 v := *refusal
299 v.Schema = 1
300 if v.Version != r.Attempt.Version {
301 return os.ErrPermission
302 }
303 r.Refusals.Values = append(r.Refusals.Values, v)
304 }
305 if r.Revision == ^uint64(0) {
306 return os.ErrPermission
307 }
308 r.Revision++
309 r.Refusals.Revision = r.Revision
310 r.Attempt = nil
311 return h.write(r)
312}
313