Commit 283f6bd6bf for openssl.org
commit 283f6bd6bff8aa0ca019573fdaea10d0bfea0067
Author: Billy Brumley <bbb@iki.fi>
Date: Thu Jul 9 05:33:01 2026 -0400
[test] check late AAD rejection across AEADs
A late AAD update (AAD supplied after the payload has started) must be
rejected, and reported the same way, for every AEAD. #31673 checked this
for ChaCha20-Poly1305 alone, so this change extends it to all AEADs.
test_evp_aead_late_aad covers both the encrypt and decrypt directions and
asserts ERR_LIB_PROV / PROV_R_UPDATE_CALL_OUT_OF_ORDER on the late update.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Daniel Kubec <kubec@openssl.foundation>
Reviewed-by: Paul Dale <paul.dale@oracle.com>
MergeDate: Mon Jul 13 15:40:39 2026
(Merged from https://github.com/openssl/openssl/pull/31906)
diff --git a/providers/implementations/ciphers/cipher_chacha20_poly1305_hw.c b/providers/implementations/ciphers/cipher_chacha20_poly1305_hw.c
index ffd2ee744c..31a163e57e 100644
--- a/providers/implementations/ciphers/cipher_chacha20_poly1305_hw.c
+++ b/providers/implementations/ciphers/cipher_chacha20_poly1305_hw.c
@@ -9,6 +9,7 @@
/* chacha20_poly1305 cipher implementation */
+#include <openssl/proverr.h>
#include "internal/endian.h"
#include "cipher_chacha20_poly1305.h"
@@ -301,8 +302,10 @@ static int chacha20_poly1305_aead_cipher(PROV_CIPHER_CTX *bctx,
if (in != NULL) { /* aad or text */
if (out == NULL) { /* aad */
- if (ctx->len.text != 0)
+ if (ctx->len.text != 0) {
+ ERR_raise(ERR_LIB_PROV, PROV_R_UPDATE_CALL_OUT_OF_ORDER);
goto err;
+ }
Poly1305_Update(poly, in, inl);
ctx->len.aad += inl;
ctx->aad = 1;
diff --git a/test/evp_extra_test.c b/test/evp_extra_test.c
index c767114f39..f1de702ec7 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -6054,6 +6054,134 @@ err:
return testresult;
}
+/*
+ * With AEAD ciphers, associated data must precede the payload. Once plaintext
+ * or ciphertext processing has begun, a further AAD update (out == NULL) must
+ * be rejected, and the rejection must be reported the same way across every
+ * AEAD: ERR_LIB_PROV / PROV_R_UPDATE_CALL_OUT_OF_ORDER. The invariant is
+ * checked in both the encrypt and decrypt directions.
+ */
+static int test_evp_aead_late_aad(int idx)
+{
+ const EVP_CIPHER_TEST_INFO *info = &cipher_list[idx];
+ EVP_CIPHER_CTX *ctx_enc = NULL; /* late AAD after plaintext: must fail */
+ EVP_CIPHER_CTX *ctx_dec = NULL; /* late AAD after ciphertext: must fail */
+
+ unsigned char key[EVP_MAX_KEY_LENGTH] = { 0 };
+ unsigned char iv[EVP_MAX_IV_LENGTH] = { 0 };
+ unsigned char aad[] = "aad";
+ unsigned char msg[] = "message";
+ unsigned char out[sizeof(msg) + EVP_MAX_BLOCK_LENGTH];
+
+ int i = 0, len = 0, testresult = 0, expected = 0;
+ char *errmsg = NULL;
+ unsigned long err_code = 0;
+
+ if (info->taglen == 0 /* skip non-AEAD */
+ || info->mode == EVP_CIPH_GCM_MODE /* rejects, raises 102 PROV_R_CIPHER_OPERATION_FAILED */
+ || info->mode == EVP_CIPH_CCM_MODE /* fails at first AAD */
+ || info->mode == EVP_CIPH_OCB_MODE /* accepts late AAD */
+ || info->mode == EVP_CIPH_GCM_SIV_MODE /* accepts late AAD */
+ /* skip TLS stitched MTE cipher */
+ || EVP_CIPHER_is_a(info->ciph, "AES-128-CBC-HMAC-SHA1")
+ /* skip TLS stitched MTE cipher */
+ || EVP_CIPHER_is_a(info->ciph, "AES-256-CBC-HMAC-SHA1")
+ /* skip TLS stitched MTE cipher */
+ || EVP_CIPHER_is_a(info->ciph, "AES-128-CBC-HMAC-SHA256")
+ /* skip TLS stitched MTE cipher */
+ || EVP_CIPHER_is_a(info->ciph, "AES-256-CBC-HMAC-SHA256"))
+ return 1;
+
+ for (i = 0; i < info->keylen && i < (int)sizeof(key); i++)
+ key[i] = (unsigned char)(0xA0 + i);
+ for (i = 0; i < info->ivlen && i < (int)sizeof(iv); i++)
+ iv[i] = (unsigned char)(0xB0 + i);
+
+ /* encrypt: aad, then plaintext, then a late aad update must be rejected */
+ if (!TEST_ptr(ctx_enc = EVP_CIPHER_CTX_new())) {
+ errmsg = "ENC_ALLOC";
+ goto err;
+ }
+ if (!TEST_true(EVP_EncryptInit_ex2(ctx_enc, info->ciph, key, iv, NULL))) {
+ errmsg = "ENC_INIT";
+ goto err;
+ }
+ if (!TEST_true(EVP_EncryptUpdate(ctx_enc, NULL, &len, aad, sizeof(aad)))) {
+ errmsg = "ENC_AAD";
+ goto err;
+ }
+ if (!TEST_true(EVP_EncryptUpdate(ctx_enc, out, &len, msg, sizeof(msg)))) {
+ errmsg = "ENC_PLAINTEXT";
+ goto err;
+ }
+ ERR_set_mark();
+ if (!TEST_false(EVP_EncryptUpdate(ctx_enc, NULL, &len, aad, sizeof(aad)))) {
+ ERR_clear_last_mark();
+ errmsg = "ENC_LATE_AAD_NOT_REJECTED";
+ goto err;
+ }
+ err_code = ERR_peek_last_error();
+ if (!TEST_int_eq(ERR_GET_LIB(err_code), ERR_LIB_PROV)
+ || !TEST_int_eq(ERR_GET_REASON(err_code), PROV_R_UPDATE_CALL_OUT_OF_ORDER)) {
+ ERR_clear_last_mark();
+ expected = PROV_R_UPDATE_CALL_OUT_OF_ORDER;
+ errmsg = "ENC_LATE_AAD_WRONG_REASON";
+ goto err;
+ }
+ ERR_pop_to_mark();
+
+ /* decrypt: same sequence, late aad after ciphertext must be rejected */
+ if (!TEST_ptr(ctx_dec = EVP_CIPHER_CTX_new())) {
+ errmsg = "DEC_ALLOC";
+ goto err;
+ }
+ if (!TEST_true(EVP_DecryptInit_ex2(ctx_dec, info->ciph, key, iv, NULL))) {
+ errmsg = "DEC_INIT";
+ goto err;
+ }
+ if (!TEST_true(EVP_DecryptUpdate(ctx_dec, NULL, &len, aad, sizeof(aad)))) {
+ errmsg = "DEC_AAD";
+ goto err;
+ }
+ /* the ciphertext content is irrelevant; the tag is never finalized here */
+ if (!TEST_true(EVP_DecryptUpdate(ctx_dec, out, &len, msg, sizeof(msg)))) {
+ errmsg = "DEC_CIPHERTEXT";
+ goto err;
+ }
+ ERR_set_mark();
+ if (!TEST_false(EVP_DecryptUpdate(ctx_dec, NULL, &len, aad, sizeof(aad)))) {
+ ERR_clear_last_mark();
+ errmsg = "DEC_LATE_AAD_NOT_REJECTED";
+ goto err;
+ }
+ err_code = ERR_peek_last_error();
+ if (!TEST_int_eq(ERR_GET_LIB(err_code), ERR_LIB_PROV)
+ || !TEST_int_eq(ERR_GET_REASON(err_code), PROV_R_UPDATE_CALL_OUT_OF_ORDER)) {
+ ERR_clear_last_mark();
+ expected = PROV_R_UPDATE_CALL_OUT_OF_ORDER;
+ errmsg = "DEC_LATE_AAD_WRONG_REASON";
+ goto err;
+ }
+ ERR_pop_to_mark();
+
+ testresult = 1;
+
+err:
+ if (errmsg != NULL) {
+ if (expected != 0)
+ TEST_info("test_evp_aead_late_aad %d, %s: %s"
+ " (expected reason %d, got %d)",
+ idx, errmsg, info->name,
+ expected, ERR_GET_REASON(err_code));
+ else
+ TEST_info("test_evp_aead_late_aad %d, %s: %s",
+ idx, errmsg, info->name);
+ }
+ EVP_CIPHER_CTX_free(ctx_enc);
+ EVP_CIPHER_CTX_free(ctx_dec);
+ return testresult;
+}
+
/*
* Verify stale key is not being used after providing a new key in multiple steps.
* This test performs a full round of encryption and then changes the
@@ -7761,30 +7889,6 @@ err:
return ret;
}
-#if !defined(OPENSSL_NO_CHACHA) && !defined(OPENSSL_NO_POLY1305)
-static int test_chacha20_poly1305_late_aad(void)
-{
- EVP_CIPHER_CTX *ctx = NULL;
- EVP_CIPHER *c = NULL;
- unsigned char key[32] = { 0 };
- unsigned char iv[12] = { 0 };
- unsigned char aad[4] = "aad";
- unsigned char msg[8] = "message";
- unsigned char out[32];
- int len, test;
-
- test = TEST_ptr(ctx = EVP_CIPHER_CTX_new())
- && TEST_ptr(c = EVP_CIPHER_fetch(testctx, "ChaCha20-Poly1305", testpropq))
- && TEST_true(EVP_EncryptInit_ex2(ctx, c, key, iv, NULL))
- && TEST_true(EVP_EncryptUpdate(ctx, NULL, &len, aad, sizeof(aad)))
- && TEST_true(EVP_EncryptUpdate(ctx, out, &len, msg, sizeof(msg)))
- && TEST_false(EVP_EncryptUpdate(ctx, NULL, &len, aad, sizeof(aad)));
-
- EVP_CIPHER_free(c);
- EVP_CIPHER_CTX_free(ctx);
- return test;
-}
-#endif
/*
* AES-SIV reuse-without-rekey:
* msg1: legit non-empty CT, tag verifies, final_ret=0
@@ -9078,7 +9182,6 @@ int setup_tests(void)
#endif
#if !defined(OPENSSL_NO_CHACHA) && !defined(OPENSSL_NO_POLY1305)
ADD_TEST(test_decrypt_null_chunks);
- ADD_TEST(test_chacha20_poly1305_late_aad);
#endif
#ifndef OPENSSL_NO_DH
ADD_TEST(test_DH_priv_pub);
@@ -9122,6 +9225,7 @@ int setup_tests(void)
ADD_ALL_TESTS(test_evp_decrypt_roundtrip_multistep, cipher_list_n);
ADD_ALL_TESTS(test_evp_oneshot_aead_zerolen, cipher_list_n);
ADD_ALL_TESTS(test_evp_aead_tag_direction, cipher_list_n);
+ ADD_ALL_TESTS(test_evp_aead_late_aad, cipher_list_n);
ADD_ALL_TESTS(test_evp_init_seq, OSSL_NELEM(evp_init_tests));
ADD_ALL_TESTS(test_evp_reset, OSSL_NELEM(evp_reset_tests));