carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

1//go:build linux
2
3package host
4
5import (
6 "bytes"
7 "context"
8 "crypto/ed25519"
9 "crypto/sha256"
10 "encoding/hex"
11 "errors"
12 "fmt"
13 "io"
14 "os"
15 "path/filepath"
16 "strings"
17 "sync/atomic"
18
19 "github.com/carlosframework/platform/internal/claimactivation"
20 "github.com/carlosframework/platform/internal/hostfloor"
21 "github.com/carlosframework/platform/internal/release"
22 "github.com/carlosframework/platform/internal/safefile"
23 "golang.org/x/sys/unix"
24)
25
26// activationOutput is a claims phase's --out file, reserved before the phase
27// can write to the bucket (controller ruling F4): an exclusive create, mode
28// 0600, in a checked root-owned directory, held open. A second invocation
29// naming the same path fails its own reservation and publishes nothing. The
30// phase's bytes are then written through the held descriptor, synced and
31// read back. A dry run only checks the path is free.
32//
33// A reservation is never removed (controller ruling F5): on any failure the
34// file stays, empty or partial, and the error names it and asks for a new
35// --out path. Removing it safely would need its identity check and the
36// unlink to be one step, which a directory another root writer can change
37// does not offer.
38type activationOutput struct {
39 dir *os.File
40 ownDir bool
41 name string
42 path string
43 f *os.File
44 dev, ino uint64
45}
46
47// reserveActivationOutput reserves path: absolute and clean, in a directory
48// activationDir accepts (root-owned, not writable by group or others).
49func reserveActivationOutput(path string, dry bool) (*activationOutput, error) {
50 if !filepath.IsAbs(path) || filepath.Clean(path) != path {
51 return nil, errActivation
52 }
53 dir, err := activationDir(filepath.Dir(path), false)
54 if err != nil {
55 return nil, err
56 }
57 o, err := reserveOutput(dir, filepath.Base(path), path, dry)
58 if err != nil {
59 dir.Close()
60 return nil, err
61 }
62 o.ownDir, o.path = true, path
63 return o, nil
64}
65
66// reserveActivationOutputAt reserves name under an already checked
67// directory. Any existing entry is refused without following it.
68func reserveActivationOutputAt(dir *os.File, name string, dry bool) (*activationOutput, error) {
69 return reserveOutput(dir, name, name, dry)
70}
71
72// reserveOutput is the reservation, naming path in its errors.
73func reserveOutput(dir *os.File, name, path string, dry bool) (*activationOutput, error) {
74 if name != filepath.Base(name) || name == "." || name == ".." || name == "/" {
75 return nil, errActivation
76 }
77 var st unix.Stat_t
78 err := unix.Fstatat(int(dir.Fd()), name, &st, unix.AT_SYMLINK_NOFOLLOW)
79 if err == nil {
80 return nil, errActivation
81 }
82 if err != unix.ENOENT {
83 return nil, err
84 }
85 o := &activationOutput{dir: dir, name: name, path: path}
86 if dry {
87 return o, nil
88 }
89 f, err := safefile.OpenFileAt(int(dir.Fd()), name, os.O_CREATE|os.O_EXCL|os.O_RDWR, 0o600)
90 if err != nil {
91 return nil, err
92 }
93 err = f.Chmod(0o600)
94 if err == nil {
95 err = unix.Fstat(int(f.Fd()), &st)
96 }
97 if err == nil && st.Uid != uint32(os.Geteuid()) {
98 err = errActivation
99 }
100 if err == nil {
101 err = unix.Fsync(int(dir.Fd()))
102 }
103 o.f, o.dev, o.ino = f, uint64(st.Dev), st.Ino
104 if err != nil {
105 f.Close()
106 return nil, outputKept(err, path, false)
107 }
108 return o, nil
109}
110
111// ours reports whether the directory entry is still the file this
112// reservation created.
113func (o *activationOutput) ours() bool {
114 var st unix.Stat_t
115 return unix.Fstatat(int(o.dir.Fd()), o.name, &st, unix.AT_SYMLINK_NOFOLLOW) == nil && uint64(st.Dev) == o.dev && st.Ino == o.ino
116}
117
118// finish writes raw through the held descriptor, syncs it and the
119// directory, and reads it back: the exact bytes, mode 0600, one link, and
120// the entry still this file.
121func (o *activationOutput) finish(raw []byte) error {
122 if o.f == nil {
123 return nil
124 }
125 if len(raw) == 0 {
126 return errActivation
127 }
128 if _, err := o.f.WriteAt(raw, 0); err != nil {
129 return err
130 }
131 if err := o.f.Sync(); err != nil {
132 return err
133 }
134 got := make([]byte, len(raw)+1)
135 n, err := o.f.ReadAt(got, 0)
136 if err != nil && !errors.Is(err, io.EOF) {
137 return err
138 }
139 var st unix.Stat_t
140 if err = unix.Fstat(int(o.f.Fd()), &st); err != nil {
141 return err
142 }
143 if n != len(raw) || !bytes.Equal(got[:n], raw) || st.Mode&0o7777 != 0o600 || st.Nlink != 1 || !o.ours() {
144 return errActivation
145 }
146 return unix.Fsync(int(o.dir.Fd()))
147}
148
149// close releases the reservation and keeps whatever the file holds.
150func (o *activationOutput) close() {
151 if o.f != nil {
152 o.f.Close()
153 o.f = nil
154 }
155 if o.ownDir {
156 o.dir.Close()
157 o.ownDir = false
158 }
159}
160
161// putAttempts wraps a phase's store and records whether any write was
162// attempted. A write that failed may still have committed, so an attempt,
163// not a success, is what the error after a failure reports.
164type putAttempts struct {
165 release.Store
166 attempted atomic.Bool
167}
168
169func (s *putAttempts) Put(ctx context.Context, key string, raw []byte, etag string) (string, error) {
170 s.attempted.Store(true)
171 return s.Store.Put(ctx, key, raw, etag)
172}
173
174func (s *putAttempts) Delete(ctx context.Context, key string) error {
175 s.attempted.Store(true)
176 return s.Store.Delete(ctx, key)
177}
178
179// runProducingPhase runs sign-policy or publish-identity with its output
180// already reserved (out), on a runner whose store is store. On success the
181// output holds the phase's file. On any failure the reserved file is kept
182// and the error names it, asks for a new --out path and says whether the
183// phase wrote to the bucket first, because the bucket may then already hold
184// what it published.
185func runProducingPhase(ctx context.Context, r claimactivation.Runner, store *putAttempts, plan claimactivation.Plan, out *activationOutput, dry bool) (claimactivation.Result, error) {
186 r.Directory.Store = store
187 result, err := r.Run(ctx, plan, dry)
188 if err == nil {
189 err = out.finish(result.Output)
190 }
191 if err == nil {
192 out.close()
193 return result, nil
194 }
195 out.close()
196 if dry {
197 return result, err
198 }
199 return result, outputKept(err, out.path, store.attempted.Load())
200}
201
202// outputKept is a failed phase's error once its reserved --out file stays.
203func outputKept(err error, path string, wrote bool) error {
204 if wrote {
205 return fmt.Errorf("%w; the phase wrote to the bucket before it failed, so %s was kept. run it again with a new --out path", err, path)
206 }
207 return fmt.Errorf("%w; %s was kept. run it again with a new --out path", err, path)
208}
209
210// consoleInstallDir is where install-console puts the protected console
211// executable and its .prev: under root's /opt/carlos/system, beside the
212// guard, outside system/router and every restore set.
213const consoleInstallDir = "/opt/carlos/system/console"
214
215// consoleMaxBytes bounds a console artefact, as Disk.VerifyFile bounds what
216// it hashes.
217const consoleMaxBytes = 512 << 20
218
219// consoleInstallTarget checks the staged identity for install-console and
220// returns its release tag and the console sha256 for platform. The staged
221// bytes must verify under the deployment key, be exactly the identity
222// published at control/system/router/releases/<tag>.json, and name this
223// build: guard-arm compares the running build with the identity's version.
224func consoleInstallTarget(identity, published []byte, pub ed25519.PublicKey, build, platform string) (string, string, error) {
225 p, err := release.VerifySystemIdentity(pub, identity)
226 if err != nil || !bytes.Equal(identity, published) || p.Current.Version != build {
227 return "", "", errActivation
228 }
229 a, ok := p.Current.ConsoleArtifacts[platform]
230 if !ok {
231 return "", "", errActivation
232 }
233 return p.Current.ReleaseTag, a.SHA256, nil
234}
235
236// consoleInstalled is what one install-console run did. An upgrade also
237// reports the build it replaced (From) and what console.prev holds after it
238// (PreviousSHA).
239type consoleInstalled struct {
240 Path, Previous string
241 Already bool
242 From, PreviousSHA string
243}
244
245// installProtectedConsole copies the console at source into dir as console
246// and console.prev, for guard-arm's --console. Both copies are the same
247// bytes, the build the identity names: the identity carries one console
248// sha256 per platform and guard-arm checks <path> and <path>.prev against
249// it, so at arming .prev can only equal current. That is install-channel's
250// A2-2 precedent for carlos and carlos.prev (controller ruling C3). A later
251// guarded update can leave an older build in .prev, because guard-exec
252// accepts any identity persisted beside the guard.
253//
254// dir and every ancestor are root-owned, not writable by group or others,
255// and searchable by others (console.service runs guard-exec as the service
256// user, which must open the file); missing components are created 0755 and
257// no component may be a symlink. The source is opened with no symlink
258// followed anywhere on its path and must be a regular file with one link.
259// Its bytes must hash to sha before anything is created. Each slot is
260// written through a private staging directory and renamed into place, mode
261// 0755 and owned by root, .prev first, then read back as guard-arm reads it
262// (hostfloor.OpenProtected) and hashed again. A slot that already holds
263// other bytes, a symlink or anything but a regular file is refused and left
264// as it is; slots that both hold the build are a no-op, after the source is
265// verified again. console.service is
266// not touched: guard-arm installs its drop-in. The dry run writes nothing.
267func installProtectedConsole(dir, source, sha string, dry bool) (consoleInstalled, error) {
268 got := consoleInstalled{Path: filepath.Join(dir, "console"), Previous: filepath.Join(dir, "console.prev")}
269 if len(sha) != 64 || !filepath.IsAbs(source) || filepath.Clean(source) != source {
270 return got, errActivation
271 }
272 if os.Geteuid() != 0 {
273 return got, errActivation
274 }
275 // The source is verified before anything is created or locked.
276 body, err := readConsoleSource(source, sha)
277 if err != nil {
278 return got, err
279 }
280 d, err := consoleDir(dir, !dry)
281 if dry && errors.Is(err, unix.ENOENT) {
282 return got, nil
283 }
284 if err != nil {
285 return got, err
286 }
287 defer d.Close()
288 if !dry {
289 if err = unix.Flock(int(d.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
290 return got, err
291 }
292 }
293 holds := map[string]bool{}
294 for _, name := range []string{"console.prev", "console"} {
295 h, err := consoleSlotHolds(d, name, sha)
296 if err != nil {
297 return got, err
298 }
299 holds[name] = h
300 }
301 if holds["console.prev"] && holds["console"] {
302 got.Already = true
303 return got, nil
304 }
305 if dry {
306 return got, nil
307 }
308 for _, name := range []string{"console.prev", "console"} {
309 if holds[name] {
310 continue
311 }
312 if err = safefile.WriteAt(int(d.Fd()), name, body, 0o755); err != nil {
313 return got, err
314 }
315 }
316 for _, path := range []string{got.Previous, got.Path} {
317 f, err := hostfloor.OpenProtected(path)
318 if err != nil {
319 return got, err
320 }
321 var st unix.Stat_t
322 err = unix.Fstat(int(f.Fd()), &st)
323 var hash string
324 if err == nil {
325 hash, err = hostfloor.DigestReader(io.LimitReader(f, consoleMaxBytes+1))
326 }
327 f.Close()
328 if err != nil {
329 return got, err
330 }
331 if hash != sha || st.Mode&0o7777 != 0o755 || st.Nlink != 1 || st.Uid != 0 || st.Gid != 0 {
332 return got, errActivation
333 }
334 }
335 return got, nil
336}
337
338// consoleSlotHolds reports whether name in d is absent (false) or a root
339// owned regular file, one link, mode 0755, holding sha (true). Anything else
340// is refused.
341func consoleSlotHolds(d *os.File, name, sha string) (bool, error) {
342 hash, err := consoleSlotSHA(d, name)
343 if err != nil || hash == "" {
344 return false, err
345 }
346 if hash != sha {
347 return false, errActivation
348 }
349 return true, nil
350}
351
352// consoleSlotSHA returns "" when name in d is absent, and otherwise the
353// sha256 of a root:root regular file with one link and mode 0755, checked
354// on the entry, not followed, and again on the descriptor it hashes.
355// Anything else is refused.
356func consoleSlotSHA(d *os.File, name string) (string, error) {
357 var st unix.Stat_t
358 err := unix.Fstatat(int(d.Fd()), name, &st, unix.AT_SYMLINK_NOFOLLOW)
359 if err == unix.ENOENT {
360 return "", nil
361 }
362 if err != nil {
363 return "", err
364 }
365 shape := func(st *unix.Stat_t) bool {
366 return st.Mode&unix.S_IFMT == unix.S_IFREG && st.Nlink == 1 && st.Uid == 0 && st.Gid == 0 && st.Mode&0o7777 == 0o755
367 }
368 if !shape(&st) {
369 return "", errActivation
370 }
371 f, err := safefile.OpenFileAt(int(d.Fd()), name, os.O_RDONLY, 0)
372 if err != nil {
373 return "", err
374 }
375 defer f.Close()
376 var held unix.Stat_t
377 if err = unix.Fstat(int(f.Fd()), &held); err != nil {
378 return "", err
379 }
380 if !shape(&held) || held.Dev != st.Dev || held.Ino != st.Ino {
381 return "", errActivation
382 }
383 return consoleDigest(f)
384}
385
386// consoleDigest hashes a console slot through EOF. A file larger than
387// consoleMaxBytes is refused, by its size and by the bytes read, never
388// hashed by its prefix.
389func consoleDigest(f *os.File) (string, error) {
390 var st unix.Stat_t
391 if err := unix.Fstat(int(f.Fd()), &st); err != nil {
392 return "", err
393 }
394 if st.Size < 0 || st.Size > consoleMaxBytes {
395 return "", errActivation
396 }
397 r := &consoleCounter{r: io.LimitReader(f, consoleMaxBytes+1)}
398 hash, err := hostfloor.DigestReader(r)
399 if err != nil {
400 return "", err
401 }
402 if r.n > consoleMaxBytes {
403 return "", errActivation
404 }
405 return hash, nil
406}
407
408// consoleCounter counts the bytes read through it.
409type consoleCounter struct {
410 r io.Reader
411 n int64
412}
413
414func (c *consoleCounter) Read(p []byte) (int, error) {
415 n, err := c.r.Read(p)
416 c.n += int64(n)
417 return n, err
418}
419
420// consoleUpgradeSync fsyncs the console directory at the end of an upgrade
421// and before an Already result. Tests replace it to see that it ran.
422var consoleUpgradeSync = func(fd int) error { return unix.Fsync(fd) }
423
424// consoleUpgradeTemp is the one name an upgrade writes the new console to
425// before renaming it into place. Only an upgrade creates it, under the
426// directory's exclusive lock, and a rerun removes a leftover one.
427const consoleUpgradeTemp = "console.next"
428
429// upgradeProtectedConsole replaces a guarded box's protected console with
430// the build at source, which must hash to sha, for claims install-console
431// --upgrade. The source and root checks are installProtectedConsole's, and
432// the directory must already exist: an upgrade never creates it.
433//
434// Under the directory's exclusive lock, console and console.prev must each
435// be absent or a root:root regular file, mode 0755, one link; anything else
436// refuses. console.prev's bytes are not checked, because they are replaced.
437// - console holding sha: Already, and nothing changes, so a rerun after
438// success is a no-op.
439// - console holding from: the upgrade.
440// - console absent and console.prev holding from: a run cut off between
441// its two renames; this run completes it.
442// - anything else refuses and changes nothing.
443//
444// A source that is, or is inside, the console directory refuses, by its
445// path and by the device and inode of the descriptor it was read from
446// against console, console.prev and console.next: an upgrade must never
447// consume its own source.
448//
449// A non-dry Already fsyncs the directory first, so a rerun after a run cut
450// off before its final fsync makes that run's renames durable.
451//
452// The upgrade writes the build to console.next (exclusive create, 0755,
453// fsync), renames console to console.prev, replacing the old .prev,
454// renames console.next to console, and fsyncs the directory. A console.next
455// left by a run cut off before its renames is removed first: only this
456// function creates that name, under the lock, in a directory only root can
457// write, and its bytes are never used. A console.next that is not a regular
458// file refuses. Both slots are then read back as guard-arm reads them
459// (hostfloor.OpenProtected): console must hash to sha and console.prev to
460// from. The dry run makes every check and writes nothing.
461func upgradeProtectedConsole(dir, source, sha, from string, dry bool) (consoleInstalled, error) {
462 got := consoleInstalled{Path: filepath.Join(dir, "console"), Previous: filepath.Join(dir, "console.prev"), From: from}
463 if len(sha) != 64 || !isLowerHex(from, 64) || !filepath.IsAbs(source) || filepath.Clean(source) != source {
464 return got, errActivation
465 }
466 if source == dir || strings.HasPrefix(source, dir+"/") {
467 return got, errActivation
468 }
469 if os.Geteuid() != 0 {
470 return got, errActivation
471 }
472 body, src, err := readConsoleSourceFile(source, sha)
473 if err != nil {
474 return got, err
475 }
476 d, err := consoleDir(dir, false)
477 if err != nil {
478 return got, err
479 }
480 defer d.Close()
481 if !dry {
482 if err = unix.Flock(int(d.Fd()), unix.LOCK_EX|unix.LOCK_NB); err != nil {
483 return got, err
484 }
485 }
486 for _, name := range []string{"console", "console.prev", consoleUpgradeTemp} {
487 var st unix.Stat_t
488 err = unix.Fstatat(int(d.Fd()), name, &st, unix.AT_SYMLINK_NOFOLLOW)
489 if err == nil && st.Dev == src.Dev && st.Ino == src.Ino {
490 return got, errActivation
491 }
492 if err != nil && err != unix.ENOENT {
493 return got, err
494 }
495 }
496 current, err := consoleSlotSHA(d, "console")
497 if err != nil {
498 return got, err
499 }
500 previous, err := consoleSlotSHA(d, "console.prev")
501 if err != nil {
502 return got, err
503 }
504 if current == sha {
505 got.Already, got.PreviousSHA = true, previous
506 if !dry {
507 if err = consoleUpgradeSync(int(d.Fd())); err != nil {
508 return got, err
509 }
510 }
511 return got, nil
512 }
513 if current != from && (current != "" || previous != from) {
514 return got, errActivation
515 }
516 got.PreviousSHA = from
517 var st unix.Stat_t
518 switch err = unix.Fstatat(int(d.Fd()), consoleUpgradeTemp, &st, unix.AT_SYMLINK_NOFOLLOW); {
519 case err == unix.ENOENT:
520 case err != nil:
521 return got, err
522 case st.Mode&unix.S_IFMT != unix.S_IFREG:
523 return got, errActivation
524 case !dry:
525 if err = unix.Unlinkat(int(d.Fd()), consoleUpgradeTemp, 0); err != nil {
526 return got, err
527 }
528 }
529 if dry {
530 return got, nil
531 }
532 if err = writeConsoleTemp(d, body); err != nil {
533 unix.Unlinkat(int(d.Fd()), consoleUpgradeTemp, 0)
534 return got, err
535 }
536 if current != "" {
537 if err = unix.Renameat(int(d.Fd()), "console", int(d.Fd()), "console.prev"); err != nil {
538 return got, err
539 }
540 }
541 if err = unix.Renameat(int(d.Fd()), consoleUpgradeTemp, int(d.Fd()), "console"); err != nil {
542 return got, err
543 }
544 if err = consoleUpgradeSync(int(d.Fd())); err != nil {
545 return got, err
546 }
547 for path, want := range map[string]string{got.Path: sha, got.Previous: from} {
548 f, err := hostfloor.OpenProtected(path)
549 if err != nil {
550 return got, err
551 }
552 err = unix.Fstat(int(f.Fd()), &st)
553 var hash string
554 if err == nil {
555 hash, err = consoleDigest(f)
556 }
557 f.Close()
558 if err != nil {
559 return got, err
560 }
561 if hash != want || st.Mode&0o7777 != 0o755 || st.Nlink != 1 || st.Uid != 0 || st.Gid != 0 {
562 return got, errActivation
563 }
564 }
565 return got, nil
566}
567
568// writeConsoleTemp creates consoleUpgradeTemp in d exclusively, never
569// following a link, writes body, sets mode 0755 whatever the umask, checks
570// it is root's and fsyncs it.
571func writeConsoleTemp(d *os.File, body []byte) error {
572 f, err := safefile.OpenFileAt(int(d.Fd()), consoleUpgradeTemp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o755)
573 if err != nil {
574 return err
575 }
576 defer f.Close()
577 if _, err = f.Write(body); err != nil {
578 return err
579 }
580 if err = f.Chmod(0o755); err != nil {
581 return err
582 }
583 var st unix.Stat_t
584 if err = unix.Fstat(int(f.Fd()), &st); err != nil {
585 return err
586 }
587 if st.Uid != 0 || st.Gid != 0 || st.Nlink != 1 {
588 return errActivation
589 }
590 if err = f.Sync(); err != nil {
591 return err
592 }
593 return f.Close()
594}
595
596// isLowerHex reports whether s is exactly n lower-case hex digits.
597func isLowerHex(s string, n int) bool {
598 if len(s) != n {
599 return false
600 }
601 for _, c := range s {
602 if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f') {
603 return false
604 }
605 }
606 return true
607}
608
609// readConsoleSource reads the source console, no symlink followed on its
610// path, a regular file with one link, at most consoleMaxBytes, and refuses
611// it unless it hashes to sha.
612func readConsoleSource(source, sha string) ([]byte, error) {
613 body, _, err := readConsoleSourceFile(source, sha)
614 return body, err
615}
616
617// readConsoleSourceFile is readConsoleSource, also returning the stat of
618// the descriptor the bytes were read from, so an upgrade can refuse a
619// source that is one of its own slots.
620func readConsoleSourceFile(source, sha string) ([]byte, unix.Stat_t, error) {
621 var st unix.Stat_t
622 f, err := safefile.OpenFile(source, os.O_RDONLY, 0)
623 if err != nil {
624 return nil, st, err
625 }
626 defer f.Close()
627 if err = unix.Fstat(int(f.Fd()), &st); err != nil {
628 return nil, st, err
629 }
630 body, err := io.ReadAll(io.LimitReader(f, consoleMaxBytes+1))
631 if err != nil {
632 return nil, st, err
633 }
634 sum := sha256.Sum256(body)
635 if len(body) > consoleMaxBytes || hex.EncodeToString(sum[:]) != sha {
636 return nil, st, errActivation
637 }
638 return body, st, nil
639}
640
641// consoleDir opens dir component by component from /, never following a
642// symlink. Every component must be root-owned, not writable by group or
643// others and searchable by others; missing ones are created 0755 when
644// create is set, and the parent is synced. The last must be exactly 0755.
645// The walk is walkNoFollowDir's, shared with the root migration's floor
646// repair.
647func consoleDir(dir string, create bool) (*os.File, error) {
648 if os.Geteuid() != 0 || !filepath.IsAbs(dir) || filepath.Clean(dir) != dir || dir == "/" {
649 return nil, errActivation
650 }
651 check := func(fd int, last bool) error {
652 var st unix.Stat_t
653 if err := unix.Fstat(fd, &st); err != nil {
654 return err
655 }
656 if st.Uid != 0 || st.Mode&0o022 != 0 || st.Mode&0o001 == 0 || last && (st.Mode&0o7777 != 0o755 || st.Gid != 0) {
657 return errActivation
658 }
659 return nil
660 }
661 fd, err := walkNoFollowDir("/", strings.Split(strings.TrimPrefix(dir, "/"), "/"), func(bool) bool { return create }, check)
662 if err != nil {
663 return nil, err
664 }
665 return os.NewFile(uintptr(fd), dir), nil
666}
667
668// runInstallConsole is install-console's work after its flags: the staged
669// identity checked against the published one in store, then the protected
670// copies. A non-empty from is --upgrade --from: the installed console must
671// hash to it, and it is replaced.
672func runInstallConsole(ctx context.Context, store release.Store, pub ed25519.PublicKey, identityPath, source, dir, build, platform, from string, dry bool) (string, string, consoleInstalled, error) {
673 identity, err := activationRead(identityPath, false)
674 if err != nil {
675 return "", "", consoleInstalled{}, err
676 }
677 p, err := release.VerifySystemIdentity(pub, identity)
678 if err != nil {
679 return "", "", consoleInstalled{}, errActivation
680 }
681 key, err := release.IdentityKey(p.Current.ReleaseTag)
682 if err != nil {
683 return "", "", consoleInstalled{}, errActivation
684 }
685 published, _, err := store.Get(ctx, key)
686 if err != nil {
687 return "", "", consoleInstalled{}, err
688 }
689 tag, sha, err := consoleInstallTarget(identity, published, pub, build, platform)
690 if err != nil {
691 return "", "", consoleInstalled{}, err
692 }
693 if from != "" {
694 got, err := upgradeProtectedConsole(dir, source, sha, from, dry)
695 return tag, sha, got, err
696 }
697 got, err := installProtectedConsole(dir, source, sha, dry)
698 return tag, sha, got, err
699}
700