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 "crypto/rand"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "errors"
10 "os"
11 "slices"
12 "strings"
13 "time"
14
15 "github.com/carlosframework/platform/internal/rootstate"
16)
17
18const reconstructionKey = "reconstruction.state"
19
20type HistoryReconstruction struct {
21 Replaces string `json:"replaces,omitempty"`
22 Schema int `json:"schema"`
23 Kind string `json:"kind"`
24 Identity string `json:"identity"`
25 Revision uint64 `json:"revision"`
26 Operation string `json:"operation"`
27 IssuedAt time.Time `json:"issued_at"`
28 Account string `json:"account"`
29 App string `json:"app"`
30 AppRevision uint64 `json:"app_revision"`
31 BakeRevision uint64 `json:"bake_revision"`
32 Hosts []HistoryReconstructionHost `json:"hosts"`
33 Production bool `json:"production"`
34 ProductionSetAt time.Time `json:"production_set_at"`
35 Holds map[string]HistoryReconstructionHold `json:"holds"`
36}
37
38type HistoryReconstructionHost struct {
39 Host string `json:"host"`
40 Source string `json:"source"`
41 Revision uint64 `json:"revision"`
42 Refusals []Refusal `json:"refusals"`
43}
44
45type HistoryReconstructionHold struct {
46 Window time.Duration `json:"window"`
47 SetAt time.Time `json:"set_at"`
48}
49
50type HistoryReconstructionStatus struct {
51 LastReconstructionDigest string `json:"last_reconstruction_digest,omitempty"`
52 Identity string `json:"identity"`
53 Revision uint64 `json:"revision"`
54 Pending bool `json:"pending"`
55}
56
57type reconstructionImage struct {
58 Name string `json:"name"`
59 Before string `json:"before"`
60 After json.RawMessage `json:"after"`
61}
62
63type reconstructionIntent struct {
64 Grant HistoryReconstruction `json:"grant"`
65 Digest string `json:"digest"`
66 Images []reconstructionImage `json:"images"`
67}
68
69type reconstructionState struct {
70 Schema int `json:"schema"`
71 Identity string `json:"identity"`
72 Revision uint64 `json:"revision"`
73 Last string `json:"last,omitempty"`
74 Pending *reconstructionIntent `json:"pending,omitempty"`
75}
76
77func reconstructionDigest(b []byte) string {
78 s := sha256.Sum256(b)
79 return hex.EncodeToString(s[:])
80}
81
82func (h *RootHistory) reconstructionState() (reconstructionState, error) {
83 var s reconstructionState
84 if h == nil || h.Store == nil {
85 return s, os.ErrPermission
86 }
87 b, err := h.Store.Read(reconstructionKey)
88 if err != nil {
89 return s, err
90 }
91 if len(b) > rootstate.MaxRecord || strictCanonicalJSON(b, &s) != nil || s.Schema != 1 || len(s.Identity) != 64 || s.Revision == 0 {
92 return reconstructionState{}, os.ErrPermission
93 }
94 if _, err := hex.DecodeString(s.Identity); err != nil {
95 return reconstructionState{}, os.ErrPermission
96 }
97 if err := h.Store.Write(reconstructionKey, b); err != nil {
98 return reconstructionState{}, err
99 }
100 return s, nil
101}
102
103func (h *RootHistory) writeReconstruction(s reconstructionState) error {
104 b, err := json.Marshal(s)
105 if err != nil {
106 return err
107 }
108 if len(b) > rootstate.MaxRecord {
109 return os.ErrPermission
110 }
111 return h.Store.Write(reconstructionKey, b)
112}
113
114func (h *RootHistory) ReconstructionStatus() (HistoryReconstructionStatus, error) {
115 s, err := h.reconstructionState()
116 if errors.Is(err, os.ErrNotExist) {
117 var id [32]byte
118 if _, err := rand.Read(id[:]); err != nil {
119 return HistoryReconstructionStatus{}, err
120 }
121 s = reconstructionState{Schema: 1, Identity: hex.EncodeToString(id[:]), Revision: 1}
122 err = h.writeReconstruction(s)
123 }
124 if err != nil {
125 return HistoryReconstructionStatus{}, err
126 }
127 return HistoryReconstructionStatus{Identity: s.Identity, Revision: s.Revision, Pending: s.Pending != nil, LastReconstructionDigest: s.Last}, nil
128}
129
130func (h *RootHistory) reconstructionReady() error {
131 s, err := h.reconstructionState()
132 if errors.Is(err, os.ErrNotExist) {
133 return nil
134 }
135 if err != nil {
136 return err
137 }
138 if s.Pending != nil {
139 return os.ErrPermission
140 }
141 return nil
142}
143
144func reconstructionSources(g HistoryReconstruction, resolve func(HistoryBinding) (string, error)) error {
145 if resolve == nil || !ValidAccount(g.Account) || !ValidApp(g.App) || len(g.Hosts) == 0 || len(g.Hosts) > historyLimit {
146 return os.ErrPermission
147 }
148 seen := map[string]bool{}
149 for _, host := range g.Hosts {
150 b := HistoryBinding{Host: host.Host, Account: g.Account, App: g.App}
151 if !b.valid() || seen[host.Host] || host.Source == "" {
152 return os.ErrPermission
153 }
154 seen[host.Host] = true
155 source, err := resolve(b)
156 if err != nil {
157 return err
158 }
159 if source != host.Source {
160 return os.ErrPermission
161 }
162 }
163 return nil
164}
165
166func (h *RootHistory) Reconstruct(pub ed25519.PublicKey, wire []byte, acct, app string, now time.Time, resolve func(HistoryBinding) (string, error)) error {
167 if len(wire) > rootstate.MaxRecord || now.IsZero() {
168 return os.ErrPermission
169 }
170 var g HistoryReconstruction
171 if err := VerifyRootEnvelope(pub, wire, &g); err != nil {
172 return err
173 }
174 if g.Schema != 1 || g.Kind != "carlos-release-history" || g.Operation == "" || len(g.Operation) > 256 || g.Account != acct || g.App != app ||
175 g.IssuedAt.Before(now.Add(-HostRetryGrantTTL)) || g.IssuedAt.After(now.Add(hostRetrySkew)) {
176 return os.ErrPermission
177 }
178 if err := reconstructionSources(g, resolve); err != nil {
179 return err
180 }
181 s, err := h.reconstructionState()
182 if err != nil {
183 return err
184 }
185 if s.Identity != g.Identity {
186 return os.ErrPermission
187 }
188 raw, err := json.Marshal(g)
189 if err != nil {
190 return err
191 }
192 digest := reconstructionDigest(raw)
193 if s.Pending != nil && s.Pending.Digest == digest {
194 return h.resumeReconstruction(s, resolve)
195 }
196 if s.Last == digest {
197 return nil
198 }
199 if s.Revision != g.Revision || s.Revision == ^uint64(0) {
200 return os.ErrPermission
201 }
202 effective := g
203 if s.Pending != nil {
204 effective, err = replacementReconstruction(g, s.Pending)
205 if err != nil {
206 return err
207 }
208 } else if g.Replaces != "" {
209 return os.ErrPermission
210 }
211 images, err := h.reconstructionImages(effective, now)
212 if err != nil {
213 return err
214 }
215 if err := reconstructionSources(g, resolve); err != nil {
216 return err
217 }
218 s.Pending = &reconstructionIntent{Grant: g, Digest: digest, Images: images}
219 if err := h.writeReconstruction(s); err != nil {
220 return err
221 }
222 return h.resumeReconstruction(s, resolve)
223}
224
225func (h *RootHistory) ResumeReconstruction(resolve func(HistoryBinding) (string, error)) error {
226 s, err := h.reconstructionState()
227 if errors.Is(err, os.ErrNotExist) {
228 return nil
229 }
230 if err != nil {
231 return err
232 }
233 if s.Pending == nil {
234 return nil
235 }
236 return h.resumeReconstruction(s, resolve)
237}
238
239func (h *RootHistory) resumeReconstruction(s reconstructionState, resolve func(HistoryBinding) (string, error)) error {
240 p := s.Pending
241 if p == nil || s.Revision == ^uint64(0) || len(p.Images) != len(p.Grant.Hosts)+2 || p.Grant.Identity != s.Identity || p.Grant.Revision != s.Revision {
242 return os.ErrPermission
243 }
244 raw, err := json.Marshal(p.Grant)
245 if err != nil || reconstructionDigest(raw) != p.Digest {
246 return os.ErrPermission
247 }
248 if err := reconstructionSources(p.Grant, resolve); err != nil {
249 return err
250 }
251 allowed := map[string]bool{rootBakeKey: true, historyAppKey(p.Grant.Account, p.Grant.App): true}
252 for _, host := range p.Grant.Hosts {
253 allowed[historyKey(HistoryBinding{Host: host.Host, Account: p.Grant.Account, App: p.Grant.App})] = true
254 }
255 for _, im := range p.Images {
256 if !allowed[im.Name] || len(im.After) == 0 {
257 return os.ErrPermission
258 }
259 delete(allowed, im.Name)
260 old, err := h.Store.Read(im.Name)
261 before := "absent"
262 if err == nil {
263 before = reconstructionDigest(old)
264 } else if !errors.Is(err, os.ErrNotExist) {
265 return err
266 }
267 if before != im.Before && before != reconstructionDigest(im.After) {
268 return os.ErrPermission
269 }
270 }
271 for _, im := range p.Images {
272 if err := h.Store.Write(im.Name, im.After); err != nil {
273 return err
274 }
275 }
276 if err := reconstructionSources(p.Grant, resolve); err != nil {
277 return err
278 }
279 s.Revision++
280 s.Last = p.Digest
281 s.Pending = nil
282 return h.writeReconstruction(s)
283}
284
285func (h *RootHistory) reconstructionImages(g HistoryReconstruction, now time.Time) ([]reconstructionImage, error) {
286 var images []reconstructionImage
287 add := func(name string, v any) error {
288 before := "absent"
289 old, err := h.Store.Read(name)
290 if err == nil {
291 before = reconstructionDigest(old)
292 } else if !errors.Is(err, os.ErrNotExist) {
293 return err
294 }
295 raw, err := json.Marshal(v)
296 if err != nil {
297 return err
298 }
299 images = append(images, reconstructionImage{Name: name, Before: before, After: raw})
300 return nil
301 }
302 a := historyApp{Schema: 1, Account: g.Account, App: g.App, Hosts: []string{}}
303 if raw, err := h.Store.Read(historyAppKey(g.Account, g.App)); err == nil {
304 if strictCanonicalJSON(raw, &a) != nil || !validHistoryApp(a) || a.Account != g.Account || a.App != g.App {
305 return nil, os.ErrPermission
306 }
307 } else if !errors.Is(err, os.ErrNotExist) {
308 return nil, err
309 }
310 if a.Revision != g.AppRevision || a.Revision == ^uint64(0) {
311 return nil, os.ErrPermission
312 }
313 names, err := h.Store.Names()
314 if err != nil {
315 return nil, err
316 }
317 for _, name := range names {
318 if name == rootBakeKey || !strings.HasSuffix(name, ".json") {
319 continue
320 }
321 raw, err := h.Store.Read(name)
322 if err != nil {
323 return nil, err
324 }
325 var r historyRecord
326 if strictCanonicalJSON(raw, &r) != nil || !validHistory(r, r.Binding) || historyKey(r.Binding) != name || r.Attempt != nil {
327 return nil, os.ErrPermission
328 }
329 if r.Binding.Account == g.Account && r.Binding.App == g.App && !slices.Contains(a.Hosts, r.Binding.Host) && !slices.ContainsFunc(g.Hosts, func(host HistoryReconstructionHost) bool { return host.Host == r.Binding.Host }) {
330 return nil, os.ErrPermission
331 }
332 }
333 for _, host := range g.Hosts {
334 b := HistoryBinding{Host: host.Host, Account: g.Account, App: g.App}
335 r, err := h.readRecord(b)
336 if errors.Is(err, os.ErrNotExist) {
337 r = historyRecord{Schema: 1, Binding: b}
338 } else if err != nil {
339 return nil, err
340 }
341 if r.Revision != host.Revision || r.Attempt != nil || r.Revision == ^uint64(0) {
342 return nil, os.ErrPermission
343 }
344 r.Revision++
345 if r.Programs.Revision == 0 {
346 r.Programs.Revision = r.Revision
347 }
348 r.Refusals.Revision, r.Refusals.Complete = r.Revision, true
349 for _, ref := range host.Refusals {
350 ref.Recovered = false
351 if !slices.Contains(r.Refusals.Values, ref) {
352 r.Refusals.Values = append(r.Refusals.Values, ref)
353 }
354 }
355 if !validHistory(r, b) {
356 return nil, os.ErrPermission
357 }
358 if err := add(historyKey(b), r); err != nil {
359 return nil, err
360 }
361 if !slices.Contains(a.Hosts, b.Host) {
362 a.Hosts = append(a.Hosts, b.Host)
363 }
364 }
365 a.Revision++
366 a.Complete = true
367 if !validHistoryApp(a) {
368 return nil, os.ErrPermission
369 }
370 if err := add(historyAppKey(g.Account, g.App), a); err != nil {
371 return nil, err
372 }
373 bake := rootBakeRecord{Schema: 1, Complete: map[string]uint64{}, Clock: FirstSeen{Channels: map[string]map[string]time.Time{}}}
374 if raw, err := h.Store.Read(rootBakeKey); err == nil {
375 if strictCanonicalJSON(raw, &bake) != nil || !validRootBake(bake) || now.Before(bake.Observed) {
376 return nil, os.ErrPermission
377 }
378 } else if !errors.Is(err, os.ErrNotExist) {
379 return nil, err
380 }
381 if bake.Revision != g.BakeRevision || bake.Revision == ^uint64(0) || len(g.Holds) > historyLimit {
382 return nil, os.ErrPermission
383 }
384 bake.Revision++
385 bake.Observed = now.UTC()
386 bake.Complete[prodKey(g.Account, g.App)] = bake.Revision
387 for key, versions := range bake.Clock.Channels {
388 if !strings.HasPrefix(key, holdKeyPrefix(g.Account, g.App)) {
389 continue
390 }
391 for version := range versions {
392 versions[version] = now.UTC()
393 }
394 }
395 if bake.Clock.Production == nil {
396 bake.Clock.Production = map[string]*prodMemory{}
397 }
398 if bake.Clock.Holds == nil {
399 bake.Clock.Holds = map[string]*holdMemory{}
400 }
401 pk := prodKey(g.Account, g.App)
402 prod := bake.Clock.Production[pk]
403 if prod == nil {
404 prod = &prodMemory{}
405 bake.Clock.Production[pk] = prod
406 }
407 if !prod.OffFirstSeen.IsZero() {
408 prod.OffFirstSeen = now.UTC()
409 }
410 if g.Production {
411 prod.WasProduction = true
412 prod.OffSetAt, prod.OffFirstSeen = time.Time{}, time.Time{}
413 }
414 if g.ProductionSetAt.After(prod.NewestSetAt) {
415 prod.NewestSetAt = g.ProductionSetAt
416 }
417 for key, mem := range bake.Clock.Holds {
418 if !strings.HasPrefix(key, holdKeyPrefix(g.Account, g.App)) {
419 continue
420 }
421 if !mem.PendingAt.IsZero() {
422 mem.PendingAt = now.UTC()
423 }
424 if !mem.DeletedAt.IsZero() {
425 mem.DeletedAt = now.UTC()
426 }
427 }
428 for channel, bound := range g.Holds {
429 if !ValidChannel(channel) || bound.Window < 0 {
430 return nil, os.ErrPermission
431 }
432 key := channelClockKey(g.Account, g.App, channel)
433 mem := bake.Clock.Holds[key]
434 if mem == nil {
435 mem = &holdMemory{}
436 bake.Clock.Holds[key] = mem
437 }
438 if bound.Window > mem.Window {
439 mem.Window = bound.Window
440 mem.Pending = 0
441 mem.PendingAt = time.Time{}
442 }
443 if bound.SetAt.After(mem.SetAt) {
444 mem.SetAt = bound.SetAt
445 }
446 }
447 if !validRootBake(bake) {
448 return nil, os.ErrPermission
449 }
450 if err := add(rootBakeKey, bake); err != nil {
451 return nil, err
452 }
453 return images, nil
454}
455