Commit f7d2d2acef for openssl.org
commit f7d2d2acef273929cf75e17374f263b483e58662
Author: Billy Brumley <bbb@iki.fi>
Date: Sat Aug 29 02:20:52 2026 -0400
AEADs: add an error to the queue on tag mismatch
Decrypt with a wrong tag for each AEAD and inspect the error reason left
on the queue. Repeat for ciphertext only, AAD only, and ciphertext + AAD.
The tag rejection must fail at EVP_DecryptFinal_ex(), and leave error
PROV_R_BAD_DECRYPT on the queue.
Assisted-by: Claude:claude-opus-4-8
Reviewed-by: Neil Horman <nhorman@openssl.org>
Reviewed-by: Daniel Kubec <kubec@openssl.foundation>
Reviewed-by: Tomas Mraz <tomas@openssl.foundation>
Merge-date: Fri Sep 4 17:39:21 2026
Merged-from: https://github.com/openssl/openssl/pull/32587
diff --git a/crypto/modes/gcm128.c b/crypto/modes/gcm128.c
index b4cab1bc00..c4b937c680 100644
--- a/crypto/modes/gcm128.c
+++ b/crypto/modes/gcm128.c
@@ -1551,6 +1551,13 @@ int CRYPTO_gcm128_decrypt_ctr32(GCM128_CONTEXT *ctx,
#endif
}
+/*
+ * Calculate the tag and verify it against the supplied tag.
+ * Returns:
+ * -1: invalid tag length
+ * 0: tag verified
+ * >0: tag mismatch
+ */
int CRYPTO_gcm128_finish(GCM128_CONTEXT *ctx, const unsigned char *tag,
size_t len)
{
diff --git a/crypto/modes/ocb128.c b/crypto/modes/ocb128.c
index c6b906a56b..7d44ff1a08 100644
--- a/crypto/modes/ocb128.c
+++ b/crypto/modes/ocb128.c
@@ -534,6 +534,10 @@ static int ocb_finish(OCB128_CONTEXT *ctx, unsigned char *tag, size_t len,
/*
* Calculate the tag and verify it against the supplied tag
+ * Returns:
+ * -1: invalid tag length
+ * 0: tag verified
+ * >0: tag mismatch
*/
int CRYPTO_ocb128_finish(OCB128_CONTEXT *ctx, const unsigned char *tag,
size_t len)
diff --git a/providers/implementations/ciphers/cipher_aes_gcm_siv_hw.c b/providers/implementations/ciphers/cipher_aes_gcm_siv_hw.c
index f215b9291f..d387973894 100644
--- a/providers/implementations/ciphers/cipher_aes_gcm_siv_hw.c
+++ b/providers/implementations/ciphers/cipher_aes_gcm_siv_hw.c
@@ -279,6 +279,8 @@ static int aes_gcm_siv_finish(PROV_AES_GCM_SIV_CTX *ctx)
return 0;
ret = CRYPTO_memcmp(ctx->tag, ctx->user_tag, sizeof(ctx->tag)) == 0;
ret &= ctx->have_user_tag;
+ if (ret == 0 && ctx->have_user_tag)
+ ERR_raise(ERR_LIB_PROV, PROV_R_BAD_DECRYPT);
return ret;
}
diff --git a/providers/implementations/ciphers/cipher_aes_ocb.c b/providers/implementations/ciphers/cipher_aes_ocb.c
index 2e1b945e2d..433e026598 100644
--- a/providers/implementations/ciphers/cipher_aes_ocb.c
+++ b/providers/implementations/ciphers/cipher_aes_ocb.c
@@ -68,7 +68,14 @@ static ossl_inline int aes_generic_ocb_gettag(PROV_AES_OCB_CTX *ctx,
static ossl_inline int aes_generic_ocb_final(PROV_AES_OCB_CTX *ctx)
{
- return (CRYPTO_ocb128_finish(&ctx->ocb, ctx->tag, ctx->taglen) == 0);
+ int ret = CRYPTO_ocb128_finish(&ctx->ocb, ctx->tag, ctx->taglen);
+
+ /* ret: -1 = bad tag length, 0 = tag verified, otherwise = tag mismatch */
+ if (ret == -1)
+ ERR_raise(ERR_LIB_PROV, PROV_R_INVALID_TAG_LENGTH);
+ else if (ret != 0)
+ ERR_raise(ERR_LIB_PROV, PROV_R_BAD_DECRYPT);
+ return ret == 0;
}
static ossl_inline void aes_generic_ocb_cleanup(PROV_AES_OCB_CTX *ctx)
diff --git a/providers/implementations/ciphers/cipher_chacha20_poly1305_hw.c b/providers/implementations/ciphers/cipher_chacha20_poly1305_hw.c
index 31a163e57e..cd93e338c7 100644
--- a/providers/implementations/ciphers/cipher_chacha20_poly1305_hw.c
+++ b/providers/implementations/ciphers/cipher_chacha20_poly1305_hw.c
@@ -247,6 +247,7 @@ static int chacha20_poly1305_tls_cipher(PROV_CIPHER_CTX *bctx,
if (bctx->enc) {
memcpy(out, ctx->tag, POLY1305_BLOCK_SIZE);
} else {
+ /* TODO: raise PROV_R_BAD_DECRYPT here too? TLS record path, silent for now */
if (CRYPTO_memcmp(tohash, in, POLY1305_BLOCK_SIZE)) {
if (len > POLY1305_BLOCK_SIZE)
memset(out - (len - POLY1305_BLOCK_SIZE), 0,
@@ -381,6 +382,7 @@ static int chacha20_poly1305_aead_cipher(PROV_CIPHER_CTX *bctx,
if (bctx->enc) {
memcpy(out, ctx->tag, POLY1305_BLOCK_SIZE);
} else {
+ /* TODO: raise PROV_R_BAD_DECRYPT here too? TLS record path, silent for now */
if (CRYPTO_memcmp(temp, in, POLY1305_BLOCK_SIZE)) {
memset(out - plen, 0, plen);
goto err;
@@ -389,8 +391,10 @@ static int chacha20_poly1305_aead_cipher(PROV_CIPHER_CTX *bctx,
inl -= POLY1305_BLOCK_SIZE;
}
} else if (!bctx->enc) {
- if (CRYPTO_memcmp(temp, ctx->tag, ctx->tag_len))
+ if (CRYPTO_memcmp(temp, ctx->tag, ctx->tag_len) != 0) {
+ ERR_raise(ERR_LIB_PROV, PROV_R_BAD_DECRYPT);
goto err;
+ }
}
}
finish:
diff --git a/providers/implementations/ciphers/ciphercommon_gcm.c b/providers/implementations/ciphers/ciphercommon_gcm.c
index 5104875a12..fcd4411bdb 100644
--- a/providers/implementations/ciphers/ciphercommon_gcm.c
+++ b/providers/implementations/ciphers/ciphercommon_gcm.c
@@ -470,8 +470,11 @@ static int gcm_cipher_internal(PROV_GCM_CTX *ctx, unsigned char *out,
ERR_raise(ERR_LIB_PROV, PROV_R_TAG_NOT_SET);
goto err;
}
- if (!hw->cipherfinal(ctx, ctx->buf))
+ if (hw->cipherfinal(ctx, ctx->buf) == 0) {
+ if (ctx->enc == 0)
+ ERR_raise(ERR_LIB_PROV, PROV_R_BAD_DECRYPT);
goto err;
+ }
ctx->iv_state = IV_STATE_FINISHED; /* Don't reuse the IV */
goto finish;
}
diff --git a/test/evp_extra_test.c b/test/evp_extra_test.c
index 23a587ce16..9997793153 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -6562,6 +6562,99 @@ err:
return testresult;
}
+/*
+ * A decrypt with a wrong tag must be rejected by EVP_DecryptFinal_ex().
+ * Negative test for the rejection, as well as the expected error reason.
+ * Does ct / aad / ct + aad variants.
+ */
+static int test_evp_aead_tag_reject(int idx)
+{
+ const EVP_CIPHER_TEST_INFO *info = &cipher_list[idx];
+ EVP_CIPHER_CTX *ctx_ct = NULL;
+ EVP_CIPHER_CTX *ctx_aad = NULL;
+ EVP_CIPHER_CTX *ctx_ct_aad = NULL;
+ EVP_CIPHER_CTX *ctx_c_ct = NULL;
+ unsigned char key[EVP_MAX_KEY_LENGTH];
+ unsigned char iv[EVP_MAX_IV_LENGTH];
+ unsigned char aad[] = "aad";
+ unsigned char ct[] = "ciphertext";
+ unsigned char out[sizeof(ct) + EVP_MAX_BLOCK_LENGTH];
+ unsigned char tag[EVPTEST_TAG_LEN_MAX] = { 0xd0 };
+ OSSL_PARAM params[2];
+ int i, len = 0, testresult = 0;
+
+ if (info->taglen == 0 /* skip non-AEAD */
+ || info->mode == EVP_CIPH_CCM_MODE /* verifies at update, not final */
+ /* skip TLS stitched MTE ciphers */
+ || EVP_CIPHER_is_a(info->ciph, "AES-128-CBC-HMAC-SHA1")
+ || EVP_CIPHER_is_a(info->ciph, "AES-256-CBC-HMAC-SHA1")
+ || EVP_CIPHER_is_a(info->ciph, "AES-128-CBC-HMAC-SHA256")
+ || 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)(0x11 + i);
+ for (i = 0; i < info->ivlen && i < (int)sizeof(iv); i++)
+ iv[i] = (unsigned char)(0x22 + i);
+ params[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+ tag, info->taglen);
+ params[1] = OSSL_PARAM_construct_end();
+
+ /* ciphertext only */
+ ERR_clear_error();
+ if (!TEST_ptr(ctx_ct = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_DecryptInit_ex2(ctx_ct, info->ciph, key, iv, params))
+ || !TEST_true(EVP_DecryptUpdate(ctx_ct, out, &len, ct, sizeof(ct)))
+ || !TEST_int_le(EVP_DecryptFinal_ex(ctx_ct, out + len, &len), 0)
+ || !TEST_err_r(ERR_LIB_PROV, PROV_R_BAD_DECRYPT)) {
+ TEST_info("test_evp_aead_tag_reject %s: ciphertext variant", info->name);
+ goto err;
+ }
+
+ /* AAD only */
+ ERR_clear_error();
+ if (!TEST_ptr(ctx_aad = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_DecryptInit_ex2(ctx_aad, info->ciph, key, iv, params))
+ || !TEST_true(EVP_DecryptUpdate(ctx_aad, NULL, &len, aad, sizeof(aad)))
+ || !TEST_int_le(EVP_DecryptFinal_ex(ctx_aad, out, &len), 0)
+ || !TEST_err_r(ERR_LIB_PROV, PROV_R_BAD_DECRYPT)) {
+ TEST_info("test_evp_aead_tag_reject %s: AAD variant", info->name);
+ goto err;
+ }
+
+ /* ciphertext + AAD */
+ ERR_clear_error();
+ if (!TEST_ptr(ctx_ct_aad = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_DecryptInit_ex2(ctx_ct_aad, info->ciph, key, iv, params))
+ || !TEST_true(EVP_DecryptUpdate(ctx_ct_aad, NULL, &len, aad, sizeof(aad)))
+ || !TEST_true(EVP_DecryptUpdate(ctx_ct_aad, out, &len, ct, sizeof(ct)))
+ || !TEST_int_le(EVP_DecryptFinal_ex(ctx_ct_aad, out + len, &len), 0)
+ || !TEST_err_r(ERR_LIB_PROV, PROV_R_BAD_DECRYPT)) {
+ TEST_info("test_evp_aead_tag_reject %s: ciphertext + AAD variant", info->name);
+ goto err;
+ }
+
+ /* ciphertext only, EVP_Cipher() interface */
+ ERR_clear_error();
+ if (!TEST_ptr(ctx_c_ct = EVP_CIPHER_CTX_new())
+ || !TEST_true(EVP_DecryptInit_ex2(ctx_c_ct, info->ciph, key, iv, params))
+ || !TEST_int_ge(EVP_Cipher(ctx_c_ct, out, ct, sizeof(ct)), 0)
+ || !TEST_int_lt(EVP_Cipher(ctx_c_ct, out, NULL, 0), 0)
+ || !TEST_err_r(ERR_LIB_PROV, PROV_R_BAD_DECRYPT)) {
+ TEST_info("test_evp_aead_tag_reject %s: ciphertext variant (EVP_Cipher)",
+ info->name);
+ goto err;
+ }
+
+ testresult = 1;
+err:
+ EVP_CIPHER_CTX_free(ctx_ct);
+ EVP_CIPHER_CTX_free(ctx_aad);
+ EVP_CIPHER_CTX_free(ctx_ct_aad);
+ EVP_CIPHER_CTX_free(ctx_c_ct);
+ 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
@@ -10098,6 +10191,7 @@ int setup_tests(void)
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);
ADD_ALL_TESTS(test_evp_init_seq, OSSL_NELEM(evp_init_tests));
ADD_ALL_TESTS(test_evp_reset, OSSL_NELEM(evp_reset_tests));