From 71e2a5d263518cf5866043bd60ee4994d59e53a3 Mon Sep 17 00:00:00 2001
From: Dmitry Belyavskiy <beldmit@gmail.com>
Date: Wed, 13 May 2026 14:03:57 +0200
Subject: [PATCH] Fix handling of empty-ciphertext messages in AES-SIV
Conflict: Adapt context in test/evp_extra_test.c for OpenSSL 3.0.9.
AES-SIV: EVP_DecryptUpdate_ex Accepts All-Zero Tag for Empty-Ciphertext
Messages on context reuse.
Fixes CVE-2026-45446
Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
MergeDate: Mon Jun 8 20:19:02 2026
.../implementations/ciphers/cipher_aes_siv.c | 3 +
test/evp_extra_test.c | 61 +++++++++++++++++++
2 files changed, 64 insertions(+)
@@ -203,6 +203,7 @@ static int aes_siv_set_ctx_params(void *vctx, const OSSL_PARAM params[])
PROV_AES_SIV_CTX *ctx = (PROV_AES_SIV_CTX *)vctx;
const OSSL_PARAM *p;
unsigned int speed = 0;
+ SIV128_CONTEXT *sctx = &ctx->siv;
if (params == NULL)
return 1;
@@ -237,6 +238,8 @@ static int aes_siv_set_ctx_params(void *vctx, const OSSL_PARAM params[])
if (keylen != ctx->keylen)
return 0;
}
+ sctx->final_ret = -1;
+
return 1;
}
@@ -4655,6 +4655,64 @@ static int test_ecx_short_keys(int tst)
return 1;
}
+/*
+ * AES-SIV reuse-without-rekey:
+ * msg1: legit non-empty CT, tag verifies, final_ret=0
+ * msg2: no reinit (or reinit with key=NULL), set forged tag,
+ * AAD only, DecryptFinal -> does stale final_ret leak through?
+ */
+static int test_aes_siv_ctx_reuse(void)
+{
+ unsigned char key[32] = { 7 }; /* AES-128-SIV => 2*16 */
+ unsigned char pt[9] = "payload!";
+ unsigned char ct[9], tagbuf[16], out[16], zero16[16] = { 0 };
+ unsigned char aad[14] = "forged header";
+ int outl, ret = 0;
+ EVP_CIPHER_CTX *e = NULL, *d = NULL;
+ EVP_CIPHER *c = EVP_CIPHER_fetch(NULL, "AES-128-SIV", NULL);
+
+ if (c == NULL) {
+ return TEST_skip("AES-128-SIV cipher is not available");
+ }
+
+ /* produce a valid (ct,tag) for msg1 */
+ e = EVP_CIPHER_CTX_new();
+ if (!TEST_ptr(e)
+ || !TEST_true(EVP_EncryptInit_ex2(e, c, key, NULL, NULL))
+ || !TEST_true(EVP_EncryptUpdate(e, NULL, &outl, (unsigned char *)"hdr1", 4))
+ || !TEST_true(EVP_EncryptUpdate(e, ct, &outl, pt, sizeof(pt)))
+ || !TEST_true(EVP_EncryptFinal_ex(e, out, &outl))
+ || !TEST_true(EVP_CIPHER_CTX_ctrl(e, EVP_CTRL_AEAD_GET_TAG, 16, tagbuf))) {
+ EVP_CIPHER_CTX_free(e);
+ goto err;
+ }
+ EVP_CIPHER_CTX_free(e);
+
+ /* msg1 decrypt */
+ d = EVP_CIPHER_CTX_new();
+ if (!TEST_ptr(d)
+ || !TEST_true(EVP_DecryptInit_ex2(d, c, key, NULL, NULL))
+ || !TEST_true(EVP_CIPHER_CTX_ctrl(d, EVP_CTRL_AEAD_SET_TAG, 16, tagbuf))
+ || !TEST_true(EVP_DecryptUpdate(d, NULL, &outl, (unsigned char *)"hdr1", 4))
+ || !TEST_true(EVP_DecryptUpdate(d, out, &outl, ct, sizeof(ct)))
+ || !TEST_true(EVP_DecryptFinal_ex(d, out, &outl)))
+ goto err;
+
+ /* msg2 on SAME ctx, reinit with key=NULL => initkey skipped, final_ret should be reset */
+ if (!TEST_true(EVP_DecryptInit_ex2(d, NULL, NULL, NULL, NULL))
+ || !TEST_true(EVP_CIPHER_CTX_ctrl(d, EVP_CTRL_AEAD_SET_TAG, 16, zero16))
+ || !TEST_true(EVP_DecryptUpdate(d, NULL, &outl, aad, sizeof(aad))) /* forged AAD */
+ || !TEST_false(EVP_DecryptFinal_ex(d, out, &outl)))
+ goto err;
+
+ ret = 1;
+
+err:
+ EVP_CIPHER_CTX_free(d);
+ EVP_CIPHER_free(c);
+ return ret;
+}
+
typedef enum OPTION_choice {
OPT_ERR = -1,
OPT_EOF = 0,
@@ -4817,6 +4875,9 @@ int setup_tests(void)
#if !defined(OPENSSL_NO_CHACHA) && !defined(OPENSSL_NO_POLY1305)
ADD_TEST(test_decrypt_null_chunks);
#endif
+ /* Test case for CVE-2026-45446 */
+ ADD_TEST(test_aes_siv_ctx_reuse);
+
#ifndef OPENSSL_NO_DH
ADD_TEST(test_DH_priv_pub);
# ifndef OPENSSL_NO_DEPRECATED_3_0