Commit 97fada9e52 for openssl.org

commit 97fada9e5243a5044a073226fe0894b287040525
Author: Mounir IDRASSI <mounir.idrassi@idrix.fr>
Date:   Sun Aug 9 18:20:35 2026 +0900

    CCM: authenticate empty Final when payload Update is skipped

    CCM Final was routed through an update helper whose pointer-based
    dispatch treats a NULL-input call as a no-op or a length declaration.
    If an empty message skips payload Update, decryption can therefore skip
    tag verification and encryption does not generate a tag.

    Process an empty payload during Final only when the existing state flags
    show that no payload operation took place. Route the NULL-input
    EVP_Cipher() form through the same Final path.

    Enable CCM in the generic zero-length AEAD test. Declare the zero payload
    length and supply AAD while deliberately omitting payload Update.

    Fixes #32253

    Assisted-by: Codex:gpt-5.6-sol
    Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
    Reviewed-by: Bob Beck <beck@openssl.org>
    Merge-date: Fri Aug 21 08:51:03 2026
    Merged-from: https://github.com/openssl/openssl/pull/32256

diff --git a/providers/implementations/ciphers/ciphercommon_ccm.c b/providers/implementations/ciphers/ciphercommon_ccm.c
index 79a61fde8a..70e16aee0a 100644
--- a/providers/implementations/ciphers/ciphercommon_ccm.c
+++ b/providers/implementations/ciphers/ciphercommon_ccm.c
@@ -1,5 +1,5 @@
 /*
- * Copyright 2019-2025 The OpenSSL Project Authors. All Rights Reserved.
+ * Copyright 2019-2026 The OpenSSL Project Authors. All Rights Reserved.
  *
  * Licensed under the Apache License 2.0 (the "License").  You may not use
  * this file except in compliance with the License.  You can obtain a copy
@@ -286,13 +286,19 @@ int ossl_ccm_stream_final(void *vctx, unsigned char *out, size_t *outl,
     size_t outsize)
 {
     PROV_CCM_CTX *ctx = (PROV_CCM_CTX *)vctx;
-    int i;
+    unsigned char dummy_in = 0, dummy_out = 0;

     if (!ossl_prov_is_running())
         return 0;

-    i = ccm_cipher_internal(ctx, out, outl, NULL, 0);
-    if (i <= 0)
+    /*
+     * Encryption sets tag_set after processing the payload, while successful
+     * decryption clears iv_set. Use those transitions to avoid processing an
+     * operation twice.
+     */
+    if (!ctx->key_set
+        || (ctx->iv_set && (!ctx->enc || !ctx->tag_set)
+            && ccm_cipher_internal(ctx, &dummy_out, outl, &dummy_in, 0) <= 0))
         return 0;

     *outl = 0;
@@ -307,6 +313,9 @@ int ossl_ccm_cipher(void *vctx, unsigned char *out, size_t *outl, size_t outsize
     if (!ossl_prov_is_running())
         return 0;

+    if (in == NULL)
+        return ossl_ccm_stream_final(vctx, out, outl, outsize);
+
     if (outsize < inl) {
         ERR_raise(ERR_LIB_PROV, PROV_R_OUTPUT_BUFFER_TOO_SMALL);
         return 0;
diff --git a/test/evp_extra_test.c b/test/evp_extra_test.c
index e810c65612..a308625665 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -5845,12 +5845,29 @@ err:
     return testresult;
 }

+static int prepare_ccm_no_payload(EVP_CIPHER_CTX *ctx,
+    const EVP_CIPHER_TEST_INFO *info)
+{
+    static const unsigned char aad[] = "CCM empty-payload Final regression";
+    int outlen = 0;
+
+    if (info->mode != EVP_CIPH_CCM_MODE)
+        return 1;
+
+    return EVP_CipherUpdate(ctx, NULL, &outlen, NULL, 0) > 0
+        && EVP_CipherUpdate(ctx, NULL, &outlen, aad,
+               (int)sizeof(aad) - 1)
+        > 0;
+}
+
 /*-
  * A zero-length AEAD message driven through the one-shot EVP_Cipher() interface
  * must agree with the streaming EVP_CipherFinal_ex() path. This checks:
  * - an empty message yields the same tag via both interfaces
  * - the true tag passes verification on decrypt
  * - the modified tag fails verification on decrypt
+ * For CCM, each operation declares a zero payload length and supplies AAD, but
+ * deliberately omits the payload Update that would otherwise authenticate it.
  */
 static int test_evp_oneshot_aead_zerolen(int idx)
 {
@@ -5881,7 +5898,6 @@ static int test_evp_oneshot_aead_zerolen(int idx)

     /* filter out various modes */
     if (info->taglen == 0
-        || info->mode == EVP_CIPH_CCM_MODE
         /* skip TLS stitched MTE cipher */
         || EVP_CIPHER_is_a(info->ciph, "AES-128-CBC-HMAC-SHA1")
         /* skip TLS stitched MTE cipher */
@@ -5906,6 +5922,10 @@ static int test_evp_oneshot_aead_zerolen(int idx)
         errmsg = "STREAM_INIT";
         goto err;
     }
+    if (!TEST_true(prepare_ccm_no_payload(ctx_stream, info))) {
+        errmsg = "STREAM_CCM_PREPARE";
+        goto err;
+    }
     if (!TEST_true(EVP_EncryptFinal_ex(ctx_stream, ct, &finlen))) {
         errmsg = "STREAM_FINAL";
         goto err;
@@ -5939,6 +5959,10 @@ static int test_evp_oneshot_aead_zerolen(int idx)
         errmsg = "ONESHOT_INIT";
         goto err;
     }
+    if (!TEST_true(prepare_ccm_no_payload(ctx_oneshot, info))) {
+        errmsg = "ONESHOT_CCM_PREPARE";
+        goto err;
+    }
     oneshot_flen = EVP_Cipher(ctx_oneshot, ct, NULL, 0);
     if (!TEST_int_ge(oneshot_flen, 0)) {
         errmsg = "ONESHOT_FINAL_NULL";
@@ -5972,6 +5996,10 @@ static int test_evp_oneshot_aead_zerolen(int idx)
         errmsg = "DEC_SET_TAG";
         goto err;
     }
+    if (!TEST_true(prepare_ccm_no_payload(ctx_dec, info))) {
+        errmsg = "DEC_CCM_PREPARE";
+        goto err;
+    }
     dec_flen = EVP_Cipher(ctx_dec, ct, NULL, 0);
     if (!TEST_int_ge(dec_flen, 0)) {
         errmsg = "DEC_VERIFY_NULL";
@@ -5999,6 +6027,10 @@ static int test_evp_oneshot_aead_zerolen(int idx)
         errmsg = "DEC_BAD_SET_TAG";
         goto err;
     }
+    if (!TEST_true(prepare_ccm_no_payload(ctx_dec_bad, info))) {
+        errmsg = "DEC_BAD_CCM_PREPARE";
+        goto err;
+    }
     if (!TEST_int_lt(EVP_Cipher(ctx_dec_bad, ct, NULL, 0), 0)) {
         errmsg = "DEC_BADTAG_NOT_REJECTED";
         goto err;
@@ -6019,6 +6051,10 @@ static int test_evp_oneshot_aead_zerolen(int idx)
         errmsg = "DEC_STREAM_SET_TAG";
         goto err;
     }
+    if (!TEST_true(prepare_ccm_no_payload(ctx_dec_s, info))) {
+        errmsg = "DEC_STREAM_CCM_PREPARE";
+        goto err;
+    }
     if (!TEST_true(EVP_DecryptFinal_ex(ctx_dec_s, ct, &finlen))) {
         errmsg = "DEC_STREAM_VERIFY";
         goto err;
@@ -6039,6 +6075,10 @@ static int test_evp_oneshot_aead_zerolen(int idx)
         errmsg = "DEC_STREAM_BAD_SET_TAG";
         goto err;
     }
+    if (!TEST_true(prepare_ccm_no_payload(ctx_dec_s_bad, info))) {
+        errmsg = "DEC_STREAM_BAD_CCM_PREPARE";
+        goto err;
+    }
     if (!TEST_false(EVP_DecryptFinal_ex(ctx_dec_s_bad, ct, &finlen))) {
         errmsg = "DEC_STREAM_BADTAG_NOT_REJECTED";
         goto err;