diff --git a/.changelog/24383.txt b/.changelog/24383.txt new file mode 100644 index 00000000000..3eb6eba1e2a --- /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 ffa7c7a467b..64bd4c047a1 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 1f65ffb98e5..e539ac1a5b5 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) {