From 5bfe9fe095c8760c3a00ef687d9a55f8df3fea2e Mon Sep 17 00:00:00 2001 From: Valentin David Date: Tue, 22 Sep 2026 15:17:53 +0200 Subject: [PATCH 1/2] crypt.go: add function to verify key for specific keyslot In order to be able to prevent removing keyslots for which we have the only key for the containers, we need to be able to check specific keyslots. We typically only know the key that got registered in keyring during boot. And they come from sealed key that we usually cannot unseal again later. If we remove any of those keyslots because an unexpected keyslot was used for activation, then we lose ability to act on the container, until we reboot and a different keyslot gets used. Unexpected keyslot might used if we for example lose power or hard reset in the middle of reprovision. --- crypt.go | 22 ++++++++++++++++-- crypt_test.go | 38 +++++++++++++++++++++++++++++++ internal/luks2/cryptsetup.go | 10 ++++++++ internal/luks2/cryptsetup_test.go | 17 ++++++++++++++ 4 files changed, 85 insertions(+), 2 deletions(-) diff --git a/crypt.go b/crypt.go index 8c3699f1..9c66957a 100755 --- a/crypt.go +++ b/crypt.go @@ -45,6 +45,8 @@ var ( // required features. ErrMissingCryptsetupFeature = luks2.ErrMissingCryptsetupFeature + ErrKeyslotNameNotExist = errors.New("no key with the specified name exists") + luks2Activate = luks2.Activate luks2AddKey = luks2.AddKey luks2Deactivate = luks2.Deactivate @@ -870,7 +872,7 @@ func DeleteLUKS2ContainerKey(devicePath, keyslotName string) error { token, id, exists := view.TokenByName(keyslotName) if !exists { - return errors.New("no key with the specified name exists") + return ErrKeyslotNameNotExist } if len(view.TokenNames()) == 1 { @@ -920,7 +922,7 @@ func renameLUKS2ContainerKey(nonAtomic *nonAtomicOperationAllowedFlag, devicePat token, id, exists := view.TokenByName(oldName) if !exists { - return errors.New("no key with the specified name exists") + return ErrKeyslotNameNotExist } if _, _, exists := view.TokenByName(newName); exists { @@ -1044,3 +1046,19 @@ func NameLegacyLUKS2ContainerKey(devicePath string, keyslot int, newName string) func TestLUKS2ContainerKey(devicePath string, key []byte) bool { return luks2.TestContainerKey(devicePath, key) } + +// Check if key is valid key for LUKS2 container at devicePath for a specific keyslot. +func TestLUKS2ContainerKeyForKeyslot(devicePath string, name string, key []byte) (bool, error) { + view, err := newLUKSView(context.TODO(), devicePath) + if err != nil { + return false, xerrors.Errorf("cannot obtain LUKS header view: %w", err) + } + + _, id, exists := view.TokenByName(name) + + if !exists { + return false, ErrKeyslotNameNotExist + } + + return luks2.TestContainerKeyForKeyslot(devicePath, id, key), nil +} diff --git a/crypt_test.go b/crypt_test.go index ecb29361..85c2fcac 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -4137,3 +4137,41 @@ func (s *cryptSuite) TestNameLegacyLUKS2ContainerKeyNameAlreadyUsed(c *C) { err := NameLegacyLUKS2ContainerKey("/dev/foo1", 0, "already-used") c.Check(err, ErrorMatches, `the new name is already in use`) } + +func (s *cryptSuiteUnmockedBase) TestTestLUKS2ContainerKeyForKeyslot(c *C) { + key := s.newPrimaryKey() + path := luks2test.CreateEmptyDiskImage(c, 20) + + initOptions := &InitializeLUKS2ContainerOptions{ + InitialKeyslotName: "initial", + } + c.Assert(InitializeLUKS2Container(path, "disk", key, initOptions), IsNil) + + otherKey := s.newPrimaryKey() + c.Assert(AddLUKS2ContainerUnlockKey(path, "other", key, otherKey), IsNil) + + recoveryKey := s.newRecoveryKey() + c.Assert(AddLUKS2ContainerRecoveryKey(path, "recovery", key, recoveryKey), IsNil) + + check := func (name string, testKey []byte, expected bool) { + res, err := TestLUKS2ContainerKeyForKeyslot(path, name, testKey) + c.Assert(err, IsNil) + c.Check(res, Equals, expected) + } + + check("initial", key, true) + check("initial", otherKey, false) + check("initial", recoveryKey[:], false) + + check("other", key, false) + check("other", otherKey, true) + check("other", recoveryKey[:], false) + + check("recovery", key, false) + check("recovery", otherKey, false) + check("recovery", recoveryKey[:], true) + + _, err := TestLUKS2ContainerKeyForKeyslot(path, "non-existent", key) + c.Check(err, ErrorMatches, `no key with the specified name exists`) + c.Check(errors.Is(err, ErrKeyslotNameNotExist), Equals, true) +} diff --git a/internal/luks2/cryptsetup.go b/internal/luks2/cryptsetup.go index 745ff80b..1e3f9be7 100755 --- a/internal/luks2/cryptsetup.go +++ b/internal/luks2/cryptsetup.go @@ -506,3 +506,13 @@ func SetSlotPriority(devicePath string, slot int, priority SlotPriority) error { func TestContainerKey(devicePath string, key []byte) bool { return cryptsetupCmd(bytes.NewReader(key), "open", "--test-passphrase", "--key-file", "-", devicePath) == nil } + +// Check if key is valid key for LUKS2 container at devicePath for a specific keyslot. +func TestContainerKeyForKeyslot(devicePath string, slot int, key []byte) bool { + err := cryptsetupCmd(bytes.NewReader(key), "open", "--test-passphrase", "--key-slot", strconv.Itoa(slot), "--key-file", "-", devicePath) + if err != nil { + fmt.Printf("got error: %v", err) + return false + } + return true +} diff --git a/internal/luks2/cryptsetup_test.go b/internal/luks2/cryptsetup_test.go index 9546f68e..15666f4b 100644 --- a/internal/luks2/cryptsetup_test.go +++ b/internal/luks2/cryptsetup_test.go @@ -1339,3 +1339,20 @@ func (s *cipherSuite) TestSelectCipherAndKeysize(c *C) { c.Check(keysize, Equals, tc.expectedKeysize) } } + +func (s *cryptsetupSuite) TestTestContainerKeyForKeyslot(c *C) { + key := make([]byte, 32) + rand.Read(key) + + otherKey := make([]byte, 32) + rand.Read(otherKey) + + devicePath := luks2test.CreateEmptyDiskImage(c, 20) + c.Assert(Format(devicePath, "test", key, &FormatOptions{}), IsNil) + c.Assert(AddKey(devicePath, key, otherKey, &AddKeyOptions{Slot: 1}), IsNil) + + c.Check(TestContainerKeyForKeyslot(devicePath, 0, key), Equals, true) + c.Check(TestContainerKeyForKeyslot(devicePath, 0, otherKey), Equals, false) + c.Check(TestContainerKeyForKeyslot(devicePath, 1, key), Equals, false) + c.Check(TestContainerKeyForKeyslot(devicePath, 1, otherKey), Equals, true) +} From b24039e2d0383808e2449adf102c9034ffb87b3a Mon Sep 17 00:00:00 2001 From: Valentin David Date: Wed, 23 Sep 2026 10:21:32 +0200 Subject: [PATCH 2/2] fixup! crypt.go: add function to verify key for specific keyslot --- crypt.go | 7 ++++--- crypt_test.go | 2 +- internal/luks2/cryptsetup.go | 7 +------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/crypt.go b/crypt.go index 9c66957a..6dd28ecf 100755 --- a/crypt.go +++ b/crypt.go @@ -1054,11 +1054,12 @@ func TestLUKS2ContainerKeyForKeyslot(devicePath string, name string, key []byte) return false, xerrors.Errorf("cannot obtain LUKS header view: %w", err) } - _, id, exists := view.TokenByName(name) - + token, _, exists := view.TokenByName(name) if !exists { return false, ErrKeyslotNameNotExist } - return luks2.TestContainerKeyForKeyslot(devicePath, id, key), nil + keyslotId := token.Keyslots()[0] + + return luks2.TestContainerKeyForKeyslot(devicePath, keyslotId, key), nil } diff --git a/crypt_test.go b/crypt_test.go index 85c2fcac..98343a99 100644 --- a/crypt_test.go +++ b/crypt_test.go @@ -4153,7 +4153,7 @@ func (s *cryptSuiteUnmockedBase) TestTestLUKS2ContainerKeyForKeyslot(c *C) { recoveryKey := s.newRecoveryKey() c.Assert(AddLUKS2ContainerRecoveryKey(path, "recovery", key, recoveryKey), IsNil) - check := func (name string, testKey []byte, expected bool) { + check := func(name string, testKey []byte, expected bool) { res, err := TestLUKS2ContainerKeyForKeyslot(path, name, testKey) c.Assert(err, IsNil) c.Check(res, Equals, expected) diff --git a/internal/luks2/cryptsetup.go b/internal/luks2/cryptsetup.go index 1e3f9be7..2cd2f37a 100755 --- a/internal/luks2/cryptsetup.go +++ b/internal/luks2/cryptsetup.go @@ -509,10 +509,5 @@ func TestContainerKey(devicePath string, key []byte) bool { // Check if key is valid key for LUKS2 container at devicePath for a specific keyslot. func TestContainerKeyForKeyslot(devicePath string, slot int, key []byte) bool { - err := cryptsetupCmd(bytes.NewReader(key), "open", "--test-passphrase", "--key-slot", strconv.Itoa(slot), "--key-file", "-", devicePath) - if err != nil { - fmt.Printf("got error: %v", err) - return false - } - return true + return cryptsetupCmd(bytes.NewReader(key), "open", "--test-passphrase", "--key-slot", strconv.Itoa(slot), "--key-file", "-", devicePath) == nil }