implement cryptsetup wrapper to change disk passphrase of constellation state disk

Signed-off-by: Malte Poll <mp@edgeless.systems>
This commit is contained in:
Malte Poll 2022-04-20 11:35:23 +02:00 committed by Malte Poll
parent 98aced1b36
commit bb56b46e21
2 changed files with 315 additions and 0 deletions

View File

@ -0,0 +1,119 @@
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("unable to initialize 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("unable to change 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("unable to read 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)

View File

@ -0,0 +1,196 @@
package diskencryption
import (
"errors"
"path"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestOpenClose(t *testing.T) {
testCases := map[string]struct {
initByNameErr error
operations []string
errExpected bool
}{
"open and close work": {
operations: []string{"open", "close"},
},
"opening twice fails": {
operations: []string{"open", "open"},
errExpected: true,
},
"closing first fails": {
operations: []string{"close"},
errExpected: true,
},
"initByName failure detected": {
initByNameErr: errors.New("initByNameErr"),
operations: []string{"open"},
errExpected: 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.errExpected {
assert.Error(err)
return
}
require.NoError(err)
})
}
}
func TestUUID(t *testing.T) {
testCases := map[string]struct {
open bool
expectedUUID string
errExpected bool
}{
"getting uuid works": {
open: true,
expectedUUID: "uuid",
},
"getting uuid on closed device fails": {
errExpected: true,
},
"empty uuid is detected": {
open: true,
errExpected: 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.expectedUUID}, nil
},
}
if tc.open {
require.NoError(crypt.Open())
defer crypt.Close()
}
uuid, err := crypt.UUID()
if tc.errExpected {
assert.Error(err)
return
}
require.NoError(err)
assert.Equal(tc.expectedUUID, uuid)
})
}
}
func TestUpdatePassphrase(t *testing.T) {
testCases := map[string]struct {
writePassphrase bool
open bool
keyslotChangeByPassphraseErr error
errExpected bool
}{
"updating passphrase works": {
writePassphrase: true,
open: true,
},
"updating passphrase on closed device fails": {
errExpected: true,
},
"reading initial passphrase can fail": {
open: true,
errExpected: true,
},
"changing keyslot passphrase can fail": {
open: true,
writePassphrase: true,
keyslotChangeByPassphraseErr: errors.New("keyslotChangeByPassphraseErr"),
errExpected: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
fs := afero.NewMemMapFs()
if tc.writePassphrase {
require.NoError(fs.MkdirAll(path.Base(initialKeyPath), 0o777))
require.NoError(afero.WriteFile(fs, initialKeyPath, []byte("key"), 0o777))
}
crypt := Cryptsetup{
fs: fs,
initByName: func(name string) (cryptdevice, error) {
return &stubCryptdevice{keyslotChangeErr: tc.keyslotChangeByPassphraseErr}, nil
},
}
if tc.open {
require.NoError(crypt.Open())
defer crypt.Close()
}
err := crypt.UpdatePassphrase("new-key")
if tc.errExpected {
assert.Error(err)
return
}
require.NoError(err)
})
}
}
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 {
uuid string
keyslotChangeErr error
}
func (s *stubCryptdevice) GetUUID() string {
return s.uuid
}
func (s *stubCryptdevice) KeyslotChangeByPassphrase(currentKeyslot int, newKeyslot int, currentPassphrase string, newPassphrase string) error {
return s.keyslotChangeErr
}
func (s *stubCryptdevice) Free() bool {
return false
}