diff --git a/bootstrapper/cmd/bootstrapper/run.go b/bootstrapper/cmd/bootstrapper/run.go index f9b133e09..5ffc57ab5 100644 --- a/bootstrapper/cmd/bootstrapper/run.go +++ b/bootstrapper/cmd/bootstrapper/run.go @@ -82,10 +82,11 @@ func run(issuer atls.Issuer, openDevice vtpm.TPMOpenFunc, fileHandler file.Handl func getDiskUUID() (string, error) { disk := diskencryption.New() - if err := disk.Open(); err != nil { + free, err := disk.Open() + if err != nil { return "", err } - defer disk.Close() + defer free() return disk.UUID() } diff --git a/bootstrapper/internal/diskencryption/BUILD.bazel b/bootstrapper/internal/diskencryption/BUILD.bazel index ab28fd387..cb89be0e6 100644 --- a/bootstrapper/internal/diskencryption/BUILD.bazel +++ b/bootstrapper/internal/diskencryption/BUILD.bazel @@ -3,27 +3,16 @@ load("//bazel/go:go_test.bzl", "go_ld_test", "go_test") go_library( name = "diskencryption", - srcs = [ - "diskencryption.go", - "diskencryption_cgo.go", - "diskencryption_cross.go", - ], + srcs = ["diskencryption.go"], importpath = "github.com/edgelesssys/constellation/v2/bootstrapper/internal/diskencryption", target_compatible_with = [ "@platforms//os:linux", ], visibility = ["//bootstrapper:__subpackages__"], - deps = select({ - "@io_bazel_rules_go//go/platform:android": [ - "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", - "@com_github_spf13_afero//:afero", - ], - "@io_bazel_rules_go//go/platform:linux": [ - "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", - "@com_github_spf13_afero//:afero", - ], - "//conditions:default": [], - }), + deps = [ + "//internal/cryptsetup", + "@com_github_spf13_afero//:afero", + ], ) go_test( diff --git a/bootstrapper/internal/diskencryption/diskencryption.go b/bootstrapper/internal/diskencryption/diskencryption.go index 2a498b655..a758cf1ca 100644 --- a/bootstrapper/internal/diskencryption/diskencryption.go +++ b/bootstrapper/internal/diskencryption/diskencryption.go @@ -4,10 +4,68 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ -/* -Package diskencryption handles interaction with a node's state disk. - -This package is not thread safe, since libcryptsetup is not thread safe. -There should only be one instance using this package per process. -*/ +// Package diskencryption handles interaction with a node's state disk. package diskencryption + +import ( + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/cryptsetup" + "github.com/spf13/afero" +) + +const ( + stateMapperDevice = "state" + initialKeyPath = "/run/cryptsetup-keys.d/state.key" + keyslot = 0 +) + +// DiskEncryption manages the encrypted state mapper device. +type DiskEncryption struct { + fs afero.Fs + device cryptdevice +} + +// New creates a new Cryptsetup. +func New() *DiskEncryption { + return &DiskEncryption{ + fs: afero.NewOsFs(), + device: cryptsetup.New(), + } +} + +// Open opens the cryptdevice. +func (c *DiskEncryption) Open() (free func(), err error) { + return c.device.InitByName(stateMapperDevice) +} + +// UUID gets the device's UUID. +// Only works after calling Open(). +func (c *DiskEncryption) UUID() (string, error) { + return c.device.GetUUID() +} + +// UpdatePassphrase switches the initial random passphrase of the mapped crypt device to a permanent passphrase. +// Only works after calling Open(). +func (c *DiskEncryption) UpdatePassphrase(passphrase string) error { + initialPassphrase, err := c.getInitialPassphrase() + if err != nil { + return err + } + return c.device.KeyslotChangeByPassphrase(keyslot, keyslot, initialPassphrase, passphrase) +} + +// getInitialPassphrase retrieves the initial passphrase used on first boot. +func (c *DiskEncryption) getInitialPassphrase() (string, error) { + passphrase, err := afero.ReadFile(c.fs, initialKeyPath) + if err != nil { + return "", fmt.Errorf("reading first boot encryption passphrase from disk: %w", err) + } + return string(passphrase), nil +} + +type cryptdevice interface { + InitByName(name string) (func(), error) + GetUUID() (string, error) + KeyslotChangeByPassphrase(currentKeyslot int, newKeyslot int, currentPassphrase string, newPassphrase string) error +} diff --git a/bootstrapper/internal/diskencryption/diskencryption_cgo.go b/bootstrapper/internal/diskencryption/diskencryption_cgo.go deleted file mode 100644 index 250252bc5..000000000 --- a/bootstrapper/internal/diskencryption/diskencryption_cgo.go +++ /dev/null @@ -1,127 +0,0 @@ -//go:build linux && cgo - -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package diskencryption - -import ( - "errors" - "fmt" - "sync" - - "github.com/martinjungblut/go-cryptsetup" - "github.com/spf13/afero" -) - -const ( - stateMapperDevice = "state" - initialKeyPath = "/run/cryptsetup-keys.d/state.key" - keyslot = 0 -) - -var ( - // packageLock is needed to block concurrent use of package functions, since libcryptsetup is not thread safe. - // See: https://gitlab.com/cryptsetup/cryptsetup/-/issues/710 - // https://stackoverflow.com/questions/30553386/cryptsetup-backend-safe-with-multithreading - packageLock = sync.Mutex{} - errDeviceNotOpen = errors.New("cryptdevice not open") - errDeviceAlreadyOpen = errors.New("cryptdevice already open") -) - -// Cryptsetup manages the encrypted state mapper device. -type Cryptsetup struct { - fs afero.Fs - device cryptdevice - initByName initByName -} - -// New creates a new Cryptsetup. -func New() *Cryptsetup { - return &Cryptsetup{ - fs: afero.NewOsFs(), - initByName: func(name string) (cryptdevice, error) { - return cryptsetup.InitByName(name) - }, - } -} - -// Open opens the cryptdevice. -func (c *Cryptsetup) Open() error { - packageLock.Lock() - defer packageLock.Unlock() - if c.device != nil { - return errDeviceAlreadyOpen - } - var err error - c.device, err = c.initByName(stateMapperDevice) - if err != nil { - return fmt.Errorf("initializing crypt device for mapped device %q: %w", stateMapperDevice, err) - } - return nil -} - -// Close closes the cryptdevice. -func (c *Cryptsetup) Close() error { - packageLock.Lock() - defer packageLock.Unlock() - if c.device == nil { - return errDeviceNotOpen - } - c.device.Free() - c.device = nil - return nil -} - -// UUID gets the device's UUID. -// Only works after calling Open(). -func (c *Cryptsetup) UUID() (string, error) { - packageLock.Lock() - defer packageLock.Unlock() - if c.device == nil { - return "", errDeviceNotOpen - } - uuid := c.device.GetUUID() - if uuid == "" { - return "", fmt.Errorf("unable to get UUID for mapped device %q", stateMapperDevice) - } - return uuid, nil -} - -// UpdatePassphrase switches the initial random passphrase of the mapped crypt device to a permanent passphrase. -// Only works after calling Open(). -func (c *Cryptsetup) UpdatePassphrase(passphrase string) error { - packageLock.Lock() - defer packageLock.Unlock() - if c.device == nil { - return errDeviceNotOpen - } - initialPassphrase, err := c.getInitialPassphrase() - if err != nil { - return err - } - if err := c.device.KeyslotChangeByPassphrase(keyslot, keyslot, initialPassphrase, passphrase); err != nil { - return fmt.Errorf("changing passphrase for mapped device %q: %w", stateMapperDevice, err) - } - return nil -} - -// getInitialPassphrase retrieves the initial passphrase used on first boot. -func (c *Cryptsetup) getInitialPassphrase() (string, error) { - passphrase, err := afero.ReadFile(c.fs, initialKeyPath) - if err != nil { - return "", fmt.Errorf("reading first boot encryption passphrase from disk: %w", err) - } - return string(passphrase), nil -} - -type cryptdevice interface { - GetUUID() string - KeyslotChangeByPassphrase(currentKeyslot int, newKeyslot int, currentPassphrase string, newPassphrase string) error - Free() bool -} - -type initByName func(name string) (cryptdevice, error) diff --git a/bootstrapper/internal/diskencryption/diskencryption_cross.go b/bootstrapper/internal/diskencryption/diskencryption_cross.go deleted file mode 100644 index 58f732109..000000000 --- a/bootstrapper/internal/diskencryption/diskencryption_cross.go +++ /dev/null @@ -1,50 +0,0 @@ -//go:build !linux || !cgo - -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -/* -Package diskencryption handles interaction with a node's state disk. - -This package is not thread safe, since libcryptsetup is not thread safe. -There should only be one instance using this package per process. -*/ -package diskencryption - -import "errors" - -// Cryptsetup manages the encrypted state mapper device. -type Cryptsetup struct{} - -// New creates a new Cryptsetup. -// This function panics if CGO is disabled. -func New() *Cryptsetup { - return &Cryptsetup{} -} - -// Open opens the cryptdevice. -// This function does nothing if CGO is disabled. -func (c *Cryptsetup) Open() error { - return errors.New("using cryptsetup requires building with CGO") -} - -// Close closes the cryptdevice. -// This function errors if CGO is disabled. -func (c *Cryptsetup) Close() error { - return errors.New("using cryptsetup requires building with CGO") -} - -// UUID gets the device's UUID. -// This function errors if CGO is disabled. -func (c *Cryptsetup) UUID() (string, error) { - return "", errors.New("using cryptsetup requires building with CGO") -} - -// UpdatePassphrase switches the initial random passphrase of the mapped crypt device to a permanent passphrase. -// This function errors if CGO is disabled. -func (c *Cryptsetup) UpdatePassphrase(_ string) error { - return errors.New("using cryptsetup requires building with CGO") -} diff --git a/bootstrapper/internal/diskencryption/diskencryption_test.go b/bootstrapper/internal/diskencryption/diskencryption_test.go index 29673869a..174e8074f 100644 --- a/bootstrapper/internal/diskencryption/diskencryption_test.go +++ b/bootstrapper/internal/diskencryption/diskencryption_test.go @@ -23,98 +23,6 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } -func TestOpenClose(t *testing.T) { - testCases := map[string]struct { - initByNameErr error - operations []string - wantErr bool - }{ - "open and close work": { - operations: []string{"open", "close"}, - }, - "opening twice fails": { - operations: []string{"open", "open"}, - wantErr: true, - }, - "closing first fails": { - operations: []string{"close"}, - wantErr: true, - }, - "initByName failure detected": { - initByNameErr: errors.New("initByNameErr"), - operations: []string{"open"}, - wantErr: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - crypt := Cryptsetup{ - fs: afero.NewMemMapFs(), - initByName: func(name string) (cryptdevice, error) { - return &stubCryptdevice{}, tc.initByNameErr - }, - } - - err := executeOperations(&crypt, tc.operations) - if tc.wantErr { - assert.Error(err) - return - } - require.NoError(err) - }) - } -} - -func TestUUID(t *testing.T) { - testCases := map[string]struct { - open bool - wantUUID string - wantErr bool - }{ - "getting uuid works": { - open: true, - wantUUID: "uuid", - }, - "getting uuid on closed device fails": { - wantErr: true, - }, - "empty uuid is detected": { - open: true, - wantErr: true, - }, - } - - for name, tc := range testCases { - t.Run(name, func(t *testing.T) { - assert := assert.New(t) - require := require.New(t) - - crypt := Cryptsetup{ - fs: afero.NewMemMapFs(), - initByName: func(name string) (cryptdevice, error) { - return &stubCryptdevice{uuid: tc.wantUUID}, nil - }, - } - - if tc.open { - require.NoError(crypt.Open()) - defer crypt.Close() - } - uuid, err := crypt.UUID() - if tc.wantErr { - assert.Error(err) - return - } - require.NoError(err) - assert.Equal(tc.wantUUID, uuid) - }) - } -} - func TestUpdatePassphrase(t *testing.T) { testCases := map[string]struct { writePassphrase bool @@ -126,9 +34,6 @@ func TestUpdatePassphrase(t *testing.T) { writePassphrase: true, open: true, }, - "updating passphrase on closed device fails": { - wantErr: true, - }, "reading initial passphrase can fail": { open: true, wantErr: true, @@ -152,17 +57,11 @@ func TestUpdatePassphrase(t *testing.T) { require.NoError(afero.WriteFile(fs, initialKeyPath, []byte("key"), 0o777)) } - crypt := Cryptsetup{ - fs: fs, - initByName: func(name string) (cryptdevice, error) { - return &stubCryptdevice{keyslotChangeErr: tc.keyslotChangeByPassphraseErr}, nil - }, + crypt := DiskEncryption{ + fs: fs, + device: &stubCryptdevice{keyslotChangeErr: tc.keyslotChangeByPassphraseErr}, } - if tc.open { - require.NoError(crypt.Open()) - defer crypt.Close() - } err := crypt.UpdatePassphrase("new-key") if tc.wantErr { assert.Error(err) @@ -173,37 +72,24 @@ func TestUpdatePassphrase(t *testing.T) { } } -func executeOperations(crypt *Cryptsetup, operations []string) error { - for _, operation := range operations { - var err error - switch operation { - case "open": - err = crypt.Open() - case "close": - err = crypt.Close() - default: - panic("unknown operation") - } - if err != nil { - return err - } - } - return nil -} - type stubCryptdevice struct { uuid string + uuidErr error keyslotChangeErr error } -func (s *stubCryptdevice) GetUUID() string { - return s.uuid +func (s *stubCryptdevice) InitByName(_ string) (func(), error) { + return func() {}, nil +} + +func (s *stubCryptdevice) GetUUID() (string, error) { + return s.uuid, s.uuidErr } func (s *stubCryptdevice) KeyslotChangeByPassphrase(_, _ int, _, _ string) error { return s.keyslotChangeErr } -func (s *stubCryptdevice) Free() bool { - return false +func (s *stubCryptdevice) Close() error { + return nil } diff --git a/bootstrapper/internal/initserver/initserver.go b/bootstrapper/internal/initserver/initserver.go index e444e3227..1355ecefc 100644 --- a/bootstrapper/internal/initserver/initserver.go +++ b/bootstrapper/internal/initserver/initserver.go @@ -299,10 +299,11 @@ func (s *Server) Stop() { } func (s *Server) setupDisk(ctx context.Context, cloudKms kms.CloudKMS) error { - if err := s.disk.Open(); err != nil { + free, err := s.disk.Open() + if err != nil { return fmt.Errorf("opening encrypted disk: %w", err) } - defer s.disk.Close() + defer free() uuid, err := s.disk.UUID() if err != nil { @@ -353,9 +354,7 @@ type ClusterInitializer interface { type encryptedDisk interface { // Open prepares the underlying device for disk operations. - Open() error - // Close closes the underlying device. - Close() error + Open() (free func(), err error) // UUID gets the device's UUID. UUID() (string, error) // UpdatePassphrase switches the initial random passphrase of the encrypted disk to a permanent passphrase. diff --git a/bootstrapper/internal/initserver/initserver_test.go b/bootstrapper/internal/initserver/initserver_test.go index 461bcd91c..5abde73da 100644 --- a/bootstrapper/internal/initserver/initserver_test.go +++ b/bootstrapper/internal/initserver/initserver_test.go @@ -360,8 +360,8 @@ type fakeDisk struct { wantKey []byte } -func (d *fakeDisk) Open() error { - return nil +func (d *fakeDisk) Open() (func(), error) { + return func() {}, nil } func (d *fakeDisk) Close() error { @@ -381,19 +381,14 @@ func (d *fakeDisk) UpdatePassphrase(passphrase string) error { type stubDisk struct { openErr error - closeErr error uuid string uuidErr error updatePassphraseErr error updatePassphraseCalled bool } -func (d *stubDisk) Open() error { - return d.openErr -} - -func (d *stubDisk) Close() error { - return d.closeErr +func (d *stubDisk) Open() (func(), error) { + return func() {}, d.openErr } func (d *stubDisk) UUID() (string, error) { diff --git a/bootstrapper/internal/joinclient/joinclient.go b/bootstrapper/internal/joinclient/joinclient.go index 08acfb3f7..ef4295710 100644 --- a/bootstrapper/internal/joinclient/joinclient.go +++ b/bootstrapper/internal/joinclient/joinclient.go @@ -340,18 +340,20 @@ func (c *JoinClient) getNodeMetadata() error { } func (c *JoinClient) updateDiskPassphrase(passphrase string) error { - if err := c.disk.Open(); err != nil { + free, err := c.disk.Open() + if err != nil { return fmt.Errorf("opening disk: %w", err) } - defer c.disk.Close() + defer free() return c.disk.UpdatePassphrase(passphrase) } func (c *JoinClient) getDiskUUID() (string, error) { - if err := c.disk.Open(); err != nil { + free, err := c.disk.Open() + if err != nil { return "", fmt.Errorf("opening disk: %w", err) } - defer c.disk.Close() + defer free() return c.disk.UUID() } @@ -427,9 +429,7 @@ type MetadataAPI interface { type encryptedDisk interface { // Open prepares the underlying device for disk operations. - Open() error - // Close closes the underlying device. - Close() error + Open() (func(), error) // UUID gets the device's UUID. UUID() (string, error) // UpdatePassphrase switches the initial random passphrase of the encrypted disk to a permanent passphrase. diff --git a/bootstrapper/internal/joinclient/joinclient_test.go b/bootstrapper/internal/joinclient/joinclient_test.go index faa4b5928..6c850a17b 100644 --- a/bootstrapper/internal/joinclient/joinclient_test.go +++ b/bootstrapper/internal/joinclient/joinclient_test.go @@ -400,19 +400,14 @@ func (j *stubClusterJoiner) JoinCluster(context.Context, *kubeadm.BootstrapToken type stubDisk struct { openErr error - closeErr error uuid string uuidErr error updatePassphraseErr error updatePassphraseCalled bool } -func (d *stubDisk) Open() error { - return d.openErr -} - -func (d *stubDisk) Close() error { - return d.closeErr +func (d *stubDisk) Open() (func(), error) { + return func() {}, d.openErr } func (d *stubDisk) UUID() (string, error) { diff --git a/csi/cryptmapper/BUILD.bazel b/csi/cryptmapper/BUILD.bazel index df32f6bd8..198102322 100644 --- a/csi/cryptmapper/BUILD.bazel +++ b/csi/cryptmapper/BUILD.bazel @@ -13,17 +13,16 @@ go_library( "@platforms//os:linux", ], visibility = ["//visibility:public"], - deps = select({ + deps = [ + "//internal/crypto", + "//internal/cryptsetup", + ] + select({ "@io_bazel_rules_go//go/platform:android": [ - "//internal/crypto", - "//internal/cryptsetup", "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", "@io_k8s_mount_utils//:mount-utils", "@io_k8s_utils//exec", ], "@io_bazel_rules_go//go/platform:linux": [ - "//internal/crypto", - "//internal/cryptsetup", "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", "@io_k8s_mount_utils//:mount-utils", "@io_k8s_utils//exec", @@ -38,19 +37,10 @@ go_test( embed = [":cryptmapper"], # keep tags = ["manual"], - deps = select({ - "@io_bazel_rules_go//go/platform:android": [ - "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", - "@com_github_stretchr_testify//assert", - "@org_uber_go_goleak//:goleak", - ], - "@io_bazel_rules_go//go/platform:linux": [ - "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", - "@com_github_stretchr_testify//assert", - "@org_uber_go_goleak//:goleak", - ], - "//conditions:default": [], - }), + deps = [ + "@com_github_stretchr_testify//assert", + "@org_uber_go_goleak//:goleak", + ], ) go_ld_test( diff --git a/csi/cryptmapper/cryptmapper.go b/csi/cryptmapper/cryptmapper.go index 63660c6ae..fc7c12494 100644 --- a/csi/cryptmapper/cryptmapper.go +++ b/csi/cryptmapper/cryptmapper.go @@ -9,9 +9,254 @@ package cryptmapper import ( "context" + "errors" + "fmt" + "io/fs" + "path/filepath" + "strings" + "time" + + "github.com/edgelesssys/constellation/v2/internal/crypto" + "github.com/edgelesssys/constellation/v2/internal/cryptsetup" ) -// KeyCreator is an interface to create data encryption keys. -type KeyCreator interface { +const ( + // LUKSHeaderSize is the amount of bytes taken up by the header of a LUKS2 partition. + // The header is 16MiB (1048576 Bytes * 16). + LUKSHeaderSize = 16777216 + cryptPrefix = "/dev/mapper/" + integritySuffix = "_dif" + integrityFSSuffix = "-integrity" + keySizeIntegrity = 96 + keySizeCrypt = 64 +) + +// CryptMapper manages dm-crypt volumes. +type CryptMapper struct { + mapper deviceMapper + kms keyCreator + getDiskFormat func(disk string) (string, error) +} + +// New initializes a new CryptMapper with the given kms client and key-encryption-key ID. +// kms is used to fetch data encryption keys for the dm-crypt volumes. +func New(kms keyCreator, mapper deviceMapper) *CryptMapper { + return &CryptMapper{ + mapper: mapper, + kms: kms, + getDiskFormat: getDiskFormat, + } +} + +// CloseCryptDevice closes the crypt device mapped for volumeID. +// Returns nil if the volume does not exist. +func (c *CryptMapper) CloseCryptDevice(volumeID string) error { + source, err := filepath.EvalSymlinks(cryptPrefix + volumeID) + if err != nil { + var pathErr *fs.PathError + if errors.As(err, &pathErr) { + return nil + } + return fmt.Errorf("getting device path for disk %q: %w", cryptPrefix+volumeID, err) + } + if err := c.closeCryptDevice(source, volumeID, "crypt"); err != nil { + return fmt.Errorf("closing crypt device: %w", err) + } + + integrity, err := filepath.EvalSymlinks(cryptPrefix + volumeID + integritySuffix) + if err == nil { + // If device was created with integrity, we need to also close the integrity device + integrityErr := c.closeCryptDevice(integrity, volumeID+integritySuffix, "integrity") + if integrityErr != nil { + return integrityErr + } + } + if err != nil { + var pathErr *fs.PathError + if errors.As(err, &pathErr) { + // integrity device does not exist + return nil + } + return fmt.Errorf("getting device path for disk %q: %w", cryptPrefix+volumeID, err) + } + + return nil +} + +// OpenCryptDevice maps the volume at source to the crypt device identified by volumeID. +// The key used to encrypt the volume is fetched using CryptMapper's kms client. +func (c *CryptMapper) OpenCryptDevice(ctx context.Context, source, volumeID string, integrity bool) (string, error) { + // Initialize the block device + free, err := c.mapper.Init(source) + if err != nil { + return "", fmt.Errorf("initializing dm-crypt to map device %q: %w", source, err) + } + defer free() + + var passphrase []byte + // Try to load LUKS headers + // If this fails, the device is either not formatted at all, or already formatted with a different FS + if err := c.mapper.LoadLUKS2(); err != nil { + passphrase, err = c.formatNewDevice(ctx, volumeID, source, integrity) + if err != nil { + return "", fmt.Errorf("formatting device: %w", err) + } + } else { + uuid, err := c.mapper.GetUUID() + if err != nil { + return "", err + } + passphrase, err = c.kms.GetDEK(ctx, uuid, crypto.StateDiskKeyLength) + if err != nil { + return "", err + } + if len(passphrase) != crypto.StateDiskKeyLength { + return "", fmt.Errorf("expected key length to be [%d] but got [%d]", crypto.StateDiskKeyLength, len(passphrase)) + } + } + + if err := c.mapper.ActivateByPassphrase(volumeID, 0, string(passphrase), cryptsetup.ReadWriteQueueBypass); err != nil { + return "", fmt.Errorf("trying to activate dm-crypt volume: %w", err) + } + + return cryptPrefix + volumeID, nil +} + +// ResizeCryptDevice resizes the underlying crypt device and returns the mapped device path. +func (c *CryptMapper) ResizeCryptDevice(ctx context.Context, volumeID string) (string, error) { + free, err := c.mapper.InitByName(volumeID) + if err != nil { + return "", fmt.Errorf("initializing device: %w", err) + } + defer free() + + if err := c.mapper.LoadLUKS2(); err != nil { + return "", fmt.Errorf("loading device: %w", err) + } + + uuid, err := c.mapper.GetUUID() + if err != nil { + return "", err + } + passphrase, err := c.kms.GetDEK(ctx, uuid, crypto.StateDiskKeyLength) + if err != nil { + return "", fmt.Errorf("getting key: %w", err) + } + + if err := c.mapper.ActivateByPassphrase("", 0, string(passphrase), resizeFlags); err != nil { + return "", fmt.Errorf("activating keyring for crypt device %q with passphrase: %w", volumeID, err) + } + + if err := c.mapper.Resize(volumeID, 0); err != nil { + return "", fmt.Errorf("resizing device: %w", err) + } + + return cryptPrefix + volumeID, nil +} + +// GetDevicePath returns the device path of a mapped crypt device. +func (c *CryptMapper) GetDevicePath(volumeID string) (string, error) { + name := strings.TrimPrefix(volumeID, cryptPrefix) + free, err := c.mapper.InitByName(name) + if err != nil { + return "", fmt.Errorf("initializing device: %w", err) + } + defer free() + + deviceName := c.mapper.GetDeviceName() + if deviceName == "" { + return "", errors.New("unable to determine device name") + } + return deviceName, nil +} + +// closeCryptDevice closes the crypt device mapped for volumeID. +func (c *CryptMapper) closeCryptDevice(source, volumeID, deviceType string) error { + free, err := c.mapper.InitByName(volumeID) + if err != nil { + return fmt.Errorf("initializing dm-%s to unmap device %q: %w", deviceType, source, err) + } + defer free() + + if err := c.mapper.Deactivate(volumeID); err != nil { + return fmt.Errorf("deactivating dm-%s volume %q for device %q: %w", deviceType, cryptPrefix+volumeID, source, err) + } + + return nil +} + +func (c *CryptMapper) formatNewDevice(ctx context.Context, volumeID, source string, integrity bool) ([]byte, error) { + format, err := c.getDiskFormat(source) + if err != nil { + return nil, fmt.Errorf("determining if disk is formatted: %w", err) + } + if format != "" { + return nil, fmt.Errorf("disk %q is already formatted as: %s", source, format) + } + + // Device is not formatted, so we can safely create a new LUKS2 partition + if err := c.mapper.Format(integrity); err != nil { + return nil, fmt.Errorf("formatting device %q: %w", source, err) + } + + uuid, err := c.mapper.GetUUID() + if err != nil { + return nil, err + } + passphrase, err := c.kms.GetDEK(ctx, uuid, crypto.StateDiskKeyLength) + if err != nil { + return nil, err + } + if len(passphrase) != crypto.StateDiskKeyLength { + return nil, fmt.Errorf("expected key length to be [%d] but got [%d]", crypto.StateDiskKeyLength, len(passphrase)) + } + + // Add a new keyslot using the internal volume key + if err := c.mapper.KeyslotAddByVolumeKey(0, "", string(passphrase)); err != nil { + return nil, fmt.Errorf("adding keyslot: %w", err) + } + + if integrity { + logProgress := func(size, offset uint64) { + prog := (float64(offset) / float64(size)) * 100 + fmt.Printf("Wipe in progress: %.2f%%\n", prog) + } + + if err := c.mapper.Wipe(volumeID, 1024*1024, 0, logProgress, 30*time.Second); err != nil { + return nil, fmt.Errorf("wiping device: %w", err) + } + } + + return passphrase, nil +} + +// IsIntegrityFS checks if the fstype string contains an integrity suffix. +// If yes, returns the trimmed fstype and true, fstype and false otherwise. +func IsIntegrityFS(fstype string) (string, bool) { + if strings.HasSuffix(fstype, integrityFSSuffix) { + return strings.TrimSuffix(fstype, integrityFSSuffix), true + } + return fstype, false +} + +// deviceMapper is an interface for device mapper methods. +type deviceMapper interface { + Init(devicePath string) (func(), error) + InitByName(name string) (func(), error) + ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error + ActivateByVolumeKey(deviceName string, volumeKey string, volumeKeySize int, flags int) error + Deactivate(deviceName string) error + Format(integrity bool) error + Free() + GetDeviceName() string + GetUUID() (string, error) + LoadLUKS2() error + KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error + Wipe(name string, wipeBlockSize int, flags int, progress func(size, offset uint64), frequency time.Duration) error + Resize(name string, newSize uint64) error +} + +// keyCreator is an interface to create data encryption keys. +type keyCreator interface { GetDEK(ctx context.Context, dekID string, dekSize int) ([]byte, error) } diff --git a/csi/cryptmapper/cryptmapper_cgo.go b/csi/cryptmapper/cryptmapper_cgo.go index 33a4b3dee..f03a48bbb 100644 --- a/csi/cryptmapper/cryptmapper_cgo.go +++ b/csi/cryptmapper/cryptmapper_cgo.go @@ -9,16 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only package cryptmapper import ( - "context" - "errors" "fmt" - "io/fs" - "path/filepath" - "strings" - "sync" - "time" - "github.com/edgelesssys/constellation/v2/internal/crypto" ccryptsetup "github.com/edgelesssys/constellation/v2/internal/cryptsetup" cryptsetup "github.com/martinjungblut/go-cryptsetup" mount "k8s.io/mount-utils" @@ -26,377 +18,15 @@ import ( ) const ( - // LUKSHeaderSize is the amount of bytes taken up by the header of a LUKS2 partition. - // The header is 16MiB (1048576 Bytes * 16). - LUKSHeaderSize = 16777216 - cryptPrefix = "/dev/mapper/" - integritySuffix = "_dif" - integrityFSSuffix = "-integrity" - keySizeIntegrity = 96 - keySizeCrypt = 64 + resizeFlags = cryptsetup.CRYPT_ACTIVATE_KEYRING_KEY | ccryptsetup.ReadWriteQueueBypass ) -// packageLock is needed to block concurrent use of package functions, since libcryptsetup is not thread safe. -// See: https://gitlab.com/cryptsetup/cryptsetup/-/issues/710 -// -// https://stackoverflow.com/questions/30553386/cryptsetup-backend-safe-with-multithreading -var packageLock = sync.Mutex{} - func init() { cryptsetup.SetDebugLevel(cryptsetup.CRYPT_LOG_NORMAL) cryptsetup.SetLogCallback(func(_ int, message string) { fmt.Printf("libcryptsetup: %s\n", message) }) } -// deviceMapper is an interface for device mapper methods. -type deviceMapper interface { - // Init initializes a crypt device backed by 'devicePath'. - // Sets the deviceMapper to the newly allocated Device or returns any error encountered. - Init(devicePath string) error - // InitByName initializes a crypt device from provided active device 'name'. - // Sets the deviceMapper to the newly allocated Device or returns any error encountered. - InitByName(name string) error - // ActivateByPassphrase activates a device by using a passphrase from a specific keyslot. - // Returns nil on success, or an error otherwise. - ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error - // ActivateByVolumeKey activates a device by using a volume key. - // Returns nil on success, or an error otherwise. - ActivateByVolumeKey(deviceName string, volumeKey string, volumeKeySize int, flags int) error - // Deactivate deactivates a device. - // Returns nil on success, or an error otherwise. - Deactivate(deviceName string) error - // Format formats a Device, using a specific device type, and type-independent parameters. - // Returns nil on success, or an error otherwise. - Format(deviceType cryptsetup.DeviceType, genericParams cryptsetup.GenericParams) error - // Free releases crypt device context and used memory. - Free() bool - // GetDeviceName gets the path to the underlying device. - GetDeviceName() string - // GetUUID gets the devices UUID - GetUUID() string - // Load loads crypt device parameters from the on-disk header. - // Returns nil on success, or an error otherwise. - Load(cryptsetup.DeviceType) error - // KeyslotAddByVolumeKey adds a key slot using a volume key to perform the required security check. - // Returns nil on success, or an error otherwise. - KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error - // Wipe removes existing data and clears the device for use with dm-integrity. - // Returns nil on success, or an error otherwise. - Wipe(devicePath string, pattern int, offset, length uint64, wipeBlockSize int, flags int, progress func(size, offset uint64) int) error - // Resize the crypt device. - // Returns nil on success, or an error otherwise. - Resize(name string, newSize uint64) error -} - -// CryptDevice is a wrapper for cryptsetup.Device. -type CryptDevice struct { - *cryptsetup.Device -} - -// Init initializes a crypt device backed by 'devicePath'. -// Sets the cryptDevice's deviceMapper to the newly allocated Device or returns any error encountered. -func (c *CryptDevice) Init(devicePath string) error { - device, err := cryptsetup.Init(devicePath) - if err != nil { - return err - } - c.Device = device - return nil -} - -// InitByName initializes a crypt device from provided active device 'name'. -// Sets the deviceMapper to the newly allocated Device or returns any error encountered. -func (c *CryptDevice) InitByName(name string) error { - device, err := cryptsetup.InitByName(name) - if err != nil { - return err - } - c.Device = device - return nil -} - -// Free releases crypt device context and used memory. -func (c *CryptDevice) Free() bool { - res := c.Device.Free() - c.Device = nil - return res -} - -// CryptMapper manages dm-crypt volumes. -type CryptMapper struct { - mapper deviceMapper - kms KeyCreator -} - -// New initializes a new CryptMapper with the given kms client and key-encryption-key ID. -// kms is used to fetch data encryption keys for the dm-crypt volumes. -func New(kms KeyCreator, mapper deviceMapper) *CryptMapper { - return &CryptMapper{ - mapper: mapper, - kms: kms, - } -} - -// CloseCryptDevice closes the crypt device mapped for volumeID. -// Returns nil if the volume does not exist. -func (c *CryptMapper) CloseCryptDevice(volumeID string) error { - source, err := filepath.EvalSymlinks(cryptPrefix + volumeID) - if err != nil { - var pathErr *fs.PathError - if errors.As(err, &pathErr) { - return nil - } - return fmt.Errorf("getting device path for disk %q: %w", cryptPrefix+volumeID, err) - } - if err := closeCryptDevice(c.mapper, source, volumeID, "crypt"); err != nil { - return fmt.Errorf("closing crypt device: %w", err) - } - - integrity, err := filepath.EvalSymlinks(cryptPrefix + volumeID + integritySuffix) - if err == nil { - // If device was created with integrity, we need to also close the integrity device - integrityErr := closeCryptDevice(c.mapper, integrity, volumeID+integritySuffix, "integrity") - if integrityErr != nil { - return integrityErr - } - } - if err != nil { - var pathErr *fs.PathError - if errors.As(err, &pathErr) { - // integrity device does not exist - return nil - } - return fmt.Errorf("getting device path for disk %q: %w", cryptPrefix+volumeID, err) - } - - return nil -} - -// OpenCryptDevice maps the volume at source to the crypt device identified by volumeID. -// The key used to encrypt the volume is fetched using CryptMapper's kms client. -func (c *CryptMapper) OpenCryptDevice(ctx context.Context, source, volumeID string, integrity bool) (string, error) { - m := &mount.SafeFormatAndMount{Exec: utilexec.New()} - return openCryptDevice(ctx, c.mapper, source, volumeID, integrity, c.kms.GetDEK, m.GetDiskFormat) -} - -// ResizeCryptDevice resizes the underlying crypt device and returns the mapped device path. -func (c *CryptMapper) ResizeCryptDevice(ctx context.Context, volumeID string) (string, error) { - if err := resizeCryptDevice(ctx, c.mapper, volumeID, c.kms.GetDEK); err != nil { - return "", err - } - - return cryptPrefix + volumeID, nil -} - -// GetDevicePath returns the device path of a mapped crypt device. -func (c *CryptMapper) GetDevicePath(volumeID string) (string, error) { - return getDevicePath(c.mapper, strings.TrimPrefix(volumeID, cryptPrefix)) -} - -// closeCryptDevice closes the crypt device mapped for volumeID. -func closeCryptDevice(device deviceMapper, source, volumeID, deviceType string) error { - packageLock.Lock() - defer packageLock.Unlock() - - if err := device.InitByName(volumeID); err != nil { - return fmt.Errorf("initializing dm-%s to unmap device %q: %w", deviceType, source, err) - } - defer device.Free() - - if err := device.Deactivate(volumeID); err != nil { - return fmt.Errorf("deactivating dm-%s volume %q for device %q: %w", deviceType, cryptPrefix+volumeID, source, err) - } - - return nil -} - -// openCryptDevice maps the volume at source to the crypt device identified by volumeID. -func openCryptDevice(ctx context.Context, device deviceMapper, source, volumeID string, integrity bool, - getKey func(ctx context.Context, keyID string, keySize int) ([]byte, error), diskInfo func(disk string) (string, error), -) (string, error) { - packageLock.Lock() - defer packageLock.Unlock() - - var integrityType string - keySize := keySizeCrypt - if integrity { - integrityType = "hmac(sha256)" - keySize = keySizeIntegrity - } - - // Initialize the block device - if err := device.Init(source); err != nil { - return "", fmt.Errorf("initializing dm-crypt to map device %q: %w", source, err) - } - defer device.Free() - - var passphrase []byte - // Try to load LUKS headers - // If this fails, the device is either not formatted at all, or already formatted with a different FS - if err := device.Load(cryptsetup.LUKS2{}); err != nil { - format, err := diskInfo(source) - if err != nil { - return "", fmt.Errorf("determining if disk is formatted: %w", err) - } - if format != "" { - return "", fmt.Errorf("disk %q is already formatted as: %s", source, format) - } - - // Device is not formatted, so we can safely create a new LUKS2 partition - if err := device.Format( - cryptsetup.LUKS2{ - SectorSize: 4096, - Integrity: integrityType, - PBKDFType: &cryptsetup.PbkdfType{ - // Use low memory recommendation from https://datatracker.ietf.org/doc/html/rfc9106#section-7 - Type: "argon2id", - TimeMs: 2000, - Iterations: 3, - ParallelThreads: 4, - MaxMemoryKb: 65536, // ~64MiB - }, - }, - cryptsetup.GenericParams{ - Cipher: "aes", - CipherMode: "xts-plain64", - VolumeKeySize: keySize, - }); err != nil { - return "", fmt.Errorf("formatting device %q: %w", source, err) - } - - uuid := device.GetUUID() - passphrase, err = getKey(ctx, uuid, crypto.StateDiskKeyLength) - if err != nil { - return "", err - } - if len(passphrase) != crypto.StateDiskKeyLength { - return "", fmt.Errorf("expected key length to be [%d] but got [%d]", crypto.StateDiskKeyLength, len(passphrase)) - } - - // Add a new keyslot using the internal volume key - if err := device.KeyslotAddByVolumeKey(0, "", string(passphrase)); err != nil { - return "", fmt.Errorf("adding keyslot: %w", err) - } - - if integrity { - if err := performWipe(device, volumeID); err != nil { - return "", fmt.Errorf("wiping device: %w", err) - } - } - } else { - uuid := device.GetUUID() - passphrase, err = getKey(ctx, uuid, crypto.StateDiskKeyLength) - if err != nil { - return "", err - } - if len(passphrase) != crypto.StateDiskKeyLength { - return "", fmt.Errorf("expected key length to be [%d] but got [%d]", crypto.StateDiskKeyLength, len(passphrase)) - } - } - - if err := device.ActivateByPassphrase(volumeID, 0, string(passphrase), ccryptsetup.ReadWriteQueueBypass); err != nil { - return "", fmt.Errorf("trying to activate dm-crypt volume: %w", err) - } - - return cryptPrefix + volumeID, nil -} - -// performWipe handles setting up parameters and clearing the device for dm-integrity. -func performWipe(device deviceMapper, volumeID string) error { - tmpDevice := "temporary-cryptsetup-" + volumeID - - // Active as temporary device - if err := device.ActivateByVolumeKey(tmpDevice, "", 0, (cryptsetup.CRYPT_ACTIVATE_PRIVATE | cryptsetup.CRYPT_ACTIVATE_NO_JOURNAL)); err != nil { - return fmt.Errorf("trying to activate temporary dm-crypt volume: %w", err) - } - - // No terminal available, limit callbacks to once every 30 seconds to not fill up logs with large amount of progress updates - ticker := time.NewTicker(30 * time.Second) - firstReq := make(chan struct{}, 1) - firstReq <- struct{}{} - defer ticker.Stop() - - logProgress := func(size, offset uint64) { - prog := (float64(offset) / float64(size)) * 100 - fmt.Printf("Wipe in progress: %.2f%%\n", prog) - } - - progressCallback := func(size, offset uint64) int { - select { - case <-firstReq: - logProgress(size, offset) - case <-ticker.C: - logProgress(size, offset) - default: - } - - return 0 - } - - // Wipe the device using the same options as used in cryptsetup: https://gitlab.com/cryptsetup/cryptsetup/-/blob/v2.4.3/src/cryptsetup.c#L1345 - if err := device.Wipe(cryptPrefix+tmpDevice, cryptsetup.CRYPT_WIPE_ZERO, 0, 0, 1024*1024, 0, progressCallback); err != nil { - return err - } - - // Deactivate the temporary device - if err := device.Deactivate(tmpDevice); err != nil { - return fmt.Errorf("deactivating temporary volume: %w", err) - } - - return nil -} - -func resizeCryptDevice(ctx context.Context, device deviceMapper, name string, - getKey func(ctx context.Context, keyID string, keySize int) ([]byte, error), -) error { - packageLock.Lock() - defer packageLock.Unlock() - - if err := device.InitByName(name); err != nil { - return fmt.Errorf("initializing device: %w", err) - } - defer device.Free() - - if err := device.Load(cryptsetup.LUKS2{}); err != nil { - return fmt.Errorf("loading device: %w", err) - } - - passphrase, err := getKey(ctx, device.GetUUID(), crypto.StateDiskKeyLength) - if err != nil { - return fmt.Errorf("getting key: %w", err) - } - - if err := device.ActivateByPassphrase("", 0, string(passphrase), cryptsetup.CRYPT_ACTIVATE_KEYRING_KEY|ccryptsetup.ReadWriteQueueBypass); err != nil { - return fmt.Errorf("activating keyring for crypt device %q with passphrase: %w", name, err) - } - - if err := device.Resize(name, 0); err != nil { - return fmt.Errorf("resizing device: %w", err) - } - - return nil -} - -func getDevicePath(device deviceMapper, name string) (string, error) { - packageLock.Lock() - defer packageLock.Unlock() - - if err := device.InitByName(name); err != nil { - return "", fmt.Errorf("initializing device: %w", err) - } - defer device.Free() - - deviceName := device.GetDeviceName() - if deviceName == "" { - return "", errors.New("unable to determine device name") - } - return deviceName, nil -} - -// IsIntegrityFS checks if the fstype string contains an integrity suffix. -// If yes, returns the trimmed fstype and true, fstype and false otherwise. -func IsIntegrityFS(fstype string) (string, bool) { - if strings.HasSuffix(fstype, integrityFSSuffix) { - return strings.TrimSuffix(fstype, integrityFSSuffix), true - } - return fstype, false +func getDiskFormat(disk string) (string, error) { + mountUtil := &mount.SafeFormatAndMount{Exec: utilexec.New()} + return mountUtil.GetDiskFormat(disk) } diff --git a/csi/cryptmapper/cryptmapper_cross.go b/csi/cryptmapper/cryptmapper_cross.go index fb09990b4..ddc4f4adc 100644 --- a/csi/cryptmapper/cryptmapper_cross.go +++ b/csi/cryptmapper/cryptmapper_cross.go @@ -9,69 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only package cryptmapper import ( - "context" "errors" + + ccryptsetup "github.com/edgelesssys/constellation/v2/internal/cryptsetup" ) -// deviceMapper is an interface for device mapper methods. -type deviceMapper interface{} +const ( + resizeFlags = 0x800 | ccryptsetup.ReadWriteQueueBypass +) -// CryptDevice is a wrapper for cryptsetup.Device. -type CryptDevice struct{} - -// Init initializes a crypt device backed by 'devicePath'. -// This function errors if CGO is disabled. -func (c *CryptDevice) Init(_ string) error { - return errors.New("using cryptmapper requires building with CGO") -} - -// InitByName initializes a crypt device from provided active device 'name'. -// This function panics if CGO is disabled. -func (c *CryptDevice) InitByName(_ string) error { - return errors.New("using cryptmapper requires building with CGO") -} - -// Free releases crypt device context and used memory. -// This function does nothing if CGO is disabled. -func (c *CryptDevice) Free() bool { - return false -} - -// CryptMapper manages dm-crypt volumes. -type CryptMapper struct{} - -// New initializes a new CryptMapper with the given kms client and key-encryption-key ID. -// This function panics if CGO is disabled. -func New(_ KeyCreator, _ deviceMapper) *CryptMapper { - panic("CGO is disabled but requested CryptMapper instance") -} - -// CloseCryptDevice closes the crypt device mapped for volumeID. -// This function errors if CGO is disabled. -func (c *CryptMapper) CloseCryptDevice(_ string) error { - return errors.New("using cryptmapper requires building with CGO") -} - -// OpenCryptDevice maps the volume at source to the crypt device identified by volumeID. -// This function errors if CGO is disabled. -func (c *CryptMapper) OpenCryptDevice(_ context.Context, _, _ string, _ bool) (string, error) { - return "", errors.New("using cryptmapper requires building with CGO") -} - -// ResizeCryptDevice resizes the underlying crypt device and returns the mapped device path. -// This function errors if CGO is disabled. -func (c *CryptMapper) ResizeCryptDevice(_ context.Context, _ string) (string, error) { - return "", errors.New("using cryptmapper requires building with CGO") -} - -// GetDevicePath returns the device path of a mapped crypt device. -// This function errors if CGO is disabled. -func (c *CryptMapper) GetDevicePath(_ string) (string, error) { - return "", errors.New("using cryptmapper requires building with CGO") -} - -// IsIntegrityFS checks if the fstype string contains an integrity suffix. -// This function does nothing if CGO is disabled. -func IsIntegrityFS(_ string) (string, bool) { - return "", false +func getDiskFormat(_ string) (string, error) { + return "", errors.New("getDiskFormat requires building with CGO enabled") } diff --git a/csi/cryptmapper/cryptmapper_test.go b/csi/cryptmapper/cryptmapper_test.go index 0fe9a609c..8283f4671 100644 --- a/csi/cryptmapper/cryptmapper_test.go +++ b/csi/cryptmapper/cryptmapper_test.go @@ -1,5 +1,3 @@ -//go:build linux && cgo - /* Copyright (c) Edgeless Systems GmbH @@ -9,11 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only package cryptmapper import ( + "bytes" "context" "errors" "testing" + "time" - cryptsetup "github.com/martinjungblut/go-cryptsetup" "github.com/stretchr/testify/assert" "go.uber.org/goleak" ) @@ -22,75 +21,6 @@ func TestMain(m *testing.M) { goleak.VerifyTestMain(m) } -type stubCryptDevice struct { - deviceName string - uuid string - initErr error - initByNameErr error - activateErr error - activatePassErr error - deactivateErr error - formatErr error - loadErr error - keySlotAddCalled bool - keySlotAddErr error - wipeErr error - resizeErr error -} - -func (c *stubCryptDevice) Init(string) error { - return c.initErr -} - -func (c *stubCryptDevice) InitByName(string) error { - return c.initByNameErr -} - -func (c *stubCryptDevice) ActivateByVolumeKey(string, string, int, int) error { - return c.activateErr -} - -func (c *stubCryptDevice) ActivateByPassphrase(string, int, string, int) error { - return c.activatePassErr -} - -func (c *stubCryptDevice) Deactivate(string) error { - return c.deactivateErr -} - -func (c *stubCryptDevice) Format(cryptsetup.DeviceType, cryptsetup.GenericParams) error { - return c.formatErr -} - -func (c *stubCryptDevice) Free() bool { - return true -} - -func (c *stubCryptDevice) GetDeviceName() string { - return c.deviceName -} - -func (c *stubCryptDevice) GetUUID() string { - return c.uuid -} - -func (c *stubCryptDevice) Load(cryptsetup.DeviceType) error { - return c.loadErr -} - -func (c *stubCryptDevice) KeyslotAddByVolumeKey(int, string, string) error { - c.keySlotAddCalled = true - return c.keySlotAddErr -} - -func (c *stubCryptDevice) Wipe(string, int, uint64, uint64, int, int, func(size, offset uint64) int) error { - return c.wipeErr -} - -func (c *stubCryptDevice) Resize(string, uint64) error { - return c.resizeErr -} - func TestCloseCryptDevice(t *testing.T) { testCases := map[string]struct { mapper *stubCryptDevice @@ -101,11 +31,11 @@ func TestCloseCryptDevice(t *testing.T) { wantErr: false, }, "error on InitByName": { - mapper: &stubCryptDevice{initByNameErr: errors.New("error")}, + mapper: &stubCryptDevice{initByNameErr: assert.AnError}, wantErr: true, }, "error on Deactivate": { - mapper: &stubCryptDevice{deactivateErr: errors.New("error")}, + mapper: &stubCryptDevice{deactivateErr: assert.AnError}, wantErr: true, }, } @@ -114,7 +44,11 @@ func TestCloseCryptDevice(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - err := closeCryptDevice(tc.mapper, "/dev/some-device", "volume0", "test") + mapper := &CryptMapper{ + kms: &fakeKMS{}, + mapper: tc.mapper, + } + err := mapper.closeCryptDevice("/dev/mapper/volume01", "volume01-unit-test", "crypt") if tc.wantErr { assert.Error(err) } else { @@ -129,22 +63,12 @@ func TestCloseCryptDevice(t *testing.T) { } func TestOpenCryptDevice(t *testing.T) { - someErr := errors.New("error") - getKeyFunc := func(context.Context, string, int) ([]byte, error) { - return []byte{ - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, 0xAA, - }, nil - } - testCases := map[string]struct { source string volumeID string integrity bool mapper *stubCryptDevice - getKey func(context.Context, string, int) ([]byte, error) + kms keyCreator diskInfo func(disk string) (string, error) wantErr bool }{ @@ -152,15 +76,15 @@ func TestOpenCryptDevice(t *testing.T) { source: "/dev/some-device", volumeID: "volume0", mapper: &stubCryptDevice{}, - getKey: getKeyFunc, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: false, }, "success with error on Load": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{loadErr: someErr}, - getKey: getKeyFunc, + mapper: &stubCryptDevice{loadErr: assert.AnError}, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: false, }, @@ -168,48 +92,48 @@ func TestOpenCryptDevice(t *testing.T) { source: "/dev/some-device", volumeID: "volume0", integrity: true, - mapper: &stubCryptDevice{loadErr: someErr}, - getKey: getKeyFunc, + mapper: &stubCryptDevice{loadErr: assert.AnError}, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: false, }, "error on Init": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{initErr: someErr}, - getKey: getKeyFunc, + mapper: &stubCryptDevice{initErr: assert.AnError}, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, "error on Format": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{loadErr: someErr, formatErr: someErr}, - getKey: getKeyFunc, + mapper: &stubCryptDevice{loadErr: assert.AnError, formatErr: assert.AnError}, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, "error on Activate": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{activatePassErr: someErr}, - getKey: getKeyFunc, + mapper: &stubCryptDevice{activatePassErr: assert.AnError}, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, "error on diskInfo": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{loadErr: someErr}, - getKey: getKeyFunc, - diskInfo: func(disk string) (string, error) { return "", someErr }, + mapper: &stubCryptDevice{loadErr: assert.AnError}, + kms: &fakeKMS{}, + diskInfo: func(disk string) (string, error) { return "", assert.AnError }, wantErr: true, }, "disk is already formatted": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{loadErr: someErr}, - getKey: getKeyFunc, + mapper: &stubCryptDevice{loadErr: assert.AnError}, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "ext4", nil }, wantErr: true, }, @@ -217,37 +141,16 @@ func TestOpenCryptDevice(t *testing.T) { source: "/dev/some-device", volumeID: "volume0", integrity: true, - mapper: &stubCryptDevice{loadErr: someErr, wipeErr: someErr}, - getKey: getKeyFunc, - diskInfo: func(disk string) (string, error) { return "", nil }, - wantErr: true, - }, - "error with integrity on activate": { - source: "/dev/some-device", - volumeID: "volume0", - integrity: true, - mapper: &stubCryptDevice{loadErr: someErr, activateErr: someErr}, - getKey: getKeyFunc, - diskInfo: func(disk string) (string, error) { return "", nil }, - wantErr: true, - }, - "error with integrity on deactivate": { - source: "/dev/some-device", - volumeID: "volume0", - integrity: true, - mapper: &stubCryptDevice{loadErr: someErr, deactivateErr: someErr}, - getKey: getKeyFunc, + mapper: &stubCryptDevice{loadErr: assert.AnError, wipeErr: assert.AnError}, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, "error on adding keyslot": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{ - loadErr: someErr, - keySlotAddErr: someErr, - }, - getKey: getKeyFunc, + mapper: &stubCryptDevice{loadErr: assert.AnError, keySlotAddErr: assert.AnError}, + kms: &fakeKMS{}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, @@ -255,15 +158,15 @@ func TestOpenCryptDevice(t *testing.T) { source: "/dev/some-device", volumeID: "volume0", mapper: &stubCryptDevice{}, - getKey: func(ctx context.Context, s string, i int) ([]byte, error) { return []byte{0x1, 0x2, 0x3}, nil }, + kms: &fakeKMS{presetKey: []byte{0x1, 0x2, 0x3}}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, "incorrect key length with error on Load": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{loadErr: someErr}, - getKey: func(ctx context.Context, s string, i int) ([]byte, error) { return []byte{0x1, 0x2, 0x3}, nil }, + mapper: &stubCryptDevice{loadErr: assert.AnError}, + kms: &fakeKMS{presetKey: []byte{0x1, 0x2, 0x3}}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, @@ -271,15 +174,15 @@ func TestOpenCryptDevice(t *testing.T) { source: "/dev/some-device", volumeID: "volume0", mapper: &stubCryptDevice{}, - getKey: func(ctx context.Context, s string, i int) ([]byte, error) { return nil, someErr }, + kms: &fakeKMS{getDEKErr: assert.AnError}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, "getKey fails with error on Load": { source: "/dev/some-device", volumeID: "volume0", - mapper: &stubCryptDevice{loadErr: someErr}, - getKey: func(ctx context.Context, s string, i int) ([]byte, error) { return nil, someErr }, + mapper: &stubCryptDevice{loadErr: assert.AnError}, + kms: &fakeKMS{getDEKErr: assert.AnError}, diskInfo: func(disk string) (string, error) { return "", nil }, wantErr: true, }, @@ -289,15 +192,13 @@ func TestOpenCryptDevice(t *testing.T) { t.Run(name, func(t *testing.T) { assert := assert.New(t) - out, err := openCryptDevice( - context.Background(), - tc.mapper, - tc.source, - tc.volumeID, - tc.integrity, - tc.getKey, - tc.diskInfo, - ) + mapper := &CryptMapper{ + mapper: tc.mapper, + kms: tc.kms, + getDiskFormat: tc.diskInfo, + } + + out, err := mapper.OpenCryptDevice(context.Background(), tc.source, tc.volumeID, tc.integrity) if tc.wantErr { assert.Error(err) } else { @@ -460,12 +361,85 @@ func TestIsIntegrityFS(t *testing.T) { } } -type fakeKMS struct{} +type fakeKMS struct { + presetKey []byte + getDEKErr error +} func (k *fakeKMS) GetDEK(_ context.Context, _ string, dekSize int) ([]byte, error) { - key := make([]byte, dekSize) - for i := range key { - key[i] = 0x41 + if k.getDEKErr != nil { + return nil, k.getDEKErr } - return key, nil + if k.presetKey != nil { + return k.presetKey, nil + } + return bytes.Repeat([]byte{0xAA}, dekSize), nil +} + +type stubCryptDevice struct { + deviceName string + uuid string + uuidErr error + initErr error + initByNameErr error + activateErr error + activatePassErr error + deactivateErr error + formatErr error + loadErr error + keySlotAddCalled bool + keySlotAddErr error + wipeErr error + resizeErr error +} + +func (c *stubCryptDevice) Init(_ string) (func(), error) { + return func() {}, c.initErr +} + +func (c *stubCryptDevice) InitByName(_ string) (func(), error) { + return func() {}, c.initByNameErr +} + +func (c *stubCryptDevice) ActivateByVolumeKey(_, _ string, _, _ int) error { + return c.activateErr +} + +func (c *stubCryptDevice) ActivateByPassphrase(_ string, _ int, _ string, _ int) error { + return c.activatePassErr +} + +func (c *stubCryptDevice) Deactivate(_ string) error { + return c.deactivateErr +} + +func (c *stubCryptDevice) Format(_ bool) error { + return c.formatErr +} + +func (c *stubCryptDevice) Free() {} + +func (c *stubCryptDevice) GetDeviceName() string { + return c.deviceName +} + +func (c *stubCryptDevice) GetUUID() (string, error) { + return c.uuid, c.uuidErr +} + +func (c *stubCryptDevice) LoadLUKS2() error { + return c.loadErr +} + +func (c *stubCryptDevice) KeyslotAddByVolumeKey(_ int, _ string, _ string) error { + c.keySlotAddCalled = true + return c.keySlotAddErr +} + +func (c *stubCryptDevice) Wipe(_ string, _ int, _ int, _ func(size, offset uint64), _ time.Duration) error { + return c.wipeErr +} + +func (c *stubCryptDevice) Resize(_ string, _ uint64) error { + return c.resizeErr } diff --git a/csi/test/mount_integration_test.go b/csi/test/mount_integration_test.go index 469fcf087..ee853f0f8 100644 --- a/csi/test/mount_integration_test.go +++ b/csi/test/mount_integration_test.go @@ -16,22 +16,27 @@ import ( "testing" "github.com/edgelesssys/constellation/v2/csi/cryptmapper" + "github.com/edgelesssys/constellation/v2/internal/cryptsetup" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/goleak" ) const ( - DevicePath string = "testDevice" - DeviceName string = "testDeviceName" + devicePath string = "testDevice" + deviceName string = "testdeviceName" ) func setup() { - _ = exec.Command("/bin/dd", "if=/dev/zero", fmt.Sprintf("of=%s", DevicePath), "bs=64M", "count=1").Run() + if err := exec.Command("/bin/dd", "if=/dev/zero", fmt.Sprintf("of=%s", devicePath), "bs=64M", "count=1").Run(); err != nil { + panic(err) + } } func teardown(devicePath string) { - _ = exec.Command("/bin/rm", "-f", devicePath).Run() + if err := exec.Command("/bin/rm", "-f", devicePath).Run(); err != nil { + panic(err) + } } func cp(source, target string) error { @@ -39,7 +44,9 @@ func cp(source, target string) error { } func resize() { - _ = exec.Command("/bin/dd", "if=/dev/zero", fmt.Sprintf("of=%s", DevicePath), "bs=32M", "count=1", "oflag=append", "conv=notrunc").Run() + if err := exec.Command("/bin/dd", "if=/dev/zero", fmt.Sprintf("of=%s", devicePath), "bs=32M", "count=1", "oflag=append", "conv=notrunc").Run(); err != nil { + panic(err) + } } func TestMain(m *testing.M) { @@ -58,17 +65,19 @@ func TestOpenAndClose(t *testing.T) { assert := assert.New(t) require := require.New(t) setup() - defer teardown(DevicePath) + defer teardown(devicePath) - mapper := cryptmapper.New(&fakeKMS{}, &cryptmapper.CryptDevice{}) + mapper := cryptmapper.New(&fakeKMS{}, cryptsetup.New()) - newPath, err := mapper.OpenCryptDevice(context.Background(), DevicePath, DeviceName, false) + newPath, err := mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, false) require.NoError(err) - assert.Equal("/dev/mapper/"+DeviceName, newPath) + defer func() { + _ = mapper.CloseCryptDevice(deviceName) + }() // assert crypt device got created _, err = os.Stat(newPath) - assert.NoError(err) + require.NoError(err) // assert no integrity device got created _, err = os.Stat(newPath + "_dif") assert.True(os.IsNotExist(err)) @@ -76,33 +85,33 @@ func TestOpenAndClose(t *testing.T) { // Resize the device resize() - resizedPath, err := mapper.ResizeCryptDevice(context.Background(), DeviceName) + resizedPath, err := mapper.ResizeCryptDevice(context.Background(), deviceName) require.NoError(err) - assert.Equal("/dev/mapper/"+DeviceName, resizedPath) + assert.Equal("/dev/mapper/"+deviceName, resizedPath) - assert.NoError(mapper.CloseCryptDevice(DeviceName)) + assert.NoError(mapper.CloseCryptDevice(deviceName)) // assert crypt device got removed _, err = os.Stat(newPath) assert.True(os.IsNotExist(err)) // check if we can reopen the device - _, err = mapper.OpenCryptDevice(context.Background(), DevicePath, DeviceName, true) + _, err = mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, true) assert.NoError(err) - assert.NoError(mapper.CloseCryptDevice(DeviceName)) + assert.NoError(mapper.CloseCryptDevice(deviceName)) } func TestOpenAndCloseIntegrity(t *testing.T) { assert := assert.New(t) require := require.New(t) setup() - defer teardown(DevicePath) + defer teardown(devicePath) - mapper := cryptmapper.New(&fakeKMS{}, &cryptmapper.CryptDevice{}) + mapper := cryptmapper.New(&fakeKMS{}, cryptsetup.New()) - newPath, err := mapper.OpenCryptDevice(context.Background(), DevicePath, DeviceName, true) + newPath, err := mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, true) require.NoError(err) - assert.Equal("/dev/mapper/"+DeviceName, newPath) + assert.Equal("/dev/mapper/"+deviceName, newPath) // assert crypt device got created _, err = os.Stat(newPath) @@ -113,10 +122,10 @@ func TestOpenAndCloseIntegrity(t *testing.T) { // integrity devices do not support resizing resize() - _, err = mapper.ResizeCryptDevice(context.Background(), DeviceName) + _, err = mapper.ResizeCryptDevice(context.Background(), deviceName) assert.Error(err) - assert.NoError(mapper.CloseCryptDevice(DeviceName)) + assert.NoError(mapper.CloseCryptDevice(deviceName)) // assert crypt device got removed _, err = os.Stat(newPath) @@ -126,30 +135,30 @@ func TestOpenAndCloseIntegrity(t *testing.T) { assert.True(os.IsNotExist(err)) // check if we can reopen the device - _, err = mapper.OpenCryptDevice(context.Background(), DevicePath, DeviceName, true) + _, err = mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, true) assert.NoError(err) - assert.NoError(mapper.CloseCryptDevice(DeviceName)) + assert.NoError(mapper.CloseCryptDevice(deviceName)) } func TestDeviceCloning(t *testing.T) { assert := assert.New(t) require := require.New(t) setup() - defer teardown(DevicePath) + defer teardown(devicePath) - mapper := cryptmapper.New(&dynamicKMS{}, &cryptmapper.CryptDevice{}) + mapper := cryptmapper.New(&dynamicKMS{}, cryptsetup.New()) - _, err := mapper.OpenCryptDevice(context.Background(), DevicePath, DeviceName, false) + _, err := mapper.OpenCryptDevice(context.Background(), devicePath, deviceName, false) assert.NoError(err) - require.NoError(cp(DevicePath, DevicePath+"-copy")) - defer teardown(DevicePath + "-copy") + require.NoError(cp(devicePath, devicePath+"-copy")) + defer teardown(devicePath + "-copy") - _, err = mapper.OpenCryptDevice(context.Background(), DevicePath+"-copy", DeviceName+"-copy", false) + _, err = mapper.OpenCryptDevice(context.Background(), devicePath+"-copy", deviceName+"-copy", false) assert.NoError(err) - assert.NoError(mapper.CloseCryptDevice(DeviceName)) - assert.NoError(mapper.CloseCryptDevice(DeviceName + "-copy")) + assert.NoError(mapper.CloseCryptDevice(deviceName)) + assert.NoError(mapper.CloseCryptDevice(deviceName + "-copy")) } type fakeKMS struct{} diff --git a/disk-mapper/cmd/BUILD.bazel b/disk-mapper/cmd/BUILD.bazel index 5be782d77..edac98c40 100644 --- a/disk-mapper/cmd/BUILD.bazel +++ b/disk-mapper/cmd/BUILD.bazel @@ -7,7 +7,7 @@ go_library( importpath = "github.com/edgelesssys/constellation/v2/disk-mapper/cmd", visibility = ["//visibility:private"], deps = [ - "//disk-mapper/internal/mapper", + "//disk-mapper/internal/diskencryption", "//disk-mapper/internal/recoveryserver", "//disk-mapper/internal/rejoinclient", "//disk-mapper/internal/setup", diff --git a/disk-mapper/cmd/main.go b/disk-mapper/cmd/main.go index 5c0cdf270..a061ecfad 100644 --- a/disk-mapper/cmd/main.go +++ b/disk-mapper/cmd/main.go @@ -14,7 +14,7 @@ import ( "os" "path/filepath" - "github.com/edgelesssys/constellation/v2/disk-mapper/internal/mapper" + "github.com/edgelesssys/constellation/v2/disk-mapper/internal/diskencryption" "github.com/edgelesssys/constellation/v2/disk-mapper/internal/recoveryserver" "github.com/edgelesssys/constellation/v2/disk-mapper/internal/rejoinclient" "github.com/edgelesssys/constellation/v2/disk-mapper/internal/setup" @@ -119,11 +119,11 @@ func main() { } // initialize device mapper - mapper, err := mapper.New(diskPath, log) + mapper, free, err := diskencryption.New(diskPath, log) if err != nil { log.With(zap.Error(err)).Fatalf("Failed to initialize device mapper") } - defer mapper.Close() + defer free() // Use TDX if available openDevice := vtpm.OpenVTPM diff --git a/disk-mapper/internal/mapper/BUILD.bazel b/disk-mapper/internal/diskencryption/BUILD.bazel similarity index 85% rename from disk-mapper/internal/mapper/BUILD.bazel rename to disk-mapper/internal/diskencryption/BUILD.bazel index 8618c3bec..83a3c0a76 100644 --- a/disk-mapper/internal/mapper/BUILD.bazel +++ b/disk-mapper/internal/diskencryption/BUILD.bazel @@ -62,3 +62,15 @@ go_library( "//conditions:default": [], }), ) + +go_library( + name = "diskencryption", + srcs = ["diskencryption.go"], + importpath = "github.com/edgelesssys/constellation/v2/disk-mapper/internal/diskencryption", + visibility = ["//disk-mapper:__subpackages__"], + deps = [ + "//internal/cryptsetup", + "//internal/logger", + "@org_uber_go_zap//:zap", + ], +) diff --git a/disk-mapper/internal/diskencryption/diskencryption.go b/disk-mapper/internal/diskencryption/diskencryption.go new file mode 100644 index 000000000..76c82996b --- /dev/null +++ b/disk-mapper/internal/diskencryption/diskencryption.go @@ -0,0 +1,108 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* +Package diskencryption uses libcryptsetup to format and map crypt devices. + +This is used by the disk-mapper to set up a node's state disk. + +All interaction with libcryptsetup should be done here. +*/ +package diskencryption + +import ( + "fmt" + "time" + + "github.com/edgelesssys/constellation/v2/internal/cryptsetup" + "github.com/edgelesssys/constellation/v2/internal/logger" + "go.uber.org/zap" +) + +// DiskEncryption handles actions for formatting and mapping crypt devices. +type DiskEncryption struct { + device cryptDevice + log *logger.Logger +} + +// New creates a new crypt device for the device at path. +func New(path string, log *logger.Logger) (*DiskEncryption, func(), error) { + device := cryptsetup.New() + free, err := device.Init(path) + if err != nil { + return nil, nil, fmt.Errorf("initializing crypt device for disk %q: %w", path, err) + } + return &DiskEncryption{device: device, log: log}, free, nil +} + +// IsLUKSDevice returns true if the device is formatted as a LUKS device. +func (d *DiskEncryption) IsLUKSDevice() bool { + return d.device.LoadLUKS2() == nil +} + +// DiskUUID gets the device's UUID. +func (d *DiskEncryption) DiskUUID() (string, error) { + return d.device.GetUUID() +} + +// FormatDisk formats the disk and adds passphrase in keyslot 0. +func (d *DiskEncryption) FormatDisk(passphrase string) error { + if err := d.device.Format(cryptsetup.FormatIntegrity); err != nil { + return fmt.Errorf("formatting disk: %w", err) + } + + if err := d.device.KeyslotAddByVolumeKey(0, "", passphrase); err != nil { + return fmt.Errorf("adding keyslot: %w", err) + } + + // wipe using 64MiB block size + if err := d.Wipe(67108864); err != nil { + return fmt.Errorf("wiping disk: %w", err) + } + + return nil +} + +// MapDisk maps a crypt device to /dev/mapper/target using the provided passphrase. +func (d *DiskEncryption) MapDisk(target, passphrase string) error { + if err := d.device.ActivateByPassphrase(target, 0, passphrase, cryptsetup.ReadWriteQueueBypass); err != nil { + return fmt.Errorf("mapping disk as %q: %w", target, err) + } + return nil +} + +// UnmapDisk removes the mapping of target. +func (d *DiskEncryption) UnmapDisk(target string) error { + return d.device.Deactivate(target) +} + +// Wipe overwrites the device with zeros to initialize integrity checksums. +func (d *DiskEncryption) Wipe(blockWipeSize int) error { + logProgress := func(size, offset uint64) { + prog := (float64(offset) / float64(size)) * 100 + d.log.With(zap.String("progress", fmt.Sprintf("%.2f%%", prog))).Infof("Wiping disk") + } + + start := time.Now() + // wipe the device + if err := d.device.Wipe("integrity", blockWipeSize, 0, logProgress, 30*time.Second); err != nil { + return fmt.Errorf("wiping disk: %w", err) + } + d.log.With(zap.Duration("duration", time.Since(start))).Infof("Wiping disk successful") + + return nil +} + +type cryptDevice interface { + ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error + ActivateByVolumeKey(deviceName string, volumeKey string, volumeKeySize int, flags int) error + Deactivate(deviceName string) error + Format(integrity bool) error + GetUUID() (string, error) + LoadLUKS2() error + KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error + Wipe(name string, wipeBlockSize int, flags int, logCallback func(size, offset uint64), logFrequency time.Duration) error +} diff --git a/disk-mapper/internal/mapper/cryptdevice.go b/disk-mapper/internal/mapper/cryptdevice.go deleted file mode 100644 index 8c2713670..000000000 --- a/disk-mapper/internal/mapper/cryptdevice.go +++ /dev/null @@ -1,46 +0,0 @@ -//go:build linux && cgo - -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package mapper - -import cryptsetup "github.com/martinjungblut/go-cryptsetup" - -type cryptDevice interface { - // ActivateByPassphrase activates a device by using a passphrase from a specific keyslot. - // Returns nil on success, or an error otherwise. - // C equivalent: crypt_activate_by_passphrase - ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error - // ActivateByVolumeKey activates a device by using a volume key. - // Returns nil on success, or an error otherwise. - ActivateByVolumeKey(deviceName string, volumeKey string, volumeKeySize int, flags int) error - // Deactivate deactivates a device. - // Returns nil on success, or an error otherwise. - // C equivalent: crypt_deactivate - Deactivate(deviceName string) error - // Format formats a Device, using a specific device type, and type-independent parameters. - // Returns nil on success, or an error otherwise. - // C equivalent: crypt_format - Format(deviceType cryptsetup.DeviceType, genericParams cryptsetup.GenericParams) error - // Free releases crypt device context and used memory. - // C equivalent: crypt_free - Free() bool - // GetUUID gets the device's UUID. - // C equivalent: crypt_get_uuid - GetUUID() string - // Load loads crypt device parameters from the on-disk header. - // Returns nil on success, or an error otherwise. - // C equivalent: crypt_load - Load(cryptsetup.DeviceType) error - // KeyslotAddByVolumeKey adds a key slot using a volume key to perform the required security check. - // Returns nil on success, or an error otherwise. - // C equivalent: crypt_keyslot_add_by_volume_key - KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error - // Wipe removes existing data and clears the device for use with dm-integrity. - // Returns nil on success, or an error otherwise. - Wipe(devicePath string, pattern int, offset, length uint64, wipeBlockSize int, flags int, progress func(size, offset uint64) int) error -} diff --git a/disk-mapper/internal/mapper/mapper.go b/disk-mapper/internal/mapper/mapper.go deleted file mode 100644 index 759ca505d..000000000 --- a/disk-mapper/internal/mapper/mapper.go +++ /dev/null @@ -1,16 +0,0 @@ -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -/* -Package mapper uses libcryptsetup to format and map crypt devices. - -This is used by the disk-mapper to set up a node's state disk. - -All interaction with libcryptsetup should be done here. - -Warning: This package is not thread safe, since libcryptsetup is not thread safe. -*/ -package mapper diff --git a/disk-mapper/internal/mapper/mapper_cgo.go b/disk-mapper/internal/mapper/mapper_cgo.go deleted file mode 100644 index f2802f8c6..000000000 --- a/disk-mapper/internal/mapper/mapper_cgo.go +++ /dev/null @@ -1,158 +0,0 @@ -//go:build linux && cgo - -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package mapper - -import ( - "errors" - "fmt" - "strings" - "sync" - "time" - - ccryptsetup "github.com/edgelesssys/constellation/v2/internal/cryptsetup" - "github.com/edgelesssys/constellation/v2/internal/logger" - cryptsetup "github.com/martinjungblut/go-cryptsetup" - "go.uber.org/zap" -) - -// packageLock is needed to block concurrent use of package functions, since libcryptsetup is not thread safe. -// See: https://gitlab.com/cryptsetup/cryptsetup/-/issues/710 -// -// https://stackoverflow.com/questions/30553386/cryptsetup-backend-safe-with-multithreading -var packageLock = sync.Mutex{} - -// Mapper handles actions for formatting and mapping crypt devices. -type Mapper struct { - device cryptDevice - log *logger.Logger -} - -// New creates a new crypt device for the device at path. -func New(path string, log *logger.Logger) (*Mapper, error) { - packageLock.Lock() - device, err := cryptsetup.Init(path) - if err != nil { - return nil, fmt.Errorf("initializing crypt device for disk %q: %w", path, err) - } - return &Mapper{device: device, log: log}, nil -} - -// Close closes and frees memory allocated for the crypt device. -func (m *Mapper) Close() error { - defer packageLock.Unlock() - if m.device.Free() { - return nil - } - return errors.New("unable to close crypt device") -} - -// IsLUKSDevice returns true if the device is formatted as a LUKS device. -func (m *Mapper) IsLUKSDevice() bool { - return m.device.Load(cryptsetup.LUKS2{}) == nil -} - -// DiskUUID gets the device's UUID. -func (m *Mapper) DiskUUID() string { - return strings.ToLower(m.device.GetUUID()) -} - -// FormatDisk formats the disk and adds passphrase in keyslot 0. -func (m *Mapper) FormatDisk(passphrase string) error { - luksParams := cryptsetup.LUKS2{ - SectorSize: 4096, - Integrity: "hmac(sha256)", - PBKDFType: &cryptsetup.PbkdfType{ - // Use low memory recommendation from https://datatracker.ietf.org/doc/html/rfc9106#section-7 - Type: "argon2id", - TimeMs: 2000, - Iterations: 3, - ParallelThreads: 4, - MaxMemoryKb: 65536, // ~64MiB - }, - } - - genericParams := cryptsetup.GenericParams{ - Cipher: "aes", - CipherMode: "xts-plain64", - VolumeKeySize: 96, // 32*2 bytes for aes-xts-plain64 encryption, 32 bytes for hmac(sha256) integrity - } - - if err := m.device.Format(luksParams, genericParams); err != nil { - return fmt.Errorf("formatting disk: %w", err) - } - - if err := m.device.KeyslotAddByVolumeKey(0, "", passphrase); err != nil { - return fmt.Errorf("adding keyslot: %w", err) - } - - // wipe using 64MiB block size - if err := m.Wipe(67108864); err != nil { - return fmt.Errorf("wiping disk: %w", err) - } - - return nil -} - -// MapDisk maps a crypt device to /dev/mapper/target using the provided passphrase. -func (m *Mapper) MapDisk(target, passphrase string) error { - if err := m.device.ActivateByPassphrase(target, 0, passphrase, ccryptsetup.ReadWriteQueueBypass); err != nil { - return fmt.Errorf("mapping disk as %q: %w", target, err) - } - return nil -} - -// UnmapDisk removes the mapping of target. -func (m *Mapper) UnmapDisk(target string) error { - return m.device.Deactivate(target) -} - -// Wipe overwrites the device with zeros to initialize integrity checksums. -func (m *Mapper) Wipe(blockWipeSize int) error { - // Activate as temporary device using the internal volume key - tmpDevice := "tmp-cryptsetup-integrity" - if err := m.device.ActivateByVolumeKey(tmpDevice, "", 0, (cryptsetup.CRYPT_ACTIVATE_PRIVATE | cryptsetup.CRYPT_ACTIVATE_NO_JOURNAL)); err != nil { - return fmt.Errorf("activating as temporary device: %w", err) - } - - // set progress logging callback once every 30 seconds - ticker := time.NewTicker(30 * time.Second) - firstReq := make(chan struct{}, 1) - firstReq <- struct{}{} - defer ticker.Stop() - logProgress := func(size, offset uint64) { - prog := (float64(offset) / float64(size)) * 100 - m.log.With(zap.String("progress", fmt.Sprintf("%.2f%%", prog))).Infof("Wiping disk") - } - - progressCallback := func(size, offset uint64) int { - select { - case <-firstReq: - logProgress(size, offset) - case <-ticker.C: - logProgress(size, offset) - default: - } - - return 0 - } - - start := time.Now() - // wipe the device - if err := m.device.Wipe("/dev/mapper/"+tmpDevice, cryptsetup.CRYPT_WIPE_ZERO, 0, 0, blockWipeSize, 0, progressCallback); err != nil { - return fmt.Errorf("wiping disk: %w", err) - } - m.log.With(zap.Duration("duration", time.Since(start))).Infof("Wiping disk successful") - - // Deactivate the temporary device - if err := m.device.Deactivate(tmpDevice); err != nil { - return fmt.Errorf("deactivating temporary device: %w", err) - } - - return nil -} diff --git a/disk-mapper/internal/mapper/mapper_cross.go b/disk-mapper/internal/mapper/mapper_cross.go deleted file mode 100644 index 8ecdcdcf9..000000000 --- a/disk-mapper/internal/mapper/mapper_cross.go +++ /dev/null @@ -1,66 +0,0 @@ -//go:build !linux || !cgo - -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ - -package mapper - -import ( - "errors" - - "github.com/edgelesssys/constellation/v2/internal/logger" -) - -// Mapper handles actions for formatting and mapping crypt devices. -type Mapper struct{} - -// New creates a new crypt device for the device at path. -// This function errors if CGO is disabled. -func New(_ string, _ *logger.Logger) (*Mapper, error) { - return nil, errors.New("using mapper requires building with CGO") -} - -// Close closes and frees memory allocated for the crypt device. -// This function errors if CGO is disabled. -func (m *Mapper) Close() error { - return errors.New("using mapper requires building with CGO") -} - -// IsLUKSDevice returns true if the device is formatted as a LUKS device. -// This function does nothing if CGO is disabled. -func (m *Mapper) IsLUKSDevice() bool { - return false -} - -// DiskUUID gets the device's UUID. -// This function does nothing if CGO is disabled. -func (m *Mapper) DiskUUID() string { - return "" -} - -// FormatDisk formats the disk and adds passphrase in keyslot 0. -// This function errors if CGO is disabled. -func (m *Mapper) FormatDisk(_ string) error { - return errors.New("using mapper requires building with CGO") -} - -// MapDisk maps a crypt device to /dev/mapper/target using the provided passphrase. -// This function errors if CGO is disabled. -func (m *Mapper) MapDisk(_, _ string) error { - return errors.New("using mapper requires building with CGO") -} - -// UnmapDisk removes the mapping of target. -// This function errors if CGO is disabled. -func (m *Mapper) UnmapDisk(_ string) error { - return errors.New("using mapper requires building with CGO") -} - -// Wipe overwrites the device with zeros to initialize integrity checksums. -// This function errors if CGO is disabled. -func (m *Mapper) Wipe(_ int) error { - return errors.New("using mapper requires building with CGO") -} diff --git a/disk-mapper/internal/setup/interface.go b/disk-mapper/internal/setup/interface.go index e4dde7f76..bcb33ec15 100644 --- a/disk-mapper/internal/setup/interface.go +++ b/disk-mapper/internal/setup/interface.go @@ -22,7 +22,7 @@ type Mounter interface { // DeviceMapper is an interface for device mapping operations. type DeviceMapper interface { - DiskUUID() string + DiskUUID() (string, error) FormatDisk(passphrase string) error MapDisk(target string, passphrase string) error UnmapDisk(target string) error diff --git a/disk-mapper/internal/setup/setup.go b/disk-mapper/internal/setup/setup.go index fffd074a4..312880713 100644 --- a/disk-mapper/internal/setup/setup.go +++ b/disk-mapper/internal/setup/setup.go @@ -78,7 +78,10 @@ func New(log *logger.Logger, csp string, diskPath string, fs afero.Afero, // PrepareExistingDisk requests and waits for a decryption key to remap the encrypted state disk. // Once the disk is mapped, the function taints the node as initialized by updating it's PCRs. func (s *Manager) PrepareExistingDisk(recover RecoveryDoer) error { - uuid := s.mapper.DiskUUID() + uuid, err := s.mapper.DiskUUID() + if err != nil { + return err + } s.log.With(zap.String("uuid", uuid)).Infof("Preparing existing state disk") endpoint := net.JoinHostPort("0.0.0.0", strconv.Itoa(constants.RecoveryPort)) @@ -124,7 +127,8 @@ func (s *Manager) PrepareExistingDisk(recover RecoveryDoer) error { // PrepareNewDisk prepares an instances state disk by formatting the disk as a LUKS device using a random passphrase. func (s *Manager) PrepareNewDisk() error { - s.log.With(zap.String("uuid", s.mapper.DiskUUID())).Infof("Preparing new state disk") + uuid, _ := s.mapper.DiskUUID() + s.log.With(zap.String("uuid", uuid)).Infof("Preparing new state disk") // generate and save temporary passphrase passphrase := make([]byte, crypto.RNGLengthDefault) diff --git a/disk-mapper/internal/setup/setup_test.go b/disk-mapper/internal/setup/setup_test.go index aa2abfa3b..fe9ecd7d3 100644 --- a/disk-mapper/internal/setup/setup_test.go +++ b/disk-mapper/internal/setup/setup_test.go @@ -394,8 +394,8 @@ type stubMapper struct { uuid string } -func (s *stubMapper) DiskUUID() string { - return s.uuid +func (s *stubMapper) DiskUUID() (string, error) { + return s.uuid, nil } func (s *stubMapper) FormatDisk(string) error { diff --git a/disk-mapper/internal/test/benchmark_test.go b/disk-mapper/internal/test/benchmark_test.go index a59db6088..6fc92a284 100644 --- a/disk-mapper/internal/test/benchmark_test.go +++ b/disk-mapper/internal/test/benchmark_test.go @@ -13,7 +13,7 @@ import ( "math" "testing" - "github.com/edgelesssys/constellation/v2/disk-mapper/internal/mapper" + "github.com/edgelesssys/constellation/v2/disk-mapper/internal/diskencryption" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/martinjungblut/go-cryptsetup" "go.uber.org/zap/zapcore" @@ -39,11 +39,11 @@ func BenchmarkMapper(b *testing.B) { } passphrase := "benchmark" - mapper, err := mapper.New(testPath, logger.New(logger.PlainLog, zapcore.InfoLevel)) + mapper, free, err := diskencryption.New(testPath, logger.New(logger.PlainLog, zapcore.InfoLevel)) if err != nil { b.Fatal("Failed to create mapper:", err) } - defer mapper.Close() + defer free() if err := mapper.FormatDisk(passphrase); err != nil { b.Fatal("Failed to format disk:", err) diff --git a/disk-mapper/internal/test/integration_test.go b/disk-mapper/internal/test/integration_test.go index b7f17755c..a0f4a3739 100644 --- a/disk-mapper/internal/test/integration_test.go +++ b/disk-mapper/internal/test/integration_test.go @@ -15,7 +15,7 @@ import ( "os/exec" "testing" - "github.com/edgelesssys/constellation/v2/disk-mapper/internal/mapper" + "github.com/edgelesssys/constellation/v2/disk-mapper/internal/diskencryption" "github.com/edgelesssys/constellation/v2/internal/logger" "github.com/martinjungblut/go-cryptsetup" "github.com/stretchr/testify/assert" @@ -64,9 +64,9 @@ func TestMapper(t *testing.T) { require.NoError(setup(1), "failed to setup test disk") defer func() { require.NoError(teardown(), "failed to delete test disk") }() - mapper, err := mapper.New(devicePath, logger.NewTest(t)) + mapper, free, err := diskencryption.New(devicePath, logger.NewTest(t)) require.NoError(err, "failed to initialize crypt device") - defer func() { require.NoError(mapper.Close(), "failed to close crypt device") }() + defer free() assert.False(mapper.IsLUKSDevice()) diff --git a/internal/cryptsetup/BUILD.bazel b/internal/cryptsetup/BUILD.bazel index 2def0d584..db8a1f465 100644 --- a/internal/cryptsetup/BUILD.bazel +++ b/internal/cryptsetup/BUILD.bazel @@ -3,8 +3,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "cryptsetup", srcs = [ - "crypsetup_cross.go", + "cryptsetup.go", "cryptsetup_cgo.go", + "cryptsetup_cross.go", ], # keep cdeps = [ @@ -13,4 +14,13 @@ go_library( cgo = True, importpath = "github.com/edgelesssys/constellation/v2/internal/cryptsetup", visibility = ["//:__subpackages__"], + deps = select({ + "@io_bazel_rules_go//go/platform:android": [ + "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", + ], + "@io_bazel_rules_go//go/platform:linux": [ + "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", + ], + "//conditions:default": [], + }), ) diff --git a/internal/cryptsetup/crypsetup_cross.go b/internal/cryptsetup/crypsetup_cross.go deleted file mode 100644 index c56d62b8f..000000000 --- a/internal/cryptsetup/crypsetup_cross.go +++ /dev/null @@ -1,15 +0,0 @@ -//go:build !linux || !cgo - -/* -Copyright (c) Edgeless Systems GmbH - -SPDX-License-Identifier: AGPL-3.0-only -*/ -package cryptsetup - -const ( - // ReadWriteQueueBypass is a flag to disable the write and read workqueues for a crypt device. - ReadWriteQueueBypass = cryptActivateNoReadWorkqueue | cryptActivateNoWriteWorkqueue - cryptActivateNoReadWorkqueue = 0x1000000 - cryptActivateNoWriteWorkqueue = 0x2000000 -) diff --git a/internal/cryptsetup/cryptsetup.go b/internal/cryptsetup/cryptsetup.go new file mode 100644 index 000000000..f13c46b5d --- /dev/null +++ b/internal/cryptsetup/cryptsetup.go @@ -0,0 +1,278 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* +Package cryptsetup provides a wrapper around libcryptsetup. +The package is used to manage encrypted disks for Constellation. + +Since libcryptsetup is not thread safe, this package uses a global lock to prevent concurrent use. +There should only be one instance using this package per process. +*/ +package cryptsetup + +import ( + "errors" + "fmt" + "strings" + "sync" + "time" +) + +const ( + // FormatIntegrity is a flag to enable dm-integrity for a crypt device when formatting. + FormatIntegrity = true + // FormatNoIntegrity is a flag to disable dm-integrity for a crypt device when formatting. + FormatNoIntegrity = false + tmpDevicePrefix = "tmp-cryptsetup-" + mappedDevicePath = "/dev/mapper/" +) + +// packageLock is needed to block concurrent use of package functions, since libcryptsetup is not thread safe. +// See: https://gitlab.com/cryptsetup/cryptsetup/-/issues/710 +// +// https://stackoverflow.com/questions/30553386/cryptsetup-backend-safe-with-multithreading +var ( + packageLock = sync.Mutex{} + errDeviceNotOpen = errors.New("crypt device not open") + errDeviceAlreadyOpen = errors.New("crypt device already open") +) + +// CryptSetup manages encrypted devices. +type CryptSetup struct { + nameInit func(name string) (cryptDevice, error) + pathInit func(path string) (cryptDevice, error) + device cryptDevice +} + +// New creates a new CryptSetup. +// Before first use, call Init() or InitByName() to open a crypt device. +func New() *CryptSetup { + return &CryptSetup{ + nameInit: initByName, + pathInit: initByDevicePath, + } +} + +// Init opens a crypt device by device path. +func (c *CryptSetup) Init(devicePath string) (free func(), err error) { + packageLock.Lock() + defer packageLock.Unlock() + if c.device != nil { + return nil, errDeviceAlreadyOpen + } + device, err := c.pathInit(devicePath) + if err != nil { + return nil, fmt.Errorf("init cryptsetup by device path %q: %w", devicePath, err) + } + c.device = device + return c.Free, nil +} + +// InitByName opens an active crypt device using its mapped name. +func (c *CryptSetup) InitByName(name string) (free func(), err error) { + packageLock.Lock() + defer packageLock.Unlock() + if c.device != nil { + return nil, errDeviceAlreadyOpen + } + device, err := c.nameInit(name) + if err != nil { + return nil, fmt.Errorf("init cryptsetup by name %q: %w", name, err) + } + c.device = device + return c.Free, nil +} + +// Free frees resources from a previously opened crypt device. +func (c *CryptSetup) Free() { + packageLock.Lock() + defer packageLock.Unlock() + if c.device != nil { + c.device.Free() + c.device = nil + } +} + +// ActivateByPassphrase actives a crypt device using a passphrase. +func (c *CryptSetup) ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return errDeviceNotOpen + } + if err := c.device.ActivateByPassphrase(deviceName, keyslot, passphrase, flags); err != nil { + return fmt.Errorf("activating crypt device %q using passphrase: %w", deviceName, err) + } + return nil +} + +// ActivateByVolumeKey activates a crypt device using a volume key. +// Set volumeKey to empty string to use the internal key. +func (c *CryptSetup) ActivateByVolumeKey(deviceName, volumeKey string, volumeKeySize, flags int) error { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return errDeviceNotOpen + } + if err := c.device.ActivateByVolumeKey(deviceName, volumeKey, volumeKeySize, flags); err != nil { + return fmt.Errorf("activating crypt device %q using volume key: %w", deviceName, err) + } + return nil +} + +// Deactivate deactivates a crypt device, removing the mapped device. +func (c *CryptSetup) Deactivate(deviceName string) error { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return errDeviceNotOpen + } + if err := c.device.Deactivate(deviceName); err != nil { + return fmt.Errorf("deactivating crypt device %q: %w", deviceName, err) + } + return nil +} + +// Format formats a disk as a LUKS2 crypt device. +// Optionally set integrity to true to enable dm-integrity for the device. +func (c *CryptSetup) Format(integrity bool) error { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return errDeviceNotOpen + } + if err := format(c.device, integrity); err != nil { + return fmt.Errorf("formatting crypt device %q: %w", c.device.GetDeviceName(), err) + } + return nil +} + +// GetDeviceName gets the path to the underlying device. +func (c *CryptSetup) GetDeviceName() string { + return c.device.GetDeviceName() +} + +// GetUUID gets the device's LUKS2 UUID. +// The UUID is returned in lowercase. +func (c *CryptSetup) GetUUID() (string, error) { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return "", errDeviceNotOpen + } + uuid := c.device.GetUUID() + if uuid == "" { + return "", fmt.Errorf("unable to get UUID for device %q", c.device.GetDeviceName()) + } + return strings.ToLower(uuid), nil +} + +// KeyslotAddByVolumeKey adds a key slot to a device, allowing later activations using the chosen passphrase. +// Set volumeKey to empty string to use the internal key. +func (c *CryptSetup) KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return errDeviceNotOpen + } + if err := c.device.KeyslotAddByVolumeKey(keyslot, volumeKey, passphrase); err != nil { + return fmt.Errorf("adding keyslot to device %q: %w", c.device.GetDeviceName(), err) + } + return nil +} + +// KeyslotChangeByPassphrase changes the passphrase for a keyslot. +func (c *CryptSetup) KeyslotChangeByPassphrase(currentKeyslot, newKeyslot int, currentPassphrase, newPassphrase string) error { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return errDeviceNotOpen + } + if err := c.device.KeyslotChangeByPassphrase(currentKeyslot, newKeyslot, currentPassphrase, newPassphrase); err != nil { + return fmt.Errorf("updating passphrase for device %q: %w", c.device.GetDeviceName(), err) + } + return nil +} + +// LoadLUKS2 loads the device as LUKS2 crypt device. +func (c *CryptSetup) LoadLUKS2() error { + if err := loadLUKS2(c.device); err != nil { + return fmt.Errorf("loading LUKS2 crypt device %q: %w", c.device.GetDeviceName(), err) + } + return nil +} + +// Resize resizes a device to the given size. +// name must be equal to the mapped device name. +// Set newSize to 0 to use the maximum available size. +func (c *CryptSetup) Resize(name string, newSize uint64) error { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return errDeviceNotOpen + } + if err := c.device.Resize(name, newSize); err != nil { + return fmt.Errorf("resizing crypt device %q: %w", c.device.GetDeviceName(), err) + } + return nil +} + +// Wipe overwrites the device with zeros to initialize integrity checksums. +func (c *CryptSetup) Wipe( + name string, blockWipeSize int, flags int, logCallback func(size, offset uint64), logFrequency time.Duration, +) (err error) { + packageLock.Lock() + defer packageLock.Unlock() + if c.device == nil { + return errDeviceNotOpen + } + + // Active temporary device to perform wipe on + tmpDevice := tmpDevicePrefix + name + if err := c.device.ActivateByVolumeKey(tmpDevice, "", 0, wipeFlags); err != nil { + return fmt.Errorf("trying to activate temporary dm-crypt volume: %w", err) + } + defer func() { + if deactivateErr := c.device.Deactivate(tmpDevice); deactivateErr != nil { + err = errors.Join(err, fmt.Errorf("deactivating temporary device %q: %w", tmpDevice, deactivateErr)) + } + }() + + // Set up non-blocking progress callback. + ticker := time.NewTicker(logFrequency) + firstReq := make(chan struct{}, 1) + firstReq <- struct{}{} + defer ticker.Stop() + + progressCallback := func(size, offset uint64) int { + select { + case <-firstReq: + logCallback(size, offset) + case <-ticker.C: + logCallback(size, offset) + default: + } + return 0 + } + + if err := c.device.Wipe(mappedDevicePath+tmpDevice, wipePattern, 0, 0, blockWipeSize, flags, progressCallback); err != nil { + return fmt.Errorf("wiping disk of device %q: %w", c.device.GetDeviceName(), err) + } + return nil +} + +type cryptDevice interface { + ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error + ActivateByVolumeKey(deviceName, volumeKey string, volumeKeySize, flags int) error + Deactivate(deviceName string) error + GetDeviceName() string + GetUUID() string + Free() bool + KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error + KeyslotChangeByPassphrase(currentKeyslot, newKeyslot int, currentPassphrase, newPassphrase string) error + Resize(name string, newSize uint64) error + Wipe(devicePath string, pattern int, offset, length uint64, wipeBlockSize int, flags int, progress func(size, offset uint64) int) error +} diff --git a/internal/cryptsetup/cryptsetup_cgo.go b/internal/cryptsetup/cryptsetup_cgo.go index eddb820ea..555e07dfe 100644 --- a/internal/cryptsetup/cryptsetup_cgo.go +++ b/internal/cryptsetup/cryptsetup_cgo.go @@ -5,14 +5,78 @@ Copyright (c) Edgeless Systems GmbH SPDX-License-Identifier: AGPL-3.0-only */ - -// Package cryptsetup contains CGO bindings for cryptsetup. package cryptsetup // #include import "C" +import ( + "errors" + + "github.com/martinjungblut/go-cryptsetup" +) + const ( // ReadWriteQueueBypass is a flag to disable the write and read workqueues for a crypt device. ReadWriteQueueBypass = C.CRYPT_ACTIVATE_NO_WRITE_WORKQUEUE | C.CRYPT_ACTIVATE_NO_READ_WORKQUEUE + wipeFlags = cryptsetup.CRYPT_ACTIVATE_PRIVATE | cryptsetup.CRYPT_ACTIVATE_NO_JOURNAL + wipePattern = cryptsetup.CRYPT_WIPE_ZERO ) + +var errInvalidType = errors.New("device is not a *cryptsetup.Device") + +func format(device cryptDevice, integrity bool) error { + switch d := device.(type) { + case cgoFormatter: + luks2Params := cryptsetup.LUKS2{ + SectorSize: 4096, + PBKDFType: &cryptsetup.PbkdfType{ + // Use low memory recommendation from https://datatracker.ietf.org/doc/html/rfc9106#section-7 + Type: "argon2id", + TimeMs: 2000, + Iterations: 3, + ParallelThreads: 4, + MaxMemoryKb: 65536, // ~64MiB + }, + } + genericParams := cryptsetup.GenericParams{ + Cipher: "aes", + CipherMode: "xts-plain64", + VolumeKeySize: 64, // 32*2 bytes for aes-xts-plain64 encryption + } + + if integrity { + luks2Params.Integrity = "hmac(sha256)" + genericParams.VolumeKeySize += 32 // 32 bytes for hmac(sha256) integrity + } + + return d.Format(luks2Params, genericParams) + default: + return errInvalidType + } +} + +func initByDevicePath(devicePath string) (cryptDevice, error) { + return cryptsetup.Init(devicePath) +} + +func initByName(name string) (cryptDevice, error) { + return cryptsetup.InitByName(name) +} + +func loadLUKS2(device cryptDevice) error { + switch d := device.(type) { + case cgoLoader: + return d.Load(cryptsetup.LUKS2{}) + default: + return errInvalidType + } +} + +type cgoFormatter interface { + Format(deviceType cryptsetup.DeviceType, genericParams cryptsetup.GenericParams) error +} + +type cgoLoader interface { + Load(deviceType cryptsetup.DeviceType) error +} diff --git a/internal/cryptsetup/cryptsetup_cross.go b/internal/cryptsetup/cryptsetup_cross.go new file mode 100644 index 000000000..df1a30790 --- /dev/null +++ b/internal/cryptsetup/cryptsetup_cross.go @@ -0,0 +1,39 @@ +//go:build !linux || !cgo + +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ +package cryptsetup + +import ( + "errors" +) + +const ( + // ReadWriteQueueBypass is a flag to disable the write and read workqueues for a crypt device. + ReadWriteQueueBypass = cryptActivateNoReadWorkqueue | cryptActivateNoWriteWorkqueue + cryptActivateNoReadWorkqueue = 0x1000000 + cryptActivateNoWriteWorkqueue = 0x2000000 + wipeFlags = 0x10 | 0x1000 + wipePattern = 0 +) + +var errCGONotSupported = errors.New("using cryptsetup requires building with CGO") + +func format(_ cryptDevice, _ bool) error { + return errCGONotSupported +} + +func initByDevicePath(_ string) (cryptDevice, error) { + return nil, errCGONotSupported +} + +func initByName(_ string) (cryptDevice, error) { + return nil, errCGONotSupported +} + +func loadLUKS2(_ cryptDevice) error { + return errCGONotSupported +}