From 316430b094fd1637ce14a723a0c3441dfa94e0a9 Mon Sep 17 00:00:00 2001 From: James Rasell Date: Thu, 7 Nov 2024 16:08:18 +0000 Subject: [PATCH] keyring: Fix a panic when decrypting aead with empty RSA block. (#24383) Clusters that have gone through several upgrades have be found to include keyring material which has an empty RSA block. In more recent versions of Nomad, an empty RSA block is omitted from being written to disk. This results in the panic not being present. Older versions, however, did not have this struct tag meaning we wrote an empty JSON block which is not accounted for in the current version. --- .changelog/24383.txt | 3 ++ nomad/encrypter.go | 2 +- nomad/encrypter_test.go | 61 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 .changelog/24383.txt diff --git a/.changelog/24383.txt b/.changelog/24383.txt new file mode 100644 index 000000000..3eb6eba1e --- /dev/null +++ b/.changelog/24383.txt @@ -0,0 +1,3 @@ +```release-note:bug +keyring: Fixed a panic on server startup when decrypting AEAD key data with empty RSA block +``` diff --git a/nomad/encrypter.go b/nomad/encrypter.go index ffa7c7a46..64bd4c047 100644 --- a/nomad/encrypter.go +++ b/nomad/encrypter.go @@ -805,7 +805,7 @@ func (e *Encrypter) loadKeyFromStore(path string) (*structs.UnwrappedRootKey, er // 1.7 an ed25519 key derived from the root key was used instead of an RSA // key. var rsaKey []byte - if kekWrapper.WrappedRSAKey != nil { + if kekWrapper.WrappedRSAKey != nil && len(kekWrapper.WrappedRSAKey.Ciphertext) > 0 { rsaKey, err = wrapper.Decrypt(e.srv.shutdownCtx, kekWrapper.WrappedRSAKey) if err != nil { return nil, fmt.Errorf("%w (rsa key): %w", ErrDecryptFailed, err) diff --git a/nomad/encrypter_test.go b/nomad/encrypter_test.go index 1f65ffb98..e539ac1a5 100644 --- a/nomad/encrypter_test.go +++ b/nomad/encrypter_test.go @@ -120,6 +120,67 @@ func TestEncrypter_LoadSave(t *testing.T) { } +// TestEncrypter_loadKeyFromStore_emptyRSA tests a panic seen by some +// operators where the aead key disk file content had an empty RSA block. +func TestEncrypter_loadKeyFromStore_emptyRSA(t *testing.T) { + ci.Parallel(t) + + srv := &Server{ + logger: testlog.HCLogger(t), + config: &Config{}, + } + + tmpDir := t.TempDir() + + key, err := structs.NewUnwrappedRootKey(structs.EncryptionAlgorithmAES256GCM) + must.NoError(t, err) + + encrypter, err := NewEncrypter(srv, tmpDir) + must.NoError(t, err) + + wrappedKey, err := encrypter.encryptDEK(key, &structs.KEKProviderConfig{}) + must.NotNil(t, wrappedKey) + must.NoError(t, err) + + // Use an artisanally crafted key file. + kek, err := json.Marshal(wrappedKey.KeyEncryptionKey) + must.NoError(t, err) + + wrappedDEKCipher, err := json.Marshal(wrappedKey.WrappedDataEncryptionKey.Ciphertext) + must.NoError(t, err) + + testData := fmt.Sprintf(` + { + "Meta": { + "KeyID": %q, + "Algorithm": "aes256-gcm", + "CreateTime": 1730000000000000000, + "CreateIndex": 1555555, + "ModifyIndex": 1555555, + "State": "active", + "PublishTime": 0 + }, + "ProviderID": "aead", + "WrappedDEK": { + "ciphertext": %s, + "key_info": { + "key_id": %q + } + }, + "WrappedRSAKey": {}, + "KEK": %s + } + `, key.Meta.KeyID, wrappedDEKCipher, key.Meta.KeyID, kek) + + path := filepath.Join(tmpDir, key.Meta.KeyID+".nks.json") + err = os.WriteFile(path, []byte(testData), 0o600) + must.NoError(t, err) + + unwrappedKey, err := encrypter.loadKeyFromStore(path) + must.NoError(t, err) + must.NotNil(t, unwrappedKey) +} + // TestEncrypter_Restore exercises the entire reload of a keystore, // including pairing metadata with key material func TestEncrypter_Restore(t *testing.T) {