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));