From b050b5e38196e5fb2b3aabf32102a53e58768b96 Mon Sep 17 00:00:00 2001 From: Valentin David Date: Wed, 15 Jul 2026 15:29:33 +0200 Subject: [PATCH 1/2] secboot: complete the decision for auto-repair/reprovision --- overlord/fdestate/activate_state.go | 6 + overlord/fdestate/autorepair.go | 53 +++++-- overlord/fdestate/autorepair_test.go | 94 +++++++++-- overlord/fdestate/export_test.go | 2 +- secboot/secboot.go | 7 + secboot/secboot_sb.go | 150 +++++++++++++----- secboot/secboot_sb_test.go | 116 ++++++++++++-- .../core-recover-from-recovery/task.yaml | 19 ++- .../manual/hybrid-fde-recovery-keys/task.yaml | 31 +++- 9 files changed, 390 insertions(+), 88 deletions(-) diff --git a/overlord/fdestate/activate_state.go b/overlord/fdestate/activate_state.go index 333e9ed8ee0..05f5221e843 100644 --- a/overlord/fdestate/activate_state.go +++ b/overlord/fdestate/activate_state.go @@ -110,6 +110,8 @@ type FDESystemState struct { // AutoRepairResult is the status of the auto-repair attempt AutoRepairResult AutoRepairResult `json:"auto-repair-result"` + Recommendations []RecommendedRemedialAction `json:"recommendations,omitempty"` + // Preinstall provides information captured during install-time checks. Preinstall FDEPreinstallInfo `json:"preinstall"` } @@ -144,6 +146,7 @@ func SystemState(st *state.State, model *asserts.Model) (*FDESystemState, error) ret.AutoRepairResult = AutoRepairNotInitialized } else { ret.AutoRepairResult = repairResult.Result + ret.Recommendations = repairResult.Recommendations } s, err := getActivateState(st) @@ -187,6 +190,9 @@ func SystemState(st *state.State, model *asserts.Model) (*FDESystemState, error) if s.NumActivatedContainersWithRecoveryKey() != 0 { ret.Status = FDEStatusRecovery } else if secboot.ActivateStateHasDegradedErrors(s) { + // TODO: we should get degraded from the autorepair + // state since there are system wide errors to look + // at. ret.Status = FDEStatusDegraded } else { ret.Status = FDEStatusActive diff --git a/overlord/fdestate/autorepair.go b/overlord/fdestate/autorepair.go index 58a8817b9c1..52a78532416 100644 --- a/overlord/fdestate/autorepair.go +++ b/overlord/fdestate/autorepair.go @@ -61,12 +61,21 @@ const ( AutoRepairSuccess AutoRepairResult = "success" ) +type RecommendedRemedialAction string + +const ( + RecommendedRemedialActionPermitManual RecommendedRemedialAction = "permit-manual" + RecommendedRemedialActionRequireReprovision RecommendedRemedialAction = "require-reprovision" + RecommendedRemedialActionRequirePlatformReset RecommendedRemedialAction = "require-platform-reset" +) + const ( postInstallCheckTimeout = 2 * time.Minute ) type repairState struct { - Result AutoRepairResult `json:"result"` + Result AutoRepairResult `json:"result"` + Recommendations []RecommendedRemedialAction `json:"recommendations,omitempty"` } type repairStateForBoot struct { @@ -248,11 +257,12 @@ func autoRepair(st *state.State, runPostInstallChecks bool) (AutoRepairResult, e } logger.Noticef("WARNING: could not auto repair keyslots due to failed platform initialization:\n%s", strings.Join(messages, "\n")) } - return AutoRepairFailedPlatformInit, nil + return AutoRepairFailedEncryptionSupport, nil } } lockoutAuthFile := device.TpmLockoutAuthUnder(boot.InstallHostFDESaveDir) + // TODO: possibly we do not need to rotate the authorization keys for a repair... if err := secbootProvisionTPM(secboot.TPMPartialReprovision, lockoutAuthFile); err != nil { logger.Noticef("WARNING: could not repair platform: %v", err) return AutoRepairFailedPlatformInit, nil @@ -287,12 +297,6 @@ func autoRepair(st *state.State, runPostInstallChecks bool) (AutoRepairResult, e // auto-repair attempted has already occurred during the current boot, // this will do nothing. func AttemptAutoRepairIfNeeded(st *state.State, lockoutResetErr error, runPostInstallChecks bool) error { - if lockoutResetErr != nil { - // FIXME: we need to either try repair in some cases and save the - // error for the status API - return lockoutResetErr - } - // let's get the result from previous attempt during the // current boot previousResult, err := getRepairAttemptResult(st) @@ -314,19 +318,32 @@ func AttemptAutoRepairIfNeeded(st *state.State, lockoutResetErr error, runPostIn } if unlockedState.UbuntuData.UnlockKey != "recovery" && unlockedState.UbuntuSave.UnlockKey != "recovery" { setRepairAttemptResult(st, &repairState{Result: AutoRepairNotAttempted}) - return nil + return lockoutResetErr } } else if os.IsNotExist(err) { logger.Noticef("WARNING: the system booted with an old initrd without unlocked status reporting") setRepairAttemptResult(st, &repairState{Result: AutoRepairNotAttempted}) - return nil + return lockoutResetErr } else if err != nil { logger.Noticef("WARNING: error while getting activation state: %v", err) setRepairAttemptResult(st, &repairState{Result: AutoRepairNotAttempted}) - return nil + return lockoutResetErr } else { - if !secbootShouldAttemptRepair(s) { - setRepairAttemptResult(st, &repairState{Result: AutoRepairNotAttempted}) + remedialActions := secbootShouldAttemptRepair(s, lockoutResetErr) + if !remedialActions.AttemptRepair { + var recommendations []RecommendedRemedialAction + + if remedialActions.RequireReprovision { + recommendations = append(recommendations, RecommendedRemedialActionRequireReprovision) + } + if remedialActions.PermitManual { + recommendations = append(recommendations, RecommendedRemedialActionPermitManual) + } + + setRepairAttemptResult(st, &repairState{ + Result: AutoRepairNotAttempted, + Recommendations: recommendations, + }) return nil } } @@ -335,7 +352,15 @@ func AttemptAutoRepairIfNeeded(st *state.State, lockoutResetErr error, runPostIn if err != nil { return err } - setRepairAttemptResult(st, &repairState{Result: result}) + + var recommendations []RecommendedRemedialAction + if result != AutoRepairSuccess { + recommendations = append(recommendations, RecommendedRemedialActionRequireReprovision) + } + setRepairAttemptResult(st, &repairState{ + Result: result, + Recommendations: recommendations, + }) return nil } diff --git a/overlord/fdestate/autorepair_test.go b/overlord/fdestate/autorepair_test.go index 8a1298a96b5..6d39dbed9e5 100644 --- a/overlord/fdestate/autorepair_test.go +++ b/overlord/fdestate/autorepair_test.go @@ -128,8 +128,10 @@ func (s *autoRepairSuite) TestAttemptAutoRepairNeeded(c *C) { return nil })() - defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState) bool { - return true + defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions { + return secboot.RemedialActions{ + AttemptRepair: true, + } })() s.mockBootAssetsStateForModeenv(c) @@ -185,8 +187,8 @@ func (s *autoRepairSuite) TestAttemptAutoRepairNotNeeded(c *C) { return fmt.Errorf("Unexpected call") })() - defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState) bool { - return false + defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions { + return secboot.RemedialActions{} })() s.mockBootAssetsStateForModeenv(c) @@ -204,6 +206,47 @@ func (s *autoRepairSuite) TestAttemptAutoRepairNotNeeded(c *C) { c.Assert(err, IsNil) c.Check(result.Result, Equals, fdestate.AutoRepairResult("not-attempted")) + c.Check(result.Recommendations, IsNil) +} + +func (s *autoRepairSuite) TestAttemptAutoRepairReprovisionRequired(c *C) { + const onClassic = false + s.startedManager(c, onClassic) + + s.st.Lock() + defer s.st.Unlock() + + c.Assert(device.StampSealedKeys(dirs.GlobalRootDir, device.SealingMethodTPM), IsNil) + + s.createUnlockedState(c, sb.ActivationSucceededWithPlatformKey) + + defer fdestate.MockSecbootProvisionTPM(func(mode secboot.TPMProvisionMode, lockoutAuthFile string) error { + c.Errorf("Unexpected call") + return fmt.Errorf("Unexpected call") + })() + + defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions { + return secboot.RemedialActions{ + RequireReprovision: true, + } + })() + + s.mockBootAssetsStateForModeenv(c) + + defer fdestate.MockBackendResealKeyForBootChains(func(manager backend.FDEStateManager, method device.SealingMethod, rootdir string, params *boot.ResealKeyForBootChainsParams) error { + c.Errorf("Unexpected call") + return fmt.Errorf("Unexpected call") + })() + + const runPostInstallChecks = true + err := fdestate.AttemptAutoRepairIfNeeded(s.st, nil, runPostInstallChecks) + c.Assert(err, IsNil) + + result, err := fdestate.GetRepairAttemptResult(s.st) + c.Assert(err, IsNil) + + c.Check(result.Result, Equals, fdestate.AutoRepairResult("not-attempted")) + c.Check(result.Recommendations, DeepEquals, []fdestate.RecommendedRemedialAction{"require-reprovision"}) } func (s *autoRepairSuite) TestAttemptAutoRepairNeededBadReprovision(c *C) { @@ -223,8 +266,10 @@ func (s *autoRepairSuite) TestAttemptAutoRepairNeededBadReprovision(c *C) { return fmt.Errorf("some error") })() - defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState) bool { - return true + defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions { + return secboot.RemedialActions{ + AttemptRepair: true, + } })() s.mockBootAssetsStateForModeenv(c) @@ -246,6 +291,7 @@ func (s *autoRepairSuite) TestAttemptAutoRepairNeededBadReprovision(c *C) { c.Assert(err, IsNil) c.Check(result.Result, Equals, fdestate.AutoRepairResult("failed-platform-init")) + c.Check(result.Recommendations, DeepEquals, []fdestate.RecommendedRemedialAction{"require-reprovision"}) } func (s *autoRepairSuite) TestAttemptAutoRepairErrorNoActivateState(c *C) { @@ -273,6 +319,7 @@ func (s *autoRepairSuite) TestAttemptAutoRepairErrorNoActivateState(c *C) { c.Assert(err, IsNil) c.Check(result.Result, Equals, fdestate.AutoRepairResult("not-attempted")) + c.Check(result.Recommendations, IsNil) c.Check(logbuf.String(), testutil.Contains, `WARNING: the system booted with an old initrd without using activation API`) } @@ -291,8 +338,10 @@ func (s *autoRepairSuite) TestAttemptAutoRepairErrorNoActivateStateRecovery(c *C return nil })() - defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState) bool { - return true + defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions { + return secboot.RemedialActions{ + AttemptRepair: true, + } })() s.mockBootAssetsStateForModeenv(c) @@ -328,6 +377,7 @@ func (s *autoRepairSuite) TestAttemptAutoRepairErrorNoActivateStateRecovery(c *C c.Assert(err, IsNil) c.Check(result.Result, Equals, fdestate.AutoRepairResult("success")) + c.Check(result.Recommendations, IsNil) c.Check(logbuf.String(), testutil.Contains, `WARNING: the system booted with an old initrd without using activation API`) @@ -360,6 +410,7 @@ func (s *autoRepairSuite) TestAttemptAutoRepairErrorActivateState(c *C) { c.Assert(err, IsNil) c.Check(result.Result, Equals, fdestate.AutoRepairResult("not-attempted")) + c.Check(result.Recommendations, IsNil) c.Check(logbuf.String(), testutil.Contains, `WARNING: error while getting activation state: cannot read state`) } @@ -389,6 +440,7 @@ func (s *autoRepairSuite) TestAttemptAutoRepairErrorNoFileActivateState(c *C) { c.Assert(err, IsNil) c.Check(result.Result, Equals, fdestate.AutoRepairResult("not-attempted")) + c.Check(result.Recommendations, IsNil) c.Check(logbuf.String(), testutil.Contains, `WARNING: the system booted with an old initrd without unlocked status reporting`) } @@ -411,8 +463,10 @@ func (s *autoRepairSuite) TestAttemptAutoRepairNeededBadReseal(c *C) { return nil })() - defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState) bool { - return true + defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions { + return secboot.RemedialActions{ + AttemptRepair: true, + } })() s.mockBootAssetsStateForModeenv(c) @@ -437,6 +491,7 @@ func (s *autoRepairSuite) TestAttemptAutoRepairNeededBadReseal(c *C) { c.Assert(err, IsNil) c.Check(result.Result, Equals, fdestate.AutoRepairResult("failed-keyslots")) + c.Check(result.Recommendations, DeepEquals, []fdestate.RecommendedRemedialAction{"require-reprovision"}) } func (s *autoRepairSuite) TestIgnoreOldAutoRepairResult(c *C) { @@ -461,6 +516,7 @@ func (s *autoRepairSuite) TestIgnoreOldAutoRepairResult(c *C) { result, err = fdestate.GetRepairAttemptResult(s.st) c.Assert(err, IsNil) c.Check(result.Result, Equals, fdestate.AutoRepairResult("failed-platform-init")) + c.Check(result.Recommendations, IsNil) } func (s *autoRepairSuite) TestAttemptAutoRepairFailedPostinstallChecks(c *C) { @@ -479,8 +535,10 @@ func (s *autoRepairSuite) TestAttemptAutoRepairFailedPostinstallChecks(c *C) { return fmt.Errorf("unexpected call") })() - defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState) bool { - return true + defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions { + return secboot.RemedialActions{ + AttemptRepair: true, + } })() s.mockBootAssetsStateForModeenv(c) @@ -505,7 +563,8 @@ func (s *autoRepairSuite) TestAttemptAutoRepairFailedPostinstallChecks(c *C) { result, err := fdestate.GetRepairAttemptResult(s.st) c.Assert(err, IsNil) - c.Check(result.Result, Equals, fdestate.AutoRepairResult("failed-platform-init")) + c.Check(result.Result, Equals, fdestate.AutoRepairResult("failed-encryption-support")) + c.Check(result.Recommendations, DeepEquals, []fdestate.RecommendedRemedialAction{"require-reprovision"}) c.Check(logbuf.String(), testutil.Contains, `WARNING: could not auto repair keyslots due to failed platform initialization: some error`) } @@ -526,8 +585,10 @@ func (s *autoRepairSuite) TestAttemptAutoRepairFailedPostinstallChecksWithDetail return fmt.Errorf("unexpected call") })() - defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState) bool { - return true + defer fdestate.MockSecbootShouldAttemptRepair(func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions { + return secboot.RemedialActions{ + AttemptRepair: true, + } })() s.mockBootAssetsStateForModeenv(c) @@ -563,7 +624,8 @@ func (s *autoRepairSuite) TestAttemptAutoRepairFailedPostinstallChecksWithDetail result, err := fdestate.GetRepairAttemptResult(s.st) c.Assert(err, IsNil) - c.Check(result.Result, Equals, fdestate.AutoRepairResult("failed-platform-init")) + c.Check(result.Result, Equals, fdestate.AutoRepairResult("failed-encryption-support")) + c.Check(result.Recommendations, DeepEquals, []fdestate.RecommendedRemedialAction{"require-reprovision"}) c.Check(logbuf.String(), testutil.Contains, "WARNING: could not auto repair keyslots due to failed platform initialization:\n- error-1\n- error-2\n") } diff --git a/overlord/fdestate/export_test.go b/overlord/fdestate/export_test.go index a76574c62d6..32ebec70ec7 100644 --- a/overlord/fdestate/export_test.go +++ b/overlord/fdestate/export_test.go @@ -190,7 +190,7 @@ func MockOsutilBootID(f func() (string, error)) (restore func()) { return testutil.Mock(&osutilBootID, f) } -func MockSecbootShouldAttemptRepair(f func(as *secboot.ActivateState) bool) (restore func()) { +func MockSecbootShouldAttemptRepair(f func(as *secboot.ActivateState, lockoutResetErr error) secboot.RemedialActions) (restore func()) { return testutil.Mock(&secbootShouldAttemptRepair, f) } diff --git a/secboot/secboot.go b/secboot/secboot.go index 20887568793..2d0cc641740 100644 --- a/secboot/secboot.go +++ b/secboot/secboot.go @@ -371,3 +371,10 @@ type Disk interface { PartitionWithFsLabel(string) (Partition, error) DiskModel() string } + +type RemedialActions struct { + AttemptRepair bool + RequireReprovision bool + PermitManual bool + RequirePlatformReset bool +} diff --git a/secboot/secboot_sb.go b/secboot/secboot_sb.go index e6faa7491c0..e6f95011932 100644 --- a/secboot/secboot_sb.go +++ b/secboot/secboot_sb.go @@ -35,6 +35,7 @@ import ( sb "github.com/snapcore/secboot" sb_luks2 "github.com/snapcore/secboot/luks2" sb_plainkey "github.com/snapcore/secboot/plainkey" + sb_tpm2 "github.com/snapcore/secboot/tpm2" "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/kernel/fde" @@ -109,42 +110,75 @@ func LockSealedKeys() error { type ActivateState = sb.ActivateState -func shouldAttemptRepairOnFailure(a *ActivateState) bool { +func shouldAttemptRepairOnFailure(a *ActivateState) RemedialActions { + someKeySlotsFailWithPlatformError := false + allKeySlotsFailWithIncompatibleRoleParams := true + allKeySlotsFailWithIncorrectUserAuth := true + for _, activation := range a.Activations { for _, errorType := range activation.KeyslotErrors { - switch errorType { - case sb.KeyslotErrorPlatformFailure: - return false - case sb.KeyslotErrorIncorrectUserAuth: - return false - case sb.KeyslotErrorInvalidKeyData: - // FIXME: for now when not using tokens, we get this error. We should get - // a different error to ignore when we use external keydata - // return false - case sb.KeyslotErrorInvalidPrimaryKey: - return false - case sb.KeyslotErrorUnknown: - return false - case sb.KeyslotErrorNone: - // This is not really clear if that should happen. - return false - // FIXME: add this case after updating secboot when we have this error - //case sb.KeyslotErrorIncorrectRoleParams: - // return false - case sb.KeyslotErrorIncompatibleRoleParams: - // FIXME: we should ignore this case only if the given keyslot is not expected - // to work for the boot mode. For now we just ignore it for every keyslot. + if errorType != sb.KeyslotErrorIncorrectUserAuth { + allKeySlotsFailWithIncorrectUserAuth = false + } + if errorType != sb.KeyslotErrorIncompatibleRoleParams { + allKeySlotsFailWithIncompatibleRoleParams = false + } + if errorType == sb.KeyslotErrorPlatformFailure { + someKeySlotsFailWithPlatformError = true } } } - // We only encountered IncompatibleRoleParams errors. That - // means it could be repaired. - return true + + if someKeySlotsFailWithPlatformError { + // TODO: spec is incomplete about this case + return RemedialActions{ + RequireReprovision: true, + PermitManual: true, + } + } + + if allKeySlotsFailWithIncompatibleRoleParams { + return RemedialActions{ + AttemptRepair: true, + } + } + + if allKeySlotsFailWithIncorrectUserAuth { + return RemedialActions{ + RequireReprovision: true, + PermitManual: true, + } + } + + return RemedialActions{ + RequireReprovision: true, + } } // ShouldAttemptRepair reads an activate state and decides whether -// a repair should be attempted. -func ShouldAttemptRepair(a *ActivateState) bool { +// a repair should be attempted, or what other operations need +// to be done manually in order to repair it. +func ShouldAttemptRepair(a *ActivateState, lockoutResetErr error) RemedialActions { + needsGlobalRepair := false + + // TODO: we need to verify the SRK and ask for repair if missing + switch { + case errors.Is(lockoutResetErr, sb_tpm2.ErrTPMLockout): + return RemedialActions{ + RequirePlatformReset: true, + } + case errors.Is(lockoutResetErr, sb_tpm2.ErrLockoutAuthNotInitialized): + // authorization was performed with an empty auth value, we need to repair that. + // let's continue to see if a full reprovision is needed though. + needsGlobalRepair = true + case lockoutResetErr != nil: + // Maybe post install check during reprovision will help diagnostic the issue. + // This includes errors like LockoutAuthPolicyNotSupported. + return RemedialActions{ + RequireReprovision: true, + } + } + // First case: recovery. We do attempt repair if all keyslots // of any disk unlocked with recovery key failed with // IncompatibleRoleParams @@ -161,7 +195,12 @@ func ShouldAttemptRepair(a *ActivateState) bool { // - If the role params are incompatible and we detect that this key should have been actually used. // - If the role params were incorrect. // * Other error point to more complicated issues that will need reprovision instead. - needAutoRepair := false + hasErrorsWeCannotIgnore := false + + someKeySlotsFailWithIncompatibleRoleParams := false + someKeySlotsFailWithInvalidRoleParams := false + someKeySlotsFailWithInvalidKeyData := false + for _, activation := range a.Activations { for _, errorType := range activation.KeyslotErrors { switch errorType { @@ -169,29 +208,52 @@ func ShouldAttemptRepair(a *ActivateState) bool { case sb.KeyslotErrorNone: case sb.KeyslotErrorIncorrectUserAuth: - // Require reprovision, auto-repair is not enough case sb.KeyslotErrorInvalidKeyData: - return false + hasErrorsWeCannotIgnore = true + // TODO: check if keyslot is plainkey, + // if only plainkey have this error, + // then we should not permit manual + // fix. + someKeySlotsFailWithInvalidKeyData = true case sb.KeyslotErrorInvalidPrimaryKey: - return false + hasErrorsWeCannotIgnore = true case sb.KeyslotErrorPlatformFailure: - return false + hasErrorsWeCannotIgnore = true case sb.KeyslotErrorUnknown: - return false - - // Repair + hasErrorsWeCannotIgnore = true case sb.KeyslotErrorIncompatibleRoleParams: - // FIXME: we need to verify the keyslot is expected to work in the current mode. - // For now, it is likely we attempted the "default" keyslots first and we are in run mode. - needAutoRepair = true - // FIXME: add this case after updating secboot when we have this error - //case sb.KeyslotErrorIncorrectRoleParams: - // needAutoRepair = true + hasErrorsWeCannotIgnore = true + someKeySlotsFailWithIncompatibleRoleParams = true + case sb.KeyslotErrorInvalidRoleParams: + hasErrorsWeCannotIgnore = true + someKeySlotsFailWithInvalidRoleParams = true } } } - return needAutoRepair + if !hasErrorsWeCannotIgnore { + return RemedialActions{AttemptRepair: needsGlobalRepair} + } + + if someKeySlotsFailWithIncompatibleRoleParams || someKeySlotsFailWithInvalidRoleParams { + return RemedialActions{ + AttemptRepair: true, + } + } + + if someKeySlotsFailWithInvalidKeyData { + return RemedialActions{ + RequireReprovision: true, + PermitManual: true, + } + } + + // TODO: check that we have keyslot compatible with recover + // mode. If not we should permit manual fix. + + return RemedialActions{ + RequireReprovision: true, + } } // ActivateStateHasDegradedErrors looks for any error that is not @@ -213,6 +275,8 @@ func ActivateStateHasDegradedErrors(a *ActivateState) bool { // key data files. Maybe secboot should // provide a different error code. + case sb.KeyslotErrorInvalidRoleParams: + return true case sb.KeyslotErrorInvalidPrimaryKey: return true case sb.KeyslotErrorPlatformFailure: diff --git a/secboot/secboot_sb_test.go b/secboot/secboot_sb_test.go index 4e4c3798241..fb37b7d5e76 100644 --- a/secboot/secboot_sb_test.go +++ b/secboot/secboot_sb_test.go @@ -5917,23 +5917,39 @@ func (s *secbootSuite) TestShouldAttemptRepairDegraded(c *C) { // Incorrect user auth should not cause repair state.Activations["a"].KeyslotErrors["b"] = sb.KeyslotErrorIncorrectUserAuth - c.Check(secboot.ShouldAttemptRepair(state), Equals, false) + actions := secboot.ShouldAttemptRepair(state, nil) + c.Check(actions, DeepEquals, secboot.RemedialActions{}) for _, failure := range []sb.KeyslotErrorType{ // No repair cases since we need reprovision instead - sb.KeyslotErrorInvalidKeyData, sb.KeyslotErrorInvalidPrimaryKey, sb.KeyslotErrorPlatformFailure, sb.KeyslotErrorUnknown, } { state.Activations["a"].KeyslotErrors["b"] = failure - c.Check(secboot.ShouldAttemptRepair(state), Equals, false) + actions = secboot.ShouldAttemptRepair(state, nil) + c.Check(actions, DeepEquals, secboot.RemedialActions{ + RequireReprovision: true, + }) + } + + for _, failure := range []sb.KeyslotErrorType{ + sb.KeyslotErrorInvalidKeyData, + } { + state.Activations["a"].KeyslotErrors["b"] = failure + + actions = secboot.ShouldAttemptRepair(state, nil) + c.Check(actions, DeepEquals, secboot.RemedialActions{ + RequireReprovision: true, + PermitManual: true, + }) } state.Activations["a"].KeyslotErrors["b"] = sb.KeyslotErrorIncompatibleRoleParams - c.Check(secboot.ShouldAttemptRepair(state), Equals, true) + actions = secboot.ShouldAttemptRepair(state, nil) + c.Check(actions, DeepEquals, secboot.RemedialActions{AttemptRepair: true}) } func (s *secbootSuite) TestShouldAttemptRepairWithRecovery(c *C) { @@ -5953,21 +5969,103 @@ func (s *secbootSuite) TestShouldAttemptRepairWithRecovery(c *C) { }, } - c.Check(secboot.ShouldAttemptRepair(state), Equals, false) + actions := secboot.ShouldAttemptRepair(state, nil) + c.Check(actions, DeepEquals, secboot.RemedialActions{ + RequireReprovision: true, + }) for _, failure := range []sb.KeyslotErrorType{ // No repair cases since we need reprovision instead - //sb.KeyslotErrorInvalidKeyData, + sb.KeyslotErrorInvalidKeyData, sb.KeyslotErrorInvalidPrimaryKey, - sb.KeyslotErrorPlatformFailure, sb.KeyslotErrorUnknown, } { state.Activations["a"].KeyslotErrors["a"] = failure - c.Check(secboot.ShouldAttemptRepair(state), Equals, false) + actions = secboot.ShouldAttemptRepair(state, nil) + c.Check(actions, DeepEquals, secboot.RemedialActions{ + RequireReprovision: true, + }) + } + + for _, failure := range []sb.KeyslotErrorType{ + sb.KeyslotErrorPlatformFailure, + } { + state.Activations["a"].KeyslotErrors["a"] = failure + actions = secboot.ShouldAttemptRepair(state, nil) + c.Check(actions, DeepEquals, secboot.RemedialActions{ + RequireReprovision: true, + PermitManual: true, + }) } state.Activations["a"].KeyslotErrors["a"] = sb.KeyslotErrorIncompatibleRoleParams - c.Check(secboot.ShouldAttemptRepair(state), Equals, true) + c.Check(secboot.ShouldAttemptRepair(state, nil).AttemptRepair, Equals, true) +} + +func (s *secbootSuite) TestShouldAttemptRepairNoRepair(c *C) { + state := &secboot.ActivateState{} + state.Activations = map[string]*sb.ContainerActivateState{ + "a": { + Status: sb.ActivationSucceededWithPlatformKey, + KeyslotErrors: map[string]sb.KeyslotErrorType{ + "a": sb.KeyslotErrorNone, + }, + }, + } + + actions := secboot.ShouldAttemptRepair(state, nil) + c.Check(actions, DeepEquals, secboot.RemedialActions{}) +} + +func (s *secbootSuite) TestShouldAttemptRepairLockoutErrorLocked(c *C) { + state := &secboot.ActivateState{} + state.Activations = map[string]*sb.ContainerActivateState{ + "a": { + Status: sb.ActivationSucceededWithPlatformKey, + KeyslotErrors: map[string]sb.KeyslotErrorType{ + "a": sb.KeyslotErrorNone, + }, + }, + } + + actions := secboot.ShouldAttemptRepair(state, sb_tpm2.ErrTPMLockout) + c.Check(actions, DeepEquals, secboot.RemedialActions{ + RequirePlatformReset: true, + }) +} + +func (s *secbootSuite) TestShouldAttemptRepairLockoutErrorAnyError(c *C) { + state := &secboot.ActivateState{} + state.Activations = map[string]*sb.ContainerActivateState{ + "a": { + Status: sb.ActivationSucceededWithPlatformKey, + KeyslotErrors: map[string]sb.KeyslotErrorType{ + "a": sb.KeyslotErrorNone, + }, + }, + } + + actions := secboot.ShouldAttemptRepair(state, fmt.Errorf("some-unknown-error")) + c.Check(actions, DeepEquals, secboot.RemedialActions{ + RequireReprovision: true, + }) +} + +func (s *secbootSuite) TestShouldAttemptRepairLockoutErrorUninitialized(c *C) { + state := &secboot.ActivateState{} + state.Activations = map[string]*sb.ContainerActivateState{ + "a": { + Status: sb.ActivationSucceededWithPlatformKey, + KeyslotErrors: map[string]sb.KeyslotErrorType{ + "a": sb.KeyslotErrorNone, + }, + }, + } + + actions := secboot.ShouldAttemptRepair(state, sb_tpm2.ErrLockoutAuthNotInitialized) + c.Check(actions, DeepEquals, secboot.RemedialActions{ + AttemptRepair: true, + }) } func (s *secbootSuite) TestGetPCRHandleFromToken(c *C) { diff --git a/tests/nested/manual/core-recover-from-recovery/task.yaml b/tests/nested/manual/core-recover-from-recovery/task.yaml index e427d7579cd..8590e5c74b5 100644 --- a/tests/nested/manual/core-recover-from-recovery/task.yaml +++ b/tests/nested/manual/core-recover-from-recovery/task.yaml @@ -10,7 +10,9 @@ details: | # used recovery keys, and that we can reprovision and reseal then # reboot without recovery keys. -systems: [ubuntu-24.04-64] +systems: + - ubuntu-24.04-64 + - ubuntu-26.04-64 environment: NESTED_ENABLE_TPM: "true" @@ -123,8 +125,19 @@ execute: | fi if [ "${CLEAR_TPM}" = true ]; then - # For now we do not have reprovision implemented - exit 0 + wait_for_auto_repair_state pre-repair-state.json not-attempted + api_get_v2_system_info_storage_encrypted | gojq -c '.result.recommendations' | MATCH 'require-reprovision' + + remote.exec "sudo snap debug api /v2/systems?running=true 2>/dev/null" | gojq '.result["storage-encryption"].support' | MATCH "available" + + echo '{"action":"generate-recovery-key"}' | remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/systems" >rkey-reprovision.resp + gojq --raw-output '.result."recovery-key"' < rkey-reprovision.resp > rkey-reprovision.out + + echo '{"action":"reprovision"}' | remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/systems" >reprovision.resp + + reprovision_change="$(gojq --raw-output '.change' "${save_state}" + gojq -r '.result."auto-repair-result"' <"${save_state}" >last-autorepair-result + if MATCH "${expected}" /dev/null" | gojq '.result["storage-encryption"].support' | MATCH "available" # TODO:FDEM: We should have a way to check fix-encryption-support, for example by not allowing VMs by default. # echo '{"action": "fix-encryption-support", "fix-action": "proceed"}' | sudo remote.exec "snap debug api -X POST -H 'Content-Type: application/json' /v2/systems" - # Second step of reprovision is generate a new recovery key + # Third step of reprovision is generate a new recovery key echo '{"action":"generate-recovery-key"}' | remote.exec "sudo snap debug api -X POST -H 'Content-Type: application/json' /v2/systems" >rkey-reprovision.resp gojq --raw-output '.result."recovery-key"' < rkey-reprovision.resp > rkey-reprovision.out @@ -278,6 +300,11 @@ execute: | api_get_v2_system_info_storage_encrypted | gojq '.result.status' | MATCH "active" # Let's do a reprovision with a provision TPM. + wait_for_auto_repair_state autorepair-just-reboot.json not-attempted + # The system is working correctly so reprovision is not recommended + api_get_v2_system_info_storage_encrypted | gojq -c '.result.recommendations' | NOMATCH 'require-reprovision' + + # ... but we are still gonna do it! remote.exec "sudo snap debug api /v2/systems?running=true 2>/dev/null" | gojq '.result["storage-encryption"].support' | MATCH "available" # TODO:FDEM: We should have a way to check fix-encryption-support, for example by not allowing VMs by default. # echo '{"action": "fix-encryption-support", "fix-action": "proceed"}' | sudo remote.exec "snap debug api -X POST -H 'Content-Type: application/json' /v2/systems" From 8f726227aa0721d2be9abbd8bda3048a4e711666 Mon Sep 17 00:00:00 2001 From: Valentin David Date: Fri, 24 Jul 2026 15:18:10 +0200 Subject: [PATCH 2/2] overlord/devicestate: set new state after reprovision --- boot/export_test.go | 2 +- boot/makebootable.go | 9 +- boot/makebootable_test.go | 11 +- boot/seal.go | 13 ++- boot/seal_test.go | 12 +-- .../devicestate_install_api_test.go | 1 + overlord/devicestate/export_test.go | 2 +- overlord/devicestate/handlers_reprovision.go | 9 +- overlord/devicestate/running_system_test.go | 6 +- overlord/fdestate/backend/seal.go | 59 ++++++++-- overlord/fdestate/backend/seal_test.go | 10 +- overlord/fdestate/fdestate.go | 102 +++++++++++++++--- secboot/secboot_sb_test.go | 4 +- secboot/secboot_tpm.go | 26 +++-- tests/nested/core/core-fde-state/task.yaml | 6 +- .../core-recover-from-recovery/task.yaml | 17 +++ .../manual/hybrid-fde-recovery-keys/task.yaml | 34 ++++++ 17 files changed, 255 insertions(+), 68 deletions(-) diff --git a/boot/export_test.go b/boot/export_test.go index 683e43473e3..f4117d9e2de 100644 --- a/boot/export_test.go +++ b/boot/export_test.go @@ -245,7 +245,7 @@ func MockResealKeyForBootChains(f func(unlocker Unlocker, method device.SealingM } } -func MockSealKeyForBootChains(f func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *SealKeyForBootChainsParams) error) (restore func()) { +func MockSealKeyForBootChains(f func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *SealKeyForBootChainsParams, fdeState InitialFDEState) error) (restore func()) { old := SealKeyForBootChains SealKeyForBootChains = f return func() { diff --git a/boot/makebootable.go b/boot/makebootable.go index ce631294556..b4fd75b43b6 100644 --- a/boot/makebootable.go +++ b/boot/makebootable.go @@ -474,7 +474,7 @@ func isSealModeenvLocked() bool { return atomic.LoadInt32(&sealModeenvLocked) == 1 } -func makeRunnableSystemSeal(modeenv *Modeenv, model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *EncryptionSetup, makeOpts makeRunnableOptions) error { +func makeRunnableSystemSeal(modeenv *Modeenv, model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *EncryptionSetup, makeOpts makeRunnableOptions, fdeState InitialFDEState) error { tokens := UseTokens(model) if tokens { logger.Debugf("key data will be stored in tokens") @@ -507,6 +507,7 @@ func makeRunnableSystemSeal(modeenv *Modeenv, model *asserts.Model, protector se model, modeenv, flags, + fdeState, ); err != nil { return err } @@ -708,7 +709,7 @@ func makeRunnableSystem(model *asserts.Model, bootWith *BootableSet, bootAssets return fmt.Errorf("cannot check for fde-setup hook key protector: %v", err) } - if err := makeRunnableSystemSeal(modeenv, model, protector, encryption, makeOpts); err != nil { + if err := makeRunnableSystemSeal(modeenv, model, protector, encryption, makeOpts, nil); err != nil { return err } } @@ -823,7 +824,7 @@ func MakeRunnableSystemAfterDataReset(model *asserts.Model, bootWith *BootableSe // MakeRunnableSystemReprovision make the systems currently running bootable again. // This is intended to repair the boot of a system that was booted for example // with a recovery key. -func MakeRunnableSystemReprovision(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *EncryptionSetup) error { +func MakeRunnableSystemReprovision(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *EncryptionSetup, fdeState InitialFDEState) error { sealModeenvLock() defer sealModeenvUnlock() @@ -835,5 +836,5 @@ func MakeRunnableSystemReprovision(model *asserts.Model, protector secboot.KeyPr return makeRunnableSystemSeal(modeenv, model, protector, encryption, makeRunnableOptions{ Reprovision: true, SeedDir: dirs.SnapSeedDir, - }) + }, fdeState) } diff --git a/boot/makebootable_test.go b/boot/makebootable_test.go index 756aca2d75a..81145e06888 100644 --- a/boot/makebootable_test.go +++ b/boot/makebootable_test.go @@ -554,6 +554,7 @@ func (s *makeBootable20Suite) TestMakeSystemRunnableSealWithHookKeyProtector(c * model *asserts.Model, modeenv *boot.Modeenv, flags boot.MockSealKeyToModeenvFlags, + fdeState boot.InitialFDEState, ) error { gotFlags = flags return nil @@ -773,7 +774,7 @@ version: 5.0 defer restore() sealKeyForBootChainsCalled := 0 - restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams) error { + restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams, fdeState boot.InitialFDEState) error { sealKeyForBootChainsCalled++ c.Check(method, Equals, device.SealingMethodTPM) c.Check(key, Equals, myKey) @@ -1297,7 +1298,7 @@ version: 5.0 defer restore() sealKeyForBootChainsCalled := 0 - restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams) error { + restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams, fdeState boot.InitialFDEState) error { sealKeyForBootChainsCalled++ c.Check(method, Equals, device.SealingMethodTPM) c.Check(key, Equals, myKey) @@ -1501,7 +1502,7 @@ version: 5.0 defer restore() sealKeyForBootChainsCalled := 0 - restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams) error { + restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams, fdeState boot.InitialFDEState) error { sealKeyForBootChainsCalled++ c.Check(method, Equals, device.SealingMethodTPM) c.Check(key, DeepEquals, myKey) @@ -2667,7 +2668,7 @@ func (s *makeBootable20Suite) TestMakeSystemRunnableReprovision(c *C) { c.Assert(err, IsNil) sealKeyForBootChainsCalled := 0 - restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams) error { + restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams, fdeState boot.InitialFDEState) error { sealKeyForBootChainsCalled++ c.Check(method, Equals, device.SealingMethodTPM) c.Check(key, Equals, myKey) @@ -2769,7 +2770,7 @@ func (s *makeBootable20Suite) TestMakeSystemRunnableReprovision(c *C) { var protector secboot.KeyProtectorFactory - err = boot.MakeRunnableSystemReprovision(model, protector, encryptionSetup) + err = boot.MakeRunnableSystemReprovision(model, protector, encryptionSetup, nil) c.Assert(err, IsNil) c.Check(sealKeyForBootChainsCalled, Equals, 1) diff --git a/boot/seal.go b/boot/seal.go index e3b1a2306b9..3e2e8e0bce4 100644 --- a/boot/seal.go +++ b/boot/seal.go @@ -76,7 +76,7 @@ func MockResealKeyToModeenv(f func(rootdir string, modeenv *Modeenv, opts Reseal type MockSealKeyToModeenvFlags = sealKeyToModeenvFlags // MockSealKeyToModeenv is used for testing from other packages. -func MockSealKeyToModeenv(f func(key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, model *asserts.Model, modeenv *Modeenv, flags MockSealKeyToModeenvFlags) error) (restore func()) { +func MockSealKeyToModeenv(f func(key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, model *asserts.Model, modeenv *Modeenv, flags MockSealKeyToModeenvFlags, fdeState InitialFDEState) error) (restore func()) { old := sealKeyToModeenv sealKeyToModeenv = f return func() { @@ -119,6 +119,7 @@ func sealKeyToModeenvImpl( model *asserts.Model, modeenv *Modeenv, flags sealKeyToModeenvFlags, + fdeState InitialFDEState, ) error { if !isSealModeenvLocked() { return fmt.Errorf("internal error: cannot seal without the seal modeenv lock") @@ -147,7 +148,7 @@ func sealKeyToModeenvImpl( defer relock() } - return sealKeyToModeenvForMethod(method, key, saveKey, primaryKey, volumesAuth, checkResult, model, modeenv, flags) + return sealKeyToModeenvForMethod(method, key, saveKey, primaryKey, volumesAuth, checkResult, model, modeenv, flags, fdeState) } type BootChains struct { @@ -183,6 +184,10 @@ type SealKeyForBootChainsParams struct { KeyProtectorFactory secboot.KeyProtectorFactory } +type InitialFDEState interface { + UpdateParameters(role string, containerRole string, bootModes []string, models []secboot.ModelForSealing, tpmPCRProfile []byte, pcrHandle uint32) error +} + func sealKeyForBootChainsImpl( method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, @@ -190,6 +195,7 @@ func sealKeyForBootChainsImpl( volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *SealKeyForBootChainsParams, + fdeState InitialFDEState, ) error { return fmt.Errorf("FDE manager backend was not built in") } @@ -205,6 +211,7 @@ func sealKeyToModeenvForMethod( model *asserts.Model, modeenv *Modeenv, flags sealKeyToModeenvFlags, + fdeState InitialFDEState, ) error { params := &SealKeyForBootChainsParams{ LegacyFactoryResetKeyPath: flags.LegacyFactoryResetKeyPath, @@ -287,7 +294,7 @@ func sealKeyToModeenvForMethod( params.RoleToBlName[bootloader.RoleRunMode] = bl.Name() } - return SealKeyForBootChains(method, key, saveKey, primaryKey, volumesAuth, checkResult, params) + return SealKeyForBootChains(method, key, saveKey, primaryKey, volumesAuth, checkResult, params, fdeState) } var resealKeyToModeenv = resealKeyToModeenvImpl diff --git a/boot/seal_test.go b/boot/seal_test.go index 222ef770a8e..853c0e1000c 100644 --- a/boot/seal_test.go +++ b/boot/seal_test.go @@ -212,7 +212,7 @@ func (s *sealSuite) TestSealKeyToModeenv(c *C) { defer restore() sealKeyForBootChainsCalled := 0 - restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams) error { + restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams, fdeState boot.InitialFDEState) error { sealKeyForBootChainsCalled++ for _, d := range []string{boot.InitramfsSeedEncryptionKeyDir, filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/device/fde")} { @@ -280,7 +280,7 @@ func (s *sealSuite) TestSealKeyToModeenv(c *C) { Reprovision: tc.reprovision, StateUnlocker: u.unlocker, UseTokens: !tc.disableTokens, - }) + }, nil) c.Check(u.unlocked, Equals, 1) c.Check(sealKeyForBootChainsCalled, Equals, tc.expSealCalls) if tc.expErr == "" { @@ -1651,7 +1651,7 @@ func (s *sealSuite) TestSealToModeenvWithSecbootProtectorHappy(c *C) { myKey2 := secboot.CreateMockBootstrappedContainer() sealKeyForBootChainsCalled := 0 - restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams) error { + restore = boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams, fdeState boot.InitialFDEState) error { sealKeyForBootChainsCalled++ c.Check(method, Equals, device.SealingMethodFDESetupHook) c.Check(key, DeepEquals, myKey) @@ -1681,7 +1681,7 @@ func (s *sealSuite) TestSealToModeenvWithSecbootProtectorHappy(c *C) { defer boot.MockSealModeenvLocked()() - err := boot.SealKeyToModeenv(myKey, myKey2, nil, nil, nil, model, modeenv, boot.MockSealKeyToModeenvFlags{HookKeyProtectorFactory: &fakeProtectorFactory{}, UseTokens: true}) + err := boot.SealKeyToModeenv(myKey, myKey2, nil, nil, nil, model, modeenv, boot.MockSealKeyToModeenvFlags{HookKeyProtectorFactory: &fakeProtectorFactory{}, UseTokens: true}, nil) c.Assert(err, IsNil) c.Check(sealKeyForBootChainsCalled, Equals, 1) } @@ -1694,7 +1694,7 @@ func (s *sealSuite) TestSealToModeenvWithSecbootProtectorSad(c *C) { model := boottest.MakeMockUC20Model() sealKeyForBootChainsCalled := 0 - restore := boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams) error { + restore := boot.MockSealKeyForBootChains(func(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, primaryKey []byte, volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams, fdeState boot.InitialFDEState) error { sealKeyForBootChainsCalled++ return fmt.Errorf("seal key failed") @@ -1714,7 +1714,7 @@ func (s *sealSuite) TestSealToModeenvWithSecbootProtectorSad(c *C) { defer boot.MockSealModeenvLocked()() - err := boot.SealKeyToModeenv(key, saveKey, nil, nil, nil, model, modeenv, boot.MockSealKeyToModeenvFlags{HookKeyProtectorFactory: &fakeProtectorFactory{}}) + err := boot.SealKeyToModeenv(key, saveKey, nil, nil, nil, model, modeenv, boot.MockSealKeyToModeenvFlags{HookKeyProtectorFactory: &fakeProtectorFactory{}}, nil) c.Assert(err, ErrorMatches, `seal key failed`) c.Check(sealKeyForBootChainsCalled, Equals, 1) } diff --git a/overlord/devicestate/devicestate_install_api_test.go b/overlord/devicestate/devicestate_install_api_test.go index b03c19a5dc6..ef748e9b44a 100644 --- a/overlord/devicestate/devicestate_install_api_test.go +++ b/overlord/devicestate/devicestate_install_api_test.go @@ -512,6 +512,7 @@ func (s *deviceMgrInstallAPISuite) testInstallFinishStep(c *C, opts finishStepOp model *asserts.Model, modeenv *boot.Modeenv, flags boot.MockSealKeyToModeenvFlags, + fdeState boot.InitialFDEState, ) error { c.Check(model.Classic(), Equals, opts.installClassic) // Note that we cannot compare the full structure and we check diff --git a/overlord/devicestate/export_test.go b/overlord/devicestate/export_test.go index d6aaf2a226a..a54342b9019 100644 --- a/overlord/devicestate/export_test.go +++ b/overlord/devicestate/export_test.go @@ -824,7 +824,7 @@ func MockSecbootDeleteContainerKey(f func(disk string, name string) error) (rest return testutil.Mock(&secbootDeleteContainerKey, f) } -func MockBootMakeRunnableReprovision(f func(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *boot.EncryptionSetup) error) (restore func()) { +func MockBootMakeRunnableReprovision(f func(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *boot.EncryptionSetup, fdeState boot.InitialFDEState) error) (restore func()) { return testutil.Mock(&bootMakeRunnableReprovision, f) } diff --git a/overlord/devicestate/handlers_reprovision.go b/overlord/devicestate/handlers_reprovision.go index 00992963448..9059f1f437e 100644 --- a/overlord/devicestate/handlers_reprovision.go +++ b/overlord/devicestate/handlers_reprovision.go @@ -413,10 +413,13 @@ func (m *DeviceManager) doReprovision(t *state.Task, _ *tomb.Tomb) error { volumesAuth, checkResult) + fdeState, err := fdestate.InitialState(primaryKey) + err = bootMakeRunnableReprovision( deviceCtx.Model(), keyProtector, encryptionParams, + &fdeState, ) if err != nil { @@ -424,12 +427,15 @@ func (m *DeviceManager) doReprovision(t *state.Task, _ *tomb.Tomb) error { } // Step 7. Swap the state - // TODO: Actually swap the state. And move it after step 6. var oldState any errGetState := st.Get("fde", &oldState) if errGetState != nil && !errors.Is(errGetState, state.ErrNoState) { return fmt.Errorf("internal error: cannot get the fde state %v", err) } + // In case the write of protector key fails but we lose power + // before we can revert, having a nil fde state will make the + // system rebuild it on reboot instead of having a state that + // does not match. st.Set("fde", nil) // Step 6. write the protector key @@ -439,6 +445,7 @@ func (m *DeviceManager) doReprovision(t *state.Task, _ *tomb.Tomb) error { } return fmt.Errorf("cannot save the system-save key: %v", err) } + st.Set("fde", fdeState) // swapping the protector key is the sign we have finished revertReprovisionAttemptOnError = false diff --git a/overlord/devicestate/running_system_test.go b/overlord/devicestate/running_system_test.go index bbc28262dec..1225595fdb4 100644 --- a/overlord/devicestate/running_system_test.go +++ b/overlord/devicestate/running_system_test.go @@ -1032,7 +1032,7 @@ version: 1.0 })() bootMakeRunnableReprovisionCalls := 0 - defer devicestate.MockBootMakeRunnableReprovision(func(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *boot.EncryptionSetup) error { + defer devicestate.MockBootMakeRunnableReprovision(func(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *boot.EncryptionSetup, fdeState boot.InitialFDEState) error { bootMakeRunnableReprovisionCalls++ c.Check(encryption.PrimaryKey(), DeepEquals, []byte("new-primary-key")) @@ -1315,7 +1315,7 @@ version: 1.0 })() bootMakeRunnableReprovisionCalls := 0 - defer devicestate.MockBootMakeRunnableReprovision(func(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *boot.EncryptionSetup) error { + defer devicestate.MockBootMakeRunnableReprovision(func(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *boot.EncryptionSetup, fdeState boot.InitialFDEState) error { bootMakeRunnableReprovisionCalls++ c.Check(encryption.PrimaryKey(), DeepEquals, []byte("new-primary-key")) @@ -1878,7 +1878,7 @@ version: 1.0 return &secboot.PreinstallCheckResult{}, nil })() - defer devicestate.MockBootMakeRunnableReprovision(func(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *boot.EncryptionSetup) error { + defer devicestate.MockBootMakeRunnableReprovision(func(model *asserts.Model, protector secboot.KeyProtectorFactory, encryption *boot.EncryptionSetup, fdeState boot.InitialFDEState) error { return fmt.Errorf("make runnable failed") })() diff --git a/overlord/fdestate/backend/seal.go b/overlord/fdestate/backend/seal.go index 6aac7839c43..6abbd919ce8 100644 --- a/overlord/fdestate/backend/seal.go +++ b/overlord/fdestate/backend/seal.go @@ -96,18 +96,21 @@ func sealRunObjectKeys( pcrHandle uint32, useTokens bool, keyRole string, + fdeState boot.InitialFDEState, ) ([]byte, error) { modelParams, err := boot.SealKeyModelParams(pbc, roleToBlName) if err != nil { return nil, fmt.Errorf("cannot prepare for key sealing: %v", err) } + var models []secboot.ModelForSealing + hasClassicModel := false for _, m := range modelParams { if m.Model.Classic() { hasClassicModel = true - break } + models = append(models, m.Model) } sealKeyParams := &secboot.SealKeysParams{ @@ -130,11 +133,17 @@ func sealRunObjectKeys( // path only unseals one object because unsealing is expensive. // Furthermore, the run object key is stored on ubuntu-boot so that we do not // need to continually write/read keys from ubuntu-seed. - primaryKey, err := secbootSealKeys(runKeySealRequests(key, useTokens), sealKeyParams) + primaryKey, pcrProfile, err := secbootSealKeys(runKeySealRequests(key, useTokens), sealKeyParams) if err != nil { return nil, fmt.Errorf("cannot seal the encryption keys: %v", err) } + if fdeState != nil { + if err := fdeState.UpdateParameters(keyRole, "all", []string{"run", "recover"}, models, pcrProfile, pcrHandle); err != nil { + return nil, err + } + } + return primaryKey, nil } @@ -149,6 +158,7 @@ func sealFallbackObjectKeys( pcrHandle uint32, useTokens bool, keyRole string, + fdeState boot.InitialFDEState, ) error { // also seal the keys to the recovery bootchains as a fallback modelParams, err := boot.SealKeyModelParams(pbc, roleToBlName) @@ -156,12 +166,14 @@ func sealFallbackObjectKeys( return fmt.Errorf("cannot prepare for fallback key sealing: %v", err) } + var models []secboot.ModelForSealing + hasClassicModel := false for _, m := range modelParams { if m.Model.Classic() { hasClassicModel = true - break } + models = append(models, m.Model) } sealKeyParams := &secboot.SealKeysParams{ @@ -177,15 +189,25 @@ func sealFallbackObjectKeys( // The fallback object contains the ubuntu-data and ubuntu-save keys. The // key files are stored on ubuntu-seed, separate from ubuntu-data so they // can be used if ubuntu-data and ubuntu-boot are corrupted or unavailable. - - if _, err := secbootSealKeys(fallbackKeySealRequests(key, saveKey, factoryResetKeyPath, useTokens), sealKeyParams); err != nil { + _, pcrProfile, err := secbootSealKeys(fallbackKeySealRequests(key, saveKey, factoryResetKeyPath, useTokens), sealKeyParams) + if err != nil { return fmt.Errorf("cannot seal the fallback encryption keys: %v", err) } + if fdeState != nil { + // FIXME: we should create different pcr profile for ech. + if err := fdeState.UpdateParameters(keyRole, "ubuntu-save", []string{"recover", "factory-reset"}, models, pcrProfile, pcrHandle); err != nil { + return err + } + if err := fdeState.UpdateParameters(keyRole, "ubuntu-data", []string{"recover"}, models, pcrProfile, pcrHandle); err != nil { + return err + } + } + return nil } -func sealKeyForBootChainsHook(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, params *boot.SealKeyForBootChainsParams) error { +func sealKeyForBootChainsHook(method device.SealingMethod, key, saveKey secboot.BootstrappedContainer, params *boot.SealKeyForBootChainsParams, fdeState boot.InitialFDEState) error { if method != device.SealingMethodFDESetupHook { return fmt.Errorf("internal error: sealKeyForBootChainsHook called with unsupported method %q", method) } @@ -198,8 +220,12 @@ func sealKeyForBootChainsHook(method device.SealingMethod, key, saveKey secboot. sealingParams.AuxKeyFile = filepath.Join(boot.InstallHostFDESaveDir, "aux-key") } + var models []secboot.ModelForSealing for _, runChain := range params.RunModeBootChains { sealingParams.Model = runChain.ModelForSealing() + models = append(models, runChain.ModelForSealing()) + // We assume that factory-reset/installation/reprovision always reseal with one unique model. + // FIMXE: for reprovision, we might need to actually clean up the modeenv. break } @@ -224,6 +250,18 @@ func sealKeyForBootChainsHook(method device.SealingMethod, key, saveKey secboot. } } + if fdeState != nil { + if err := fdeState.UpdateParameters("run+recover", "all", []string{"run", "recover"}, models, nil, 0); err != nil { + return err + } + if err := fdeState.UpdateParameters("recover", "ubuntu-save", []string{"recover", "factory-reset"}, models, nil, 0); err != nil { + return err + } + if err := fdeState.UpdateParameters("recover", "ubuntu-data", []string{"recover"}, models, nil, 0); err != nil { + return err + } + } + return nil } @@ -234,10 +272,11 @@ func sealKeyForBootChainsBackend( volumesAuth *device.VolumesAuthOptions, checkResult *secboot.PreinstallCheckResult, params *boot.SealKeyForBootChainsParams, + fdeState boot.InitialFDEState, ) error { if method == device.SealingMethodFDESetupHook { // volumes authentication is not supported when using secboot hooks - return sealKeyForBootChainsHook(method, key, saveKey, params) + return sealKeyForBootChainsHook(method, key, saveKey, params, fdeState) } pbc := boot.ToPredictableBootChains(append(params.RunModeBootChains, params.RecoveryBootChains...)) @@ -261,13 +300,13 @@ func sealKeyForBootChainsBackend( // TODO:FDEM: refactor sealing functions to take a struct instead of so many // parameters - primaryKey, err = sealRunObjectKeys(key, pbc, primaryKey, volumesAuth, checkResult, params.RoleToBlName, handle, params.UseTokens, "run+recover") + primaryKey, err = sealRunObjectKeys(key, pbc, primaryKey, volumesAuth, checkResult, params.RoleToBlName, handle, params.UseTokens, "run+recover", fdeState) if err != nil { return err } err = sealFallbackObjectKeys(key, saveKey, rpbc, primaryKey, volumesAuth, checkResult, params.RoleToBlName, params.LegacyFactoryResetKeyPath, - handle, params.UseTokens, "recover") + handle, params.UseTokens, "recover", fdeState) if err != nil { return err } @@ -314,7 +353,7 @@ func MockSecbootProvisionTPM(f func(mode secboot.TPMProvisionMode, lockoutAuthFi } } -func MockSecbootSealKeys(f func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) ([]byte, error)) (restore func()) { +func MockSecbootSealKeys(f func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) ([]byte, secboot.SerializedPCRProfile, error)) (restore func()) { old := secbootSealKeys secbootSealKeys = f return func() { diff --git a/overlord/fdestate/backend/seal_test.go b/overlord/fdestate/backend/seal_test.go index 53d583a4a77..a6c22cc292b 100644 --- a/overlord/fdestate/backend/seal_test.go +++ b/overlord/fdestate/backend/seal_test.go @@ -213,7 +213,7 @@ func (s *sealSuite) TestSealKeyForBootChains(c *C) { // set mock key sealing sealKeysCalls := 0 - restore = fdeBackend.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) ([]byte, error) { + restore = fdeBackend.MockSecbootSealKeys(func(keys []secboot.SealKeyRequest, params *secboot.SealKeysParams) ([]byte, secboot.SerializedPCRProfile, error) { c.Check(params.AllowInsufficientDmaProtection, Equals, tc.onCore) c.Assert(provisionCalls, Equals, 1, Commentf("TPM must have been provisioned before")) c.Check(params.PCRPolicyCounterHandle, Equals, uint32(42)) @@ -293,7 +293,7 @@ func (s *sealSuite) TestSealKeyForBootChains(c *C) { } c.Assert(params.ModelParams[0].Model.Model(), Equals, modelName) - return nil, tc.sealErr + return nil, nil, tc.sealErr }) defer restore() @@ -388,7 +388,7 @@ func (s *sealSuite) TestSealKeyForBootChains(c *C) { InstallHostWritableDir: filepath.Join(boot.InstallUbuntuDataDir, "system-data"), UseTokens: !tc.disableTokens, } - err := boot.SealKeyForBootChains(device.SealingMethodTPM, myKey, myKey2, nil, volumesAuth, checkResult, params) + err := boot.SealKeyForBootChains(device.SealingMethodTPM, myKey, myKey2, nil, volumesAuth, checkResult, params, nil) c.Check(provisionCalls, Equals, tc.expProvisionCalls) c.Check(sealKeysCalls, Equals, tc.expSealCalls) @@ -591,7 +591,7 @@ func (s *sealSuite) testSealToModeenvWithFdeHookHappy(c *C, useTokens bool) { PrimaryKey: []byte{1, 2, 3, 4}, KeyProtectorFactory: mockFactory, } - err := boot.SealKeyForBootChains(device.SealingMethodFDESetupHook, dataContainer, saveContainer, nil, nil, nil, params) + err := boot.SealKeyForBootChains(device.SealingMethodFDESetupHook, dataContainer, saveContainer, nil, nil, nil, params, nil) c.Assert(err, IsNil) // check that runFDESetupHook was called the expected way c.Check(runFDESetupHookReqs, DeepEquals, []*fde.SetupRequest{ @@ -675,7 +675,7 @@ func (s *sealSuite) TestSealToModeenvWithFdeHookSad(c *C) { BootChains: bootChains, InstallHostWritableDir: filepath.Join(boot.InstallUbuntuDataDir, "system-data"), } - err := boot.SealKeyForBootChains(device.SealingMethodFDESetupHook, key, saveKey, nil, nil, nil, params) + err := boot.SealKeyForBootChains(device.SealingMethodFDESetupHook, key, saveKey, nil, nil, nil, params, nil) c.Assert(err, ErrorMatches, "hook failed") marker := filepath.Join(dirs.SnapFDEDirUnder(filepath.Join(dirs.GlobalRootDir, "/run/mnt/ubuntu-data/system-data")), "sealed-keys") c.Check(marker, testutil.FileAbsent) diff --git a/overlord/fdestate/fdestate.go b/overlord/fdestate/fdestate.go index 10e43201fea..d3a6fc3734d 100644 --- a/overlord/fdestate/fdestate.go +++ b/overlord/fdestate/fdestate.go @@ -20,6 +20,8 @@ package fdestate import ( "crypto" + "crypto/hmac" + "crypto/rand" "errors" "fmt" "sort" @@ -177,22 +179,43 @@ type FdeState struct { const fdeStateKey = "fde" -func initializeState(st *state.State) error { +func InitialState(primaryKey []byte) (FdeState, error) { var s FdeState - err := st.Get(fdeStateKey, &s) - if err == nil { - // TODO:FDEM: Do we need to do something in recover? - return nil + + var saltArray [32]byte + if _, err := rand.Read(saltArray[:]); err != nil { + return s, err } - if !errors.Is(err, state.ErrNoState) { - return err + h := hmac.New(secboot.HashAlg(defaultHashAlg).New, saltArray[:]) + h.Write(primaryKey) + + digest := KeyDigest{ + Algorithm: secboot.HashAlg(defaultHashAlg), + Salt: saltArray[:], + Digest: h.Sum(nil), } - disks, err := GetEncryptedContainers(st) - if err != nil { - return fmt.Errorf("cannot get encrypted disks: %w", err) + s.PrimaryKeys = map[int]PrimaryKeyInfo{ + 0: PrimaryKeyInfo{Digest: digest}, } + s.KeyslotRoles = map[string]KeyslotRoleInfo{ + "run": { + PrimaryKeyID: 0, + }, + "run+recover": { + PrimaryKeyID: 0, + }, + "recover": { + PrimaryKeyID: 0, + }, + } + + return s, nil +} + +func buildInitialState(disks []backend.EncryptedContainer) (FdeState, error) { + var s FdeState s.PrimaryKeys = map[int]PrimaryKeyInfo{} // Note that Parameters will be updated on first update @@ -217,7 +240,7 @@ func initializeState(st *state.State) error { digest, err := getPrimaryKeyDigest(disk.DevPath()) if err != nil { if !errors.Is(err, secboot.ErrKernelKeyNotFound) { - return fmt.Errorf("cannot obtain primary key digest for data device %s: %w", disk.DevPath(), err) + return s, fmt.Errorf("cannot obtain primary key digest for data device %s: %w", disk.DevPath(), err) } logger.Noticef("cannot obtain primary key digest for data device %s: %v", disk.DevPath(), err) } else { @@ -227,11 +250,11 @@ func initializeState(st *state.State) error { sameDigest, err := statePrimaryKey.Digest.verifyPrimaryKeyDigest(disk.DevPath()) if err != nil { if !errors.Is(err, secboot.ErrKernelKeyNotFound) { - return fmt.Errorf("cannot obtain primary key digest for data device %s: %w", disk.DevPath(), err) + return s, fmt.Errorf("cannot obtain primary key digest for data device %s: %w", disk.DevPath(), err) } logger.Noticef("cannot obtain primary key digest for data device %s: %v", disk.DevPath(), err) } else if !sameDigest { - return fmt.Errorf("primary key for data and save partition are not the same") + return s, fmt.Errorf("primary key for data and save partition are not the same") } } @@ -252,7 +275,7 @@ func initializeState(st *state.State) error { // we hint what is the best way to parse it. handle, err := secbootGetPCRHandle(disk.DevPath(), keyName, legacyKey, hintExpectFDEHook) if err != nil { - return fmt.Errorf("cannot obtain counter handle for %s (default): %w", disk.DevPath(), err) + return s, fmt.Errorf("cannot obtain counter handle for %s (default): %w", disk.DevPath(), err) } var profiles []string switch keyName { @@ -267,12 +290,34 @@ func initializeState(st *state.State) error { role.TPM2PCRPolicyRevocationCounter = handle s.KeyslotRoles[profile] = role } else if role.TPM2PCRPolicyRevocationCounter != handle { - return fmt.Errorf("found multiple revocation count for run keys") + return s, fmt.Errorf("found multiple revocation count for run keys") } } } } + return s, nil +} + +func initializeState(st *state.State) error { + var s FdeState + err := st.Get(fdeStateKey, &s) + if err == nil { + // TODO:FDEM: Do we need to do something in recover? + return nil + } + + if !errors.Is(err, state.ErrNoState) { + return err + } + + disks, err := GetEncryptedContainers(st) + if err != nil { + return fmt.Errorf("cannot get encrypted disks: %w", err) + } + + s, err = buildInitialState(disks) + _, hasStatePrimaryKey := s.PrimaryKeys[0] if !hasStatePrimaryKey { logger.Noticef("WARNING: no primary key was found") @@ -283,6 +328,33 @@ func initializeState(st *state.State) error { return nil } +func (s *FdeState) UpdateParameters(role string, containerRole string, bootModes []string, models []secboot.ModelForSealing, tpmPCRProfile []byte, pcrHandle uint32) error { + roleInfo, hasRole := s.KeyslotRoles[role] + if !hasRole { + return fmt.Errorf("cannot find keyslot role %s", role) + } + + var convertedModels []*Model + for _, model := range models { + convertedModels = append(convertedModels, newModel(model)) + } + + if roleInfo.Parameters == nil { + roleInfo.Parameters = make(map[string]KeyslotRoleParameters) + } + roleInfo.Parameters[containerRole] = KeyslotRoleParameters{ + Models: convertedModels, + BootModes: bootModes, + TPM2PCRProfile: tpmPCRProfile, + } + + roleInfo.TPM2PCRPolicyRevocationCounter = pcrHandle + + s.KeyslotRoles[role] = roleInfo + + return nil +} + func (s *FdeState) updateParameters(role string, containerRole string, bootModes []string, models []secboot.ModelForSealing, tpmPCRProfile []byte) error { roleInfo, hasRole := s.KeyslotRoles[role] if !hasRole { diff --git a/secboot/secboot_sb_test.go b/secboot/secboot_sb_test.go index fb37b7d5e76..a99349015f2 100644 --- a/secboot/secboot_sb_test.go +++ b/secboot/secboot_sb_test.go @@ -1494,7 +1494,7 @@ func (s *secbootSuite) TestSealKey(c *C) { }) defer restore() - _, err := secboot.SealKeys(myKeys, &myParams) + _, _, err := secboot.SealKeys(myKeys, &myParams) if tc.expectedErr == "" { c.Assert(err, IsNil) c.Assert(addPCRProfileCalls, Equals, 2) @@ -1983,7 +1983,7 @@ func (s *secbootSuite) TestSealKeyNoModelParams(c *C) { TPMPolicyAuthKeyFile: "policy-auth-key-file", } - _, err := secboot.SealKeys(myKeys, &myParams) + _, _, err := secboot.SealKeys(myKeys, &myParams) c.Assert(err, ErrorMatches, "at least one set of model-specific parameters is required") } diff --git a/secboot/secboot_tpm.go b/secboot/secboot_tpm.go index 916613d882d..9440813fbc1 100644 --- a/secboot/secboot_tpm.go +++ b/secboot/secboot_tpm.go @@ -531,24 +531,24 @@ func newTPMProtectedKey(tpm *sb_tpm2.Connection, creationParams *sb_tpm2.Protect // SealKeys seals the encryption keys according to the specified parameters. The // TPM must have already been provisioned. If sealed key already exists at the // PCR handle, SealKeys will fail and return an error. -func SealKeys(keys []SealKeyRequest, params *SealKeysParams) ([]byte, error) { +func SealKeys(keys []SealKeyRequest, params *SealKeysParams) ([]byte, SerializedPCRProfile, error) { numModels := len(params.ModelParams) if numModels < 1 { - return nil, fmt.Errorf("at least one set of model-specific parameters is required") + return nil, nil, fmt.Errorf("at least one set of model-specific parameters is required") } tpm, err := sbConnectToDefaultTPM() if err != nil { - return nil, fmt.Errorf("cannot connect to TPM: %v", err) + return nil, nil, fmt.Errorf("cannot connect to TPM: %v", err) } defer tpm.Close() if !isTPMEnabled(tpm) { - return nil, fmt.Errorf("TPM device is not enabled") + return nil, nil, fmt.Errorf("TPM device is not enabled") } pcrProfile, err := buildPCRProtectionProfile(params.ModelParams, params.CheckResult, params.AllowInsufficientDmaProtection) if err != nil { - return nil, err + return nil, nil, err } pcrHandle := params.PCRPolicyCounterHandle @@ -570,19 +570,19 @@ func SealKeys(keys []SealKeyRequest, params *SealKeysParams) ([]byte, error) { primaryKey = primaryKeyOut } if err != nil { - return nil, err + return nil, nil, err } if err := key.BootstrappedContainer.AddKey(key.SlotName, unlockKey); err != nil { - return nil, err + return nil, nil, err } keyWriter, err := key.getWriter() if err != nil { - return nil, err + return nil, nil, err } if err := protectedKey.WriteAtomic(keyWriter); err != nil { - return nil, err + return nil, nil, err } if key.SlotName == "default" { @@ -594,11 +594,15 @@ func SealKeys(keys []SealKeyRequest, params *SealKeysParams) ([]byte, error) { if primaryKey != nil && params.TPMPolicyAuthKeyFile != "" { if err := osutil.AtomicWriteFile(params.TPMPolicyAuthKeyFile, primaryKey, 0600, 0); err != nil { - return nil, fmt.Errorf("cannot write the policy auth key file: %v", err) + return nil, nil, fmt.Errorf("cannot write the policy auth key file: %v", err) } } - return primaryKey, nil + profileOut, err := mu.MarshalToBytes(pcrProfile) + if err != nil { + return nil, nil, fmt.Errorf("cannot marshal PCR profile: %w", err) + } + return primaryKey, profileOut, nil } // MaybeSealedKeyData interface wraps a sb_tpm2.SealedKeyData diff --git a/tests/nested/core/core-fde-state/task.yaml b/tests/nested/core/core-fde-state/task.yaml index 306e6454a78..4c2d8ea93cb 100644 --- a/tests/nested/core/core-fde-state/task.yaml +++ b/tests/nested/core/core-fde-state/task.yaml @@ -4,7 +4,11 @@ details: | This test checks checks that the FDE state is correctly initialized on first boot. -systems: [ubuntu-20.04-64, ubuntu-22.04-64, ubuntu-24.04-64] +systems: + - ubuntu-20.04-64 + - ubuntu-22.04-64 + - ubuntu-24.04-64 + - ubuntu-26.04-64 execute: | echo "Wait for the system to be seeded first" diff --git a/tests/nested/manual/core-recover-from-recovery/task.yaml b/tests/nested/manual/core-recover-from-recovery/task.yaml index 8590e5c74b5..ac3ea9f5e85 100644 --- a/tests/nested/manual/core-recover-from-recovery/task.yaml +++ b/tests/nested/manual/core-recover-from-recovery/task.yaml @@ -138,6 +138,23 @@ execute: | reprovision_change="$(gojq --raw-output '.change' new-state.json + + primary_alg="$(jq '.data.fde."primary-keys"."0".digest.alg' state.json)" + primary_digest="$(jq '.data.fde."primary-keys"."0".digest.digest' state.json)" + primary_salt="$(jq '.data.fde."primary-keys"."0".digest.salt' state.json)" + test "${primary_alg}" = '"sha256"' + test -n "${primary_digest}" + test -n "${primary_salt}" + + recover_pkey="$(jq '.data.fde."keyslot-roles".recover."primary-key-id"' state.json)" + run_pkey="$(jq '.data.fde."keyslot-roles".run."primary-key-id"' state.json)" + runrecover_pkey="$(jq '.data.fde."keyslot-roles"."run+recover"."primary-key-id"' state.json)" + + test "${recover_pkey}" = "0" + test "${run_pkey}" = "0" + test "${runrecover_pkey}" = "0" fi tests.nested vm set-recovery-key "" diff --git a/tests/nested/manual/hybrid-fde-recovery-keys/task.yaml b/tests/nested/manual/hybrid-fde-recovery-keys/task.yaml index c3402ebd55d..36f2699cb6c 100644 --- a/tests/nested/manual/hybrid-fde-recovery-keys/task.yaml +++ b/tests/nested/manual/hybrid-fde-recovery-keys/task.yaml @@ -292,6 +292,23 @@ execute: | retry -n 100 --wait 5 sh -c "remote.exec sudo snap changes | MATCH '^${reprovision_change}\s+(Done|Undone|Error)'" remote.exec sudo snap changes | MATCH "^${reprovision_change}\s+Done" + remote.exec "sudo cat /var/lib/snapd/state.json" >new-state.json + + primary_alg="$(jq '.data.fde."primary-keys"."0".digest.alg' state.json)" + primary_digest="$(jq '.data.fde."primary-keys"."0".digest.digest' state.json)" + primary_salt="$(jq '.data.fde."primary-keys"."0".digest.salt' state.json)" + test "${primary_alg}" = '"sha256"' + test -n "${primary_digest}" + test -n "${primary_salt}" + + recover_pkey="$(jq '.data.fde."keyslot-roles".recover."primary-key-id"' state.json)" + run_pkey="$(jq '.data.fde."keyslot-roles".run."primary-key-id"' state.json)" + runrecover_pkey="$(jq '.data.fde."keyslot-roles"."run+recover"."primary-key-id"' state.json)" + + test "${recover_pkey}" = "0" + test "${run_pkey}" = "0" + test "${runrecover_pkey}" = "0" + # We should be able to boot without recovery key now tests.nested vm set-recovery-key "" tests.nested vm stop @@ -318,6 +335,23 @@ execute: | retry -n 100 --wait 5 sh -c "remote.exec sudo snap changes | MATCH '^${reprovision_change_2}\s+(Done|Undone|Error)'" remote.exec sudo snap changes | MATCH "^${reprovision_change_2}\s+Done" + remote.exec "sudo cat /var/lib/snapd/state.json" >new-state.json + + primary_alg="$(jq '.data.fde."primary-keys"."0".digest.alg' state.json)" + primary_digest="$(jq '.data.fde."primary-keys"."0".digest.digest' state.json)" + primary_salt="$(jq '.data.fde."primary-keys"."0".digest.salt' state.json)" + test "${primary_alg}" = '"sha256"' + test -n "${primary_digest}" + test -n "${primary_salt}" + + recover_pkey="$(jq '.data.fde."keyslot-roles".recover."primary-key-id"' state.json)" + run_pkey="$(jq '.data.fde."keyslot-roles".run."primary-key-id"' state.json)" + runrecover_pkey="$(jq '.data.fde."keyslot-roles"."run+recover"."primary-key-id"' state.json)" + + test "${recover_pkey}" = "0" + test "${run_pkey}" = "0" + test "${runrecover_pkey}" = "0" + tests.nested vm stop tests.nested vm start