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(+)

diff --git a/providers/implementations/ciphers/cipher_aes_siv.c b/providers/implementations/ciphers/cipher_aes_siv.c
index 510c1581b5939..c3facacfbcf45 100644
--- a/providers/implementations/ciphers/cipher_aes_siv.c
+++ b/providers/implementations/ciphers/cipher_aes_siv.c
@@ -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;
 }
 
diff --git a/test/evp_extra_test.c b/test/evp_extra_test.c
index a666710..9604ee4 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -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