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

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

View file

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

View file

@ -62,3 +62,15 @@ go_library(
"//conditions:default": [],
}),
)
go_library(
name = "diskencryption",
srcs = ["diskencryption.go"],
importpath = "github.com/edgelesssys/constellation/v2/disk-mapper/internal/diskencryption",
visibility = ["//disk-mapper:__subpackages__"],
deps = [
"//internal/cryptsetup",
"//internal/logger",
"@org_uber_go_zap//:zap",
],
)

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.
type DeviceMapper interface {
DiskUUID() string
DiskUUID() (string, error)
FormatDisk(passphrase string) error
MapDisk(target string, passphrase 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.
// Once the disk is mapped, the function taints the node as initialized by updating it's PCRs.
func (s *Manager) PrepareExistingDisk(recover RecoveryDoer) error {
uuid := s.mapper.DiskUUID()
uuid, err := s.mapper.DiskUUID()
if err != nil {
return err
}
s.log.With(zap.String("uuid", uuid)).Infof("Preparing existing state disk")
endpoint := net.JoinHostPort("0.0.0.0", strconv.Itoa(constants.RecoveryPort))
@ -124,7 +127,8 @@ func (s *Manager) PrepareExistingDisk(recover RecoveryDoer) error {
// PrepareNewDisk prepares an instances state disk by formatting the disk as a LUKS device using a random passphrase.
func (s *Manager) PrepareNewDisk() error {
s.log.With(zap.String("uuid", s.mapper.DiskUUID())).Infof("Preparing new state disk")
uuid, _ := s.mapper.DiskUUID()
s.log.With(zap.String("uuid", uuid)).Infof("Preparing new state disk")
// generate and save temporary passphrase
passphrase := make([]byte, crypto.RNGLengthDefault)

View file

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

View file

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

View file

@ -15,7 +15,7 @@ import (
"os/exec"
"testing"
"github.com/edgelesssys/constellation/v2/disk-mapper/internal/mapper"
"github.com/edgelesssys/constellation/v2/disk-mapper/internal/diskencryption"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/martinjungblut/go-cryptsetup"
"github.com/stretchr/testify/assert"
@ -64,9 +64,9 @@ func TestMapper(t *testing.T) {
require.NoError(setup(1), "failed to setup test disk")
defer func() { require.NoError(teardown(), "failed to delete test disk") }()
mapper, err := mapper.New(devicePath, logger.NewTest(t))
mapper, free, err := diskencryption.New(devicePath, logger.NewTest(t))
require.NoError(err, "failed to initialize crypt device")
defer func() { require.NoError(mapper.Close(), "failed to close crypt device") }()
defer free()
assert.False(mapper.IsLUKSDevice())