summaryrefslogtreecommitdiff
diff options
authorMichael Vogt <mvo@ubuntu.com>2020-11-12 09:28:41 +0100
committerMichael Vogt <mvo@ubuntu.com>2020-11-12 09:28:41 +0100
commitcb57d0eff40b4e342340bbc3266be009c0b0b6c4 (patch)
treecc8ec9b2ec78aee282a4c54e025a8b1c4646abae
parent3849d71d9d4a4837cc85dadcf6d5636af9da5907 (diff)
parente4f85071a1cb84c14c74c66293bf853c4b2d0d2e (diff)
Merge remote-tracking branch 'upstream/release/2.48' into fdehook-skeleton-2fdehook-skeleton-2
-rw-r--r--client/client.go10
-rw-r--r--client/client_test.go12
-rw-r--r--cmd/snap-bootstrap/cmd_initramfs_mounts.go835
-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.go1552
-rw-r--r--cmd/snap-bootstrap/export_test.go22
-rw-r--r--cmd/snap-update-ns/change.go6
-rw-r--r--cmd/snap-update-ns/system.go13
-rw-r--r--cmd/snap-update-ns/system_test.go11
-rw-r--r--cmd/snap-update-ns/trespassing.go41
-rw-r--r--cmd/snap/cmd_recovery.go38
-rw-r--r--cmd/snap/cmd_recovery_test.go51
-rw-r--r--cmd/snapd-generator/main.c2
-rw-r--r--daemon/api.go1
-rw-r--r--daemon/api_system_recovery_keys.go54
-rw-r--r--daemon/api_system_recovery_keys_test.go87
-rw-r--r--gadget/gadget.go6
-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--gadget/validate.go35
-rw-r--r--gadget/validate_test.go65
-rw-r--r--interfaces/builtin/fwupd.go4
-rw-r--r--interfaces/builtin/kvm_test.go3
-rw-r--r--interfaces/builtin/x11.go71
-rw-r--r--interfaces/builtin/x11_test.go106
-rw-r--r--interfaces/udev/spec.go4
-rw-r--r--interfaces/udev/spec_test.go28
-rw-r--r--overlord/devicestate/devicestate_gadget_test.go7
-rw-r--r--overlord/devicestate/devicestate_install_mode_test.go73
-rw-r--r--overlord/devicestate/firstboot.go6
-rw-r--r--overlord/devicestate/firstboot_preseed_test.go68
-rw-r--r--overlord/devicestate/firstboot_test.go7
-rw-r--r--overlord/devicestate/handlers_install.go42
-rwxr-xr-xpackaging/debian-sid/rules2
-rw-r--r--secboot/encrypt.go25
-rw-r--r--secboot/encrypt_dummy.go25
-rw-r--r--secboot/encrypt_tpm.go4
-rw-r--r--secboot/secboot_tpm.go7
-rw-r--r--secboot/secboot_tpm_test.go37
-rw-r--r--snap/pack/pack.go5
-rw-r--r--spread.yaml2
-rw-r--r--tests/main/interfaces-x11-unix-socket/task.yaml51
-rwxr-xr-xtests/main/interfaces-x11-unix-socket/x11-client/bin/rm.sh2
-rwxr-xr-xtests/main/interfaces-x11-unix-socket/x11-client/bin/xclient.sh2
-rw-r--r--tests/main/interfaces-x11-unix-socket/x11-client/meta/snap.yaml12
-rwxr-xr-xtests/main/interfaces-x11-unix-socket/x11-server/bin/xserver.sh7
-rw-r--r--tests/main/interfaces-x11-unix-socket/x11-server/meta/snap.yaml10
-rw-r--r--tests/main/lxd-mount-units/task.yaml38
-rw-r--r--tests/main/lxd/task.yaml4
-rw-r--r--tests/nested/core20/basic/task.yaml9
-rw-r--r--tests/nested/manual/preseed/task.yaml15
-rw-r--r--vendor/vendor.json6
56 files changed, 3641 insertions, 211 deletions
diff --git a/client/client.go b/client/client.go
index b1ffe0abec..fa3875eb8c 100644
--- a/client/client.go
+++ b/client/client.go
@@ -733,3 +733,13 @@ func (client *Client) DebugGet(aspect string, result interface{}, params map[str
_, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result)
return err
}
+
+type SystemRecoveryKeysResponse struct {
+ RecoveryKey string `json:"recovery-key"`
+ ReinstallKey string `json:"reinstall-key"`
+}
+
+func (client *Client) SystemRecoveryKeys(result interface{}) error {
+ _, err := client.doSync("GET", "/v2/system-recovery-keys", nil, nil, nil, &result)
+ return err
+}
diff --git a/client/client_test.go b/client/client_test.go
index c4e711c141..fcab4a77ac 100644
--- a/client/client_test.go
+++ b/client/client_test.go
@@ -629,3 +629,15 @@ func (cs *integrationSuite) TestClientTimeoutLP1837804(c *C) {
_, err = cli.Do("POST", "/", nil, nil, nil, nil)
c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`)
}
+
+func (cs *clientSuite) TestClientSystemRecoveryKeys(c *C) {
+ cs.rsp = `{"type":"sync", "result":{"recovery-key":"42"}}`
+
+ var key client.SystemRecoveryKeysResponse
+ err := cs.cli.SystemRecoveryKeys(&key)
+ c.Assert(err, IsNil)
+ c.Check(cs.reqs, HasLen, 1)
+ c.Check(cs.reqs[0].Method, Equals, "GET")
+ c.Check(cs.reqs[0].URL.Path, Equals, "/v2/system-recovery-keys")
+ c.Check(key.RecoveryKey, Equals, "42")
+}
diff --git a/cmd/snap-bootstrap/cmd_initramfs_mounts.go b/cmd/snap-bootstrap/cmd_initramfs_mounts.go
index b5bda920fe..ee3a4319a0 100644
--- a/cmd/snap-bootstrap/cmd_initramfs_mounts.go
+++ b/cmd/snap-bootstrap/cmd_initramfs_mounts.go
@@ -20,6 +20,8 @@
package main
import (
+ "crypto/subtle"
+ "encoding/json"
"fmt"
"io/ioutil"
"os"
@@ -32,6 +34,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 +82,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
)
@@ -131,7 +136,7 @@ func generateInitramfsMounts() error {
// no longer generates more mount points and just returns an empty output.
func generateMountsModeInstall(mst *initramfsMountsState) error {
// steps 1 and 2 are shared with recover mode
- if err := generateMountsCommonInstallRecover(mst); err != nil {
+ if _, err := generateMountsCommonInstallRecover(mst); err != nil {
return err
}
@@ -234,6 +239,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 +286,442 @@ 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"
+ partitionMountedUntrusted = "mounted-untrusted"
+ // 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
+
+ // device model
+ model *asserts.Model
+
+ // 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
+}
+
+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
+ }
- // 2.X mount ubuntu-boot for access to the run mode key to unseal
- // ubuntu-data
+ // 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(model *asserts.Model, disk disks.Disk) *stateMachine {
+ m := &stateMachine{
+ model: model,
+ 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
+ finished = next == nil
+ if finished && err == nil {
+ if err := m.finalize(); err != nil {
+ return true, err
+ }
+ }
+ return finished, err
+}
+
+func (m *stateMachine) finalize() error {
+ // check soundness
+ // the grade check makes sure that if data was mounted unencrypted
+ // but the model is secured it will end up marked as untrusted
+ isEncrypted := m.isEncryptedDev || m.model.Grade() == asserts.ModelSecured
+ part := m.degradedState.partition("ubuntu-data")
+ if part.MountState == partitionMounted && isEncrypted {
+ // check that save and data match
+ // We want to avoid a chosen ubuntu-data
+ // (e.g. activated with a recovery key) to get access
+ // via its logins to the secrets in ubuntu-save (in
+ // particular the policy update auth key)
+ trustData, _ := checkDataAndSavaPairing(boot.InitramfsHostWritableDir)
+ if !trustData {
+ part.MountState = partitionMountedUntrusted
+ m.degradedState.LogErrorf("cannot trust ubuntu-data, ubuntu-save and ubuntu-data are not marked as from the same install")
+ }
+ }
+ return nil
+}
+
+func (m *stateMachine) trustData() bool {
+ return m.degradedState.partition("ubuntu-data").MountState == partitionMounted
+}
+
+// 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 +731,273 @@ 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 {
- return err
+ 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
+}
- // 3.1. mount ubuntu-save (if present)
- haveSave, err := maybeMountSave(disk, boot.InitramfsHostWritableDir, unlockRes.IsDecryptedDevice, nil)
+func generateMountsModeRecover(mst *initramfsMountsState) error {
+ // steps 1 and 2 are shared with install mode
+ model, err := generateMountsCommonInstallRecover(mst)
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
+ // 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
}
- matches, err = disk.MountPointIsFromDisk(boot.InitramfsHostUbuntuDataDir, diskOpts)
+ // 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)
+ }
+ }()
+
+ // first state to execute is to unlock ubuntu-data with the run key
+ machine = newStateMachine(model, disk)
+ for {
+ finished, err := machine.execute()
+ // TODO: consider whether certain errors are fatal or not
+ if err != nil {
+ return nil, err
+ }
+ if finished {
+ 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 +1005,33 @@ 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
+ // Proceed only if we trust ubuntu-data to be paired with ubuntu-save
+ if machine.trustData() {
+ // 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{
@@ -398,6 +1055,27 @@ func generateMountsModeRecover(mst *initramfsMountsState) error {
return nil
}
+// checkDataAndSavaPairing make sure that ubuntu-data and ubuntu-save
+// come from the same install by comparing secret markers in them
+func checkDataAndSavaPairing(rootdir string) (bool, error) {
+ // read the secret marker file from ubuntu-data
+ markerFile1 := filepath.Join(dirs.SnapFDEDirUnder(rootdir), "marker")
+ marker1, err := ioutil.ReadFile(markerFile1)
+ if err != nil {
+ return false, err
+ }
+ // read the secret marker file from ubuntu-save
+ // TODO:UC20: this is a bit of an abuse of the Install*Dir variable, we
+ // should really only be using Initramfs*Dir variables since we are in the
+ // initramfs and not in install mode, no?
+ markerFile2 := filepath.Join(boot.InstallHostFDESaveDir, "marker")
+ marker2, err := ioutil.ReadFile(markerFile2)
+ if err != nil {
+ return false, err
+ }
+ return subtle.ConstantTimeCompare(marker1, marker2) == 1, nil
+}
+
// mountPartitionMatchingKernelDisk will select the partition to mount at dir,
// using the boot package function FindPartitionUUIDForBootedKernelDisk to
// determine what partition the booted kernel came from. If which disk the
@@ -422,18 +1100,18 @@ func mountPartitionMatchingKernelDisk(dir, fallbacklabel string) error {
return doSystemdMount(partSrc, dir, opts)
}
-func generateMountsCommonInstallRecover(mst *initramfsMountsState) error {
+func generateMountsCommonInstallRecover(mst *initramfsMountsState) (*asserts.Model, error) {
// 1. always ensure seed partition is mounted first before the others,
// since the seed partition is needed to mount the snap files there
if err := mountPartitionMatchingKernelDisk(boot.InitramfsUbuntuSeedDir, "ubuntu-seed"); err != nil {
- return err
+ return nil, err
}
// load model and verified essential snaps metadata
typs := []snap.Type{snap.TypeBase, snap.TypeKernel, snap.TypeSnapd, snap.TypeGadget}
model, essSnaps, err := mst.ReadEssential("", typs)
if err != nil {
- return fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", typs, err)
+ return nil, fmt.Errorf("cannot load metadata and verify essential bootstrap snaps %v: %v", typs, err)
}
// 2.1. measure model
@@ -443,7 +1121,7 @@ func generateMountsCommonInstallRecover(mst *initramfsMountsState) error {
})
})
if err != nil {
- return err
+ return nil, err
}
// 2.2. (auto) select recovery system and mount seed snaps
@@ -457,7 +1135,7 @@ func generateMountsCommonInstallRecover(mst *initramfsMountsState) error {
dir := snapTypeToMountDir[essentialSnap.EssentialType]
// TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub
if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil {
- return err
+ return nil, err
}
}
@@ -480,7 +1158,7 @@ func generateMountsCommonInstallRecover(mst *initramfsMountsState) error {
}
err = doSystemdMount("tmpfs", boot.InitramfsDataDir, mntOpts)
if err != nil {
- return err
+ return nil, err
}
// finally get the gadget snap from the essential snaps and use it to
@@ -506,7 +1184,11 @@ func generateMountsCommonInstallRecover(mst *initramfsMountsState) error {
TargetRootDir: boot.InitramfsWritableDir,
GadgetSnap: gadgetSnap,
}
- return sysconfig.ConfigureTargetSystem(configOpts)
+ if err := sysconfig.ConfigureTargetSystem(configOpts); err != nil {
+ return nil, err
+ }
+
+ return model, err
}
func maybeMountSave(disk disks.Disk, rootdir string, encrypted bool, mountOpts *systemdMountOptions) (haveSave bool, err error) {
@@ -605,9 +1287,10 @@ func generateMountsModeRun(mst *initramfsMountsState) error {
if err := doSystemdMount(unlockRes.Device, boot.InitramfsDataDir, fsckSystemdOpts); err != nil {
return err
}
+ isEncryptedDev := unlockRes.IsDecryptedDevice
// 3.3. mount ubuntu-save (if present)
- haveSave, err := maybeMountSave(disk, boot.InitramfsWritableDir, unlockRes.IsDecryptedDevice, fsckSystemdOpts)
+ haveSave, err := maybeMountSave(disk, boot.InitramfsWritableDir, isEncryptedDev, fsckSystemdOpts)
if err != nil {
return err
}
@@ -638,6 +1321,24 @@ func generateMountsModeRun(mst *initramfsMountsState) error {
if !matches {
return fmt.Errorf("cannot validate boot: ubuntu-save mountpoint is expected to be from disk %s but is not", disk.Dev())
}
+
+ if isEncryptedDev {
+ // in run mode the path to open an encrypted save is for
+ // data to be encrypted and the save key in it
+ // to be successfully used. This already should stop
+ // allowing to chose ubuntu-data to try to access
+ // save. as safety boot also stops if the keys cannot
+ // be locked.
+ // for symmetry with recover code and extra paranoia
+ // though also check that the markers match.
+ paired, err := checkDataAndSavaPairing(boot.InitramfsWritableDir)
+ if err != nil {
+ return err
+ }
+ if !paired {
+ return fmt.Errorf("cannot validate boot: ubuntu-save and ubuntu-data are not marked as from the same install")
+ }
+ }
}
// 4.2. read 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..1c5f1e9451 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
}))
}
@@ -253,10 +242,21 @@ func (s *initramfsMountsSuite) mockProcCmdlineContent(c *C, newContent string) {
s.AddCleanup(restore)
}
-func (s *initramfsMountsSuite) mockUbuntuSaveKey(c *C, rootDir, key string) {
+func (s *initramfsMountsSuite) mockUbuntuSaveKeyAndMarker(c *C, rootDir, key, marker string) {
keyPath := filepath.Join(dirs.SnapFDEDirUnder(rootDir), "ubuntu-save.key")
c.Assert(os.MkdirAll(filepath.Dir(keyPath), 0700), IsNil)
c.Assert(ioutil.WriteFile(keyPath, []byte(key), 0600), IsNil)
+
+ if marker != "" {
+ markerPath := filepath.Join(dirs.SnapFDEDirUnder(rootDir), "marker")
+ c.Assert(ioutil.WriteFile(markerPath, []byte(marker), 0600), IsNil)
+ }
+}
+
+func (s *initramfsMountsSuite) mockUbuntuSaveMarker(c *C, rootDir, marker string) {
+ markerPath := filepath.Join(rootDir, "device/fde", "marker")
+ c.Assert(os.MkdirAll(filepath.Dir(markerPath), 0700), IsNil)
+ c.Assert(ioutil.WriteFile(markerPath, []byte(marker), 0600), IsNil)
}
func (s *initramfsMountsSuite) TestInitramfsMountsNoModeError(c *C) {
@@ -944,6 +944,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 +1089,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 +1404,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 +1500,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{
@@ -1507,7 +1517,8 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataHappy(c *C
})
defer restore()
- s.mockUbuntuSaveKey(c, boot.InitramfsWritableDir, "foo")
+ s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsWritableDir, "foo", "marker")
+ s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker")
saveActivated := false
restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) {
@@ -1605,7 +1616,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 +1690,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
@@ -1690,7 +1701,7 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRunModeEncryptedDataUnhappyUnl
})
defer restore()
- s.mockUbuntuSaveKey(c, boot.InitramfsWritableDir, "foo")
+ s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsWritableDir, "foo", "")
restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) {
c.Check(dataActivated, Equals, true, Commentf("ubuntu-data not yet activated"))
return "", fmt.Errorf("ubuntu-save unlock fail")
@@ -2129,6 +2140,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 +2208,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 +2295,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 +2377,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 +2446,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 +2479,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),
@@ -2466,7 +2495,8 @@ func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeHappyEncrypted(c *C
})
defer restore()
- s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo")
+ s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker")
+ s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker")
saveActivated := false
restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) {
@@ -2530,6 +2560,1462 @@ 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.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker")
+ s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker")
+
+ 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)
+ 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) 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.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker")
+ s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker")
+
+ 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.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker")
+ s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker")
+
+ 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.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker")
+ s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker")
+
+ 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.mockUbuntuSaveKeyAndMarker(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.mockUbuntuSaveKeyAndMarker(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.mockUbuntuSaveKeyAndMarker(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) TestInitramfsMountsRecoverModeEncryptedMismatchedMarker(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
+ 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(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,
+ }, nil
+ })
+ defer restore()
+
+ s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "other-marker")
+ s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker")
+
+ saveActivated := false
+ 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()
+
+ // 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{}{
+ "device": "/dev/disk/by-partuuid/ubuntu-data-enc-partuuid",
+ "unlock-state": "unlocked",
+ "find-state": "found",
+ "mount-state": "mounted-untrusted",
+ "unlock-key": "run",
+ "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 trust ubuntu-data, ubuntu-save and ubuntu-data are not marked as from the same install"},
+ })
+
+ 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(dataActivated, Equals, true)
c.Check(saveActivated, Equals, true)
c.Check(measureEpochCalls, Equals, 1)
@@ -2591,24 +4077,24 @@ 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()
- s.mockUbuntuSaveKey(c, boot.InitramfsHostWritableDir, "foo")
+ s.mockUbuntuSaveKeyAndMarker(c, boot.InitramfsHostWritableDir, "foo", "marker")
+ s.mockUbuntuSaveMarker(c, boot.InitramfsUbuntuSaveDir, "marker")
restore = main.MockSecbootUnlockEncryptedVolumeUsingKey(func(disk disks.Disk, name string, key []byte) (string, error) {
encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc")
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/cmd/snap-update-ns/change.go b/cmd/snap-update-ns/change.go
index 4cf0a6aff1..37d72999bb 100644
--- a/cmd/snap-update-ns/change.go
+++ b/cmd/snap-update-ns/change.go
@@ -95,10 +95,10 @@ func (c *Change) createPath(path string, pokeHoles bool, as *Assumptions) ([]*Ch
// In case we need to create something, some constants.
const (
- mode = 0755
- uid = 0
- gid = 0
+ uid = 0
+ gid = 0
)
+ mode := as.ModeForPath(path)
// If the element doesn't exist we can attempt to create it. We will
// create the parent directory and then the final element relative to it.
diff --git a/cmd/snap-update-ns/system.go b/cmd/snap-update-ns/system.go
index 949b844f73..73b1a323c4 100644
--- a/cmd/snap-update-ns/system.go
+++ b/cmd/snap-update-ns/system.go
@@ -72,6 +72,19 @@ func (upCtx *SystemProfileUpdateContext) Assumptions() *Assumptions {
if snapName := snap.InstanceSnap(instanceName); snapName != instanceName {
as.AddUnrestrictedPaths("/snap/" + snapName)
}
+ // Allow snap-update-ns to write to host's /tmp directory. This is
+ // specifically here to allow two snaps to share X11 sockets that are placed
+ // in the /tmp/.X11-unix/ directory in the private /tmp directories provided
+ // by snap-confine. The X11 interface cannot offer a precise permission for
+ // the slot-side snap, as there is no mechanism to convey this information.
+ // As such, provide write access to all of /tmp.
+ as.AddUnrestrictedPaths("/var/lib/snapd/hostfs/tmp")
+ as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*", 0700)
+ as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*/tmp", 1777)
+ // This is to ensure that unprivileged users can create the socket. This
+ // permission only matters if the plug-side app constructs its mount
+ // namespace before the slot-side app is launched.
+ as.AddModeHint("/var/lib/snapd/hostfs/tmp/snap.*/tmp/.X11-unix", 1777)
return as
}
diff --git a/cmd/snap-update-ns/system_test.go b/cmd/snap-update-ns/system_test.go
index 17fffcc6f2..bb8b93acda 100644
--- a/cmd/snap-update-ns/system_test.go
+++ b/cmd/snap-update-ns/system_test.go
@@ -52,12 +52,19 @@ func (s *systemSuite) TestAssumptions(c *C) {
// Non-instances can access /tmp, /var/snap and /snap/$SNAP_NAME
upCtx := update.NewSystemProfileUpdateContext("foo", false)
as := upCtx.Assumptions()
- c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo"})
+ c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo", "/var/lib/snapd/hostfs/tmp"})
+ c.Check(as.ModeForPath("/stuff"), Equals, os.FileMode(0755))
+ c.Check(as.ModeForPath("/tmp"), Equals, os.FileMode(0755))
+ c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp"), Equals, os.FileMode(0755))
+ c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server"), Equals, os.FileMode(0700))
+ c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/tmp"), Equals, os.FileMode(1777))
+ c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/foo"), Equals, os.FileMode(0755))
+ c.Check(as.ModeForPath("/var/lib/snapd/hostfs/tmp/snap.x11-server/tmp/.X11-unix"), Equals, os.FileMode(1777))
// Instances can, in addition, access /snap/$SNAP_INSTANCE_NAME
upCtx = update.NewSystemProfileUpdateContext("foo_instance", false)
as = upCtx.Assumptions()
- c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo_instance", "/snap/foo"})
+ c.Check(as.UnrestrictedPaths(), DeepEquals, []string{"/tmp", "/var/snap", "/snap/foo_instance", "/snap/foo", "/var/lib/snapd/hostfs/tmp"})
}
func (s *systemSuite) TestLoadDesiredProfile(c *C) {
diff --git a/cmd/snap-update-ns/trespassing.go b/cmd/snap-update-ns/trespassing.go
index 3eebb54f1f..c7b9b4faaf 100644
--- a/cmd/snap-update-ns/trespassing.go
+++ b/cmd/snap-update-ns/trespassing.go
@@ -21,6 +21,7 @@ package main
import (
"fmt"
+ "os"
"path/filepath"
"strings"
"syscall"
@@ -42,6 +43,16 @@ type Assumptions struct {
// major:minor number is packed into one uint64 as in syscall.Stat_t.Dev
// field.
verifiedDevices map[uint64]bool
+
+ // modeHints overrides implicit 0755 mode of directories created while
+ // ensuring source and target paths exist.
+ modeHints []ModeHint
+}
+
+// ModeHint provides mode for directories created to satisfy mount changes.
+type ModeHint struct {
+ PathGlob string
+ Mode os.FileMode
}
// AddUnrestrictedPaths adds a list of directories where writing is allowed
@@ -52,6 +63,36 @@ func (as *Assumptions) AddUnrestrictedPaths(paths ...string) {
as.unrestrictedPaths = append(as.unrestrictedPaths, paths...)
}
+// AddModeHint adds a path glob and mode used when creating path elements.
+func (as *Assumptions) AddModeHint(pathGlob string, mode os.FileMode) {
+ as.modeHints = append(as.modeHints, ModeHint{PathGlob: pathGlob, Mode: mode})
+}
+
+// ModeForPath returns the mode for creating a directory at a given path.
+//
+// The default mode is 0755 but AddModeHint calls can influence the mode at a
+// specific path. When matching path elements, "*" does not match the directory
+// separator. In effect it can only be used as a wildcard for a specific
+// directory name. This constraint makes hints easier to model in practice.
+//
+// When multiple hints match the given path, ModeForPath panics.
+func (as *Assumptions) ModeForPath(path string) os.FileMode {
+ mode := os.FileMode(0755)
+ var foundHint *ModeHint
+ for _, hint := range as.modeHints {
+ if ok, _ := filepath.Match(hint.PathGlob, path); ok {
+ if foundHint == nil {
+ mode = hint.Mode
+ foundHint = &hint
+ } else {
+ panic(fmt.Errorf("cannot find unique mode for path %q: %q and %q both provide hints",
+ path, foundHint.PathGlob, foundHint.PathGlob))
+ }
+ }
+ }
+ return mode
+}
+
// isRestricted checks whether a path falls under restricted writing scheme.
//
// Provided path is the full, absolute path of the entity that needs to be
diff --git a/cmd/snap/cmd_recovery.go b/cmd/snap/cmd_recovery.go
index ff4391608f..9edbea2d8c 100644
--- a/cmd/snap/cmd_recovery.go
+++ b/cmd/snap/cmd_recovery.go
@@ -20,30 +20,41 @@
package main
import (
+ "errors"
"fmt"
+ "io"
"strings"
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/release"
)
type cmdRecovery struct {
clientMixin
colorMixin
+
+ ShowKeys bool `long:"show-keys"`
}
var shortRecoveryHelp = i18n.G("List available recovery systems")
var longRecoveryHelp = i18n.G(`
The recovery command lists the available recovery systems.
+
+With --show-keys it displays recovery keys that can be used to unlock the encrypted partitions if the device-specific automatic unlocking does not work.
`)
func init() {
addCommand("recovery", shortRecoveryHelp, longRecoveryHelp, func() flags.Commander {
// XXX: if we want more/nicer details we can add `snap recovery <system>` later
return &cmdRecovery{}
- }, nil, nil)
+ }, colorDescs.also(
+ map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "show-keys": i18n.G("Show recovery keys (if available) to unlock encrypted partitions."),
+ }), nil)
}
func notesForSystem(sys *client.System) string {
@@ -53,11 +64,33 @@ func notesForSystem(sys *client.System) string {
return "-"
}
+func (x *cmdRecovery) showKeys(w io.Writer) error {
+ if release.OnClassic {
+ return errors.New(`command "show-keys" is not available on classic systems`)
+ }
+ var srk *client.SystemRecoveryKeysResponse
+ err := x.client.SystemRecoveryKeys(&srk)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(w, "recovery:\t%s\n", srk.RecoveryKey)
+ fmt.Fprintf(w, "reinstall:\t%s\n", srk.ReinstallKey)
+ return nil
+}
+
func (x *cmdRecovery) Execute(args []string) error {
if len(args) > 0 {
return ErrExtraArgs
}
+ esc := x.getEscapes()
+ w := tabWriter()
+ defer w.Flush()
+
+ if x.ShowKeys {
+ return x.showKeys(w)
+ }
+
systems, err := x.client.ListSystems()
if err != nil {
return err
@@ -67,9 +100,6 @@ func (x *cmdRecovery) Execute(args []string) error {
return nil
}
- esc := x.getEscapes()
- w := tabWriter()
- defer w.Flush()
fmt.Fprintf(w, i18n.G("Label\tBrand\tModel\tNotes\n"))
for _, sys := range systems {
// doing it this way because otherwise it's a sea of %s\t%s\t%s
diff --git a/cmd/snap/cmd_recovery_test.go b/cmd/snap/cmd_recovery_test.go
index 601918010e..c90b2e79b6 100644
--- a/cmd/snap/cmd_recovery_test.go
+++ b/cmd/snap/cmd_recovery_test.go
@@ -26,6 +26,7 @@ import (
. "gopkg.in/check.v1"
snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/release"
)
func (s *SnapSuite) TestRecoveryHelp(c *C) {
@@ -34,9 +35,16 @@ func (s *SnapSuite) TestRecoveryHelp(c *C) {
The recovery command lists the available recovery systems.
+With --show-keys it displays recovery keys that can be used to unlock the
+encrypted partitions if the device-specific automatic unlocking does not work.
+
[recovery command options]
- --color=[auto|never|always]
- --unicode=[auto|never|always]
+ --color=[auto|never|always] Use a little bit of color to highlight
+ some things. (default: auto)
+ --unicode=[auto|never|always] Use a little bit of Unicode to improve
+ legibility. (default: auto)
+ --show-keys Show recovery keys (if available) to
+ unlock encrypted partitions.
`
s.testSubCommandHelp(c, "recovery", msg)
}
@@ -145,3 +153,42 @@ func (s *SnapSuite) TestNoRecoverySystemsError(c *C) {
_, err := snap.Parser(snap.Client()).ParseArgs([]string{"recovery"})
c.Check(err, ErrorMatches, `cannot list recovery systems: permission denied`)
}
+
+func (s *SnapSuite) TestRecoveryShowRecoveryKeyOnClassicErrors(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ c.Fatalf("unexpected server call")
+ })
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"recovery", "--show-keys"})
+ c.Assert(err, ErrorMatches, `command "show-keys" is not available on classic systems`)
+}
+
+func (s *SnapSuite) TestRecoveryShowRecoveryKeyHappy(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, Equals, "GET")
+ c.Check(r.URL.Path, Equals, "/v2/system-recovery-keys")
+ c.Check(r.URL.RawQuery, Equals, "")
+ fmt.Fprintln(w, `{"type": "sync", "result": {"recovery-key": "61665-00531-54469-09783-47273-19035-40077-28287", "reinstall-key":"1234"}}`)
+ default:
+ c.Fatalf("expected to get 1 requests, now on %d", n+1)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"recovery", "--show-keys"})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, `recovery: 61665-00531-54469-09783-47273-19035-40077-28287
+reinstall: 1234
+`)
+ c.Check(s.Stderr(), Equals, "")
+ c.Check(n, Equals, 1)
+}
diff --git a/cmd/snapd-generator/main.c b/cmd/snapd-generator/main.c
index 006ea84e92..fad70b496e 100644
--- a/cmd/snapd-generator/main.c
+++ b/cmd/snapd-generator/main.c
@@ -185,7 +185,7 @@ int ensure_fusesquashfs_inside_container(const char *normal_dir)
fprintf(stderr, "cannot open %s: %m\n", fname);
return 2;
}
- fprintf(f, "[Mount]\nType=%s\n", fstype);
+ fprintf(f, "[Mount]\nType=%s\nOptions=nodev,ro,x-gdu.hide,allow_other\nLazyUnmount=yes\n", fstype);
}
return 0;
diff --git a/daemon/api.go b/daemon/api.go
index cac8c9c032..870732cc81 100644
--- a/daemon/api.go
+++ b/daemon/api.go
@@ -114,6 +114,7 @@ var api = []*Command{
systemsCmd,
systemsActionCmd,
routineConsoleConfStartCmd,
+ systemRecoveryKeysCmd,
}
var servicestateControl = servicestate.Control
diff --git a/daemon/api_system_recovery_keys.go b/daemon/api_system_recovery_keys.go
new file mode 100644
index 0000000000..65fe954c3f
--- /dev/null
+++ b/daemon/api_system_recovery_keys.go
@@ -0,0 +1,54 @@
+// -*- 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 daemon
+
+import (
+ "net/http"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/secboot"
+)
+
+var systemRecoveryKeysCmd = &Command{
+ Path: "/v2/system-recovery-keys",
+ GET: getSystemRecoveryKeys,
+ RootOnly: true,
+}
+
+func getSystemRecoveryKeys(c *Command, r *http.Request, user *auth.UserState) Response {
+ var rsp client.SystemRecoveryKeysResponse
+
+ rkey, err := secboot.RecoveryKeyFromFile(filepath.Join(dirs.SnapFDEDir, "recovery.key"))
+ if err != nil {
+ return InternalError(err.Error())
+ }
+ rsp.RecoveryKey = rkey.String()
+
+ reinstallKey, err := secboot.RecoveryKeyFromFile(filepath.Join(dirs.SnapFDEDir, "reinstall.key"))
+ if err != nil {
+ return InternalError(err.Error())
+ }
+ rsp.ReinstallKey = reinstallKey.String()
+
+ return SyncResponse(&rsp, nil)
+}
diff --git a/daemon/api_system_recovery_keys_test.go b/daemon/api_system_recovery_keys_test.go
new file mode 100644
index 0000000000..687a006849
--- /dev/null
+++ b/daemon/api_system_recovery_keys_test.go
@@ -0,0 +1,87 @@
+// -*- 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 daemon
+
+import (
+ "encoding/hex"
+ "io/ioutil"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/secboot"
+)
+
+func mockSystemRecoveryKeys(c *C) {
+ // same inputs/outputs as secboot:crypt_test.go in this test
+ rkeystr, err := hex.DecodeString("e1f01302c5d43726a9b85b4a8d9c7f6e")
+ c.Assert(err, IsNil)
+ rkeyPath := filepath.Join(dirs.SnapFDEDir, "recovery.key")
+ err = os.MkdirAll(filepath.Dir(rkeyPath), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(rkeyPath, []byte(rkeystr), 0644)
+ c.Assert(err, IsNil)
+
+ skeystr := "1234567890123456"
+ c.Assert(err, IsNil)
+ skeyPath := filepath.Join(dirs.SnapFDEDir, "reinstall.key")
+ err = ioutil.WriteFile(skeyPath, []byte(skeystr), 0644)
+ c.Assert(err, IsNil)
+}
+
+func (s *apiSuite) TestSystemGetRecoveryKeysAsRootHappy(c *C) {
+ if (secboot.RecoveryKey{}).String() == "not-implemented" {
+ c.Skip("needs working secboot recovery key")
+ }
+
+ s.daemon(c)
+ mockSystemRecoveryKeys(c)
+
+ req, err := http.NewRequest("GET", "/v2/system-recovery-keys", nil)
+ c.Assert(err, IsNil)
+
+ rsp := getSystemRecoveryKeys(systemRecoveryKeysCmd, req, nil).(*resp)
+ c.Assert(rsp.Status, Equals, 200)
+ srk := rsp.Result.(*client.SystemRecoveryKeysResponse)
+ c.Assert(srk, DeepEquals, &client.SystemRecoveryKeysResponse{
+ RecoveryKey: "61665-00531-54469-09783-47273-19035-40077-28287",
+ ReinstallKey: "12849-13363-13877-14391-12345-12849-13363-13877",
+ })
+}
+
+func (s *apiSuite) TestSystemGetRecoveryAsUserErrors(c *C) {
+ s.daemon(c)
+ mockSystemRecoveryKeys(c)
+
+ req, err := http.NewRequest("GET", "/v2/system-recovery-key", nil)
+ c.Assert(err, IsNil)
+
+ req.RemoteAddr = "pid=100;uid=1000;socket=;"
+ rec := httptest.NewRecorder()
+ systemsActionCmd.ServeHTTP(rec, req)
+
+ systemRecoveryKeysCmd.ServeHTTP(rec, req)
+ c.Assert(rec.Code, Equals, 401)
+}
diff --git a/gadget/gadget.go b/gadget/gadget.go
index ba5dd4ac86..207ac7288c 100644
--- a/gadget/gadget.go
+++ b/gadget/gadget.go
@@ -943,12 +943,6 @@ func IsCompatible(current, new *Info) error {
// PositionedVolumeFromGadget takes a gadget rootdir and positions the
// partitions as specified.
func PositionedVolumeFromGadget(gadgetRoot string) (*LaidOutVolume, error) {
- // TODO:UC20: since this is unconstrained via the model, it returns an
- // err == nil and an empty info when the gadgetRoot does not
- // actually contain the required gadget.yaml file (for example
- // when you have a typo in the args to snap-bootstrap
- // create-partitions). anyways just verify this more because
- // otherwise it's unhelpful :-/
info, err := ReadInfo(gadgetRoot, nil)
if err != nil {
return nil, err
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/gadget/validate.go b/gadget/validate.go
index 2219c90d39..63b13b68b3 100644
--- a/gadget/validate.go
+++ b/gadget/validate.go
@@ -50,10 +50,33 @@ func validateVolumeContentsPresence(gadgetSnapRootDir string, vol *LaidOutVolume
return nil
}
+func validateEncryptionSupport(info *Info) error {
+ for name, vol := range info.Volumes {
+ var haveSave bool
+ for _, s := range vol.Structure {
+ if s.Role == SystemSave {
+ haveSave = true
+ }
+ }
+ if !haveSave {
+ return fmt.Errorf("volume %q has no structure with system-save role", name)
+ }
+ // XXX: shall we make sure that size of ubuntu-save is reasonable?
+ }
+ return nil
+}
+
+type ValidationConstraints struct {
+ // EncryptedData when true indicates that the gadget will be used on a
+ // device where the data partition will be encrypted.
+ EncryptedData bool
+}
+
// Validate checks whether the given directory contains valid gadget snap
// metadata and a matching content, under the provided model constraints, which
-// are handled identically to ReadInfo().
-func Validate(gadgetSnapRootDir string, model Model) error {
+// are handled identically to ReadInfo(). Optionally takes additional validation
+// constraints, which for instance may only be known at run time,
+func Validate(gadgetSnapRootDir string, model Model, extra *ValidationConstraints) error {
info, err := ReadInfo(gadgetSnapRootDir, model)
if err != nil {
return fmt.Errorf("invalid gadget metadata: %v", err)
@@ -68,6 +91,12 @@ func Validate(gadgetSnapRootDir string, model Model) error {
return fmt.Errorf("invalid volume %q: %v", name, err)
}
}
-
+ if extra != nil {
+ if extra.EncryptedData {
+ if err := validateEncryptionSupport(info); err != nil {
+ return fmt.Errorf("gadget does not support encrypted data: %v", err)
+ }
+ }
+ }
return nil
}
diff --git a/gadget/validate_test.go b/gadget/validate_test.go
index a570f8dc2d..060fc1acd9 100644
--- a/gadget/validate_test.go
+++ b/gadget/validate_test.go
@@ -55,7 +55,7 @@ volumes:
`
makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent))
- err := gadget.Validate(s.dir, nil)
+ err := gadget.Validate(s.dir, nil, nil)
c.Assert(err, ErrorMatches, `invalid layout of volume "pc": cannot lay out structure #0 \("foo"\): content "foo.img": stat .*/foo.img: no such file or directory`)
}
@@ -83,7 +83,7 @@ volumes:
// only content for the first volume
makeSizedFile(c, filepath.Join(s.dir, "first.img"), 1, nil)
- err := gadget.Validate(s.dir, nil)
+ err := gadget.Validate(s.dir, nil, nil)
c.Assert(err, ErrorMatches, `invalid layout of volume "second": cannot lay out structure #0 \("second-foo"\): content "second.img": stat .*/second.img: no such file or directory`)
}
@@ -100,7 +100,7 @@ volumes:
`
makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent))
- err := gadget.Validate(s.dir, nil)
+ err := gadget.Validate(s.dir, nil, nil)
c.Assert(err, ErrorMatches, `invalid gadget metadata: bootloader must be one of .*`)
}
@@ -121,13 +121,13 @@ volumes:
`
makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent))
- err := gadget.Validate(s.dir, nil)
+ err := gadget.Validate(s.dir, nil, nil)
c.Assert(err, ErrorMatches, `invalid volume "bad": structure #0 \("bad-struct"\), content source:foo/: source path does not exist`)
// make it a file, which conflicts with foo/ as 'source'
fooPath := filepath.Join(s.dir, "foo")
makeSizedFile(c, fooPath, 1, nil)
- err = gadget.Validate(s.dir, nil)
+ err = gadget.Validate(s.dir, nil, nil)
c.Assert(err, ErrorMatches, `invalid volume "bad": structure #0 \("bad-struct"\), content source:foo/: cannot specify trailing / for a source which is not a directory`)
// make it a directory
@@ -136,7 +136,7 @@ volumes:
err = os.Mkdir(fooPath, 0755)
c.Assert(err, IsNil)
// validate should no longer complain
- err = gadget.Validate(s.dir, nil)
+ err = gadget.Validate(s.dir, nil, nil)
c.Assert(err, IsNil)
}
@@ -146,13 +146,13 @@ func (s *validateGadgetTestSuite) TestValidateClassic(c *C) {
`
makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent))
- err := gadget.Validate(s.dir, nil)
+ err := gadget.Validate(s.dir, nil, nil)
c.Assert(err, IsNil)
- err = gadget.Validate(s.dir, &modelConstraints{classic: true})
+ err = gadget.Validate(s.dir, &modelConstraints{classic: true}, nil)
c.Assert(err, IsNil)
- err = gadget.Validate(s.dir, &modelConstraints{classic: false})
+ err = gadget.Validate(s.dir, &modelConstraints{classic: false}, nil)
c.Assert(err, ErrorMatches, "invalid gadget metadata: bootloader not declared in any volume")
}
@@ -174,7 +174,52 @@ volumes:
role: %[1]s
`, role)
makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContent))
- err := gadget.Validate(s.dir, nil)
+ err := gadget.Validate(s.dir, nil, nil)
c.Assert(err, ErrorMatches, fmt.Sprintf(`invalid gadget metadata: invalid volume "pc": cannot have more than one partition with %s role`, role))
}
}
+
+var gadgetYamlContentNoSave = `
+volumes:
+ vol1:
+ bootloader: grub
+ structure:
+ - name: ubuntu-seed
+ role: system-seed
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ filesystem: ext4
+ - name: ubuntu-boot
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ filesystem: ext4
+ - name: ubuntu-data
+ role: system-data
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ filesystem: ext4
+`
+
+var gadgetYamlContentWithSave = gadgetYamlContentNoSave + `
+ - name: ubuntu-save
+ role: system-save
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ filesystem: ext4
+`
+
+func (s *validateGadgetTestSuite) TestValidateEncryptionSupportErr(c *C) {
+ makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContentNoSave))
+ err := gadget.Validate(s.dir, &modelConstraints{systemSeed: true}, &gadget.ValidationConstraints{
+ EncryptedData: true,
+ })
+ c.Assert(err, ErrorMatches, `gadget does not support encrypted data: volume "vol1" has no structure with system-save role`)
+}
+
+func (s *validateGadgetTestSuite) TestValidateEncryptionSupportHappy(c *C) {
+ makeSizedFile(c, filepath.Join(s.dir, "meta/gadget.yaml"), 0, []byte(gadgetYamlContentWithSave))
+ err := gadget.Validate(s.dir, &modelConstraints{systemSeed: true}, &gadget.ValidationConstraints{
+ EncryptedData: true,
+ })
+ c.Assert(err, IsNil)
+}
diff --git a/interfaces/builtin/fwupd.go b/interfaces/builtin/fwupd.go
index 3ea9ee51ce..c7814fbafe 100644
--- a/interfaces/builtin/fwupd.go
+++ b/interfaces/builtin/fwupd.go
@@ -72,6 +72,10 @@ const fwupdPermanentSlotAppArmor = `
# Allow write access for efi firmware updater
/boot/efi/{,**/} r,
+ # allow access to fwupd* and fw/ under boot/ for core systems
+ /boot/efi/EFI/boot/fwupd*.efi* rw,
+ /boot/efi/EFI/boot/fw/** rw,
+ # allow access to fwupd* and fw/ under ubuntu/ for classic systems
/boot/efi/EFI/ubuntu/fwupd*.efi* rw,
/boot/efi/EFI/ubuntu/fw/** rw,
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/builtin/x11.go b/interfaces/builtin/x11.go
index 9ff7013a21..4a9314fec7 100644
--- a/interfaces/builtin/x11.go
+++ b/interfaces/builtin/x11.go
@@ -20,13 +20,15 @@
package builtin
import (
+ "fmt"
"strings"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/mount"
"github.com/snapcore/snapd/interfaces/seccomp"
"github.com/snapcore/snapd/interfaces/udev"
- "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
@@ -162,8 +164,67 @@ type x11Interface struct {
commonInterface
}
+func (iface *x11Interface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ if implicitSystemConnectedSlot(slot) {
+ // X11 slot is provided by the host system. Bring the host's
+ // /tmp/.X11-unix/ directory over to the snap mount namespace.
+ return spec.AddMountEntry(osutil.MountEntry{
+ Name: "/var/lib/snapd/hostfs/tmp/.X11-unix",
+ Dir: "/tmp/.X11-unix",
+ Options: []string{"bind", "ro"},
+ })
+ }
+
+ // X11 slot is provided by another snap on the system. Bring that snap's
+ // /tmp/.X11-unix/ directory over to the snap mount namespace. Here we
+ // rely on the predictable naming of the private /tmp directory of the
+ // slot-side snap which is currently provided by snap-confine.
+
+ // But if the same snap is providing both the plug and the slot, this is
+ // not necessary.
+ if plug.Snap().InstanceName() == slot.Snap().InstanceName() {
+ return nil
+ }
+ slotSnapName := slot.Snap().InstanceName()
+ return spec.AddMountEntry(osutil.MountEntry{
+ Name: fmt.Sprintf("/var/lib/snapd/hostfs/tmp/snap.%s/tmp/.X11-unix", slotSnapName),
+ Dir: "/tmp/.X11-unix",
+ Options: []string{"bind", "ro"},
+ })
+}
+
+func (iface *x11Interface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ if err := iface.commonInterface.AppArmorConnectedPlug(spec, plug, slot); err != nil {
+ return err
+ }
+ // Consult the comments in MountConnectedPlug for the rationale of the control flow.
+ if implicitSystemConnectedSlot(slot) {
+ spec.AddUpdateNS(`
+ /{,var/lib/snapd/hostfs/}tmp/.X11-unix/ rw,
+ mount options=(rw, bind) /var/lib/snapd/hostfs/tmp/.X11-unix/ -> /tmp/.X11-unix/,
+ mount options=(ro, remount, bind) -> /tmp/.X11-unix/,
+ mount options=(rslave) -> /tmp/.X11-unix/,
+ umount /tmp/.X11-unix/,
+ `)
+ return nil
+ }
+ if plug.Snap().InstanceName() == slot.Snap().InstanceName() {
+ return nil
+ }
+ slotSnapName := slot.Snap().InstanceName()
+ spec.AddUpdateNS(fmt.Sprintf(`
+ /tmp/.X11-unix/ rw,
+ /var/lib/snapd/hostfs/tmp/snap.%s/tmp/.X11-unix/ rw,
+ mount options=(rw, bind) /var/lib/snapd/hostfs/tmp/snap.%s/tmp/.X11-unix/ -> /tmp/.X11-unix/,
+ mount options=(ro, remount, bind) -> /tmp/.X11-unix/,
+ mount options=(rslave) -> /tmp/.X11-unix/,
+ umount /tmp/.X11-unix/,
+ `, slotSnapName, slotSnapName))
+ return nil
+}
+
func (iface *x11Interface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
- if !release.OnClassic {
+ if !implicitSystemConnectedSlot(slot) {
old := "###PLUG_SECURITY_TAGS###"
new := plugAppLabelExpr(plug)
snippet := strings.Replace(x11ConnectedSlotAppArmor, old, new, -1)
@@ -173,21 +234,21 @@ func (iface *x11Interface) AppArmorConnectedSlot(spec *apparmor.Specification, p
}
func (iface *x11Interface) SecCompPermanentSlot(spec *seccomp.Specification, slot *snap.SlotInfo) error {
- if !release.OnClassic {
+ if !implicitSystemPermanentSlot(slot) {
spec.AddSnippet(x11PermanentSlotSecComp)
}
return nil
}
func (iface *x11Interface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *snap.SlotInfo) error {
- if !release.OnClassic {
+ if !implicitSystemPermanentSlot(slot) {
spec.AddSnippet(x11PermanentSlotAppArmor)
}
return nil
}
func (iface *x11Interface) UDevPermanentSlot(spec *udev.Specification, slot *snap.SlotInfo) error {
- if !release.OnClassic {
+ if !implicitSystemPermanentSlot(slot) {
spec.TriggerSubsystem("input")
spec.TagDevice(`KERNEL=="tty[0-9]*"`)
spec.TagDevice(`KERNEL=="mice"`)
diff --git a/interfaces/builtin/x11_test.go b/interfaces/builtin/x11_test.go
index e7aeaa8edd..669a4cccc0 100644
--- a/interfaces/builtin/x11_test.go
+++ b/interfaces/builtin/x11_test.go
@@ -25,8 +25,10 @@ import (
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/apparmor"
"github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/interfaces/mount"
"github.com/snapcore/snapd/interfaces/seccomp"
"github.com/snapcore/snapd/interfaces/udev"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
@@ -36,6 +38,8 @@ type X11InterfaceSuite struct {
iface interfaces.Interface
coreSlotInfo *snap.SlotInfo
coreSlot *interfaces.ConnectedSlot
+ corePlugInfo *snap.PlugInfo
+ corePlug *interfaces.ConnectedPlug
classicSlotInfo *snap.SlotInfo
classicSlot *interfaces.ConnectedSlot
plugInfo *snap.PlugInfo
@@ -57,8 +61,15 @@ apps:
const x11CoreYaml = `name: x11
version: 0
apps:
- app1:
- slots: [x11]
+ app:
+ slots: [x11-provider]
+ plugs: [x11-consumer]
+plugs:
+ x11-consumer:
+ interface: x11
+slots:
+ x11-provider:
+ interface: x11
`
// an x11 slot on the core snap (as automatically added on classic)
@@ -72,7 +83,8 @@ slots:
func (s *X11InterfaceSuite) SetUpTest(c *C) {
s.plug, s.plugInfo = MockConnectedPlug(c, x11MockPlugSnapInfoYaml, nil, "x11")
- s.coreSlot, s.coreSlotInfo = MockConnectedSlot(c, x11CoreYaml, nil, "x11")
+ s.coreSlot, s.coreSlotInfo = MockConnectedSlot(c, x11CoreYaml, nil, "x11-provider")
+ s.corePlug, s.corePlugInfo = MockConnectedPlug(c, x11CoreYaml, nil, "x11-consumer")
s.classicSlot, s.classicSlotInfo = MockConnectedSlot(c, x11ClassicYaml, nil, "x11")
}
@@ -89,28 +101,80 @@ func (s *X11InterfaceSuite) TestSanitizePlug(c *C) {
c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
}
+func (s *X11InterfaceSuite) TestMountSpec(c *C) {
+ // case A: x11 slot is provided by the system
+ spec := &mount.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil)
+ c.Assert(spec.MountEntries(), DeepEquals, []osutil.MountEntry{{
+ Name: "/var/lib/snapd/hostfs/tmp/.X11-unix",
+ Dir: "/tmp/.X11-unix",
+ Options: []string{"bind", "ro"},
+ }})
+ c.Assert(spec.UserMountEntries(), HasLen, 0)
+
+ // case B: x11 slot is provided by another snap on the system
+ spec = &mount.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil)
+ c.Assert(spec.MountEntries(), DeepEquals, []osutil.MountEntry{{
+ Name: "/var/lib/snapd/hostfs/tmp/snap.x11/tmp/.X11-unix",
+ Dir: "/tmp/.X11-unix",
+ Options: []string{"bind", "ro"},
+ }})
+ c.Assert(spec.UserMountEntries(), HasLen, 0)
+
+ // case C: x11 slot is both provided and consumed by a snap on the system.
+ spec = &mount.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.corePlug, s.coreSlot), IsNil)
+ c.Assert(spec.MountEntries(), HasLen, 0)
+ c.Assert(spec.UserMountEntries(), HasLen, 0)
+}
+
func (s *X11InterfaceSuite) TestAppArmorSpec(c *C) {
- // on a core system with x11 slot coming from a regular app snap.
- restore := release.MockOnClassic(false)
+ // case A: x11 slot is provided by the classic system
+ restore := release.MockOnClassic(true)
defer restore()
- // connected plug to core slot
+ // Plug side connection permissions
spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "fontconfig")
+ c.Assert(spec.UpdateNS(), HasLen, 1)
+ c.Assert(spec.UpdateNS()[0], testutil.Contains, `mount options=(rw, bind) /var/lib/snapd/hostfs/tmp/.X11-unix/ -> /tmp/.X11-unix/,`)
+
+ // case B: x11 slot is provided by another snap on the system
+ restore = release.MockOnClassic(false)
+ defer restore()
+
+ // Plug side connection permissions
+ spec = &apparmor.Specification{}
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "fontconfig")
+ c.Assert(spec.UpdateNS(), HasLen, 1)
+ c.Assert(spec.UpdateNS()[0], testutil.Contains, `mount options=(rw, bind) /var/lib/snapd/hostfs/tmp/snap.x11/tmp/.X11-unix/ -> /tmp/.X11-unix/,`)
- // connected core slot to plug
+ // Slot side connection permissions
spec = &apparmor.Specification{}
c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil)
- c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app1"})
- c.Assert(spec.SnippetForTag("snap.x11.app1"), testutil.Contains, `peer=(label="snap.consumer.app"),`)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app"})
+ c.Assert(spec.SnippetForTag("snap.x11.app"), testutil.Contains, `peer=(label="snap.consumer.app"),`)
+ c.Assert(spec.UpdateNS(), HasLen, 0)
- // permanent core slot
+ // Slot side permantent permissions
spec = &apparmor.Specification{}
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
- c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app1"})
- c.Assert(spec.SnippetForTag("snap.x11.app1"), testutil.Contains, "capability sys_tty_config,")
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app"})
+ c.Assert(spec.SnippetForTag("snap.x11.app"), testutil.Contains, "capability sys_tty_config,")
+ c.Assert(spec.UpdateNS(), HasLen, 0)
+
+ // case C: x11 slot is both provided and consumed by a snap on the system.
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.corePlug, s.coreSlot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.x11.app"})
+ c.Assert(spec.SnippetForTag("snap.x11.app"), testutil.Contains, "fontconfig")
+ // Self-connection does not need bind mounts, so no additional permissions are provided to snap-update-ns.
+ c.Assert(spec.UpdateNS(), HasLen, 0)
}
func (s *X11InterfaceSuite) TestAppArmorSpecOnClassic(c *C) {
@@ -163,8 +227,8 @@ func (s *X11InterfaceSuite) TestSecCompOnCore(c *C) {
c.Assert(err, IsNil)
// both app and x11 have secomp rules set
- c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.x11.app1"})
- c.Assert(seccompSpec.SnippetForTag("snap.x11.app1"), testutil.Contains, "listen\n")
+ c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.x11.app"})
+ c.Assert(seccompSpec.SnippetForTag("snap.x11.app"), testutil.Contains, "listen\n")
c.Assert(seccompSpec.SnippetForTag("snap.consumer.app"), testutil.Contains, "bind\n")
}
@@ -177,16 +241,16 @@ func (s *X11InterfaceSuite) TestUDev(c *C) {
c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
c.Assert(spec.Snippets(), HasLen, 6)
c.Assert(spec.Snippets(), testutil.Contains, `# x11
-KERNEL=="event[0-9]*", TAG+="snap_x11_app1"`)
+KERNEL=="event[0-9]*", TAG+="snap_x11_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `# x11
-KERNEL=="mice", TAG+="snap_x11_app1"`)
+KERNEL=="mice", TAG+="snap_x11_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `# x11
-KERNEL=="mouse[0-9]*", TAG+="snap_x11_app1"`)
+KERNEL=="mouse[0-9]*", TAG+="snap_x11_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `# x11
-KERNEL=="ts[0-9]*", TAG+="snap_x11_app1"`)
+KERNEL=="ts[0-9]*", TAG+="snap_x11_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `# x11
-KERNEL=="tty[0-9]*", TAG+="snap_x11_app1"`)
- c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_x11_app1", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_x11_app1 $devpath $major:$minor"`)
+KERNEL=="tty[0-9]*", TAG+="snap_x11_app"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_x11_app", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_x11_app $devpath $major:$minor"`)
c.Assert(spec.TriggeredSubsystems(), DeepEquals, []string{"input"})
// on a classic system with x11 slot coming from the core snap.
@@ -194,7 +258,7 @@ KERNEL=="tty[0-9]*", TAG+="snap_x11_app1"`)
defer restore()
spec = &udev.Specification{}
- c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
+ c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil)
c.Assert(spec.Snippets(), HasLen, 0)
c.Assert(spec.TriggeredSubsystems(), IsNil)
}
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/devicestate_gadget_test.go b/overlord/devicestate/devicestate_gadget_test.go
index 4c914ada70..0d21c6207c 100644
--- a/overlord/devicestate/devicestate_gadget_test.go
+++ b/overlord/devicestate/devicestate_gadget_test.go
@@ -82,6 +82,13 @@ volumes:
size: 50M
`
+var uc20gadgetYamlWithSave = uc20gadgetYaml + `
+ - name: ubuntu-save
+ role: system-save
+ type: 21686148-6449-6E6F-744E-656564454649
+ size: 50M
+`
+
func (s *deviceMgrGadgetSuite) setupModelWithGadget(c *C, gadget string) {
s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
"architecture": "amd64",
diff --git a/overlord/devicestate/devicestate_install_mode_test.go b/overlord/devicestate/devicestate_install_mode_test.go
index 6999219fde..4493f0fa8e 100644
--- a/overlord/devicestate/devicestate_install_mode_test.go
+++ b/overlord/devicestate/devicestate_install_mode_test.go
@@ -121,7 +121,7 @@ func (s *deviceMgrInstallModeSuite) makeMockInstalledPcGadget(c *C, grade, gadge
Active: true,
})
snaptest.MockSnapWithFiles(c, "name: pc\ntype: gadget", si, [][]string{
- {"meta/gadget.yaml", gadgetYaml + gadgetDefaultsYaml},
+ {"meta/gadget.yaml", uc20gadgetYamlWithSave + gadgetDefaultsYaml},
})
si = &snap.SideInfo{
@@ -473,6 +473,10 @@ func (s *deviceMgrInstallModeSuite) TestInstallSecuredWithTPMAndSave(c *C) {
c.Check(filepath.Join(boot.InstallHostFDEDataDir, "recovery.key"), testutil.FileEquals, dataRecoveryKey[:])
c.Check(filepath.Join(boot.InstallHostFDEDataDir, "ubuntu-save.key"), testutil.FileEquals, saveKey[:])
c.Check(filepath.Join(boot.InstallHostFDEDataDir, "reinstall.key"), testutil.FileEquals, reinstallKey[:])
+ marker, err := ioutil.ReadFile(filepath.Join(boot.InstallHostFDEDataDir, "marker"))
+ c.Assert(err, IsNil)
+ c.Check(marker, HasLen, 32)
+ c.Check(filepath.Join(boot.InstallHostFDESaveDir, "marker"), testutil.FileEquals, marker)
}
func (s *deviceMgrInstallModeSuite) TestInstallSecuredBypassEncryption(c *C) {
@@ -726,3 +730,70 @@ func (s *deviceMgrInstallModeSuite) TestInstallModeWritesModel(c *C) {
c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "device/model"), testutil.FileEquals, buf.String())
}
+
+func (s *deviceMgrInstallModeSuite) testInstallGadgetNoSave(c *C) {
+ err := ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd/modeenv"),
+ []byte("mode=install\n"), 0644)
+ c.Assert(err, IsNil)
+
+ s.state.Lock()
+ s.makeMockInstalledPcGadget(c, "dangerous", "")
+ info, err := snapstate.CurrentInfo(s.state, "pc")
+ c.Assert(err, IsNil)
+ // replace gadget yaml with one that has no ubuntu-save
+ c.Assert(uc20gadgetYaml, Not(testutil.Contains), "ubuntu-save")
+ err = ioutil.WriteFile(filepath.Join(info.MountDir(), "meta/gadget.yaml"), []byte(uc20gadgetYaml), 0644)
+ c.Assert(err, IsNil)
+ devicestate.SetSystemMode(s.mgr, "install")
+ s.state.Unlock()
+
+ s.settle(c)
+}
+
+func (s *deviceMgrInstallModeSuite) TestInstallWithEncryptionValidatesGadgetErr(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ gadget.ContentObserver) (*install.InstalledSystemSideData, error) {
+ return nil, fmt.Errorf("unexpected call")
+ })
+ defer restore()
+
+ // pretend we have a TPM
+ restore = devicestate.MockSecbootCheckKeySealingSupported(func() error { return nil })
+ defer restore()
+
+ s.testInstallGadgetNoSave(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ installSystem := s.findInstallSystem()
+ c.Check(installSystem.Err(), ErrorMatches, `(?ms)cannot perform the following tasks:
+- Setup system for run mode \(cannot use gadget: gadget does not support encrypted data: volume "pc" has no structure with system-save role\)`)
+ // no restart request on failure
+ c.Check(s.restartRequests, HasLen, 0)
+}
+
+func (s *deviceMgrInstallModeSuite) TestInstallWithoutEncryptionValidatesGadgetWithoutSaveHappy(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ restore = devicestate.MockInstallRun(func(gadgetRoot, device string, options install.Options, _ gadget.ContentObserver) (*install.InstalledSystemSideData, error) {
+ return nil, nil
+ })
+ defer restore()
+
+ // pretend we have a TPM
+ restore = devicestate.MockSecbootCheckKeySealingSupported(func() error { return fmt.Errorf("TPM2 not available") })
+ defer restore()
+
+ s.testInstallGadgetNoSave(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ installSystem := s.findInstallSystem()
+ c.Check(installSystem.Err(), IsNil)
+ c.Check(s.restartRequests, HasLen, 1)
+}
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..79ba924456 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,51 @@ 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")
+ switch hsup.Snap {
+ case "core", "core18", "snapd":
+ // ignore
+ default:
+ // snaps other than core/core18/snapd
+ var waitsForMarkPreseeded, waitsForPreviousSnapHook, waitsForPreviousSnap bool
+ for _, wt := range hookEdgeTask.WaitTasks() {
+ switch wt.Kind() {
+ case "setup-aliases":
+ continue
+ case "run-hook":
+ var wtsup hookstate.HookSetup
+ c.Assert(wt.Get("hook-setup", &wtsup), IsNil)
+ c.Check(wtsup.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 +314,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 +330,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 +354,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 +380,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 +441,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 +449,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/overlord/devicestate/handlers_install.go b/overlord/devicestate/handlers_install.go
index 1a231b0c1f..e22864879e 100644
--- a/overlord/devicestate/handlers_install.go
+++ b/overlord/devicestate/handlers_install.go
@@ -35,6 +35,7 @@ import (
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/randutil"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/sysconfig"
@@ -132,6 +133,14 @@ func (m *DeviceManager) doSetupRunSystem(t *state.Task, _ *tomb.Tomb) error {
}
bopts.Encrypt = useEncryption
+ // make sure that gadget is usable for the set up we want to use it in
+ gadgetContaints := gadget.ValidationConstraints{
+ EncryptedData: useEncryption,
+ }
+ if err := gadget.Validate(gadgetDir, deviceCtx.Model(), &gadgetContaints); err != nil {
+ return fmt.Errorf("cannot use gadget: %v", err)
+ }
+
var trustedInstallObserver *boot.TrustedAssetsInstallObserver
// get a nice nil interface by default
var installObserver gadget.ContentObserver
@@ -178,6 +187,10 @@ func (m *DeviceManager) doSetupRunSystem(t *state.Task, _ *tomb.Tomb) error {
if err := saveKeys(installedSystem.KeysForRoles); err != nil {
return err
}
+ // write markers containing a secret to pair data and save
+ if err := writeMarkers(); err != nil {
+ return err
+ }
}
// keep track of the model we installed
@@ -225,6 +238,35 @@ func (m *DeviceManager) doSetupRunSystem(t *state.Task, _ *tomb.Tomb) error {
return nil
}
+// writeMarkers writes markers containing the same secret to pair data and save.
+func writeMarkers() error {
+ // ensure directory for markers exists
+ if err := os.MkdirAll(boot.InstallHostFDEDataDir, 0755); err != nil {
+ return err
+ }
+ if err := os.MkdirAll(boot.InstallHostFDESaveDir, 0755); err != nil {
+ return err
+ }
+
+ // generate a secret random marker
+ markerSecret, err := randutil.CryptoTokenBytes(32)
+ if err != nil {
+ return fmt.Errorf("cannot create ubuntu-data/save marker secret: %v", err)
+ }
+
+ dataMarker := filepath.Join(boot.InstallHostFDEDataDir, "marker")
+ if err := osutil.AtomicWriteFile(dataMarker, markerSecret, 0600, 0); err != nil {
+ return err
+ }
+
+ saveMarker := filepath.Join(boot.InstallHostFDESaveDir, "marker")
+ if err := osutil.AtomicWriteFile(saveMarker, markerSecret, 0600, 0); err != nil {
+ return err
+ }
+
+ return nil
+}
+
func saveKeys(keysForRoles map[string]*install.EncryptionKeySet) error {
dataKeySet := keysForRoles[gadget.SystemData]
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/secboot/encrypt.go b/secboot/encrypt.go
index 550db1f512..4b85661b7d 100644
--- a/secboot/encrypt.go
+++ b/secboot/encrypt.go
@@ -21,6 +21,8 @@ package secboot
import (
"crypto/rand"
+ "fmt"
+ "io"
"os"
"path/filepath"
@@ -32,6 +34,8 @@ const (
// key.
encryptionKeySize = 64
+ // XXX: needs to be in sync with
+ // github.com/snapcore/secboot/crypto.go:"type RecoveryKey"
// Size of the recovery key.
recoveryKeySize = 16
)
@@ -74,3 +78,24 @@ func (key RecoveryKey) Save(filename string) error {
}
return osutil.AtomicWriteFile(filename, key[:], 0600, 0)
}
+
+func RecoveryKeyFromFile(recoveryKeyFile string) (*RecoveryKey, error) {
+ f, err := os.Open(recoveryKeyFile)
+ if err != nil {
+ return nil, fmt.Errorf("cannot open recovery key: %v", err)
+ }
+ defer f.Close()
+ st, err := f.Stat()
+ if err != nil {
+ return nil, fmt.Errorf("cannot stat recovery key: %v", err)
+ }
+ if st.Size() != int64(len(RecoveryKey{})) {
+ return nil, fmt.Errorf("cannot read recovery key: unexpected size %v for the recovery key file %s", st.Size(), recoveryKeyFile)
+ }
+
+ var rkey RecoveryKey
+ if _, err := io.ReadFull(f, rkey[:]); err != nil {
+ return nil, fmt.Errorf("cannot read recovery key: %v", err)
+ }
+ return &rkey, nil
+}
diff --git a/secboot/encrypt_dummy.go b/secboot/encrypt_dummy.go
new file mode 100644
index 0000000000..60c9cdd661
--- /dev/null
+++ b/secboot/encrypt_dummy.go
@@ -0,0 +1,25 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build nosecboot
+
+/*
+ * 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 secboot
+
+func (k RecoveryKey) String() string {
+ return "not-implemented"
+}
diff --git a/secboot/encrypt_tpm.go b/secboot/encrypt_tpm.go
index 44547edec8..32c1b137ff 100644
--- a/secboot/encrypt_tpm.go
+++ b/secboot/encrypt_tpm.go
@@ -51,3 +51,7 @@ func FormatEncryptedDevice(key EncryptionKey, label, node string) error {
func AddRecoveryKey(key EncryptionKey, rkey RecoveryKey, node string) error {
return sbAddRecoveryKeyToLUKS2Container(node, key[:], sb.RecoveryKey(rkey))
}
+
+func (k RecoveryKey) String() string {
+ return sb.RecoveryKey(k).String()
+}
diff --git a/secboot/secboot_tpm.go b/secboot/secboot_tpm.go
index 0a1fc64685..9eeb90005f 100644
--- a/secboot/secboot_tpm.go
+++ b/secboot/secboot_tpm.go
@@ -291,11 +291,6 @@ func LockTPMSealedKeys() error {
return fmt.Errorf("cannot lock TPM: %v", tpmErr)
}
defer tpm.Close()
- // Also check if the TPM device is enabled. The platform firmware may disable the storage
- // and endorsement hierarchies, but the device will remain visible to the operating system.
- if !isTPMEnabled(tpm) {
- return nil
- }
// Lock access to the sealed keys. This should be called whenever there
// is a TPM device detected, regardless of whether secure boot is enabled
@@ -387,7 +382,7 @@ func UnlockVolumeUsingSealedKeyIfEncrypted(
var mapperName string
err = func() error {
defer func() {
- if opts.LockKeysOnFinish && tpmDeviceAvailable {
+ if opts.LockKeysOnFinish && tpmErr == nil {
// Lock access to the sealed keys. This should be called whenever there
// is a TPM device detected, regardless of whether secure boot is enabled
// or there is an encrypted volume to unlock. Note that snap-bootstrap can
diff --git a/secboot/secboot_tpm_test.go b/secboot/secboot_tpm_test.go
index bb802cdf69..7dc58704bf 100644
--- a/secboot/secboot_tpm_test.go
+++ b/secboot/secboot_tpm_test.go
@@ -251,20 +251,25 @@ func (s *secbootSuite) TestLockTPMSealedKeys(c *C) {
tpmErr: fmt.Errorf("failed to connect to tpm"),
expError: "cannot lock TPM: failed to connect to tpm",
},
- // tpm is not enabled, no errors
+ // no TPM2 device, shouldn't return an error
+ {
+ tpmErr: sb.ErrNoTPM2Device,
+ },
+ // tpm is not enabled but we can lock it
{
tpmEnabled: false,
+ lockOk: true,
},
// can't lock pcr protection profile
{
- lockOk: false,
tpmEnabled: true,
+ lockOk: false,
expError: "block failed",
},
// tpm enabled, we can lock it
{
- lockOk: true,
tpmEnabled: true,
+ lockOk: true,
},
}
@@ -290,22 +295,16 @@ func (s *secbootSuite) TestLockTPMSealedKeys(c *C) {
defer restore()
err := secboot.LockTPMSealedKeys()
- if tc.expError != "" {
+ if tc.expError == "" {
+ c.Assert(err, IsNil)
+ } else {
c.Assert(err, ErrorMatches, tc.expError)
- // if there was not a tpm error, we should have locked it
- if tc.tpmErr == nil {
- c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 1)
- } else {
- c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 0)
- }
+ }
+ // if there was no TPM connection error, we should have tried to lock it
+ if tc.tpmErr == nil {
+ c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 1)
} else {
- c.Assert(err, IsNil)
- // if the tpm was enabled, we should have locked it
- if tc.tpmEnabled {
- c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 1)
- } else {
- c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 0)
- }
+ c.Assert(sbBlockPCRProtectionPolicesCalls, Equals, 0)
}
}
}
@@ -441,6 +440,10 @@ func (s *secbootSuite) TestUnlockVolumeUsingSealedKeyIfEncrypted(c *C) {
// tpm disabled, no encrypted device
disk: mockDiskWithUnencDev,
}, {
+ // tpm disabled, no encrypted device, lock succeeds
+ lockRequest: true, lockOk: true,
+ disk: mockDiskWithUnencDev,
+ }, {
// tpm disabled, has encrypted device, unlocked using the recovery key
hasEncdev: true,
rkAllow: true,
diff --git a/snap/pack/pack.go b/snap/pack/pack.go
index e1e2a4b0cd..7f751df4aa 100644
--- a/snap/pack/pack.go
+++ b/snap/pack/pack.go
@@ -126,7 +126,10 @@ func loadAndValidate(sourceDir string) (*snap.Info, error) {
}
if info.SnapType == snap.TypeGadget {
- if err := gadget.Validate(sourceDir, nil); err != nil {
+ // TODO:UC20: optionally pass model
+ // TODO:UC20: pass validation constraints which indicate intent
+ // to have data encrypted
+ if err := gadget.Validate(sourceDir, nil, nil); err != nil {
return nil, err
}
}
diff --git a/spread.yaml b/spread.yaml
index 9aad572900..de2634119a 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -103,7 +103,7 @@ backends:
# XXX: old name, remove once new spread is deployed
secureboot: true
- ubuntu-20.10-64:
- workers: 6
+ workers: 8
image: ubuntu-20.10-64
- debian-9-64:
diff --git a/tests/main/interfaces-x11-unix-socket/task.yaml b/tests/main/interfaces-x11-unix-socket/task.yaml
new file mode 100644
index 0000000000..6233524297
--- /dev/null
+++ b/tests/main/interfaces-x11-unix-socket/task.yaml
@@ -0,0 +1,51 @@
+summary: ensure that the x11 interface shares UNIX domain sockets
+description: |
+ In addition to the abstract "@/tmp/.X11-unix/X?" socket an X
+ server listens on, it also listens on a regular UNIX domain socket
+ in /tmp/.X11-unix.
+
+ The x11 plug will bind mount the socket directory from the slot
+ providing it: either the host system's /tmp for the implicit
+ system:x11 slot, or an application snap's private /tmp if it is a
+ regular slot.
+
+restore: |
+ rm -f /tmp/.X11-unix/X0
+
+execute: |
+ echo "Install test snaps"
+ "$TESTSTOOLS"/snaps-state install-local x11-client
+ "$TESTSTOOLS"/snaps-state install-local x11-server
+
+ echo "Ensure x11 plug is not connected to implicit slot"
+ snap disconnect x11-client:x11
+
+ echo "Connect x11-client to x11-server"
+ snap connect x11-client:x11 x11-server:x11
+
+ echo "The snaps can communicate via the unix domain socket in /tmp"
+ x11-server &
+ retry -n 4 --wait 0.5 test -e /tmp/snap.x11-server/tmp/.X11-unix/X0
+ x11-client | MATCH "Hello from xserver"
+
+ echo "The client cannot remove the unix domain sockets shared with it"
+ not x11-client.rm -f /tmp/.X11-unix/X0
+
+ # Ubuntu Core does not have a system:x11 implicit slot
+ if [[ "$SPREAD_SYSTEM" = ubuntu-core-* ]]; then
+ exit 0
+ fi
+
+ echo "Connect the client snap to the implicit system slot"
+ snap disconnect x11-client:x11
+ snap connect x11-client:x11
+
+ echo "The client can communicate with the host system X socket"
+ mkdir -p /tmp/.X11-unix
+ rm -f /tmp/.X11-unix/X0
+ echo "Hello from host system" | nc -l -w 1 -U /tmp/.X11-unix/X0 &
+ retry -n 4 --wait 0.5 test -e /tmp/.X11-unix/X0
+ x11-client | MATCH "Hello from host system"
+
+ echo "The client cannot remove host system sockets either"
+ not x11-client.rm -f /tmp/.X11-unix/X0
diff --git a/tests/main/interfaces-x11-unix-socket/x11-client/bin/rm.sh b/tests/main/interfaces-x11-unix-socket/x11-client/bin/rm.sh
new file mode 100755
index 0000000000..3dacb522bf
--- /dev/null
+++ b/tests/main/interfaces-x11-unix-socket/x11-client/bin/rm.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec rm "$@"
diff --git a/tests/main/interfaces-x11-unix-socket/x11-client/bin/xclient.sh b/tests/main/interfaces-x11-unix-socket/x11-client/bin/xclient.sh
new file mode 100755
index 0000000000..94685b2bcc
--- /dev/null
+++ b/tests/main/interfaces-x11-unix-socket/x11-client/bin/xclient.sh
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec nc -w 30 -U /tmp/.X11-unix/X0
diff --git a/tests/main/interfaces-x11-unix-socket/x11-client/meta/snap.yaml b/tests/main/interfaces-x11-unix-socket/x11-client/meta/snap.yaml
new file mode 100644
index 0000000000..a5d52efc00
--- /dev/null
+++ b/tests/main/interfaces-x11-unix-socket/x11-client/meta/snap.yaml
@@ -0,0 +1,12 @@
+name: x11-client
+version: 1.0
+summary: Fake x11 client
+description: Fake x11 client
+
+apps:
+ x11-client:
+ command: bin/xclient.sh
+ plugs: [x11, network]
+ rm:
+ command: bin/rm.sh
+ plugs: [x11]
diff --git a/tests/main/interfaces-x11-unix-socket/x11-server/bin/xserver.sh b/tests/main/interfaces-x11-unix-socket/x11-server/bin/xserver.sh
new file mode 100755
index 0000000000..b08bf087f4
--- /dev/null
+++ b/tests/main/interfaces-x11-unix-socket/x11-server/bin/xserver.sh
@@ -0,0 +1,7 @@
+#!/bin/sh
+
+mkdir -p /tmp/.X11-unix
+
+SOCKET=/tmp/.X11-unix/X0
+rm -f $SOCKET
+echo "Hello from xserver" | nc -l -w 1 -U $SOCKET
diff --git a/tests/main/interfaces-x11-unix-socket/x11-server/meta/snap.yaml b/tests/main/interfaces-x11-unix-socket/x11-server/meta/snap.yaml
new file mode 100644
index 0000000000..483565aa01
--- /dev/null
+++ b/tests/main/interfaces-x11-unix-socket/x11-server/meta/snap.yaml
@@ -0,0 +1,10 @@
+name: x11-server
+version: 1.0
+summary: Fake x11 server
+description: Fake x11 server
+
+apps:
+ x11-server:
+ command: bin/xserver.sh
+ plugs: [network]
+ slots: [x11]
diff --git a/tests/main/lxd-mount-units/task.yaml b/tests/main/lxd-mount-units/task.yaml
new file mode 100644
index 0000000000..6d3b5ab65d
--- /dev/null
+++ b/tests/main/lxd-mount-units/task.yaml
@@ -0,0 +1,38 @@
+summary: Test mount units generated for snaps inside lxd container are correct.
+
+details: |
+ Test that mount units generated by snapd-generator inside lxd container
+ are correct for snapfuse.
+
+# only 20.10+, we want lxd images that come with snaps preinstalled.
+systems: [ubuntu-20.10*]
+
+execute: |
+ echo "Install lxd"
+ snap install lxd
+
+ lxd waitready
+ lxd init --auto
+
+ VERSION_ID="$(. /etc/os-release && echo "$VERSION_ID" )"
+ lxd.lxc launch --quiet "ubuntu:$VERSION_ID" ubuntu
+
+ lxd.lxc file push --quiet "$GOHOME"/snapd_*.deb "ubuntu/root/"
+
+ DEB=$(basename "$GOHOME"/snapd_*.deb)
+ lxd.lxc exec ubuntu -- apt install -y /root/"$DEB"
+ lxd.lxc restart ubuntu
+
+ echo "Sanity check that mount overrides were generated inside the container"
+ lxd.lxc exec ubuntu -- find /var/run/systemd/generator/ -name container.conf | MATCH "/var/run/systemd/generator/snap-core18.*mount.d/container.conf"
+ lxd.lxc exec ubuntu -- test -f /var/run/systemd/generator/snap.mount
+
+ echo "Sanity check that core18 snap is mounted correctly"
+ # Make sure core18 is mounted and readable for a regular user
+ retry -n 5 --wait 1 sh -c 'lxd.lxc exec ubuntu -- sudo --user ubuntu --login ls /snap/core18/current'
+ retry -n 5 --wait 1 sh -c 'lxd.lxc exec ubuntu -- sudo --user ubuntu --login mount | grep core18| grep allow_other'
+
+restore: |
+ lxd.lxc stop ubuntu --force || true
+ lxd.lxc delete ubuntu || true
+ snap remove --purge lxd
diff --git a/tests/main/lxd/task.yaml b/tests/main/lxd/task.yaml
index cd139ebeb9..ad1514ee07 100644
--- a/tests/main/lxd/task.yaml
+++ b/tests/main/lxd/task.yaml
@@ -2,10 +2,8 @@ summary: Ensure that lxd works
# Only run this on ubuntu 16+, lxd will not work on !ubuntu systems
# 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*, ubuntu-18*, ubuntu-2*, ubuntu-core-*]
# autopkgtest run only a subset of tests that deals with the integration
# with the distro
diff --git a/tests/nested/core20/basic/task.yaml b/tests/nested/core20/basic/task.yaml
index 1654e8a76f..ed0492e79c 100644
--- a/tests/nested/core20/basic/task.yaml
+++ b/tests/nested/core20/basic/task.yaml
@@ -31,3 +31,12 @@ execute: |
echo "Ensure 'snap list' works and test-snapd-sh snap is removed"
nested_exec "! snap list test-snapd-sh"
+
+ echo "Ensure 'snap debug show-keys' works as root"
+ nested_exec "sudo snap recovery --show-keys" | MATCH 'recovery:\s+[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}'
+ nested_exec "sudo snap recovery --show-keys" | MATCH 'reinstall:\s+[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}-[0-9]{5}'
+ echo "But not as user (normal file permissions prevent this)"
+ if nested_exec "snap recovery --show-key"; then
+ echo "snap recovery --show-key should not work as a user"
+ exit 1
+ fi
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
diff --git a/vendor/vendor.json b/vendor/vendor.json
index 21e184e1ce..08174a3450 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -116,10 +116,10 @@
"revisionTime": "2017-09-28T14:21:59Z"
},
{
- "checksumSHA1": "MqzQfXfdSBWmXEI02auMw7kfoLM=",
+ "checksumSHA1": "G2sH9o/0sihaKYbjmfWBOFU3Avs=",
"path": "github.com/snapcore/secboot",
- "revision": "bd7a6eabe9371024327d0133a5e1915df27c9eed",
- "revisionTime": "2020-10-27T12:12:33Z"
+ "revision": "fa14f1ac3b14d38025312da287728054f7c06b67",
+ "revisionTime": "2020-11-11T08:01:43Z"
},
{
"checksumSHA1": "c7jHLQSWFWbymTcFWZMQH0C5Wik=",