From b99bb2477e2cdadbaf785f33008906687da12a43 Mon Sep 17 00:00:00 2001 From: Zeyad Yasser Date: Fri, 20 Dec 2024 18:59:15 +0200 Subject: [PATCH] many: update install api to support passphrase authentication (#14845) * many: add skeleton to expose encryption features for target install system Signed-off-by: Zeyad Gouda * many: update install api to support passphrase authentication This just allows passing the authentication options during the setup-storage-encryption install step but the underlying passphrase support implementation is not there yet. Signed-off-by: Zeyad Gouda * fixup! secboot: add AuthModePIN Signed-off-by: Zeyad Gouda * fixup! client: add pin-auth feature enum Signed-off-by: Zeyad Gouda * fixup! daemon: use field initializers for readability Signed-off-by: Zeyad Gouda * fixup! o/devicestate: fix test matching old error message Signed-off-by: Zeyad Gouda * fixup! daemon: drop default initialization values Signed-off-by: Zeyad Gouda * fixup! many: move VolumesAuthOptions under gadget/device This avoids import snapd/secboot from the client package Signed-off-by: Zeyad Gouda * fixup! many: mark setup task when volumes auth is set This protects against an unexpected restart of snapd where the cached auth options will not be there. In this case, the task should detect this and fail. Signed-off-by: Zeyad Gouda * fixup! gadget/install: formatting improvements Signed-off-by: Zeyad Gouda --------- Signed-off-by: Zeyad Gouda --- client/systems.go | 16 +++ client/systems_test.go | 19 +++- daemon/api_systems.go | 5 +- daemon/api_systems_test.go | 80 ++++++++++---- daemon/export_api_systems_test.go | 3 +- gadget/device/encrypt.go | 54 +++++++++ gadget/device/encrypt_test.go | 42 +++++++ gadget/install/install.go | 10 +- gadget/install/install_dummy.go | 7 +- gadget/install/install_test.go | 4 +- overlord/devicestate/devicestate.go | 13 ++- .../devicestate_install_api_test.go | 103 +++++++++++++++++- .../devicestate_install_mode_test.go | 51 ++++++++- overlord/devicestate/export_test.go | 6 +- overlord/devicestate/handlers_install.go | 24 +++- overlord/install/install.go | 8 ++ 16 files changed, 397 insertions(+), 48 deletions(-) diff --git a/client/systems.go b/client/systems.go index 9ca6df2b8b0..2bc7a36f3eb 100644 --- a/client/systems.go +++ b/client/systems.go @@ -156,10 +156,22 @@ const ( StorageEncryptionSupportDefective = "defective" ) +type StorageEncryptionFeature string + +const ( + // Indicates that passphrase authentication is available. + StorageEncryptionFeaturePassphraseAuth StorageEncryptionFeature = "passphrase-auth" + // Indicates that PIN authentication is available. + StorageEncryptionFeaturePINAuth StorageEncryptionFeature = "pin-auth" +) + type StorageEncryption struct { // Support describes the level of hardware support available. Support StorageEncryptionSupport `json:"support"` + // Features is a list of available encryption features. + Features []StorageEncryptionFeature `json:"features"` + // StorageSafety can have values of asserts.StorageSafety StorageSafety string `json:"storage-safety,omitempty"` @@ -241,6 +253,10 @@ type InstallSystemOptions struct { // snaps and components, provide an empty OptionalInstallRequest with the // All field set to false. OptionalInstall *OptionalInstallRequest `json:"optional-install,omitempty"` + // VolumesAuth contains options for volumes authentication (e.g. passphrase + // authentication). If VolumesAuth is nil, the default is to have no + // authentication. + VolumesAuth *device.VolumesAuthOptions `json:"volumes-auth,omitempty"` } type OptionalInstallRequest struct { diff --git a/client/systems_test.go b/client/systems_test.go index 3a8ef1a55fc..c19fd8ab1d0 100644 --- a/client/systems_test.go +++ b/client/systems_test.go @@ -22,11 +22,13 @@ package client_test import ( "encoding/json" "io" + "time" "gopkg.in/check.v1" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/snap" ) @@ -367,9 +369,16 @@ func (cs *clientSuite) TestRequestSystemInstallHappy(c *check.C) { }, }, } + volumesAuth := &device.VolumesAuthOptions{ + Mode: device.AuthModePassphrase, + Passphrase: "1234", + KDFType: "argon2i", + KDFTime: 2 * time.Second, + } opts := &client.InstallSystemOptions{ - Step: client.InstallStepFinish, - OnVolumes: vols, + Step: client.InstallStepFinish, + OnVolumes: vols, + VolumesAuth: volumesAuth, } chgID, err := cs.cli.InstallSystem("1234", opts) c.Assert(err, check.IsNil) @@ -412,5 +421,11 @@ func (cs *clientSuite) TestRequestSystemInstallHappy(c *check.C) { }, }, }, + "volumes-auth": map[string]interface{}{ + "mode": "passphrase", + "passphrase": "1234", + "kdf-type": "argon2i", + "kdf-time": float64(2 * time.Second), + }, }) } diff --git a/daemon/api_systems.go b/daemon/api_systems.go index 4e45109a9b9..4ee28d50528 100644 --- a/daemon/api_systems.go +++ b/daemon/api_systems.go @@ -140,6 +140,9 @@ func storageEncryption(encInfo *install.EncryptionSupportInfo) *client.StorageEn storageEnc.Support = client.StorageEncryptionSupportUnavailable storageEnc.UnavailableReason = encInfo.UnavailableWarning } + if encInfo.PassphraseAuthAvailable { + storageEnc.Features = append(storageEnc.Features, client.StorageEncryptionFeaturePassphraseAuth) + } return storageEnc } @@ -322,7 +325,7 @@ func postSystemActionInstall(c *Command, systemLabel string, req *systemActionRe switch req.Step { case client.InstallStepSetupStorageEncryption: - chg, err := devicestateInstallSetupStorageEncryption(st, systemLabel, req.OnVolumes) + chg, err := devicestateInstallSetupStorageEncryption(st, systemLabel, req.OnVolumes, req.VolumesAuth) if err != nil { return BadRequest("cannot setup storage encryption for install from %q: %v", systemLabel, err) } diff --git a/daemon/api_systems_test.go b/daemon/api_systems_test.go index 9c32adaed15..3b70957ca98 100644 --- a/daemon/api_systems_test.go +++ b/daemon/api_systems_test.go @@ -817,45 +817,70 @@ func (s *systemsSuite) TestSystemsGetSystemDetailsForLabel(c *check.C) { } for _, tc := range []struct { - disabled, available bool - storageSafety asserts.StorageSafety - typ device.EncryptionType - unavailableErr, unavailableWarning string + disabled, available, passphraseAuthAvailable bool + storageSafety asserts.StorageSafety + typ device.EncryptionType + unavailableErr, unavailableWarning string expectedSupport client.StorageEncryptionSupport expectedStorageSafety, expectedUnavailableReason string + expectedEncryptionFeatures []client.StorageEncryptionFeature }{ { - true, false, asserts.StorageSafetyPreferEncrypted, "", "", "", - client.StorageEncryptionSupportDisabled, "", "", + disabled: true, + storageSafety: asserts.StorageSafetyPreferEncrypted, + + expectedSupport: client.StorageEncryptionSupportDisabled, }, { - false, false, asserts.StorageSafetyPreferEncrypted, "", "", "unavailable-warn", - client.StorageEncryptionSupportUnavailable, "prefer-encrypted", "unavailable-warn", + storageSafety: asserts.StorageSafetyPreferEncrypted, + unavailableWarning: "unavailable-warn", + + expectedSupport: client.StorageEncryptionSupportUnavailable, + expectedStorageSafety: "prefer-encrypted", + expectedUnavailableReason: "unavailable-warn", }, { - false, true, asserts.StorageSafetyPreferEncrypted, "cryptsetup", "", "", - client.StorageEncryptionSupportAvailable, "prefer-encrypted", "", + available: true, + storageSafety: asserts.StorageSafetyPreferEncrypted, + typ: "cryptsetup", + + expectedSupport: client.StorageEncryptionSupportAvailable, + expectedStorageSafety: "prefer-encrypted", }, { - false, true, asserts.StorageSafetyPreferUnencrypted, "cryptsetup", "", "", - client.StorageEncryptionSupportAvailable, "prefer-unencrypted", "", + available: true, + storageSafety: asserts.StorageSafetyPreferUnencrypted, + typ: "cryptsetup", + + expectedSupport: client.StorageEncryptionSupportAvailable, + expectedStorageSafety: "prefer-unencrypted", }, { - false, false, asserts.StorageSafetyEncrypted, "", "unavailable-err", "", - client.StorageEncryptionSupportDefective, "encrypted", "unavailable-err", + storageSafety: asserts.StorageSafetyEncrypted, + unavailableErr: "unavailable-err", + + expectedSupport: client.StorageEncryptionSupportDefective, + expectedStorageSafety: "encrypted", + expectedUnavailableReason: "unavailable-err", }, { - false, true, asserts.StorageSafetyEncrypted, "", "", "", - client.StorageEncryptionSupportAvailable, "encrypted", "", + available: true, + passphraseAuthAvailable: true, + storageSafety: asserts.StorageSafetyEncrypted, + + expectedSupport: client.StorageEncryptionSupportAvailable, + expectedStorageSafety: "encrypted", + expectedEncryptionFeatures: []client.StorageEncryptionFeature{client.StorageEncryptionFeaturePassphraseAuth}, }, } { mockEncryptionSupportInfo := &install.EncryptionSupportInfo{ - Available: tc.available, - Disabled: tc.disabled, - StorageSafety: tc.storageSafety, - UnavailableErr: errors.New(tc.unavailableErr), - UnavailableWarning: tc.unavailableWarning, + Available: tc.available, + Disabled: tc.disabled, + StorageSafety: tc.storageSafety, + UnavailableErr: errors.New(tc.unavailableErr), + UnavailableWarning: tc.unavailableWarning, + PassphraseAuthAvailable: tc.passphraseAuthAvailable, } r := daemon.MockDeviceManagerSystemAndGadgetAndEncryptionInfo(func(mgr *devicestate.DeviceManager, label string) (*devicestate.System, *gadget.Info, *install.EncryptionSupportInfo, error) { @@ -890,6 +915,7 @@ func (s *systemsSuite) TestSystemsGetSystemDetailsForLabel(c *check.C) { }, StorageEncryption: &client.StorageEncryption{ Support: tc.expectedSupport, + Features: tc.expectedEncryptionFeatures, StorageSafety: tc.expectedStorageSafety, UnavailableReason: tc.expectedUnavailableReason, }, @@ -1221,9 +1247,11 @@ func (s *systemsSuite) TestSystemInstallActionSetupStorageEncryptionCallsDevices nCalls := 0 var gotOnVolumes map[string]*gadget.Volume var gotLabel string - r := daemon.MockDevicestateInstallSetupStorageEncryption(func(st *state.State, label string, onVolumes map[string]*gadget.Volume) (*state.Change, error) { + var gotVolumesAuth *device.VolumesAuthOptions + r := daemon.MockDevicestateInstallSetupStorageEncryption(func(st *state.State, label string, onVolumes map[string]*gadget.Volume, volumesAuth *device.VolumesAuthOptions) (*state.Change, error) { gotLabel = label gotOnVolumes = onVolumes + gotVolumesAuth = volumesAuth nCalls++ return st.NewChange("foo", "..."), nil }) @@ -1237,6 +1265,10 @@ func (s *systemsSuite) TestSystemInstallActionSetupStorageEncryptionCallsDevices "bootloader": "grub", }, }, + "volumes-auth": map[string]interface{}{ + "mode": "passphrase", + "passphrase": "1234", + }, } b, err := json.Marshal(body) c.Assert(err, check.IsNil) @@ -1258,6 +1290,10 @@ func (s *systemsSuite) TestSystemInstallActionSetupStorageEncryptionCallsDevices Bootloader: "grub", }, }) + c.Check(gotVolumesAuth, check.DeepEquals, &device.VolumesAuthOptions{ + Mode: device.AuthModePassphrase, + Passphrase: "1234", + }) c.Check(soon, check.Equals, 1) } diff --git a/daemon/export_api_systems_test.go b/daemon/export_api_systems_test.go index c42821d63da..8dde3bd7718 100644 --- a/daemon/export_api_systems_test.go +++ b/daemon/export_api_systems_test.go @@ -21,6 +21,7 @@ package daemon import ( "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/install" "github.com/snapcore/snapd/overlord/state" @@ -51,7 +52,7 @@ func MockDevicestateInstallFinish(f func(*state.State, string, map[string]*gadge return restore } -func MockDevicestateInstallSetupStorageEncryption(f func(*state.State, string, map[string]*gadget.Volume) (*state.Change, error)) (restore func()) { +func MockDevicestateInstallSetupStorageEncryption(f func(*state.State, string, map[string]*gadget.Volume, *device.VolumesAuthOptions) (*state.Change, error)) (restore func()) { restore = testutil.Backup(&devicestateInstallSetupStorageEncryption) devicestateInstallSetupStorageEncryption = f return restore diff --git a/gadget/device/encrypt.go b/gadget/device/encrypt.go index f1620e9c337..40050653966 100644 --- a/gadget/device/encrypt.go +++ b/gadget/device/encrypt.go @@ -24,6 +24,7 @@ import ( "fmt" "os" "path/filepath" + "time" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" @@ -151,3 +152,56 @@ const ( func (et EncryptionType) IsLUKS() bool { return et == EncryptionTypeLUKS || et == EncryptionTypeLUKSWithICE } + +// AuthMode corresponds to an authentication mechanism. +type AuthMode string + +const ( + AuthModePassphrase AuthMode = "passphrase" + AuthModePIN AuthMode = "pin" +) + +// VolumesAuthOptions contains options for the volumes authentication +// mechanism (e.g. passphrase authentication). +// +// TODO: Add PIN option when secboot support lands. +type VolumesAuthOptions struct { + Mode AuthMode `json:"mode,omitempty"` + Passphrase string `json:"passphrase,omitempty"` + KDFType string `json:"kdf-type,omitempty"` + KDFTime time.Duration `json:"kdf-time,omitempty"` +} + +// Validates authentication options. +func (o *VolumesAuthOptions) Validate() error { + if o == nil { + return nil + } + + switch o.Mode { + case AuthModePassphrase: + // TODO: Add entropy/quality checks on passphrase. + if len(o.Passphrase) == 0 { + return fmt.Errorf("passphrase cannot be empty") + } + case AuthModePIN: + if o.KDFType != "" { + return fmt.Errorf("%q authentication mode does not support custom kdf types", AuthModePIN) + } + return fmt.Errorf("%q authentication mode is not implemented", AuthModePIN) + default: + return fmt.Errorf("invalid authentication mode %q, only %q and %q modes are supported", o.Mode, AuthModePassphrase, AuthModePIN) + } + + switch o.KDFType { + case "argon2i", "argon2id", "pbkdf2", "": + default: + return fmt.Errorf("invalid kdf type %q, only \"argon2i\", \"argon2id\" and \"pbkdf2\" are supported", o.KDFType) + } + + if o.KDFTime < 0 { + return fmt.Errorf("kdf time cannot be negative") + } + + return nil +} diff --git a/gadget/device/encrypt_test.go b/gadget/device/encrypt_test.go index dfcb9ae7295..5b9537afbdc 100644 --- a/gadget/device/encrypt_test.go +++ b/gadget/device/encrypt_test.go @@ -23,6 +23,7 @@ import ( "os" "path/filepath" "testing" + "time" . "gopkg.in/check.v1" @@ -150,3 +151,44 @@ func (s *deviceSuite) TestSealedKeysMethodWithWrongContentHappy(c *C) { c.Check(err, IsNil) c.Check(string(mth), Equals, "invalid-sealing-method") } + +func (s *deviceSuite) TestVolumesAuthOptionsValidateHappy(c *C) { + var opts *device.VolumesAuthOptions + + // VolumesAuthOptions can be nil + c.Assert(opts.Validate(), IsNil) + // Valid kdf types + for _, kdfType := range []string{"argon2i", "argon2id", "pbkdf2"} { + opts = &device.VolumesAuthOptions{ + Mode: device.AuthModePassphrase, + Passphrase: "1234", + KDFType: kdfType, + KDFTime: 2 * time.Second, + } + c.Assert(opts.Validate(), IsNil) + } + // KDF type and time are optional + opts = &device.VolumesAuthOptions{Mode: device.AuthModePassphrase, Passphrase: "1234"} + c.Assert(opts.Validate(), IsNil) +} + +func (s *deviceSuite) TestVolumesAuthOptionsValidateError(c *C) { + // Bad auth mode + opts := &device.VolumesAuthOptions{Mode: "bad-mode", Passphrase: "1234"} + c.Assert(opts.Validate(), ErrorMatches, `invalid authentication mode "bad-mode", only "passphrase" and "pin" modes are supported`) + // Empty passphrase + opts = &device.VolumesAuthOptions{Mode: device.AuthModePassphrase} + c.Assert(opts.Validate(), ErrorMatches, "passphrase cannot be empty") + // PIN mode not implemented yet + opts = &device.VolumesAuthOptions{Mode: device.AuthModePIN} + c.Assert(opts.Validate(), ErrorMatches, `"pin" authentication mode is not implemented`) + // PIN mode + custom kdf type + opts = &device.VolumesAuthOptions{Mode: device.AuthModePIN, KDFType: "argon2i"} + c.Assert(opts.Validate(), ErrorMatches, `"pin" authentication mode does not support custom kdf types`) + // Bad kdf type + opts = &device.VolumesAuthOptions{Mode: device.AuthModePassphrase, Passphrase: "1234", KDFType: "bad-type"} + c.Assert(opts.Validate(), ErrorMatches, `invalid kdf type "bad-type", only "argon2i", "argon2id" and "pbkdf2" are supported`) + // Negative kdf time + opts = &device.VolumesAuthOptions{Mode: device.AuthModePassphrase, Passphrase: "1234", KDFTime: -1} + c.Assert(opts.Validate(), ErrorMatches, "kdf time cannot be negative") +} diff --git a/gadget/install/install.go b/gadget/install/install.go index 3073457168c..6b249674b60 100644 --- a/gadget/install/install.go +++ b/gadget/install/install.go @@ -636,12 +636,12 @@ func SaveStorageTraits(model gadget.Model, vols map[string]*gadget.Volume, encry } func EncryptPartitions( - onVolumes map[string]*gadget.Volume, - encryptionType device.EncryptionType, - model *asserts.Model, - gadgetRoot, kernelRoot string, - perfTimings timings.Measurer, + onVolumes map[string]*gadget.Volume, volumesAuth *device.VolumesAuthOptions, + encryptionType device.EncryptionType, model *asserts.Model, + gadgetRoot, kernelRoot string, perfTimings timings.Measurer, ) (*EncryptionSetupData, error) { + // TODO: Attach passed volumes auth options to encryption setup data. + setupData := &EncryptionSetupData{ parts: make(map[string]partEncryptionData), } diff --git a/gadget/install/install_dummy.go b/gadget/install/install_dummy.go index cbc00da2148..e8c44b4c111 100644 --- a/gadget/install/install_dummy.go +++ b/gadget/install/install_dummy.go @@ -50,8 +50,11 @@ func SaveStorageTraits(model gadget.Model, vols map[string]*gadget.Volume, encry return fmt.Errorf("build without secboot support") } -func EncryptPartitions(onVolumes map[string]*gadget.Volume, encryptionType device.EncryptionType, model *asserts.Model, gadgetRoot, kernelRoot string, - perfTimings timings.Measurer) (*EncryptionSetupData, error) { +func EncryptPartitions( + onVolumes map[string]*gadget.Volume, volumesAuth *device.VolumesAuthOptions, + encryptionType device.EncryptionType, model *asserts.Model, + gadgetRoot, kernelRoot string, perfTimings timings.Measurer, +) (*EncryptionSetupData, error) { return nil, fmt.Errorf("build without secboot support") } diff --git a/gadget/install/install_test.go b/gadget/install/install_test.go index d2ff915ae54..dc036ba2ed3 100644 --- a/gadget/install/install_test.go +++ b/gadget/install/install_test.go @@ -1122,7 +1122,7 @@ func (s *installSuite) testEncryptPartitions(c *C, opts encryptPartitionsOpts) { return nil })() - encryptSetup, err := install.EncryptPartitions(ginfo.Volumes, opts.encryptType, model, gadgetRoot, "", timings.New(nil)) + encryptSetup, err := install.EncryptPartitions(ginfo.Volumes, nil, opts.encryptType, model, gadgetRoot, "", timings.New(nil)) c.Assert(err, IsNil) c.Assert(encryptSetup, NotNil) err = install.CheckEncryptionSetupData(encryptSetup, map[string]string{ @@ -1151,7 +1151,7 @@ func (s *installSuite) TestInstallEncryptPartitionsNoDeviceSet(c *C) { c.Assert(err, IsNil) defer restore() - encryptSetup, err := install.EncryptPartitions(ginfo.Volumes, device.EncryptionTypeLUKS, model, gadgetRoot, "", timings.New(nil)) + encryptSetup, err := install.EncryptPartitions(ginfo.Volumes, nil, device.EncryptionTypeLUKS, model, gadgetRoot, "", timings.New(nil)) c.Check(err.Error(), Equals, `volume "pc" has no device assigned`) c.Check(encryptSetup, IsNil) diff --git a/overlord/devicestate/devicestate.go b/overlord/devicestate/devicestate.go index aebce48590a..fedc4699d62 100644 --- a/overlord/devicestate/devicestate.go +++ b/overlord/devicestate/devicestate.go @@ -37,6 +37,7 @@ import ( "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/device" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/netutil" @@ -1942,18 +1943,28 @@ func InstallFinish(st *state.State, label string, onVolumes map[string]*gadget.V // InstallSetupStorageEncryption creates a change that will setup the // storage encryption for the install of the given label and // volumes. -func InstallSetupStorageEncryption(st *state.State, label string, onVolumes map[string]*gadget.Volume) (*state.Change, error) { +func InstallSetupStorageEncryption(st *state.State, label string, onVolumes map[string]*gadget.Volume, volumesAuth *device.VolumesAuthOptions) (*state.Change, error) { if label == "" { return nil, fmt.Errorf("cannot setup storage encryption with an empty system label") } if onVolumes == nil { return nil, fmt.Errorf("cannot setup storage encryption without volumes data") } + if volumesAuth != nil { + if err := volumesAuth.Validate(); err != nil { + return nil, err + } + // Auth data must be in memory to avoid leaking credentials. + st.Cache(volumesAuthOptionsKey{label}, volumesAuth) + } chg := st.NewChange("install-step-setup-storage-encryption", fmt.Sprintf("Setup storage encryption for installing system %q", label)) setupStorageEncryptionTask := st.NewTask("install-setup-storage-encryption", fmt.Sprintf("Setup storage encryption for installing system %q", label)) setupStorageEncryptionTask.Set("system-label", label) setupStorageEncryptionTask.Set("on-volumes", onVolumes) + if volumesAuth != nil { + setupStorageEncryptionTask.Set("volumes-auth-required", true) + } chg.AddTask(setupStorageEncryptionTask) return chg, nil diff --git a/overlord/devicestate/devicestate_install_api_test.go b/overlord/devicestate/devicestate_install_api_test.go index 8ee71c7ec6c..11a6197b515 100644 --- a/overlord/devicestate/devicestate_install_api_test.go +++ b/overlord/devicestate/devicestate_install_api_test.go @@ -823,10 +823,11 @@ func (s *deviceMgrInstallAPISuite) TestInstallFinishNoLabel(c *C) { - install API finish step \(cannot load assertions for label "classic": no seed assertions\)`) } -func (s *deviceMgrInstallAPISuite) testInstallSetupStorageEncryption(c *C, hasTPM bool) { +func (s *deviceMgrInstallAPISuite) testInstallSetupStorageEncryption(c *C, hasTPM, withVolumesAuth bool) { // Mock label label := "classic" isClassic := true + mockVolumesAuth := &device.VolumesAuthOptions{Mode: device.AuthModePassphrase, Passphrase: "1234"} seedCopyFn := func(seedDir string, opts seed.CopyOptions, tm timings.Measurer) error { return fmt.Errorf("unexpected copy call") } @@ -843,7 +844,7 @@ func (s *deviceMgrInstallAPISuite) testInstallSetupStorageEncryption(c *C, hasTP // Mock encryption of partitions encrytpPartCalls := 0 - restore := devicestate.MockInstallEncryptPartitions(func(onVolumes map[string]*gadget.Volume, encryptionType device.EncryptionType, model *asserts.Model, gadgetRoot, kernelRoot string, perfTimings timings.Measurer) (*install.EncryptionSetupData, error) { + restore := devicestate.MockInstallEncryptPartitions(func(onVolumes map[string]*gadget.Volume, volumesAuth *device.VolumesAuthOptions, encryptionType device.EncryptionType, model *asserts.Model, gadgetRoot, kernelRoot string, perfTimings timings.Measurer) (*install.EncryptionSetupData, error) { encrytpPartCalls++ c.Check(encryptionType, Equals, device.EncryptionTypeLUKS) saveFound := false @@ -856,6 +857,11 @@ func (s *deviceMgrInstallAPISuite) testInstallSetupStorageEncryption(c *C, hasTP dataFound = true } } + if withVolumesAuth { + c.Check(volumesAuth, Equals, mockVolumesAuth) + } else { + c.Check(volumesAuth, IsNil) + } c.Check(saveFound, Equals, true) c.Check(dataFound, Equals, true) return &install.EncryptionSetupData{}, nil @@ -872,6 +878,10 @@ func (s *deviceMgrInstallAPISuite) testInstallSetupStorageEncryption(c *C, hasTP "install API set-up encryption step") encryptTask.Set("system-label", label) encryptTask.Set("on-volumes", ginfo.Volumes) + if withVolumesAuth { + encryptTask.Set("volumes-auth-required", true) + s.state.Cache(devicestate.VolumesAuthOptionsKeyByLabel(label), mockVolumesAuth) + } chg.AddTask(encryptTask) // now let the change run - some checks will happen in the mocked functions @@ -915,11 +925,21 @@ func (s *deviceMgrInstallAPISuite) testInstallSetupStorageEncryption(c *C, hasTP } func (s *deviceMgrInstallAPISuite) TestInstallSetupStorageEncryptionHappy(c *C) { - s.testInstallSetupStorageEncryption(c, true) + const hasTPM = true + const withVolumesAuth = false + s.testInstallSetupStorageEncryption(c, hasTPM, withVolumesAuth) +} + +func (s *deviceMgrInstallAPISuite) TestInstallSetupStorageEncryptionWithVolumesAuth(c *C) { + const hasTPM = true + const withVolumesAuth = true + s.testInstallSetupStorageEncryption(c, hasTPM, withVolumesAuth) } func (s *deviceMgrInstallAPISuite) TestInstallSetupStorageEncryptionNoCrypto(c *C) { - s.testInstallSetupStorageEncryption(c, false) + const hasTPM = false + const withVolumesAuth = false + s.testInstallSetupStorageEncryption(c, hasTPM, withVolumesAuth) } func (s *deviceMgrInstallAPISuite) TestInstallSetupStorageEncryptionNoLabel(c *C) { @@ -956,3 +976,78 @@ func (s *deviceMgrInstallAPISuite) TestInstallSetupStorageEncryptionNoLabel(c *C c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks: - install API set-up encryption step \(cannot load assertions for label "classic": no seed assertions\)`) } + +func (s *deviceMgrInstallAPISuite) TestInstallSetupStorageEncryptionMissingVolumesAuthOptions(c *C) { + // Mock label + label := "classic" + isClassic := true + seedCopyFn := func(seedDir string, opts seed.CopyOptions, tm timings.Measurer) error { + return fmt.Errorf("unexpected copy call") + } + _, _, ginfo, _ := s.mockSystemSeedWithLabel(c, label, isClassic, false, false, seedCopyFn) + + s.state.Lock() + defer s.state.Unlock() + + // Create change + chg := s.state.NewChange("install-step-setup-storage-encryption", + "Setup storage encryption") + encryptTask := s.state.NewTask("install-setup-storage-encryption", + "install API set-up encryption step") + encryptTask.Set("system-label", label) + encryptTask.Set("on-volumes", ginfo.Volumes) + // Set volumes auth as required without corresponding cached options + // mimicing unexpected restart of snapd. + encryptTask.Set("volumes-auth-required", true) + chg.AddTask(encryptTask) + + // now let the change run - some checks will happen in the mocked functions + s.state.Unlock() + defer s.state.Lock() + + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + // Checks now + c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks: +- install API set-up encryption step \(volumes authentication is required but cannot find corresponding cached options\)`) +} + +func (s *deviceMgrInstallAPISuite) TestInstallSetupStorageEncryptionBadVolumesAuthOptionsType(c *C) { + // Mock label + label := "classic" + isClassic := true + seedCopyFn := func(seedDir string, opts seed.CopyOptions, tm timings.Measurer) error { + return fmt.Errorf("unexpected copy call") + } + _, _, ginfo, _ := s.mockSystemSeedWithLabel(c, label, isClassic, false, false, seedCopyFn) + + s.state.Lock() + defer s.state.Unlock() + + // Create change + chg := s.state.NewChange("install-step-setup-storage-encryption", + "Setup storage encryption") + encryptTask := s.state.NewTask("install-setup-storage-encryption", + "install API set-up encryption step") + encryptTask.Set("system-label", label) + encryptTask.Set("on-volumes", ginfo.Volumes) + encryptTask.Set("volumes-auth-required", true) + s.state.Cache(devicestate.VolumesAuthOptionsKeyByLabel(label), "bad-type") + chg.AddTask(encryptTask) + + // now let the change run - some checks will happen in the mocked functions + s.state.Unlock() + defer s.state.Lock() + + s.settle(c) + + s.state.Lock() + defer s.state.Unlock() + + // Checks now + c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks: +- install API set-up encryption step \(internal error: wrong data type under volumesAuthOptionsKey\)`) +} diff --git a/overlord/devicestate/devicestate_install_mode_test.go b/overlord/devicestate/devicestate_install_mode_test.go index 9fdebb48ffe..1661ea4c984 100644 --- a/overlord/devicestate/devicestate_install_mode_test.go +++ b/overlord/devicestate/devicestate_install_mode_test.go @@ -21,6 +21,7 @@ package devicestate_test import ( "compress/gzip" + "errors" "fmt" "io" "os" @@ -2590,7 +2591,7 @@ func (s *installStepSuite) TestDeviceManagerInstallSetupStorageEncryptionEmptyLa s.state.Lock() defer s.state.Unlock() - chg, err := devicestate.InstallSetupStorageEncryption(s.state, "", mockOnVolumes) + chg, err := devicestate.InstallSetupStorageEncryption(s.state, "", mockOnVolumes, nil) c.Check(err, ErrorMatches, "cannot setup storage encryption with an empty system label") c.Check(chg, IsNil) } @@ -2599,16 +2600,31 @@ func (s *installStepSuite) TestDeviceManagerInstallSetupStorageEncryptionNoVolum s.state.Lock() defer s.state.Unlock() - chg, err := devicestate.InstallSetupStorageEncryption(s.state, "1234", nil) + chg, err := devicestate.InstallSetupStorageEncryption(s.state, "1234", nil, nil) c.Check(err, ErrorMatches, "cannot setup storage encryption without volumes data") c.Check(chg, IsNil) } -func (s *installStepSuite) TestDeviceManagerInstallSetupStorageEncryptionTasksAndChange(c *C) { +func (s *installStepSuite) TestDeviceManagerInstallSetupStorageEncryptionVolumeAuthError(c *C) { s.state.Lock() defer s.state.Unlock() - chg, err := devicestate.InstallSetupStorageEncryption(s.state, "1234", mockOnVolumes) + volumeOpts := &device.VolumesAuthOptions{Mode: "bad-mode", Passphrase: "1234"} + chg, err := devicestate.InstallSetupStorageEncryption(s.state, "1234", mockOnVolumes, volumeOpts) + c.Check(err, ErrorMatches, `invalid authentication mode "bad-mode", only "passphrase" and "pin" modes are supported`) + c.Check(chg, IsNil) +} + +func (s *installStepSuite) testDeviceManagerInstallSetupStorageEncryptionTasksAndChange(c *C, withVolumesAuth bool) { + s.state.Lock() + defer s.state.Unlock() + + var volumesAuth *device.VolumesAuthOptions + if withVolumesAuth { + volumesAuth = &device.VolumesAuthOptions{Mode: device.AuthModePassphrase, Passphrase: "1234"} + } + + chg, err := devicestate.InstallSetupStorageEncryption(s.state, "1234", mockOnVolumes, volumesAuth) c.Assert(err, IsNil) c.Assert(chg, NotNil) c.Check(chg.Summary(), Matches, `Setup storage encryption for installing system "1234"`) @@ -2624,6 +2640,31 @@ func (s *installStepSuite) TestDeviceManagerInstallSetupStorageEncryptionTasksAn err = tskInstallFinish.Get("on-volumes", &onVols) c.Assert(err, IsNil) c.Assert(onVols, DeepEquals, mockOnVolumes) + + var volumesAuthRequired bool + err = tskInstallFinish.Get("volumes-auth-required", &volumesAuthRequired) + cached := s.state.Cached(devicestate.VolumesAuthOptionsKeyByLabel("1234")) + if withVolumesAuth { + c.Assert(err, IsNil) + c.Assert(volumesAuthRequired, Equals, true) + c.Assert(cached, NotNil) + cachedVolumesAuth := cached.(*device.VolumesAuthOptions) + c.Check(cachedVolumesAuth, Equals, volumesAuth) + } else { + c.Assert(errors.Is(err, state.ErrNoState), Equals, true) + c.Assert(volumesAuthRequired, Equals, false) + c.Assert(cached, IsNil) + } +} + +func (s *installStepSuite) TestDeviceManagerInstallSetupStorageEncryptionTasksAndChange(c *C) { + const withVolumesAuth = false + s.testDeviceManagerInstallSetupStorageEncryptionTasksAndChange(c, withVolumesAuth) +} + +func (s *installStepSuite) TestDeviceManagerInstallSetupStorageEncryptionTasksAndChangeWithVolumesAuth(c *C) { + const withVolumesAuth = true + s.testDeviceManagerInstallSetupStorageEncryptionTasksAndChange(c, withVolumesAuth) } // TODO make this test a happy one @@ -2633,7 +2674,7 @@ func (s *installStepSuite) TestDeviceManagerInstallSetupStorageEncryptionRunthro defer st.Unlock() s.state.Set("seeded", true) - chg, err := devicestate.InstallSetupStorageEncryption(s.state, "1234", mockOnVolumes) + chg, err := devicestate.InstallSetupStorageEncryption(s.state, "1234", mockOnVolumes, nil) c.Assert(err, IsNil) st.Unlock() diff --git a/overlord/devicestate/export_test.go b/overlord/devicestate/export_test.go index dad4578ada8..aa6aef0bb78 100644 --- a/overlord/devicestate/export_test.go +++ b/overlord/devicestate/export_test.go @@ -422,7 +422,7 @@ func MockInstallMountVolumes(f func(onVolumes map[string]*gadget.Volume, encSetu } } -func MockInstallEncryptPartitions(f func(onVolumes map[string]*gadget.Volume, encryptionType device.EncryptionType, model *asserts.Model, gadgetRoot, kernelRoot string, perfTimings timings.Measurer) (*install.EncryptionSetupData, error)) (restore func()) { +func MockInstallEncryptPartitions(f func(onVolumes map[string]*gadget.Volume, volumesAuth *device.VolumesAuthOptions, encryptionType device.EncryptionType, model *asserts.Model, gadgetRoot, kernelRoot string, perfTimings timings.Measurer) (*install.EncryptionSetupData, error)) (restore func()) { old := installEncryptPartitions installEncryptPartitions = f return func() { @@ -641,3 +641,7 @@ func MockDisksDMCryptUUIDFromMountPoint(f func(mountpoint string) (string, error disksDMCryptUUIDFromMountPoint = old } } + +func VolumesAuthOptionsKeyByLabel(label string) volumesAuthOptionsKey { + return volumesAuthOptionsKey{label} +} diff --git a/overlord/devicestate/handlers_install.go b/overlord/devicestate/handlers_install.go index 6ffd1609ce8..962874127e4 100644 --- a/overlord/devicestate/handlers_install.go +++ b/overlord/devicestate/handlers_install.go @@ -1156,6 +1156,10 @@ func (m *DeviceManager) doInstallFinish(t *state.Task, _ *tomb.Tomb) error { return nil } +type volumesAuthOptionsKey struct { + systemLabel string +} + func (m *DeviceManager) doInstallSetupStorageEncryption(t *state.Task, _ *tomb.Tomb) error { st := t.State() st.Lock() @@ -1173,6 +1177,22 @@ func (m *DeviceManager) doInstallSetupStorageEncryption(t *state.Task, _ *tomb.T return err } logger.Debugf("install-setup-storage-encryption for %q on %v", systemLabel, onVolumes) + var volumesAuthRequired bool + if err := t.Get("volumes-auth-required", &volumesAuthRequired); err != nil && !errors.Is(err, state.ErrNoState) { + return err + } + var volumesAuth *device.VolumesAuthOptions + if volumesAuthRequired { + cached := st.Cached(volumesAuthOptionsKey{systemLabel}) + if cached == nil { + return errors.New("volumes authentication is required but cannot find corresponding cached options") + } + var ok bool + volumesAuth, ok = cached.(*device.VolumesAuthOptions) + if !ok { + return fmt.Errorf("internal error: wrong data type under volumesAuthOptionsKey") + } + } st.Unlock() systemAndSeeds, mntPtForType, unmount, err := m.loadAndMountSystemLabelSnaps(systemLabel) @@ -1206,9 +1226,9 @@ func (m *DeviceManager) doInstallSetupStorageEncryption(t *state.Task, _ *tomb.T return fmt.Errorf("encryption unavailable on this device: %v", whyStr) } - // TODO:ICE: support secboot.EncryptionTypeLUKSWithICE in the API + // TODO:ICE: support device.EncryptionTypeLUKSWithICE in the API encType := device.EncryptionTypeLUKS - encryptionSetupData, err := installEncryptPartitions(onVolumes, encType, systemAndSeeds.Model, mntPtForType[snap.TypeGadget], mntPtForType[snap.TypeKernel], perfTimings) + encryptionSetupData, err := installEncryptPartitions(onVolumes, volumesAuth, encType, systemAndSeeds.Model, mntPtForType[snap.TypeGadget], mntPtForType[snap.TypeKernel], perfTimings) if err != nil { return err } diff --git a/overlord/install/install.go b/overlord/install/install.go index a03f6a0912d..50fee35deba 100644 --- a/overlord/install/install.go +++ b/overlord/install/install.go @@ -79,6 +79,10 @@ type EncryptionSupportInfo struct { // UnavailbleWarning describes why encryption support is not // available in case it is optional. UnavailableWarning string + + // PassphraseAuthAvailable is set if the passphrase authentication + // is supported. + PassphraseAuthAvailable bool } var ( @@ -155,6 +159,10 @@ func GetEncryptionSupportInfo(model *asserts.Model, tpmMode secboot.TPMProvision // If encryption is available check if the gadget is // compatible with encryption. if res.Available { + // TODO: Set res.PassphraseAuthAvailable when passphrase + // support is implemented. + // TODO: Check that the target system supports passphrase + // authentication (e.g. supported kernel/snapd versions). opts := &gadget.ValidationConstraints{ EncryptedData: true, }