diff options
| author | Michael Vogt <mvo@ubuntu.com> | 2020-11-12 09:28:41 +0100 |
|---|---|---|
| committer | Michael Vogt <mvo@ubuntu.com> | 2020-11-12 09:28:41 +0100 |
| commit | cb57d0eff40b4e342340bbc3266be009c0b0b6c4 (patch) | |
| tree | cc8ec9b2ec78aee282a4c54e025a8b1c4646abae | |
| parent | 3849d71d9d4a4837cc85dadcf6d5636af9da5907 (diff) | |
| parent | e4f85071a1cb84c14c74c66293bf853c4b2d0d2e (diff) | |
Merge remote-tracking branch 'upstream/release/2.48' into fdehook-skeleton-2fdehook-skeleton-2
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, °radedJSONObj) + c.Assert(err, IsNil) + + c.Assert(degradedJSONObj, DeepEquals, exp) +} + +func (s *initramfsMountsSuite) TestInitramfsMountsRecoverModeEncryptedDegradedFallbackDataHappy(c *C) { + s.mockProcCmdlineContent(c, "snapd_recovery_mode=recover snapd_recovery_system="+s.sysLabel) + + restore := main.MockPartitionUUIDForBootedKernelDisk("") + defer restore() + + // setup a bootloader for setting the bootenv after we are done + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + defer bootloader.Force(nil) + + restore = disks.MockMountPointDisksToPartitionMapping( + map[disks.Mountpoint]*disks.MockDiskMapping{ + {Mountpoint: boot.InitramfsUbuntuSeedDir}: defaultEncBootDisk, + {Mountpoint: boot.InitramfsUbuntuBootDir}: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsHostUbuntuDataDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + { + Mountpoint: boot.InitramfsUbuntuSaveDir, + IsDecryptedDevice: true, + }: defaultEncBootDisk, + }, + ) + defer restore() + + dataActivated := false + saveActivated := false + unlockVolumeWithSealedKeyCalls := 0 + restore = main.MockSecbootUnlockVolumeUsingSealedKeyIfEncrypted(func(disk disks.Disk, name string, sealedEncryptionKeyFile string, opts *secboot.UnlockVolumeUsingSealedKeyOptions) (secboot.UnlockResult, error) { + unlockVolumeWithSealedKeyCalls++ + switch unlockVolumeWithSealedKeyCalls { + + case 1: + // pretend we can't unlock ubuntu-data with the main run key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-boot/device/fde/ubuntu-data.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{}) + return secboot.UnlockResult{IsDecryptedDevice: true}, fmt.Errorf("failed to unlock ubuntu-data") + + case 2: + // now we can unlock ubuntu-data with the fallback key + c.Assert(name, Equals, "ubuntu-data") + c.Assert(sealedEncryptionKeyFile, Equals, filepath.Join(s.tmpDir, "run/mnt/ubuntu-seed/device/fde/ubuntu-data.recovery.sealed-key")) + encDevPartUUID, err := disk.FindMatchingPartitionUUID(name + "-enc") + c.Assert(err, IsNil) + c.Assert(encDevPartUUID, Equals, "ubuntu-data-enc-partuuid") + c.Assert(opts, DeepEquals, &secboot.UnlockVolumeUsingSealedKeyOptions{ + AllowRecoveryKey: true, + }) + dataActivated = true + return secboot.UnlockResult{ + Device: filepath.Join("/dev/disk/by-partuuid", encDevPartUUID), + IsDecryptedDevice: true, + UnlockMethod: secboot.UnlockedWithSealedKey, + }, nil + + default: + c.Errorf("unexpected call to UnlockVolumeUsingSealedKeyIfEncrypted (num %d)", unlockVolumeWithSealedKeyCalls) + return secboot.UnlockResult{}, fmt.Errorf("broken test") + } + }) + defer restore() + + s.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=", |
