mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-09-19 12:34:44 -04:00

* deps: update go-cryptsetup * cryptsetup: use detached headers when opening existing crypt devices * cryptsetup: only activate disks with detached header --------- Signed-off-by: Daniel Weiße <dw@edgeless.systems>
429 lines
14 KiB
Go
429 lines
14 KiB
Go
//go:build linux && cgo
|
|
|
|
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: BUSL-1.1
|
|
*/
|
|
package cryptsetup
|
|
|
|
// #include <libcryptsetup.h>
|
|
import "C"
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/martinjungblut/go-cryptsetup"
|
|
"golang.org/x/sys/unix"
|
|
)
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
// headerRestore restores the header of the given device from the header in the given file.
|
|
// Reloading the device is required for the changes to be reflected in the active [cryptDevice] struct.
|
|
func headerRestore(device cryptDevice, headerFile string) error {
|
|
switch d := device.(type) {
|
|
case cgoRestorer:
|
|
return d.HeaderRestore(cryptsetup.LUKS2{}, headerFile)
|
|
default:
|
|
return errInvalidType
|
|
}
|
|
}
|
|
|
|
// headerBackup creates a backup of the cryptDevice's header to the given file.
|
|
func headerBackup(device cryptDevice, headerFile string) error {
|
|
switch d := device.(type) {
|
|
case cgoBackuper:
|
|
if err := os.Remove(headerFile); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
return fmt.Errorf("removing existing header file %q: %w", headerFile, err)
|
|
}
|
|
if err := d.HeaderBackup(cryptsetup.LUKS2{}, headerFile); err != nil {
|
|
return fmt.Errorf("creating header backup: %w", err)
|
|
}
|
|
return nil
|
|
default:
|
|
return errInvalidType
|
|
}
|
|
}
|
|
|
|
func initByDevicePath(devicePath string) (deviceDetachedHeader, deviceAttachedHeader cryptDevice, headerDevice string, headerFile string, err error) {
|
|
tmpDevice, err := cryptsetup.Init(devicePath)
|
|
if err != nil {
|
|
return nil, nil, "", "", fmt.Errorf("init device by path %s: %w", devicePath, err)
|
|
}
|
|
// If the device is not LUKS2 formatted, this is treated as a new device,
|
|
// meaning no header exists yet
|
|
if err := tmpDevice.Load(cryptsetup.LUKS2{}); err != nil {
|
|
return nil, tmpDevice, "", "", nil
|
|
}
|
|
defer tmpDevice.Free()
|
|
|
|
deviceAttachedHeader, err = cryptsetup.Init(devicePath)
|
|
if err != nil {
|
|
return nil, nil, "", "", fmt.Errorf("init device by path %s: %w", devicePath, err)
|
|
}
|
|
defer func() {
|
|
if err != nil && deviceAttachedHeader != nil {
|
|
deviceAttachedHeader.Free()
|
|
}
|
|
}()
|
|
|
|
headerDevice, headerFile, err = detachHeader(tmpDevice)
|
|
if err != nil {
|
|
return nil, nil, "", "", err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
_ = detachLoopbackDevice(headerDevice)
|
|
}
|
|
}()
|
|
|
|
cryptDevice, err := cryptsetup.InitDataDevice(headerDevice, devicePath)
|
|
return cryptDevice, deviceAttachedHeader, headerDevice, headerFile, err
|
|
}
|
|
|
|
func initByName(name string) (deviceDetachedHeader, deviceAttachedHeader cryptDevice, headerDevice string, headerFile string, err error) {
|
|
tmpDevice, err := cryptsetup.InitByName(name)
|
|
if err != nil {
|
|
return nil, nil, "", "", fmt.Errorf("init device by name %s: %w", name, err)
|
|
}
|
|
// If the device is not LUKS2 formatted, this is treated as a new device,
|
|
// meaning no header exists yet
|
|
if err := tmpDevice.Load(cryptsetup.LUKS2{}); err != nil {
|
|
return nil, tmpDevice, "", "", nil
|
|
}
|
|
defer tmpDevice.Free()
|
|
|
|
deviceAttachedHeader, err = cryptsetup.InitByName(name)
|
|
if err != nil {
|
|
return nil, nil, "", "", fmt.Errorf("init device by name %s: %w", name, err)
|
|
}
|
|
defer func() {
|
|
if err != nil && deviceAttachedHeader != nil {
|
|
deviceAttachedHeader.Free()
|
|
}
|
|
}()
|
|
|
|
headerDevice, headerFile, err = detachHeader(tmpDevice)
|
|
if err != nil {
|
|
return nil, nil, "", "", err
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
_ = detachLoopbackDevice(headerDevice)
|
|
}
|
|
}()
|
|
|
|
cryptDevice, err := cryptsetup.InitByNameAndHeader(name, headerDevice)
|
|
return cryptDevice, deviceAttachedHeader, headerDevice, headerFile, err
|
|
}
|
|
|
|
func loadLUKS2(device cryptDevice) error {
|
|
switch d := device.(type) {
|
|
case cgoLoader:
|
|
return d.Load(cryptsetup.LUKS2{})
|
|
default:
|
|
return errInvalidType
|
|
}
|
|
}
|
|
|
|
// detachHeader loads reads the header from the given cryptsetup device and returns a loopback device with just the header.
|
|
func detachHeader(device *cryptsetup.Device) (headerDevice, headerFile string, err error) {
|
|
headerFile = filepath.Join(os.TempDir(), fmt.Sprintf("luks-header-%s", uuid.New().String()))
|
|
if err = headerBackup(device, headerFile); err != nil {
|
|
return "", "", err
|
|
}
|
|
|
|
headerDevice, err = createLoopbackDevice(headerFile)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("create loopback device: %w", err)
|
|
}
|
|
defer func() {
|
|
if err != nil {
|
|
_ = detachLoopbackDevice(headerDevice)
|
|
}
|
|
}()
|
|
|
|
headerCryptDevice, err := cryptsetup.Init(headerDevice)
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("init header device: %w", err)
|
|
}
|
|
defer headerCryptDevice.Free()
|
|
if err := headerCryptDevice.Load(cryptsetup.LUKS2{}); err != nil {
|
|
return "", "", fmt.Errorf("creating header backup: %w", err)
|
|
}
|
|
metadataJSON, err := headerCryptDevice.DumpJSON()
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("dumping device metadata: %w", err)
|
|
}
|
|
|
|
var metadata cryptsetupMetadata
|
|
decoder := json.NewDecoder(strings.NewReader(metadataJSON))
|
|
decoder.DisallowUnknownFields() // Ensure no unknown fields are present in the JSON data
|
|
if err := decoder.Decode(&metadata); err != nil {
|
|
return "", "", fmt.Errorf("decoding LUKS header JSON from %s: %w", headerFile, err)
|
|
}
|
|
|
|
if err := verifyLUKS2Header(metadata); err != nil {
|
|
return "", "", fmt.Errorf("verifying LUKS2 header: %w", err)
|
|
}
|
|
|
|
return headerDevice, headerFile, nil
|
|
}
|
|
|
|
// verifyLUKS2Header verifies a LUKS2 header contains the expected configuration for Constellation.
|
|
func verifyLUKS2Header(metadata cryptsetupMetadata) error {
|
|
if len(metadata.KeySlots) == 0 {
|
|
return errors.New("no key slots found in LUKS2 header")
|
|
}
|
|
for slotName, slot := range metadata.KeySlots {
|
|
if slot.Type != "luks2" {
|
|
return fmt.Errorf("unsupported key slot type %q for slot %q", slot.Type, slotName)
|
|
}
|
|
if slot.KeySize != 64 && slot.KeySize != 96 { // 64 for encryption, 96 if integrity is added
|
|
return fmt.Errorf("unsupported key size %d for slot %q", slot.KeySize, slotName)
|
|
}
|
|
if slot.AntiForensicSplitter.Type != "luks1" {
|
|
return fmt.Errorf("unsupported anti-forensic splitter type %q for slot %q", slot.AntiForensicSplitter.Type, slotName)
|
|
}
|
|
if slot.AntiForensicSplitter.Stripes != 4000 {
|
|
return fmt.Errorf("unsupported anti-forensic splitter stripes %d for slot %q", slot.AntiForensicSplitter.Stripes, slotName)
|
|
}
|
|
if slot.AntiForensicSplitter.Hash != "sha256" {
|
|
return fmt.Errorf("unsupported anti-forensic splitter hash %q for slot %q", slot.AntiForensicSplitter.Hash, slotName)
|
|
}
|
|
if slot.Area.Type != "raw" {
|
|
return fmt.Errorf("unsupported area type %q for slot %q", slot.Area.Type, slotName)
|
|
}
|
|
if slot.Area.Encryption != "aes-xts-plain64" {
|
|
return fmt.Errorf("unsupported area encryption %q for slot %q", slot.Area.Encryption, slotName)
|
|
}
|
|
if slot.Area.KeySize != 64 {
|
|
return fmt.Errorf("unsupported area key size %d for slot %q", slot.Area.KeySize, slotName)
|
|
}
|
|
if slot.KDF.Type != "argon2id" {
|
|
return fmt.Errorf("unsupported KDF type %q for slot %q", slot.KDF.Type, slotName)
|
|
}
|
|
if slot.KDF.Memory == 0 {
|
|
return fmt.Errorf("unsupported KDF memory %d for slot %q", slot.KDF.Memory, slotName)
|
|
}
|
|
if slot.KDF.Salt == "" {
|
|
return fmt.Errorf("unsupported KDF salt for slot %q", slotName)
|
|
}
|
|
}
|
|
if len(metadata.Segments) == 0 {
|
|
return errors.New("no segments found in LUKS2 header")
|
|
}
|
|
for segmentName, segment := range metadata.Segments {
|
|
if segment.Type != "crypt" {
|
|
return fmt.Errorf("unsupported segment type %q for segment %q", segment.Type, segmentName)
|
|
}
|
|
if segment.SectorSize != 4096 {
|
|
return fmt.Errorf("unsupported segment sector size %d for segment %q", segment.SectorSize, segmentName)
|
|
}
|
|
if segment.IVTweak != "0" {
|
|
return fmt.Errorf("unsupported segment IV tweak %q for segment %q", segment.IVTweak, segmentName)
|
|
}
|
|
if segment.Encryption != "aes-xts-plain64" {
|
|
return fmt.Errorf("unsupported segment encryption %q for segment %q", segment.Encryption, segmentName)
|
|
}
|
|
switch segment.Integrity.Type {
|
|
case "hmac(sha256)":
|
|
if segment.Integrity.JournalEncryption != "none" {
|
|
return fmt.Errorf("unsupported segment integrity journal encryption %q for segment %q", segment.Integrity.JournalEncryption, segmentName)
|
|
}
|
|
if segment.Integrity.JournalIntegrity != "none" {
|
|
return fmt.Errorf("unsupported segment integrity journal integrity %q for segment %q", segment.Integrity.JournalIntegrity, segmentName)
|
|
}
|
|
case "":
|
|
if segment.Integrity.JournalEncryption != "" {
|
|
return fmt.Errorf("unsupported segment integrity journal encryption %q for segment %q", segment.Integrity.JournalEncryption, segmentName)
|
|
}
|
|
if segment.Integrity.JournalIntegrity != "" {
|
|
return fmt.Errorf("unsupported segment integrity journal integrity %q for segment %q", segment.Integrity.JournalIntegrity, segmentName)
|
|
}
|
|
default:
|
|
return fmt.Errorf("unsupported segment integrity type %q for segment %q", segment.Integrity.Type, segmentName)
|
|
}
|
|
}
|
|
if len(metadata.Digests) == 0 {
|
|
return errors.New("no digests found in LUKS2 header")
|
|
}
|
|
for digestName, digest := range metadata.Digests {
|
|
if digest.Type != "pbkdf2" {
|
|
return fmt.Errorf("unsupported digest type %q for digest %q", digest.Type, digestName)
|
|
}
|
|
if digest.Hash != "sha256" {
|
|
return fmt.Errorf("unsupported digest hash %q for digest %q", digest.Hash, digestName)
|
|
}
|
|
if digest.Salt == "" {
|
|
return fmt.Errorf("unsupported digest salt for digest %q", digestName)
|
|
}
|
|
if digest.Digest == "" {
|
|
return fmt.Errorf("unsupported digest value for digest %q", digestName)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// createLoopbackDevice sets up a loop device for the given file and returns the loop device path (e.g., /dev/loop0).
|
|
func createLoopbackDevice(filePath string) (string, error) {
|
|
file, err := os.OpenFile(filePath, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open backing file: %w", err)
|
|
}
|
|
defer file.Close()
|
|
|
|
// Get a free loop device number
|
|
ctrl, err := os.OpenFile("/dev/loop-control", os.O_RDWR, 0)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open /dev/loop-control: %w", err)
|
|
}
|
|
defer ctrl.Close()
|
|
loopNum, _, errno := unix.Syscall(unix.SYS_IOCTL, ctrl.Fd(), unix.LOOP_CTL_GET_FREE, 0)
|
|
if errno != 0 {
|
|
return "", fmt.Errorf("LOOP_CTL_GET_FREE: %v", errno)
|
|
}
|
|
|
|
// Open the loop device
|
|
loopDev := fmt.Sprintf("/dev/loop%d", loopNum)
|
|
loop, err := os.OpenFile(loopDev, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open loop device: %w", err)
|
|
}
|
|
defer loop.Close()
|
|
|
|
// Associate the file with the loop device
|
|
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, loop.Fd(), unix.LOOP_SET_FD, file.Fd()); errno != 0 {
|
|
return "", fmt.Errorf("LOOP_SET_FD: %v", errno)
|
|
}
|
|
|
|
return loopDev, nil
|
|
}
|
|
|
|
// detachLoopbackDevice removes the specified loopback device.
|
|
func detachLoopbackDevice(loopDev string) error {
|
|
loop, err := os.OpenFile(loopDev, os.O_RDWR, 0)
|
|
if err != nil {
|
|
return fmt.Errorf("open loop device: %w", err)
|
|
}
|
|
defer loop.Close()
|
|
|
|
if _, _, errno := unix.Syscall(unix.SYS_IOCTL, loop.Fd(), unix.LOOP_CLR_FD, 0); errno != 0 {
|
|
return fmt.Errorf("LOOP_CLR_FD: %v", errno)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type cryptsetupMetadata struct {
|
|
KeySlots map[string]struct {
|
|
Type string `json:"type"`
|
|
KeySize int `json:"key_size"`
|
|
AntiForensicSplitter struct {
|
|
Type string `json:"type"`
|
|
Stripes int `json:"stripes"`
|
|
Hash string `json:"hash"`
|
|
} `json:"af"`
|
|
Area struct {
|
|
Type string `json:"type"`
|
|
Offset string `json:"offset"`
|
|
Size string `json:"size"`
|
|
Encryption string `json:"encryption"`
|
|
KeySize int `json:"key_size"`
|
|
} `json:"area"`
|
|
KDF struct {
|
|
Type string `json:"type"`
|
|
Time int `json:"time"`
|
|
Memory int `json:"memory"`
|
|
CPUs int `json:"cpus"`
|
|
Salt string `json:"salt"`
|
|
} `json:"kdf"`
|
|
} `json:"keyslots"`
|
|
Tokens map[string]any `json:"tokens"`
|
|
Segments map[string]struct {
|
|
Type string `json:"type"`
|
|
Offset string `json:"offset"`
|
|
Size string `json:"size"`
|
|
Flags []string `json:"flags,omitempty"`
|
|
IVTweak string `json:"iv_tweak"`
|
|
Encryption string `json:"encryption"`
|
|
SectorSize int `json:"sector_size"`
|
|
Integrity struct {
|
|
Type string `json:"type"`
|
|
JournalEncryption string `json:"journal_encryption"`
|
|
JournalIntegrity string `json:"journal_integrity"`
|
|
}
|
|
} `json:"segments"`
|
|
Digests map[string]struct {
|
|
Type string `json:"type"`
|
|
Keyslots []string `json:"keyslots"`
|
|
Segments []string `json:"segments"`
|
|
Hash string `json:"hash"`
|
|
Iterations int `json:"iterations"`
|
|
Salt string `json:"salt"`
|
|
Digest string `json:"digest"`
|
|
} `json:"digests"`
|
|
Config struct {
|
|
JSONSize string `json:"json_size"`
|
|
KeyslotsSize string `json:"keyslots_size"`
|
|
}
|
|
}
|
|
|
|
type cgoFormatter interface {
|
|
Format(deviceType cryptsetup.DeviceType, genericParams cryptsetup.GenericParams) error
|
|
}
|
|
|
|
type cgoLoader interface {
|
|
Load(deviceType cryptsetup.DeviceType) error
|
|
}
|
|
|
|
type cgoRestorer interface {
|
|
HeaderRestore(deviceType cryptsetup.DeviceType, headerFile string) error
|
|
}
|
|
type cgoBackuper interface {
|
|
HeaderBackup(deviceType cryptsetup.DeviceType, headerFile string) error
|
|
}
|