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;