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 "context"
5 "crypto/ed25519"
6 "encoding/base64"
7 "encoding/json"
8 "errors"
9 "fmt"
10 "os"
11 "regexp"
12 "time"
13)
14
15// SystemPointer is the platform's own channel pointer: what a ChannelPointer
16// is for an app, for the thing that does the adopting. It lives under
17// control/, not apps/, because the substrate is not an app (the
18// console-as-an-app spec's dividing line), and it is ROOT-KEY-ONLY: a scoped
19// grant's rungs name app channels, and a key that can roll the platform is a
20// bigger key than any app promoter should hold. Spec §3.
21type SystemPointer struct {
22 ActivationID string `json:"activation_id,omitempty"`
23 CancellationID string `json:"cancellation_id,omitempty"`
24 Schema int `json:"schema"`
25 Component string `json:"component"`
26 Channel string `json:"channel"`
27 Current SystemRelease `json:"current"`
28 History []SystemRelease `json:"history,omitempty"`
29 Spread string `json:"spread"`
30 PromotedAt time.Time `json:"promoted_at"` // of THIS write; the box's replay guard reads only this
31 Audit []Promotion `json:"audit,omitempty"`
32}
33
34// SystemRelease is everything a box needs to install one version. It travels
35// whole into History so a rollback re-installs what was there, not a version
36// string with someone else's artifacts.
37type SystemRelease struct {
38 ConsoleArtifacts map[string]SystemArtifact `json:"console_artifacts,omitempty"`
39 ReleaseTag string `json:"release_tag,omitempty"`
40 SeedsProtocol int `json:"seeds_protocol,omitempty"`
41 MovesProtocol int `json:"moves_protocol,omitempty"`
42 Version string `json:"version"`
43 Artifacts map[string]SystemArtifact `json:"artifacts"` // keyed by "<goos>-<goarch>"
44 Source string `json:"source,omitempty"`
45 ReleasedAt time.Time `json:"released_at"` // audit only; never compared
46}
47
48// SystemArtifact is one mirrored binary. Its sha in the signed pointer IS
49// the artifact's signature: boxes need no .sig object.
50type SystemArtifact struct {
51 Key string `json:"key"`
52 SHA256 string `json:"sha256"`
53}
54
55// SystemComponentRouter is the one component today: the substrate binary
56// (edge + activator + adoption, one process). A split adds a name here.
57const SystemComponentRouter = "router"
58
59// ErrNoSystemHistory is RollbackSystem's answer when the pointer has never
60// moved: there is nothing older to go back to.
61var ErrNoSystemHistory = errors.New("system pointer: nothing to roll back to")
62
63// ErrBadSystemPointer marks every way an object on the wire can fail to be
64// this component and channel's signed pointer: a torn wrapper, a grant, a
65// signature that does not verify, a payload that does not validate. Callers
66// that must tell "this is not authority" from "the bucket is unreachable" —
67// the box's self-update refuses on the first and defers on the second — test
68// with errors.Is instead of reading the message.
69var ErrBadSystemPointer = errors.New("system pointer does not verify")
70
71const (
72 systemSpreadMax = 24 * time.Hour
73 systemSpreadDefault = 5 * time.Minute
74)
75
76var (
77 systemComponentRe = regexp.MustCompile(`^[a-z][a-z0-9-]{0,31}$`)
78 systemPlatformRe = regexp.MustCompile(`^[a-z0-9]+-[a-z0-9]+$`)
79 systemSHA256Re = regexp.MustCompile(`^[0-9a-f]{64}$`)
80)
81
82// SystemPointerKey is control/system/<component>/<channel>.json.
83func SystemPointerKey(component, channel string) string {
84 return "control/system/" + component + "/" + channel + ".json"
85}
86
87// SystemArtifactKey is bootstrap/router/<version>/carlos-<platform>: under
88// bootstrap/ because the store door already means "the platform's own
89// binary" by that name, per-version because canary and stable boxes must be
90// able to run different binaries at once.
91func SystemArtifactKey(version, platform string) string {
92 return "bootstrap/router/" + version + "/carlos-" + platform
93}
94
95// SpreadDuration is Spread parsed, or the default when it is unparseable —
96// validation refuses the unparseable case on the way in, so this only
97// defaults for a hand-edited or pre-field pointer.
98func (p *SystemPointer) SpreadDuration() time.Duration {
99 d, err := time.ParseDuration(p.Spread)
100 if err != nil || d < 0 || d > systemSpreadMax {
101 return systemSpreadDefault
102 }
103 return d
104}
105
106func validateSystemRelease(r SystemRelease) error {
107 if !ValidVersion(r.Version) || r.Version == "dev" {
108 return fmt.Errorf("version %q is not a valid platform version", r.Version)
109 }
110 for platform, artifact := range r.ConsoleArtifacts {
111 if !systemPlatformRe.MatchString(platform) || artifact.Key != "bootstrap/console/"+r.Version+"/carlos-"+platform || !systemSHA256Re.MatchString(artifact.SHA256) {
112 return ErrBadSystemPointer
113 }
114 }
115 if len(r.Artifacts) == 0 {
116 return fmt.Errorf("version %s names no artifacts", r.Version)
117 }
118 for plat, a := range r.Artifacts {
119 if !systemPlatformRe.MatchString(plat) {
120 return fmt.Errorf("platform %q is not <goos>-<goarch>", plat)
121 }
122 if a.Key != SystemArtifactKey(r.Version, plat) {
123 return fmt.Errorf("artifact key %q is not %s", a.Key, SystemArtifactKey(r.Version, plat))
124 }
125 if !systemSHA256Re.MatchString(a.SHA256) {
126 return fmt.Errorf("artifact %s: sha256 %q is not 64 hex characters", a.Key, a.SHA256)
127 }
128 }
129 return nil
130}
131
132func validateSystemPointer(p *SystemPointer, wantComponent, wantChannel string) error {
133 if p.Schema != 1 {
134 return fmt.Errorf("system pointer schema %d unsupported", p.Schema)
135 }
136 if p.Component != wantComponent || p.Channel != wantChannel {
137 return fmt.Errorf("system pointer is for %s/%s, not %s/%s", p.Component, p.Channel, wantComponent, wantChannel)
138 }
139 if !systemComponentRe.MatchString(p.Component) || !ValidChannel(p.Channel) {
140 return fmt.Errorf("system pointer %s/%s: bad component or channel", p.Component, p.Channel)
141 }
142 if p.CancellationID != "" && p.Current.Version == "" {
143 return nil
144 }
145 if err := validateSystemRelease(p.Current); err != nil {
146 return fmt.Errorf("system pointer %s/%s: %w", p.Component, p.Channel, err)
147 }
148 if d, err := time.ParseDuration(p.Spread); err != nil || d < 0 || d > systemSpreadMax {
149 return fmt.Errorf("system pointer %s/%s: spread %q must be a duration between 0 and %s", p.Component, p.Channel, p.Spread, systemSpreadMax)
150 }
151 if p.PromotedAt.IsZero() {
152 return fmt.Errorf("system pointer %s/%s: promoted_at unset", p.Component, p.Channel)
153 }
154 if len(p.History) > historyMax {
155 return fmt.Errorf("system pointer %s/%s: history longer than %d", p.Component, p.Channel, historyMax)
156 }
157 for _, h := range p.History {
158 if err := validateSystemRelease(h); err != nil {
159 return fmt.Errorf("system pointer %s/%s: history: %w", p.Component, p.Channel, err)
160 }
161 }
162 if len(p.Audit) > auditMax {
163 return fmt.Errorf("system pointer %s/%s: audit longer than %d", p.Component, p.Channel, auditMax)
164 }
165 return nil
166}
167
168// signSystemPointer mirrors signPointer with two differences: the signer
169// must be the root key, and the wrapper never carries a grant.
170func signSystemPointer(sg Signer, p *SystemPointer) ([]byte, error) {
171 if err := sg.usable(); err != nil {
172 return nil, err
173 }
174 if sg.scope != nil {
175 return nil, errors.New("system pointer: signed by a scoped key — the platform takes the root key only")
176 }
177 p.Schema = 1
178 p.PromotedAt = time.Now().UTC().Truncate(time.Second)
179 if err := validateSystemPointer(p, p.Component, p.Channel); err != nil {
180 return nil, err
181 }
182 payload, err := json.MarshalIndent(p, "", " ")
183 if err != nil {
184 return nil, err
185 }
186 return signRootPayload(sg, payload)
187}
188
189// verifySystemPointer is the single parse site for system pointers, and so
190// the one place ErrBadSystemPointer is attached: every refusal below reaches
191// the caller wrapped, whatever its own wording.
192func verifySystemPointer(pub ed25519.PublicKey, wrapper []byte, wantComponent, wantChannel string) (*SystemPointer, error) {
193 p, err := parseSystemPointer(pub, wrapper, wantComponent, wantChannel)
194 if err != nil {
195 return nil, fmt.Errorf("%w: %v", ErrBadSystemPointer, err)
196 }
197 return p, nil
198}
199
200// parseSystemPointer does the work. A grant on the wire is refused before
201// the signature is looked at (the production flag's rule, production.go).
202func parseSystemPointer(pub ed25519.PublicKey, wrapper []byte, wantComponent, wantChannel string) (*SystemPointer, error) {
203 var sw signedWrapper
204 if err := json.Unmarshal(wrapper, &sw); err != nil {
205 return nil, fmt.Errorf("system pointer %s/%s: %w", wantComponent, wantChannel, err)
206 }
207 if sw.Grant != nil {
208 return nil, fmt.Errorf("system pointer %s/%s: signed by a scoped key — the platform takes the root key only", wantComponent, wantChannel)
209 }
210 sig, err := base64.StdEncoding.DecodeString(sw.Sig)
211 if err != nil {
212 return nil, fmt.Errorf("system pointer %s/%s: bad signature encoding", wantComponent, wantChannel)
213 }
214 if len(pub) != ed25519.PublicKeySize || !ed25519.Verify(pub, []byte(sw.Payload), sig) {
215 return nil, fmt.Errorf("system pointer %s/%s: signature does not verify", wantComponent, wantChannel)
216 }
217 payload, err := base64.StdEncoding.DecodeString(sw.Payload)
218 if err != nil {
219 return nil, fmt.Errorf("system pointer %s/%s: bad payload encoding", wantComponent, wantChannel)
220 }
221 var p SystemPointer
222 if err := json.Unmarshal(payload, &p); err != nil {
223 return nil, fmt.Errorf("system pointer %s/%s: %w", wantComponent, wantChannel, err)
224 }
225 if err := validateSystemPointer(&p, wantComponent, wantChannel); err != nil {
226 return nil, err
227 }
228 return &p, nil
229}
230
231// GetSystemPointer reads and verifies one channel's pointer. A missing key
232// is os.ErrNotExist, which is the normal state of every deployment before
233// its first `carlos system update`.
234func GetSystemPointer(ctx context.Context, s Store, pub ed25519.PublicKey, component, channel string) (*SystemPointer, string, error) {
235 body, etag, err := s.Get(ctx, SystemPointerKey(component, channel))
236 if err != nil {
237 return nil, "", err
238 }
239 p, err := verifySystemPointer(pub, body, component, channel)
240 if err != nil {
241 return nil, "", err
242 }
243 if p.CancellationID != "" && p.Current.Version == "" {
244 return nil, etag, os.ErrNotExist
245 }
246 return p, etag, nil
247}
248
249// SystemPromoteOptions: who did it, and how long boxes spread their starts.
250type SystemPromoteOptions struct {
251 Actor string
252 Spread time.Duration // 0 = hotfix, every box at once; unset in the CLI means the default
253}
254
255// PromoteSystem writes the pointer for one channel, CAS-guarded. The
256// outgoing release goes whole into History unless it is the same version
257// (a re-promote of the running version is a no-op for boxes and must not
258// push that version into its own history).
259func PromoteSystem(ctx context.Context, s Store, sg Signer, component, channel string, rel SystemRelease, o SystemPromoteOptions) (*SystemPointer, error) {
260 prepared, err := PrepareSystemPromotion(ctx, s, sg, component, channel, rel, o, "")
261 if err != nil {
262 return nil, err
263 }
264 return PublishSystemPublication(ctx, s, sg.verifyPub(), prepared)
265}
266
267func PrepareSystemPromotion(ctx context.Context, s Store, sg Signer, component, channel string, rel SystemRelease, o SystemPromoteOptions, id string) (SystemPublication, error) {
268 key := SystemPointerKey(component, channel)
269 var old *SystemPointer
270 body, etag, err := s.Get(ctx, key)
271 switch {
272 case err == nil:
273 if old, err = verifySystemPointer(sg.verifyPub(), body, component, channel); err != nil {
274 return SystemPublication{}, err
275 }
276 case errors.Is(err, os.ErrNotExist):
277 etag = ""
278 default:
279 return SystemPublication{}, err
280 }
281 if old != nil && old.CancellationID != "" && old.Current.Version == "" {
282 old = nil
283 }
284 next := &SystemPointer{ActivationID: id, Component: component, Channel: channel, Current: rel, Spread: o.Spread.String()}
285 if rel.ReleasedAt.IsZero() {
286 next.Current.ReleasedAt = time.Now().UTC().Truncate(time.Second)
287 }
288 from := ""
289 if old != nil {
290 next.History, next.Audit = old.History, old.Audit
291 from = old.Current.Version
292 if old.Current.Version != rel.Version {
293 next.History = prependCapped(old.Current, old.History, historyMax)
294 }
295 }
296 next.Audit = prependCapped(Promotion{Version: rel.Version, From: from, To: channel, Actor: o.Actor, At: time.Now().UTC()}, next.Audit, auditMax)
297 wrapper, err := signSystemPointer(sg, next)
298 if err != nil {
299 return SystemPublication{}, err
300 }
301 return SystemPublication{Component: component, Channel: channel, ID: id, ExpectedETag: etag, Before: body, After: wrapper}, nil
302}
303
304// RollbackSystem POPS History[0] rather than swapping with it: History[0]
305// becomes Current and is removed from History, and the release that was
306// Current is not pushed back on. So a second rollback goes one release
307// further back, not forward again to where it started — the verb means "go
308// back another one", and repeating it walks the history down.
309//
310// It writes a NEW pointer — new PromotedAt — so every box's replay guard
311// sees a promote, not a replay.
312func RollbackSystem(ctx context.Context, s Store, sg Signer, component, channel, actor string, expectedETag ...string) (*SystemPointer, error) {
313 prepared, err := PrepareSystemRollback(ctx, s, sg, component, channel, actor, "", expectedETag...)
314 if err != nil {
315 return nil, err
316 }
317 return PublishSystemPublication(ctx, s, sg.verifyPub(), prepared)
318}
319
320func PrepareSystemRollback(ctx context.Context, s Store, sg Signer, component, channel, actor, id string, expectedETag ...string) (SystemPublication, error) {
321 key := SystemPointerKey(component, channel)
322 body, etag, err := s.Get(ctx, key)
323 if err != nil {
324 return SystemPublication{}, err
325 }
326 if len(expectedETag) > 1 || (len(expectedETag) == 1 && etag != expectedETag[0]) {
327 return SystemPublication{}, ErrCASConflict
328 }
329 old, err := verifySystemPointer(sg.verifyPub(), body, component, channel)
330 if err != nil {
331 return SystemPublication{}, err
332 }
333 if len(old.History) == 0 {
334 return SystemPublication{}, ErrNoSystemHistory
335 }
336 next := &SystemPointer{ActivationID: id,
337 Component: component, Channel: channel,
338 Current: old.History[0], History: old.History[1:], Spread: old.Spread,
339 Audit: prependCapped(Promotion{Version: old.History[0].Version, From: old.Current.Version, To: channel, Actor: actor, At: time.Now().UTC()}, old.Audit, auditMax),
340 }
341 wrapper, err := signSystemPointer(sg, next)
342 if err != nil {
343 return SystemPublication{}, err
344 }
345 return SystemPublication{Component: component, Channel: channel, ID: id, ExpectedETag: etag, Before: body, After: wrapper}, nil
346}
347