Skip to content

Commit

Permalink
secboot: add and remove recovery with new slots when possible
Browse files Browse the repository at this point in the history
  • Loading branch information
valentindavid committed Jun 4, 2024
1 parent a9367f8 commit c59edb2
Show file tree
Hide file tree
Showing 3 changed files with 234 additions and 38 deletions.
153 changes: 116 additions & 37 deletions secboot/encrypt_sb.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//go:build !nosecboot

/*
* Copyright (C) 2022 Canonical Ltd
* Copyright (C) 2022-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand All @@ -25,6 +25,7 @@ import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"

sb "github.com/snapcore/secboot"
Expand All @@ -38,7 +39,11 @@ import (
)

var (
sbInitializeLUKS2Container = sb.InitializeLUKS2Container
sbInitializeLUKS2Container = sb.InitializeLUKS2Container
sbGetDiskUnlockKeyFromKernel = sb.GetDiskUnlockKeyFromKernel
sbAddLUKS2ContainerRecoveryKey = sb.AddLUKS2ContainerRecoveryKey
sbListLUKS2ContainerUnlockKeyNames = sb.ListLUKS2ContainerUnlockKeyNames
sbDeleteLUKS2ContainerKey = sb.DeleteLUKS2ContainerKey
)

const keyslotsAreaKiBSize = 2560 // 2.5MB
Expand Down Expand Up @@ -95,36 +100,84 @@ func runSnapFDEKeymgr(args []string, stdin io.Reader) error {
// EnsureRecoveryKey makes sure the encrypted block devices have a recovery key.
// It takes the path where to store the key and encrypted devices to operate on.
func EnsureRecoveryKey(keyFile string, rkeyDevs []RecoveryKeyDevice) (keys.RecoveryKey, error) {
// support multiple devices with the same key
command := []string{
"add-recovery-key",
"--key-file", keyFile,
var legacyCmdline []string
var newDevices []struct {
node string
keyFile string
}
for _, rkeyDev := range rkeyDevs {
dev, err := devByPartUUIDFromMount(rkeyDev.Mountpoint)
if err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot find matching device for: %v", err)
}
logger.Debugf("ensuring recovery key on device: %v", dev)
authzMethod := "keyring"
if rkeyDev.AuthorizingKeyFile != "" {
authzMethod = "file:" + rkeyDev.AuthorizingKeyFile
slots, err := sbListLUKS2ContainerUnlockKeyNames(dev)
if err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot find list keys for: %v", err)
}
if len(slots) == 0 {
authzMethod := "keyring"
if rkeyDev.AuthorizingKeyFile != "" {
authzMethod = "file:" + rkeyDev.AuthorizingKeyFile
}
legacyCmdline = append(legacyCmdline, []string{
"--devices", dev,
"--authorizations", authzMethod,
}...)
} else {
newDevices = append(newDevices, struct {
node string
keyFile string
}{dev, rkeyDev.AuthorizingKeyFile})
}
command = append(command, []string{
"--devices", dev,
"--authorizations", authzMethod,
}...)
}

if err := runSnapFDEKeymgr(command, nil); err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot run keymgr tool: %v", err)
if len(legacyCmdline) != 0 && len(newDevices) != 0 {
return keys.RecoveryKey{}, fmt.Errorf("some encrypted partitions use new slots, whereas other use legacy slots")
}
if len(legacyCmdline) == 0 {
recoveryKey, err := keys.NewRecoveryKey()
if err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot create new recovery key: %v", err)
}
for _, device := range newDevices {
var unlockKey []byte
if device.keyFile != "" {
key, err := os.ReadFile(device.keyFile)
if err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot get key from '%s': %v", device.keyFile, err)
}
unlockKey = key
} else {
const defaultPrefix = "ubuntu-fde"
key, err := sbGetDiskUnlockKeyFromKernel(defaultPrefix, device.node, false)
if err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot get key for unlocked disk: %v", err)
}
unlockKey = key
}

rk, err := keys.RecoveryKeyFromFile(keyFile)
if err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot read recovery key: %v", err)
if err := sbAddLUKS2ContainerRecoveryKey(device.node, "default-recovery", sb.DiskUnlockKey(unlockKey), sb.RecoveryKey(recoveryKey)); err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot enroll new recovery key: %v", err)
}
}

return recoveryKey, nil
} else {
command := []string{
"add-recovery-key",
"--key-file", keyFile,
}
command = append(command, legacyCmdline...)

if err := runSnapFDEKeymgr(command, nil); err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot run keymgr tool: %v", err)
}

rk, err := keys.RecoveryKeyFromFile(keyFile)
if err != nil {
return keys.RecoveryKey{}, fmt.Errorf("cannot read recovery key: %v", err)
}
return *rk, nil
}
return *rk, nil
}

func devByPartUUIDFromMount(mp string) (string, error) {
Expand All @@ -142,31 +195,57 @@ func devByPartUUIDFromMount(mp string) (string, error) {
// It takes a map from the recovery key device to where their recovery key is
// stored, mount points might share the latter.
func RemoveRecoveryKeys(rkeyDevToKey map[RecoveryKeyDevice]string) error {
// support multiple devices and key files
command := []string{
"remove-recovery-key",
}
var legacyCmdline []string
var newDevices []string
for rkeyDev, keyFile := range rkeyDevToKey {
dev, err := devByPartUUIDFromMount(rkeyDev.Mountpoint)
if err != nil {
return fmt.Errorf("cannot find matching device for: %v", err)
}
logger.Debugf("removing recovery key from device: %v", dev)
authzMethod := "keyring"
if rkeyDev.AuthorizingKeyFile != "" {
authzMethod = "file:" + rkeyDev.AuthorizingKeyFile
slots, err := sbListLUKS2ContainerUnlockKeyNames(dev)
if err != nil {
return fmt.Errorf("cannot find list keys for: %v", err)
}
if len(slots) == 0 {
logger.Debugf("removing recovery key from device: %v", dev)
authzMethod := "keyring"
if rkeyDev.AuthorizingKeyFile != "" {
authzMethod = "file:" + rkeyDev.AuthorizingKeyFile
}
legacyCmdline = append(legacyCmdline, []string{
"--devices", dev,
"--authorizations", authzMethod,
"--key-files", keyFile,
}...)
} else {
newDevices = append(newDevices, dev)
}
command = append(command, []string{
"--devices", dev,
"--authorizations", authzMethod,
"--key-files", keyFile,
}...)
}

if err := runSnapFDEKeymgr(command, nil); err != nil {
return fmt.Errorf("cannot run keymgr tool: %v", err)
if len(legacyCmdline) != 0 && len(newDevices) != 0 {
return fmt.Errorf("some encrypted partitions use new slots, whereas other use legacy slots")
}
if len(legacyCmdline) == 0 {
for _, device := range newDevices {
if err := sbDeleteLUKS2ContainerKey(device, "default-recovery"); err != nil {
return fmt.Errorf("cannot remove recovery key: %v", err)
}
}

return nil

} else {
// support multiple devices and key files
command := []string{
"remove-recovery-key",
}
command = append(command, legacyCmdline...)

if err := runSnapFDEKeymgr(command, nil); err != nil {
return fmt.Errorf("cannot run keymgr tool: %v", err)
}
return nil
}
return nil
}

// StageEncryptionKeyChange stages a new encryption key for a given encrypted
Expand Down
87 changes: 86 additions & 1 deletion secboot/encrypt_sb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//go:build !nosecboot

/*
* Copyright (C) 2022 Canonical Ltd
* Copyright (C) 2022-2024 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
Expand All @@ -25,6 +25,7 @@ import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"

sb "github.com/snapcore/secboot"
Expand Down Expand Up @@ -344,6 +345,47 @@ done
func (s *keymgrSuite) TestEnsureRecoveryKey(c *C) {
udevadmCmd := s.mocksForDeviceMounts(c)

defer secboot.MockListLUKS2ContainerUnlockKeyNames(func(devicePath string) ([]string, error) {
return []string{"default"}, nil
})()
defer secboot.MockGetDiskUnlockKeyFromKernel(func(prefix string, devicePath string, remove bool) (sb.DiskUnlockKey, error) {
return []byte{1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4, 1, 2, 3, 4}, nil
})()
defer secboot.MockAddLUKS2ContainerRecoveryKey(func(devicePath string, keyslotName string, existingKey sb.DiskUnlockKey, recoveryKey sb.RecoveryKey) error {
return nil
})()

keyFilePath := filepath.Join(c.MkDir(), "key.file")
err := os.WriteFile(keyFilePath, []byte{}, 0644)
c.Assert(err, IsNil)
_, err = secboot.EnsureRecoveryKey(filepath.Join(s.d, "recovery.key"), []secboot.RecoveryKeyDevice{
{Mountpoint: "/foo"},
{Mountpoint: "/bar", AuthorizingKeyFile: keyFilePath},
})
c.Assert(err, IsNil)
c.Check(udevadmCmd.Calls(), DeepEquals, [][]string{
{"udevadm", "info", "--query", "property", "--name", "/dev/mapper/foo"},
{"udevadm", "info", "--query", "property", "--name", "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1304"},
{"udevadm", "info", "--query", "property", "--name", "/dev/mapper/bar"},
{"udevadm", "info", "--query", "property", "--name", "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1305"},
})

}

func (s *keymgrSuite) TestEnsureRecoveryKeyLegacy(c *C) {
udevadmCmd := s.mocksForDeviceMounts(c)

defer secboot.MockListLUKS2ContainerUnlockKeyNames(func(devicePath string) ([]string, error) {
return []string{}, nil
})()
defer secboot.MockGetDiskUnlockKeyFromKernel(func(prefix string, devicePath string, remove bool) (sb.DiskUnlockKey, error) {
c.Errorf("unexpected call")
return sb.DiskUnlockKey{}, nil
})()
defer secboot.MockAddLUKS2ContainerRecoveryKey(func(devicePath string, keyslotName string, existingKey sb.DiskUnlockKey, recoveryKey sb.RecoveryKey) error {
c.Errorf("unexpected call")
return nil
})()
rkey, err := secboot.EnsureRecoveryKey(filepath.Join(s.d, "recovery.key"), []secboot.RecoveryKeyDevice{
{Mountpoint: "/foo"},
{Mountpoint: "/bar", AuthorizingKeyFile: "/authz/key.file"},
Expand Down Expand Up @@ -382,6 +424,49 @@ func (s *keymgrSuite) TestEnsureRecoveryKey(c *C) {
func (s *keymgrSuite) TestRemoveRecoveryKey(c *C) {
udevadmCmd := s.mocksForDeviceMounts(c)

defer secboot.MockListLUKS2ContainerUnlockKeyNames(func(devicePath string) ([]string, error) {
return []string{"default", "default-recovery"}, nil
})()
defer secboot.MockRemoveLUKS2ContainerKey(func(devicePath string, keyslotName string) error {
c.Assert(keyslotName, Equals, "default-recovery")
return nil
})()

snaptest.PopulateDir(s.d, [][]string{
{"recovery.key", "foobar"},
})
// only one of the key files exists
err := secboot.RemoveRecoveryKeys(map[secboot.RecoveryKeyDevice]string{
{Mountpoint: "/foo"}: filepath.Join(s.d, "recovery.key"),
{Mountpoint: "/bar", AuthorizingKeyFile: "/authz/key.file"}: filepath.Join(s.d, "missing-recovery.key"),
})
c.Assert(err, IsNil)

expectedUdevCalls := [][]string{
// order can change depending on map iteration
{"udevadm", "info", "--query", "property", "--name", "/dev/mapper/foo"},
{"udevadm", "info", "--query", "property", "--name", "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1304"},
{"udevadm", "info", "--query", "property", "--name", "/dev/mapper/bar"},
{"udevadm", "info", "--query", "property", "--name", "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1305"},
}

udevCalls := udevadmCmd.Calls()
c.Assert(udevCalls, HasLen, len(expectedUdevCalls))
// iteration order can be different though
c.Assert(udevCalls[0], HasLen, 6)
}

func (s *keymgrSuite) TestRemoveRecoveryKeyLegacy(c *C) {
udevadmCmd := s.mocksForDeviceMounts(c)

defer secboot.MockListLUKS2ContainerUnlockKeyNames(func(devicePath string) ([]string, error) {
return []string{}, nil
})()
defer secboot.MockRemoveLUKS2ContainerKey(func(devicePath string, keyslotName string) error {
c.Errorf("unexpected call")
return nil
})()

snaptest.PopulateDir(s.d, [][]string{
{"recovery.key", "foobar"},
})
Expand Down
32 changes: 32 additions & 0 deletions secboot/export_sb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,35 @@ func MockReadKeyFile(f func(keyfile string) (*sb.KeyData, *sb_tpm2.SealedKeyObje
readKeyFile = old
}
}

func MockListLUKS2ContainerUnlockKeyNames(f func(devicePath string) ([]string, error)) (restore func()) {
old := sbListLUKS2ContainerUnlockKeyNames
sbListLUKS2ContainerUnlockKeyNames = f
return func() {
sbListLUKS2ContainerUnlockKeyNames = old
}
}

func MockGetDiskUnlockKeyFromKernel(f func(prefix string, devicePath string, remove bool) (sb.DiskUnlockKey, error)) (restore func()) {
old := sbGetDiskUnlockKeyFromKernel
sbGetDiskUnlockKeyFromKernel = f
return func() {
sbGetDiskUnlockKeyFromKernel = old
}
}

func MockAddLUKS2ContainerRecoveryKey(f func(devicePath string, keyslotName string, existingKey sb.DiskUnlockKey, recoveryKey sb.RecoveryKey) error) (restore func()) {
old := sbAddLUKS2ContainerRecoveryKey
sbAddLUKS2ContainerRecoveryKey = f
return func() {
sbAddLUKS2ContainerRecoveryKey = old
}
}

func MockRemoveLUKS2ContainerKey(f func(devicePath string, keyslotName string) error) (restore func()) {
old := sbDeleteLUKS2ContainerKey
sbDeleteLUKS2ContainerKey = f
return func() {
sbDeleteLUKS2ContainerKey = old
}
}

0 comments on commit c59edb2

Please sign in to comment.