Commit a933605a62 for openssl.org
commit a933605a6261dae1bd30bd6e952717d0c1d2db03
Author: Billy Brumley <bbb@iki.fi>
Date: Wed Jun 17 02:17:45 2026 -0400
[test] various zero-length message positive and negative tests for AEAD ciphers
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
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
Reviewed-by: Neil Horman <nhorman@openssl.org>
MergeDate: Wed Jun 24 12:47:11 2026
(Merged from https://github.com/openssl/openssl/pull/31555)
diff --git a/test/evp_extra_test.c b/test/evp_extra_test.c
index 1e3207cd85..b0c37e476e 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -5711,6 +5711,224 @@ err:
EVP_CIPHER_CTX_free(ctx_onestep);
return testresult;
}
+
+/*-
+ * 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
+ */
+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
+ || info->mode == EVP_CIPH_CCM_MODE
+ || info->mode == EVP_CIPH_OCB_MODE
+ || info->mode == EVP_CIPH_GCM_SIV_MODE
+ /* 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")
+ || EVP_CIPHER_is_a(info->ciph, "ChaCha20-Poly1305"))
+ 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(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;
+ }
+ 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;
+ }
+ 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_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(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_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;
+}
+
/*
* 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
@@ -8693,6 +8911,7 @@ 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_init_seq, OSSL_NELEM(evp_init_tests));
ADD_ALL_TESTS(test_evp_reset, OSSL_NELEM(evp_reset_tests));