mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-12-15 16:09:39 -05:00
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:
parent
f52c6752e2
commit
ac1128d07f
34 changed files with 1061 additions and 1307 deletions
|
|
@ -3,8 +3,9 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library")
|
|||
go_library(
|
||||
name = "cryptsetup",
|
||||
srcs = [
|
||||
"crypsetup_cross.go",
|
||||
"cryptsetup.go",
|
||||
"cryptsetup_cgo.go",
|
||||
"cryptsetup_cross.go",
|
||||
],
|
||||
# keep
|
||||
cdeps = [
|
||||
|
|
@ -13,4 +14,13 @@ go_library(
|
|||
cgo = True,
|
||||
importpath = "github.com/edgelesssys/constellation/v2/internal/cryptsetup",
|
||||
visibility = ["//:__subpackages__"],
|
||||
deps = select({
|
||||
"@io_bazel_rules_go//go/platform:android": [
|
||||
"@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup",
|
||||
],
|
||||
"@io_bazel_rules_go//go/platform:linux": [
|
||||
"@com_github_martinjungblut_go_cryptsetup//:go-cryptsetup",
|
||||
],
|
||||
"//conditions:default": [],
|
||||
}),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
278
internal/cryptsetup/cryptsetup.go
Normal file
278
internal/cryptsetup/cryptsetup.go
Normal 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
|
||||
}
|
||||
|
|
@ -5,14 +5,78 @@ Copyright (c) Edgeless Systems GmbH
|
|||
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
// Package cryptsetup contains CGO bindings for cryptsetup.
|
||||
package cryptsetup
|
||||
|
||||
// #include <libcryptsetup.h>
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/martinjungblut/go-cryptsetup"
|
||||
)
|
||||
|
||||
const (
|
||||
// ReadWriteQueueBypass is a flag to disable the write and read workqueues for a crypt device.
|
||||
ReadWriteQueueBypass = C.CRYPT_ACTIVATE_NO_WRITE_WORKQUEUE | C.CRYPT_ACTIVATE_NO_READ_WORKQUEUE
|
||||
wipeFlags = cryptsetup.CRYPT_ACTIVATE_PRIVATE | cryptsetup.CRYPT_ACTIVATE_NO_JOURNAL
|
||||
wipePattern = cryptsetup.CRYPT_WIPE_ZERO
|
||||
)
|
||||
|
||||
var errInvalidType = errors.New("device is not a *cryptsetup.Device")
|
||||
|
||||
func format(device cryptDevice, integrity bool) error {
|
||||
switch d := device.(type) {
|
||||
case cgoFormatter:
|
||||
luks2Params := cryptsetup.LUKS2{
|
||||
SectorSize: 4096,
|
||||
PBKDFType: &cryptsetup.PbkdfType{
|
||||
// Use low memory recommendation from https://datatracker.ietf.org/doc/html/rfc9106#section-7
|
||||
Type: "argon2id",
|
||||
TimeMs: 2000,
|
||||
Iterations: 3,
|
||||
ParallelThreads: 4,
|
||||
MaxMemoryKb: 65536, // ~64MiB
|
||||
},
|
||||
}
|
||||
genericParams := cryptsetup.GenericParams{
|
||||
Cipher: "aes",
|
||||
CipherMode: "xts-plain64",
|
||||
VolumeKeySize: 64, // 32*2 bytes for aes-xts-plain64 encryption
|
||||
}
|
||||
|
||||
if integrity {
|
||||
luks2Params.Integrity = "hmac(sha256)"
|
||||
genericParams.VolumeKeySize += 32 // 32 bytes for hmac(sha256) integrity
|
||||
}
|
||||
|
||||
return d.Format(luks2Params, genericParams)
|
||||
default:
|
||||
return errInvalidType
|
||||
}
|
||||
}
|
||||
|
||||
func initByDevicePath(devicePath string) (cryptDevice, error) {
|
||||
return cryptsetup.Init(devicePath)
|
||||
}
|
||||
|
||||
func initByName(name string) (cryptDevice, error) {
|
||||
return cryptsetup.InitByName(name)
|
||||
}
|
||||
|
||||
func loadLUKS2(device cryptDevice) error {
|
||||
switch d := device.(type) {
|
||||
case cgoLoader:
|
||||
return d.Load(cryptsetup.LUKS2{})
|
||||
default:
|
||||
return errInvalidType
|
||||
}
|
||||
}
|
||||
|
||||
type cgoFormatter interface {
|
||||
Format(deviceType cryptsetup.DeviceType, genericParams cryptsetup.GenericParams) error
|
||||
}
|
||||
|
||||
type cgoLoader interface {
|
||||
Load(deviceType cryptsetup.DeviceType) error
|
||||
}
|
||||
|
|
|
|||
39
internal/cryptsetup/cryptsetup_cross.go
Normal file
39
internal/cryptsetup/cryptsetup_cross.go
Normal 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
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue