Commit 4c77e4679d for openssl.org

commit 4c77e4679d6dd78c74fcf30719641a97aaf041eb
Author: Billy Brumley <bbb@iki.fi>
Date:   Thu Sep 17 18:45:26 2026 -0400

    check that AEADs reject Update after Final

    Once Final has run, feeding more data to the same context must fail in
    both directions. This was only checked for a handful of ciphers in
    evp_extra_test.c; run it against every AEAD exposed by the lib.

    Assisted-by: Claude:claude-fable-5-1
    Reviewed-by: Daniel Kubec <kubec@openssl.foundation>
    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Reviewed-by: Paul Dale <paul.dale@oracle.com>
    Merge-date: Thu Oct  1 19:07:26 2026
    Merged-from: https://github.com/openssl/openssl/pull/32875

diff --git a/test/evp_aead_test.c b/test/evp_aead_test.c
index bc9c0493f9..912f48c60f 100644
--- a/test/evp_aead_test.c
+++ b/test/evp_aead_test.c
@@ -519,6 +519,97 @@ err:
     return testresult;
 }

+/*
+ * After Final, a further Update on the same context must be rejected, both
+ * encrypting and decrypting.
+ *
+ * TODO: also check the error reason (PROV_R_CIPHER_OPERATION_FAILED, as GCM
+ * and CCM report); SIV, GCM-SIV and ChaCha20-Poly1305 currently reject
+ * without raising one.
+ */
+static int test_evp_aead_finished_ctx(int idx)
+{
+    const AEAD_DATA *info = &aead_list[idx];
+    EVP_CIPHER_CTX *ctx_enc = NULL;
+    EVP_CIPHER_CTX *ctx_dec = NULL;
+
+    OSSL_PARAM tagparams[2];
+
+    int taglen = info->taglen;
+    unsigned char key[EVP_MAX_KEY_LENGTH] = { 0 };
+    unsigned char iv[EVP_MAX_IV_LENGTH] = { 0 };
+    unsigned char tag[EVPTEST_TAG_LEN_MAX] = { 0 };
+
+    static const unsigned char msg[] = "finished context regression";
+    unsigned char ct[sizeof(msg) + EVP_MAX_BLOCK_LENGTH] = { 0 };
+    unsigned char pt[sizeof(msg) + EVP_MAX_BLOCK_LENGTH] = { 0 };
+
+    int i = 0, len = 0, testresult = 0;
+
+    for (i = 0; i < info->keylen; i++)
+        key[i] = (unsigned char)(0xA0 + i);
+    for (i = 0; i < info->ivlen; i++)
+        iv[i] = (unsigned char)(0xB0 + i);
+
+    /* encrypt: Init, (CCM len), Update, Final */
+    if (!TEST_ptr(ctx_enc = EVP_CIPHER_CTX_new())
+        || !TEST_true(EVP_EncryptInit_ex2(ctx_enc, info->ciph, key, iv, NULL))
+        || (info->mode == EVP_CIPH_CCM_MODE
+            && !TEST_true(EVP_EncryptUpdate(ctx_enc, NULL, &len, NULL,
+                sizeof(msg))))
+        || !TEST_true(EVP_EncryptUpdate(ctx_enc, ct, &len, msg, sizeof(msg)))
+        || !TEST_true(EVP_EncryptFinal_ex(ctx_enc, ct + len, &len))) {
+        TEST_info("%s: encrypt", info->name);
+        goto err;
+    }
+
+    /* TODO: drop once AEAD_DATA.taglen is resolved from the cipher */
+    i = EVP_CIPHER_CTX_get_tag_length(ctx_enc);
+    if (i > 0)
+        taglen = i;
+
+    tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+        tag, taglen);
+    tagparams[1] = OSSL_PARAM_construct_end();
+    if (!TEST_true(EVP_CIPHER_CTX_get_params(ctx_enc, tagparams))) {
+        TEST_info("%s: encrypt get tag", info->name);
+        goto err;
+    }
+
+    /* the finished encrypt context must reject another Update */
+    if (!TEST_false(EVP_EncryptUpdate(ctx_enc, pt, &len, msg, sizeof(msg)))) {
+        TEST_info("%s: encrypt Update after Final", info->name);
+        goto err;
+    }
+
+    /* decrypt: Init, set tag, (CCM len), Update, Final, compare */
+    if (!TEST_ptr(ctx_dec = EVP_CIPHER_CTX_new())
+        || !TEST_true(EVP_DecryptInit_ex2(ctx_dec, info->ciph, key, iv, NULL))
+        || !TEST_true(EVP_CIPHER_CTX_set_params(ctx_dec, tagparams))
+        || (info->mode == EVP_CIPH_CCM_MODE
+            && !TEST_true(EVP_DecryptUpdate(ctx_dec, NULL, &len, NULL,
+                sizeof(msg))))
+        || !TEST_true(EVP_DecryptUpdate(ctx_dec, pt, &len, ct, sizeof(msg)))
+        || !TEST_true(EVP_DecryptFinal_ex(ctx_dec, pt + len, &len))
+        || !TEST_mem_eq(pt, sizeof(msg), msg, sizeof(msg))) {
+        TEST_info("%s: decrypt", info->name);
+        goto err;
+    }
+
+    /* the finished decrypt context must reject another Update */
+    if (!TEST_false(EVP_DecryptUpdate(ctx_dec, pt, &len, ct, sizeof(msg)))) {
+        TEST_info("%s: decrypt Update after Final", info->name);
+        goto err;
+    }
+
+    testresult = 1;
+err:
+    ERR_clear_error();
+    EVP_CIPHER_CTX_free(ctx_enc);
+    EVP_CIPHER_CTX_free(ctx_dec);
+    return testresult;
+}
+
 int setup_tests(void)
 {
     int i = 0;
@@ -535,6 +626,7 @@ int setup_tests(void)
     ADD_ALL_TESTS(test_evp_aead_params, aead_list_n);
     ADD_ALL_TESTS(test_evp_oneshot_aead_zerolen, aead_list_n);
     ADD_ALL_TESTS(test_evp_aead_late_aad, aead_list_n);
+    ADD_ALL_TESTS(test_evp_aead_finished_ctx, aead_list_n);
     return 1;
 }