disk-mapper: set LUKS2 token to allow reusing unintialized state disks (#2083)

Signed-off-by: Daniel Weiße <dw@edgeless.systems>
This commit is contained in:
Daniel Weiße 2023-07-18 16:20:03 +02:00 committed by GitHub
parent dc373971b2
commit 6a40c73ff7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 161 additions and 31 deletions

View file

@ -3569,8 +3569,9 @@ def go_dependencies():
patches = [ patches = [
"//3rdparty/bazel/com_github_martinjungblut_go_cryptsetup:com_github_martinjungblut_go_cryptsetup.patch", # keep "//3rdparty/bazel/com_github_martinjungblut_go_cryptsetup:com_github_martinjungblut_go_cryptsetup.patch", # keep
], ],
sum = "h1:YDjLk3wsL5ZLhLC4TIwIvT2NkSCAdAV6pzzZaRfj4jk=", replace = "github.com/daniel-weisse/go-cryptsetup",
version = "v0.0.0-20220520180014-fd0874fd07a6", sum = "h1:ToajP6trZoiqlZ3Z4uoG1P02/wtqSw1AcowOXOYjATk=",
version = "v0.0.0-20230705150314-d8c07bd1723c",
) )
go_repository( go_repository(
name = "com_github_masterminds_goutils", name = "com_github_masterminds_goutils",

View file

@ -52,7 +52,12 @@ func (c *DiskEncryption) UpdatePassphrase(passphrase string) error {
if err != nil { if err != nil {
return err return err
} }
return c.device.KeyslotChangeByPassphrase(keyslot, keyslot, initialPassphrase, passphrase) if err := c.device.KeyslotChangeByPassphrase(keyslot, keyslot, initialPassphrase, passphrase); err != nil {
return err
}
// Set token as initialized.
return c.device.SetConstellationStateDiskToken(cryptsetup.SetDiskInitialized)
} }
// getInitialPassphrase retrieves the initial passphrase used on first boot. // getInitialPassphrase retrieves the initial passphrase used on first boot.
@ -68,4 +73,5 @@ type cryptdevice interface {
InitByName(name string) (func(), error) InitByName(name string) (func(), error)
GetUUID() (string, error) GetUUID() (string, error)
KeyslotChangeByPassphrase(currentKeyslot int, newKeyslot int, currentPassphrase string, newPassphrase string) error KeyslotChangeByPassphrase(currentKeyslot int, newKeyslot int, currentPassphrase string, newPassphrase string) error
SetConstellationStateDiskToken(bool) error
} }

View file

@ -90,6 +90,6 @@ func (s *stubCryptdevice) KeyslotChangeByPassphrase(_, _ int, _, _ string) error
return s.keyslotChangeErr return s.keyslotChangeErr
} }
func (s *stubCryptdevice) Close() error { func (s *stubCryptdevice) SetConstellationStateDiskToken(bool) error {
return nil return nil
} }

View file

@ -147,7 +147,7 @@ func main() {
} }
// prepare the state disk // prepare the state disk
if mapper.IsLUKSDevice() { if mapper.IsInitialized() {
// set up rejoin client // set up rejoin client
var self metadata.InstanceMetadata var self metadata.InstanceMetadata
self, err = metadataClient.Self(context.Background()) self, err = metadataClient.Self(context.Background())

View file

@ -24,23 +24,30 @@ import (
// DiskEncryption handles actions for formatting and mapping crypt devices. // DiskEncryption handles actions for formatting and mapping crypt devices.
type DiskEncryption struct { type DiskEncryption struct {
device cryptDevice device cryptDevice
log *logger.Logger devicePath string
log *logger.Logger
} }
// New creates a new crypt device for the device at path. // New creates a new crypt device for the device at path.
func New(path string, log *logger.Logger) (*DiskEncryption, func(), error) { func New(path string, log *logger.Logger) (*DiskEncryption, func(), error) {
device := cryptsetup.New() device := cryptsetup.New()
free, err := device.Init(path) _, err := device.Init(path)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("initializing crypt device for disk %q: %w", path, err) return nil, nil, fmt.Errorf("initializing crypt device for disk %q: %w", path, err)
} }
return &DiskEncryption{device: device, log: log}, free, nil d := &DiskEncryption{device: device, devicePath: path, log: log}
return d, d.free, nil
} }
// IsLUKSDevice returns true if the device is formatted as a LUKS device. // IsInitialized returns true if the device is formatted as a LUKS device,
func (d *DiskEncryption) IsLUKSDevice() bool { // and has been successfully initialized before (successfully joined a cluster).
return d.device.LoadLUKS2() == nil func (d *DiskEncryption) IsInitialized() bool {
if err := d.device.LoadLUKS2(); err != nil {
return false
}
return d.device.ConstellationStateDiskTokenIsInitialized()
} }
// DiskUUID gets the device's UUID. // DiskUUID gets the device's UUID.
@ -50,6 +57,14 @@ func (d *DiskEncryption) DiskUUID() (string, error) {
// FormatDisk formats the disk and adds passphrase in keyslot 0. // FormatDisk formats the disk and adds passphrase in keyslot 0.
func (d *DiskEncryption) FormatDisk(passphrase string) error { func (d *DiskEncryption) FormatDisk(passphrase string) error {
// Successfully calling LoadLUKS2() before FormatDisk() will cause format to fail.
// To make sure format is idempotent, we need to run it on a freshly initialized device.
// Therefore we free the device and reinitialize it.
d.free()
if _, err := d.device.Init(d.devicePath); err != nil {
return fmt.Errorf("re-initializing crypt device for disk %q: %w", d.devicePath, err)
}
if err := d.device.Format(cryptsetup.FormatIntegrity); err != nil { if err := d.device.Format(cryptsetup.FormatIntegrity); err != nil {
return fmt.Errorf("formatting disk: %w", err) return fmt.Errorf("formatting disk: %w", err)
} }
@ -63,6 +78,9 @@ func (d *DiskEncryption) FormatDisk(passphrase string) error {
return fmt.Errorf("wiping disk: %w", err) return fmt.Errorf("wiping disk: %w", err)
} }
if err := d.device.SetConstellationStateDiskToken(cryptsetup.SetDiskNotInitialized); err != nil {
return fmt.Errorf("setting disk token: %w", err)
}
return nil return nil
} }
@ -96,13 +114,21 @@ func (d *DiskEncryption) Wipe(blockWipeSize int) error {
return nil return nil
} }
func (d *DiskEncryption) free() {
d.device.Free()
}
type cryptDevice interface { type cryptDevice interface {
ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error
ActivateByVolumeKey(deviceName string, volumeKey string, volumeKeySize int, flags int) error ActivateByVolumeKey(deviceName string, volumeKey string, volumeKeySize int, flags int) error
Deactivate(deviceName string) error Deactivate(deviceName string) error
Format(integrity bool) error Format(integrity bool) error
Free()
GetUUID() (string, error) GetUUID() (string, error)
Init(path string) (func(), error)
LoadLUKS2() error LoadLUKS2() error
KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error
SetConstellationStateDiskToken(diskIsInitialized bool) error
ConstellationStateDiskTokenIsInitialized() bool
Wipe(name string, wipeBlockSize int, flags int, logCallback func(size, offset uint64), logFrequency time.Duration) error Wipe(name string, wipeBlockSize int, flags int, logCallback func(size, offset uint64), logFrequency time.Duration) error
} }

View file

@ -413,6 +413,10 @@ func (s *stubMapper) UnmapDisk(string) error {
return s.unmapDiskErr return s.unmapDiskErr
} }
func (s *stubMapper) SetDiskToInitialized() error {
return nil
}
type stubMounter struct { type stubMounter struct {
mountCalled bool mountCalled bool
mountErr error mountErr error

View file

@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
package integration package integration
import ( import (
"encoding/json"
"flag" "flag"
"fmt" "fmt"
"os" "os"
@ -16,8 +17,9 @@ import (
"testing" "testing"
"github.com/edgelesssys/constellation/v2/disk-mapper/internal/diskencryption" "github.com/edgelesssys/constellation/v2/disk-mapper/internal/diskencryption"
ccryptsetup "github.com/edgelesssys/constellation/v2/internal/cryptsetup"
"github.com/edgelesssys/constellation/v2/internal/logger" "github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/martinjungblut/go-cryptsetup" cryptsetup "github.com/martinjungblut/go-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"
@ -68,7 +70,7 @@ func TestMapper(t *testing.T) {
require.NoError(err, "failed to initialize crypt device") require.NoError(err, "failed to initialize crypt device")
defer free() defer free()
assert.False(mapper.IsLUKSDevice()) assert.False(mapper.IsInitialized())
// Format and map disk // Format and map disk
passphrase := "unit-test" passphrase := "unit-test"
@ -76,23 +78,33 @@ func TestMapper(t *testing.T) {
require.NoError(mapper.MapDisk(mappedDevice, passphrase), "failed to map disk") require.NoError(mapper.MapDisk(mappedDevice, passphrase), "failed to map disk")
require.NoError(mapper.UnmapDisk(mappedDevice), "failed to remove disk mapping") require.NoError(mapper.UnmapDisk(mappedDevice), "failed to remove disk mapping")
assert.True(mapper.IsLUKSDevice()) // Make sure token was set
ccrypt := ccryptsetup.New()
freeDevice, err := ccrypt.Init(devicePath)
require.NoError(err, "failed to initialize crypt device")
defer freeDevice()
require.NoError(ccrypt.LoadLUKS2(), "failed to load LUKS2")
tokenJSON, err := ccrypt.TokenJSONGet(ccryptsetup.ConstellationStateDiskTokenID)
require.NoError(err, "token should have been set")
var token struct {
Type string `json:"type"`
Keyslots []string `json:"keyslots"`
DiskIsInitialized bool `json:"diskIsInitialized"`
}
require.NoError(json.Unmarshal([]byte(tokenJSON), &token))
assert.False(token.DiskIsInitialized, "disk should be marked as not initialized")
assert.False(ccrypt.ConstellationStateDiskTokenIsInitialized(), "disk should be marked as not initialized")
// Disk should still be marked as not initialized because token is set to false.
assert.False(mapper.IsInitialized())
// Try to map disk with incorrect passphrase // Try to map disk with incorrect passphrase
assert.Error(mapper.MapDisk(mappedDevice, "invalid-passphrase"), "was able to map disk with incorrect passphrase") assert.Error(mapper.MapDisk(mappedDevice, "invalid-passphrase"), "was able to map disk with incorrect passphrase")
}
/* // Disk can be reformatted without manually re-initializing a mapper
type fakeMetadataAPI struct{} passphrase2 := passphrase + "2"
require.NoError(mapper.FormatDisk(passphrase2), "failed to format disk")
func (f *fakeMetadataAPI) List(ctx context.Context) ([]metadata.InstanceMetadata, error) { require.NoError(mapper.MapDisk(mappedDevice, passphrase2), "failed to map disk")
return []metadata.InstanceMetadata{ require.NoError(mapper.UnmapDisk(mappedDevice), "failed to remove disk mapping")
{
Name: "instanceName",
ProviderID: "fake://instance-id",
Role: role.Unknown,
VPCIP: "192.0.2.1",
},
}, nil
} }
*/

1
go.mod
View file

@ -34,6 +34,7 @@ replace (
replace ( replace (
github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api => ./operators/constellation-node-operator/api github.com/edgelesssys/constellation/v2/operators/constellation-node-operator/v2/api => ./operators/constellation-node-operator/api
github.com/google/go-tpm => github.com/thomasten/go-tpm v0.0.0-20230629092004-f43f8e2a59eb github.com/google/go-tpm => github.com/thomasten/go-tpm v0.0.0-20230629092004-f43f8e2a59eb
github.com/martinjungblut/go-cryptsetup => github.com/daniel-weisse/go-cryptsetup v0.0.0-20230705150314-d8c07bd1723c
) )
require ( require (

4
go.sum
View file

@ -279,6 +279,8 @@ github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7 h
github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyberphone/json-canonicalization v0.0.0-20220623050100-57a0ce2678a7/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw=
github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI= github.com/cyphar/filepath-securejoin v0.2.3 h1:YX6ebbZCZP7VkM3scTTokDgBL2TY741X51MTk3ycuNI=
github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4=
github.com/daniel-weisse/go-cryptsetup v0.0.0-20230705150314-d8c07bd1723c h1:ToajP6trZoiqlZ3Z4uoG1P02/wtqSw1AcowOXOYjATk=
github.com/daniel-weisse/go-cryptsetup v0.0.0-20230705150314-d8c07bd1723c/go.mod h1:gZoZ0+POlM1ge/VUxWpMmZVNPzzMJ7l436CgkQ5+qzU=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -756,8 +758,6 @@ github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY
github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI=
github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI=
github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0=
github.com/martinjungblut/go-cryptsetup v0.0.0-20220520180014-fd0874fd07a6 h1:YDjLk3wsL5ZLhLC4TIwIvT2NkSCAdAV6pzzZaRfj4jk=
github.com/martinjungblut/go-cryptsetup v0.0.0-20220520180014-fd0874fd07a6/go.mod h1:gZoZ0+POlM1ge/VUxWpMmZVNPzzMJ7l436CgkQ5+qzU=
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=

View file

@ -14,6 +14,7 @@ There should only be one instance using this package per process.
package cryptsetup package cryptsetup
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"strings" "strings"
@ -22,6 +23,13 @@ import (
) )
const ( const (
// ConstellationStateDiskTokenID is the ID of Constellation's state disk token.
ConstellationStateDiskTokenID = 0
// SetDiskInitialized is a flag to set the Constellation state disk token to initialized.
SetDiskInitialized = true
// SetDiskNotInitialized is a flag to set the Constellation state disk token to not initialized.
SetDiskNotInitialized = false
// FormatIntegrity is a flag to enable dm-integrity for a crypt device when formatting. // FormatIntegrity is a flag to enable dm-integrity for a crypt device when formatting.
FormatIntegrity = true FormatIntegrity = true
// FormatNoIntegrity is a flag to disable dm-integrity for a crypt device when formatting. // FormatNoIntegrity is a flag to disable dm-integrity for a crypt device when formatting.
@ -220,6 +228,70 @@ func (c *CryptSetup) Resize(name string, newSize uint64) error {
return nil return nil
} }
// TokenJSONGet gets the JSON data for a token.
func (c *CryptSetup) TokenJSONGet(token int) (string, error) {
packageLock.Lock()
defer packageLock.Unlock()
if c.device == nil {
return "", errDeviceNotOpen
}
json, err := c.device.TokenJSONGet(token)
if err != nil {
return "", fmt.Errorf("getting JSON data for token %d: %w", token, err)
}
return json, nil
}
// TokenJSONSet sets the JSON data for a token.
// The JSON data must be a valid LUKS2 token.
// Required fields are:
// - type [string] the token type (tokens with luks2- prefix are reserved)
// - keyslots [array] the array of keyslot objects names that are assigned to the token
//
// Returns the allocated token ID on success.
func (c *CryptSetup) TokenJSONSet(token int, json string) (int, error) {
packageLock.Lock()
defer packageLock.Unlock()
if c.device == nil {
return -1, errDeviceNotOpen
}
tokenID, err := c.device.TokenJSONSet(token, json)
if err != nil {
return -1, fmt.Errorf("setting JSON data for token %d: %w", token, err)
}
return tokenID, nil
}
// SetConstellationStateDiskToken sets the Constellation state disk token.
func (c *CryptSetup) SetConstellationStateDiskToken(diskIsInitialized bool) error {
token := constellationLUKS2Token{
Type: "constellation-state-disk",
Keyslots: []string{},
DiskIsInitialized: diskIsInitialized,
}
json, err := json.Marshal(token)
if err != nil {
return fmt.Errorf("marshaling token: %w", err)
}
if _, err := c.device.TokenJSONSet(ConstellationStateDiskTokenID, string(json)); err != nil {
return fmt.Errorf("setting token: %w", err)
}
return nil
}
// ConstellationStateDiskTokenIsInitialized returns true if the Constellation state disk token is set to initialized.
func (c *CryptSetup) ConstellationStateDiskTokenIsInitialized() bool {
stateDiskToken, err := c.device.TokenJSONGet(ConstellationStateDiskTokenID)
if err != nil {
return false
}
var token constellationLUKS2Token
if err := json.Unmarshal([]byte(stateDiskToken), &token); err != nil {
return false
}
return token.DiskIsInitialized
}
// Wipe overwrites the device with zeros to initialize integrity checksums. // Wipe overwrites the device with zeros to initialize integrity checksums.
func (c *CryptSetup) Wipe( func (c *CryptSetup) Wipe(
name string, blockWipeSize int, flags int, logCallback func(size, offset uint64), logFrequency time.Duration, name string, blockWipeSize int, flags int, logCallback func(size, offset uint64), logFrequency time.Duration,
@ -264,6 +336,12 @@ func (c *CryptSetup) Wipe(
return nil return nil
} }
type constellationLUKS2Token struct {
Type string `json:"type"`
Keyslots []string `json:"keyslots"`
DiskIsInitialized bool `json:"diskIsInitialized"`
}
type cryptDevice interface { type cryptDevice interface {
ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error ActivateByPassphrase(deviceName string, keyslot int, passphrase string, flags int) error
ActivateByVolumeKey(deviceName, volumeKey string, volumeKeySize, flags int) error ActivateByVolumeKey(deviceName, volumeKey string, volumeKeySize, flags int) error
@ -274,5 +352,7 @@ type cryptDevice interface {
KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error KeyslotAddByVolumeKey(keyslot int, volumeKey string, passphrase string) error
KeyslotChangeByPassphrase(currentKeyslot, newKeyslot int, currentPassphrase, newPassphrase string) error KeyslotChangeByPassphrase(currentKeyslot, newKeyslot int, currentPassphrase, newPassphrase string) error
Resize(name string, newSize uint64) error Resize(name string, newSize uint64) error
TokenJSONGet(token int) (string, error)
TokenJSONSet(token int, json string) (int, error)
Wipe(devicePath string, pattern int, offset, length uint64, wipeBlockSize int, flags int, progress func(size, offset uint64) int) error Wipe(devicePath string, pattern int, offset, length uint64, wipeBlockSize int, flags int, progress func(size, offset uint64) int) error
} }