cryptsetup: unify code (#2043)

* Add common backend for interacting with cryptsetup

* Use common cryptsetup backend in bootstrapper

* Use common cryptsetup backend in disk-mapper

* Use common cryptsetup backend in csi lib

---------

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2023-07-17 13:55:31 +02:00 committed by GitHub
parent f52c6752e2
commit ac1128d07f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 1061 additions and 1307 deletions

View File

@ -82,10 +82,11 @@ func run(issuer atls.Issuer, openDevice vtpm.TPMOpenFunc, fileHandler file.Handl
func getDiskUUID() (string, error) { func getDiskUUID() (string, error) {
disk := diskencryption.New() disk := diskencryption.New()
if err := disk.Open(); err != nil { free, err := disk.Open()
if err != nil {
return "", err return "", err
} }
defer disk.Close() defer free()
return disk.UUID() return disk.UUID()
} }

View File

@ -3,27 +3,16 @@ load("//bazel/go:go_test.bzl", "go_ld_test", "go_test")
go_library( go_library(
name = "diskencryption", name = "diskencryption",
srcs = [ srcs = ["diskencryption.go"],
"diskencryption.go",
"diskencryption_cgo.go",
"diskencryption_cross.go",
],
importpath = "github.com/edgelesssys/constellation/v2/bootstrapper/internal/diskencryption", importpath = "github.com/edgelesssys/constellation/v2/bootstrapper/internal/diskencryption",
target_compatible_with = [ target_compatible_with = [
"@platforms//os:linux", "@platforms//os:linux",
], ],
visibility = ["//bootstrapper:__subpackages__"], visibility = ["//bootstrapper:__subpackages__"],
deps = select({ deps = [
"@io_bazel_rules_go//go/platform:android": [ "//internal/cryptsetup",
"@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", "@com_github_spf13_afero//:afero",
"@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": [],
}),
) )
go_test( go_test(

View File

@ -4,10 +4,68 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
/* // Package diskencryption handles interaction with a node's state disk.
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 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
}

View File

@ -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)

View File

@ -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")
}

View File

@ -23,98 +23,6 @@ func TestMain(m *testing.M) {
goleak.VerifyTestMain(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) { func TestUpdatePassphrase(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
writePassphrase bool writePassphrase bool
@ -126,9 +34,6 @@ func TestUpdatePassphrase(t *testing.T) {
writePassphrase: true, writePassphrase: true,
open: true, open: true,
}, },
"updating passphrase on closed device fails": {
wantErr: true,
},
"reading initial passphrase can fail": { "reading initial passphrase can fail": {
open: true, open: true,
wantErr: true, wantErr: true,
@ -152,17 +57,11 @@ func TestUpdatePassphrase(t *testing.T) {
require.NoError(afero.WriteFile(fs, initialKeyPath, []byte("key"), 0o777)) require.NoError(afero.WriteFile(fs, initialKeyPath, []byte("key"), 0o777))
} }
crypt := Cryptsetup{ crypt := DiskEncryption{
fs: fs, fs: fs,
initByName: func(name string) (cryptdevice, error) { device: &stubCryptdevice{keyslotChangeErr: tc.keyslotChangeByPassphraseErr},
return &stubCryptdevice{keyslotChangeErr: tc.keyslotChangeByPassphraseErr}, nil
},
} }
if tc.open {
require.NoError(crypt.Open())
defer crypt.Close()
}
err := crypt.UpdatePassphrase("new-key") err := crypt.UpdatePassphrase("new-key")
if tc.wantErr { if tc.wantErr {
assert.Error(err) 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 { type stubCryptdevice struct {
uuid string uuid string
uuidErr error
keyslotChangeErr error keyslotChangeErr error
} }
func (s *stubCryptdevice) GetUUID() string { func (s *stubCryptdevice) InitByName(_ string) (func(), error) {
return s.uuid return func() {}, nil
}
func (s *stubCryptdevice) GetUUID() (string, error) {
return s.uuid, s.uuidErr
} }
func (s *stubCryptdevice) KeyslotChangeByPassphrase(_, _ int, _, _ string) error { func (s *stubCryptdevice) KeyslotChangeByPassphrase(_, _ int, _, _ string) error {
return s.keyslotChangeErr return s.keyslotChangeErr
} }
func (s *stubCryptdevice) Free() bool { func (s *stubCryptdevice) Close() error {
return false return nil
} }

View File

@ -299,10 +299,11 @@ func (s *Server) Stop() {
} }
func (s *Server) setupDisk(ctx context.Context, cloudKms kms.CloudKMS) error { 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) return fmt.Errorf("opening encrypted disk: %w", err)
} }
defer s.disk.Close() defer free()
uuid, err := s.disk.UUID() uuid, err := s.disk.UUID()
if err != nil { if err != nil {
@ -353,9 +354,7 @@ type ClusterInitializer interface {
type encryptedDisk interface { type encryptedDisk interface {
// Open prepares the underlying device for disk operations. // Open prepares the underlying device for disk operations.
Open() error Open() (free func(), err error)
// Close closes the underlying device.
Close() error
// UUID gets the device's UUID. // UUID gets the device's UUID.
UUID() (string, error) UUID() (string, error)
// UpdatePassphrase switches the initial random passphrase of the encrypted disk to a permanent passphrase. // UpdatePassphrase switches the initial random passphrase of the encrypted disk to a permanent passphrase.

View File

@ -360,8 +360,8 @@ type fakeDisk struct {
wantKey []byte wantKey []byte
} }
func (d *fakeDisk) Open() error { func (d *fakeDisk) Open() (func(), error) {
return nil return func() {}, nil
} }
func (d *fakeDisk) Close() error { func (d *fakeDisk) Close() error {
@ -381,19 +381,14 @@ func (d *fakeDisk) UpdatePassphrase(passphrase string) error {
type stubDisk struct { type stubDisk struct {
openErr error openErr error
closeErr error
uuid string uuid string
uuidErr error uuidErr error
updatePassphraseErr error updatePassphraseErr error
updatePassphraseCalled bool updatePassphraseCalled bool
} }
func (d *stubDisk) Open() error { func (d *stubDisk) Open() (func(), error) {
return d.openErr return func() {}, d.openErr
}
func (d *stubDisk) Close() error {
return d.closeErr
} }
func (d *stubDisk) UUID() (string, error) { func (d *stubDisk) UUID() (string, error) {

View File

@ -340,18 +340,20 @@ func (c *JoinClient) getNodeMetadata() error {
} }
func (c *JoinClient) updateDiskPassphrase(passphrase string) 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) return fmt.Errorf("opening disk: %w", err)
} }
defer c.disk.Close() defer free()
return c.disk.UpdatePassphrase(passphrase) return c.disk.UpdatePassphrase(passphrase)
} }
func (c *JoinClient) getDiskUUID() (string, error) { 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) return "", fmt.Errorf("opening disk: %w", err)
} }
defer c.disk.Close() defer free()
return c.disk.UUID() return c.disk.UUID()
} }
@ -427,9 +429,7 @@ type MetadataAPI interface {
type encryptedDisk interface { type encryptedDisk interface {
// Open prepares the underlying device for disk operations. // Open prepares the underlying device for disk operations.
Open() error Open() (func(), error)
// Close closes the underlying device.
Close() error
// UUID gets the device's UUID. // UUID gets the device's UUID.
UUID() (string, error) UUID() (string, error)
// UpdatePassphrase switches the initial random passphrase of the encrypted disk to a permanent passphrase. // UpdatePassphrase switches the initial random passphrase of the encrypted disk to a permanent passphrase.

View File

@ -400,19 +400,14 @@ func (j *stubClusterJoiner) JoinCluster(context.Context, *kubeadm.BootstrapToken
type stubDisk struct { type stubDisk struct {
openErr error openErr error
closeErr error
uuid string uuid string
uuidErr error uuidErr error
updatePassphraseErr error updatePassphraseErr error
updatePassphraseCalled bool updatePassphraseCalled bool
} }
func (d *stubDisk) Open() error { func (d *stubDisk) Open() (func(), error) {
return d.openErr return func() {}, d.openErr
}
func (d *stubDisk) Close() error {
return d.closeErr
} }
func (d *stubDisk) UUID() (string, error) { func (d *stubDisk) UUID() (string, error) {

View File

@ -13,17 +13,16 @@ go_library(
"@platforms//os:linux", "@platforms//os:linux",
], ],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = select({ deps = [
"//internal/crypto",
"//internal/cryptsetup",
] + select({
"@io_bazel_rules_go//go/platform:android": [ "@io_bazel_rules_go//go/platform:android": [
"//internal/crypto",
"//internal/cryptsetup",
"@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup",
"@io_k8s_mount_utils//:mount-utils", "@io_k8s_mount_utils//:mount-utils",
"@io_k8s_utils//exec", "@io_k8s_utils//exec",
], ],
"@io_bazel_rules_go//go/platform:linux": [ "@io_bazel_rules_go//go/platform:linux": [
"//internal/crypto",
"//internal/cryptsetup",
"@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", "@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup",
"@io_k8s_mount_utils//:mount-utils", "@io_k8s_mount_utils//:mount-utils",
"@io_k8s_utils//exec", "@io_k8s_utils//exec",
@ -38,19 +37,10 @@ go_test(
embed = [":cryptmapper"], embed = [":cryptmapper"],
# keep # keep
tags = ["manual"], tags = ["manual"],
deps = select({ deps = [
"@io_bazel_rules_go//go/platform:android": [ "@com_github_stretchr_testify//assert",
"@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup", "@org_uber_go_goleak//:goleak",
"@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": [],
}),
) )
go_ld_test( go_ld_test(

View File

@ -9,9 +9,254 @@ package cryptmapper
import ( import (
"context" "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. const (
type KeyCreator interface { // 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) GetDEK(ctx context.Context, dekID string, dekSize int) ([]byte, error)
} }

View File

@ -9,16 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
package cryptmapper package cryptmapper
import ( import (
"context"
"errors"
"fmt" "fmt"
"io/fs"
"path/filepath"
"strings"
"sync"
"time"
"github.com/edgelesssys/constellation/v2/internal/crypto"
ccryptsetup "github.com/edgelesssys/constellation/v2/internal/cryptsetup" ccryptsetup "github.com/edgelesssys/constellation/v2/internal/cryptsetup"
cryptsetup "github.com/martinjungblut/go-cryptsetup" cryptsetup "github.com/martinjungblut/go-cryptsetup"
mount "k8s.io/mount-utils" mount "k8s.io/mount-utils"
@ -26,377 +18,15 @@ import (
) )
const ( const (
// LUKSHeaderSize is the amount of bytes taken up by the header of a LUKS2 partition. resizeFlags = cryptsetup.CRYPT_ACTIVATE_KEYRING_KEY | ccryptsetup.ReadWriteQueueBypass
// The header is 16MiB (1048576 Bytes * 16).
LUKSHeaderSize = 16777216
cryptPrefix = "/dev/mapper/"
integritySuffix = "_dif"
integrityFSSuffix = "-integrity"
keySizeIntegrity = 96
keySizeCrypt = 64
) )
// 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() { func init() {
cryptsetup.SetDebugLevel(cryptsetup.CRYPT_LOG_NORMAL) cryptsetup.SetDebugLevel(cryptsetup.CRYPT_LOG_NORMAL)
cryptsetup.SetLogCallback(func(_ int, message string) { fmt.Printf("libcryptsetup: %s\n", message) }) cryptsetup.SetLogCallback(func(_ int, message string) { fmt.Printf("libcryptsetup: %s\n", message) })
} }
// deviceMapper is an interface for device mapper methods. func getDiskFormat(disk string) (string, error) {
type deviceMapper interface { mountUtil := &mount.SafeFormatAndMount{Exec: utilexec.New()}
// Init initializes a crypt device backed by 'devicePath'. return mountUtil.GetDiskFormat(disk)
// 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
} }

View File

@ -9,69 +9,15 @@ SPDX-License-Identifier: AGPL-3.0-only
package cryptmapper package cryptmapper
import ( import (
"context"
"errors" "errors"
ccryptsetup "github.com/edgelesssys/constellation/v2/internal/cryptsetup"
) )
// deviceMapper is an interface for device mapper methods. const (
type deviceMapper interface{} resizeFlags = 0x800 | ccryptsetup.ReadWriteQueueBypass
)
// CryptDevice is a wrapper for cryptsetup.Device. func getDiskFormat(_ string) (string, error) {
type CryptDevice struct{} return "", errors.New("getDiskFormat requires building with CGO enabled")
// 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
} }

View File

@ -1,5 +1,3 @@
//go:build linux && cgo
/* /*
Copyright (c) Edgeless Systems GmbH Copyright (c) Edgeless Systems GmbH
@ -9,11 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
package cryptmapper package cryptmapper
import ( import (
"bytes"
"context" "context"
"errors" "errors"
"testing" "testing"
"time"
cryptsetup "github.com/martinjungblut/go-cryptsetup"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"go.uber.org/goleak" "go.uber.org/goleak"
) )
@ -22,75 +21,6 @@ func TestMain(m *testing.M) {
goleak.VerifyTestMain(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) { func TestCloseCryptDevice(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
mapper *stubCryptDevice mapper *stubCryptDevice
@ -101,11 +31,11 @@ func TestCloseCryptDevice(t *testing.T) {
wantErr: false, wantErr: false,
}, },
"error on InitByName": { "error on InitByName": {
mapper: &stubCryptDevice{initByNameErr: errors.New("error")}, mapper: &stubCryptDevice{initByNameErr: assert.AnError},
wantErr: true, wantErr: true,
}, },
"error on Deactivate": { "error on Deactivate": {
mapper: &stubCryptDevice{deactivateErr: errors.New("error")}, mapper: &stubCryptDevice{deactivateErr: assert.AnError},
wantErr: true, wantErr: true,
}, },
} }
@ -114,7 +44,11 @@ func TestCloseCryptDevice(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(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 { if tc.wantErr {
assert.Error(err) assert.Error(err)
} else { } else {
@ -129,22 +63,12 @@ func TestCloseCryptDevice(t *testing.T) {
} }
func TestOpenCryptDevice(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 { testCases := map[string]struct {
source string source string
volumeID string volumeID string
integrity bool integrity bool
mapper *stubCryptDevice mapper *stubCryptDevice
getKey func(context.Context, string, int) ([]byte, error) kms keyCreator
diskInfo func(disk string) (string, error) diskInfo func(disk string) (string, error)
wantErr bool wantErr bool
}{ }{
@ -152,15 +76,15 @@ func TestOpenCryptDevice(t *testing.T) {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{}, mapper: &stubCryptDevice{},
getKey: getKeyFunc, kms: &fakeKMS{},
diskInfo: func(disk string) (string, error) { return "", nil }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: false, wantErr: false,
}, },
"success with error on Load": { "success with error on Load": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{loadErr: someErr}, mapper: &stubCryptDevice{loadErr: assert.AnError},
getKey: getKeyFunc, kms: &fakeKMS{},
diskInfo: func(disk string) (string, error) { return "", nil }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: false, wantErr: false,
}, },
@ -168,48 +92,48 @@ func TestOpenCryptDevice(t *testing.T) {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
integrity: true, integrity: true,
mapper: &stubCryptDevice{loadErr: someErr}, mapper: &stubCryptDevice{loadErr: assert.AnError},
getKey: getKeyFunc, kms: &fakeKMS{},
diskInfo: func(disk string) (string, error) { return "", nil }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: false, wantErr: false,
}, },
"error on Init": { "error on Init": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{initErr: someErr}, mapper: &stubCryptDevice{initErr: assert.AnError},
getKey: getKeyFunc, kms: &fakeKMS{},
diskInfo: func(disk string) (string, error) { return "", nil }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
"error on Format": { "error on Format": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{loadErr: someErr, formatErr: someErr}, mapper: &stubCryptDevice{loadErr: assert.AnError, formatErr: assert.AnError},
getKey: getKeyFunc, kms: &fakeKMS{},
diskInfo: func(disk string) (string, error) { return "", nil }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
"error on Activate": { "error on Activate": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{activatePassErr: someErr}, mapper: &stubCryptDevice{activatePassErr: assert.AnError},
getKey: getKeyFunc, kms: &fakeKMS{},
diskInfo: func(disk string) (string, error) { return "", nil }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
"error on diskInfo": { "error on diskInfo": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{loadErr: someErr}, mapper: &stubCryptDevice{loadErr: assert.AnError},
getKey: getKeyFunc, kms: &fakeKMS{},
diskInfo: func(disk string) (string, error) { return "", someErr }, diskInfo: func(disk string) (string, error) { return "", assert.AnError },
wantErr: true, wantErr: true,
}, },
"disk is already formatted": { "disk is already formatted": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{loadErr: someErr}, mapper: &stubCryptDevice{loadErr: assert.AnError},
getKey: getKeyFunc, kms: &fakeKMS{},
diskInfo: func(disk string) (string, error) { return "ext4", nil }, diskInfo: func(disk string) (string, error) { return "ext4", nil },
wantErr: true, wantErr: true,
}, },
@ -217,37 +141,16 @@ func TestOpenCryptDevice(t *testing.T) {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
integrity: true, integrity: true,
mapper: &stubCryptDevice{loadErr: someErr, wipeErr: someErr}, mapper: &stubCryptDevice{loadErr: assert.AnError, wipeErr: assert.AnError},
getKey: getKeyFunc, kms: &fakeKMS{},
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,
diskInfo: func(disk string) (string, error) { return "", nil }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
"error on adding keyslot": { "error on adding keyslot": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{ mapper: &stubCryptDevice{loadErr: assert.AnError, keySlotAddErr: assert.AnError},
loadErr: someErr, kms: &fakeKMS{},
keySlotAddErr: someErr,
},
getKey: getKeyFunc,
diskInfo: func(disk string) (string, error) { return "", nil }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
@ -255,15 +158,15 @@ func TestOpenCryptDevice(t *testing.T) {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{}, 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 }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
"incorrect key length with error on Load": { "incorrect key length with error on Load": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{loadErr: someErr}, mapper: &stubCryptDevice{loadErr: assert.AnError},
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 }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
@ -271,15 +174,15 @@ func TestOpenCryptDevice(t *testing.T) {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{}, 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 }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
"getKey fails with error on Load": { "getKey fails with error on Load": {
source: "/dev/some-device", source: "/dev/some-device",
volumeID: "volume0", volumeID: "volume0",
mapper: &stubCryptDevice{loadErr: someErr}, mapper: &stubCryptDevice{loadErr: assert.AnError},
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 }, diskInfo: func(disk string) (string, error) { return "", nil },
wantErr: true, wantErr: true,
}, },
@ -289,15 +192,13 @@ func TestOpenCryptDevice(t *testing.T) {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
out, err := openCryptDevice( mapper := &CryptMapper{
context.Background(), mapper: tc.mapper,
tc.mapper, kms: tc.kms,
tc.source, getDiskFormat: tc.diskInfo,
tc.volumeID, }
tc.integrity,
tc.getKey, out, err := mapper.OpenCryptDevice(context.Background(), tc.source, tc.volumeID, tc.integrity)
tc.diskInfo,
)
if tc.wantErr { if tc.wantErr {
assert.Error(err) assert.Error(err)
} else { } 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) { func (k *fakeKMS) GetDEK(_ context.Context, _ string, dekSize int) ([]byte, error) {
key := make([]byte, dekSize) if k.getDEKErr != nil {
for i := range key { return nil, k.getDEKErr
key[i] = 0x41
} }
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
} }

View File

@ -16,22 +16,27 @@ import (
"testing" "testing"
"github.com/edgelesssys/constellation/v2/csi/cryptmapper" "github.com/edgelesssys/constellation/v2/csi/cryptmapper"
"github.com/edgelesssys/constellation/v2/internal/cryptsetup"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/goleak" "go.uber.org/goleak"
) )
const ( const (
DevicePath string = "testDevice" devicePath string = "testDevice"
DeviceName string = "testDeviceName" deviceName string = "testdeviceName"
) )
func setup() { 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) { 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 { func cp(source, target string) error {
@ -39,7 +44,9 @@ func cp(source, target string) error {
} }
func resize() { 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) { func TestMain(m *testing.M) {
@ -58,17 +65,19 @@ func TestOpenAndClose(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t) require := require.New(t)
setup() 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) require.NoError(err)
assert.Equal("/dev/mapper/"+DeviceName, newPath) defer func() {
_ = mapper.CloseCryptDevice(deviceName)
}()
// assert crypt device got created // assert crypt device got created
_, err = os.Stat(newPath) _, err = os.Stat(newPath)
assert.NoError(err) require.NoError(err)
// assert no integrity device got created // assert no integrity device got created
_, err = os.Stat(newPath + "_dif") _, err = os.Stat(newPath + "_dif")
assert.True(os.IsNotExist(err)) assert.True(os.IsNotExist(err))
@ -76,33 +85,33 @@ func TestOpenAndClose(t *testing.T) {
// Resize the device // Resize the device
resize() resize()
resizedPath, err := mapper.ResizeCryptDevice(context.Background(), DeviceName) resizedPath, err := mapper.ResizeCryptDevice(context.Background(), deviceName)
require.NoError(err) 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 // assert crypt device got removed
_, err = os.Stat(newPath) _, err = os.Stat(newPath)
assert.True(os.IsNotExist(err)) assert.True(os.IsNotExist(err))
// check if we can reopen the device // 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(err)
assert.NoError(mapper.CloseCryptDevice(DeviceName)) assert.NoError(mapper.CloseCryptDevice(deviceName))
} }
func TestOpenAndCloseIntegrity(t *testing.T) { func TestOpenAndCloseIntegrity(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t) require := require.New(t)
setup() 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) require.NoError(err)
assert.Equal("/dev/mapper/"+DeviceName, newPath) assert.Equal("/dev/mapper/"+deviceName, newPath)
// assert crypt device got created // assert crypt device got created
_, err = os.Stat(newPath) _, err = os.Stat(newPath)
@ -113,10 +122,10 @@ func TestOpenAndCloseIntegrity(t *testing.T) {
// integrity devices do not support resizing // integrity devices do not support resizing
resize() resize()
_, err = mapper.ResizeCryptDevice(context.Background(), DeviceName) _, err = mapper.ResizeCryptDevice(context.Background(), deviceName)
assert.Error(err) assert.Error(err)
assert.NoError(mapper.CloseCryptDevice(DeviceName)) assert.NoError(mapper.CloseCryptDevice(deviceName))
// assert crypt device got removed // assert crypt device got removed
_, err = os.Stat(newPath) _, err = os.Stat(newPath)
@ -126,30 +135,30 @@ func TestOpenAndCloseIntegrity(t *testing.T) {
assert.True(os.IsNotExist(err)) assert.True(os.IsNotExist(err))
// check if we can reopen the device // 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(err)
assert.NoError(mapper.CloseCryptDevice(DeviceName)) assert.NoError(mapper.CloseCryptDevice(deviceName))
} }
func TestDeviceCloning(t *testing.T) { func TestDeviceCloning(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
require := require.New(t) require := require.New(t)
setup() 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) assert.NoError(err)
require.NoError(cp(DevicePath, DevicePath+"-copy")) require.NoError(cp(devicePath, devicePath+"-copy"))
defer teardown(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(err)
assert.NoError(mapper.CloseCryptDevice(DeviceName)) assert.NoError(mapper.CloseCryptDevice(deviceName))
assert.NoError(mapper.CloseCryptDevice(DeviceName + "-copy")) assert.NoError(mapper.CloseCryptDevice(deviceName + "-copy"))
} }
type fakeKMS struct{} type fakeKMS struct{}

View File

@ -7,7 +7,7 @@ go_library(
importpath = "github.com/edgelesssys/constellation/v2/disk-mapper/cmd", importpath = "github.com/edgelesssys/constellation/v2/disk-mapper/cmd",
visibility = ["//visibility:private"], visibility = ["//visibility:private"],
deps = [ deps = [
"//disk-mapper/internal/mapper", "//disk-mapper/internal/diskencryption",
"//disk-mapper/internal/recoveryserver", "//disk-mapper/internal/recoveryserver",
"//disk-mapper/internal/rejoinclient", "//disk-mapper/internal/rejoinclient",
"//disk-mapper/internal/setup", "//disk-mapper/internal/setup",

View File

@ -14,7 +14,7 @@ import (
"os" "os"
"path/filepath" "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/recoveryserver"
"github.com/edgelesssys/constellation/v2/disk-mapper/internal/rejoinclient" "github.com/edgelesssys/constellation/v2/disk-mapper/internal/rejoinclient"
"github.com/edgelesssys/constellation/v2/disk-mapper/internal/setup" "github.com/edgelesssys/constellation/v2/disk-mapper/internal/setup"
@ -119,11 +119,11 @@ func main() {
} }
// initialize device mapper // initialize device mapper
mapper, err := mapper.New(diskPath, log) mapper, free, err := diskencryption.New(diskPath, log)
if err != nil { if err != nil {
log.With(zap.Error(err)).Fatalf("Failed to initialize device mapper") log.With(zap.Error(err)).Fatalf("Failed to initialize device mapper")
} }
defer mapper.Close() defer free()
// Use TDX if available // Use TDX if available
openDevice := vtpm.OpenVTPM openDevice := vtpm.OpenVTPM

View File

@ -62,3 +62,15 @@ go_library(
"//conditions:default": [], "//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",
],
)

View File

@ -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
}

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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")
}

View File

@ -22,7 +22,7 @@ type Mounter interface {
// DeviceMapper is an interface for device mapping operations. // DeviceMapper is an interface for device mapping operations.
type DeviceMapper interface { type DeviceMapper interface {
DiskUUID() string DiskUUID() (string, error)
FormatDisk(passphrase string) error FormatDisk(passphrase string) error
MapDisk(target string, passphrase string) error MapDisk(target string, passphrase string) error
UnmapDisk(target string) error UnmapDisk(target string) error

View File

@ -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. // 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. // Once the disk is mapped, the function taints the node as initialized by updating it's PCRs.
func (s *Manager) PrepareExistingDisk(recover RecoveryDoer) error { 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") s.log.With(zap.String("uuid", uuid)).Infof("Preparing existing state disk")
endpoint := net.JoinHostPort("0.0.0.0", strconv.Itoa(constants.RecoveryPort)) 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. // PrepareNewDisk prepares an instances state disk by formatting the disk as a LUKS device using a random passphrase.
func (s *Manager) PrepareNewDisk() error { 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 // generate and save temporary passphrase
passphrase := make([]byte, crypto.RNGLengthDefault) passphrase := make([]byte, crypto.RNGLengthDefault)

View File

@ -394,8 +394,8 @@ type stubMapper struct {
uuid string uuid string
} }
func (s *stubMapper) DiskUUID() string { func (s *stubMapper) DiskUUID() (string, error) {
return s.uuid return s.uuid, nil
} }
func (s *stubMapper) FormatDisk(string) error { func (s *stubMapper) FormatDisk(string) error {

View File

@ -13,7 +13,7 @@ import (
"math" "math"
"testing" "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/edgelesssys/constellation/v2/internal/logger"
"github.com/martinjungblut/go-cryptsetup" "github.com/martinjungblut/go-cryptsetup"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
@ -39,11 +39,11 @@ func BenchmarkMapper(b *testing.B) {
} }
passphrase := "benchmark" 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 { if err != nil {
b.Fatal("Failed to create mapper:", err) b.Fatal("Failed to create mapper:", err)
} }
defer mapper.Close() defer free()
if err := mapper.FormatDisk(passphrase); err != nil { if err := mapper.FormatDisk(passphrase); err != nil {
b.Fatal("Failed to format disk:", err) b.Fatal("Failed to format disk:", err)

View File

@ -15,7 +15,7 @@ import (
"os/exec" "os/exec"
"testing" "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/edgelesssys/constellation/v2/internal/logger"
"github.com/martinjungblut/go-cryptsetup" "github.com/martinjungblut/go-cryptsetup"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -64,9 +64,9 @@ func TestMapper(t *testing.T) {
require.NoError(setup(1), "failed to setup test disk") require.NoError(setup(1), "failed to setup test disk")
defer func() { require.NoError(teardown(), "failed to delete 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") 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()) assert.False(mapper.IsLUKSDevice())

View File

@ -3,8 +3,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library( go_library(
name = "cryptsetup", name = "cryptsetup",
srcs = [ srcs = [
"crypsetup_cross.go", "cryptsetup.go",
"cryptsetup_cgo.go", "cryptsetup_cgo.go",
"cryptsetup_cross.go",
], ],
# keep # keep
cdeps = [ cdeps = [
@ -13,4 +14,13 @@ go_library(
cgo = True, cgo = True,
importpath = "github.com/edgelesssys/constellation/v2/internal/cryptsetup", importpath = "github.com/edgelesssys/constellation/v2/internal/cryptsetup",
visibility = ["//:__subpackages__"], 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": [],
}),
) )

View File

@ -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
)

View File

@ -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
}

View File

@ -5,14 +5,78 @@ Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only SPDX-License-Identifier: AGPL-3.0-only
*/ */
// Package cryptsetup contains CGO bindings for cryptsetup.
package cryptsetup package cryptsetup
// #include <libcryptsetup.h> // #include <libcryptsetup.h>
import "C" import "C"
import (
"errors"
"github.com/martinjungblut/go-cryptsetup"
)
const ( const (
// ReadWriteQueueBypass is a flag to disable the write and read workqueues for a crypt device. // 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 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
}

View File

@ -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
}