diff options
| -rw-r--r-- | cmd/snap-bootstrap/cmd_initramfs_mounts.go | 734 | ||||
| -rw-r--r-- | cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go | 6 | ||||
| -rw-r--r-- | cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go | 292 | ||||
| -rw-r--r-- | cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go | 1 | ||||
| -rw-r--r-- | cmd/snap-bootstrap/cmd_initramfs_mounts_test.go | 1349 | ||||
| -rw-r--r-- | cmd/snap-bootstrap/export_test.go | 22 | ||||
| -rw-r--r-- | gadget/install/install.go | 14 | ||||
| -rw-r--r-- | gadget/install/partition_test.go | 3 | ||||
| -rw-r--r-- | gadget/ondisk.go | 6 | ||||
| -rw-r--r-- | gadget/ondisk_test.go | 7 | ||||
| -rw-r--r-- | interfaces/builtin/kvm_test.go | 3 | ||||
| -rw-r--r-- | interfaces/udev/spec.go | 4 | ||||
| -rw-r--r-- | interfaces/udev/spec_test.go | 28 | ||||
| -rw-r--r-- | overlord/devicestate/firstboot.go | 6 | ||||
| -rw-r--r-- | overlord/devicestate/firstboot_preseed_test.go | 64 | ||||
| -rw-r--r-- | overlord/devicestate/firstboot_test.go | 7 | ||||
| -rwxr-xr-x | packaging/debian-sid/rules | 2 | ||||
| -rw-r--r-- | tests/main/lxd/task.yaml | 3 | ||||
| -rw-r--r-- | tests/nested/manual/preseed/task.yaml | 15 |
19 files changed, 2456 insertions, 110 deletions
diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go index b5bda920fe..4a61993dad 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go @@ -20,6 +20,7 @@ package main import ( + "encoding/json" "fmt" "io/ioutil" "os" @@ -32,6 +33,7 @@ import ( "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/overlord/state" @@ -79,6 +81,8 @@ var ( secbootUnlockVolumeUsingSealedKeyIfEncrypted func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) secbootUnlockEncryptedVolumeUsingKey func(disk disks.Disk, name string, key []byte) (string, error) + secbootLockTPMSealedKeys func() error + bootFindPartitionUUIDForBootedKernelDisk = boot.FindPartitionUUIDForBootedKernelDisk ) @@ -234,6 +238,18 @@ func copyUbuntuDataAuth(src, dst string) error { return nil } +// copySafeDefaultData will copy to the destination a "safe" set of data for +// a blank recover mode, i.e. one where we cannot copy authentication, etc. from +// the actual host ubuntu-data. Currently this is just a file to disable +// console-conf from running. +func copySafeDefaultData(dst string) error { + consoleConfCompleteFile := filepath.Join(dst, "system-data/var/lib/console-conf/complete") + if err := os.MkdirAll(filepath.Dir(consoleConfCompleteFile), 0755); err != nil { + return err + } + return ioutil.WriteFile(consoleConfCompleteFile, nil, 0644) +} + func copyFromGlobHelper(src, dst, globEx string) error { matches, err := filepath.Glob(filepath.Join(src, globEx)) if err != nil { @@ -269,29 +285,406 @@ func copyFromGlobHelper(src, dst, globEx string) error { return nil } -func generateMountsModeRecover(mst *initramfsMountsState) error { - // steps 1 and 2 are shared with install mode - if err := generateMountsCommonInstallRecover(mst); err != nil { +// states for partition state +const ( + // states for LocateState + partitionFound = "found" + partitionNotFound = "not-found" + partitionErrFinding = "error-finding" + // states for MountState + partitionMounted = "mounted" + partitionErrMounting = "error-mounting" + partitionAbsentOptional = "absent-but-optional" + // states for UnlockState + partitionUnlocked = "unlocked" + partitionErrUnlocking = "error-unlocking" + // keys used to unlock for UnlockKey + keyRun = "run" + keyFallback = "fallback" + keyRecovery = "recovery" +) + +// partitionState is the state of a partition after recover mode has completed +// for degraded mode. +type partitionState struct { + // MountState is whether the partition was mounted successfully or not. + MountState string `json:"mount-state,omitempty"` + // MountLocation is where the partition was mounted. + MountLocation string `json:"mount-location,omitempty"` + // Device is what device the partition corresponds to. + Device string `json:"device,omitempty"` + // FindState indicates whether the partition was found on the disk or not. + FindState string `json:"find-state,omitempty"` + // UnlockState was whether the partition was unlocked successfully or not. + UnlockState string `json:"unlock-state,omitempty"` + // UnlockKey was what key the partition was unlocked with, either "run", + // "fallback" or "recovery". + UnlockKey string `json:"unlock-key,omitempty"` +} + +type recoverDegradedState struct { + // UbuntuData is the state of the ubuntu-data (or ubuntu-data-enc) + // partition. + UbuntuData partitionState `json:"ubuntu-data,omitempty"` + // UbuntuBoot is the state of the ubuntu-boot partition. + UbuntuBoot partitionState `json:"ubuntu-boot,omitempty"` + // UbuntuSave is the state of the ubuntu-save (or ubuntu-save-enc) + // partition. + UbuntuSave partitionState `json:"ubuntu-save,omitempty"` + // ErrorLog is the log of error messages encountered during recover mode + // setting up degraded mode. + ErrorLog []string `json:"error-log"` +} + +func (r *recoverDegradedState) partition(part string) *partitionState { + switch part { + case "ubuntu-data": + return &r.UbuntuData + case "ubuntu-boot": + return &r.UbuntuBoot + case "ubuntu-save": + return &r.UbuntuSave + } + panic(fmt.Sprintf("unknown partition %s", part)) +} + +func (r *recoverDegradedState) LogErrorf(format string, v ...interface{}) { + msg := fmt.Sprintf(format, v...) + r.ErrorLog = append(r.ErrorLog, msg) + logger.Noticef(msg) +} + +// stateFunc is a function which executes a state action, returns the next +// function (for the next) state or nil if it is the final state. +type stateFunc func() (stateFunc, error) + +// stateMachine is a state machine implementing the logic for degraded recover +// mode. the following state diagram shows the logic for the various states and +// transitions: +/** + + +TODO: this state diagram actually is missing a state transition from +"unlock save w/ run key" to "locate unencrypted save" (which is a state that is +missing from this diagram), and then from "locate unencrypted save" to either +"done" or "mount save" states + + + +---------+ +----------+ + | start | | mount | fail + | +------------------->+ boot +------------------------+ + | | | | | + +---------+ +----+-----+ | + | | + success | | + | | + v v + fail or +-------------------+ fail, +----+------+ fail, +--------+-------+ + not needed | locate save | unencrypt |unlock data| encrypted | unlock data w/ | + +--------------+ unencrypted +<-----------+w/ run key +--------------+ fallback key +-------+ + | | | | | | | | + | +--------+----------+ +-----+-----+ +--------+-------+ | + | | | | | + | |success |success | | + | | | success | fail | + v v v | | ++---+---+ +-------+----+ +-------+----+ | | +| | | mount | success | mount data | | | +| done +<----------+ save | +---------+ +<---------------------------+ | +| | | | | | | | ++--+----+ +----+-------+ | +----------+-+ | + ^ ^ | | | + | | success v | | + | | +--------+----+ fail |fail | + | | | unlock save +--------+ | | + | +-----+ w/ run key | v v | + | ^ +-------------+ +----+------+-----+ | + | | | unlock save | | + | | | w/ fallback key +----------------------------------------+ + | +-----------------------+ | + | success +-------+---------+ + | | + | | + | | + +-----------------------------------------------------+ + fail + +*/ + +type stateMachine struct { + // the current state is the one that is about to be executed + current stateFunc + + // the disk we have all our partitions on + disk disks.Disk + + isEncryptedDev bool + + // state for tracking what happens as we progress through degraded mode of + // recovery + degradedState *recoverDegradedState +} + +// degraded returns whether a degraded recover mode state has fallen back from +// the typical operation to some sort of degraded mode. +func (m *stateMachine) degraded() bool { + r := m.degradedState + + if m.isEncryptedDev { + // for encrypted devices, we need to have ubuntu-save mounted + if r.UbuntuSave.MountState != partitionMounted { + return true + } + + // we also should have all the unlock keys as run keys + if r.UbuntuData.UnlockKey != keyRun { + return true + } + + if r.UbuntuSave.UnlockKey != keyRun { + return true + } + } else { + // for unencrypted devices, ubuntu-save must either be mounted or + // absent-but-optional + if r.UbuntuSave.MountState != partitionMounted { + if r.UbuntuSave.MountState != partitionAbsentOptional { + return true + } + } + } + + // ubuntu-boot and ubuntu-data should both be mounted + if r.UbuntuBoot.MountState != partitionMounted { + return true + } + if r.UbuntuData.MountState != partitionMounted { + return true + } + + // TODO: should we also check MountLocation too? + + // we should have nothing in the error log + if len(r.ErrorLog) != 0 { + return true + } + + return false +} + +func (m *stateMachine) diskOpts() *disks.Options { + if m.isEncryptedDev { + return &disks.Options{ + IsDecryptedDevice: true, + } + } + return nil +} + +func (m *stateMachine) verifyMountPoint(dir, name string) error { + matches, err := m.disk.MountPointIsFromDisk(dir, m.diskOpts()) + if err != nil { return err } + if !matches { + 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()) + } + return nil +} - // get the disk that we mounted the ubuntu-seed partition from as a - // reference point for future mounts - disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil) +func (m *stateMachine) setFindState(part, partUUID string, err error, logNotFoundErr bool) error { + if err == nil { + // device was found + part := m.degradedState.partition(part) + part.FindState = partitionFound + part.Device = fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID) + return nil + } + if _, ok := err.(disks.FilesystemLabelNotFoundError); ok { + // explicit error that the device was not found + m.degradedState.partition(part).FindState = partitionNotFound + if logNotFoundErr { + m.degradedState.LogErrorf("cannot find %v partition on disk %s", part, m.disk.Dev()) + } + return nil + } + // the error is not "not-found", so we have a real error + m.degradedState.partition(part).FindState = partitionErrFinding + m.degradedState.LogErrorf("error finding %v partition on disk %s: %v", part, m.disk.Dev(), err) + return nil +} + +func (m *stateMachine) setMountState(part, where string, err error) error { if err != nil { + m.degradedState.LogErrorf("cannot mount %v: %v", part, err) + m.degradedState.partition(part).MountState = partitionErrMounting + return nil + } + + m.degradedState.partition(part).MountState = partitionMounted + m.degradedState.partition(part).MountLocation = where + + if err := m.verifyMountPoint(where, part); err != nil { + m.degradedState.LogErrorf("cannot verify %s mount point at %v: %v", + part, where, err) return err } + return nil +} - // 2.X mount ubuntu-boot for access to the run mode key to unseal - // ubuntu-data +func (m *stateMachine) setUnlockStateWithRunKey(partName string, unlockRes secboot.UnlockResult, err error) error { + part := m.degradedState.partition(partName) + // save the device if we found it from secboot + if unlockRes.Device != "" { + part.FindState = partitionFound + part.Device = unlockRes.Device + } else { + part.FindState = partitionNotFound + } + if unlockRes.IsDecryptedDevice { + // if the unlock result deduced we have a decrypted device, save that + m.isEncryptedDev = true + } + + if err != nil { + // create different error message for encrypted vs unencrypted + if unlockRes.IsDecryptedDevice { + devStr := partName + if unlockRes.Device != "" { + devStr += fmt.Sprintf(" (device %s)", unlockRes.Device) + } + m.degradedState.LogErrorf("cannot unlock encrypted %s with sealed run key: %v", devStr, err) + part.UnlockState = partitionErrUnlocking + + } else { + // TODO: we don't know if this is a plain not found or a different error + m.degradedState.LogErrorf("cannot locate %s partition for mounting host data: %v", part, err) + } + + return nil + } + + if unlockRes.IsDecryptedDevice { + part.UnlockState = partitionUnlocked + part.UnlockKey = keyRun + } + + return nil +} + +func (m *stateMachine) setUnlockStateWithFallbackKey(partName string, unlockRes secboot.UnlockResult, err error) error { + part := m.degradedState.partition(partName) + + // first check the result and error for consistency; since we are using udev + // there could be inconsistent results at different points in time + // TODO: when we refactor UnlockVolumeUsingSealedKeyIfEncrypted to not also + // find the partition on the disk, we should eliminate this + // consistency checking as we can code it such that we don't get these + // possible inconsistencies + // ensure consistency between encrypted state of the device/disk and what we + // may have seen previously + if m.isEncryptedDev && !unlockRes.IsDecryptedDevice { + // then we previously were able to positively identify an + // ubuntu-data-enc but can't anymore, so we have inconsistent results + // from inspecting the disk which is suspicious and we should fail + return fmt.Errorf("inconsistent disk encryption status: previous access resulted in encrypted, but now is unencrypted from partition %s", partName) + } + + // if isEncryptedDev hasn't been set on the state machine yet, then set that + // on the state machine before continuing - this is okay because we might + // not have been able to do anything with ubuntu-data if we couldn't mount + // ubuntu-boot, so this might be the first time we tried to unlock + // ubuntu-data and m.isEncryptedDev may have the default value of false + if !m.isEncryptedDev && unlockRes.IsDecryptedDevice { + m.isEncryptedDev = unlockRes.IsDecryptedDevice + } + + // also make sure that if we previously saw a device that we see the same + // device again + if unlockRes.Device != "" && part.Device != "" && unlockRes.Device != part.Device { + return fmt.Errorf("inconsistent partitions found for %s: previously found %s but now found %s", partName, part.Device, unlockRes.Device) + } + + if unlockRes.Device != "" { + part.FindState = partitionFound + part.Device = unlockRes.Device + } + + if !unlockRes.IsDecryptedDevice && unlockRes.Device != "" && err != nil { + // this case should be impossible to enter, if we have an unencrypted + // device and we know what the device is then what is the error? + return fmt.Errorf("internal error: inconsistent return values from UnlockVolumeUsingSealedKeyIfEncrypted for partition %s", partName) + } + + if err != nil { + // create different error message for encrypted vs unencrypted + if m.isEncryptedDev { + m.degradedState.LogErrorf("cannot unlock encrypted %s partition with sealed fallback key: %v", partName, err) + part.UnlockState = partitionErrUnlocking + } else { + // if we don't have an encrypted device and err != nil, then the + // device must be not-found, see above checks + m.degradedState.LogErrorf("cannot locate %s partition: %v", partName, err) + } + + return nil + } + + if m.isEncryptedDev { + part.UnlockState = partitionUnlocked + + // figure out which key/method we used to unlock the partition + switch unlockRes.UnlockMethod { + case secboot.UnlockedWithSealedKey: + part.UnlockKey = keyFallback + case secboot.UnlockedWithRecoveryKey: + part.UnlockKey = keyRecovery + + // TODO: should we fail with internal error for default case here? + } + } + + return nil +} + +func newStateMachine(disk disks.Disk) *stateMachine { + m := &stateMachine{ + disk: disk, + degradedState: &recoverDegradedState{ + ErrorLog: []string{}, + }, + } + // first step is to mount ubuntu-boot to check for run mode keys to unlock + // ubuntu-data + m.current = m.mountBoot + return m +} + +func (m *stateMachine) execute() (finished bool, err error) { + next, err := m.current() + m.current = next + return next == nil, err +} + +// mountBoot is the first state to execute in the state machine, it can +// transition to the following states: +// - if ubuntu-boot is mounted successfully, execute unlockDataRunKey +// - if ubuntu-boot can't be mounted, execute unlockDataFallbackKey +// - if we mounted the wrong ubuntu-boot (or otherwise can't verify which one we +// mounted), return fatal error +func (m *stateMachine) mountBoot() (stateFunc, error) { + part := m.degradedState.partition("ubuntu-boot") // use the disk we mounted ubuntu-seed from as a reference to find // ubuntu-seed and mount it - // TODO: w/ degraded mode we need to be robust against not being able to - // find/mount ubuntu-boot and fallback to using keys from ubuntu-seed in - // that case - partUUID, err := disk.FindMatchingPartitionUUID("ubuntu-boot") - if err != nil { - return err + partUUID, findErr := m.disk.FindMatchingPartitionUUID("ubuntu-boot") + if err := m.setFindState("ubuntu-boot", partUUID, findErr, true); err != nil { + return nil, err + } + if part.FindState != partitionFound { + // if we didn't find ubuntu-boot, we can't try to unlock data with the + // run key, and should instead just jump straight to attempting to + // unlock with the fallback key + return m.unlockDataFallbackKey, nil } // should we fsck ubuntu-boot? probably yes because on some platforms @@ -301,65 +694,272 @@ func generateMountsModeRecover(mst *initramfsMountsState) error { fsckSystemdOpts := &systemdMountOptions{ NeedsFsck: true, } - if err := doSystemdMount(fmt.Sprintf("/dev/disk/by-partuuid/%s", partUUID), boot.InitramfsUbuntuBootDir, fsckSystemdOpts); err != nil { - return err + mountErr := doSystemdMount(part.Device, boot.InitramfsUbuntuBootDir, fsckSystemdOpts) + if err := m.setMountState("ubuntu-boot", boot.InitramfsUbuntuBootDir, mountErr); err != nil { + return nil, err } - - // 2.X+1, verify ubuntu-boot comes from same disk as ubuntu-seed - matches, err := disk.MountPointIsFromDisk(boot.InitramfsUbuntuBootDir, nil) - if err != nil { - return err - } - if !matches { - return fmt.Errorf("cannot validate boot: ubuntu-boot mountpoint is expected to be from disk %s but is not", disk.Dev()) + if part.MountState == partitionErrMounting { + // if we didn't mount data, then try to unlock data with the + // fallback key + return m.unlockDataFallbackKey, nil } - // 3. mount ubuntu-data for recovery using run mode key + // next step try to unlock data with run object + return m.unlockDataRunKey, nil +} + +// stateUnlockDataRunKey will try to unlock ubuntu-data with the normal run-mode +// key, and if it fails, progresses to the next state, which is either: +// - failed to unlock data, but we know it's an encrypted device -> try to unlock with fallback key +// - failed to find data at all -> try to unlock save +// - unlocked data with run key -> mount data +func (m *stateMachine) unlockDataRunKey() (stateFunc, error) { + // TODO: don't allow recovery key at all for this invocation, we only allow + // recovery key to be used after we try the fallback key runModeKey := filepath.Join(boot.InitramfsBootEncryptionKeyDir, "ubuntu-data.sealed-key") - opts := &secboot.UnlockVolumeUsingSealedKeyOptions{ - LockKeysOnFinish: true, + unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + // don't allow using the recovery key to unlock, we only try using the + // recovery key after we first try the fallback object + AllowRecoveryKey: false, + // don't lock keys, we manually do that at the end always, we don't know + // if this call to unlock a volume will be the last one or not + LockKeysOnFinish: false, + } + unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", runModeKey, unlockOpts) + if err := m.setUnlockStateWithRunKey("ubuntu-data", unlockRes, unlockErr); err != nil { + return nil, err + } + if unlockErr != nil { + // we couldn't unlock ubuntu-data with the primary key, or we didn't + // find it in the unencrypted case + if unlockRes.IsDecryptedDevice { + // we know the device is encrypted, so the next state is to try + // unlocking with the fallback key + return m.unlockDataFallbackKey, nil + } + + // not an encrypted device, so nothing to fall back to try and unlock + // data, so just mark it as not found and continue on to try and mount + // an unencrypted ubuntu-save directly + return m.locateUnencryptedSave, nil + } + + // otherwise successfully unlocked it (or just found it if it was unencrypted) + // so just mount it + return m.mountData, nil +} + +func (m *stateMachine) unlockDataFallbackKey() (stateFunc, error) { + // try to unlock data with the fallback key on ubuntu-seed, which must have + // been mounted at this point + unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + // we want to allow using the recovery key if the fallback key fails as + // using the fallback object is the last chance before we give up trying + // to unlock data AllowRecoveryKey: true, + // don't lock keys, we manually do that at the end always, we don't know + // if this call to unlock a volume will be the last one or not + LockKeysOnFinish: false, + } + // TODO: this prompts for a recovery key + // TODO: we should somehow customize the prompt to mention what key we need + // the user to enter, and what we are unlocking (as currently the prompt + // says "recovery key" and the partition UUID for what is being unlocked) + dataFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-data.recovery.sealed-key") + unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-data", dataFallbackKey, unlockOpts) + if err := m.setUnlockStateWithFallbackKey("ubuntu-data", unlockRes, unlockErr); err != nil { + return nil, err + } + if unlockErr != nil { + // skip trying to mount data, since we did not unlock data we cannot + // open save with with the run key, so try the fallback one + return m.unlockSaveFallbackKey, nil + } + + // unlocked it, now go mount it + return m.mountData, nil +} + +func (m *stateMachine) mountData() (stateFunc, error) { + data := m.degradedState.partition("ubuntu-data") + // don't do fsck on the data partition, it could be corrupted + mountErr := doSystemdMount(data.Device, boot.InitramfsHostUbuntuDataDir, nil) + if err := m.setMountState("ubuntu-data", boot.InitramfsHostUbuntuDataDir, mountErr); err != nil { + return nil, err } - unlockRes, err := secbootUnlockVolumeUsingSealedKeyIfEncrypted(disk, "ubuntu-data", runModeKey, opts) + if data.MountState == partitionErrMounting { + // no point trying to unlock save with the run key, we need data to be + // mounted for that and we failed to mount it + return m.unlockSaveFallbackKey, nil + } + + // next step: try to unlock with run save key if we are encrypted + if m.isEncryptedDev { + return m.unlockSaveRunKey, nil + } + + // if we are unencrypted just try to find unencrypted ubuntu-save and then + // maybe mount it + return m.locateUnencryptedSave, nil +} + +func (m *stateMachine) locateUnencryptedSave() (stateFunc, error) { + part := m.degradedState.partition("ubuntu-save") + partUUID, findErr := m.disk.FindMatchingPartitionUUID("ubuntu-save") + if err := m.setFindState("ubuntu-save", partUUID, findErr, false); err != nil { + return nil, nil + } + if part.FindState != partitionFound { + if part.FindState == partitionNotFound { + // this is ok, ubuntu-save may not exist for + // non-encrypted device + part.MountState = partitionAbsentOptional + } + // all done, nothing left to try and mount, even if errors + // occurred + return nil, nil + } + + // we found the unencrypted device, now mount it + return m.mountSave, nil +} + +func (m *stateMachine) unlockSaveRunKey() (stateFunc, error) { + // to get to this state, we needed to have mounted ubuntu-data on host, so + // if encrypted, we can try to read the run key from host ubuntu-data + saveKey := filepath.Join(dirs.SnapFDEDirUnder(boot.InitramfsHostWritableDir), "ubuntu-save.key") + key, err := ioutil.ReadFile(saveKey) if err != nil { - return err + // log the error and skip to trying the fallback key + m.degradedState.LogErrorf("cannot access run ubuntu-save key: %v", err) + return m.unlockSaveFallbackKey, nil } - // don't do fsck on the data partition, it could be corrupted - if err := doSystemdMount(unlockRes.Device, boot.InitramfsHostUbuntuDataDir, nil); err != nil { + saveDevice, unlockErr := secbootUnlockEncryptedVolumeUsingKey(m.disk, "ubuntu-save", key) + // TODO:UC20: UnlockEncryptedVolumeUsingKey should return an UnlockResult, + // but until then we create our own and pass it along + unlockRes := secboot.UnlockResult{ + Device: saveDevice, + IsDecryptedDevice: true, + } + if err := m.setUnlockStateWithRunKey("ubuntu-save", unlockRes, unlockErr); err != nil { + return nil, err + } + if unlockErr != nil { + // failed to unlock with run key, try fallback key + return m.unlockSaveFallbackKey, nil + } + + // unlocked it properly, go mount it + return m.mountSave, nil +} + +func (m *stateMachine) unlockSaveFallbackKey() (stateFunc, error) { + // try to unlock save with the fallback key on ubuntu-seed, which must have + // been mounted at this point + unlockOpts := &secboot.UnlockVolumeUsingSealedKeyOptions{ + // we want to allow using the recovery key if the fallback key fails as + // using the fallback object is the last chance before we give up trying + // to unlock save + AllowRecoveryKey: true, + // while this is technically always the last call to unlock the volume + // if we get here, to keep things simple we just always lock after + // running the state machine so don't lock keys here + LockKeysOnFinish: false, + } + saveFallbackKey := filepath.Join(boot.InitramfsSeedEncryptionKeyDir, "ubuntu-save.recovery.sealed-key") + // TODO: this prompts again for a recover key, but really this is the + // reinstall key we will prompt for + // TODO: we should somehow customize the prompt to mention what key we need + // the user to enter, and what we are unlocking (as currently the prompt + // says "recovery key" and the partition UUID for what is being unlocked) + unlockRes, unlockErr := secbootUnlockVolumeUsingSealedKeyIfEncrypted(m.disk, "ubuntu-save", saveFallbackKey, unlockOpts) + if err := m.setUnlockStateWithFallbackKey("ubuntu-save", unlockRes, unlockErr); err != nil { + return nil, err + } + if unlockErr != nil { + // all done, nothing left to try and mount, everything failed + return nil, nil + } + + // otherwise we unlocked it, so go mount it + return m.mountSave, nil +} + +func (m *stateMachine) mountSave() (stateFunc, error) { + saveDev := m.degradedState.partition("ubuntu-save").Device + // TODO: should we fsck ubuntu-save ? + mountErr := doSystemdMount(saveDev, boot.InitramfsUbuntuSaveDir, nil) + if err := m.setMountState("ubuntu-save", boot.InitramfsUbuntuSaveDir, mountErr); err != nil { + return nil, err + } + // all done, nothing left to try and mount + return nil, nil +} + +func generateMountsModeRecover(mst *initramfsMountsState) error { + // steps 1 and 2 are shared with install mode + if err := generateMountsCommonInstallRecover(mst); err != nil { return err } - // 3.1. mount ubuntu-save (if present) - haveSave, err := maybeMountSave(disk, boot.InitramfsHostWritableDir, unlockRes.IsDecryptedDevice, nil) + // get the disk that we mounted the ubuntu-seed partition from as a + // reference point for future mounts + disk, err := disks.DiskFromMountPoint(boot.InitramfsUbuntuSeedDir, nil) if err != nil { return err } - // 3.2 verify that the host ubuntu-data comes from where we expect it to - // right device - diskOpts := &disks.Options{} - if unlockRes.IsDecryptedDevice { - // then we need to specify that the data mountpoint is expected to be a - // decrypted device, applies to both ubuntu-data and ubuntu-save - diskOpts.IsDecryptedDevice = true - } + // 3. run the state machine logic for mounting partitions, this involves + // trying to unlock then mount ubuntu-data, and then unlocking and + // mounting ubuntu-save + // see the state* functions for details of what each step does and + // possible transition points + + machine, err := func() (machine *stateMachine, err error) { + // ensure that the last thing we do after mounting everything is to lock + // access to sealed keys + defer func() { + if err := secbootLockTPMSealedKeys(); err != nil { + logger.Noticef("error locking access to sealed keys: %v", err) + } + }() - matches, err = disk.MountPointIsFromDisk(boot.InitramfsHostUbuntuDataDir, diskOpts) + // first state to execute is to unlock ubuntu-data with the run key + machine = newStateMachine(disk) + for { + final, err := machine.execute() + // TODO: consider whether certain errors are fatal or not + if err != nil { + return nil, err + } + if final { + break + } + } + + return machine, nil + }() if err != nil { return err } - if !matches { - return fmt.Errorf("cannot validate boot: ubuntu-data mountpoint is expected to be from disk %s but is not", disk.Dev()) - } - if haveSave { - // 3.2a we have ubuntu-save, verify it as well - matches, err = disk.MountPointIsFromDisk(boot.InitramfsUbuntuSaveDir, diskOpts) + + // 3.1 write out degraded.json if we ended up falling back somewhere + if machine.degraded() { + b, err := json.Marshal(machine.degradedState) if err != nil { return err } - if !matches { - return fmt.Errorf("cannot validate boot: ubuntu-save mountpoint is expected to be from disk %s but is not", disk.Dev()) + + err = os.MkdirAll(dirs.SnapBootstrapRunDir, 0755) + if err != nil { + return err + } + + // leave the information about degraded state at an ephemeral location + err = ioutil.WriteFile(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), b, 0644) + if err != nil { + return err } } @@ -367,14 +967,32 @@ func generateMountsModeRecover(mst *initramfsMountsState) error { // the real ubuntu-data dir to the ephemeral ubuntu-data // dir, write the modeenv to the tmpfs data, and disable // cloud-init in recover mode - if err := copyUbuntuDataAuth(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { - return err - } - if err := copyNetworkConfig(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { - return err - } - if err := copyUbuntuDataMisc(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { - return err + + // if we have the host location, then we were able to successfully mount + // ubuntu-data, and as such we can proceed with copying files from there + // onto the tmpfs + if machine.degradedState.partition("ubuntu-data").MountLocation != "" { + // TODO: erroring here should fallback to copySafeDefaultData and + // proceed on with degraded mode anyways + if err := copyUbuntuDataAuth(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { + return err + } + if err := copyNetworkConfig(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { + return err + } + if err := copyUbuntuDataMisc(boot.InitramfsHostUbuntuDataDir, boot.InitramfsDataDir); err != nil { + return err + } + } else { + // we don't have ubuntu-data host mountpoint, so we should setup safe + // defaults for i.e. console-conf in the running image to block + // attackers from accessing the system - just because we can't access + // ubuntu-data doesn't mean that attackers wouldn't be able to if they + // could login + + if err := copySafeDefaultData(boot.InitramfsHostUbuntuDataDir); err != nil { + return err + } } modeEnv := &boot.Modeenv{ diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go index 1655533446..b9f424f0cf 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go @@ -39,10 +39,14 @@ func init() { secbootMeasureSnapModelWhenPossible = func(_ func() (*asserts.Model, error)) error { return errNotImplemented } - secbootUnlockVolumeUsingSealedKeyIfEncrypted = func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + secbootUnlockVolumeUsingSealedKeyIfEncrypted = func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { return secboot.UnlockResult{}, errNotImplemented } secbootUnlockEncryptedVolumeUsingKey = func(disk disks.Disk, name string, key []byte) (string, error) { return "", errNotImplemented } + + secbootLockTPMSealedKeys = func() error { + return errNotImplemented + } } diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go new file mode 100644 index 0000000000..59767ef599 --- /dev/null +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go @@ -0,0 +1,292 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main_test + +import ( + . "gopkg.in/check.v1" + + main "github.com/snapcore/snapd/cmd/snap-bootstrap" +) + +func (s *initramfsMountsSuite) TestInitramfsDegradedState(c *C) { + tt := []struct { + r main.RecoverDegradedState + encrypted bool + degraded bool + comment string + }{ + // unencrypted happy + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + }, + UbuntuSave: main.PartitionState{ + MountState: "absent-but-optional", + }, + }, + degraded: false, + comment: "happy unencrypted no save", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + }, + }, + degraded: false, + comment: "happy unencrypted save", + }, + // unencrypted unhappy + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "error-mounting", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + }, + UbuntuSave: main.PartitionState{ + MountState: "absent-but-optional", + }, + ErrorLog: []string{ + "cannot find ubuntu-boot partition on disk 259:0", + }, + }, + degraded: true, + comment: "unencrypted, error mounting boot", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "error-mounting", + }, + UbuntuSave: main.PartitionState{ + MountState: "absent-but-optional", + }, + ErrorLog: []string{ + "cannot find ubuntu-data partition on disk 259:0", + }, + }, + degraded: true, + comment: "unencrypted, error mounting data", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + }, + UbuntuSave: main.PartitionState{ + MountState: "error-mounting", + }, + ErrorLog: []string{ + "cannot find ubuntu-save partition on disk 259:0", + }, + }, + degraded: true, + comment: "unencrypted, error mounting save", + }, + + // encrypted happy + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + }, + encrypted: true, + degraded: false, + comment: "happy encrypted", + }, + // encrypted unhappy + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "error-mounting", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + ErrorLog: []string{ + "cannot find ubuntu-boot partition on disk 259:0", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, no boot, fallback data", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-data with sealed run key: failed to unlock ubuntu-data", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, fallback data", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-save with sealed run key: failed to unlock ubuntu-save", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, fallback save", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "run", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "recovery", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-save with sealed run key: failed to unlock ubuntu-save", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, recovery save", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + UbuntuSave: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-data with sealed run key: failed to unlock ubuntu-data", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, fallback data, fallback save", + }, + { + r: main.RecoverDegradedState{ + UbuntuBoot: main.PartitionState{ + MountState: "mounted", + }, + UbuntuData: main.PartitionState{ + MountState: "mounted", + UnlockState: "unlocked", + UnlockKey: "fallback", + }, + UbuntuSave: main.PartitionState{ + MountState: "not-mounted", + UnlockState: "not-unlocked", + }, + ErrorLog: []string{ + "cannot unlock encrypted ubuntu-save with sealed run key: failed to unlock ubuntu-save", + "cannot unlock encrypted ubuntu-save with sealed fallback key: failed to unlock ubuntu-save", + }, + }, + encrypted: true, + degraded: true, + comment: "encrypted, fallback data, no save", + }, + } + + for _, t := range tt { + var comment CommentInterface + if t.comment != "" { + comment = Commentf(t.comment) + } + + c.Assert(t.r.Degraded(t.encrypted), Equals, t.degraded, comment) + } +} diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go index 2facb089dd..80a7d61aaa 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go @@ -29,4 +29,5 @@ func init() { secbootMeasureSnapModelWhenPossible = secboot.MeasureSnapModelWhenPossible secbootUnlockVolumeUsingSealedKeyIfEncrypted = secboot.UnlockVolumeUsingSealedKeyIfEncrypted secbootUnlockEncryptedVolumeUsingKey = secboot.UnlockEncryptedVolumeUsingKey + secbootLockTPMSealedKeys = secboot.LockTPMSealedKeys } diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go index 8a76740744..a841289052 100644 --- a/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go +++ b/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go @@ -21,6 +21,7 @@ package main_test import ( "bytes" + "encoding/json" "fmt" "io/ioutil" "os" @@ -84,10 +85,6 @@ var ( // a boot disk without ubuntu-save defaultBootDisk = &disks.MockDiskMapping{ FilesystemLabelToPartUUID: map[string]string{ - // ubuntu-boot not strictly necessary, since we mount it first we - // don't go looking for the label ubuntu-boot on a disk, we just - // mount it and hope it's what we need, unless we have UEFI vars or - // something "ubuntu-boot": "ubuntu-boot-partuuid", "ubuntu-seed": "ubuntu-seed-partuuid", "ubuntu-data": "ubuntu-data-partuuid", @@ -98,10 +95,6 @@ var ( defaultBootWithSaveDisk = &disks.MockDiskMapping{ FilesystemLabelToPartUUID: map[string]string{ - // ubuntu-boot not strictly necessary, since we mount it first we - // don't go looking for the label ubuntu-boot on a disk, we just - // mount it and hope it's what we need, unless we have UEFI vars or - // something "ubuntu-boot": "ubuntu-boot-partuuid", "ubuntu-seed": "ubuntu-seed-partuuid", "ubuntu-data": "ubuntu-data-partuuid", @@ -113,10 +106,6 @@ var ( defaultEncBootDisk = &disks.MockDiskMapping{ FilesystemLabelToPartUUID: map[string]string{ - // ubuntu-boot not strictly necessary, since we mount it first we - // don't ever search a particular disk for the ubuntu-boot label, - // we just mount it and hope it's what we need, unless we have UEFI - // vars or something a la boot.PartitionUUIDForBootedKernelDisk "ubuntu-boot": "ubuntu-boot-partuuid", "ubuntu-seed": "ubuntu-seed-partuuid", "ubuntu-data-enc": "ubuntu-data-enc-partuuid", @@ -227,7 +216,7 @@ func (s *initramfsMountsSuite) SetUpTest(c *C) { c.Check(f, NotNil) return nil })) - s.AddCleanup(main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + s.AddCleanup(main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { return secboot.UnlockResult{Device: filepath.Join("/dev/disk/by-partuuid", name+"-partuuid")}, nil })) } @@ -944,6 +933,9 @@ After=%[1]s "--fsck=no", }, }) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeWithSaveHappyRealSystemdMount(c *C) { @@ -1086,6 +1078,9 @@ After=%[1]s "--fsck=no", }, }) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } func (s *initramfsMountsSuite) TestInitramfsMountsRunModeHappyNoSaveRealSystemdMount(c *C) { @@ -1398,6 +1393,9 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeFirstBootRecoverySystem _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) c.Assert(err, IsNil) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } func (s *initramfsMountsSuite) TestInitramfsMountsRunModeWithBootedKernelPartUUIDHappy(c *C) { @@ -1491,13 +1489,14 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataHappy(c *C c.Assert(err, IsNil) dataActivated := false - restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { c.Assert(name, Equals, "ubuntu-data") - c.Assert(encryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ LockKeysOnFinish: true, AllowRecoveryKey: true, }) + dataActivated = true // return true because we are using an encrypted device return secboot.UnlockResult{ @@ -1605,7 +1604,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataUnhappyNoS defer restore() dataActivated := false - restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { c.Assert(name, Equals, "ubuntu-data") dataActivated = true // return true because we are using an encrypted device @@ -1679,7 +1678,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataUnhappyUnl defer restore() dataActivated := false - restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { c.Assert(name, Equals, "ubuntu-data") dataActivated = true // return true because we are using an encrypted device @@ -2129,6 +2128,14 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeUpgradeScenarios(c *C) } func (s *initramfsMountsSuite) testRecoverModeHappy(c *C) { + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore := main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + // mock various files that are copied around during recover mode (and files // that shouldn't be copied around) ephemeralUbuntuData := filepath.Join(boot.InitramfsRunMntDir, "data/") @@ -2189,6 +2196,9 @@ func (s *initramfsMountsSuite) testRecoverModeHappy(c *C) { _, err = main.Parser().ParseArgs([]string{"initramfs-mounts"}) c.Assert(err, IsNil) + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + modeEnv := filepath.Join(ephemeralUbuntuData, "/system-data/var/lib/snapd/modeenv") c.Check(modeEnv, testutil.FileEquals, `mode=recover recovery_system=20191118 @@ -2273,6 +2283,9 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappy(c *C) { defer restore() s.testRecoverModeHappy(c) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeGadgetDefaultsHappy(c *C) { @@ -2352,6 +2365,9 @@ defaults: s.testRecoverModeHappy(c) + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) + c.Assert(osutil.FileExists(filepath.Join(boot.InitramfsWritableDir, "_writable_defaults/etc/cloud/cloud-init.disabled")), Equals, true) // check that everything from the gadget defaults was setup @@ -2418,6 +2434,9 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyBootedKernelPa defer restore() s.testRecoverModeHappy(c) + + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) } func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyEncrypted(c *C) { @@ -2448,16 +2467,14 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyEncrypted(c *C defer restore() dataActivated := false - restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { c.Assert(name, Equals, "ubuntu-data") - c.Assert(encryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") c.Assert(err, IsNil) c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") - c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ - LockKeysOnFinish: true, - AllowRecoveryKey: true, - }) + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) dataActivated = true return secboot.UnlockResult{ Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), @@ -2530,7 +2547,190 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyEncrypted(c *C s.testRecoverModeHappy(c) + // we should not have written a degraded.json + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json"), testutil.FileAbsent) + + c.Check(dataActivated, Equals, true) + c.Check(saveActivated, Equals, true) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func checkDegradedJSON(c *C, exp map[string]interface{}) { + b, err := ioutil.ReadFile(filepath.Join(dirs.SnapBootstrapRunDir, "degraded.json")) + c.Assert(err, IsNil) + degradedJSONObj := make(map[string]interface{}, 0) + err = json.Unmarshal(b, °radedJSONObj) + c.Assert(err, IsNil) + + c.Assert(degradedJSONObj, DeepEquals, exp) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedFallbackDataHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivated := false + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // pretend we can't unlock ubuntu-data with the main run key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-data") + + case 2: + // now we can unlock ubuntu-data with the fallback key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + return secboot.UnlockResult{ + Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), + IsDecryptedDevice: true, + UnlockMethod: secboot.UnlockedWithSealedKey, + }, nil + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + saveActivated = true + return filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), nil + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + s.testRecoverModeHappy(c) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "find-state": "found", + "mount-state": "mounted", + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted", + "unlock-key": "fallback", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + "unlock-key": "run", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-data with sealed run key: failed to unlock ubuntu-data", + }, + }) + c.Check(dataActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 2) c.Check(saveActivated, Equals, true) c.Check(measureEpochCalls, Equals, 1) c.Check(measureModelCalls, Equals, 1) @@ -2540,6 +2740,1100 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyEncrypted(c *C c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) } +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedFallbackSaveHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivated := false + saveActivationAttempted := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can be unlocked fine + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivated = true + return secboot.UnlockResult{ + Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), + IsDecryptedDevice: true, + UnlockMethod: secboot.UnlockedWithSealedKey, + }, nil + + case 2: + // then after ubuntu-save is attempted to be unlocked with the + // unsealed run object on the encrypted data partition, we fall back + // to using the sealed object on ubuntu-seed for save + c.Assert(saveActivationAttempted, Equals, true) + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + return secboot.UnlockResult{ + Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), + IsDecryptedDevice: true, + UnlockMethod: secboot.UnlockedWithSealedKey, + }, nil + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + saveActivationAttempted = true + return "", fmt.Errorf("failed to unlock ubuntu-save with run object") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + s.testRecoverModeHappy(c) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "find-state": "found", + "mount-state": "mounted", + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted", + "unlock-key": "run", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + "unlock-key": "fallback", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-save with sealed run key: failed to unlock ubuntu-save with run object", + }, + }) + + c.Check(dataActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 2) + c.Check(saveActivationAttempted, Equals, true) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedNoBootDataFallbackHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + defaultEncDiskNoBoot := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + }, + DiskHasPartitions: true, + DevNum: "defaultEncDevNoBoot", + } + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncDiskNoBoot, + // no ubuntu-boot so we fall back to unlocking data with fallback + // key right away + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncDiskNoBoot, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncDiskNoBoot, + }, + ) + defer restore() + + dataActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + case 1: + // we skip trying to unlock with run key on ubuntu-boot and go + // directly to using the fallback key on ubuntu-seed + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + return secboot.UnlockResult{ + Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), + IsDecryptedDevice: true, + UnlockMethod: secboot.UnlockedWithSealedKey, + }, nil + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + return filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), nil + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + // no ubuntu-boot + { + "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + s.testRecoverModeHappy(c) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "find-state": "not-found", + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted", + "unlock-key": "fallback", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + "unlock-key": "run", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot find ubuntu-boot partition on disk defaultEncDevNoBoot", + }, + }) + + c.Check(dataActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 1) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedNoBootDataRecoveryKeyFallbackHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + defaultEncDiskNoBoot := &disks.MockDiskMapping{ + FilesystemLabelToPartUUID: map[string]string{ + "ubuntu-seed": "ubuntu-seed-partuuid", + "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + }, + DiskHasPartitions: true, + DevNum: "defaultEncDevNoBoot", + } + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncDiskNoBoot, + // no ubuntu-boot so we fall back to unlocking data with fallback + // key right away + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncDiskNoBoot, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncDiskNoBoot, + }, + ) + defer restore() + + dataActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + case 1: + // we skip trying to unlock with run key on ubuntu-boot and go + // directly to using the fallback key on ubuntu-seed + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + return secboot.UnlockResult{ + Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), + IsDecryptedDevice: true, + // it was unlocked with a recovery key + UnlockMethod: secboot.UnlockedWithRecoveryKey, + }, nil + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) { + c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not activated yet")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(key, DeepEquals, []byte("foo")) + return filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), nil + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + // no ubuntu-boot + { + "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + boot.InitramfsHostUbuntuDataDir, + nil, + }, + { + "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + s.testRecoverModeHappy(c) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "find-state": "not-found", + }, + "ubuntu-data": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid", + "unlock-state": "unlocked", + "find-state": "found", + "mount-state": "mounted", + "unlock-key": "recovery", + "mount-location": boot.InitramfsHostUbuntuDataDir, + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + "unlock-key": "run", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot find ubuntu-boot partition on disk defaultEncDevNoBoot", + }, + }) + + c.Check(dataActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 1) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedNoDataFallbackSaveHappy(c *C) { + // test a scenario when unsealing of data fails with both the run key + // and fallback key, but save can be unlocked using the fallback key + + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivationAttempts := 0 + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can't be unlocked with run key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivationAttempts++ + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-data with run object") + + case 2: + // nor can it be unlocked with fallback key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivationAttempts++ + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-data with fallback object") + + case 3: + // we can however still unlock ubuntu-save (somehow?) + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + saveActivated = true + return secboot.UnlockResult{ + Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), + IsDecryptedDevice: true, + UnlockMethod: secboot.UnlockedWithSealedKey, + }, nil + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) { + // nothing can call this function in the tested scenario + c.Fatalf("unexpected call") + return "", fmt.Errorf("unexpected call") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + modeEnv := filepath.Join(boot.InitramfsWritableDir, "var/lib/snapd/modeenv") + c.Check(modeEnv, testutil.FileEquals, `mode=recover +recovery_system=20191118 +`) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "unlock-state": "error-unlocking", + "find-state": "not-found", + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + "unlock-key": "fallback", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-data with sealed run key: failed to unlock ubuntu-data with run object", + "cannot unlock encrypted ubuntu-data partition with sealed fallback key: failed to unlock ubuntu-data with fallback object", + }, + }) + + bloader2, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + m, err := bloader2.GetBootVars("snapd_recovery_system", "snapd_recovery_mode") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_system": "20191118", + "snapd_recovery_mode": "run", + }) + + // since we didn't mount data at all, we won't have copied in files from + // there and instead will copy safe defaults to the ephemeral data + c.Assert(filepath.Join(boot.InitramfsHostWritableDir, "var/lib/console-conf/complete"), testutil.FilePresent) + + c.Check(dataActivationAttempts, Equals, 2) + c.Check(saveActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 3) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedNoDataRecoverySaveHappy(c *C) { + // test a scenario when unsealing of data fails with both the run key + // and fallback key, but save can be unlocked using the fallback key + + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivationAttempts := 0 + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can't be unlocked with run key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivationAttempts++ + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-data with run object") + + case 2: + // nor can it be unlocked with fallback key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivationAttempts++ + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-data with fallback object") + + case 3: + // we can however still unlock ubuntu-save (somehow?) + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + saveActivated = true + return secboot.UnlockResult{ + Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), + IsDecryptedDevice: true, + // it was unlocked with the recovery key + UnlockMethod: secboot.UnlockedWithRecoveryKey, + }, nil + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) { + // nothing can call this function in the tested scenario + c.Fatalf("unexpected call") + return "", fmt.Errorf("unexpected call") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + boot.InitramfsUbuntuSaveDir, + nil, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + modeEnv := filepath.Join(boot.InitramfsWritableDir, "var/lib/snapd/modeenv") + c.Check(modeEnv, testutil.FileEquals, `mode=recover +recovery_system=20191118 +`) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "unlock-state": "error-unlocking", + "find-state": "not-found", + }, + "ubuntu-save": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-save-enc-partuuid", + "unlock-key": "recovery", + "unlock-state": "unlocked", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuSaveDir, + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-data with sealed run key: failed to unlock ubuntu-data with run object", + "cannot unlock encrypted ubuntu-data partition with sealed fallback key: failed to unlock ubuntu-data with fallback object", + }, + }) + + bloader2, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + m, err := bloader2.GetBootVars("snapd_recovery_system", "snapd_recovery_mode") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_system": "20191118", + "snapd_recovery_mode": "run", + }) + + // since we didn't mount data at all, we won't have copied in files from + // there and instead will copy safe defaults to the ephemeral data + c.Assert(filepath.Join(boot.InitramfsHostWritableDir, "var/lib/console-conf/complete"), testutil.FilePresent) + + c.Check(dataActivationAttempts, Equals, 2) + c.Check(saveActivated, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 3) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedNoDataNoSaveHappy(c *C) { + // test a scenario when unlocking data with both run and fallback keys + // fails, followed by a failure to unlock save with the fallback key + + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivationAttempts := 0 + saveUnsealActivationAttempted := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // ubuntu data can't be unlocked with run key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + dataActivationAttempts++ + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-data with run object") + + case 2: + // nor can it be unlocked with fallback key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivationAttempts++ + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-data with fallback object") + + case 3: + // we also fail to unlock save + + // no attempts to activate ubuntu-save yet + c.Assert(name, Equals, "ubuntu-save") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-save.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-save-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + saveUnsealActivationAttempted = true + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-save with fallback object") + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo") + + restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) { + // nothing can call this function in the tested scenario + c.Fatalf("unexpected call") + return "", fmt.Errorf("unexpected call") + }) + defer restore() + + measureEpochCalls := 0 + measureModelCalls := 0 + restore = main.MockSecbootMeasureSnapSystemEpochWhenPossible(func() error { + measureEpochCalls++ + return nil + }) + defer restore() + + var measuredModel *asserts.Model + restore = main.MockSecbootMeasureSnapModelWhenPossible(func(findModel func() (*asserts.Model, error)) error { + measureModelCalls++ + var err error + measuredModel, err = findModel() + if err != nil { + return err + } + return nil + }) + defer restore() + + restore = s.mockSystemdMountSequence(c, []systemdMount{ + ubuntuLabelMount("ubuntu-seed", "recover"), + s.makeSeedSnapSystemdMount(snap.TypeSnapd), + s.makeSeedSnapSystemdMount(snap.TypeKernel), + s.makeSeedSnapSystemdMount(snap.TypeBase), + { + "tmpfs", + boot.InitramfsDataDir, + tmpfsMountOpts, + }, + { + "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + boot.InitramfsUbuntuBootDir, + needsFsckDiskMountOpts, + }, + }, nil) + defer restore() + + // ensure that we check that access to sealed keys were locked + sealedKeysLocked := false + restore = main.MockSecbootLockTPMSealedKeys(func() error { + sealedKeysLocked = true + return nil + }) + defer restore() + + _, err := main.Parser().ParseArgs([]string{"initramfs-mounts"}) + c.Assert(err, IsNil) + + // we always need to lock access to sealed keys + c.Check(sealedKeysLocked, Equals, true) + + modeEnv := filepath.Join(boot.InitramfsRunMntDir, "data/system-data/var/lib/snapd/modeenv") + c.Check(modeEnv, testutil.FileEquals, `mode=recover +recovery_system=20191118 +`) + + checkDegradedJSON(c, map[string]interface{}{ + "ubuntu-boot": map[string]interface{}{ + "device": "/dev/disk/by-partuuid/ubuntu-boot-partuuid", + "mount-state": "mounted", + "find-state": "found", + "mount-location": boot.InitramfsUbuntuBootDir, + }, + "ubuntu-data": map[string]interface{}{ + "unlock-state": "error-unlocking", + "find-state": "not-found", + }, + "ubuntu-save": map[string]interface{}{ + "unlock-state": "error-unlocking", + }, + "error-log": []interface{}{ + "cannot unlock encrypted ubuntu-data with sealed run key: failed to unlock ubuntu-data with run object", + "cannot unlock encrypted ubuntu-data partition with sealed fallback key: failed to unlock ubuntu-data with fallback object", + "cannot unlock encrypted ubuntu-save partition with sealed fallback key: failed to unlock ubuntu-save with fallback object", + }, + }) + + bloader2, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + m, err := bloader2.GetBootVars("snapd_recovery_system", "snapd_recovery_mode") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snapd_recovery_system": "20191118", + "snapd_recovery_mode": "run", + }) + + // since we didn't mount data at all, we won't have copied in files from + // there and instead will copy safe defaults to the ephemeral data + c.Assert(filepath.Join(boot.InitramfsHostWritableDir, "var/lib/console-conf/complete"), testutil.FilePresent) + + c.Check(dataActivationAttempts, Equals, 2) + c.Check(saveUnsealActivationAttempted, Equals, true) + c.Check(unlockVolumeWithSealedKeyCalls, Equals, 3) + c.Check(measureEpochCalls, Equals, 1) + c.Check(measureModelCalls, Equals, 1) + c.Check(measuredModel, DeepEquals, s.model) + + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, "secboot-epoch-measured"), testutil.FilePresent) + c.Assert(filepath.Join(dirs.SnapBootstrapRunDir, fmt.Sprintf("%s-model-measured", s.sysLabel)), testutil.FilePresent) +} + func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedAttackerFSAttachedHappy(c *C) { s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) @@ -2591,19 +3885,18 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedAttackerFS defer restore() activated := false - restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { c.Assert(name, Equals, "ubuntu-data") encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") c.Assert(err, IsNil) c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") - c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ - LockKeysOnFinish: true, - AllowRecoveryKey: true, - }) + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + activated = true return secboot.UnlockResult{ Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), IsDecryptedDevice: true, + UnlockMethod: secboot.UnlockedWithSealedKey, }, nil }) defer restore() diff --git a/cmd/snap-bootstrap/export_test.go b/cmd/snap-bootstrap/export_test.go index 10fc1a682d..34a95615de 100644 --- a/cmd/snap-bootstrap/export_test.go +++ b/cmd/snap-bootstrap/export_test.go @@ -36,6 +36,18 @@ var ( type SystemdMountOptions = systemdMountOptions +type RecoverDegradedState = recoverDegradedState + +type PartitionState = partitionState + +func (r *RecoverDegradedState) Degraded(isEncrypted bool) bool { + m := stateMachine{ + isEncryptedDev: isEncrypted, + degradedState: r, + } + return m.degraded() +} + func MockTimeNow(f func() time.Time) (restore func()) { old := timeNow timeNow = f @@ -78,7 +90,7 @@ func MockDefaultMarkerFile(p string) (restore func()) { } } -func MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(f func(disk disks.Disk, name string, encryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error)) (restore func()) { +func MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(f func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error)) (restore func()) { old := secbootUnlockVolumeUsingSealedKeyIfEncrypted secbootUnlockVolumeUsingSealedKeyIfEncrypted = f return func() { @@ -110,6 +122,14 @@ func MockSecbootMeasureSnapModelWhenPossible(f func(findModel func() (*asserts.M } } +func MockSecbootLockTPMSealedKeys(f func() error) (restore func()) { + old := secbootLockTPMSealedKeys + secbootLockTPMSealedKeys = f + return func() { + secbootLockTPMSealedKeys = old + } +} + func MockPartitionUUIDForBootedKernelDisk(uuid string) (restore func()) { old := bootFindPartitionUUIDForBootedKernelDisk bootFindPartitionUUIDForBootedKernelDisk = func() (string, error) { diff --git a/gadget/install/install.go b/gadget/install/install.go index cbfdbd951d..548cf556dd 100644 --- a/gadget/install/install.go +++ b/gadget/install/install.go @@ -27,6 +27,7 @@ import ( "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/secboot" ) @@ -53,6 +54,11 @@ func deviceFromRole(lv *gadget.LaidOutVolume, role string) (device string, err e // Run bootstraps the partitions of a device, by either creating // missing ones or recreating installed ones. func Run(gadgetRoot, device string, options Options, observer gadget.ContentObserver) (*InstalledSystemSideData, error) { + logger.Noticef("installing a new system") + logger.Noticef(" gadget data from: %v", gadgetRoot) + if options.Encrypt { + logger.Noticef(" encryption: on") + } if gadgetRoot == "" { return nil, fmt.Errorf("cannot use empty gadget root directory") } @@ -124,11 +130,18 @@ func Run(gadgetRoot, device string, options Options, observer gadget.ContentObse var keysForRoles map[string]*EncryptionKeySet for _, part := range created { + roleFmt := "" + if part.Role != "" { + roleFmt = fmt.Sprintf("role %v", part.Role) + } + logger.Noticef("created new partition %v for structure %v (size %v) %s", + part.Node, part, part.Size.IECString(), roleFmt) if options.Encrypt && roleNeedsEncryption(part.Role) { keys, err := makeKeySet() if err != nil { return nil, err } + logger.Noticef("encrypting partition device %v", part.Node) dataPart, err := newEncryptedDevice(&part, keys.Key, part.Label) if err != nil { return nil, err @@ -144,6 +157,7 @@ func Run(gadgetRoot, device string, options Options, observer gadget.ContentObse keysForRoles = map[string]*EncryptionKeySet{} } keysForRoles[part.Role] = keys + logger.Noticef("encrypted device %v", part.Node) } if err := makeFilesystem(&part); err != nil { diff --git a/gadget/install/partition_test.go b/gadget/install/partition_test.go index 4f04258a57..99e2b7caa8 100644 --- a/gadget/install/partition_test.go +++ b/gadget/install/partition_test.go @@ -30,6 +30,7 @@ import ( "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/install" + "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/testutil" ) @@ -172,6 +173,8 @@ var mockOnDiskStructureWritable = gadget.OnDiskStructure{ StartOffset: 1260388352, Index: 3, }, + // expanded to fill the disk + Size: 2*quantity.SizeGiB + 845*quantity.SizeMiB + 1031680, } func (s *partitionTestSuite) TestCreatePartitions(c *C) { diff --git a/gadget/ondisk.go b/gadget/ondisk.go index 67937f9383..d60c0ef42e 100644 --- a/gadget/ondisk.go +++ b/gadget/ondisk.go @@ -91,6 +91,11 @@ type OnDiskStructure struct { // Node identifies the device node of the block device. Node string + + // Size of the on disk structure, which is at least equal to the + // LaidOutStructure.Size but may be bigger if the partition was + // expanded. + Size quantity.Size } // OnDiskVolume holds information about the disk device including its partitioning @@ -322,6 +327,7 @@ func BuildPartitionList(dl *OnDiskVolume, pv *LaidOutVolume) (sfdiskInput *bytes toBeCreated = append(toBeCreated, OnDiskStructure{ LaidOutStructure: p, Node: node, + Size: size, }) } diff --git a/gadget/ondisk_test.go b/gadget/ondisk_test.go index 1392fc5d68..6c0c8c8e99 100644 --- a/gadget/ondisk_test.go +++ b/gadget/ondisk_test.go @@ -153,7 +153,7 @@ var mockOnDiskStructureSave = gadget.OnDiskStructure{ LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Name: "Save", - Size: 134217728, + Size: 128 * quantity.SizeMiB, Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", Role: "system-save", Label: "ubuntu-save", @@ -162,6 +162,7 @@ var mockOnDiskStructureSave = gadget.OnDiskStructure{ StartOffset: 1260388352, Index: 3, }, + Size: 128 * quantity.SizeMiB, } var mockOnDiskStructureWritable = gadget.OnDiskStructure{ @@ -169,7 +170,7 @@ var mockOnDiskStructureWritable = gadget.OnDiskStructure{ LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ Name: "Writable", - Size: 1258291200, + Size: 1200 * quantity.SizeMiB, Type: "83,0FC63DAF-8483-4772-8E79-3D69D8477DE4", Role: "system-data", Label: "ubuntu-data", @@ -178,6 +179,8 @@ var mockOnDiskStructureWritable = gadget.OnDiskStructure{ StartOffset: 1394606080, Index: 4, }, + // expanded to fill the disk + Size: 2*quantity.SizeGiB + 717*quantity.SizeMiB + 1031680, } func (s *ondiskTestSuite) TestDeviceInfoGPT(c *C) { diff --git a/interfaces/builtin/kvm_test.go b/interfaces/builtin/kvm_test.go index a5b14bee45..6531f9934d 100644 --- a/interfaces/builtin/kvm_test.go +++ b/interfaces/builtin/kvm_test.go @@ -20,6 +20,7 @@ package builtin_test import ( + "fmt" "io/ioutil" "path/filepath" @@ -117,7 +118,7 @@ func (s *kvmInterfaceSuite) TestUDevSpec(c *C) { c.Assert(spec.Snippets(), HasLen, 2) c.Assert(spec.Snippets()[0], Equals, `# kvm KERNEL=="kvm", TAG+="snap_consumer_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_consumer_app", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`) + c.Assert(spec.Snippets(), testutil.Contains, fmt.Sprintf(`TAG=="snap_consumer_app", RUN+="%s/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`, dirs.DistroLibExecDir)) } func (s *kvmInterfaceSuite) TestStaticInfo(c *C) { diff --git a/interfaces/udev/spec.go b/interfaces/udev/spec.go index 2c81937368..d2d92dea28 100644 --- a/interfaces/udev/spec.go +++ b/interfaces/udev/spec.go @@ -24,6 +24,7 @@ import ( "sort" "strings" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/strutil" @@ -91,7 +92,8 @@ func (spec *Specification) TagDevice(snippet string) { for _, securityTag := range spec.securityTags { tag := udevTag(securityTag) spec.addEntry(fmt.Sprintf("# %s\n%s, TAG+=\"%s\"", spec.iface, snippet, tag), tag) - spec.addEntry(fmt.Sprintf("TAG==\"%s\", RUN+=\"/usr/lib/snapd/snap-device-helper $env{ACTION} %s $devpath $major:$minor\"", tag, tag), tag) + spec.addEntry(fmt.Sprintf("TAG==\"%s\", RUN+=\"%s/snap-device-helper $env{ACTION} %s $devpath $major:$minor\"", + tag, dirs.DistroLibExecDir, tag), tag) } } diff --git a/interfaces/udev/spec_test.go b/interfaces/udev/spec_test.go index e47017a436..0b2b2c40c2 100644 --- a/interfaces/udev/spec_test.go +++ b/interfaces/udev/spec_test.go @@ -20,11 +20,15 @@ package udev_test import ( + "fmt" + . "gopkg.in/check.v1" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/ifacetest" "github.com/snapcore/snapd/interfaces/udev" + "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" ) @@ -93,7 +97,7 @@ func (s *specSuite) TestAddSnippte(c *C) { c.Assert(s.spec.Snippets(), DeepEquals, []string{"foo"}) } -func (s *specSuite) TestTagDevice(c *C) { +func (s *specSuite) testTagDevice(c *C, helperDir string) { // TagDevice acts in the scope of the plug/slot (as appropriate) and // affects all of the apps and hooks related to the given plug or slot // (with the exception that slots cannot have hooks). @@ -120,15 +124,33 @@ func (s *specSuite) TestTagDevice(c *C) { kernel="voodoo", TAG+="snap_snap1_foo"`, `# iface-2 kernel="hoodoo", TAG+="snap_snap1_foo"`, - `TAG=="snap_snap1_foo", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_snap1_foo $devpath $major:$minor"`, + fmt.Sprintf(`TAG=="snap_snap1_foo", RUN+="%s/snap-device-helper $env{ACTION} snap_snap1_foo $devpath $major:$minor"`, helperDir), `# iface-1 kernel="voodoo", TAG+="snap_snap1_hook_configure"`, `# iface-2 kernel="hoodoo", TAG+="snap_snap1_hook_configure"`, - `TAG=="snap_snap1_hook_configure", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_snap1_hook_configure $devpath $major:$minor"`, + fmt.Sprintf(`TAG=="snap_snap1_hook_configure", RUN+="%[1]s/snap-device-helper $env{ACTION} snap_snap1_hook_configure $devpath $major:$minor"`, helperDir), }) } +func (s *specSuite) TestTagDevice(c *C) { + defer func() { dirs.SetRootDir("") }() + restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) + defer restore() + dirs.SetRootDir("") + s.testTagDevice(c, "/usr/lib/snapd") +} + +func (s *specSuite) TestTagDeviceAltLibexecdir(c *C) { + defer func() { dirs.SetRootDir("") }() + restore := release.MockReleaseInfo(&release.OS{ID: "fedora"}) + defer restore() + dirs.SetRootDir("") + // sanity + c.Check(dirs.DistroLibExecDir, Equals, "/usr/libexec/snapd") + s.testTagDevice(c, "/usr/libexec/snapd") +} + // The spec.Specification can be used through the interfaces.Specification interface func (s *specSuite) TestSpecificationIface(c *C) { var r interfaces.Specification = s.spec diff --git a/overlord/devicestate/firstboot.go b/overlord/devicestate/firstboot.go index edc49ad44b..1a9476010b 100644 --- a/overlord/devicestate/firstboot.go +++ b/overlord/devicestate/firstboot.go @@ -170,6 +170,10 @@ func populateStateFromSeedImpl(st *state.State, opts *populateStateFromSeedOptio if beginTask != nil { // hooks must wait for mark-preseeded hooksTask.WaitFor(preseedDoneTask) + if n := len(all); n > 0 { + // the first hook of the snap waits for all tasks of previous snap + hooksTask.WaitAll(all[n-1]) + } if lastBeforeHooksTask != nil { beginTask.WaitFor(lastBeforeHooksTask) } @@ -273,6 +277,7 @@ func populateStateFromSeedImpl(st *state.State, opts *populateStateFromSeedOptio return nil, fmt.Errorf("cannot proceed, no snaps to seed") } + // ts is the taskset of the last snap ts := tsAll[len(tsAll)-1] endTs := state.NewTaskSet() @@ -290,6 +295,7 @@ func populateStateFromSeedImpl(st *state.State, opts *populateStateFromSeedOptio } markSeeded.Set("seed-system", whatSeeds) + // mark-seeded waits for the taskset of last snap markSeeded.WaitAll(ts) endTs.AddTask(markSeeded) tsAll = append(tsAll, endTs) diff --git a/overlord/devicestate/firstboot_preseed_test.go b/overlord/devicestate/firstboot_preseed_test.go index cbce25e0da..eb8202aac4 100644 --- a/overlord/devicestate/firstboot_preseed_test.go +++ b/overlord/devicestate/firstboot_preseed_test.go @@ -179,10 +179,6 @@ func checkPreseedOrder(c *C, tsAll []*state.TaskSet, snaps ...string) { continue } - snapsup, err := snapstate.TaskSnapSetup(task0) - c.Assert(err, IsNil, Commentf("%#v", task0)) - c.Check(snapsup.InstanceName(), Equals, snaps[matched]) - matched++ if i == 0 { c.Check(waitTasks, HasLen, 0) } else { @@ -191,6 +187,47 @@ func checkPreseedOrder(c *C, tsAll []*state.TaskSet, snaps ...string) { c.Check(waitTasks[0], Equals, prevTask) } + // make sure that install-hooks wait for the previous snap, and for + // mark-preseeded. + hookEdgeTask, err := ts.Edge(snapstate.HooksEdge) + c.Assert(err, IsNil) + c.Assert(hookEdgeTask.Kind(), Equals, "run-hook") + var hsup hookstate.HookSetup + c.Assert(hookEdgeTask.Get("hook-setup", &hsup), IsNil) + c.Check(hsup.Hook, Equals, "install") + if hsup.Snap != "core" && hsup.Snap != "core18" && hsup.Snap != "snapd" { + var waitsForMarkPreseeded, waitsForPreviousSnapHook, waitsForPreviousSnap bool + for _, wt := range hookEdgeTask.WaitTasks() { + switch wt.Kind() { + case "setup-aliases": + continue + case "run-hook": + var hsup hookstate.HookSetup + c.Assert(wt.Get("hook-setup", &hsup), IsNil) + c.Check(hsup.Snap, Equals, snaps[matched-1]) + waitsForPreviousSnapHook = true + case "mark-preseeded": + waitsForMarkPreseeded = true + case "prerequisites": + default: + snapsup, err := snapstate.TaskSnapSetup(wt) + c.Assert(err, IsNil, Commentf("%#v", wt)) + c.Check(snapsup.SnapName(), Equals, snaps[matched-1], Commentf("%s: %#v", hsup.Snap, wt)) + waitsForPreviousSnap = true + } + } + c.Assert(waitsForMarkPreseeded, Equals, true) + c.Assert(waitsForPreviousSnapHook, Equals, true) + if snaps[matched-1] != "core" && snaps[matched-1] != "core18" && snaps[matched-1] != "pc" { + c.Check(waitsForPreviousSnap, Equals, true, Commentf("%s", snaps[matched-1])) + } + } + + snapsup, err := snapstate.TaskSnapSetup(task0) + c.Assert(err, IsNil, Commentf("%#v", task0)) + c.Check(snapsup.InstanceName(), Equals, snaps[matched]) + matched++ + // find setup-aliases task in current taskset; its position // is not fixed due to e.g. optional update-gadget-assets task. var aliasesTask *state.Task @@ -273,6 +310,13 @@ version: 1.0 fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid") s.WriteAssertions("foo.asserts", s.devAcct, fooRev, fooDecl) + // put a firstboot snap into the SnapBlobDir + snapYaml2 := `name: bar +version: 1.0 +` + barFname, barDecl, barRev := s.MakeAssertedSnap(c, snapYaml2, nil, snap.R(33), "developerid") + s.WriteAssertions("bar.asserts", s.devAcct, barRev, barDecl) + // add a model assertion and its chain assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil) s.WriteAssertions("model.asserts", assertsChain...) @@ -282,9 +326,11 @@ version: 1.0 snaps: - name: foo file: %s + - name: bar + file: %s - name: core file: %s -`, fooFname, coreFname)) +`, fooFname, barFname, coreFname)) err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644) c.Assert(err, IsNil) @@ -304,7 +350,7 @@ snaps: } c.Assert(st.Changes(), HasLen, 1) - checkPreseedOrder(c, tsAll, "core", "foo") + checkPreseedOrder(c, tsAll, "core", "foo", "bar") st.Unlock() err = s.overlord.Settle(settleTimeout) @@ -330,6 +376,8 @@ snaps: c.Check(err, IsNil) _, err = snapstate.CurrentInfo(diskState, "foo") c.Check(err, IsNil) + _, err = snapstate.CurrentInfo(diskState, "bar") + c.Check(err, IsNil) // but we're not considered seeded var seeded bool @@ -389,8 +437,6 @@ snaps: tsAll, err := devicestate.PopulateStateFromSeedImpl(st, opts, s.perfTimings) c.Assert(err, IsNil) - checkPreseedOrder(c, tsAll, "snapd", "core18", "foo") - // now run the change and check the result chg := st.NewChange("seed", "run the populate from seed changes") for _, ts := range tsAll { @@ -399,6 +445,8 @@ snaps: c.Assert(st.Changes(), HasLen, 1) c.Assert(chg.Err(), IsNil) + checkPreseedOrder(c, tsAll, "snapd", "core18", "foo") + st.Unlock() err = s.overlord.Settle(settleTimeout) st.Lock() diff --git a/overlord/devicestate/firstboot_test.go b/overlord/devicestate/firstboot_test.go index 65a3a3ed86..3c7cfda231 100644 --- a/overlord/devicestate/firstboot_test.go +++ b/overlord/devicestate/firstboot_test.go @@ -514,10 +514,6 @@ snaps: tsAll, err := devicestate.PopulateStateFromSeedImpl(st, opts, s.perfTimings) c.Assert(err, IsNil) - checkOrder(c, tsAll, "core", "pc-kernel", "pc", "foo", "local") - - checkTasks(c, tsAll) - // now run the change and check the result // use the expected kind otherwise settle with start another one chg := st.NewChange("seed", "run the populate from seed changes") @@ -526,6 +522,9 @@ snaps: } c.Assert(st.Changes(), HasLen, 1) + checkOrder(c, tsAll, "core", "pc-kernel", "pc", "foo", "local") + checkTasks(c, tsAll) + // avoid device reg chg1 := st.NewChange("become-operational", "init device") chg1.SetStatus(state.DoingStatus) diff --git a/packaging/debian-sid/rules b/packaging/debian-sid/rules index 4188377f4f..15f0b208b3 100755 --- a/packaging/debian-sid/rules +++ b/packaging/debian-sid/rules @@ -149,7 +149,7 @@ override_dh_auto_build: find _build/src/$(DH_GOPKG)/cmd/snap-bootstrap -name "*.go" | xargs rm -f find _build/src/$(DH_GOPKG)/gadget/install -name "*.go" | grep -vE '(params\.go|install_dummy\.go)'| xargs rm -f # XXX: once dh-golang understands go build tags this would not be needed - find _build/src/$(DH_GOPKG)/secboot/ -name "*.go" | grep -Ev '(encrypt\.go|secboot_dummy\.go|secboot\.go)' | xargs rm -f + find _build/src/$(DH_GOPKG)/secboot/ -name "*.go" | grep -Ev '(encrypt\.go|secboot_dummy\.go|secboot\.go|encrypt_dummy\.go)' | xargs rm -f # and build dh_auto_build -- $(BUILDFLAGS) -tags "$(TAGS)" $(GCCGOFLAGS) diff --git a/tests/main/lxd/task.yaml b/tests/main/lxd/task.yaml index cd139ebeb9..575a44ada1 100644 --- a/tests/main/lxd/task.yaml +++ b/tests/main/lxd/task.yaml @@ -4,8 +4,7 @@ summary: Ensure that lxd works # currently nor on ubuntu 14.04 # TODO:UC20: enable for UC20 # TODO: enable for ubuntu-16-32 again -# TODO: enable ubuntu-20.10-64 once the image is available -systems: [ubuntu-16.04*64, ubuntu-18.04*, ubuntu-20.04*, ubuntu-core-1*] +systems: [ubuntu-16.04*64, ubuntu-18.04*, ubuntu-20.04*, ubuntu-20.10*, ubuntu-core-1*] # autopkgtest run only a subset of tests that deals with the integration # with the distro diff --git a/tests/nested/manual/preseed/task.yaml b/tests/nested/manual/preseed/task.yaml index 33766e40b5..39e5390bb9 100644 --- a/tests/nested/manual/preseed/task.yaml +++ b/tests/nested/manual/preseed/task.yaml @@ -133,3 +133,18 @@ execute: | # wait for postgres to come online sleep 10 nested_exec "snap services" | MATCH "+test-postgres-system-usernames.postgres +enabled +active" + + echo "Checking that mark-seeded task was executed last" + # snap debug timings are sorts by read-time, mark-seeded should be last + nested_exec "sudo snap debug timings 1" | tail -2 | MATCH "Mark system seeded" + # no task should have ready time after mark-seeded + # shellcheck disable=SC2046 + MARK_SEEDED_TIME=$(date -d $(snap change 1 --abs-time | grep "Mark system seeded" | awk '{print $3}') "+%s") + for RT in $(snap change 1 --abs-time | grep Done | awk '{print $3}' ) + do + READY_TIME=$(date -d "$RT" "+%s") + if [ "$READY_TIME" -gt "$MARK_SEEDED_TIME" ]; then + echo "Unexpected ready time greater than mark-seeded ready" + snap change 1 + fi + done |
