carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1package host
2
3import (
4 "bytes"
5 "context"
6 "crypto/ed25519"
7 "encoding/json"
8 "errors"
9 "flag"
10 "fmt"
11 "os"
12 "path/filepath"
13 "runtime"
14 "strings"
15
16 "github.com/carlosframework/platform/internal/cliflag"
17 "github.com/carlosframework/platform/internal/hostfloor"
18 "github.com/carlosframework/platform/internal/release"
19 "github.com/carlosframework/platform/internal/version"
20)
21
22// carlos system install-channel (region HA spec 2 §5 step 3, A1-6, A2-2).
23//
24// A group box boots from a bootstrap object, which is whatever build the
25// deployment last uploaded there, not its neighbours' release. This puts
26// the release the box's signed channel pointer names in place instead,
27// through the same path `carlos system update` takes: the pointer verified
28// with CARLOS_RELEASE_PUBKEY (verifyPointer), the artifact fetched against
29// the signed sha256 into the agent's cache, then copied into root's
30// staging, checked again and asked its version (stageCandidate).
31//
32// Both protected slots, carlos and carlos.prev, become that artifact
33// (A2-2): guard-arm checks both against the release identity and refuses
34// a box with any other build in either. The release identity is kept
35// beside them (channelIdentityPath) for the join step's guard-arm, and the
36// release must pass the published host floor (hostfloor.CheckImage, the
37// check guard-arm makes) before anything is fetched.
38//
39// It is for a fresh box. A box whose guard is armed, or that already runs
40// another release this way or through an update, is refused: its releases
41// come through `carlos system update`. Run again on a box it has already
42// installed, it changes nothing.
43//
44// --policy FILE (claim trial, prev-rehearsal ruling R1) is for arming a box
45// before the host floor is published. FILE is exactly what `carlos system
46// claims run` sign-policy writes. Its floor is verified as guard-arm
47// verifies it (the root-signed moves-protocol authorization, for this
48// deployment's bucket, under CARLOS_RELEASE_PUBKEY) and stands in for the
49// published floor while there is none; a published floor, or either half
50// of one, must be the policy's own bytes. Every other refusal stands. It
51// also admits the one updated box A2-2 would otherwise leave unarmable:
52// one whose carlos already is the channel release's signed artifact. Its
53// carlos.prev, the release before, is replaced by that artifact
54// (refreshPrev), because guard-arm requires both slots to be the
55// identity's release. Without --policy nothing here changes.
56//
57// --identity FILE (claim trial, untagged-pointer ruling R1) goes with
58// --policy, and only with it. FILE is exactly what `carlos system claims run`
59// publish-identity writes. The channel pointer `carlos system update` writes
60// before a floor is published carries no release tag (the console tags a
61// release only once a floor is published), so with --policy the release's
62// identity is never found through the pointer: --identity must verify under
63// CARLOS_RELEASE_PUBKEY, be byte for byte the object published under its own
64// tag, and name the version and this platform's artifact sha256 the verified
65// pointer names (and its tag, if the pointer carries one). That identity is
66// the one the floor is checked against and the one kept for guard-arm.
67
68const installChannelUsage = "usage: carlos system install-channel [--policy FILE --identity FILE]\n\nInstall the release this box's channel names, as root, on a box that has none yet.\n"
69
70const installChannelPolicyFlag = "the armed policy carlos system claims run sign-policy wrote, used as the host floor until one is published"
71
72const installChannelIdentityFlag = "the release identity carlos system claims run publish-identity wrote, used with --policy in place of the channel's release tag"
73
74const installChannelNotRoot = "carlos system install-channel runs as root: it installs the platform binary"
75
76// channelIdentityPath is where the installed release's signed identity is
77// kept for guard-arm (§5 step 5.3).
78func channelIdentityPath(optDir string) string {
79 return filepath.Join(systemRootDir(optDir), "channel-identity.json")
80}
81
82// channelInstall is one run. fetchTo is release.FetchToFile over the
83// pointer's store, and own hands a directory tree to the service user
84// (ownByService); both are seams for tests.
85type channelInstall struct {
86 a *systemApplier
87 bucket string
88 fetchTo func(ctx context.Context, s release.Store, key, dst, sha string) error
89 own func(path string) error
90 // policy is --policy's file, byte for byte; nil without the flag.
91 policy []byte
92 // identity is --identity's file, byte for byte; set with policy and
93 // only with it.
94 identity []byte
95}
96
97// slotKind is what one protected slot holds (slotState).
98type slotKind int
99
100const (
101 slotAbsent slotKind = iota // nothing there
102 slotHolds // the signed artifact
103 slotForeign // a symlink, a non-regular file or any other build
104)
105
106// channelInstalled is what a run did.
107type channelInstalled struct {
108 Version, Tag, Channel string
109 Already bool
110 // Replaced: --policy replaced an updated box's carlos.prev (refreshPrev).
111 Replaced bool
112}
113
114// RunSystemInstallChannel is `carlos system install-channel`.
115func RunSystemInstallChannel(args []string) {
116 fs := flag.NewFlagSet("system install-channel", flag.ExitOnError)
117 policyPath := fs.String("policy", "", installChannelPolicyFlag)
118 identityPath := fs.String("identity", "", installChannelIdentityFlag)
119 fs.Usage = cliflag.Usage(fs, installChannelUsage)
120 _ = fs.Parse(args)
121 if fs.NArg() != 0 {
122 fs.Usage()
123 os.Exit(2)
124 }
125 if (*policyPath == "") != (*identityPath == "") {
126 fmt.Fprintln(os.Stderr, errChannelPolicyIdentity)
127 os.Exit(2)
128 }
129 if os.Geteuid() != 0 {
130 fmt.Fprintln(os.Stderr, installChannelNotRoot)
131 os.Exit(1)
132 }
133 channel, invalid := systemChannel(os.Getenv)
134 if invalid != "" {
135 fmt.Fprintln(os.Stderr, fmt.Sprintf("%s=%q is not a channel, so nothing was installed", systemChannelEnv, invalid))
136 os.Exit(1)
137 }
138 policy, identity, err := readChannelInputs(*policyPath, *identityPath)
139 if err != nil {
140 fmt.Fprintln(os.Stderr, err)
141 os.Exit(1)
142 }
143 in := newChannelInstall(channel, policy, identity)
144 got, err := in.run(context.Background())
145 if err != nil {
146 fmt.Fprintln(os.Stderr, err)
147 os.Exit(1)
148 }
149 fmt.Println(installedLine(got))
150}
151
152// channelPolicyLimit caps --policy's file: sign-policy writes about 500
153// bytes.
154const channelPolicyLimit = 64 << 10
155
156// errChannelPolicy is --policy's one refusal of the policy itself, said
157// alike before and after its signature is checked.
158var errChannelPolicy = errors.New("the --policy file is not an armed policy signed for this deployment, so nothing was installed")
159
160// errChannelPolicyIdentity refuses --policy without --identity, or
161// --identity without --policy.
162var errChannelPolicyIdentity = errors.New("--policy and --identity go together, so nothing was installed")
163
164// errChannelIdentity is --identity's refusal of a file that is not a release
165// identity signed under the box's release key.
166var errChannelIdentity = errors.New("the --identity file is not a release identity signed for this deployment, so nothing was installed")
167
168// newChannelInstall is the production run. The --policy path refreshes
169// carlos.prev as an update does, so its applier reads the root
170// participation census as the update's does (systemapply.go): on a census
171// box it takes the root moves floor branch, where a floor step that fails
172// fails (fix round 1, I2). Without --policy the applier is as it was.
173func newChannelInstall(channel string, policy, identity []byte) channelInstall {
174 in := channelInstall{
175 a: &systemApplier{ops: productionSysOps(), openStore: openSystemStore, channel: channel, optDir: release.OptDir(),
176 dataDir: dataDir(), etcDir: "/etc", build: version.Build, platform: runtime.GOOS + "-" + runtime.GOARCH},
177 bucket: os.Getenv("CARLOS_DEPLOYMENT_BUCKET"),
178 fetchTo: func(ctx context.Context, s release.Store, key, dst, sha string) error {
179 return release.FetchToFile(ctx, s, key, dst, systemArtifactLimit, sha)
180 },
181 own: ownByService,
182 policy: policy,
183 identity: identity,
184 }
185 if policy != nil {
186 in.a.rootMoves = rootMovesFloorIdentity
187 }
188 return in
189}
190
191// readChannelInputs reads --policy's and --identity's files, before the lock
192// or any store read: both or neither (errChannelPolicyIdentity), the policy
193// first (readChannelPolicy), then the identity through the same hardened
194// open (openChannelPolicy). The identity is verified once the release key is
195// known (channelIdentity). With neither flag nothing is read.
196func readChannelInputs(policyPath, identityPath string) (policy, identity []byte, err error) {
197 if (policyPath == "") != (identityPath == "") {
198 return nil, nil, errChannelPolicyIdentity
199 }
200 if policyPath == "" {
201 return nil, nil, nil
202 }
203 if policy, err = readChannelPolicy(policyPath); err != nil {
204 return nil, nil, err
205 }
206 if identity, err = openChannelPolicy(identityPath, channelPolicyLimit); err != nil {
207 return nil, nil, fmt.Errorf("the identity %s could not be read, so nothing was installed: %w", identityPath, err)
208 }
209 return policy, identity, nil
210}
211
212// readChannelPolicy reads --policy's file and refuses, before anything else
213// is read, a file that is not an armed policy at all. It is opened without
214// following a symlink or waiting on a FIFO, and must be a regular file of
215// at most channelPolicyLimit bytes (openChannelPolicy). Its signature is
216// checked once the release key is known (channelPolicy).
217func readChannelPolicy(path string) ([]byte, error) {
218 raw, err := openChannelPolicy(path, channelPolicyLimit)
219 if err != nil {
220 return nil, fmt.Errorf("the policy %s could not be read, so nothing was installed: %w", path, err)
221 }
222 var p hostfloor.Policy
223 if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" {
224 return nil, errChannelPolicy
225 }
226 return raw, nil
227}
228
229// channelPolicy is guard-arm's check of --policy (guardArmInputs, first
230// half): an armed policy whose floor carries the root-signed
231// moves-protocol authorization for this deployment's bucket.
232func channelPolicy(raw []byte, pub ed25519.PublicKey, bucket string) (hostfloor.Policy, hostfloor.Verified, error) {
233 var p hostfloor.Policy
234 if json.Unmarshal(raw, &p) != nil || p.Mode != "armed" {
235 return hostfloor.Policy{}, hostfloor.Verified{}, hostfloor.ErrFloor
236 }
237 f, err := p.Verify(pub, bucket)
238 if err != nil {
239 return hostfloor.Policy{}, hostfloor.Verified{}, err
240 }
241 return p, f, nil
242}
243
244func installedLine(got channelInstalled) string {
245 if got.Replaced {
246 return fmt.Sprintf("replaced the rollback copy carlos.prev with %s (%s) from the %s channel, the release carlos already runs. the release carlos.prev held before is no longer on this box", got.Version, got.Tag, got.Channel)
247 }
248 if got.Already {
249 return fmt.Sprintf("%s (%s) from the %s channel is already installed", got.Version, got.Tag, got.Channel)
250 }
251 return fmt.Sprintf("installed %s (%s) from the %s channel as carlos and carlos.prev", got.Version, got.Tag, got.Channel)
252}
253
254// run takes the apply lock, so no update runs beside it, and installs.
255func (in channelInstall) run(ctx context.Context) (channelInstalled, error) {
256 a := in.a
257 if err := os.MkdirAll(a.rootDir(), 0o755); err != nil {
258 return channelInstalled{}, err
259 }
260 lock, err := os.OpenFile(a.lockPath(), os.O_CREATE|os.O_RDWR, 0o600)
261 if err != nil {
262 return channelInstalled{}, err
263 }
264 defer lock.Close()
265 if err := flockExclusiveNonblock(lock); err != nil {
266 return channelInstalled{}, fmt.Errorf("an update is running on this box, so nothing was installed: %w", err)
267 }
268 return in.install(ctx)
269}
270
271func (in channelInstall) install(ctx context.Context) (channelInstalled, error) {
272 a := in.a
273 var pub ed25519.PublicKey
274 open := a.openStore
275 a.openStore = func(ctx context.Context) (release.Store, ed25519.PublicKey, error) {
276 s, k, err := open(ctx)
277 pub = k
278 return s, k, err
279 }
280 p, err := a.verifyPointer(ctx)
281 if err != nil {
282 return channelInstalled{}, fmt.Errorf("the %s channel could not be read, so nothing was installed: %w", a.channel, err)
283 }
284 got := channelInstalled{Version: p.Current.Version, Tag: p.Current.ReleaseTag, Channel: a.channel}
285 var policy *hostfloor.Policy
286 var policyFloor hostfloor.Verified
287 if in.policy != nil {
288 if in.identity == nil {
289 return got, errChannelPolicyIdentity
290 }
291 pol, f, err := channelPolicy(in.policy, pub, in.bucket)
292 if err != nil {
293 return got, errChannelPolicy
294 }
295 policy, policyFloor = &pol, f
296 }
297 art, ok := p.Current.Artifacts[a.platform]
298 if !ok {
299 return got, fmt.Errorf("the %s channel names %s with no artifact for %s, so nothing was installed", a.channel, p.Current.Version, a.platform)
300 }
301 // A box whose guard is armed takes releases only through an update.
302 guard := hostfloor.Disk{Dir: systemGuardDir(a.optDir), Bucket: in.bucket, PublicKey: pub}
303 if policy, _, err := guard.Load(); err == nil && policy.Mode != "unpublished" {
304 return got, errors.New("this box's host lifecycle guard is armed, so carlos system update installs its releases")
305 } else if err != nil && !errors.Is(err, os.ErrNotExist) {
306 return got, fmt.Errorf("this box's host lifecycle guard could not be read, so nothing was installed: %w", err)
307 }
308 applied, err := readSystemApplied(a.optDir)
309 if err != nil {
310 return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
311 }
312 if applied != nil && applied.Version != p.Current.Version {
313 return got, fmt.Errorf("this box already runs %s, so carlos system update installs its releases", applied.Version)
314 }
315 wire, tag, err := in.checkFloor(ctx, p, art, pub, policy, policyFloor)
316 if err != nil {
317 return got, err
318 }
319 // The tag is the identity's: with --policy it is --identity's, which an
320 // untagged pointer cannot name.
321 got.Tag = tag
322 // With --policy, a box whose update has not finished (an open journal,
323 // or an update-begun marker beside the guard) is refused, as guard-arm
324 // refuses it: recovery owns carlos.prev until then (fix round 1, I1).
325 if policy != nil {
326 if err := guardArmIdle(systemGuardDir(a.optDir), systemRootDir(a.optDir)); err != nil {
327 // The reason is the journal or marker guard-arm names, without
328 // guard-arm's floor prefix: it is an unfinished update.
329 if errors.Is(err, hostfloor.ErrFloor) {
330 err = errors.New(strings.TrimPrefix(err.Error(), hostfloor.ErrFloor.Error()+": "))
331 }
332 return got, fmt.Errorf("an update on this box has not finished, so nothing was installed: %w", err)
333 }
334 }
335 already := systemCacheHolds(systemBinaryPath(a.optDir), art.SHA256) && systemCacheHolds(systemPrevPath(a.optDir), art.SHA256) &&
336 systemCacheHolds(channelIdentityPath(a.optDir), hostfloor.Digest(wire))
337 if applied != nil {
338 // applied.json is the last thing a run writes, so a box with one is
339 // either this install, complete, or a box an update has since
340 // managed: its releases come through carlos system update.
341 if already {
342 // With --policy, "already installed" also needs carlos.prev's
343 // version record to name this release, and its judgement to have
344 // run. A record a cut-off run left missing or stale is completed
345 // first (which judges); otherwise the judgement runs again, as it
346 // is idempotent, so one a run left unfinished is finished. Either
347 // failing fails the run (fix round 2).
348 if policy != nil {
349 var err error
350 if !prevRecorded(a.optDir, p.Current.Version) {
351 err = in.completePrev(ctx, p.Current.Version, false)
352 } else {
353 err = channelPrevJudge(a, ctx)
354 }
355 if err != nil {
356 return got, err
357 }
358 }
359 got.Already = true
360 return got, nil
361 }
362 if policy != nil {
363 return in.refreshPrev(ctx, p, art, wire, got)
364 }
365 return got, fmt.Errorf("this box already runs %s, so carlos system update installs its releases", applied.Version)
366 }
367 // With no applied.json, a slot that holds anything but the signed
368 // artifact is a build this command did not install, and is refused and
369 // left as it is. A run cut off part way leaves only the artifact behind
370 // (carlos.prev is written first), so a retry passes. Each slot is opened
371 // without following a symlink and hashed from the descriptor, against the
372 // verified pointer's sha256; nothing is executed to learn its version.
373 for _, slot := range []string{systemBinaryPath(a.optDir), systemPrevPath(a.optDir)} {
374 state, err := slotState(slot, art.SHA256)
375 if err != nil {
376 return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
377 }
378 if state == slotForeign {
379 return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", slot)
380 }
381 }
382 cache := systemCachePath(a.dataDir, p.Current.Version)
383 defer os.RemoveAll(a.stagingDir())
384 if err := in.cacheDir(p.Current.Version); err != nil {
385 return got, err
386 }
387 // Only now is the version directory ours to remove, and it is removed
388 // through the held router directory, never by a path that could pass a
389 // symlink (the service user owns the parents).
390 defer func() { _ = removeCacheEntry(a.dataDir, p.Current.Version) }()
391 if err := in.fetchTo(ctx, a.store, art.Key, cache, art.SHA256); err != nil {
392 return got, fmt.Errorf("%s could not be fetched, so nothing was installed: %w", p.Current.Version, err)
393 }
394 staged, err := a.stageCandidate(ctx, p, systemRequest{Version: p.Current.Version, PromotedAt: p.PromotedAt})
395 if err != nil {
396 return got, fmt.Errorf("%s did not verify, so nothing was installed: %w", p.Current.Version, err)
397 }
398 // carlos.prev first: a run cut off between the two copies leaves the
399 // new build in .prev and none in the current slot, which the next run
400 // replaces, never the other way round.
401 for _, slot := range []string{systemPrevPath(a.optDir), systemBinaryPath(a.optDir)} {
402 if err := copyFileDurable(staged, slot, 0o755); err != nil {
403 return got, err
404 }
405 if !systemCacheHolds(slot, art.SHA256) {
406 return got, fmt.Errorf("%s does not hold %s after the copy", slot, p.Current.Version)
407 }
408 }
409 // With --policy, carlos.prev's version record is part of the install
410 // (completePrev); without it the install is exactly as it was.
411 if policy != nil {
412 if err := in.completePrev(ctx, p.Current.Version, false); err != nil {
413 return got, err
414 }
415 }
416 if err := hostfloor.Durable(channelIdentityPath(a.optDir), wire, 0o644); err != nil {
417 return got, err
418 }
419 seen, err := readSystemSeen(a.optDir)
420 if err != nil {
421 return got, err
422 }
423 seen[a.channel] = p.PromotedAt
424 if err := writeJSONDurable(a.file("seen.json"), seen, 0o644); err != nil {
425 return got, err
426 }
427 if err := writeJSONDurable(a.file("applied.json"), systemApplied{Version: p.Current.Version, Channel: a.channel, At: a.ops.now().UTC()}, 0o644); err != nil {
428 return got, err
429 }
430 return got, nil
431}
432
433// checkFloor reads the published host floor and the release's signed
434// identity, and returns the identity once the release passes the floor
435// exactly as guard-arm will check it. No floor, or one that cannot be read,
436// refuses: a joining box could not arm its guard without one, and a read
437// that failed says nothing about where the floor is. With --policy
438// (policy non-nil) the policy's verified floor is the floor instead
439// (policyFloorPublished), and the identity is --identity's, never one found
440// through the pointer's tag (channelIdentity). The tag returned is the
441// identity's.
442func (in channelInstall) checkFloor(ctx context.Context, p *release.SystemPointer, art release.SystemArtifact, pub ed25519.PublicKey, policy *hostfloor.Policy, policyFloor hostfloor.Verified) ([]byte, string, error) {
443 s := in.a.store
444 var floor hostfloor.Verified
445 var wire []byte
446 tag := p.Current.ReleaseTag
447 if policy != nil {
448 if err := policyFloorPublished(ctx, s, *policy); err != nil {
449 return nil, "", err
450 }
451 floor = policyFloor
452 var err error
453 if wire, tag, err = in.channelIdentity(ctx, s, p, art, pub); err != nil {
454 return nil, "", err
455 }
456 } else {
457 raw, _, err := s.Get(ctx, hostfloor.Key)
458 if errors.Is(err, os.ErrNotExist) {
459 return nil, "", errors.New("no host floor is published, so this box could not arm its guard and nothing was installed")
460 }
461 if err != nil {
462 return nil, "", fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err)
463 }
464 auth, _, err := s.Get(ctx, hostfloor.AuthKey)
465 if err != nil {
466 return nil, "", fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err)
467 }
468 floor, err = hostfloor.Verify(pub, in.bucket, raw, auth)
469 if err != nil {
470 return nil, "", fmt.Errorf("the host floor does not verify, so nothing was installed: %w", err)
471 }
472 key, err := release.IdentityKey(p.Current.ReleaseTag)
473 if err != nil {
474 return nil, "", fmt.Errorf("the %s channel names %s with no release tag, so nothing was installed", in.a.channel, p.Current.Version)
475 }
476 wire, _, err = s.Get(ctx, key)
477 if err != nil {
478 return nil, "", fmt.Errorf("the release identity for %s could not be read, so nothing was installed: %w", p.Current.ReleaseTag, err)
479 }
480 }
481 if err := hostfloor.CheckImage(pub, floor, wire, p.Current.Version, in.a.platform, art.SHA256); err != nil {
482 return nil, "", fmt.Errorf("%s (%s) is below the host floor %s or is not the release its identity names, so nothing was installed", p.Current.Version, tag, floor.Floor.MinRelease)
483 }
484 return wire, tag, nil
485}
486
487// channelIdentity is --identity's check (untagged-pointer ruling R1), once
488// the release key is known: the file verifies as a release identity under
489// it, is byte for byte the object published at release.IdentityKey of its
490// own tag (none published under it is its own refusal), and names what the
491// verified pointer names: the same version and
492// the same sha256 for this platform's artifact, and, when the pointer
493// carries a release tag, that tag. It returns the identity's bytes and tag.
494// Nothing is read through the pointer's tag.
495func (in channelInstall) channelIdentity(ctx context.Context, s release.Store, p *release.SystemPointer, art release.SystemArtifact, pub ed25519.PublicKey) ([]byte, string, error) {
496 id, err := release.VerifySystemIdentity(pub, in.identity)
497 if err != nil {
498 return nil, "", errChannelIdentity
499 }
500 tag := id.Current.ReleaseTag
501 key, err := release.IdentityKey(tag)
502 if err != nil {
503 return nil, "", errChannelIdentity
504 }
505 published, _, err := s.Get(ctx, key)
506 if err != nil && !errors.Is(err, os.ErrNotExist) {
507 return nil, "", fmt.Errorf("the release identity for %s could not be read, so nothing was installed: %w", tag, err)
508 }
509 if err != nil {
510 return nil, "", fmt.Errorf("no release identity is published for %s, so nothing was installed", tag)
511 }
512 if !bytes.Equal(published, in.identity) {
513 return nil, "", fmt.Errorf("the --identity file is not the identity published for %s, so nothing was installed", tag)
514 }
515 router, ok := id.Current.Artifacts[in.a.platform]
516 if !ok || router.SHA256 != art.SHA256 || id.Current.Version != p.Current.Version || p.Current.ReleaseTag != "" && p.Current.ReleaseTag != tag {
517 return nil, "", fmt.Errorf("the --identity file names %s (%s), not the release the %s channel names, so nothing was installed", id.Current.Version, tag, in.a.channel)
518 }
519 return in.identity, tag, nil
520}
521
522// policyFloorPublished checks --policy against what the bucket holds: no
523// floor published, or a published pair (or the half of one that a torn
524// publication leaves; hostfloor.Publish writes the authorization first)
525// that is the policy's floor and authorization byte for byte. Anything
526// else, a read that fails included, refuses.
527func policyFloorPublished(ctx context.Context, s release.Store, policy hostfloor.Policy) error {
528 for _, half := range []struct {
529 key string
530 want []byte
531 }{{hostfloor.AuthKey, policy.Authorization}, {hostfloor.Key, policy.Floor}} {
532 got, _, err := s.Get(ctx, half.key)
533 if errors.Is(err, os.ErrNotExist) {
534 continue
535 }
536 if err != nil {
537 return fmt.Errorf("the host floor could not be read, so nothing was installed: %w", err)
538 }
539 if !bytes.Equal(got, half.want) {
540 return errors.New("the published host floor is not the floor in the --policy file, so nothing was installed")
541 }
542 }
543 return nil
544}
545
546// channelPrevVersionWrite is writePrevVersion, a seam for the tests that
547// the record's write failing fails the --policy install.
548var channelPrevVersionWrite = writePrevVersion
549
550// prevRecorded reports whether carlos.prev's version record names version.
551func prevRecorded(optDir, version string) bool {
552 v, ok := readLine(prevVersionPath(optDir))
553 return ok && v == version
554}
555
556// completePrev is --policy's refresh of carlos.prev's records, the steps
557// an update's notePrev takes but with every one mandatory, armed floor or
558// not (prev-rehearsal fix round 1): under the prev lock, the verdicts and
559// the version record of what carlos.prev held are removed, carlos is
560// copied over it when copyCurrent is set, and the version record is
561// written and read back. Any failure fails the run, which then keeps no
562// identity and reports nothing done; a retry completes it. The floor's
563// judgement follows as an update makes it (notePrev without a replace):
564// a judgement that cannot be made leaves no verdict, which refuses.
565func (in channelInstall) completePrev(ctx context.Context, version string, copyCurrent bool) error {
566 a := in.a
567 err := withPrevLock(a.optDir, func() error {
568 for _, path := range []string{prevAllowedPath(a.optDir), seedsPrevAllowedPath(a.optDir), prevVersionPath(a.optDir)} {
569 if err := removeDurable(path); err != nil {
570 return err
571 }
572 }
573 if copyCurrent {
574 if err := copyFileDurable(systemBinaryPath(a.optDir), systemPrevPath(a.optDir), 0o755); err != nil {
575 return err
576 }
577 }
578 return channelPrevVersionWrite(a.optDir, version)
579 })
580 if err == nil && !prevRecorded(a.optDir, version) {
581 err = fmt.Errorf("%s does not name %s", prevVersionPath(a.optDir), version)
582 }
583 if err != nil {
584 return fmt.Errorf("the record of the release in carlos.prev could not be written, so the install is not complete. run it again: %w", err)
585 }
586 return channelPrevJudge(a, ctx)
587}
588
589// channelPrevJudge is the floor's judgement of carlos.prev, as an update
590// makes it (notePrev without a replace); a seam for the tests of a
591// judgement that fails or is cut off.
592var channelPrevJudge = func(a *systemApplier, ctx context.Context) error { return a.notePrev(ctx, false, "") }
593
594// refreshPrev is --policy's one admission of an updated box (applied.json
595// names the channel release, and the slots and the identity are not yet
596// install-channel's): carlos must already hold the channel release's
597// signed artifact, hashed from a descriptor opened with O_NOFOLLOW
598// (slotState). Its carlos.prev, the release the update replaced, is then
599// refreshed from carlos the way an update refreshes it (notePrev: the
600// sidecar and verdicts of the old carlos.prev removed first, the copy, the
601// version sidecar, the floor's judgement), every step mandatory here
602// (completePrev), and only then is the identity kept beside
603// them. Nothing is fetched, and applied.json and seen.json are the
604// update's and stay as they are. The caller has already refused an armed
605// guard, another release and a release below the policy's floor.
606//
607// Why A2-2 allows it: A2-2 puts the identity's artifact in both slots so
608// guard-arm's two-slot rule can pass, and on a fresh box carlos.prev is
609// then a copy of carlos. Here carlos already is that artifact, so this
610// reaches A2-2's end state exactly. What is lost is the rollback to the
611// release before, which is below the floor whenever the floor is the
612// release being armed, and which the armed guard would refuse to run.
613func (in channelInstall) refreshPrev(ctx context.Context, p *release.SystemPointer, art release.SystemArtifact, wire []byte, got channelInstalled) (channelInstalled, error) {
614 a := in.a
615 current, prev := systemBinaryPath(a.optDir), systemPrevPath(a.optDir)
616 state, err := slotState(current, art.SHA256)
617 if err != nil {
618 return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
619 }
620 if state != slotHolds {
621 return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", current)
622 }
623 held, err := slotState(prev, art.SHA256)
624 if err != nil {
625 return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
626 }
627 if held == slotForeign {
628 // The release before is replaced; a symlink or anything but a
629 // regular file is not what an update leaves, and is refused.
630 if fi, err := os.Lstat(prev); err != nil {
631 return got, fmt.Errorf("this box's install record could not be read, so nothing was installed: %w", err)
632 } else if !fi.Mode().IsRegular() {
633 return got, fmt.Errorf("%s holds a build this command did not install, so nothing was installed", prev)
634 }
635 }
636 // Always, even over a carlos.prev a cut-off run already copied, so the
637 // version record is this release's. The record is mandatory here.
638 if err := in.completePrev(ctx, p.Current.Version, true); err != nil {
639 return got, err
640 }
641 // The replaced line names a release that was there; a retry that found
642 // carlos.prev already this release, or none, says only what is installed.
643 replaced := held == slotForeign
644 for _, slot := range []string{prev, current} {
645 if state, err := slotState(slot, art.SHA256); err != nil || state != slotHolds {
646 return got, fmt.Errorf("%s does not hold %s after the copy", slot, p.Current.Version)
647 }
648 }
649 if err := hostfloor.Durable(channelIdentityPath(a.optDir), wire, 0o644); err != nil {
650 return got, err
651 }
652 got.Replaced = replaced
653 return got, nil
654}
655
656// cacheDir makes the agent's cache entry for version and reconciles the
657// ownership of every directory on its path below the data dir, on every
658// run: system, system/router and system/router/<version> are created as
659// bootstrap creates directories (bootstrapMakeDir: each component opened
660// with no symlink followed, so a symlinked parent is refused) and each is
661// handed to the service user, as bootstrap's chown of /data does
662// (privilegedPlan), whether this run created it or an earlier run that
663// failed before its handoff did. Nothing else is chowned. The agent
664// (User=carlos) later downloads releases beside the entry and writes its
665// update requests there. Only the entry itself is removed after the
666// install; its parents stay, owned by the agent.
667func (in channelInstall) cacheDir(version string) error {
668 parts := systemCacheParts(version)
669 dir := in.a.dataDir
670 for _, part := range parts[:len(parts)-1] {
671 dir = filepath.Join(dir, part)
672 if err := bootstrapMakeDir(dir, 0o755); err != nil {
673 return err
674 }
675 if err := in.own(dir); err != nil {
676 return err
677 }
678 }
679 return nil
680}
681