summaryrefslogtreecommitdiff
diff options
-rw-r--r--cmd/snap-bootstrap/cmd_initramfs_mounts.go734
-rw-r--r--cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go6
-rw-r--r--cmd/snap-bootstrap/cmd_initramfs_mounts_recover_degraded_test.go292
-rw-r--r--cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go1
-rw-r--r--cmd/snap-bootstrap/cmd_initramfs_mounts_test.go1349
-rw-r--r--cmd/snap-bootstrap/export_test.go22
-rw-r--r--gadget/install/install.go14
-rw-r--r--gadget/install/partition_test.go3
-rw-r--r--gadget/ondisk.go6
-rw-r--r--gadget/ondisk_test.go7
-rw-r--r--interfaces/builtin/kvm_test.go3
-rw-r--r--interfaces/udev/spec.go4
-rw-r--r--interfaces/udev/spec_test.go28
-rw-r--r--overlord/devicestate/firstboot.go6
-rw-r--r--overlord/devicestate/firstboot_preseed_test.go64
-rw-r--r--overlord/devicestate/firstboot_test.go7
-rwxr-xr-xpackaging/debian-sid/rules2
-rw-r--r--tests/main/lxd/task.yaml3
-rw-r--r--tests/nested/manual/preseed/task.yaml15
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, &degradedJSONObj)
+ 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