Commit 609bcb2486 for openssl.org

commit 609bcb24866d64f1fc49d38b852edbb39057d721
Author: Dmitry Belyavskiy <beldmit@gmail.com>
Date:   Wed May 13 11:45:51 2026 +0200

    Fix handling of empty-ciphertext messages in AES-GCM-SIV and AES-SIV

    AES-GCM-SIV: EVP_DecryptFinal_ex Accepts All-Zero Tag for Empty-Ciphertext
    Messages.

    AES-SIV: EVP_DecryptUpdate_ex Accepts All-Zero Tag for Empty-Ciphertext
    Messages on context reuse.

    Fixes CVE-2026-45446

    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Eugene Syromiatnikov <esyr@openssl.org>
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    MergeDate: Mon Jun  8 20:12:25 2026

diff --git a/providers/implementations/ciphers/cipher_aes_gcm_siv_hw.c b/providers/implementations/ciphers/cipher_aes_gcm_siv_hw.c
index 4b2117a94c..452c9f00fe 100644
--- a/providers/implementations/ciphers/cipher_aes_gcm_siv_hw.c
+++ b/providers/implementations/ciphers/cipher_aes_gcm_siv_hw.c
@@ -58,6 +58,9 @@ static int aes_gcm_siv_initkey(void *vctx)
     memset(&data, 0, sizeof(data));
     memcpy(&data.block[sizeof(data.counter)], ctx->nonce, NONCE_SIZE);

+    ctx->generated_tag = 0;
+    memset(ctx->tag, 0, TAG_SIZE);
+
     /* msg_auth_key is always 16 bytes in size, regardless of AES128/AES256 */
     /* counter is stored little-endian */
     for (i = 0; i < BLOCK_SIZE; i += 8) {
@@ -134,17 +137,6 @@ static int aes_gcm_siv_aad(PROV_AES_GCM_SIV_CTX *ctx,
     return 1;
 }

-static int aes_gcm_siv_finish(PROV_AES_GCM_SIV_CTX *ctx)
-{
-    int ret = 0;
-
-    if (ctx->enc)
-        return ctx->generated_tag;
-    ret = !CRYPTO_memcmp(ctx->tag, ctx->user_tag, sizeof(ctx->tag));
-    ret &= ctx->have_user_tag;
-    return ret;
-}
-
 static int aes_gcm_siv_encrypt(PROV_AES_GCM_SIV_CTX *ctx, const unsigned char *in,
     unsigned char *out, size_t len)
 {
@@ -271,6 +263,19 @@ static int aes_gcm_siv_decrypt(PROV_AES_GCM_SIV_CTX *ctx, const unsigned char *i
     return !error;
 }

+static int aes_gcm_siv_finish(PROV_AES_GCM_SIV_CTX *ctx)
+{
+    int ret = 0;
+
+    if (ctx->enc)
+        return ctx->generated_tag;
+    if (!ctx->generated_tag)
+        aes_gcm_siv_decrypt(ctx, NULL, NULL, 0);
+    ret = !CRYPTO_memcmp(ctx->tag, ctx->user_tag, sizeof(ctx->tag));
+    ret &= ctx->have_user_tag;
+    return ret;
+}
+
 static int aes_gcm_siv_cipher(void *vctx, unsigned char *out,
     const unsigned char *in, size_t len)
 {
diff --git a/providers/implementations/ciphers/cipher_aes_siv.c b/providers/implementations/ciphers/cipher_aes_siv.c
index 38f6977bf7..f67c015f86 100644
--- a/providers/implementations/ciphers/cipher_aes_siv.c
+++ b/providers/implementations/ciphers/cipher_aes_siv.c
@@ -193,6 +193,7 @@ static int aes_siv_set_ctx_params(void *vctx, const OSSL_PARAM params[])
     PROV_AES_SIV_CTX *ctx = (PROV_AES_SIV_CTX *)vctx;
     struct aes_siv_set_ctx_params_st p;
     unsigned int speed = 0;
+    SIV128_CONTEXT *sctx = &ctx->siv;

     if (ctx == NULL || !aes_siv_set_ctx_params_decoder(params, &p))
         return 0;
@@ -226,6 +227,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 0965f73032..d020f91aff 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -7164,6 +7164,142 @@ static int test_aes_rc4_keylen_change_cve_2023_5363(void)
 }
 #endif

+static int test_aes_gcm_siv_empty_data(void)
+{
+    unsigned char key[16] = { 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
+        0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10 };
+    unsigned char nonce[12] = { 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 0x00, 0x11,
+        0x22, 0x33, 0x44, 0x55 };
+    unsigned char aad[33] = "this AAD was never authenticated";
+    unsigned char zero_tag[16] = { 0 };
+    unsigned char real_tag[16];
+    unsigned char out[16];
+    int outl, ret = 0;
+    EVP_CIPHER_CTX *ctx = NULL;
+    EVP_CIPHER *c = EVP_CIPHER_fetch(NULL, "AES-128-GCM-SIV", NULL);
+
+    if (c == NULL) {
+        return TEST_skip("AES-128-GCM-SIV cipher is not available");
+    }
+
+    /* Compute the CORRECT tag for (key,nonce,aad,pt="") via encrypt */
+    ctx = EVP_CIPHER_CTX_new();
+    if (!TEST_ptr(ctx)
+        || !TEST_true(EVP_EncryptInit_ex2(ctx, c, key, nonce, NULL))
+        || !TEST_true(EVP_EncryptUpdate(ctx, NULL, &outl, aad, sizeof(aad))) /* AAD */
+        || !TEST_true(EVP_EncryptUpdate(ctx, out, &outl, aad, 0)) /* empty PT, out!=NULL */
+        || !TEST_true(EVP_EncryptFinal_ex(ctx, out, &outl))
+        || !TEST_true(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_GET_TAG, 16, real_tag)))
+        goto err;
+    EVP_CIPHER_CTX_free(ctx);
+
+    /* SANITY: decrypt with CORRECT tag and an explicit empty-PT Update */
+    ctx = EVP_CIPHER_CTX_new();
+    if (!TEST_ptr(ctx)
+        || !TEST_true(EVP_DecryptInit_ex2(ctx, c, key, nonce, NULL))
+        || !TEST_true(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, real_tag))
+        || !TEST_true(EVP_DecryptUpdate(ctx, NULL, &outl, aad, sizeof(aad)))
+        || !TEST_true(EVP_DecryptUpdate(ctx, out, &outl, aad, 0)) /* force aes_gcm_siv_decrypt(len=0) */
+        || !TEST_true(EVP_DecryptFinal_ex(ctx, out, &outl)))
+        goto err;
+    EVP_CIPHER_CTX_free(ctx);
+
+    /* FORGERY A: AAD only, NO ciphertext Update, ALL-ZERO tag */
+    ctx = EVP_CIPHER_CTX_new();
+    if (!TEST_ptr(ctx)
+        || !TEST_true(EVP_DecryptInit_ex2(ctx, c, key, nonce, NULL))
+        || !TEST_true(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, zero_tag))
+        || !TEST_true(EVP_DecryptUpdate(ctx, NULL, &outl, aad, sizeof(aad))) /* AAD only, out==NULL */
+        || !TEST_false(EVP_DecryptFinal_ex(ctx, out, &outl)))
+        goto err;
+    EVP_CIPHER_CTX_free(ctx);
+
+    /* FORGERY B: no AAD, no Update at all, ALL-ZERO tag */
+    ctx = EVP_CIPHER_CTX_new();
+    if (!TEST_ptr(ctx)
+        || !TEST_true(EVP_DecryptInit_ex2(ctx, c, key, nonce, NULL))
+        || !TEST_true(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, zero_tag))
+        || !TEST_false(EVP_DecryptFinal_ex(ctx, out, &outl)))
+        goto err;
+    EVP_CIPHER_CTX_free(ctx);
+
+    /* CONTROL: AAD only, NO ciphertext Update, CORRECT tag */
+    ctx = EVP_CIPHER_CTX_new();
+    if (!TEST_ptr(ctx)
+        || !TEST_true(EVP_DecryptInit_ex2(ctx, c, key, nonce, NULL))
+        || !TEST_true(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG, 16, real_tag))
+        || !TEST_true(EVP_DecryptUpdate(ctx, NULL, &outl, aad, sizeof(aad)))
+        || !TEST_true(EVP_DecryptFinal_ex(ctx, out, &outl)))
+        goto err;
+    EVP_CIPHER_CTX_free(ctx);
+    ctx = NULL;
+
+    ret = 1;
+err:
+    EVP_CIPHER_CTX_free(ctx);
+
+    EVP_CIPHER_free(c);
+    return ret;
+}
+
+/*
+ * 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;
+}
+
 static int test_invalid_ctx_for_digest(void)
 {
     int ret;
@@ -8402,6 +8538,10 @@ int setup_tests(void)

     ADD_ALL_TESTS(test_aead_oneshot_roundtrip, 2 * OSSL_NELEM(aead_oneshot_cfgs));

+    /* Test cases for CVE-2026-45446 */
+    ADD_TEST(test_aes_gcm_siv_empty_data);
+    ADD_TEST(test_aes_siv_ctx_reuse);
+
     ADD_TEST(test_invalid_ctx_for_digest);

     ADD_TEST(test_evp_cipher_negative_length);