mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04: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
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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(
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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)
|
|
@ -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")
|
|
||||||
}
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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.
|
||||||
|
@ -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) {
|
||||||
|
@ -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.
|
||||||
|
@ -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) {
|
||||||
|
@ -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(
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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{}
|
||||||
|
@ -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",
|
||||||
|
@ -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
|
||||||
|
@ -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",
|
||||||
|
],
|
||||||
|
)
|
108
disk-mapper/internal/diskencryption/diskencryption.go
Normal file
108
disk-mapper/internal/diskencryption/diskencryption.go
Normal 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
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -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
|
|
@ -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
|
|
||||||
}
|
|
@ -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")
|
|
||||||
}
|
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
|
@ -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())
|
||||||
|
|
||||||
|
@ -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": [],
|
||||||
|
}),
|
||||||
)
|
)
|
||||||
|
@ -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
|
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
|
||||||
|
}
|
||||||
|
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…
Reference in New Issue
Block a user