Skip to content

Commit

Permalink
many: update install api to support passphrase authentication (#14845)
Browse files Browse the repository at this point in the history
* many: add skeleton to expose encryption features for target install system

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>

* 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 <zeyad.gouda@canonical.com>

* fixup! secboot: add AuthModePIN

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>

* fixup! client: add pin-auth feature enum

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>

* fixup! daemon: use field initializers for readability

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>

* fixup! o/devicestate: fix test matching old error message

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>

* fixup! daemon: drop default initialization values

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>

* fixup! many: move VolumesAuthOptions under gadget/device

This avoids import snapd/secboot from the client package

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>

* 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 <zeyad.gouda@canonical.com>

* fixup! gadget/install: formatting improvements

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>

---------

Signed-off-by: Zeyad Gouda <zeyad.gouda@canonical.com>
  • Loading branch information
ZeyadYasser authored Dec 20, 2024
1 parent 9d06b3d commit b99bb24
Show file tree
Hide file tree
Showing 16 changed files with 397 additions and 48 deletions.
16 changes: 16 additions & 0 deletions client/systems.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`

Expand Down Expand Up @@ -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 {
Expand Down
19 changes: 17 additions & 2 deletions client/systems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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),
},
})
}
5 changes: 4 additions & 1 deletion daemon/api_systems.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down
80 changes: 58 additions & 22 deletions daemon/api_systems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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,
},
Expand Down Expand Up @@ -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
})
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down
3 changes: 2 additions & 1 deletion daemon/export_api_systems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions gadget/device/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"fmt"
"os"
"path/filepath"
"time"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
Expand Down Expand Up @@ -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
}
42 changes: 42 additions & 0 deletions gadget/device/encrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"os"
"path/filepath"
"testing"
"time"

. "gopkg.in/check.v1"

Expand Down Expand Up @@ -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")
}
10 changes: 5 additions & 5 deletions gadget/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
Expand Down
7 changes: 5 additions & 2 deletions gadget/install/install_dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand Down
Loading

0 comments on commit b99bb24

Please sign in to comment.