Commit 4d8bbfcec0 for openssl.org
commit 4d8bbfcec0b5fa08d6698938354ac92240973648
Author: Trust-Worthy <jondeveloper0@gmail.com>
Date: Wed Sep 9 14:36:41 2026 -0400
Migrate test_evp_oneshot_aead_zerolen to new evp_aead_test.c
Introduces evp_aead_test.c, modeled on evp_xof_test.c, with a
dynamically-populated aead_list (mirroring cipher_list in
evp_extra_test.c) scoped to AEAD ciphers only. Unlike cipher_list,
EVP_CIPH_SIV_MODE is not excluded here.
Migrates test_evp_oneshot_aead_zerolen and its prepare_ccm_no_payload
helper out of evp_extra_test.c as the first test in an incremental,
one-test-per-PR migration.
Assisted-by: Claude:claude-sonnet-5
Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Andrew Dinh <andrewd@openssl.org>
Merge-date: Tue Sep 29 18:45:50 2026
Merged-from: https://github.com/openssl/openssl/pull/32803
diff --git a/test/build.info b/test/build.info
index 74b63b558d..c995d5dcde 100644
--- a/test/build.info
+++ b/test/build.info
@@ -52,6 +52,7 @@ IF[{- !$disabled{tests} -}]
destest mdc2test sha_test \
exptest pbetest localetest evp_pkey_ctx_new_from_name \
evp_pkey_provided_test evp_test evp_extra_test evp_extra_test2 \
+ evp_aead_test \
evp_fetch_prov_test evp_libctx_test ossl_store_test \
v3nametest v3ext byteorder_test punycode_test evp_byname_test \
crltest danetest bad_dtls_test lhash_test sparse_array_test \
@@ -255,6 +256,10 @@ IF[{- !$disabled{tests} -}]
../providers/libcommon.a
ENDIF
+ SOURCE[evp_aead_test]=evp_aead_test.c
+ INCLUDE[evp_aead_test]=../include ../apps/include
+ DEPEND[evp_aead_test]=../libcrypto.a libtestutil.a
+
SOURCE[hpke_test]=hpke_test.c
INCLUDE[hpke_test]=../include ../apps/include
DEPEND[hpke_test]=../libcrypto libtestutil.a
diff --git a/test/evp_aead_test.c b/test/evp_aead_test.c
new file mode 100644
index 0000000000..53b2655925
--- /dev/null
+++ b/test/evp_aead_test.c
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2023-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
+ * in the file LICENSE in the source distribution or at
+ * https://www.openssl.org/source/license.html
+ */
+
+#include <openssl/evp.h>
+#include <openssl/rand.h>
+#include <openssl/core_names.h>
+#include "testutil.h"
+#include "internal/nelem.h"
+
+/*
+ * Dynamically discover all applicable AEAD ciphers and verify their
+ * behavior at the EVP interface level. This test should require zero
+ * maintenance when new AEAD ciphers are added, as the discovery loop
+ * handles them.
+ *
+ * This is a copy of the cipher_list discovery mechanism in
+ * evp_extra_test.c, narrowed to AEAD ciphers only. Unlike that file,
+ * EVP_CIPH_SIV_MODE is deliberately NOT excluded here -- SIV mode is
+ * an AEAD mode and belongs in this table.
+ */
+
+/* maximum AEAD tag buffer size */
+#define EVPTEST_TAG_LEN_MAX EVP_MAX_MD_SIZE
+
+/* default AEAD tag length for standard modes (GCM, CCM, OCB, etc.) */
+#define EVPTEST_TAG_LEN_DEFAULT 16
+
+typedef struct {
+ const EVP_CIPHER *ciph;
+ const char *name;
+ int keylen;
+ int ivlen;
+ int mode;
+ int taglen;
+} AEAD_DATA;
+
+static AEAD_DATA *aead_list = NULL;
+static int aead_list_n = 0;
+
+static int seen_name(const char *name)
+{
+ int i = 0;
+
+ if (name == NULL) {
+ return 1;
+ }
+ for (i = 0; i < aead_list_n; i++) {
+ if (OPENSSL_strcasecmp(aead_list[i].name, name) == 0)
+ return 1;
+ }
+ return 0;
+}
+
+static void collect_aead_cipher_cb(EVP_CIPHER *ciph, void *arg)
+{
+ AEAD_DATA *info = NULL;
+ const char *name = NULL;
+
+ size_t *allocated = arg;
+
+ if (ciph == NULL
+ /*
+ * Only collect AEAD ciphers for this test file.
+ *
+ * Note: unlike evp_extra_test.c's cipher_list, EVP_CIPH_SIV_MODE is
+ * intentionally NOT excluded here -- it's a valid AEAD mode and this
+ * test file exists in part to give it real coverage.
+ */
+ || (EVP_CIPHER_get_flags(ciph) & EVP_CIPH_FLAG_AEAD_CIPHER) == 0
+ /*
+ * Exclude legacy TLS Encrypt-then-MAC (ETM) ciphers.
+ *
+ * These are special-purpose stitched TLS ciphers with a
+ * different calling sequence that breaks this test's logic.
+ */
+ || EVP_CIPHER_is_a(ciph, "AES-128-CBC-HMAC-SHA1")
+ || EVP_CIPHER_is_a(ciph, "AES-256-CBC-HMAC-SHA1")
+ || EVP_CIPHER_is_a(ciph, "AES-128-CBC-HMAC-SHA256")
+ || EVP_CIPHER_is_a(ciph, "AES-256-CBC-HMAC-SHA256")
+ || EVP_CIPHER_is_a(ciph, "AES-128-CBC-HMAC-SHA1-ETM")
+ || EVP_CIPHER_is_a(ciph, "AES-128-CBC-HMAC-SHA256-ETM")
+ || EVP_CIPHER_is_a(ciph, "AES-128-CBC-HMAC-SHA512-ETM")
+ || EVP_CIPHER_is_a(ciph, "AES-192-CBC-HMAC-SHA1-ETM")
+ || EVP_CIPHER_is_a(ciph, "AES-192-CBC-HMAC-SHA256-ETM")
+ || EVP_CIPHER_is_a(ciph, "AES-192-CBC-HMAC-SHA512-ETM")
+ || EVP_CIPHER_is_a(ciph, "AES-256-CBC-HMAC-SHA1-ETM")
+ || EVP_CIPHER_is_a(ciph, "AES-256-CBC-HMAC-SHA256-ETM")
+ || EVP_CIPHER_is_a(ciph, "AES-256-CBC-HMAC-SHA512-ETM")
+ /* fetch name and skip potential dupes */
+ || (name = EVP_CIPHER_get0_name(ciph)) == NULL
+ || seen_name(name) == 1)
+ return;
+
+ /* First pass only counts ciphers to allocate memory. */
+ if (allocated != NULL) {
+ (*allocated)++;
+ return;
+ }
+
+ info = &aead_list[aead_list_n];
+
+ if (!EVP_CIPHER_up_ref(ciph))
+ return;
+
+ info->ciph = ciph;
+ info->name = name;
+ info->keylen = EVP_CIPHER_get_key_length(ciph);
+ info->ivlen = EVP_CIPHER_get_iv_length(ciph);
+ info->mode = EVP_CIPHER_get_mode(ciph);
+ info->taglen = EVPTEST_TAG_LEN_DEFAULT;
+
+ aead_list_n++;
+}
+
+static int setup_aead_list(void)
+{
+ size_t aead_list_size = 0;
+
+ aead_list = NULL;
+ aead_list_n = 0;
+
+ /* First pass counts only, to know how much to allocate. */
+ EVP_CIPHER_do_all_provided(NULL, collect_aead_cipher_cb, &aead_list_size);
+
+ if (!TEST_size_t_gt(aead_list_size, 0))
+ return 0;
+
+ aead_list = OPENSSL_malloc(aead_list_size * sizeof(*aead_list));
+ if (!TEST_ptr(aead_list))
+ return 0;
+
+ /* Second pass actually populates aead_list. */
+ EVP_CIPHER_do_all_provided(NULL, collect_aead_cipher_cb, NULL);
+ return TEST_true(aead_list_n > 0);
+}
+
+static void cleanup_aead_list(void)
+{
+ int i;
+
+ if (aead_list == NULL)
+ return;
+
+ for (i = 0; i < aead_list_n; i++) {
+ if (aead_list[i].ciph != NULL)
+ EVP_CIPHER_free((EVP_CIPHER *)aead_list[i].ciph);
+ }
+
+ OPENSSL_free(aead_list);
+ aead_list = NULL;
+ aead_list_n = 0;
+}
+
+/*
+ * CCM requires the total payload length to be declared up front (via an
+ * EVP_CipherUpdate call with a NULL input buffer) before AAD or payload
+ * can be processed. For a zero-length payload test, this declares a
+ * length of 0, then feeds in AAD. For all other AEAD modes this is a
+ * no-op.
+ */
+static int prepare_ccm_no_payload(EVP_CIPHER_CTX *ctx,
+ const AEAD_DATA *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)
+{
+ const AEAD_DATA *info = &aead_list[idx];
+ EVP_CIPHER_CTX *ctx_stream = NULL; /* reference: final only */
+ EVP_CIPHER_CTX *ctx_oneshot = NULL; /* encrypt via EVP_Cipher(in == NULL) */
+ EVP_CIPHER_CTX *ctx_dec = NULL; /* decrypt via EVP_Cipher(in == NULL) */
+ EVP_CIPHER_CTX *ctx_dec_bad = NULL; /* decrypt with a corrupted tag */
+ EVP_CIPHER_CTX *ctx_dec_s = NULL; /* streaming decrypt */
+ EVP_CIPHER_CTX *ctx_dec_s_bad = NULL; /* streaming decrypt, corrupted tag */
+
+ OSSL_PARAM get_tagparams[2];
+ OSSL_PARAM set_tagparams[2];
+
+ int taglen = info->taglen;
+ unsigned char key[EVP_MAX_KEY_LENGTH] = { 0 };
+ unsigned char iv[EVP_MAX_IV_LENGTH] = { 0 };
+
+ unsigned char ct[16] = { 0 }; /* scratch out; AEAD finalize writes no data */
+
+ unsigned char tag_stream[EVPTEST_TAG_LEN_MAX] = { 0 };
+ unsigned char tag_oneshot[EVPTEST_TAG_LEN_MAX] = { 0 };
+ unsigned char tag_bad[EVPTEST_TAG_LEN_MAX] = { 0 };
+
+ int i = 0, finlen = 0, testresult = 0;
+
+ /* Adding more verbose testing output messages */
+ TEST_info("test_evp_oneshot_aead_zerolen: idx=%d, cipher=%s", idx, info->name);
+
+ 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);
+
+ /* reference: streaming finalize of an empty message */
+ if (!TEST_ptr(ctx_stream = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_EncryptInit_ex2(ctx_stream, info->ciph, key, iv,
+ NULL))
+ || !TEST_true(prepare_ccm_no_payload(ctx_stream, info))
+ || !TEST_true(EVP_EncryptFinal_ex(ctx_stream, ct, &finlen))) {
+ TEST_info("stream encrypt final (reference) failed: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ /* clamp to the negotiated tag length if the cipher reports one */
+ i = EVP_CIPHER_CTX_get_tag_length(ctx_stream);
+ if (i > 0)
+ taglen = i;
+
+ /* bound the memcpy, should never happen but helps static analysis */
+ if (!TEST_int_le(taglen, EVPTEST_TAG_LEN_MAX)) {
+ TEST_info("tag length exceeds buffer: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ get_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+ tag_stream, taglen);
+ get_tagparams[1] = OSSL_PARAM_construct_end();
+ if (!TEST_true(EVP_CIPHER_CTX_get_params(ctx_stream, get_tagparams))) {
+ TEST_info("stream get tag failed: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ /* one-shot encrypt: finalize the empty message with in == NULL */
+ if (!TEST_ptr(ctx_oneshot = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_EncryptInit_ex2(ctx_oneshot, info->ciph, key, iv,
+ NULL))
+ || !TEST_true(prepare_ccm_no_payload(ctx_oneshot, info))
+ || !TEST_int_ge(EVP_Cipher(ctx_oneshot, ct, NULL, 0), 0)) {
+ TEST_info("one-shot encrypt final failed: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ get_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+ tag_oneshot, taglen);
+ if (!TEST_true(EVP_CIPHER_CTX_get_params(ctx_oneshot, get_tagparams))
+ || !TEST_mem_eq(tag_oneshot, taglen, tag_stream, taglen)) {
+ TEST_info("one-shot get tag / compare failed: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ /* one-shot decrypt: the correct tag must verify via in == NULL */
+ set_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+ tag_stream, taglen);
+ set_tagparams[1] = OSSL_PARAM_construct_end();
+ 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, set_tagparams))
+ || !TEST_true(prepare_ccm_no_payload(ctx_dec, info))
+ || !TEST_int_ge(EVP_Cipher(ctx_dec, ct, NULL, 0), 0)) {
+ TEST_info("one-shot decrypt, good tag failed: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ /*
+ * ...and a corrupted tag must be rejected. EVP_Cipher() returns < 0 on a
+ * failed one-shot, so a non-negative result here means verification was
+ * skipped or wrongly accepted the bad tag.
+ */
+ memcpy(tag_bad, tag_stream, taglen);
+ tag_bad[0] ^= 0x01;
+ set_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+ tag_bad, taglen);
+ if (!TEST_ptr(ctx_dec_bad = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_DecryptInit_ex2(ctx_dec_bad, info->ciph, key, iv,
+ NULL))
+ || !TEST_true(EVP_CIPHER_CTX_set_params(ctx_dec_bad, set_tagparams))
+ || !TEST_true(prepare_ccm_no_payload(ctx_dec_bad, info))
+ || !TEST_int_lt(EVP_Cipher(ctx_dec_bad, ct, NULL, 0), 0)) {
+ TEST_info("one-shot decrypt, bad tag failed: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ /* streaming decrypt: the correct tag must verify via EVP_DecryptFinal_ex */
+ set_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+ tag_stream, taglen);
+ if (!TEST_ptr(ctx_dec_s = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_DecryptInit_ex2(ctx_dec_s, info->ciph, key, iv,
+ NULL))
+ || !TEST_true(EVP_CIPHER_CTX_set_params(ctx_dec_s, set_tagparams))
+ || !TEST_true(prepare_ccm_no_payload(ctx_dec_s, info))
+ || !TEST_true(EVP_DecryptFinal_ex(ctx_dec_s, ct, &finlen))) {
+ TEST_info("stream decrypt, good tag failed: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ /* streaming decrypt: a corrupted tag must be rejected */
+ set_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+ tag_bad, taglen);
+ if (!TEST_ptr(ctx_dec_s_bad = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_DecryptInit_ex2(ctx_dec_s_bad, info->ciph, key, iv,
+ NULL))
+ || !TEST_true(EVP_CIPHER_CTX_set_params(ctx_dec_s_bad, set_tagparams))
+ || !TEST_true(prepare_ccm_no_payload(ctx_dec_s_bad, info))
+ || !TEST_false(EVP_DecryptFinal_ex(ctx_dec_s_bad, ct, &finlen))) {
+ TEST_info("stream decrypt, bad tag failed: idx=%d cipher=%s"
+ " mode=%d keylen=%d ivlen=%d taglen=%d",
+ idx, info->name, info->mode, info->keylen, info->ivlen,
+ taglen);
+ goto err;
+ }
+
+ testresult = 1;
+err:
+ EVP_CIPHER_CTX_free(ctx_stream);
+ EVP_CIPHER_CTX_free(ctx_oneshot);
+ EVP_CIPHER_CTX_free(ctx_dec);
+ EVP_CIPHER_CTX_free(ctx_dec_bad);
+ EVP_CIPHER_CTX_free(ctx_dec_s);
+ EVP_CIPHER_CTX_free(ctx_dec_s_bad);
+ return testresult;
+}
+
+int setup_tests(void)
+{
+ if (!setup_aead_list())
+ return 0;
+
+ ADD_ALL_TESTS(test_evp_oneshot_aead_zerolen, aead_list_n);
+ return 1;
+}
+
+void cleanup_tests(void)
+{
+ cleanup_aead_list();
+}
diff --git a/test/evp_extra_test.c b/test/evp_extra_test.c
index e170896d58..a4b00e3249 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -6056,260 +6056,6 @@ 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)
-{
- const EVP_CIPHER_TEST_INFO *info = &cipher_list[idx];
- EVP_CIPHER_CTX *ctx_stream = NULL; /* reference: final only */
- EVP_CIPHER_CTX *ctx_oneshot = NULL; /* encrypt via EVP_Cipher(in == NULL) */
- EVP_CIPHER_CTX *ctx_dec = NULL; /* decrypt via EVP_Cipher(in == NULL) */
- EVP_CIPHER_CTX *ctx_dec_bad = NULL; /* decrypt with a corrupted tag */
- EVP_CIPHER_CTX *ctx_dec_s = NULL; /* streaming decrypt */
- EVP_CIPHER_CTX *ctx_dec_s_bad = NULL; /* streaming decrypt, corrupted tag */
-
- OSSL_PARAM get_tagparams[2];
- OSSL_PARAM set_tagparams[2];
-
- int taglen = info->taglen;
- unsigned char key[EVP_MAX_KEY_LENGTH] = { 0 };
- unsigned char iv[EVP_MAX_IV_LENGTH] = { 0 };
-
- unsigned char ct[16] = { 0 }; /* scratch out; AEAD finalize writes no data */
- int finlen = 0, oneshot_flen = 0, dec_flen = 0;
-
- unsigned char tag_stream[EVPTEST_TAG_LEN_MAX] = { 0 };
- unsigned char tag_oneshot[EVPTEST_TAG_LEN_MAX] = { 0 };
- unsigned char tag_bad[EVPTEST_TAG_LEN_MAX] = { 0 };
-
- int i = 0, testresult = 1;
- char *errmsg = NULL;
-
- /* filter out various modes */
- if (info->taglen == 0
- /* 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);
-
- /* reference: streaming finalize of an empty message */
- if (!TEST_ptr(ctx_stream = EVP_CIPHER_CTX_new())) {
- errmsg = "STREAM_ALLOC";
- goto err;
- }
- if (!TEST_true(EVP_EncryptInit_ex2(ctx_stream, info->ciph, key, iv, NULL))) {
- 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;
- }
-
- /* clamp to the negotiated tag length if the cipher reports one */
- i = EVP_CIPHER_CTX_get_tag_length(ctx_stream);
- if (i > 0)
- taglen = i;
-
- /* bound the memcpy, should never happen but helps static analysis */
- if (taglen > EVPTEST_TAG_LEN_MAX) {
- errmsg = "TAGLEN_EXCEEDS_BUF";
- goto err;
- }
-
- get_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
- tag_stream, taglen);
- get_tagparams[1] = OSSL_PARAM_construct_end();
- if (!TEST_true(EVP_CIPHER_CTX_get_params(ctx_stream, get_tagparams))) {
- errmsg = "STREAM_GET_TAG";
- goto err;
- }
-
- /* one-shot encrypt: finalize the empty message with in == NULL */
- if (!TEST_ptr(ctx_oneshot = EVP_CIPHER_CTX_new())) {
- errmsg = "ONESHOT_ALLOC";
- goto err;
- }
- if (!TEST_true(EVP_EncryptInit_ex2(ctx_oneshot, info->ciph, key, iv, NULL))) {
- 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";
- goto err;
- }
-
- get_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
- tag_oneshot, taglen);
- if (!TEST_true(EVP_CIPHER_CTX_get_params(ctx_oneshot, get_tagparams))) {
- errmsg = "ONESHOT_GET_TAG";
- goto err;
- }
- if (!TEST_mem_eq(tag_oneshot, taglen, tag_stream, taglen)) {
- errmsg = "TAG_MISMATCH_ONESHOT_vs_STREAM";
- goto err;
- }
-
- /* one-shot decrypt: the correct tag must verify via in == NULL */
- 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;
- }
- set_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
- tag_stream, taglen);
- set_tagparams[1] = OSSL_PARAM_construct_end();
- if (!TEST_true(EVP_CIPHER_CTX_set_params(ctx_dec, set_tagparams))) {
- 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";
- goto err;
- }
-
- /*
- * ...and a corrupted tag must be rejected. EVP_Cipher() returns < 0 on a
- * failed one-shot, so a non-negative result here means verification was
- * skipped or wrongly accepted the bad tag.
- */
- memcpy(tag_bad, tag_stream, taglen);
- tag_bad[0] ^= 0x01;
- if (!TEST_ptr(ctx_dec_bad = EVP_CIPHER_CTX_new())) {
- errmsg = "DEC_BAD_ALLOC";
- goto err;
- }
- if (!TEST_true(EVP_DecryptInit_ex2(ctx_dec_bad, info->ciph, key, iv, NULL))) {
- errmsg = "DEC_BAD_INIT";
- goto err;
- }
- set_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
- tag_bad, taglen);
- if (!TEST_true(EVP_CIPHER_CTX_set_params(ctx_dec_bad, set_tagparams))) {
- 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;
- }
-
- /* streaming decrypt: the correct tag must verify via EVP_DecryptFinal_ex */
- if (!TEST_ptr(ctx_dec_s = EVP_CIPHER_CTX_new())) {
- errmsg = "DEC_STREAM_ALLOC";
- goto err;
- }
- if (!TEST_true(EVP_DecryptInit_ex2(ctx_dec_s, info->ciph, key, iv, NULL))) {
- errmsg = "DEC_STREAM_INIT";
- goto err;
- }
- set_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
- tag_stream, taglen);
- if (!TEST_true(EVP_CIPHER_CTX_set_params(ctx_dec_s, set_tagparams))) {
- 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;
- }
-
- /* streaming decrypt: a corrupted tag must be rejected */
- if (!TEST_ptr(ctx_dec_s_bad = EVP_CIPHER_CTX_new())) {
- errmsg = "DEC_STREAM_BAD_ALLOC";
- goto err;
- }
- if (!TEST_true(EVP_DecryptInit_ex2(ctx_dec_s_bad, info->ciph, key, iv, NULL))) {
- errmsg = "DEC_STREAM_BAD_INIT";
- goto err;
- }
- set_tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
- tag_bad, taglen);
- if (!TEST_true(EVP_CIPHER_CTX_set_params(ctx_dec_s_bad, set_tagparams))) {
- 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;
- }
-
-err:
- if (errmsg != NULL) {
- TEST_info("test_evp_oneshot_aead_zerolen %d, %s: %s",
- idx, errmsg, info->name);
- testresult = 0;
- }
- EVP_CIPHER_CTX_free(ctx_stream);
- EVP_CIPHER_CTX_free(ctx_oneshot);
- EVP_CIPHER_CTX_free(ctx_dec);
- EVP_CIPHER_CTX_free(ctx_dec_bad);
- EVP_CIPHER_CTX_free(ctx_dec_s);
- EVP_CIPHER_CTX_free(ctx_dec_s_bad);
- return testresult;
-}
-
/*
* With AEAD ciphers, a tag is an input for decryption (the value to verify)
* and an output of encryption (the generated value). Therefore:
@@ -10329,7 +10075,6 @@ int setup_tests(void)
ADD_ALL_TESTS(test_evp_diff_order_init, cipher_list_n);
ADD_ALL_TESTS(test_evp_stale_key_reinit, cipher_list_n);
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_aead_tag_reject, cipher_list_n);
diff --git a/test/recipes/30-test_evp_aead.t b/test/recipes/30-test_evp_aead.t
new file mode 100644
index 0000000000..3c6eb344fb
--- /dev/null
+++ b/test/recipes/30-test_evp_aead.t
@@ -0,0 +1,10 @@
+#! /usr/bin/env perl
+# Copyright 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
+# in the file LICENSE in the source distribution or at
+# https://www.openssl.org/source/license.html
+
+use OpenSSL::Test::Simple;
+simple_test("test_evp_aead", "evp_aead_test");