diff --git a/README.md b/README.md index b2add27..c3cbfd2 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,85 @@ nullboot is a boot manager for environments that do not need a boot manager. Instead of running a boot manager at boot, it directly manages the UEFI boot entries for you. +Use cases +--------- + +Usage: +``` +1. nullbootctl [install] [OPTIONS] +2. nullbootctl recovery-key [OPTIONS] +``` + +### nullbootctl [install] + +Install shim and UKI in EFI System Partition. + +More specifically: +- Install shim and UKI (Unified Kernel Image) to the EFI System Partition (ESP, + ie: /boot/efi) +- Update the TPM sealing policy of the LUKS passphrase +- Set EFI variable BootNext to point to the newly installed shim (and kernel?) (TODO) + +It is intended to be called upon shim and UKI update. + +See `nullbootctl install -h` for options: +``` +Usage of nullbootctl install: + -no-boot-next + Disables use of BootNext. This flag must be disabled in order to upgrade to a new kernel version. + -no-efivars + Do not use or update the EFI variables. Disables kernel fallback mechanism + -no-tpm + Do not do any resealing with the TPM + -output-json string + JSON file to write. Disables writing real EFI variables and enablement of the kernel fallback mechanism +``` + + +### nullbootctl recovery-key + +Manage recovery keys (LUKS passphrases) of a LUKS container. + +This shall setup, list, delete and verify recovery keys for a LUKS container. + +See `nullbootctl recovery-key -h` for options: +``` +Usage: +nullbootctl recovery-key --create [--device ] [--name ] + --delete [--device ] [--name ] + --list [--device ] + --verify [--device ] + +Options: + LUKS2 device + Name of the recovery key +``` + +Example: +``` +# nullbootctl recovery-key --create --name R1 +Creating recovery key 'recovery-0001' in '/dev/disk/by-label/cloudimg-rootfs-enc' +38785-16377-60025-09490-45202-37955-36799-29994 + +# nullbootctl recovery-key --create --name R2 +Creating recovery key 'R2' in '/dev/disk/by-label/cloudimg-rootfs-enc' +43392-00597-12755-38614-06919-19535-19552-05646 + +# nullbootctl recovery-key --list +Listing recovery keys in '/dev/disk/by-label/cloudimg-rootfs-enc' +R1 +R2 + +# echo 38785-16377-60025-09490-45202-37955-36799-29994 | nullbootctl recovery-key --verify +success +``` + +Build +----- +``` +$ go build -o . ./... +``` + Licensing --------- This program is free software: you can redistribute it and/or modify it under diff --git a/cmd/nullbootctl/main.go b/cmd/nullbootctl/main.go index 80342ce..1db15fd 100644 --- a/cmd/nullbootctl/main.go +++ b/cmd/nullbootctl/main.go @@ -4,17 +4,127 @@ package main -import "golang.org/x/sys/unix" -import "github.com/canonical/nullboot/efibootmgr" -import "flag" -import "log" -import "os" -import "syscall" - -var noTPM = flag.Bool("no-tpm", false, "Do not do any resealing with the TPM") -var noEfivars = flag.Bool("no-efivars", false, "Do not use or update the EFI variables. Disables kernel fallback mechanism") -var outputJSON = flag.String("output-json", "", "JSON file to write. Disables writing real EFI variables and enablement of the kernel fallback mechanism") -var noBootNext = flag.Bool("no-boot-next", false, "Disables use of BootNext. This flag must be disabled in order to upgrade to a new kernel version.") +import ( + "flag" + "fmt" + "github.com/canonical/nullboot/efibootmgr" + "github.com/canonical/nullboot/luks2" + "golang.org/x/sys/unix" + "log" + "os" + "syscall" +) + +const Usage = `usage: + +1. nullbootctl [install] +2. nullbootctl recovery-key + +Commands: + install Install boot artifacts + recovery-key Manage disk encryption recovery keys + +See 'nullbootctl -h' for more help. +` + +const UsageRecoveryKey = `Usage: +nullbootctl recovery-key --create [--device ] [--name ] + --delete [--device ] [--name ] + --list [--device ] + --verify [--device ] + +Options: + LUKS2 device + Name of the recovery key +` + +func main() { + + if len(os.Args) > 1 { + if os.Args[1] == "recovery-key" { + os.Args = os.Args[1:] // Strip the first item + os.Args[0] = "nullbootctl recovery-key" + cmd_recovery_key() + return + } + if os.Args[1] == "-h" || os.Args[1] == "--help" { + fmt.Print(Usage) + return + } + if os.Args[1] == "install" { + os.Args = os.Args[1:] // Strip this command (it is the default) + } + if len(os.Args[1]) == 0 || os.Args[1][0] != '-' { + fmt.Fprintf(os.Stderr, "nullbootctl: '%v' is not a valid command. See 'nullbootctl --help'.\n", os.Args[1]) + os.Exit(1) + } + } + + // Default "install" command + os.Args[0] = "nullbootctl install" + cmd_default() +} + +const ( + default_cloudimg_encrypted_device = "/dev/disk/by-label/" + efibootmgr.RootfsLabel + default_recovery_name = "recovery-0001" +) + +func usageRecoveryKey() { + fmt.Print(UsageRecoveryKey) +} + +func cmd_recovery_key() { + + var devicePath string + var recoveryName string + doCreate := flag.Bool("create", false, "Create and set a recovery key") + doList := flag.Bool("list", false, "List recovery keys") + doDelete := flag.Bool("delete", false, "Delete a recovery key") + doVerify := flag.Bool("verify", false, "Verify a recovery key") + flag.StringVar(&devicePath, "device", default_cloudimg_encrypted_device, "Device of the encrypted volume") + flag.StringVar(&recoveryName, "name", default_recovery_name, "Name of the recovery key") + flag.Usage = usageRecoveryKey + flag.Parse() + + mutuallyExclusiveOptsCount := 0 + if *doCreate { + mutuallyExclusiveOptsCount++ + } + if *doDelete { + mutuallyExclusiveOptsCount++ + } + if *doList { + mutuallyExclusiveOptsCount++ + } + if *doVerify { + mutuallyExclusiveOptsCount++ + } + + if mutuallyExclusiveOptsCount > 1 { + fmt.Fprintf(os.Stderr, "Options cannot be used together: --create, --list, --delete, --verify\n") + os.Exit(1) + } + + var err error + if *doList { + err = luks2.ListRecoveryKeys(devicePath) + } else if *doDelete { + err = luks2.DeleteRecoveryKey(devicePath, recoveryName) + } else if *doCreate { + err = luks2.CreateRecoveryKey(devicePath, recoveryName) + } else if *doVerify { + err = luks2.VerifyRecoveryKey(devicePath) + } else { + fmt.Fprintf(os.Stderr, "Please select at least one action: --create, --list, --delete, --verify\n") + os.Exit(1) + } + + if err != nil { + fmt.Fprintf(os.Stderr, "%s\n", err.Error()) + os.Exit(1) + } +} func syncDirectory(dir string) error { dirFd, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0755) @@ -25,9 +135,15 @@ func syncDirectory(dir string) error { return err } -func main() { +func cmd_default() { var assets *efibootmgr.TrustedAssets var err error + + noTPM := flag.Bool("no-tpm", false, "Do not do any resealing with the TPM") + noEfivars := flag.Bool("no-efivars", false, "Do not use or update the EFI variables. Disables kernel fallback mechanism") + outputJSON := flag.String("output-json", "", "JSON file to write. Disables writing real EFI variables and enablement of the kernel fallback mechanism") + noBootNext := flag.Bool("no-boot-next", false, "Disables use of BootNext. This flag must be disabled in order to upgrade to a new kernel version.") + flag.Parse() const ( diff --git a/efibootmgr/efivars.go b/efibootmgr/efivars.go index c3d6afe..e5ded38 100644 --- a/efibootmgr/efivars.go +++ b/efibootmgr/efivars.go @@ -24,7 +24,7 @@ type EFIVariables interface { } // RealEFIVariables provides the real implementation of efivars -type RealEFIVariables struct {} +type RealEFIVariables struct{} // ListVariables proxy func (vars RealEFIVariables) ListVariables() ([]efi.VariableDescriptor, error) { diff --git a/efibootmgr/reseal.go b/efibootmgr/reseal.go index dbe9223..df246c8 100644 --- a/efibootmgr/reseal.go +++ b/efibootmgr/reseal.go @@ -30,7 +30,7 @@ import ( const ( keyFilePath = "device/fde/cloudimg-rootfs.sealed-key" keyringPrefix = "ubuntu-fde" - rootfsLabel = "cloudimg-rootfs-enc" + RootfsLabel = "cloudimg-rootfs-enc" ) var ( @@ -111,7 +111,7 @@ func resolveLink(path string) (string, error) { } func getPrimaryKeyFromKernel() (secboot.PrimaryKey, error) { - devPath, err := resolveLink(filepath.Join("/dev/disk/by-label", rootfsLabel)) + devPath, err := resolveLink(filepath.Join("/dev/disk/by-label", RootfsLabel)) if err != nil { return nil, fmt.Errorf("cannot resolve devive symlink: %w", err) } diff --git a/luks2/luks2.go b/luks2/luks2.go new file mode 100644 index 0000000..f7c4d65 --- /dev/null +++ b/luks2/luks2.go @@ -0,0 +1,109 @@ +// This file is part of nullboot +// Copyright 2026 Canonical Ltd. +// SPDX-License-Identifier: GPL-3.0-only + +package luks2 + +import ( + "bufio" + "crypto/rand" + "fmt" + efi "github.com/canonical/go-efilib" + "github.com/snapcore/secboot" + _ "github.com/snapcore/secboot/luks2" // This gets the LUKS2 backend initialized + "io" + "os" + "strings" +) + +var ( + sbAddLUKS2ContainerRecoveryKey = secboot.AddLUKS2ContainerRecoveryKey + sbDeleteLUKS2ContainerKey = secboot.DeleteLUKS2ContainerKey + sbFindStorageContainer = secboot.FindStorageContainer + sbGetKeyFromKernel = secboot.GetKeyFromKernel + sbListLUKS2ContainerRecoveryKeyNames = secboot.ListLUKS2ContainerRecoveryKeyNames + sbParseRecoveryKey = secboot.ParseRecoveryKey + sbTestLUKS2ContainerKey = secboot.TestLUKS2ContainerKey + + stdin io.Reader = os.Stdin + stdout io.Writer = os.Stdout + stderr io.Writer = os.Stderr +) + +// CreateRecoveryKey creates a new recovery key for a device and prints +// it to stdout. +func CreateRecoveryKey(devicePath string, recoveryName string) error { + fmt.Fprintf(stderr, "Creating recovery key '%v' in '%v'\n", recoveryName, devicePath) + container, err := sbFindStorageContainer(efi.DefaultVarContext, devicePath) + if err != nil { + return fmt.Errorf("Cannot find storage (LUKS) container: %w", err) + } + purpose := secboot.KeyringKeyPurposeUnlock + // prefix "" defaults to "ubuntu-fde" + diskUnlockKey, err := sbGetKeyFromKernel(efi.DefaultVarContext, container, purpose, "") + if err != nil { + return fmt.Errorf("Cannot get disk unlock key from kernel: %w", err) + } + + recoveryKey := secboot.RecoveryKey{} // generate random from crypto rand package + rand.Read(recoveryKey[:]) + + keyslotName := recoveryName // anything, stored in the token + err = sbAddLUKS2ContainerRecoveryKey(devicePath, keyslotName, diskUnlockKey, recoveryKey) + if err != nil { + return fmt.Errorf("Cannot add recovery key to LUKS container: %w", err) + } + + fmt.Fprintf(stdout, "%s\n", recoveryKey.String()) + + return nil +} + +// VerifyRecoveryKey reads a recovery from stdin and verifies if it can unlock a device. +func VerifyRecoveryKey(devicePath string) error { + reader := bufio.NewReader(stdin) + fmt.Print("Enter recovery key: ") + recoveryKeyString, err := reader.ReadString('\n') + if err != nil && err != io.EOF { + return err + } + recoveryKeyString = strings.TrimSpace(recoveryKeyString) + + key, err := sbParseRecoveryKey(recoveryKeyString) + if err != nil { + return err + } + + ok := sbTestLUKS2ContainerKey(devicePath, key[:]) + if !ok { + fmt.Fprintf(stdout, "fail\n") + return fmt.Errorf("This recovery key cannot unlock the device") + } + + fmt.Fprintf(stdout, "success\n") + return nil +} + +// ListRecoveryKeys lists recovery keys of a device. +func ListRecoveryKeys(devicePath string) error { + fmt.Fprintf(stderr, "Listing recovery keys in '%v'\n", devicePath) + recovery_names, err := sbListLUKS2ContainerRecoveryKeyNames(devicePath) + if err != nil { + return fmt.Errorf("Cannot list recovery keys: %w", err) + } + + for _, name := range recovery_names { + fmt.Fprintf(stdout, "%v\n", name) + } + return nil +} + +// DeleteRecoveryKey deletes the given recovery key from a device. +func DeleteRecoveryKey(devicePath string, recoveryKeyName string) error { + fmt.Fprintf(stdout, "Deleting recovery key '%v' in '%v'\n", recoveryKeyName, devicePath) + err := sbDeleteLUKS2ContainerKey(devicePath, recoveryKeyName) + if err != nil { + return fmt.Errorf("Cannot delete recovery key: %w", err) + } + return nil +} diff --git a/luks2/luks2_test.go b/luks2/luks2_test.go new file mode 100644 index 0000000..4ca80eb --- /dev/null +++ b/luks2/luks2_test.go @@ -0,0 +1,337 @@ +// This file is part of nullboot +// Copyright 2026 Canonical Ltd. +// SPDX-License-Identifier: GPL-3.0-only + +package luks2 + +import ( + "bytes" + "context" + "fmt" + "github.com/snapcore/secboot" + "gopkg.in/check.v1" + "os" + "reflect" + "testing" +) + +func Test(t *testing.T) { check.TestingT(t) } + +type luks2Suite struct { + restoreFindStorageContainer func(context.Context, string) (secboot.StorageContainer, error) + restoreGetKeyFromKernel func(context.Context, secboot.StorageContainer, secboot.KeyringKeyPurpose, string) ([]byte, error) + restoreAddLUKS2ContainerRecoveryKey func(string, string, secboot.DiskUnlockKey, secboot.RecoveryKey) error + restoreListLUKS2ContainerRecoveryKeyNames func(string) ([]string, error) + restoreDeleteLUKS2ContainerKey func(string, string) error + restoreParseRecoveryKey func(string) (secboot.RecoveryKey, error) + restoreTestLUKS2ContainerKey func(string, []byte) bool + + mockStdin bytes.Buffer + mockStdout bytes.Buffer + mockStderr bytes.Buffer +} + +var containers map[string]*mockStorageContainer + +var _ = check.Suite(&luks2Suite{}) + +type recoveryKeyStruct struct { + keyslotName string // name of the associated keyslot + parentKey []byte // the unlock key that was used to create the recovery key + recoveryKey secboot.RecoveryKey +} + +type mockStorageContainer struct { + path string + recoveryKeys []recoveryKeyStruct +} + +func (sc mockStorageContainer) Path() string { + return sc.path +} + +func (sc mockStorageContainer) BackendName() string { + return "backend-1" +} + +func (sc mockStorageContainer) CredentialName() string { + return "credential-name-1" +} + +func (sc mockStorageContainer) Activate(ctx context.Context, ks secboot.Keyslot, key []byte, cfg secboot.ActivateConfigGetter) error { + return nil +} + +func (sc mockStorageContainer) Deactivate(ctx context.Context) error { + return nil +} + +func (sc mockStorageContainer) OpenRead(ctx context.Context) (secboot.StorageContainerReader, error) { + return nil, nil +} + +func (s *luks2Suite) SetUpTest(c *check.C) { + s.restoreFindStorageContainer = sbFindStorageContainer + sbFindStorageContainer = mockFindStorageContainer + + s.restoreGetKeyFromKernel = sbGetKeyFromKernel + sbGetKeyFromKernel = mockGetKeyFromKernel + + s.restoreAddLUKS2ContainerRecoveryKey = sbAddLUKS2ContainerRecoveryKey + sbAddLUKS2ContainerRecoveryKey = mockAddLUKS2ContainerRecoveryKey + + s.restoreListLUKS2ContainerRecoveryKeyNames = sbListLUKS2ContainerRecoveryKeyNames + sbListLUKS2ContainerRecoveryKeyNames = mockListLUKS2ContainerRecoveryKeyNames + + s.restoreDeleteLUKS2ContainerKey = sbDeleteLUKS2ContainerKey + sbDeleteLUKS2ContainerKey = mockDeleteLUKS2ContainerKey + + s.restoreParseRecoveryKey = sbParseRecoveryKey + sbParseRecoveryKey = mockParseRecoveryKey + + s.restoreTestLUKS2ContainerKey = sbTestLUKS2ContainerKey + sbTestLUKS2ContainerKey = mockTestLUKS2ContainerKey + + stderr = &s.mockStderr + stdout = &s.mockStdout + stdin = &s.mockStdin +} + +func (s *luks2Suite) TearDownTest(c *check.C) { + sbFindStorageContainer = s.restoreFindStorageContainer + sbGetKeyFromKernel = s.restoreGetKeyFromKernel + sbAddLUKS2ContainerRecoveryKey = s.restoreAddLUKS2ContainerRecoveryKey + sbListLUKS2ContainerRecoveryKeyNames = s.restoreListLUKS2ContainerRecoveryKeyNames + stdout = os.Stdout + stderr = os.Stderr + stdin = os.Stdin +} + +func mockFindStorageContainer(ctx context.Context, path string) (secboot.StorageContainer, error) { + container, ok := containers[path] + if ok { + return container, nil + } + return nil, fmt.Errorf("no such container") +} + +var masterKey = []byte("12345678") +var badRecoveryKey = []byte("9999999999999999") + +func mockGetKeyFromKernel(ctx context.Context, storage secboot.StorageContainer, purpose secboot.KeyringKeyPurpose, prefix string) ([]byte, error) { + if secboot.KeyringKeyPurposeUnlock != purpose { + return nil, fmt.Errorf("invalid purpose") + } + if masterKey == nil { + return nil, fmt.Errorf("no master key") + } + return masterKey, nil +} + +func mockAddLUKS2ContainerRecoveryKey(devicePath, keyslotName string, existingKey secboot.DiskUnlockKey, recoveryKey secboot.RecoveryKey) error { + if devicePath == "/dev/raise-error-in-AddLUKS2ContainerRecoveryKey" { + return fmt.Errorf("error triggered in AddLUKS2ContainerRecoveryKey") + } + + container, ok := containers[devicePath] + if !ok { + return fmt.Errorf("no such container") + } + container.recoveryKeys = append(container.recoveryKeys, recoveryKeyStruct{ + keyslotName: keyslotName, + parentKey: existingKey, + recoveryKey: recoveryKey, + }) + return nil +} + +func mockListLUKS2ContainerRecoveryKeyNames(devicePath string) ([]string, error) { + container, ok := containers[devicePath] + if !ok { + return []string{}, fmt.Errorf("no such container") + } + names := []string{} + for _, recoveryKey := range container.recoveryKeys { + names = append(names, recoveryKey.keyslotName) + } + + return names, nil +} + +func mockDeleteLUKS2ContainerKey(devicePath, keyslotName string) error { + container, ok := containers[devicePath] + if !ok { + return fmt.Errorf("no such container") + } + resultingKeys := []recoveryKeyStruct{} + for _, recoveryKey := range container.recoveryKeys { + if recoveryKey.keyslotName != keyslotName { + // do not delete this one, add it in the result + resultingKeys = append(resultingKeys, recoveryKey) + } // else ignore it + } + container.recoveryKeys = resultingKeys + return nil +} + +func mockParseRecoveryKey(recoveryKeyString string) (secboot.RecoveryKey, error) { + if len(recoveryKeyString) != len(secboot.RecoveryKey{}) { + return secboot.RecoveryKey{}, fmt.Errorf("parse error") + } + recoveryKeyBytes := secboot.RecoveryKey{} + for index, c := range recoveryKeyString { + recoveryKeyBytes[index] = byte(c) + } + return recoveryKeyBytes, nil +} + +func mockTestLUKS2ContainerKey(devicePath string, key []byte) bool { + _, ok := containers[devicePath] + if !ok { + return false + } + if reflect.DeepEqual(key, badRecoveryKey) { + return false + } + return true +} + +func (s *luks2Suite) TestCreateRecoveryKey(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{} + + err := CreateRecoveryKey("/dev/sda1", "recovery-key-1") + + c.Assert(err, check.IsNil) + container, _ := containers["/dev/sda1"] + c.Assert(container, check.NotNil) + c.Check(len(container.recoveryKeys), check.Equals, 1) + c.Check(container.recoveryKeys[0].keyslotName, check.Equals, "recovery-key-1") + c.Check(container.recoveryKeys[0].parentKey, check.DeepEquals, masterKey) +} + +func (s *luks2Suite) TestCreateRecoveryKeyErrStorageContainer(c *check.C) { + containers = make(map[string]*mockStorageContainer) + + err := CreateRecoveryKey("/dev/sda1", "recovery-key-1") + + c.Check(err, check.NotNil) + c.Check(err, check.ErrorMatches, "Cannot find storage \\(LUKS\\) container: .*") +} + +func (s *luks2Suite) TestCreateRecoveryKeyErrKernelKeyring(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{} + restore := masterKey + defer func() { masterKey = restore }() + masterKey = nil + + err := CreateRecoveryKey("/dev/sda1", "recovery-key-1") + + c.Check(err, check.NotNil) + c.Check(err, check.ErrorMatches, "Cannot get disk unlock key from kernel: .*") +} + +func (s *luks2Suite) TestCreateRecoveryKeyErrAdd(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/raise-error-in-AddLUKS2ContainerRecoveryKey"] = &mockStorageContainer{} + + err := CreateRecoveryKey("/dev/raise-error-in-AddLUKS2ContainerRecoveryKey", "recovery-key-1") + + c.Check(err, check.NotNil) + c.Check(err, check.ErrorMatches, "Cannot add recovery key to LUKS container: .*") +} + +func (s *luks2Suite) TestVerifyRecoveryKey(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{} + s.mockStdin.Reset() + s.mockStdin.WriteString("0123456789abcdef\n") + + err := VerifyRecoveryKey("/dev/sda1") + + c.Check(err, check.IsNil) +} + +func (s *luks2Suite) TestVerifyRecoveryKeyErrParse(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{} + s.mockStdin.Reset() + s.mockStdin.WriteString("hello") + + err := VerifyRecoveryKey("/dev/sda1") + + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "parse error") +} + +func (s *luks2Suite) TestVerifyRecoveryKeyErrBadKey(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{} + s.mockStdin.Reset() + s.mockStdin.WriteString(string(badRecoveryKey)) + + err := VerifyRecoveryKey("/dev/sda1") + + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "This recovery key cannot unlock the device") +} + +func (s *luks2Suite) TestListRecoveryKeys(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{ + path: "/dev/sda1", + recoveryKeys: []recoveryKeyStruct{ + {keyslotName: "recovery-1"}, + {keyslotName: "recovery-2"}, + }, + } + s.mockStdout.Reset() + + err := ListRecoveryKeys("/dev/sda1") + + c.Assert(err, check.IsNil) + c.Check(s.mockStdout.String(), check.Equals, "recovery-1\nrecovery-2\n") +} + +func (s *luks2Suite) TestListRecoveryKeysErrNoSuchDevice(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{path: "/dev/sda1"} + + err := ListRecoveryKeys("/dev/sdaXXX") + + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "Cannot list recovery keys: .*") +} + +func (s *luks2Suite) TestDeleteRecoveryKey(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{ + path: "/dev/sda1", + recoveryKeys: []recoveryKeyStruct{ + {keyslotName: "recovery-1"}, + {keyslotName: "recovery-2"}, + {keyslotName: "recovery-3"}, + }, + } + + err := DeleteRecoveryKey("/dev/sda1", "recovery-2") + + c.Assert(err, check.IsNil) + container, _ := containers["/dev/sda1"] + c.Assert(container, check.NotNil) + + c.Check(len(container.recoveryKeys), check.Equals, 2) + c.Check(container.recoveryKeys[0].keyslotName, check.Equals, "recovery-1") + c.Check(container.recoveryKeys[1].keyslotName, check.Equals, "recovery-3") +} + +func (s *luks2Suite) TestDeleteRecoveryKeyErrNoSuchDevice(c *check.C) { + containers = make(map[string]*mockStorageContainer) + containers["/dev/sda1"] = &mockStorageContainer{path: "/dev/sda1"} + + err := DeleteRecoveryKey("/dev/sdaXXX", "recovery-2") + + c.Assert(err, check.NotNil) + c.Check(err, check.ErrorMatches, "Cannot delete recovery key: .*") +}