github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/cmd/snap-bootstrap/cmd_initramfs_mounts.go (about) 1 // -*- Mode: Go; indent-tabs-mode: t -*- 2 3 /* 4 * Copyright (C) 2019-2020 Canonical Ltd 5 * 6 * This program is free software: you can redistribute it and/or modify 7 * it under the terms of the GNU General Public License version 3 as 8 * published by the Free Software Foundation. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License 16 * along with this program. If not, see <http://www.gnu.org/licenses/>. 17 * 18 */ 19 20 package main 21 22 import ( 23 "crypto/subtle" 24 "encoding/json" 25 "fmt" 26 "io/ioutil" 27 "os" 28 "path/filepath" 29 "strings" 30 "syscall" 31 32 "github.com/jessevdk/go-flags" 33 34 "github.com/snapcore/snapd/asserts" 35 "github.com/snapcore/snapd/boot" 36 "github.com/snapcore/snapd/dirs" 37 "github.com/snapcore/snapd/logger" 38 "github.com/snapcore/snapd/osutil" 39 "github.com/snapcore/snapd/osutil/disks" 40 "github.com/snapcore/snapd/overlord/state" 41 "github.com/snapcore/snapd/secboot" 42 "github.com/snapcore/snapd/snap" 43 "github.com/snapcore/snapd/snap/squashfs" 44 "github.com/snapcore/snapd/sysconfig" 45 46 // to set sysconfig.ApplyFilesystemOnlyDefaultsImpl 47 _ "github.com/snapcore/snapd/overlord/configstate/configcore" 48 ) 49 50 func init() { 51 const ( 52 short = "Generate mounts for the initramfs" 53 long = "Generate and perform all mounts for the initramfs before transitioning to userspace" 54 ) 55 56 addCommandBuilder(func(parser *flags.Parser) { 57 if _, err := parser.AddCommand("initramfs-mounts", short, long, &cmdInitramfsMounts{}); err != nil { 58 panic(err) 59 } 60 }) 61 62 snap.SanitizePlugsSlots = func(*snap.Info) {} 63 } 64 65 type cmdInitramfsMounts struct{} 66 67 func (c *cmdInitramfsMounts) Execute(args []string) error { 68 return generateInitramfsMounts() 69 } 70 71 var ( 72 osutilIsMounted = osutil.IsMounted 73 74 snapTypeToMountDir = map[snap.Type]string{ 75 snap.TypeBase: "base", 76 snap.TypeKernel: "kernel", 77 snap.TypeSnapd: "snapd", 78 } 79 80 secbootMeasureSnapSystemEpochWhenPossible func() error 81 secbootMeasureSnapModelWhenPossible func(findModel func() (*asserts.Model, error)) error 82 secbootUnlockVolumeUsingSealedKeyIfEncrypted func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) 83 secbootUnlockEncryptedVolumeUsingKey func(disk disks.Disk, name string, key []byte) (secboot.UnlockResult, error) 84 85 secbootLockSealedKeys func() error 86 87 bootFindPartitionUUIDForBootedKernelDisk = boot.FindPartitionUUIDForBootedKernelDisk 88 ) 89 90 func stampedAction(stamp string, action func() error) error { 91 stampFile := filepath.Join(dirs.SnapBootstrapRunDir, stamp) 92 if osutil.FileExists(stampFile) { 93 return nil 94 } 95 if err := os.MkdirAll(filepath.Dir(stampFile), 0755); err != nil { 96 return err 97 } 98 if err := action(); err != nil { 99 return err 100 } 101 return ioutil.WriteFile(stampFile, nil, 0644) 102 } 103 104 func generateInitramfsMounts() (err error) { 105 // ensure that the last thing we do is to lock access to sealed keys, 106 // regardless of mode or early failures. 107 defer func() { 108 if e := secbootLockSealedKeys(); e != nil { 109 e = fmt.Errorf("error locking access to sealed keys: %v", e) 110 if err == nil { 111 err = e 112 } else { 113 // preserve err but log 114 logger.Noticef("%v", e) 115 } 116 } 117 }() 118 119 // Ensure there is a very early initial measurement 120 err = stampedAction("secboot-epoch-measured", func() error { 121 return secbootMeasureSnapSystemEpochWhenPossible() 122 }) 123 if err != nil { 124 return err 125 } 126 127 mode, recoverySystem, err := boot.ModeAndRecoverySystemFromKernelCommandLine() 128 if err != nil { 129 return err 130 } 131 132 mst := &initramfsMountsState{ 133 mode: mode, 134 recoverySystem: recoverySystem, 135 } 136 137 switch mode { 138 case "recover": 139 return generateMountsModeRecover(mst) 140 case "install": 141 return generateMountsModeInstall(mst) 142 case "run": 143 return generateMountsModeRun(mst) 144 } 145 // this should never be reached 146 return fmt.Errorf("internal error: mode in generateInitramfsMounts not handled") 147 } 148 149 // generateMountsMode* is called multiple times from initramfs until it 150 // no longer generates more mount points and just returns an empty output. 151 func generateMountsModeInstall(mst *initramfsMountsState) error { 152 // steps 1 and 2 are shared with recover mode 153 model, snaps, err := generateMountsCommonInstallRecover(mst) 154 if err != nil { 155 return err 156 } 157 158 // 3. final step: write modeenv to tmpfs data dir and disable cloud-init in 159 // install mode 160 modeEnv, err := mst.EphemeralModeenvForModel(model, snaps) 161 if err != nil { 162 return err 163 } 164 if err := modeEnv.WriteTo(boot.InitramfsWritableDir); err != nil { 165 return err 166 } 167 168 // done, no output, no error indicates to initramfs we are done with 169 // mounting stuff 170 return nil 171 } 172 173 // copyNetworkConfig copies the network configuration to the target 174 // directory. This is used to copy the network configuration 175 // data from a real uc20 ubuntu-data partition into a ephemeral one. 176 func copyNetworkConfig(src, dst string) error { 177 for _, globEx := range []string{ 178 // for network configuration setup by console-conf, etc. 179 // TODO:UC20: we want some way to "try" or "verify" the network 180 // configuration or to only use known-to-be-good network 181 // configuration i.e. from ubuntu-save before installing it 182 // onto recover mode, because the network configuration could 183 // have been what was broken so we don't want to break 184 // network configuration for recover mode as well, but for 185 // now this is fine 186 "system-data/etc/netplan/*", 187 // etc/machine-id is part of what systemd-networkd uses to generate a 188 // DHCP clientid (the other part being the interface name), so to have 189 // the same IP addresses across run mode and recover mode, we need to 190 // also copy the machine-id across 191 "system-data/etc/machine-id", 192 } { 193 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 194 return err 195 } 196 } 197 return nil 198 } 199 200 // copyUbuntuDataMisc copies miscellaneous other files from the run mode system 201 // to the recover system such as: 202 // - timesync clock to keep the same time setting in recover as in run mode 203 func copyUbuntuDataMisc(src, dst string) error { 204 for _, globEx := range []string{ 205 // systemd's timesync clock file so that the time in recover mode moves 206 // forward to what it was in run mode 207 // NOTE: we don't sync back the time movement from recover mode to run 208 // mode currently, unclear how/when we could do this, but recover mode 209 // isn't meant to be long lasting and as such it's probably not a big 210 // problem to "lose" the time spent in recover mode 211 "system-data/var/lib/systemd/timesync/clock", 212 } { 213 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 214 return err 215 } 216 } 217 218 return nil 219 } 220 221 // copyUbuntuDataAuth copies the authentication files like 222 // - extrausers passwd,shadow etc 223 // - sshd host configuration 224 // - user .ssh dir 225 // to the target directory. This is used to copy the authentication 226 // data from a real uc20 ubuntu-data partition into a ephemeral one. 227 func copyUbuntuDataAuth(src, dst string) error { 228 for _, globEx := range []string{ 229 "system-data/var/lib/extrausers/*", 230 "system-data/etc/ssh/*", 231 "user-data/*/.ssh/*", 232 // this ensures we get proper authentication to snapd from "snap" 233 // commands in recover mode 234 "user-data/*/.snap/auth.json", 235 // this ensures we also get non-ssh enabled accounts copied 236 "user-data/*/.profile", 237 // so that users have proper perms, i.e. console-conf added users are 238 // sudoers 239 "system-data/etc/sudoers.d/*", 240 } { 241 if err := copyFromGlobHelper(src, dst, globEx); err != nil { 242 return err 243 } 244 } 245 246 // ensure the user state is transferred as well 247 srcState := filepath.Join(src, "system-data/var/lib/snapd/state.json") 248 dstState := filepath.Join(dst, "system-data/var/lib/snapd/state.json") 249 err := state.CopyState(srcState, dstState, []string{"auth.users", "auth.macaroon-key", "auth.last-id"}) 250 if err != nil && err != state.ErrNoState { 251 return fmt.Errorf("cannot copy user state: %v", err) 252 } 253 254 return nil 255 } 256 257 // copySafeDefaultData will copy to the destination a "safe" set of data for 258 // a blank recover mode, i.e. one where we cannot copy authentication, etc. from 259 // the actual host ubuntu-data. Currently this is just a file to disable 260 // console-conf from running. 261 func copySafeDefaultData(dst string) error { 262 consoleConfCompleteFile := filepath.Join(dst, "system-data/var/lib/console-conf/complete") 263 if err := os.MkdirAll(filepath.Dir(consoleConfCompleteFile), 0755); err != nil { 264 return err 265 } 266 return ioutil.WriteFile(consoleConfCompleteFile, nil, 0644) 267 } 268 269 func copyFromGlobHelper(src, dst, globEx string) error { 270 matches, err := filepath.Glob(filepath.Join(src, globEx)) 271 if err != nil { 272 return err 273 } 274 for _, p := range matches { 275 comps := strings.Split(strings.TrimPrefix(p, src), "/") 276 for i := range comps { 277 part := filepath.Join(comps[0 : i+1]...) 278 fi, err := os.Stat(filepath.Join(src, part)) 279 if err != nil { 280 return err 281 } 282 if fi.IsDir() { 283 if err := os.Mkdir(filepath.Join(dst, part), fi.Mode()); err != nil && !os.IsExist(err) { 284 return err 285 } 286 st, ok := fi.Sys().(*syscall.Stat_t) 287 if !ok { 288 return fmt.Errorf("cannot get stat data: %v", err) 289 } 290 if err := os.Chown(filepath.Join(dst, part), int(st.Uid), int(st.Gid)); err != nil { 291 return err 292 } 293 } else { 294 if err := osutil.CopyFile(p, filepath.Join(dst, part), osutil.CopyFlagPreserveAll); err != nil { 295 return err 296 } 297 } 298 } 299 } 300 301 return nil 302 } 303 304 // states for partition state 305 const ( 306 // states for LocateState 307 partitionFound = "found" 308 partitionNotFound = "not-found" 309 partitionErrFinding = "error-finding" 310 // states for MountState 311 partitionMounted = "mounted" 312 partitionErrMounting = "error-mounting" 313 partitionAbsentOptional = "absent-but-optional" 314 partitionMountedUntrusted = "mounted-untrusted" 315 // states for UnlockState 316 partitionUnlocked = "unlocked" 317 partitionErrUnlocking = "error-unlocking" 318 // keys used to unlock for UnlockKey 319 keyRun = "run" 320 keyFallback = "fallback" 321 keyRecovery = "recovery" 322 ) 323 324 // partitionState is the state of a partition after recover mode has completed 325 // for degraded mode. 326 type partitionState struct { 327 // MountState is whether the partition was mounted successfully or not. 328 MountState string `json:"mount-state,omitempty"` 329 // MountLocation is where the partition was mounted. 330 MountLocation string `json:"mount-location,omitempty"` 331 // Device is what device the partition corresponds to. It can be the 332 // physical block device if the partition is unencrypted or if it was not 333 // successfully unlocked, or it can be a decrypted mapper device if the 334 // partition was encrypted and successfully decrypted, or it can be the 335 // empty string (or missing) if the partition was not found at all. 336 Device string `json:"device,omitempty"` 337 // FindState indicates whether the partition was found on the disk or not. 338 FindState string `json:"find-state,omitempty"` 339 // UnlockState was whether the partition was unlocked successfully or not. 340 UnlockState string `json:"unlock-state,omitempty"` 341 // UnlockKey was what key the partition was unlocked with, either "run", 342 // "fallback" or "recovery". 343 UnlockKey string `json:"unlock-key,omitempty"` 344 345 // unexported internal fields for tracking the device, these are used during 346 // state machine execution, and then combined into Device during finalize() 347 // for simple representation to the consumer of degraded.json 348 349 // fsDevice is what decrypted mapper device corresponds to the 350 // partition, it can have the following states 351 // - successfully decrypted => the decrypted mapper device 352 // - unencrypted => the block device of the partition 353 // - identified as decrypted, but failed to decrypt => empty string 354 fsDevice string 355 // partDevice is always the physical block device of the partition, in the 356 // encrypted case this is the physical encrypted partition. 357 partDevice string 358 } 359 360 type recoverDegradedState struct { 361 // UbuntuData is the state of the ubuntu-data (or ubuntu-data-enc) 362 // partition. 363 UbuntuData partitionState `json:"ubuntu-data,omitempty"` 364 // UbuntuBoot is the state of the ubuntu-boot partition. 365 UbuntuBoot partitionState `json:"ubuntu-boot,omitempty"` 366 // UbuntuSave is the state of the ubuntu-save (or ubuntu-save-enc) 367 // partition. 368 UbuntuSave partitionState `json:"ubuntu-save,omitempty"` 369 // ErrorLog is the log of error messages encountered during recover mode 370 // setting up degraded mode. 371 ErrorLog []string `json:"error-log"` 372 } 373 374 func (r *recoverDegradedState) partition(part string) *partitionState { 375 switch part { 376 case "ubuntu-data": 377 return &r.UbuntuData 378 case "ubuntu-boot": 379 return &r.UbuntuBoot 380 case "ubuntu-save": 381 return &r.UbuntuSave 382 } 383 panic(fmt.Sprintf("unknown partition %s", part)) 384 } 385 386 func (r *recoverDegradedState) LogErrorf(format string, v ...interface{}) { 387 msg := fmt.Sprintf(format, v...) 388 r.ErrorLog = append(r.ErrorLog, msg) 389 logger.Noticef(msg) 390 } 391 392 // stateFunc is a function which executes a state action, returns the next 393 // function (for the next) state or nil if it is the final state. 394 type stateFunc func() (stateFunc, error) 395 396 // recoverModeStateMachine is a state machine implementing the logic for 397 // degraded recover mode. 398 // A full state diagram for the state machine can be found in 399 // /cmd/snap-bootstrap/degraded-recover-mode.svg in this repo. 400 type recoverModeStateMachine struct { 401 // the current state is the one that is about to be executed 402 current stateFunc 403 404 // device model 405 model *asserts.Model 406 407 // the disk we have all our partitions on 408 disk disks.Disk 409 410 // TODO:UC20: for clarity turn this into into tristate: 411 // unknown|encrypted|unencrypted 412 isEncryptedDev bool 413 414 // state for tracking what happens as we progress through degraded mode of 415 // recovery 416 degradedState *recoverDegradedState 417 } 418 419 // degraded returns whether a degraded recover mode state has fallen back from 420 // the typical operation to some sort of degraded mode. 421 func (m *recoverModeStateMachine) degraded() bool { 422 r := m.degradedState 423 424 if m.isEncryptedDev { 425 // for encrypted devices, we need to have ubuntu-save mounted 426 if r.UbuntuSave.MountState != partitionMounted { 427 return true 428 } 429 430 // we also should have all the unlock keys as run keys 431 if r.UbuntuData.UnlockKey != keyRun { 432 return true 433 } 434 435 if r.UbuntuSave.UnlockKey != keyRun { 436 return true 437 } 438 } else { 439 // for unencrypted devices, ubuntu-save must either be mounted or 440 // absent-but-optional 441 if r.UbuntuSave.MountState != partitionMounted { 442 if r.UbuntuSave.MountState != partitionAbsentOptional { 443 return true 444 } 445 } 446 } 447 448 // ubuntu-boot and ubuntu-data should both be mounted 449 if r.UbuntuBoot.MountState != partitionMounted { 450 return true 451 } 452 if r.UbuntuData.MountState != partitionMounted { 453 return true 454 } 455 456 // TODO: should we also check MountLocation too? 457 458 // we should have nothing in the error log 459 if len(r.ErrorLog) != 0 { 460 return true 461 } 462 463 return false 464 } 465 466 func (m *recoverModeStateMachine) diskOpts() *disks.Options { 467 if m.isEncryptedDev { 468 return &disks.Options{ 469 IsDecryptedDevice: true, 470 } 471 } 472 return nil 473 } 474 475 func (m *recoverModeStateMachine) verifyMountPoint(dir, name string) error { 476 matches, err := m.disk.MountPointIsFromDisk(dir, m.diskOpts()) 477 if err != nil { 478 return err 479 } 480 if !matches { 481 return fmt.Errorf("cannot validate mount: %s mountpoint target %s is expected to be from disk %s but is not", name, dir, m.disk.Dev()) 482 } 483 return nil 484 } 485 486 func (m *recoverModeStateMachine) setFindState(partName, partUUID string, err error) error { 487 part := m.degradedState.partition(partName) 488 if err != nil { 489 if _, ok := err.(disks.PartitionNotFoundError); ok { 490 // explicit error that the device was not found 491 part.FindState = partitionNotFound 492 m.degradedState.LogErrorf("cannot find %v partition on disk %s", partName, m.disk.Dev()) 493 return nil 494 } 495 // the error is not "not-found", so we have a real error 496 part.FindState = partitionErrFinding 497 m.degradedState.LogErrorf("error finding %v partition on disk %s: %v", partName, m.disk.Dev(), err) 498 return nil 499 } 500 501 // device was found 502 part.FindState = partitionFound 503 dev := fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID) 504 part.partDevice = dev 505 part.fsDevice = dev 506 return nil 507 } 508 509 func (m *recoverModeStateMachine) setMountState(part, where string, err error) error { 510 if err != nil { 511 m.degradedState.LogErrorf("cannot mount %v: %v", part, err) 512 m.degradedState.partition(part).MountState = partitionErrMounting 513 return nil 514 } 515 516 m.degradedState.partition(part).MountState = partitionMounted 517 m.degradedState.partition(part).MountLocation = where 518 519 if err := m.verifyMountPoint(where, part); err != nil { 520 m.degradedState.LogErrorf("cannot verify %s mount point at %v: %v", part, where, err) 521 return err 522 } 523 return nil 524 } 525 526 func (m *recoverModeStateMachine) setUnlockStateWithRunKey(partName string, unlockRes secboot.UnlockResult, err error) error { 527 part := m.degradedState.partition(partName) 528 // save the device if we found it from secboot 529 if unlockRes.PartDevice != "" { 530 part.FindState = partitionFound 531 part.partDevice = unlockRes.PartDevice 532 part.fsDevice = unlockRes.FsDevice 533 } else { 534 part.FindState = partitionNotFound 535 } 536 if unlockRes.IsEncrypted { 537 m.isEncryptedDev = true 538 } 539 540 if err != nil { 541 // create different error message for encrypted vs unencrypted 542 if unlockRes.IsEncrypted { 543 // if we know the device is decrypted we must also always know at 544 // least the partDevice (which is the encrypted block device) 545 m.degradedState.LogErrorf("cannot unlock encrypted %s (device %s) with sealed run key: %v", partName, part.partDevice, err) 546 part.UnlockState = partitionErrUnlocking 547 } else { 548 // TODO: we don't know if this is a plain not found or a different error 549 m.degradedState.LogErrorf("cannot locate %s partition for mounting host data: %v", partName, err) 550 } 551 552 return nil 553 } 554 555 if unlockRes.IsEncrypted { 556 // unlocked successfully 557 part.UnlockState = partitionUnlocked 558 part.UnlockKey = keyRun 559 } 560 561 return nil 562 } 563 564 func (m *recoverModeStateMachine) setUnlockStateWithFallbackKey(partName string, unlockRes secboot.UnlockResult, err error, partitionOptional bool) error { 565 // first check the result and error for consistency; since we are using udev 566 // there could be inconsistent results at different points in time 567 568 // TODO: consider refactoring UnlockVolumeUsingSealedKeyIfEncrypted to not 569 // also find the partition on the disk, that should eliminate this 570 // consistency checking as we can code it such that we don't get these 571 // possible inconsistencies 572 573 // do basic consistency checking on unlockRes to make sure the 574 // result makes sense. 575 if unlockRes.FsDevice != "" && err != nil { 576 // This case should be impossible to enter, we can't 577 // have a filesystem device but an error set 578 return fmt.Errorf("internal error: inconsistent return values from UnlockVolumeUsingSealedKeyIfEncrypted for partition %s: %v", partName, err) 579 } 580 581 part := m.degradedState.partition(partName) 582 // Also make sure that if we previously saw a partition device that we see 583 // the same device again. 584 if unlockRes.PartDevice != "" && part.partDevice != "" && unlockRes.PartDevice != part.partDevice { 585 return fmt.Errorf("inconsistent partitions found for %s: previously found %s but now found %s", partName, part.partDevice, unlockRes.PartDevice) 586 } 587 588 // ensure consistency between encrypted state of the device/disk and what we 589 // may have seen previously 590 if m.isEncryptedDev && !unlockRes.IsEncrypted { 591 // then we previously were able to positively identify an 592 // ubuntu-data-enc but can't anymore, so we have inconsistent results 593 // from inspecting the disk which is suspicious and we should fail 594 return fmt.Errorf("inconsistent disk encryption status: previous access resulted in encrypted, but now is unencrypted from partition %s", partName) 595 } 596 597 // now actually process the result into the state 598 if unlockRes.PartDevice != "" { 599 part.FindState = partitionFound 600 // Note that in some case this may be redundantly assigning the same 601 // value to partDevice again. 602 part.partDevice = unlockRes.PartDevice 603 part.fsDevice = unlockRes.FsDevice 604 } 605 606 // There are a few cases where this could be the first time that we found a 607 // decrypted device in the UnlockResult, but m.isEncryptedDev is still 608 // false. 609 // - The first case is if we couldn't find ubuntu-boot at all, in which case 610 // we can't use the run object keys from there and instead need to directly 611 // fallback to trying the fallback object keys from ubuntu-seed 612 // - The second case is if we couldn't identify an ubuntu-data-enc or an 613 // ubuntu-data partition at all, we still could have an ubuntu-save-enc 614 // partition in which case we maybe could still have an encrypted disk that 615 // needs unlocking with the fallback object keys from ubuntu-seed 616 // 617 // As such, if m.isEncryptedDev is false, but unlockRes.IsEncrypted is 618 // true, then it is safe to assign m.isEncryptedDev to true. 619 if !m.isEncryptedDev && unlockRes.IsEncrypted { 620 m.isEncryptedDev = true 621 } 622 623 if err != nil { 624 // create different error message for encrypted vs unencrypted 625 if m.isEncryptedDev { 626 m.degradedState.LogErrorf("cannot unlock encrypted %s partition with sealed fallback key: %v", partName, err) 627 part.UnlockState = partitionErrUnlocking 628 } else { 629 // if we don't have an encrypted device and err != nil, then the 630 // device must be not-found, see above checks 631 632 // if the partition is optional (like ubuntu-save is) then don't 633 // report an error for ubuntu-save not being found and also set it 634 // as absent-but-optional 635 if unlockRes.PartDevice == "" && partitionOptional { 636 part.MountState = partitionAbsentOptional 637 } else { 638 // log the error the partition is mandatory 639 m.degradedState.LogErrorf("cannot locate %s partition: %v", partName, err) 640 } 641 } 642 643 return nil 644 } 645 646 if m.isEncryptedDev { 647 // unlocked successfully 648 part.UnlockState = partitionUnlocked 649 650 // figure out which key/method we used to unlock the partition 651 switch unlockRes.UnlockMethod { 652 case secboot.UnlockedWithSealedKey: 653 part.UnlockKey = keyFallback 654 case secboot.UnlockedWithRecoveryKey: 655 part.UnlockKey = keyRecovery 656 657 // TODO: should we fail with internal error for default case here? 658 } 659 } 660 661 return nil 662 } 663 664 func newRecoverModeStateMachine(model *asserts.Model, disk disks.Disk) *recoverModeStateMachine { 665 m := &recoverModeStateMachine{ 666 model: model, 667 disk: disk, 668 degradedState: &recoverDegradedState{ 669 ErrorLog: []string{}, 670 }, 671 } 672 // first step is to mount ubuntu-boot to check for run mode keys to unlock 673 // ubuntu-data 674 m.current = m.mountBoot 675 return m 676 } 677 678 func (m *recoverModeStateMachine) execute() (finished bool, err error) { 679 next, err := m.current() 680 m.current = next 681 finished = next == nil 682 if finished && err == nil { 683 if err := m.finalize(); err != nil { 684 return true, err 685 } 686 } 687 return finished, err 688 } 689 690 func (m *recoverModeStateMachine) finalize() error { 691 // check soundness 692 // the grade check makes sure that if data was mounted unencrypted 693 // but the model is secured it will end up marked as untrusted 694 isEncrypted := m.isEncryptedDev || m.model.StorageSafety() == asserts.StorageSafetyEncrypted 695 part := m.degradedState.partition("ubuntu-data") 696 if part.MountState == partitionMounted && isEncrypted { 697 // check that save and data match 698 // We want to avoid a chosen ubuntu-data 699 // (e.g. activated with a recovery key) to get access 700 // via its logins to the secrets in ubuntu-save (in 701 // particular the policy update auth key) 702 // TODO:UC20: we should try to be a bit more specific here in checking that 703 // data and save match, and not mark data as untrusted if we 704 // know that the real save is locked/protected (or doesn't exist 705 // in the case of bad corruption) because currently this code will 706 // mark data as untrusted, even if it was unlocked with the run 707 // object key and we failed to unlock ubuntu-save at all, which is 708 // undesirable. This effectively means that you need to have both 709 // ubuntu-data and ubuntu-save unlockable and have matching marker 710 // files in order to use the files from ubuntu-data to log-in, 711 // etc. 712 trustData, _ := checkDataAndSavePairing(boot.InitramfsHostWritableDir) 713 if !trustData { 714 part.MountState = partitionMountedUntrusted 715 m.degradedState.LogErrorf("cannot trust ubuntu-data, ubuntu-save and ubuntu-data are not marked as from the same install") 716 } 717 } 718 719 // finally, combine the states of partDevice and fsDevice into the 720 // exported Device field for marshalling 721 // ubuntu-boot is easy - it will always be unencrypted so we just set 722 // Device to partDevice 723 m.degradedState.partition("ubuntu-boot").Device = m.degradedState.partition("ubuntu-boot").partDevice 724 725 // for ubuntu-data and save, we need to actually look at the states 726 for _, partName := range []string{"ubuntu-data", "ubuntu-save"} { 727 part := m.degradedState.partition(partName) 728 if part.fsDevice == "" { 729 // then the device is encrypted, but we failed to decrypt it, so 730 // set Device to the encrypted block device 731 part.Device = part.partDevice 732 } else { 733 // all other cases, fsDevice is set to what we want to 734 // export, either it is set to the decrypted mapper device in the 735 // case it was successfully decrypted, or it is set to the encrypted 736 // block device if we failed to decrypt it, or it was set to the 737 // unencrypted block device if it was unencrypted 738 part.Device = part.fsDevice 739 } 740 } 741 742 return nil 743 } 744 745 func (m *recoverModeStateMachine) trustData() bool { 746 return m.degradedState.partition("ubuntu-data").MountState == partitionMounted 747 } 748 749 // mountBoot is the first state to execute in the state machine, it can 750 // transition to the following states: 751 // - if ubuntu-boot is mounted successfully, execute unlockDataRunKey 752 // - if ubuntu-boot can't be mounted, execute unlockDataFallbackKey 753 // - if we mounted the wrong ubuntu-boot (or otherwise can't verify which one we 754 // mounted), return fatal error 755 func (m *recoverModeStateMachine) mountBoot() (stateFunc, error) { 756 part := m.degradedState.partition("ubuntu-boot") 757 // use the disk we mounted ubuntu-seed from as a reference to find 758 // ubuntu-seed and mount it 759 partUUID, findErr := m.disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-boot") 760 if err := m.setFindState("ubuntu-boot", partUUID, findErr); err != nil { 761 return nil, err 762 } 763 if part.FindState != partitionFound { 764 // if we didn't find ubuntu-boot, we can't try to unlock data with the 765 // run key, and should instead just jump straight to attempting to 766 // unlock with the fallback key 767 return m.unlockDataFallbackKey, nil 768 } 769 770 // should we fsck ubuntu-boot? probably yes because on some platforms 771 // (u-boot for example) ubuntu-boot is vfat and it could have been unmounted 772 // dirtily, and we need to fsck it to ensure it is mounted safely before 773 // reading keys from it 774 fsckSystemdOpts := &systemdMountOptions{ 775 NeedsFsck: true, 776 } 777 mountErr := doSystemdMount(part.fsDevice, boot.InitramfsUbuntuBootDir, fsckSystemdOpts) 778 if err := m.setMountState("ubuntu-boot", boot.InitramfsUbuntuBootDir, mountErr); err != nil { 779 return nil, err 780 } 781 if part.MountState == partitionErrMounting { 782 // if we didn't mount data, then try to unlock data with the 783 // fallback key 784 return m.unlockDataFallbackKey, nil 785 } 786 787 // next step try to unlock data with run object 788 return m.unlockDataRunKey, nil 789 } 790 791 // stateUnlockDataRunKey will try to unlock ubuntu-data with the normal run-mode 792 // key, and if it fails, progresses to the next state, which is either: 793 // - failed to unlock data, but we know it's an encrypted device -> try to unlock with fallback key 794 // - failed to find data at all -> try to unlock save 795 // - unlocked data with run key -> mount data 796 func (m *recoverModeStateMachine) unlockDataRunKey() (stateFunc, error) { 797 runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") 798 unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ 799 // don't allow using the recovery key to unlock, we only try using the 800 // recovery key after we first try the fallback object 801 AllowRecoveryKey: false, 802 } 803 unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", runModeKey, unlockOpts) 804 if err := m.setUnlockStateWithRunKey("ubuntu-data", unlockRes, unlockErr); err != nil { 805 return nil, err 806 } 807 if unlockErr != nil { 808 // we couldn't unlock ubuntu-data with the primary key, or we didn't 809 // find it in the unencrypted case 810 if unlockRes.IsEncrypted { 811 // we know the device is encrypted, so the next state is to try 812 // unlocking with the fallback key 813 return m.unlockDataFallbackKey, nil 814 } 815 816 // if we didn't even find the device to the point where it would have 817 // been identified as decrypted or unencrypted device, we could have 818 // just entirely lost ubuntu-data-enc, and we could still have an 819 // encrypted device, so instead try to unlock ubuntu-save with the 820 // fallback key, the logic there can also handle an unencrypted ubuntu-save 821 return m.unlockSaveFallbackKey, nil 822 } 823 824 // otherwise successfully unlocked it (or just found it if it was unencrypted) 825 // so just mount it 826 return m.mountData, nil 827 } 828 829 func (m *recoverModeStateMachine) unlockDataFallbackKey() (stateFunc, error) { 830 // try to unlock data with the fallback key on ubuntu-seed, which must have 831 // been mounted at this point 832 unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ 833 // we want to allow using the recovery key if the fallback key fails as 834 // using the fallback object is the last chance before we give up trying 835 // to unlock data 836 AllowRecoveryKey: true, 837 } 838 // TODO: this prompts for a recovery key 839 // TODO: we should somehow customize the prompt to mention what key we need 840 // the user to enter, and what we are unlocking (as currently the prompt 841 // says "recovery key" and the partition UUID for what is being unlocked) 842 dataFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key") 843 unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", dataFallbackKey, unlockOpts) 844 const partitionMandatory = false 845 if err := m.setUnlockStateWithFallbackKey("ubuntu-data", unlockRes, unlockErr, partitionMandatory); err != nil { 846 return nil, err 847 } 848 if unlockErr != nil { 849 // skip trying to mount data, since we did not unlock data we cannot 850 // open save with with the run key, so try the fallback one 851 return m.unlockSaveFallbackKey, nil 852 } 853 854 // unlocked it, now go mount it 855 return m.mountData, nil 856 } 857 858 func (m *recoverModeStateMachine) mountData() (stateFunc, error) { 859 data := m.degradedState.partition("ubuntu-data") 860 // don't do fsck on the data partition, it could be corrupted 861 mountErr := doSystemdMount(data.fsDevice, boot.InitramfsHostUbuntuDataDir, nil) 862 if err := m.setMountState("ubuntu-data", boot.InitramfsHostUbuntuDataDir, mountErr); err != nil { 863 return nil, err 864 } 865 if mountErr == nil && m.isEncryptedDev { 866 // if we succeeded in mounting data and we are encrypted, the next step 867 // is to unlock save with the run key from ubuntu-data 868 return m.unlockSaveRunKey, nil 869 } 870 871 // otherwise we always fall back to unlocking save with the fallback key, 872 // this could be two cases: 873 // 1. we are unencrypted in which case the secboot function used in 874 // unlockSaveRunKey will fail and then proceed to trying the fallback key 875 // function anyways which uses a secboot function that is suitable for 876 // unencrypted data 877 // 2. we are encrypted and we failed to mount data successfully, meaning we 878 // don't have the bare key from ubuntu-data to use, and need to fall back 879 // to the sealed key from ubuntu-seed 880 return m.unlockSaveFallbackKey, nil 881 } 882 883 func (m *recoverModeStateMachine) unlockSaveRunKey() (stateFunc, error) { 884 // to get to this state, we needed to have mounted ubuntu-data on host, so 885 // if encrypted, we can try to read the run key from host ubuntu-data 886 saveKey := filepath.Join(dirs.SnapFDEDirUnder(boot.InitramfsHostWritableDir), "ubuntu-save.key") 887 key, err := ioutil.ReadFile(saveKey) 888 if err != nil { 889 // log the error and skip to trying the fallback key 890 m.degradedState.LogErrorf("cannot access run ubuntu-save key: %v", err) 891 return m.unlockSaveFallbackKey, nil 892 } 893 894 unlockRes, unlockErr := secbootUnlockEncryptedVolumeUsingKey(m.disk, "ubuntu-save", key) 895 if err := m.setUnlockStateWithRunKey("ubuntu-save", unlockRes, unlockErr); err != nil { 896 return nil, err 897 } 898 if unlockErr != nil { 899 // failed to unlock with run key, try fallback key 900 return m.unlockSaveFallbackKey, nil 901 } 902 903 // unlocked it properly, go mount it 904 return m.mountSave, nil 905 } 906 907 func (m *recoverModeStateMachine) unlockSaveFallbackKey() (stateFunc, error) { 908 // remember what we assumed about encryption before looking at 909 // save 910 assumeEncrypted := m.isEncryptedDev 911 912 // try to unlock save with the fallback key on ubuntu-seed, which must have 913 // been mounted at this point 914 unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ 915 // we want to allow using the recovery key if the fallback key fails as 916 // using the fallback object is the last chance before we give up trying 917 // to unlock save 918 AllowRecoveryKey: true, 919 } 920 saveFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key") 921 // TODO: this prompts again for a recover key, but really this is the 922 // reinstall key we will prompt for 923 // TODO: we should somehow customize the prompt to mention what key we need 924 // the user to enter, and what we are unlocking (as currently the prompt 925 // says "recovery key" and the partition UUID for what is being unlocked) 926 unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-save", saveFallbackKey, unlockOpts) 927 const partitionOptionalIfUnencrypted = true 928 if err := m.setUnlockStateWithFallbackKey("ubuntu-save", unlockRes, unlockErr, partitionOptionalIfUnencrypted); err != nil { 929 return nil, err 930 } 931 if unlockErr != nil { 932 // all done, nothing left to try and mount, mounting ubuntu-save is the 933 // last step but we couldn't find or unlock it 934 return nil, nil 935 } 936 937 // do a consistency check to make sure that if we found ubuntu-data 938 // unencrypted that we don't also mount ubuntu-save as encrypted 939 data := m.degradedState.partition("ubuntu-data") 940 if unlockRes.IsEncrypted && data.FindState == partitionFound && !assumeEncrypted { 941 return nil, fmt.Errorf("inconsistent encryption status for disk %s: ubuntu-data (device %s) was found unencrypted but ubuntu-save (device %s) was found to be encrypted", m.disk.Dev(), data.fsDevice, unlockRes.FsDevice) 942 } 943 944 // otherwise we unlocked it, so go mount it 945 return m.mountSave, nil 946 } 947 948 func (m *recoverModeStateMachine) mountSave() (stateFunc, error) { 949 save := m.degradedState.partition("ubuntu-save") 950 // TODO: should we fsck ubuntu-save ? 951 mountErr := doSystemdMount(save.fsDevice, boot.InitramfsUbuntuSaveDir, nil) 952 if err := m.setMountState("ubuntu-save", boot.InitramfsUbuntuSaveDir, mountErr); err != nil { 953 return nil, err 954 } 955 // all done, nothing left to try and mount 956 return nil, nil 957 } 958 959 func generateMountsModeRecover(mst *initramfsMountsState) error { 960 // steps 1 and 2 are shared with install mode 961 model, snaps, err := generateMountsCommonInstallRecover(mst) 962 if err != nil { 963 return err 964 } 965 966 // get the disk that we mounted the ubuntu-seed partition from as a 967 // reference point for future mounts 968 disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil) 969 if err != nil { 970 return err 971 } 972 973 // 3. run the state machine logic for mounting partitions, this involves 974 // trying to unlock then mount ubuntu-data, and then unlocking and 975 // mounting ubuntu-save 976 // see the state* functions for details of what each step does and 977 // possible transition points 978 979 machine, err := func() (machine *recoverModeStateMachine, err error) { 980 // first state to execute is to unlock ubuntu-data with the run key 981 machine = newRecoverModeStateMachine(model, disk) 982 for { 983 finished, err := machine.execute() 984 // TODO: consider whether certain errors are fatal or not 985 if err != nil { 986 return nil, err 987 } 988 if finished { 989 break 990 } 991 } 992 993 return machine, nil 994 }() 995 if err != nil { 996 return err 997 } 998 999 // 3.1 write out degraded.json if we ended up falling back somewhere 1000 if machine.degraded() { 1001 b, err := json.Marshal(machine.degradedState) 1002 if err != nil { 1003 return err 1004 } 1005 1006 if err := os.MkdirAll(dirs.SnapBootstrapRunDir, 0755); err != nil { 1007 return err 1008 } 1009 1010 // leave the information about degraded state at an ephemeral location 1011 if err := ioutil.WriteFile(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), b, 0644); err != nil { 1012 return err 1013 } 1014 } 1015 1016 // 4. final step: copy the auth data and network config from 1017 // the real ubuntu-data dir to the ephemeral ubuntu-data 1018 // dir, write the modeenv to the tmpfs data, and disable 1019 // cloud-init in recover mode 1020 1021 // if we have the host location, then we were able to successfully mount 1022 // ubuntu-data, and as such we can proceed with copying files from there 1023 // onto the tmpfs 1024 // Proceed only if we trust ubuntu-data to be paired with ubuntu-save 1025 if machine.trustData() { 1026 // TODO: erroring here should fallback to copySafeDefaultData and 1027 // proceed on with degraded mode anyways 1028 if err := copyUbuntuDataAuth(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 1029 return err 1030 } 1031 if err := copyNetworkConfig(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 1032 return err 1033 } 1034 if err := copyUbuntuDataMisc(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { 1035 return err 1036 } 1037 } else { 1038 // we don't have ubuntu-data host mountpoint, so we should setup safe 1039 // defaults for i.e. console-conf in the running image to block 1040 // attackers from accessing the system - just because we can't access 1041 // ubuntu-data doesn't mean that attackers wouldn't be able to if they 1042 // could login 1043 1044 if err := copySafeDefaultData(boot.InitramfsDataDir); err != nil { 1045 return err 1046 } 1047 } 1048 1049 modeEnv, err := mst.EphemeralModeenvForModel(model, snaps) 1050 if err != nil { 1051 return err 1052 } 1053 if err := modeEnv.WriteTo(boot.InitramfsWritableDir); err != nil { 1054 return err 1055 } 1056 1057 // finally we need to modify the bootenv to mark the system as successful, 1058 // this ensures that when you reboot from recover mode without doing 1059 // anything else, you are auto-transitioned back to run mode 1060 // TODO:UC20: as discussed unclear we need to pass the recovery system here 1061 if err := boot.EnsureNextBootToRunMode(mst.recoverySystem); err != nil { 1062 return err 1063 } 1064 1065 // done, no output, no error indicates to initramfs we are done with 1066 // mounting stuff 1067 return nil 1068 } 1069 1070 // checkDataAndSavePairing make sure that ubuntu-data and ubuntu-save 1071 // come from the same install by comparing secret markers in them 1072 func checkDataAndSavePairing(rootdir string) (bool, error) { 1073 // read the secret marker file from ubuntu-data 1074 markerFile1 := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "marker") 1075 marker1, err := ioutil.ReadFile(markerFile1) 1076 if err != nil { 1077 return false, err 1078 } 1079 // read the secret marker file from ubuntu-save 1080 markerFile2 := filepath.Join(dirs.SnapFDEDirUnderSave(boot.InitramfsUbuntuSaveDir), "marker") 1081 marker2, err := ioutil.ReadFile(markerFile2) 1082 if err != nil { 1083 return false, err 1084 } 1085 return subtle.ConstantTimeCompare(marker1, marker2) == 1, nil 1086 } 1087 1088 // mountPartitionMatchingKernelDisk will select the partition to mount at dir, 1089 // using the boot package function FindPartitionUUIDForBootedKernelDisk to 1090 // determine what partition the booted kernel came from. If which disk the 1091 // kernel came from cannot be determined, then it will fallback to mounting via 1092 // the specified disk label. 1093 func mountPartitionMatchingKernelDisk(dir, fallbacklabel string) error { 1094 partuuid, err := bootFindPartitionUUIDForBootedKernelDisk() 1095 // TODO: the by-partuuid is only available on gpt disks, on mbr we need 1096 // to use by-uuid or by-id 1097 partSrc := filepath.Join("/dev/disk/by-partuuid", partuuid) 1098 if err != nil { 1099 // no luck, try mounting by label instead 1100 partSrc = filepath.Join("/dev/disk/by-label", fallbacklabel) 1101 } 1102 1103 opts := &systemdMountOptions{ 1104 // always fsck the partition when we are mounting it, as this is the 1105 // first partition we will be mounting, we can't know if anything is 1106 // corrupted yet 1107 NeedsFsck: true, 1108 } 1109 return doSystemdMount(partSrc, dir, opts) 1110 } 1111 1112 func generateMountsCommonInstallRecover(mst *initramfsMountsState) (model *asserts.Model, sysSnaps map[snap.Type]snap.PlaceInfo, err error) { 1113 // 1. always ensure seed partition is mounted first before the others, 1114 // since the seed partition is needed to mount the snap files there 1115 if err := mountPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "ubuntu-seed"); err != nil { 1116 return nil, nil, err 1117 } 1118 1119 // load model and verified essential snaps metadata 1120 typs := []snap.Type{snap.TypeBase, snap.TypeKernel, snap.TypeSnapd, snap.TypeGadget} 1121 model, essSnaps, err := mst.ReadEssential("", typs) 1122 if err != nil { 1123 return nil, nil, fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", typs, err) 1124 } 1125 1126 // 2.1. measure model 1127 err = stampedAction(fmt.Sprintf("%s-model-measured", mst.recoverySystem), func() error { 1128 return secbootMeasureSnapModelWhenPossible(func() (*asserts.Model, error) { 1129 return model, nil 1130 }) 1131 }) 1132 if err != nil { 1133 return nil, nil, err 1134 } 1135 // at this point on a system with TPM-based encryption 1136 // data can be open only if the measured model matches the actual 1137 // expected recovery model we sealed against. 1138 // TODO:UC20: on ARM systems and no TPM with encryption 1139 // we need other ways to make sure that the disk is opened 1140 // and we continue booting only for expected recovery models 1141 1142 // 2.2. (auto) select recovery system and mount seed snaps 1143 // TODO:UC20: do we need more cross checks here? 1144 1145 systemSnaps := make(map[snap.Type]snap.PlaceInfo) 1146 1147 for _, essentialSnap := range essSnaps { 1148 if essentialSnap.EssentialType == snap.TypeGadget { 1149 // don't need to mount the gadget anywhere, but we use the snap 1150 // later hence it is loaded 1151 continue 1152 } 1153 systemSnaps[essentialSnap.EssentialType] = essentialSnap.PlaceInfo() 1154 1155 dir := snapTypeToMountDir[essentialSnap.EssentialType] 1156 // TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub 1157 if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { 1158 return nil, nil, err 1159 } 1160 } 1161 1162 // TODO:UC20: after we have the kernel and base snaps mounted, we should do 1163 // the bind mounts from the kernel modules on top of the base 1164 // mount and delete the corresponding systemd units from the 1165 // initramfs layout 1166 1167 // TODO:UC20: after the kernel and base snaps are mounted, we should setup 1168 // writable here as well to take over from "the-modeenv" script 1169 // in the initrd too 1170 1171 // TODO:UC20: after the kernel and base snaps are mounted and writable is 1172 // mounted, we should also implement writable-paths here too as 1173 // writing it in Go instead of shellscript is desirable 1174 1175 // 2.3. mount "ubuntu-data" on a tmpfs 1176 mntOpts := &systemdMountOptions{ 1177 Tmpfs: true, 1178 } 1179 err = doSystemdMount("tmpfs", boot.InitramfsDataDir, mntOpts) 1180 if err != nil { 1181 return nil, nil, err 1182 } 1183 1184 // finally get the gadget snap from the essential snaps and use it to 1185 // configure the ephemeral system 1186 // should only be one seed snap 1187 gadgetPath := "" 1188 for _, essentialSnap := range essSnaps { 1189 if essentialSnap.EssentialType == snap.TypeGadget { 1190 gadgetPath = essentialSnap.Path 1191 } 1192 } 1193 gadgetSnap := squashfs.New(gadgetPath) 1194 1195 // we need to configure the ephemeral system with defaults and such using 1196 // from the seed gadget 1197 configOpts := &sysconfig.Options{ 1198 // never allow cloud-init to run inside the ephemeral system, in the 1199 // install case we don't want it to ever run, and in the recover case 1200 // cloud-init will already have run in run mode, so things like network 1201 // config and users should already be setup and we will copy those 1202 // further down in the setup for recover mode 1203 AllowCloudInit: false, 1204 TargetRootDir: boot.InitramfsWritableDir, 1205 GadgetSnap: gadgetSnap, 1206 } 1207 if err := sysconfig.ConfigureTargetSystem(configOpts); err != nil { 1208 return nil, nil, err 1209 } 1210 1211 return model, systemSnaps, err 1212 } 1213 1214 func maybeMountSave(disk disks.Disk, rootdir string, encrypted bool, mountOpts *systemdMountOptions) (haveSave bool, err error) { 1215 var saveDevice string 1216 if encrypted { 1217 saveKey := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "ubuntu-save.key") 1218 // if ubuntu-save exists and is encrypted, the key has been created during install 1219 if !osutil.FileExists(saveKey) { 1220 // ubuntu-data is encrypted, but we appear to be missing 1221 // a key to open ubuntu-save 1222 return false, fmt.Errorf("cannot find ubuntu-save encryption key at %v", saveKey) 1223 } 1224 // we have save.key, volume exists and is encrypted 1225 key, err := ioutil.ReadFile(saveKey) 1226 if err != nil { 1227 return true, err 1228 } 1229 unlockRes, err := secbootUnlockEncryptedVolumeUsingKey(disk, "ubuntu-save", key) 1230 if err != nil { 1231 return true, fmt.Errorf("cannot unlock ubuntu-save volume: %v", err) 1232 } 1233 saveDevice = unlockRes.FsDevice 1234 } else { 1235 partUUID, err := disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-save") 1236 if err != nil { 1237 if _, ok := err.(disks.PartitionNotFoundError); ok { 1238 // this is ok, ubuntu-save may not exist for 1239 // non-encrypted device 1240 return false, nil 1241 } 1242 return false, err 1243 } 1244 saveDevice = filepath.Join("/dev/disk/by-partuuid", partUUID) 1245 } 1246 if err := doSystemdMount(saveDevice, boot.InitramfsUbuntuSaveDir, mountOpts); err != nil { 1247 return true, err 1248 } 1249 return true, nil 1250 } 1251 1252 func generateMountsModeRun(mst *initramfsMountsState) error { 1253 // 1. mount ubuntu-boot 1254 if err := mountPartitionMatchingKernelDisk(boot.InitramfsUbuntuBootDir, "ubuntu-boot"); err != nil { 1255 return err 1256 } 1257 1258 // get the disk that we mounted the ubuntu-boot partition from as a 1259 // reference point for future mounts 1260 disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuBootDir, nil) 1261 if err != nil { 1262 return err 1263 } 1264 1265 // 2. mount ubuntu-seed 1266 // use the disk we mounted ubuntu-boot from as a reference to find 1267 // ubuntu-seed and mount it 1268 partUUID, err := disk.FindMatchingPartitionUUIDWithFsLabel("ubuntu-seed") 1269 if err != nil { 1270 return err 1271 } 1272 1273 // fsck is safe to run on ubuntu-seed as per the manpage, it should not 1274 // meaningfully contribute to corruption if we fsck it every time we boot, 1275 // and it is important to fsck it because it is vfat and mounted writable 1276 // TODO:UC20: mount it as read-only here and remount as writable when we 1277 // need it to be writable for i.e. transitioning to recover mode 1278 fsckSystemdOpts := &systemdMountOptions{ 1279 NeedsFsck: true, 1280 } 1281 if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), boot.InitramfsUbuntuSeedDir, fsckSystemdOpts); err != nil { 1282 return err 1283 } 1284 1285 // 3.1. measure model 1286 err = stampedAction("run-model-measured", func() error { 1287 return secbootMeasureSnapModelWhenPossible(mst.UnverifiedBootModel) 1288 }) 1289 if err != nil { 1290 return err 1291 } 1292 // at this point on a system with TPM-based encryption 1293 // data can be open only if the measured model matches the actual 1294 // run model. 1295 // TODO:UC20: on ARM systems and no TPM with encryption 1296 // we need other ways to make sure that the disk is opened 1297 // and we continue booting only for expected models 1298 1299 // 3.2. mount Data 1300 runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") 1301 opts := &secboot.UnlockVolumeUsingSealedKeyOptions{ 1302 AllowRecoveryKey: true, 1303 } 1304 unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "ubuntu-data", runModeKey, opts) 1305 if err != nil { 1306 return err 1307 } 1308 1309 // TODO: do we actually need fsck if we are mounting a mapper device? 1310 // probably not? 1311 if err := doSystemdMount(unlockRes.FsDevice, boot.InitramfsDataDir, fsckSystemdOpts); err != nil { 1312 return err 1313 } 1314 isEncryptedDev := unlockRes.IsEncrypted 1315 1316 // 3.3. mount ubuntu-save (if present) 1317 haveSave, err := maybeMountSave(disk, boot.InitramfsWritableDir, isEncryptedDev, fsckSystemdOpts) 1318 if err != nil { 1319 return err 1320 } 1321 1322 // 4.1 verify that ubuntu-data comes from where we expect it to 1323 diskOpts := &disks.Options{} 1324 if unlockRes.IsEncrypted { 1325 // then we need to specify that the data mountpoint is expected to be a 1326 // decrypted device, applies to both ubuntu-data and ubuntu-save 1327 diskOpts.IsDecryptedDevice = true 1328 } 1329 1330 matches, err := disk.MountPointIsFromDisk(boot.InitramfsDataDir, diskOpts) 1331 if err != nil { 1332 return err 1333 } 1334 if !matches { 1335 // failed to verify that ubuntu-data mountpoint comes from the same disk 1336 // as ubuntu-boot 1337 return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) 1338 } 1339 if haveSave { 1340 // 4.1a we have ubuntu-save, verify it as well 1341 matches, err = disk.MountPointIsFromDisk(boot.InitramfsUbuntuSaveDir, diskOpts) 1342 if err != nil { 1343 return err 1344 } 1345 if !matches { 1346 return fmt.Errorf("cannot validate boot: ubuntu-save mountpoint is expected to be from disk %s but is not", disk.Dev()) 1347 } 1348 1349 if isEncryptedDev { 1350 // in run mode the path to open an encrypted save is for 1351 // data to be encrypted and the save key in it 1352 // to be successfully used. This already should stop 1353 // allowing to chose ubuntu-data to try to access 1354 // save. as safety boot also stops if the keys cannot 1355 // be locked. 1356 // for symmetry with recover code and extra paranoia 1357 // though also check that the markers match. 1358 paired, err := checkDataAndSavePairing(boot.InitramfsWritableDir) 1359 if err != nil { 1360 return err 1361 } 1362 if !paired { 1363 return fmt.Errorf("cannot validate boot: ubuntu-save and ubuntu-data are not marked as from the same install") 1364 } 1365 } 1366 } 1367 1368 // 4.2. read modeenv 1369 modeEnv, err := boot.ReadModeenv(boot.InitramfsWritableDir) 1370 if err != nil { 1371 return err 1372 } 1373 1374 typs := []snap.Type{snap.TypeBase, snap.TypeKernel} 1375 1376 // 4.2 choose base and kernel snaps (this includes updating modeenv if 1377 // needed to try the base snap) 1378 mounts, err := boot.InitramfsRunModeSelectSnapsToMount(typs, modeEnv) 1379 if err != nil { 1380 return err 1381 } 1382 1383 // TODO:UC20: with grade > dangerous, verify the kernel snap hash against 1384 // what we booted using the tpm log, this may need to be passed 1385 // to the function above to make decisions there, or perhaps this 1386 // code actually belongs in the bootloader implementation itself 1387 1388 // 4.3 mount base and kernel snaps 1389 // make sure this is a deterministic order 1390 for _, typ := range []snap.Type{snap.TypeBase, snap.TypeKernel} { 1391 if sn, ok := mounts[typ]; ok { 1392 dir := snapTypeToMountDir[typ] 1393 snapPath := filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) 1394 if err := doSystemdMount(snapPath, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { 1395 return err 1396 } 1397 } 1398 } 1399 1400 // 4.4 mount snapd snap only on first boot 1401 if modeEnv.RecoverySystem != "" { 1402 // load the recovery system and generate mount for snapd 1403 _, essSnaps, err := mst.ReadEssential(modeEnv.RecoverySystem, []snap.Type{snap.TypeSnapd}) 1404 if err != nil { 1405 return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err) 1406 } 1407 1408 return doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), nil) 1409 } 1410 1411 return nil 1412 }