Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

many: move secboot.EncryptionType under gadget/device #14855

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions client/systems.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
"golang.org/x/xerrors"

"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/snap"
)

Expand Down Expand Up @@ -162,9 +163,8 @@ type StorageEncryption struct {
// StorageSafety can have values of asserts.StorageSafety
StorageSafety string `json:"storage-safety,omitempty"`

// Type has values of secboot.EncryptionType: "", "cryptsetup",
// "cryptsetup-with-inline-crypto-engine"
Type string `json:"encryption-type,omitempty"`
// Type has values of device.EncryptionType.
Type device.EncryptionType `json:"encryption-type,omitempty"`

// UnavailableReason describes why the encryption is not
// available in a human readable form. Depending on if
Expand Down
2 changes: 1 addition & 1 deletion cmd/snap-bootstrap/cmd_initramfs_mounts.go
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ func doInstall(mst *initramfsMountsState, model *asserts.Model, sysSnaps map[sna
if err != nil {
return err
}
useEncryption := (encryptionSupport != secboot.EncryptionTypeNone)
useEncryption := (encryptionSupport != device.EncryptionTypeNone)

installObserver, trustedInstallObserver, err := installBuildInstallObserver(model, gadgetMountDir, useEncryption)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion daemon/api_systems.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ func storageEncryption(encInfo *install.EncryptionSupportInfo) *client.StorageEn
}
storageEnc := &client.StorageEncryption{
StorageSafety: string(encInfo.StorageSafety),
Type: string(encInfo.Type),
Type: encInfo.Type,
}
required := (encInfo.StorageSafety == asserts.StorageSafetyEncrypted)
switch {
Expand Down
4 changes: 2 additions & 2 deletions daemon/api_systems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import (
"github.com/snapcore/snapd/daemon"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/gadget/quantity"
"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
"github.com/snapcore/snapd/overlord/auth"
Expand All @@ -55,7 +56,6 @@ import (
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/seed"
"github.com/snapcore/snapd/seed/seedtest"
"github.com/snapcore/snapd/snap"
Expand Down Expand Up @@ -819,7 +819,7 @@ func (s *systemsSuite) TestSystemsGetSystemDetailsForLabel(c *check.C) {
for _, tc := range []struct {
disabled, available bool
storageSafety asserts.StorageSafety
typ secboot.EncryptionType
typ device.EncryptionType
unavailableErr, unavailableWarning string

expectedSupport client.StorageEncryptionSupport
Expand Down
14 changes: 14 additions & 0 deletions gadget/device/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,3 +137,17 @@
}
return SealingMethod(content), err
}

// EncryptionType specifies what encryption backend should be used (if any)
type EncryptionType string

const (
EncryptionTypeNone EncryptionType = ""
EncryptionTypeLUKS EncryptionType = "cryptsetup"
EncryptionTypeLUKSWithICE EncryptionType = "cryptsetup-with-inline-crypto-engine"
)

// TODO:ICE: all EncryptionTypes are LUKS based now so this could be removed?
func (et EncryptionType) IsLUKS() bool {
return et == EncryptionTypeLUKS || et == EncryptionTypeLUKSWithICE

Check warning on line 152 in gadget/device/encrypt.go

View check run for this annotation

Codecov / codecov/patch

gadget/device/encrypt.go#L151-L152

Added lines #L151 - L152 were not covered by tests
}
9 changes: 7 additions & 2 deletions gadget/gadget.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,14 @@ import (
"gopkg.in/yaml.v2"

"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/gadget/edition"
"github.com/snapcore/snapd/gadget/quantity"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/metautil"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/osutil/kcmdline"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/snap/snapfile"
Expand Down Expand Up @@ -1725,7 +1725,12 @@ func checkCompatibleSchema(old, new *Volume) error {
// LaidOutVolumesFromGadget takes gadget volumes, gadget and kernel rootdirs
// and lays out the partitions on all volumes as specified. It returns the
// volumes mentioned in the gadget.yaml and their laid out representations.
func LaidOutVolumesFromGadget(vols map[string]*Volume, gadgetRoot, kernelRoot string, encType secboot.EncryptionType, volToGadgetToDiskStruct map[string]map[int]*OnDiskStructure) (all map[string]*LaidOutVolume, err error) {
func LaidOutVolumesFromGadget(
vols map[string]*Volume,
gadgetRoot, kernelRoot string,
encType device.EncryptionType,
volToGadgetToDiskStruct map[string]map[int]*OnDiskStructure,
) (all map[string]*LaidOutVolume, err error) {

all = make(map[string]*LaidOutVolume)
// layout all volumes saving them
Expand Down
14 changes: 7 additions & 7 deletions gadget/gadget_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ import (

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/gadget/gadgettest"
"github.com/snapcore/snapd/gadget/quantity"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/osutil/kcmdline"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snapfile"
"github.com/snapcore/snapd/snap/snaptest"
Expand Down Expand Up @@ -2353,7 +2353,7 @@ func (s *gadgetYamlTestSuite) TestLaidOutVolumesFromGadgetMultiVolume(c *C) {
err = os.WriteFile(filepath.Join(s.dir, "u-boot.imz"), nil, 0644)
c.Assert(err, IsNil)

all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", uc20Mod, secboot.EncryptionTypeNone, nil)
all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", uc20Mod, device.EncryptionTypeNone, nil)
c.Assert(err, IsNil)

c.Assert(all, HasLen, 2)
Expand Down Expand Up @@ -2395,7 +2395,7 @@ func (s *gadgetYamlTestSuite) TestLaidOutVolumesFromGadgetHappy(c *C) {
c.Assert(err, IsNil)
}

all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", coreMod, secboot.EncryptionTypeNone, nil)
all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", coreMod, device.EncryptionTypeNone, nil)
c.Assert(err, IsNil)
c.Assert(all, HasLen, 1)
c.Assert(all["pc"].Volume.Bootloader, Equals, "grub")
Expand All @@ -2419,7 +2419,7 @@ func (s *gadgetYamlTestSuite) TestLaidOutVolumesFromGadgetAndDiskHappy(c *C) {
4: {Name: "ubuntu-save"},
5: {Name: "ubuntu-data"},
}
all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", uc20Mod, secboot.EncryptionTypeNone, nil)
all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", uc20Mod, device.EncryptionTypeNone, nil)
c.Assert(err, IsNil)
c.Assert(all, HasLen, 1)
c.Assert(all["pc"].Volume.Bootloader, Equals, "grub")
Expand All @@ -2446,7 +2446,7 @@ func (s *gadgetYamlTestSuite) TestLaidOutVolumesFromGadgetAndDiskFail(c *C) {
volToGadgetToDiskStruct := map[string]map[int]*gadget.OnDiskStructure{
"pc": gadgetToDiskStruct,
}
all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", uc20Mod, secboot.EncryptionTypeNone, volToGadgetToDiskStruct)
all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", uc20Mod, device.EncryptionTypeNone, volToGadgetToDiskStruct)
c.Assert(err.Error(), Equals, `internal error: partition "ubuntu-seed" not in disk map`)
c.Assert(all, IsNil)
}
Expand All @@ -2459,7 +2459,7 @@ func (s *gadgetYamlTestSuite) testLaidOutVolumesFromGadgetUCHappy(c *C, gadgetYa
c.Assert(err, IsNil)
}

all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", uc20Mod, secboot.EncryptionTypeNone, nil)
all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", uc20Mod, device.EncryptionTypeNone, nil)
c.Assert(err, IsNil)
c.Assert(all, HasLen, 1)
c.Assert(all["pc"].Volume.Bootloader, Equals, "grub")
Expand Down Expand Up @@ -4337,7 +4337,7 @@ func (s *gadgetYamlTestSuite) TestLaidOutVolumesFromClassicWithModesGadgetHappy(
c.Assert(err, IsNil)
}

all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", classicWithModesMod, secboot.EncryptionTypeNone, nil)
all, err := gadgettest.LaidOutVolumesFromGadget(s.dir, "", classicWithModesMod, device.EncryptionTypeNone, nil)
c.Assert(err, IsNil)
c.Assert(all, HasLen, 1)
c.Assert(all["pc"].Volume.Bootloader, Equals, "grub")
Expand Down
13 changes: 9 additions & 4 deletions gadget/gadgettest/gadgettest.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,17 @@ import (
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/gadget/quantity"
"github.com/snapcore/snapd/osutil/disks"
"github.com/snapcore/snapd/secboot"
)

func LaidOutVolumesFromGadget(gadgetRoot, kernelRoot string, model gadget.Model, encType secboot.EncryptionType, volToGadgetToDiskStruct map[string]map[int]*gadget.OnDiskStructure) (all map[string]*gadget.LaidOutVolume, err error) {
func LaidOutVolumesFromGadget(
gadgetRoot, kernelRoot string,
model gadget.Model,
encType device.EncryptionType,
volToGadgetToDiskStruct map[string]map[int]*gadget.OnDiskStructure,
) (all map[string]*gadget.LaidOutVolume, err error) {
// rely on the basic validation from ReadInfo to ensure that the system-*
// roles are all on the same volume for example
info, err := gadget.ReadInfoAndValidate(gadgetRoot, model, nil)
Expand Down Expand Up @@ -63,7 +68,7 @@ func LayoutMultiVolumeFromYaml(newDir, kernelDir, gadgetYaml string, model gadge
return nil, err
}

allVolumes, err := LaidOutVolumesFromGadget(gadgetRoot, kernelDir, model, secboot.EncryptionTypeNone, nil)
allVolumes, err := LaidOutVolumesFromGadget(gadgetRoot, kernelDir, model, device.EncryptionTypeNone, nil)
if err != nil {
return nil, fmt.Errorf("cannot layout volumes: %v", err)
}
Expand Down Expand Up @@ -267,7 +272,7 @@ func MockGadgetPartitionedDisk(gadgetYaml, gadgetRoot string) (ginfo *gadget.Inf
if err != nil {
return nil, nil, nil, nil, err
}
laidVols, err = LaidOutVolumesFromGadget(gadgetRoot, "", model, secboot.EncryptionTypeNone, nil)
laidVols, err = LaidOutVolumesFromGadget(gadgetRoot, "", model, device.EncryptionTypeNone, nil)
if err != nil {
return nil, nil, nil, nil, err
}
Expand Down
3 changes: 2 additions & 1 deletion gadget/install/encrypt.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ package install
import (
"fmt"

"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/secboot"
)

Expand All @@ -48,7 +49,7 @@ var _ = encryptedDevice(&encryptedDeviceLUKS{})

// newEncryptedDeviceLUKS creates an encrypted device in the existing
// partition using the specified key with the LUKS backend.
func newEncryptedDeviceLUKS(devNode string, encType secboot.EncryptionType, key secboot.DiskUnlockKey, label, name string) (encryptedDevice, error) {
func newEncryptedDeviceLUKS(devNode string, encType device.EncryptionType, key secboot.DiskUnlockKey, label, name string) (encryptedDevice, error) {
encLabel := label + "-enc"
if err := secbootFormatEncryptedDevice(key, encType, encLabel, devNode); err != nil {
return nil, fmt.Errorf("cannot format encrypted device: %v", err)
Expand Down
5 changes: 3 additions & 2 deletions gadget/install/encrypt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import (
. "gopkg.in/check.v1"

"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/gadget/install"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/secboot/keys"
Expand Down Expand Up @@ -88,7 +89,7 @@ func (s *encryptSuite) TestNewEncryptedDeviceLUKS(c *C) {
})()

calls := 0
restore := install.MockSecbootFormatEncryptedDevice(func(key []byte, encType secboot.EncryptionType, label, node string) error {
restore := install.MockSecbootFormatEncryptedDevice(func(key []byte, encType device.EncryptionType, label, node string) error {
calls++
c.Assert(key, DeepEquals, []byte(s.mockedEncryptionKey))
c.Assert(label, Equals, "some-label-enc")
Expand All @@ -97,7 +98,7 @@ func (s *encryptSuite) TestNewEncryptedDeviceLUKS(c *C) {
})
defer restore()

dev, err := install.NewEncryptedDeviceLUKS("/dev/node1", secboot.EncryptionTypeLUKS, secboot.DiskUnlockKey(s.mockedEncryptionKey), "some-label", "some-label")
dev, err := install.NewEncryptedDeviceLUKS("/dev/node1", device.EncryptionTypeLUKS, secboot.DiskUnlockKey(s.mockedEncryptionKey), "some-label", "some-label")
c.Assert(calls, Equals, 1)
if tc.expectedErr == "" {
c.Assert(err, IsNil)
Expand Down
3 changes: 2 additions & 1 deletion gadget/install/export_secboot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
package install

import (
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/testutil"
)
Expand All @@ -30,7 +31,7 @@ var (
NewEncryptedDeviceLUKS = newEncryptedDeviceLUKS
)

func MockSecbootFormatEncryptedDevice(f func(key []byte, encType secboot.EncryptionType, label, node string) error) (restore func()) {
func MockSecbootFormatEncryptedDevice(f func(key []byte, encType device.EncryptionType, label, node string) error) (restore func()) {
r := testutil.Backup(&secbootFormatEncryptedDevice)
secbootFormatEncryptedDevice = f
return r
Expand Down
30 changes: 21 additions & 9 deletions gadget/install/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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/gadget/quantity"
"github.com/snapcore/snapd/kernel"
"github.com/snapcore/snapd/logger"
Expand Down Expand Up @@ -99,10 +100,15 @@ func saveStorageTraits(mod gadget.Model, allVols map[string]*gadget.Volume, opts
return nil
}

func maybeEncryptPartition(dgpair *gadget.OnDiskAndGadgetStructurePair, encryptionType secboot.EncryptionType, sectorSize quantity.Size, perfTimings timings.Measurer) (fsParams *mkfsParams, diskEncryptionKey secboot.DiskUnlockKey, err error) {
func maybeEncryptPartition(
dgpair *gadget.OnDiskAndGadgetStructurePair,
encryptionType device.EncryptionType,
sectorSize quantity.Size,
perfTimings timings.Measurer,
) (fsParams *mkfsParams, diskEncryptionKey secboot.DiskUnlockKey, err error) {
diskPart := dgpair.DiskStructure
volStruct := dgpair.GadgetStructure
mustEncrypt := (encryptionType != secboot.EncryptionTypeNone)
mustEncrypt := (encryptionType != device.EncryptionTypeNone)
// fsParams.Device is the kernel device that carries the
// filesystem, which is either the raw /dev/<partition>, or
// the mapped LUKS device if the structure is encrypted (if
Expand Down Expand Up @@ -134,7 +140,7 @@ func maybeEncryptPartition(dgpair *gadget.OnDiskAndGadgetStructurePair, encrypti
logger.Noticef("encrypting partition device %v", diskPart.Node)
var dataPart encryptedDevice
switch encryptionType {
case secboot.EncryptionTypeLUKS, secboot.EncryptionTypeLUKSWithICE:
case device.EncryptionTypeLUKS, device.EncryptionTypeLUKSWithICE:
timings.Run(perfTimings, fmt.Sprintf("new-encrypted-device[%s] (%v)", volStruct.Role, encryptionType),
fmt.Sprintf("Create encryption device for %s (%s)", volStruct.Role, encryptionType),
func(timings.Measurer) {
Expand Down Expand Up @@ -192,7 +198,7 @@ func writePartitionContent(laidOut *gadget.LaidOutStructure, kSnapInfo *KernelSn

func installOnePartition(dgpair *gadget.OnDiskAndGadgetStructurePair,
kernelInfo *kernel.Info, kernelSnapInfo *KernelSnapInfo, gadgetRoot string,
encryptionType secboot.EncryptionType, sectorSize quantity.Size,
encryptionType device.EncryptionType, sectorSize quantity.Size,
observer gadget.ContentObserver, perfTimings timings.Measurer,
) (fsDevice string, encryptionKey secboot.DiskUnlockKey, err error) {
// 1. Encrypt
Expand Down Expand Up @@ -305,9 +311,9 @@ func createPartitions(model gadget.Model, info *gadget.Info, gadgetRoot, kernelR
return bootVolGadgetName, created, bootVolSectorSize, nil
}

func createEncryptionParams(encTyp secboot.EncryptionType) gadget.StructureEncryptionParameters {
func createEncryptionParams(encTyp device.EncryptionType) gadget.StructureEncryptionParameters {
switch encTyp {
case secboot.EncryptionTypeLUKS, secboot.EncryptionTypeLUKSWithICE:
case device.EncryptionTypeLUKS, device.EncryptionTypeLUKSWithICE:
return gadget.StructureEncryptionParameters{
// TODO:ICE: remove "Method" entirely, there is only LUKS
Method: gadget.EncryptionLUKS,
Expand Down Expand Up @@ -629,7 +635,13 @@ func SaveStorageTraits(model gadget.Model, vols map[string]*gadget.Volume, encry
return nil
}

func EncryptPartitions(onVolumes map[string]*gadget.Volume, encryptionType secboot.EncryptionType, model *asserts.Model, gadgetRoot, kernelRoot string, perfTimings timings.Measurer) (*EncryptionSetupData, error) {
func EncryptPartitions(
onVolumes map[string]*gadget.Volume,
encryptionType device.EncryptionType,
model *asserts.Model,
gadgetRoot, kernelRoot string,
perfTimings timings.Measurer,
) (*EncryptionSetupData, error) {
setupData := &EncryptionSetupData{
parts: make(map[string]partEncryptionData),
}
Expand Down Expand Up @@ -721,10 +733,10 @@ func FactoryReset(model gadget.Model, gadgetRoot string, kernelSnapInfo *KernelS
AssumeCreatablePartitionsCreated: true,
ExpectedStructureEncryption: map[string]gadget.StructureEncryptionParameters{},
}
if options.EncryptionType != secboot.EncryptionTypeNone {
if options.EncryptionType != device.EncryptionTypeNone {
var encryptionParam gadget.StructureEncryptionParameters
switch options.EncryptionType {
case secboot.EncryptionTypeLUKS, secboot.EncryptionTypeLUKSWithICE:
case device.EncryptionTypeLUKS, device.EncryptionTypeLUKSWithICE:
encryptionParam = gadget.StructureEncryptionParameters{Method: gadget.EncryptionLUKS}
default:
// XXX what about ICE?
Expand Down
3 changes: 2 additions & 1 deletion gadget/install/install_dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (

"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/gadget/device"
"github.com/snapcore/snapd/secboot"
"github.com/snapcore/snapd/timings"
)
Expand All @@ -49,7 +50,7 @@ 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 secboot.EncryptionType, model *asserts.Model, gadgetRoot, kernelRoot string,
func EncryptPartitions(onVolumes map[string]*gadget.Volume, 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
Loading