Rename coordinator to bootstrapper and rename roles

This commit is contained in:
katexochen 2022-06-29 15:26:29 +02:00 committed by Paul Meyer
parent 3280ed200c
commit 916e5d6b55
191 changed files with 1763 additions and 2030 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("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)

View file

@ -0,0 +1,201 @@
package diskencryption
import (
"errors"
"path"
"testing"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/goleak"
)
func TestMain(m *testing.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) {
testCases := map[string]struct {
writePassphrase bool
open bool
keyslotChangeByPassphraseErr error
wantErr bool
}{
"updating passphrase works": {
writePassphrase: true,
open: true,
},
"updating passphrase on closed device fails": {
wantErr: true,
},
"reading initial passphrase can fail": {
open: true,
wantErr: true,
},
"changing keyslot passphrase can fail": {
open: true,
writePassphrase: true,
keyslotChangeByPassphraseErr: errors.New("keyslotChangeByPassphraseErr"),
wantErr: 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.wantErr {
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
}