From 5802840fa4de9a0fa558414e5b4e3fbc2174ef87 Mon Sep 17 00:00:00 2001 From: Maciej Borzecki Date: Tue, 7 Jan 2025 13:50:04 +0100 Subject: [PATCH] overlord/fdestate: EFI DBX support (#14615) Final pieces of EFI DBX support in fdestate, which implement the following logical 'actions' triggered through the snapd API: 'prepare' - triggered before fwupd or other agent performs the actual update, results in resealing of keys with the content of the DBX update 'cleanup - triggered after the update has completed, results in resealing of keys with the current content of DBX 'startup' - triggered whenever said agent starts up, such that we can detect whenever an unexpected reboot occurred or the agent crashed/restarted, which may trigger a reseal if an update was previously started. The updates are tracked as 'external' operations in fdestate. * tests/nested/manual/core20-fde-dbx: spread test for DBX related operations Add a spread test for DBX related operations Signed-off-by: Maciej Borzecki * overlord/fdestate: implement EFI DBX operations Implement operations for notifying snapd of changes to the EFI DBX. Signed-off-by: Maciej Borzecki * fixup! overlord/fdestate: implement EFI DBX operations Signed-off-by: Maciej Borzecki * fixup! overlord/fdestate: implement EFI DBX operations Signed-off-by: Maciej Borzecki * fixup! tests/nested/manual/core20-fde-dbx: spread test for DBX related operations Signed-off-by: Maciej Borzecki * fixup! tests/nested/manual/core20-fde-dbx: spread test for DBX related operations Signed-off-by: Maciej Borzecki * fixup! overlord/fdestate: implement EFI DBX operations Signed-off-by: Maciej Borzecki * fixup! tests/nested/manual/core20-fde-dbx: spread test for DBX related operations Signed-off-by: Maciej Borzecki * fixup! overlord/fdestate: implement EFI DBX operations Signed-off-by: Maciej Borzecki * fixup! tests/nested/manual/core20-fde-dbx: spread test for DBX related operations Signed-off-by: Maciej Borzecki * fixup! overlord/fdestate: implement EFI DBX operations Signed-off-by: Maciej Borzecki * fixup! overlord/fdestate: implement EFI DBX operations Signed-off-by: Maciej Borzecki * fixup! overlord/fdestate: implement EFI DBX operations Signed-off-by: Maciej Borzecki * fixup! tests/nested/manual/core20-fde-dbx: spread test for DBX related operations Signed-off-by: Maciej Borzecki * fixup! overlord/fdestate: implement EFI DBX operations Signed-off-by: Maciej Borzecki --------- Signed-off-by: Maciej Borzecki --- overlord/fdestate/dbx.go | 602 ++++++++ overlord/fdestate/dbx_test.go | 1264 +++++++++++++++++ overlord/fdestate/export_test.go | 16 + overlord/fdestate/fdemgr.go | 16 + overlord/fdestate/fdemgr_test.go | 38 +- overlord/fdestate/fdestate.go | 65 +- .../manual/core20-fde-dbx/dbx-1-update.auth | Bin 0 -> 2837 bytes tests/nested/manual/core20-fde-dbx/task.yaml | 157 ++ 8 files changed, 2110 insertions(+), 48 deletions(-) create mode 100644 overlord/fdestate/dbx.go create mode 100644 overlord/fdestate/dbx_test.go create mode 100644 tests/nested/manual/core20-fde-dbx/dbx-1-update.auth create mode 100644 tests/nested/manual/core20-fde-dbx/task.yaml diff --git a/overlord/fdestate/dbx.go b/overlord/fdestate/dbx.go new file mode 100644 index 00000000000..5915917c057 --- /dev/null +++ b/overlord/fdestate/dbx.go @@ -0,0 +1,602 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2024 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 . + * + */ +package fdestate + +import ( + "encoding/json" + "errors" + "fmt" + + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/device" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/fdestate/backend" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" +) + +var ( + backendResealKeysForSignaturesDBUpdate = backend.ResealKeysForSignaturesDBUpdate +) + +type EFISecurebootKeyDatabase int + +const ( + EFISecurebootPK EFISecurebootKeyDatabase = iota + EFISecurebootKEK + EFISecurebootDB + EFISecurebootDBX +) + +// EFISecureBootDBUpdatePrepare notifies that the local EFI key +// database manager is about to update the database. +func EFISecureBootDBUpdatePrepare(st *state.State, db EFISecurebootKeyDatabase, payload []byte) error { + method, err := device.SealedKeysMethod(dirs.GlobalRootDir) + if err != nil { + if err == device.ErrNoSealedKeys { + return nil + } + return err + } + + st.Lock() + defer st.Unlock() + + if err := checkDBXChangeConflicts(st); err != nil { + return err + } + + op, err := addEFISecurebootDBUpdateChange(st, method, payload) + if err != nil { + return err + } + + chgID := op.ChangeID + + chg := st.Change(chgID) + + // we're good so far, kick off the change + st.EnsureBefore(0) + + // we want to synchronize with the prepare task completing successfully as + // at this point the keys will have been resealed with the proposed DBX + // payload + chgFailed := false + afterPrepareOKC := dbxUpdatePreparedOKChan(st, chgID) + st.Unlock() + // there is no timeout as we expect to observe one of the two outcomes: we + // either complete the prepare step successfully or the change fails (and + // becomes ready); we are not holding the state lock, so other processing + // tasks not blocked + select { + case <-afterPrepareOKC: + // prepare step has completed successfully + case <-chg.Ready(): + // change failed unexpectedly + chgFailed = true + } + st.Lock() + + if chgFailed { + // The change is unexpectedly ready, which means that the prepare task + // has failed. Need to ensure that the pending operation is either in a + // failed state, or gone (which would be achieved by cleanup). + op, err = findFirstExternalOperationByChangeID(st, chgID) + if err != nil { + return fmt.Errorf("internal error: cannot look up external operation by change ID: %w", err) + } + + err = chg.Err() + + if op != nil { + // it's still there, so let's update the status so that it does not + // block other operations + op.SetFailed(fmt.Sprintf("prepare task failed early: %v", err)) + updateExternalOperation(st, op) + } + return fmt.Errorf("prepare change failed: %w", err) + } + + return nil +} + +// EFISecureBootDBUpdateCleanup notifies that the local EFI key database manager +// has reached a cleanup stage of the update process. +func EFISecureBootDBUpdateCleanup(st *state.State) error { + st.Lock() + defer st.Unlock() + + op, err := findFirstPendingExternalOperationByKind(st, "fde-efi-secureboot-db-update") + if err != nil { + return err + } + + if op == nil { + logger.Debugf("no pending DBX update request for cleanup") + return nil + } + + // ensure that a cleanup can only be called when operation has obtained + // 'Doing' status which prevents attempting cleanup when we briefly unlock + // the state doing the initial reseal for prepare in the 'do' path, and + // similarly in the 'undo' path + if op.Status != DoingStatus { + return &snapstate.ChangeConflictError{ + ChangeKind: "fde-efi-secureboot-db-update", + Message: "cannot perform DBX update 'cleanup' action when conflicting actions are in progress", + } + } + + // mark as successful + op.SetStatus(CompletingStatus) + + if err := updateExternalOperation(st, op); err != nil { + return err + } + + chg := st.Change(op.ChangeID) + // complete unlocks the state waiting for change to become ready + return completeEFISecurebootDBUpdateChange(chg) +} + +// EFISecureBootDBManagerStartup indicates that the local EFI key database +// manager has started. +func EFISecureBootDBManagerStartup(st *state.State) error { + st.Lock() + defer st.Unlock() + + op, err := findFirstPendingExternalOperationByKind(st, "fde-efi-secureboot-db-update") + if err != nil { + return err + } + + if op == nil { + logger.Debugf("no pending DBX update request") + return nil + } + + // at this point we have a pending request, which means we must mark it as + // failed and reseal with the current content of EFI DBX + + // ensure that the external operation has obtained 'Doing' status which + // prevents attempting startup/cleanup when we briefly unlock the state + // doing the initial reseal for prepare in the 'do' path, and similarly in + // the 'undo' path + if op.Status != DoingStatus { + return &snapstate.ChangeConflictError{ + ChangeKind: "fde-efi-secureboot-db-update", + Message: "cannot perform DBX update 'startup' action when conflicting actions are in progress", + } + } + + op.SetStatus(AbortingStatus) + op.Err = "'startup' action invoked while an operation is in progress" + if err := updateExternalOperation(st, op); err != nil { + return nil + } + + chg := st.Change(op.ChangeID) + // complete unlocks the state waiting for change to become ready + return completeEFISecurebootDBUpdateChange(chg) +} + +type dbxUpdateContext struct { + Payload []byte `json:"payload"` + Method device.SealingMethod `json:"sealing-method"` +} + +// addEFISecurebootDBUpdateChange adds a state change related to the DBX +// update. The state must be locked by the caller. +func addEFISecurebootDBUpdateChange(st *state.State, method device.SealingMethod, payload []byte) (*externalOperation, error) { + // add a change carrying 2 tasks: + // - efi-secureboot-db-update-prepare: with a noop do, but the undo handler + // preforms necessary cleanup + // - efi-secureboot-db-update: waits for the external caller to indicate + // that the action is complete or failed + // + // if the original requester never calls cleanup/startup, the change + // will be pruned automatically and the undo will perform a reseal + + tPrep := st.NewTask("efi-secureboot-db-update-prepare", "Prepare for external EFI DBX update") + tUpdateWait := st.NewTask("efi-secureboot-db-update", "Reseal after external EFI DBX update") + tUpdateWait.WaitFor(tPrep) + ts := state.NewTaskSet(tPrep, tUpdateWait) + + chg := st.NewChange("fde-efi-secureboot-db-update", "External EFI DBX update") + chg.AddAll(ts) + + data, err := json.Marshal(dbxUpdateContext{ + Payload: payload, + Method: method, + }) + if err != nil { + return nil, err + } + + op := &externalOperation{ + // match the change kind + Kind: "fde-efi-secureboot-db-update", + ChangeID: chg.ID(), + Context: json.RawMessage(data), + Status: PreparingStatus, + } + + err = addExternalOperation(st, op) + if err != nil { + return nil, err + } + + setupDBXNotifyPrepareDoneOKChan(st, chg.ID()) + + return op, nil +} + +// completeEFISecurebootDBUpdateChange waits for the change to complete and +// returns the error result obtained from the change +func completeEFISecurebootDBUpdateChange(chg *state.Change) error { + st := chg.State() + + // trigger ensure so that the task runner attempts to run our tasks + st.EnsureBefore(0) + + // there is no timeout as we expect the change to complete, either + // successfully or with an error; note we are not holding the state lock so + // other tasks are not blocked + st.Unlock() + logger.Debugf("waiting for FDE DBX change %v to become ready", chg.ID()) + <-chg.Ready() + logger.Debugf("DBX change complete") + st.Lock() + + chg = st.Change(chg.ID()) + if err := chg.Err(); err != nil { + logger.Debugf("completed DBX update change error: %v", chg.Err()) + } + + return nil +} + +// postUpdateReseal performs a reseal after a DBX update. +func postUpdateReseal(mgr *FDEManager, unlocker boot.Unlocker, method device.SealingMethod) error { + return boot.WithBootChains(func(bc *boot.ResealKeyForBootChainsParams) error { + logger.Debugf("attempting post DBX update reseal") + + const expectReseal = true + return mgr.resealKeyForBootChains(unlocker, method, dirs.GlobalRootDir, bc, expectReseal) + }, method) +} + +func (m *FDEManager) doEFISecurebootDBUpdatePrepare(t *state.Task, tomb *tomb.Tomb) error { + // the do handler perform the initial reseal with DBX payload which will be + // used during update + + st := t.State() + + st.Lock() + defer st.Unlock() + + chgID := t.Change().ID() + op, err := findFirstExternalOperationByChangeID(st, chgID) + if err != nil { + return fmt.Errorf("internal error: no matching external operation for change ID %v", chgID) + } + + if op.Status != PreparingStatus { + return fmt.Errorf("internal error: external operation already in state %q, but expected %q", + op.Status, PreparingStatus) + } + + var updateData dbxUpdateContext + if err := json.Unmarshal(op.Context, &updateData); err != nil { + return fmt.Errorf("cannot unmarshal DBX update context data: %v", err) + } + + err = func() error { + mgr := fdeMgr(st) + + return boot.WithBootChains(func(bc *boot.ResealKeyForBootChainsParams) error { + // TODO are we logging too much? + logger.Debugf("attempting reseal for DBX update") + logger.Debugf("boot chains: %v\n", bc) + logger.Debugf("DBX update payload: %x", updateData.Payload) + + // unlocks the state internally as needed + return backendResealKeysForSignaturesDBUpdate( + &unlockedStateManager{ + FDEManager: mgr, + unlocker: st.Unlocker(), + }, + updateData.Method, dirs.GlobalRootDir, bc, updateData.Payload, + ) + }, updateData.Method) + }() + + if err != nil { + err = fmt.Errorf("cannot perform initial reseal of keys for DBX update: %w", err) + op.SetFailed(err.Error()) + } else { + op.SetStatus(DoingStatus) + } + + updateExternalOperation(st, op) + + if err == nil { + t.SetStatus(state.DoneStatus) + notifyDBXUpdatePrepareDoneOK(st, chgID) + } + + return err +} + +func (m *FDEManager) undoEFISecurebootDBUpdatePrepare(t *state.Task, tomb *tomb.Tomb) error { + // the undo handler runs when both the external change has failed, eg. due + // to startup called after prepare, or when the task was aborted due to the + // original not making any calls after the initial prepare + st := t.State() + + st.Lock() + defer st.Unlock() + + op, err := findFirstExternalOperationByChangeID(st, t.Change().ID()) + if err != nil || op == nil { + return fmt.Errorf("internal error: cannot execute efi-dbx-update handler: %v", err) + } + + var updateData dbxUpdateContext + if err := json.Unmarshal(op.Context, &updateData); err != nil { + return fmt.Errorf("cannot unmarshal DBX update context data: %v", err) + } + + t.Logf("DBX update prepare undo called with operation in status: %v", op.Status) + + switch op.Status { + case ErrorStatus: + // operation status already indicates error, which means that it failed + // in the efi-secureboot-db-update handler + + // TODO should we perform a reseal? one attempt in the 'do' handler + // already failed + t.Logf("action already in error state with error: %v", op.Err) + return nil + case DoingStatus, AbortingStatus: + // we hit abort, the external operation is still in doing state, update its + // state and continue the undo sequence + + mgr := fdeMgr(st) + err = postUpdateReseal(mgr, st.Unlocker(), updateData.Method) + if err != nil { + t.Logf("cannot complete post update reseal in undo: %v", err) + op.SetFailed( + fmt.Sprintf("cannot perform post update reseal: %v, "+ + "while aborting explicitly or due to timeout waiting for subsequent request from the caller", + err)) + } else { + reason := "aborted explicitly or due to timeout waiting for subsequent request from the caller" + if op.Status == AbortingStatus && op.Err != "" { + // aborting with explicit reason + reason = op.Err + } + op.SetFailed(reason) + } + + if updateErr := updateExternalOperation(st, op); updateErr != nil { + return updateErr + } + + t.Logf("external action state updated to %v: %v", op.Status, op.Err) + if err != nil { + return fmt.Errorf("cannot complete reseal in undo: %v", err) + } + return nil + } + + return fmt.Errorf("internal error: unexpected state of external action in undo handler: %v", op.Status) +} + +func (m *FDEManager) doEFISecurebootDBUpdate(t *state.Task, tomb *tomb.Tomb) error { + // the handler is running, which means that we are no longer blocked waiting + // for the op to complete + + st := t.State() + + st.Lock() + defer st.Unlock() + + op, err := findFirstExternalOperationByChangeID(st, t.Change().ID()) + if err != nil || op == nil { + return fmt.Errorf("internal error: cannot execute efi-dbx-update handler: %v", err) + } + + switch op.Status { + case CompletingStatus: + // handled below + case AbortingStatus: + // explicit error when operation got aborted + reason := "aborted by external request" + if op.Err != "" { + // aborting with explicit reason + reason = op.Err + } + return errors.New(reason) + default: + return fmt.Errorf("cannot perform post update reseal, operation in status %v", op.Status) + } + + var updateData dbxUpdateContext + if err := json.Unmarshal(op.Context, &updateData); err != nil { + return fmt.Errorf("cannot unmarshal DBX update context data: %v", err) + } + + mgr := fdeMgr(st) + err = postUpdateReseal(mgr, st.Unlocker(), updateData.Method) + if err != nil { + t.Errorf("cannot complete post update reseal: %v", err) + } + + if err != nil { + op.SetFailed( + fmt.Sprintf("cannot complete post update reseal: %v, while completing due to external request", err)) + } else { + op.SetStatus(DoneStatus) + } + + if updateErr := updateExternalOperation(st, op); updateErr != nil { + t.Logf("cannot update operation status: %v", updateErr) + return updateErr + } + + if err == nil { + // update task status before unlocking + t.SetStatus(state.DoneStatus) + } + + return err +} + +func (m *FDEManager) doEFISecurebootDBUpdatePrepareCleanup(t *state.Task, tomb *tomb.Tomb) error { + st := t.State() + + st.Lock() + defer st.Unlock() + + chgID := t.Change().ID() + return withFdeState(st, func(fde *FdeState) (modified bool, err error) { + for idx, op := range fde.PendingExternalOperations { + logger.Debugf("cleaning up external operation for change %v", op.ChangeID) + if op.ChangeID == chgID { + fde.PendingExternalOperations = append(fde.PendingExternalOperations[:idx], + fde.PendingExternalOperations[idx+1:]...) + + cleanupUpdatePreparedOKChan(st, chgID) + + return true, nil + } + } + return false, nil + }) +} + +func isEFISecurebootDBUpdateBlocked(t *state.Task) bool { + extChg, err := findFirstExternalOperationByChangeID(t.State(), t.Change().ID()) + if err != nil { + // error obtaining data from the state, which does not mean the + // operation isn't there, best case, leave it running and wait for the + // change to abort + return true + } + + if extChg == nil { + // no operation, then why were we called? + return true + } + + switch extChg.Status { + case CompletingStatus, AbortingStatus: + // operation states that unblock the efi-secureboot-update-db task + return false + default: + return true + } +} + +func dbxUpdateAffectedSnaps(t *state.Task) ([]string, error) { + // TODO check if we have sealed keys at all + + // DBX updates cause a reseal, so any snaps which are either directly + // measured or their content is measured during the boot will count as + // affected + + // XXX this effectively blocks updates of gadget, kernel & base until the + // change completes + + return fdeRelevantSnaps(t.State()) +} + +func checkDBXChangeConflicts(st *state.State) error { + // TODO check if we have sealed keys at all + + snaps, err := fdeRelevantSnaps(st) + if err != nil { + return err + } + + if len(snaps) == 0 { + return nil + } + + // make sure that are no other DBX changes in progress + op, err := findFirstPendingExternalOperationByKind(st, "fde-efi-secureboot-db-update") + if err != nil { + return err + } + + if op != nil { + return &snapstate.ChangeConflictError{ + ChangeKind: "fde-efi-secureboot-db-update", + Message: "cannot start a new DBX update when conflicting actions are in progress", + } + } + + // make sure that there are no changes for the snaps that are relevant for + // FDE + return snapstate.CheckChangeConflictMany(st, snaps, "") +} + +type dbxUpdatePrepareSyncKey struct{} + +func setupDBXNotifyPrepareDoneOKChan(st *state.State, changeID string) { + var syncChs map[string]chan struct{} + + val := st.Cached(dbxUpdatePrepareSyncKey{}) + if val == nil { + syncChs = make(map[string]chan struct{}) + } else { + syncChs = val.(map[string]chan struct{}) + } + + syncChs[changeID] = make(chan struct{}) + st.Cache(dbxUpdatePrepareSyncKey{}, syncChs) +} + +func notifyDBXUpdatePrepareDoneOK(st *state.State, changeID string) { + val := st.Cached(dbxUpdatePrepareSyncKey{}) + + if val != nil { + syncChs := val.(map[string]chan struct{}) + close(syncChs[changeID]) + } +} + +func dbxUpdatePreparedOKChan(st *state.State, changeID string) <-chan struct{} { + val := st.Cached(dbxUpdatePrepareSyncKey{}) + + syncChs := val.(map[string]chan struct{}) + return syncChs[changeID] +} + +func cleanupUpdatePreparedOKChan(st *state.State, changeID string) { + val := st.Cached(dbxUpdatePrepareSyncKey{}) + if val != nil { + syncChs := val.(map[string]chan struct{}) + delete(syncChs, changeID) + } +} diff --git a/overlord/fdestate/dbx_test.go b/overlord/fdestate/dbx_test.go new file mode 100644 index 00000000000..40695166099 --- /dev/null +++ b/overlord/fdestate/dbx_test.go @@ -0,0 +1,1264 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot + +/* + * Copyright (C) 2024 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 . + * + */ + +package fdestate_test + +import ( + "fmt" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/snapasserts" + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/device" + "github.com/snapcore/snapd/overlord/fdestate" + "github.com/snapcore/snapd/overlord/fdestate/backend" + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/secboot" + "github.com/snapcore/snapd/seed" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/timings" +) + +func (s *fdeMgrSuite) TestEFIDBXNoSealedKeys(c *C) { + // no sealed keys in the system, all operations are NOP + + st := s.st + onClassic := true + s.startedManager(c, onClassic) + + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate(func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + panic("unexpected call") + })() + + err := fdestate.EFISecureBootDBManagerStartup(st) + c.Assert(err, IsNil) + + err = fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + func() { + st.Lock() + defer st.Unlock() + // make sure nothing was added to the state + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + c.Check(fdeSt.PendingExternalOperations, HasLen, 0) + }() + + err = fdestate.EFISecureBootDBUpdateCleanup(st) + c.Assert(err, IsNil) +} + +func (s *fdeMgrSuite) TestEFIDBXStartupClean(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + s.startedManager(c, onClassic) + + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate(func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + panic("unexpected call") + })() + + err := fdestate.EFISecureBootDBManagerStartup(st) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(fdeSt.PendingExternalOperations, HasLen, 0) +} + +func (s *fdeMgrSuite) TestEFIDBXPrepareHappy(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate(func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealCalls++ + c.Check(mgr, NotNil) + c.Check(params.RunModeBootChains, HasLen, 1) + c.Check(update, DeepEquals, []byte("payload")) + + // normally executed by the backend code + c.Check(mgr.Update("run", "default", &backend.SealingParameters{ + BootModes: []string{"run"}, + Models: []secboot.ModelForSealing{model}, + TpmPCRProfile: []byte("PCR-profile"), + }), IsNil) + return nil + })() + + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(resealCalls, Equals, 1) + c.Check(fdeSt.PendingExternalOperations, HasLen, 1) + c.Check(fdeSt.PendingExternalOperations[0], DeepEquals, fdestate.ExternalOperation{ + Kind: "fde-efi-secureboot-db-update", + ChangeID: "1", + Context: []byte(`{"payload":"cGF5bG9hZA==","sealing-method":"tpm"}`), + Status: fdestate.DoingStatus, + }) + c.Check(fdeSt.KeyslotRoles, DeepEquals, map[string]fdestate.KeyslotRoleInfo{ + "recover": { + PrimaryKeyID: 0, + Parameters: nil, + TPM2PCRPolicyRevocationCounter: 0x1880002, + }, + "run": { + PrimaryKeyID: 0, Parameters: map[string]fdestate.KeyslotRoleParameters{ + "default": { + Models: []*fdestate.Model{fdestate.NewModel(model)}, + BootModes: []string{"run"}, + TPM2PCRProfile: secboot.SerializedPCRProfile([]byte("PCR-profile")), + }, + }, + TPM2PCRPolicyRevocationCounter: 0x1880001, + }, + "run+recover": { + PrimaryKeyID: 0, + Parameters: nil, + TPM2PCRPolicyRevocationCounter: 0x1880001, + }, + }) + + // execute a single iteration of task runner, to have the task state switched to doing + err = func() error { + st.Unlock() + defer st.Lock() + return s.runner.Ensure() + }() + c.Assert(err, IsNil) + + // and we have change in the state + chgs := st.Changes() + c.Assert(chgs, HasLen, 1) + chg := chgs[0] + c.Check(chg.IsReady(), Equals, false) + tsks := chg.Tasks() + c.Assert(tsks, HasLen, 2) + c.Check(tsks[0].Kind(), Equals, "efi-secureboot-db-update-prepare") + c.Check(tsks[0].Status(), Equals, state.DoneStatus) + c.Check(tsks[1].Kind(), Equals, "efi-secureboot-db-update") + c.Check(tsks[1].Status(), Equals, state.DoStatus) + c.Check(tsks[1].WaitTasks(), DeepEquals, []*state.Task{tsks[0]}) +} + +func (s *fdeMgrSuite) TestEFIDBXPrepareConflictSelf(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate(func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealCalls++ + return nil + })() + + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(resealCalls, Equals, 1) + c.Check(fdeSt.PendingExternalOperations, HasLen, 1) + + // running prepare again will cause a conflicts + err = func() error { + st.Unlock() + defer st.Lock() + return fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + }() + c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ + ChangeKind: "fde-efi-secureboot-db-update", + Message: "cannot start a new DBX update when conflicting actions are in progress", + }) +} + +func (s *fdeMgrSuite) TestEFIDBXPrepareConflictOperationNotInDoingYet(c *C) { + // attempting to run cleanup or startup when the operation has not yet + // reached Doing status raises a conflict + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate(func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealCalls++ + return nil + })() + + st.Lock() + defer st.Unlock() + + // mock an operation which has just been added in prepare, but the initial reseal has not yet completed + c.Assert(fdestate.AddExternalOperation(st, &fdestate.ExternalOperation{ + ChangeID: "1234", + Kind: "fde-efi-secureboot-db-update", + Status: fdestate.PreparingStatus, + }), IsNil) + + st.Unlock() + defer st.Lock() + err := fdestate.EFISecureBootDBUpdateCleanup(st) + c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ + ChangeKind: "fde-efi-secureboot-db-update", + Message: "cannot perform DBX update 'cleanup' action when conflicting actions are in progress", + }) + + err = fdestate.EFISecureBootDBManagerStartup(st) + c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ + ChangeKind: "fde-efi-secureboot-db-update", + Message: "cannot perform DBX update 'startup' action when conflicting actions are in progress", + }) +} + +func (s *fdeMgrSuite) TestEFIDBXPrepareConflictSnapChanges(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + defer testutil.Mock(&snapstate.EnforcedValidationSets, func(st *state.State, extraVss ...*asserts.ValidationSet) (*snapasserts.ValidationSets, error) { + return nil, nil + })() + + st.Lock() + defer st.Unlock() + snapstate.Set(st, model.Kernel(), &snapstate.SnapState{ + Active: true, + Sequence: snapstatetest.NewSequenceFromSnapSideInfos([]*snap.SideInfo{ + {RealName: model.Kernel(), Revision: snap.R(1)}, + {RealName: model.Kernel(), Revision: snap.R(2)}, + }), + Current: snap.R(2), + SnapType: "kernel", + }) + + chg := st.NewChange("kernel-snap-remove", "...") + rmTasks, err := snapstate.Remove(st, model.Kernel(), snap.R(1), nil) + c.Assert(err, IsNil) + c.Assert(rmTasks, NotNil) + chg.AddAll(rmTasks) + + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate(func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + c.Fatalf("unexpected call") + return fmt.Errorf("unexpected call") + })() + + st.Unlock() + defer st.Lock() + err = fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, DeepEquals, &snapstate.ChangeConflictError{ + ChangeKind: "kernel-snap-remove", + Snap: "pc-kernel", + ChangeID: "1", + }) +} + +func (s *fdeMgrSuite) TestEFIDBXUpdateAndCleanupRunningAction(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealForDBUPdateCalls := 0 + resealForBootChainsCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealForDBUPdateCalls++ + // normally executed by the backend code + c.Assert(mgr.Update("run", "default", &backend.SealingParameters{ + BootModes: []string{"run"}, + Models: []secboot.ModelForSealing{model}, + TpmPCRProfile: []byte("PCR-profile-dbx-update"), + }), IsNil) + return nil + })() + + defer fdestate.MockBackendResealKeyForBootChains( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, expectReseal bool) error { + resealForBootChainsCalls++ + // normally executed by the backend code + c.Assert(mgr.Update("run", "default", &backend.SealingParameters{ + BootModes: []string{"run"}, + Models: []secboot.ModelForSealing{model}, + TpmPCRProfile: []byte("PCR-profile-boot-chains"), + }), IsNil) + return nil + })() + + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 0) + c.Check(fdeSt.PendingExternalOperations, HasLen, 1) + c.Check(fdeSt.KeyslotRoles["run"], DeepEquals, fdestate.KeyslotRoleInfo{ + PrimaryKeyID: 0, + Parameters: map[string]fdestate.KeyslotRoleParameters{ + "default": { + Models: []*fdestate.Model{fdestate.NewModel(model)}, + BootModes: []string{"run"}, + TPM2PCRProfile: secboot.SerializedPCRProfile([]byte("PCR-profile-dbx-update")), + }, + }, + TPM2PCRPolicyRevocationCounter: 0x1880001, + }) + + // execute a single iteration of task runner, to have the task state switched to doing + //s.runnerIterationLocked(c) + + // and we have change in the state + chgs := st.Changes() + c.Assert(chgs, HasLen, 1) + chg := chgs[0] + c.Check(chg.IsReady(), Equals, false) + + st.Unlock() + defer st.Lock() + + // cleanup completes the change, and waits internally for change to become ready + err = fdestate.EFISecureBootDBUpdateCleanup(st) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + // post cleanup inspect + var fdeStAfter fdestate.FdeState + err = st.Get("fde", &fdeStAfter) + c.Assert(err, IsNil) + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 1) + // task cleanup may have run + if l := len(fdeStAfter.PendingExternalOperations); l == 1 { + c.Check(fdeStAfter.PendingExternalOperations[0].Status, Equals, fdestate.DoneStatus) + } else if l != 0 { + c.Fatalf("unexpected number of pending external operations: %v", l) + } + + c.Check(fdeStAfter.KeyslotRoles["run"], DeepEquals, fdestate.KeyslotRoleInfo{ + PrimaryKeyID: 0, + Parameters: map[string]fdestate.KeyslotRoleParameters{ + "default": { + Models: []*fdestate.Model{fdestate.NewModel(model)}, + BootModes: []string{"run"}, + TPM2PCRProfile: secboot.SerializedPCRProfile([]byte("PCR-profile-boot-chains")), + }, + }, + TPM2PCRPolicyRevocationCounter: 0x1880001, + }) + + c.Check(chg.IsReady(), Equals, true) + c.Check(chg.IsClean(), Equals, false) + c.Check(chg.Status(), Equals, state.DoneStatus) + c.Check(chg.Err(), IsNil) + + st.Unlock() + // wait for change to become clean + iterateUnlockedStateWaitingFor(st, chg.IsClean) + st.Lock() + + var fdeStAfterCleanup fdestate.FdeState + err = st.Get("fde", &fdeStAfterCleanup) + c.Assert(err, IsNil) + // operation has been dropped from the state in cleanup + c.Check(fdeStAfterCleanup.PendingExternalOperations, HasLen, 0) +} + +func (s *fdeMgrSuite) TestEFIDBXUpdateAndUnexpectedStartupAction(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealForDBUPdateCalls := 0 + resealForBootChainsCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealForDBUPdateCalls++ + // normally executed by the backend code + c.Assert(mgr.Update("run", "default", &backend.SealingParameters{ + BootModes: []string{"run"}, + Models: []secboot.ModelForSealing{model}, + TpmPCRProfile: []byte("PCR-profile-dbx-update"), + }), IsNil) + return nil + })() + + defer fdestate.MockBackendResealKeyForBootChains( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, expectReseal bool) error { + resealForBootChainsCalls++ + // normally executed by the backend code + c.Assert(mgr.Update("run", "default", &backend.SealingParameters{ + BootModes: []string{"run"}, + Models: []secboot.ModelForSealing{model}, + TpmPCRProfile: []byte("PCR-profile-boot-chains-startup"), + }), IsNil) + return nil + })() + + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 0) + c.Check(fdeSt.PendingExternalOperations, HasLen, 1) + c.Check(fdeSt.KeyslotRoles["run"], DeepEquals, fdestate.KeyslotRoleInfo{ + PrimaryKeyID: 0, + Parameters: map[string]fdestate.KeyslotRoleParameters{ + "default": { + Models: []*fdestate.Model{fdestate.NewModel(model)}, + BootModes: []string{"run"}, + TPM2PCRProfile: secboot.SerializedPCRProfile([]byte("PCR-profile-dbx-update")), + }, + }, + TPM2PCRPolicyRevocationCounter: 0x1880001, + }) + + // and we have change in the state + chgs := st.Changes() + c.Assert(chgs, HasLen, 1) + chg := chgs[0] + c.Check(chg.IsReady(), Equals, false) + tsks := chg.Tasks() + c.Assert(tsks, HasLen, 2) + tsk := tsks[1] + c.Assert(tsk.Kind(), Equals, "efi-secureboot-db-update") + c.Assert(tsk.Status(), Equals, state.DoStatus) + + st.Unlock() + defer st.Lock() + + // startup aborts the change and reseals with current boot chains, waits for + // change to be complete + err = fdestate.EFISecureBootDBManagerStartup(st) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + // post cleanup inspect + var fdeStAfter fdestate.FdeState + err = st.Get("fde", &fdeStAfter) + c.Assert(err, IsNil) + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 1) + c.Assert(fdeStAfter.PendingExternalOperations, HasLen, 1) + c.Check(fdeStAfter.PendingExternalOperations[0].Status, Equals, fdestate.ErrorStatus) + c.Check(fdeStAfter.KeyslotRoles["run"], DeepEquals, fdestate.KeyslotRoleInfo{ + PrimaryKeyID: 0, + Parameters: map[string]fdestate.KeyslotRoleParameters{ + "default": { + Models: []*fdestate.Model{fdestate.NewModel(model)}, + BootModes: []string{"run"}, + TPM2PCRProfile: secboot.SerializedPCRProfile([]byte("PCR-profile-boot-chains-startup")), + }, + }, + TPM2PCRPolicyRevocationCounter: 0x1880001, + }) + + // change has an error now + c.Check(chg.IsReady(), Equals, true) + c.Check(chg.IsClean(), Equals, false) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n"+ + "- Reseal after external EFI DBX update .'startup' action invoked while an operation is in progress.") + c.Check(tsk.Status(), Equals, state.ErrorStatus) + + // this should return immediately, as the operation has completed + st.Unlock() + c.Logf("-- wait ready") + <-chg.Ready() + st.Lock() + + c.Check(chg.IsReady(), Equals, true) + c.Check(chg.IsReady(), Equals, true) + + st.Unlock() + // wait for change to become clean + iterateUnlockedStateWaitingFor(st, chg.IsClean) + st.Lock() + + var fdeStAfterCleanup fdestate.FdeState + err = st.Get("fde", &fdeStAfterCleanup) + c.Assert(err, IsNil) + // operation has been dropped from the state during cleanup + c.Check(fdeStAfterCleanup.PendingExternalOperations, HasLen, 0) +} + +func (s *fdeMgrSuite) TestEFIDBXUpdateAbort(c *C) { + // simulate a case when prepare is requested, but neither cleanup nor + // startup is called, the change will wait till it is auto aborted + + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealForDBUpdateCalls := 0 + resealForBootChainsCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealForDBUpdateCalls++ + return nil + })() + + defer fdestate.MockBackendResealKeyForBootChains( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, expectReseal bool) error { + resealForBootChainsCalls++ + return nil + })() + + // the loop is running now + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(resealForDBUpdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 0) + c.Check(fdeSt.PendingExternalOperations, HasLen, 1) + c.Check(fdeSt.PendingExternalOperations[0].Status, Equals, fdestate.DoingStatus) + + // and we have change in the state + chgs := st.Changes() + c.Assert(chgs, HasLen, 1) + chg := chgs[0] + c.Check(chg.IsReady(), Equals, false) + tsks := chg.Tasks() + c.Assert(tsks, HasLen, 2) + c.Assert(tsks[0].Kind(), Equals, "efi-secureboot-db-update-prepare") + c.Assert(tsks[1].Kind(), Equals, "efi-secureboot-db-update") + + st.Unlock() + defer st.Lock() + + iterateUnlockedStateWaitingFor(st, func() bool { + return tsks[0].Status() == state.DoneStatus + }) + + st.Lock() + chg.Abort() + st.EnsureBefore(0) + st.Unlock() + + c.Logf("-- wait ready") + <-chg.Ready() + c.Logf("-- ready") + + st.Lock() + defer st.Unlock() + + // change has been undone + c.Check(chg.IsReady(), Equals, true) + c.Check(chg.IsClean(), Equals, false) + c.Check(chg.Status(), Equals, state.UndoneStatus) + c.Check(tsks[0].Status(), Equals, state.UndoneStatus) + c.Check(tsks[1].Status(), Equals, state.HoldStatus) + + // post cleanup inspect + var fdeStAfter fdestate.FdeState + err = st.Get("fde", &fdeStAfter) + c.Assert(err, IsNil) + + // no more reseal for update calls + c.Check(resealForDBUpdateCalls, Equals, 1) + // but one post-update reseal + c.Check(resealForBootChainsCalls, Equals, 1) + c.Check(fdeStAfter.PendingExternalOperations, HasLen, 1) + c.Check(fdeStAfter.PendingExternalOperations[0].Status, Equals, fdestate.ErrorStatus) + + st.Unlock() + // wait for change to become clean + iterateUnlockedStateWaitingFor(st, chg.IsClean) + st.Lock() + + var fdeStAfterCleanup fdestate.FdeState + err = st.Get("fde", &fdeStAfterCleanup) + c.Assert(err, IsNil) + c.Check(fdeStAfterCleanup.PendingExternalOperations, HasLen, 0) +} + +func (s *fdeMgrSuite) TestEFIDBXUpdateResealFailedAborts(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealForDBUPdateCalls := 0 + resealForBootChainsCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealForDBUPdateCalls++ + return fmt.Errorf("mock error") + })() + defer fdestate.MockBackendResealKeyForBootChains( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, expectReseal bool) error { + resealForBootChainsCalls++ + return nil + })() + + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, ErrorMatches, "(?sm).*cannot perform initial reseal of keys for DBX update: mock error.*") + + st.Lock() + defer st.Unlock() + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 0) + // depending on whether cleanup ran, the there can either be one or no + // operations in the state + if l := len(fdeSt.PendingExternalOperations); l == 1 { + c.Check(fdeSt.PendingExternalOperations[0].Status, Equals, fdestate.ErrorStatus) + } else if l > 1 { + c.Fatalf("unexpected number of operations in the state: %v", l) + } + + // and we have change in the state, but it is in an error status already + chgs := st.Changes() + c.Assert(chgs, HasLen, 1) + chg := chgs[0] + c.Check(chg.IsReady(), Equals, true) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n"+ + "- Prepare for external EFI DBX update .cannot perform initial reseal of keys for DBX update: mock error.") +} + +func (s *fdeMgrSuite) TestEFIDBXUpdatePostUpdateResealFailed(c *C) { + // mock an error in a reseal which happens in the 'do' handler after snapd + // has been notified of a completed update + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealForDBUPdateCalls := 0 + resealForBootChainsCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealForDBUPdateCalls++ + return nil + })() + defer fdestate.MockBackendResealKeyForBootChains( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, expectReseal bool) error { + resealForBootChainsCalls++ + return fmt.Errorf("mock error") + })() + + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 0) + + st.Unlock() + defer st.Lock() + + // cleanup triggers post update reseal + err = fdestate.EFISecureBootDBUpdateCleanup(st) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 1) + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + // depending on whether cleanup ran, the there can either be one or no + // operations in the state + if l := len(fdeSt.PendingExternalOperations); l == 1 { + c.Check(fdeSt.PendingExternalOperations[0].Status, Equals, fdestate.ErrorStatus) + } else if l > 1 { + c.Fatalf("unexpected number of operations in the state: %v", l) + } + + // and we have change in the state, but it is in an error status already + chgs := st.Changes() + c.Assert(chgs, HasLen, 1) + chg := chgs[0] + c.Check(chg.IsReady(), Equals, true) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n"+ + // error logged in the task + "- Reseal after external EFI DBX update .cannot complete post update reseal: mock error.\n"+ + // actual error + "- Reseal after external EFI DBX update .mock error.") +} + +func (s *fdeMgrSuite) TestEFIDBXUpdateUndoResealFails(c *C) { + // mock an error in a reseal which happens in the 'undo' path after snapd + // has been notified of a restart in the external DBX manager process + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealForDBUPdateCalls := 0 + resealForBootChainsCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealForDBUPdateCalls++ + return nil + })() + + defer fdestate.MockBackendResealKeyForBootChains( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, expectReseal bool) error { + resealForBootChainsCalls++ + return fmt.Errorf("mock error") + })() + + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 0) + + st.Unlock() + defer st.Lock() + + // 'external' DBX manger restarted + err = fdestate.EFISecureBootDBManagerStartup(st) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + // post cleanup inspect + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 1) + + // task cleanup may have run + if l := len(fdeSt.PendingExternalOperations); l == 1 { + c.Check(fdeSt.PendingExternalOperations[0].Status, Equals, fdestate.ErrorStatus) + } else if l > 1 { + c.Fatalf("unexpected number of operations in the state: %v", l) + } + + // and we have change in the state, but it is in an error status already + chgs := st.Changes() + c.Assert(chgs, HasLen, 1) + chg := chgs[0] + c.Check(chg.IsReady(), Equals, true) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n"+ + // undo failure + "- Prepare for external EFI DBX update .cannot complete reseal in undo: mock error.\n"+ + "- Reseal after external EFI DBX update .'startup' action invoked while an operation is in progress.") +} + +func (s *fdeMgrSuite) TestEFIDBXCleanupNoChange(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + s.startedManager(c, onClassic) + + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate(func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + panic("unexpected call") + })() + + err := fdestate.EFISecureBootDBUpdateCleanup(st) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + var fdeSt fdestate.FdeState + err = st.Get("fde", &fdeSt) + c.Assert(err, IsNil) + + c.Check(fdeSt.PendingExternalOperations, HasLen, 0) +} + +func createMockGrubCfg(baseDir string) error { + cfg := filepath.Join(baseDir, "EFI/ubuntu/grub.cfg") + if err := os.MkdirAll(filepath.Dir(cfg), 0755); err != nil { + return err + } + return os.WriteFile(cfg, []byte("# Snapd-Boot-Config-Edition: 1\n"), 0644) +} + +func (s *fdeMgrSuite) mockBootAssetsStateForModeenv(c *C) *asserts.Model { + model := boottest.MakeMockUC20Model() + + rootdir := dirs.GlobalRootDir + + modeenv := &boot.Modeenv{ + Mode: "run", + + // no recovery systems to keep things relatively short + // + CurrentTrustedRecoveryBootAssets: map[string][]string{ + "grubx64.efi": {"grub-hash"}, + "bootx64.efi": {"shim-hash"}, + }, + + CurrentTrustedBootAssets: map[string][]string{ + "grubx64.efi": {"run-grub-hash"}, + }, + + CurrentKernels: []string{"pc-kernel_500.snap"}, + + CurrentKernelCommandLines: []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + }, + Model: model.Model(), + BrandID: model.BrandID(), + Grade: string(model.Grade()), + ModelSignKeyID: model.SignKeyID(), + } + + c.Assert(modeenv.WriteTo(rootdir), IsNil) + + err := createMockGrubCfg(filepath.Join(rootdir, "run/mnt/ubuntu-seed")) + c.Assert(err, IsNil) + + err = createMockGrubCfg(filepath.Join(rootdir, "run/mnt/ubuntu-boot")) + c.Assert(err, IsNil) + + collectAssetHashes := func(bmaps ...map[string][]string) []string { + uniqAssetsHashes := map[string]bool{} + + for _, bm := range bmaps { + for bl, hashes := range bm { + for _, h := range hashes { + uniqAssetsHashes[fmt.Sprintf("%s-%s", bl, h)] = true + } + } + } + + l := make([]string, 0, len(uniqAssetsHashes)) + for h := range uniqAssetsHashes { + l = append(l, h) + } + + c.Logf("assets: %v", l) + return l + } + + // mock asset cache + boottest.MockAssetsCache(c, rootdir, "grub", + collectAssetHashes(modeenv.CurrentTrustedBootAssets, modeenv.CurrentTrustedRecoveryBootAssets)) + + const gadgetSnapYaml = `name: gadget +version: 1.0 +type: gadget +` + + restore := boot.MockSeedReadSystemEssential(func(seedDir, label string, essentialTypes []snap.Type, tm timings.Measurer) (*asserts.Model, []*seed.Snap, error) { + return model, []*seed.Snap{ + boottest.MockNamedKernelSeedSnap(snap.R(1), "pc-kernel"), + boottest.MockGadgetSeedSnap(c, gadgetSnapYaml, nil), + }, nil + }) + s.AddCleanup(restore) + + return model +} + +func (s *fdeMgrSuite) TestEFIDBXBlockedTasks(c *C) { + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + s.startedManager(c, onClassic) + + st.Lock() + defer st.Unlock() + + chg := st.NewChange("fde-efi-secureboot-db-update", "EFI secure boot key database update 1") + tsk := st.NewTask("efi-secureboot-db-update", "External EFI secure boot key database update") + chg.AddTask(tsk) + + op := &fdestate.ExternalOperation{ + Kind: "fde-efi-secureboot-db-update", + ChangeID: chg.ID(), + Status: fdestate.DoingStatus, + } + c.Assert(fdestate.AddExternalOperation(st, op), IsNil) + + c.Check(fdestate.IsEFISecurebootDBUpdateBlocked(tsk), Equals, true) + + // execute a single iteration of task runner + s.runnerIterationLocked(c) + c.Check(tsk.Status(), Equals, state.DoStatus) + + // now unblock it + op.SetStatus(fdestate.CompletingStatus) + c.Assert(fdestate.UpdateExternalOperation(st, op), IsNil) + + c.Check(fdestate.IsEFISecurebootDBUpdateBlocked(tsk), Equals, false) + + // execute a single iteration of task runner + s.runnerIterationLocked(c) + c.Check(tsk.Status(), Equals, state.DoingStatus) + + st.Unlock() + iterateUnlockedStateWaitingFor(st, chg.IsReady) + st.Lock() +} + +func (s *fdeMgrSuite) TestEFIDBXOperationAddWait(c *C) { + // add 2 changes, ant exercise the notification mechanism + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + s.startedManager(c, onClassic) + + st.Lock() + defer st.Unlock() + + op1, err := fdestate.AddEFISecurebootDBUpdateChange(st, device.SealingMethodTPM, []byte("payload 1")) + c.Assert(err, IsNil) + + op2, err := fdestate.AddEFISecurebootDBUpdateChange(st, device.SealingMethodTPM, []byte("payload 2")) + c.Assert(err, IsNil) + + sync1PreparedC := fdestate.DbxUpdatePreparedOKChan(st, op1.ChangeID) + sync2PreparedC := fdestate.DbxUpdatePreparedOKChan(st, op2.ChangeID) + + syncC := make(chan struct{}) + defer close(syncC) + doneC := make(chan struct{}) + + go func() { + <-syncC + st.Lock() + fdestate.NotifyDBXUpdatePrepareDoneOK(st, op1.ChangeID) + st.Unlock() + + <-syncC + st.Lock() + fdestate.NotifyDBXUpdatePrepareDoneOK(st, op2.ChangeID) + st.Unlock() + + close(doneC) + }() + + st.Unlock() + defer st.Lock() + syncC <- struct{}{} + <-sync1PreparedC + syncC <- struct{}{} + <-sync2PreparedC + <-doneC +} + +func (s *fdeMgrSuite) TestEFIDBXUpdateAffectedSnaps(c *C) { + // add 2 changes, ant exercise the notification mechanism + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + s.startedManager(c, onClassic) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + st.Lock() + defer st.Unlock() + + tsk := st.NewTask("foo", "foo task") + + names, err := fdestate.DbxUpdateAffectedSnaps(tsk) + c.Assert(err, IsNil) + c.Check(names, DeepEquals, []string{ + "pc", // gadget + "pc-kernel", // kernel + "core20", // base + }) +} + +func (s *fdeMgrSuite) TestEFIDBXConflictingSnaps(c *C) { + // mock an error in a reseal which happens in the 'undo' path after snapd + // has been notified of a restart in the external DBX manager process + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + st := s.st + onClassic := true + fdemgr := s.startedManager(c, onClassic) + s.o.AddManager(fdemgr) + s.o.AddManager(s.o.TaskRunner()) + c.Assert(s.o.StartUp(), IsNil) + + model := s.mockBootAssetsStateForModeenv(c) + s.mockDeviceInState(model) + + resealForDBUPdateCalls := 0 + resealForBootChainsCalls := 0 + defer fdestate.MockBackendResealKeysForSignaturesDBUpdate( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + resealForDBUPdateCalls++ + return nil + })() + + defer fdestate.MockBackendResealKeyForBootChains( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, expectReseal bool) error { + resealForBootChainsCalls++ + return fmt.Errorf("mock error") + })() + + st.Lock() + st.Set("seeded", true) + st.Unlock() + + c.Logf("overlord loop start") + s.o.Loop() + defer s.o.Stop() + + err := fdestate.EFISecureBootDBUpdatePrepare(st, fdestate.EFISecurebootDBX, []byte("payload")) + c.Assert(err, IsNil) + + st.Lock() + defer st.Unlock() + + c.Check(resealForDBUPdateCalls, Equals, 1) + c.Check(resealForBootChainsCalls, Equals, 0) + + gadgetSnapYamlContent := fmt.Sprintf(` +name: %s +version: "1.0" +type: gadget +`[1:], model.Gadget()) + kernelSnapYamlContent := fmt.Sprintf(` +name: %s +version: "1.0" +type: kernel +`[1:], model.Kernel()) + baseSnapYamlContent := fmt.Sprintf(` +name: %s +version: "1.0" +type: base +`[1:], model.Base()) + appSnapYamlContent := ` +name: apps +version: "1.0" +type: app +`[1:] + + for _, sn := range []struct { + snapYaml string + name string + noConflict bool + }{ + {snapYaml: gadgetSnapYamlContent, name: model.Gadget()}, + {snapYaml: kernelSnapYamlContent, name: model.Kernel()}, + {snapYaml: baseSnapYamlContent, name: model.Base()}, + {snapYaml: appSnapYamlContent, name: "apps", noConflict: true}, + } { + c.Logf("checking snap %s:\n%s", sn.name, sn.snapYaml) + path := snaptest.MakeTestSnapWithFiles(c, sn.snapYaml, nil) + + _, _, err = snapstate.InstallPath(st, &snap.SideInfo{ + RealName: sn.name, + }, path, "", "", snapstate.Flags{}, nil) + + if !sn.noConflict { + c.Check(err, ErrorMatches, fmt.Sprintf(`snap %q has \"fde-efi-secureboot-db-update\" change in progress`, sn.name)) + } else { + c.Check(err, IsNil) + } + } + +} + +func iterateUnlockedStateWaitingFor(st *state.State, pred func() bool) { + ok := false + for !ok { + st.Lock() + ok = pred() + if !ok { + st.EnsureBefore(0) + } + st.Unlock() + } +} diff --git a/overlord/fdestate/export_test.go b/overlord/fdestate/export_test.go index 6819793b2d6..7a192a3ad7e 100644 --- a/overlord/fdestate/export_test.go +++ b/overlord/fdestate/export_test.go @@ -31,10 +31,18 @@ var ( UpdateParameters = updateParameters + IsEFISecurebootDBUpdateBlocked = isEFISecurebootDBUpdateBlocked + FindFirstPendingExternalOperationByKind = findFirstPendingExternalOperationByKind FindFirstExternalOperationByChangeID = findFirstExternalOperationByChangeID AddExternalOperation = addExternalOperation + AddEFISecurebootDBUpdateChange = addEFISecurebootDBUpdateChange UpdateExternalOperation = updateExternalOperation + + NotifyDBXUpdatePrepareDoneOK = notifyDBXUpdatePrepareDoneOK + DbxUpdatePreparedOKChan = dbxUpdatePreparedOKChan + + DbxUpdateAffectedSnaps = dbxUpdateAffectedSnaps ) type ExternalOperation = externalOperation @@ -45,4 +53,12 @@ func MockBackendResealKeyForBootChains(f func(manager backend.FDEStateManager, m return restore } +func MockBackendResealKeysForSignaturesDBUpdate(f func(updateState backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, payload []byte) error) (restore func()) { + restore = testutil.Backup(&backendResealKeysForSignaturesDBUpdate) + backendResealKeysForSignaturesDBUpdate = f + return restore +} + +var NewModel = newModel + func (m *FDEManager) IsFunctional() error { return m.isFunctional() } diff --git a/overlord/fdestate/fdemgr.go b/overlord/fdestate/fdemgr.go index 79d5ef397cf..32019cef132 100644 --- a/overlord/fdestate/fdemgr.go +++ b/overlord/fdestate/fdemgr.go @@ -29,6 +29,7 @@ import ( "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/fdestate/backend" + "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/secboot" "github.com/snapcore/snapd/snapdenv" @@ -87,6 +88,21 @@ func Manager(st *state.State, runner *state.TaskRunner) (*FDEManager, error) { defer st.Unlock() st.Cache(fdeMgrKey{}, m) + snapstate.RegisterAffectedSnapsByKind("efi-secureboot-db-update", dbxUpdateAffectedSnaps) + + runner.AddHandler("efi-secureboot-db-update-prepare", + m.doEFISecurebootDBUpdatePrepare, m.undoEFISecurebootDBUpdatePrepare) + runner.AddCleanup("efi-secureboot-db-update-prepare", m.doEFISecurebootDBUpdatePrepareCleanup) + runner.AddHandler("efi-secureboot-db-update", m.doEFISecurebootDBUpdate, nil) + runner.AddBlocked(func(t *state.Task, running []*state.Task) bool { + switch t.Kind() { + case "efi-secureboot-db-update": + return isEFISecurebootDBUpdateBlocked(t) + } + + return false + }) + return m, nil } diff --git a/overlord/fdestate/fdemgr_test.go b/overlord/fdestate/fdemgr_test.go index 4da9424c9e2..b80b5cb451b 100644 --- a/overlord/fdestate/fdemgr_test.go +++ b/overlord/fdestate/fdemgr_test.go @@ -34,10 +34,14 @@ import ( "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget/device" + "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil/disks" + "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/fdestate" "github.com/snapcore/snapd/overlord/fdestate/backend" + "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/secboot" @@ -54,6 +58,7 @@ type fdeMgrSuite struct { rootdir string st *state.State runner *state.TaskRunner + o *overlord.Overlord } var _ = Suite(&fdeMgrSuite{}) @@ -67,8 +72,15 @@ func (s *fdeMgrSuite) SetUpTest(c *C) { dirs.SetRootDir(s.rootdir) s.AddCleanup(func() { dirs.SetRootDir("") }) - s.st = state.New(nil) - s.runner = state.NewTaskRunner(s.st) + s.o = overlord.Mock() + + s.st = s.o.State() + s.runner = s.o.TaskRunner() + + s.st.Lock() + repo := interfaces.NewRepository() + ifacerepo.Replace(s.st, repo) + s.st.Unlock() buf, restore := logger.MockLogger() s.AddCleanup(restore) @@ -92,6 +104,10 @@ func (s *fdeMgrSuite) SetUpTest(c *C) { s.AddCleanup(fdestate.MockVerifyPrimaryKeyDigest(func(devicePath string, alg crypto.Hash, salt, digest []byte) (bool, error) { panic("VerifyPrimaryKeyDigest is not mocked") })) + s.AddCleanup(fdestate.MockBackendResealKeysForSignaturesDBUpdate( + func(mgr backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams, update []byte) error { + panic("BackendResealKeysForSignaturesDBUpdate not mocked") + })) m := boot.Modeenv{ Mode: boot.ModeRun, @@ -107,6 +123,24 @@ func (s *fdeMgrSuite) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) } +func (s *fdeMgrSuite) mockDeviceInState(model *asserts.Model) { + s.st.Lock() + defer s.st.Unlock() + + s.AddCleanup(snapstatetest.MockDeviceContext(&snapstatetest.TrivialDeviceContext{ + DeviceModel: model, + })) +} + +func (s *fdeMgrSuite) runnerIterationLocked(c *C) { + err := func() error { + s.st.Unlock() + defer s.st.Lock() + return s.runner.Ensure() + }() + c.Assert(err, IsNil) +} + type instrumentedUnlocker struct { state *state.State unlocked int diff --git a/overlord/fdestate/fdestate.go b/overlord/fdestate/fdestate.go index 7721840f710..879d7dc358f 100644 --- a/overlord/fdestate/fdestate.go +++ b/overlord/fdestate/fdestate.go @@ -25,61 +25,20 @@ import ( "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" + "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/secboot" ) -var errNotImplemented = errors.New("not implemented") - var ( disksDMCryptUUIDFromMountPoint = disks.DMCryptUUIDFromMountPoint secbootGetPrimaryKeyDigest = secboot.GetPrimaryKeyDigest secbootVerifyPrimaryKeyDigest = secboot.VerifyPrimaryKeyDigest ) -// EFISecureBootDBManagerStartup indicates that the local EFI key database -// manager has started. -func EFISecureBootDBManagerStartup(st *state.State) error { - if _, err := device.SealedKeysMethod(dirs.GlobalRootDir); err == device.ErrNoSealedKeys { - return nil - } - - return errNotImplemented -} - -type EFISecurebootKeyDatabase int - -const ( - EFISecurebootPK EFISecurebootKeyDatabase = iota - EFISecurebootKEK - EFISecurebootDB - EFISecurebootDBX -) - -// EFISecureBootDBUpdatePrepare notifies notifies that the local EFI key -// database manager is about to update the database. -func EFISecureBootDBUpdatePrepare(st *state.State, db EFISecurebootKeyDatabase, payload []byte) error { - if _, err := device.SealedKeysMethod(dirs.GlobalRootDir); err == device.ErrNoSealedKeys { - return nil - } - - return errNotImplemented -} - -// EFISecureBootDBUpdateCleanup notifies that the local EFI key database manager -// has reached a cleanup stage of the update process. -func EFISecureBootDBUpdateCleanup(st *state.State) error { - if _, err := device.SealedKeysMethod(dirs.GlobalRootDir); err == device.ErrNoSealedKeys { - return nil - } - - return errNotImplemented -} - // Model is a json serializable secboot.ModelForSealing type Model struct { SeriesValue string `json:"series"` @@ -120,8 +79,8 @@ func (m *Model) SignKeyID() string { return m.SignKeyIDValue } -func newModel(m secboot.ModelForSealing) Model { - return Model{ +func newModel(m secboot.ModelForSealing) *Model { + return &Model{ SeriesValue: m.Series(), BrandIDValue: m.BrandID(), ModelValue: m.Model(), @@ -317,8 +276,7 @@ func (s *FdeState) updateParameters(role string, containerRole string, bootModes var convertedModels []*Model for _, model := range models { - m := newModel(model) - convertedModels = append(convertedModels, &m) + convertedModels = append(convertedModels, newModel(model)) } if roleInfo.Parameters == nil { @@ -392,6 +350,21 @@ func withFdeState(st *state.State, op func(fdeSt *FdeState) (modified bool, err return nil } +// fdeRelevantSnaps returns a list of snaps that are relevant in the context of +// FDE and associated boot policies. Specifically this includes the kernel, +// gadget and base snaps. +func fdeRelevantSnaps(st *state.State) ([]string, error) { + devCtx, err := snapstate.DeviceCtx(st, nil, nil) + if err != nil { + return nil, err + } + + // these snaps, or either their content is measured during boot + // TODO do we need anything for components? + + return []string{devCtx.Gadget(), devCtx.Kernel(), devCtx.Base()}, nil +} + func MockDMCryptUUIDFromMountPoint(f func(mountpoint string) (string, error)) (restore func()) { osutil.MustBeTestBinary("mocking DMCryptUUIDFromMountPoint can be done only from tests") diff --git a/tests/nested/manual/core20-fde-dbx/dbx-1-update.auth b/tests/nested/manual/core20-fde-dbx/dbx-1-update.auth new file mode 100644 index 0000000000000000000000000000000000000000..2215222e350edc3c0807cc46a505aa5132bfa853 GIT binary patch literal 2837 zcma)+c{r478^GtCnMTIU*e0eLds$L>XZRXKWo?lnWy_Knd)6{Zp`l?Em6MJoYbJ`2 zt&?Sx5HXCsp(sV65RF0NOkbVrt3KyC=lSD(uKRlL=f0lne(v870{MCU4(G?vkC@bs z!hM786MJKBYn&rll5;>RJOl~=1Yr=35IjcP1CU{GD6k3w3LykH(e}`IXg~nSpuzx< z3jG37f#D1o6o5jp!Adeu)yQS=shvjy2zmP6ISc!PzLxlY1`y(xzz|R%5&=5`hoN95 zMkb)NB$}uJY7x~5L=7z>!B!Hj{xu|g5C1Qp3dsLhl7A~06%dB-Z-InT0RZxf5cx&s zo)3ss+AGi4D|lC*VSCK?$tz$T12(v$)k`8b^VISO@x9Nq=!dpQ}znEzqDM+pzM zQ*Z-eF4JfX!VHfU&jpAo+4ChW76d|*I0WsUJzk0LEE-G_+!IKC zA*t4bGUO+uQ2giiggGffakZ~MuHyfv{OG6r&@G9RYrMzGhGpZBHPwd#AC znxsY$%`5O~L3zg)TW*e^P|}4oF$R9X*J!Ju8&_;5-IjBs6m9uwJ+_0r5Hz3+A%2j# zVZ3IYvDLjG#$<}C^SbUa;p|)+c2EM~yA>OB!cA&0bNEy_bnA($Ji`|B(YLUa(ib9Q z@V?Kn+q_jhSOy*Nc}o02)2^;-vY$v-kjfH+lFfn>X`zKxRldj19`{U`G`-$u+dD&e zN~LBAcsVNJZ^%qOoMp56bge70e57mV+&h-fkL}ky&rIg7XBE9MJeRK6Y|q#qU-e9Y zIbbP7paSOn(KF@K0xJGGtA7u~k4*i*2Y;C1Q~+Wi->5C9QDX0?KK}Ho{Y+X$NiJLO z{AcD&@M}r0Fs<=knA`A>f)D0Jj+pL=?COKgwIr^mEkdz0-`m3KCbhmUx-b=SYa(_K9pF+}6p(B;&)6QNT^WkGb<;*; zNa6724X}1=mMQjFWpO3?oK!=eYbqK&9b?Vz%3)>s7&kn!7_EICOc>tHWLx?7UzmSe zq4qA&LZ=~q!D;Ej=cd@S*X9y!5El|4_u|M)%QWa?r}dK2jo3j_tDtVd@^b+>_x>s{ zU)REfQW1=tjJN6Z_L%03On3ni2QTY*J?P1_dB}KkEoWa;S>75u)u{ji?3=dM@OM%E z`uhD#WZ-`-1dM(+kOD=hA33L*So?n8#6D3MtGB`w$6w|a6d_Db)jaCgDHv4X{md z=9tm=2#pVzt3*zq2PGmoR2T%~_#)&gu=BE?#}{cNrCydYra{!R)Z2Uv|1t1A|fik>@XBXTOfcjy$i&I>Q;) z7>hlh`TSk4<^)GGaCFF{SeMWae-z_qD0f|dcSenJh?YA!#3F>$K@00#&evDWqyKih z_x32yy1QLA%J;RxN)|E@9tw(^zAczEpnB%1MX$8BmHw`uEVN?6zp;@1oRWFKivkUm zf(ZfIfLq8m`(}T-y%Lb#@cZ{K-C* zUCU_obBY3WBSpq`-J(L`Nojc5ta&3>Gqo=x?w)~8Z27yS_P$&I` z4e@&j{@uBMf~UNxWIWIH<8)i{?n~G2#+oWTwDX!I$#qSk?ZNZ)_G3M zcKV@+!*xI5Blg|J@4{{6 zIc9cA?=WKV{LcQO))jr8*Z3x_)dp3|F?h+S3N5npS zHNrODHgjqvc+?gN%QlN(`JQW&j<`NKE!+Asr=Q?iG2!39<0ISNmXv7FIA47azJ+Fq z3Rg|3IrNAWox0j>z!J|H#L}L3^ ztG6?IkWQP3DB{hAE3u!1PR6l46nwc72QElsMBg-H`(Fh)SHnpZftGmT&g{5PY6>~7 z_K?#mht;{|W^v?&;S^;b`#V{P(^b2)IwyCcs!P%*W+D3^O=mFfDKLNW;e-Qux literal 0 HcmV?d00001 diff --git a/tests/nested/manual/core20-fde-dbx/task.yaml b/tests/nested/manual/core20-fde-dbx/task.yaml new file mode 100644 index 00000000000..baff5d92bc0 --- /dev/null +++ b/tests/nested/manual/core20-fde-dbx/task.yaml @@ -0,0 +1,157 @@ +summary: Verify EFI DBX updates in a Core20+ system using FDE + +details: | + Check that the EFI DBX can be updated in a system using FDE + +systems: [ubuntu-2*] + +environment: + NESTED_ENABLE_TPM: true + NESTED_ENABLE_SECURE_BOOT: true + NESTED_BUILD_SNAPD_FROM_CURRENT: true + NESTED_UBUNTU_SEED_SIZE: 1500M + +prepare: | + # TODO copy nested vars file to "$NESTED_ASSETS_DIR/OVMF_VARS.snakeoil.fd" + # + tests.nested build-image core + tests.nested create-vm core + +execute: | + echo "Establish initial state" + remote.exec sudo cat /var/lib/snapd/device/fde/boot-chains > boot-chains-before.json + reseal_count_start="$(jq -r '.["reseal-count"]' < boot-chains-before.json )" + + fetch_and_check_reseal_count_equal() { + local reseal_count_now + remote.exec sudo cat /var/lib/snapd/device/fde/boot-chains > boot-chains.json + reseal_count_now="$(jq -r '.["reseal-count"]' < boot-chains.json )" + test "$reseal_count_now" = "$1" + } + + echo "Smoke test action 'startup' without prior prepare call" + echo '{"action":"efi-secureboot-update-startup"}' | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > startup-smoke.out + + fetch_and_check_reseal_count_equal "$reseal_count_start" + + echo "Smoke test action 'cleanup' without prior prepare call" + echo '{"action":"efi-secureboot-update-db-cleanup"}' | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > cleanup-smoke.out + + fetch_and_check_reseal_count_equal "$reseal_count_start" + + echo "Attempt to 'prepare' with invalid data" + # fails with invalid data + update_payload_invalid="$(echo "foobar" | base64 -w0)" + echo "{\"action\":\"efi-secureboot-update-db-prepare\",\"key-database\":\"DBX\",\"payload\":\"$update_payload_invalid\"}" | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > prepare-invalid.out + + fetch_and_check_reseal_count_equal "$reseal_count_start" + + jq -r .result.message < prepare-invalid.out | \ + MATCH "cannot perform initial reseal of keys for DBX update: cannot add EFI secure boot and boot manager policy profiles" + + echo "Attempt a valid 'prepare' request" + # succeeds with correct update payload + update_payload="$(base64 -w0 dbx-1-update.auth)" + echo "{\"action\":\"efi-secureboot-update-db-prepare\",\"key-database\":\"DBX\",\"payload\":\"$update_payload\"}" | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > prepare.out + + jq -r .status < prepare.out | MATCH "OK" + remote.exec snap change --last=fde-efi-secureboot-db-update | \ + MATCH 'Done .* Prepare for external EFI DBX update' + remote.exec snap change --last=fde-efi-secureboot-db-update | \ + MATCH 'Do .* Reseal after external EFI DBX update' + + # there should have been a reaseal now + fetch_and_check_reseal_count_equal "$((reseal_count_start + 1))" + + echo "Attempt a valid 'prepare' request, thus causing a conflict" + echo "{\"action\":\"efi-secureboot-update-db-prepare\",\"key-database\":\"DBX\",\"payload\":\"$update_payload\"}" | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > prepare-conflict.out + + jq -r .result.message < prepare-conflict.out | \ + MATCH "cannot notify of update prepare: cannot start a new DBX update when conflicting actions are in progress" + + # reseal count unchanged + fetch_and_check_reseal_count_equal "$((reseal_count_start + 1))" + + echo "Complete the request with a 'cleanup' call" + echo '{"action":"efi-secureboot-update-db-cleanup"}' | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > cleanup-happy.out + + # which caused reseal + fetch_and_check_reseal_count_equal "$((reseal_count_start + 2))" + remote.exec snap change --last=fde-efi-secureboot-db-update | MATCH 'Done .* Reseal after external EFI DBX update' + + echo "Attempt a valid 'prepare' request" + echo "{\"action\":\"efi-secureboot-update-db-prepare\",\"key-database\":\"DBX\",\"payload\":\"$update_payload\"}" | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > prepare.out + jq -r .status < prepare.out | MATCH "OK" + remote.exec snap change --last=fde-efi-secureboot-db-update | \ + MATCH 'Done .* Prepare for external EFI DBX update' + remote.exec snap change --last=fde-efi-secureboot-db-update | \ + MATCH 'Do .* Reseal after external EFI DBX update' + + fetch_and_check_reseal_count_equal "$((reseal_count_start + 3))" + + echo "Which gets aborted due to external request" + echo '{"action":"efi-secureboot-update-startup"}' | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > startup.out + + fetch_and_check_reseal_count_equal "$((reseal_count_start + 4))" + remote.exec snap change --last=fde-efi-secureboot-db-update > snap-change-abort.out + MATCH 'Error .* Reseal after external EFI DBX update' < snap-change-abort.out + MATCH 'Undone .* Prepare for external EFI DBX update' < snap-change-abort.out + + echo "Attempt a valid 'prepare' request, followed by abort" + echo "{\"action\":\"efi-secureboot-update-db-prepare\",\"key-database\":\"DBX\",\"payload\":\"$update_payload\"}" | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > prepare.out + jq -r .status < prepare.out | MATCH "OK" + remote.exec snap change --last=fde-efi-secureboot-db-update | \ + MATCH 'Done .* Prepare for external EFI DBX update' + remote.exec snap change --last=fde-efi-secureboot-db-update | \ + MATCH 'Do .* Reseal after external EFI DBX update' + + fetch_and_check_reseal_count_equal "$((reseal_count_start + 5))" + + echo "Which gets aborted explicitly" + remote.exec sudo snap abort --last=fde-efi-secureboot-db-update + # snap watch will wait for change to complete, but exits with an error if + # the change is failed/undone like the one here + remote.exec sudo snap watch --last=fde-efi-secureboot-db-update || true + + remote.exec snap change --last=fde-efi-secureboot-db-update > snap-change-abort-explicit.out + MATCH 'Hold .* Reseal after external EFI DBX update' < snap-change-abort-explicit.out + MATCH 'Undone .* Prepare for external EFI DBX update' < snap-change-abort-explicit.out + + fetch_and_check_reseal_count_equal "$((reseal_count_start + 6))" + + # TODO update DBX + + echo "Attempt a valid 'prepare' request, followed by a reboot" + echo "{\"action\":\"efi-secureboot-update-db-prepare\",\"key-database\":\"DBX\",\"payload\":\"$update_payload\"}" | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > prepare.out + jq -r .status < prepare.out | MATCH "OK" + remote.exec snap change --last=fde-efi-secureboot-db-update | \ + MATCH 'Done .* Prepare for external EFI DBX update' + remote.exec snap change --last=fde-efi-secureboot-db-update | \ + MATCH 'Do .* Reseal after external EFI DBX update' + + fetch_and_check_reseal_count_equal "$((reseal_count_start + 7))" + + boot_id="$( tests.nested boot-id )" + remote.exec "sudo reboot" || true + remote.wait-for reboot "${boot_id}" + + # the system should come up + remote.exec "snap list" + + echo "Completed with a 'cleanup request'" + echo '{"action":"efi-secureboot-update-db-cleanup"}' | \ + remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/system-secureboot" > cleanup.out + jq -r .status < prepare.out | MATCH "OK" + remote.exec snap change --last=fde-efi-secureboot-db-update | MATCH 'Done .* Reseal after external EFI DBX update' + + fetch_and_check_reseal_count_equal "$((reseal_count_start + 8))"