Commit ea655177e0 for openssl.org

commit ea655177e0d0dd7885640d1d9a921ef6d2c9bf8c
Author: Billy Brumley <bbb@iki.fi>
Date:   Thu Jun 25 07:20:25 2026 -0400

    [test] check tag abuse for AEAD ciphers

    With AEAD ciphers, a tag is an input for decryption (the value to verify)
    and an output of encryption (the generated value). Therefore:
    - supplying a tag value while encrypting must fail
    - reading a tag while decrypting must fail
    - error codes should be consistent across all AEADs

    Assisted-by: Claude:claude-opus-4-8

    Reviewed-by: Dmitry Belyavskiy <beldmit@gmail.com>
    Reviewed-by: Daniel Kubec <kubec@openssl.foundation>
    MergeDate: Tue Jun 30 07:47:03 2026
    (Merged from https://github.com/openssl/openssl/pull/31734)

diff --git a/providers/implementations/ciphers/cipher_aes_gcm_siv.c b/providers/implementations/ciphers/cipher_aes_gcm_siv.c
index 1fd97fe3d1..2f4a86dc56 100644
--- a/providers/implementations/ciphers/cipher_aes_gcm_siv.c
+++ b/providers/implementations/ciphers/cipher_aes_gcm_siv.c
@@ -182,8 +182,11 @@ static int ossl_aes_gcm_siv_get_ctx_params(void *vctx, OSSL_PARAM params[])
         return 0;

     if (p.tag != NULL && p.tag->data_type == OSSL_PARAM_OCTET_STRING) {
-        if (!ctx->enc || !ctx->generated_tag
-            || p.tag->data_size != sizeof(ctx->tag)
+        if (!ctx->enc || !ctx->generated_tag) {
+            ERR_raise(ERR_LIB_PROV, PROV_R_TAG_NOT_SET);
+            return 0;
+        }
+        if (p.tag->data_size != sizeof(ctx->tag)
             || !OSSL_PARAM_set_octet_string(p.tag, ctx->tag,
                 sizeof(ctx->tag))) {
             ERR_raise(ERR_LIB_PROV, PROV_R_FAILED_TO_SET_PARAMETER);
@@ -227,6 +230,9 @@ static int ossl_aes_gcm_siv_set_ctx_params(void *vctx, const OSSL_PARAM params[]
         if (!ctx->enc) {
             memcpy(ctx->user_tag, p.tag->data, sizeof(ctx->tag));
             ctx->have_user_tag = 1;
+        } else if (p.tag->data != NULL) {
+            ERR_raise(ERR_LIB_PROV, PROV_R_TAG_NOT_NEEDED);
+            return 0;
         }
     }

diff --git a/providers/implementations/ciphers/cipher_aes_ocb.c b/providers/implementations/ciphers/cipher_aes_ocb.c
index 329d4bfa3e..1bd5281cc2 100644
--- a/providers/implementations/ciphers/cipher_aes_ocb.c
+++ b/providers/implementations/ciphers/cipher_aes_ocb.c
@@ -376,7 +376,7 @@ static int aes_ocb_set_ctx_params(void *vctx, const OSSL_PARAM params[])
             ctx->taglen = p.tag->data_size;
         } else {
             if (ctx->base.enc) {
-                ERR_raise(ERR_LIB_PROV, ERR_R_PASSED_INVALID_ARGUMENT);
+                ERR_raise(ERR_LIB_PROV, PROV_R_TAG_NOT_NEEDED);
                 return 0;
             }
             if (p.tag->data_size != ctx->taglen) {
@@ -476,7 +476,11 @@ static int aes_ocb_get_ctx_params(void *vctx, OSSL_PARAM params[])
             ERR_raise(ERR_LIB_PROV, PROV_R_FAILED_TO_GET_PARAMETER);
             return 0;
         }
-        if (!ctx->base.enc || p.tag->data_size != ctx->taglen) {
+        if (!ctx->base.enc) {
+            ERR_raise(ERR_LIB_PROV, PROV_R_TAG_NOT_SET);
+            return 0;
+        }
+        if (p.tag->data_size != ctx->taglen) {
             ERR_raise(ERR_LIB_PROV, PROV_R_INVALID_TAG_LENGTH);
             return 0;
         }
diff --git a/providers/implementations/ciphers/ciphercommon_gcm.c b/providers/implementations/ciphers/ciphercommon_gcm.c
index d9be50f513..c93b0767b0 100644
--- a/providers/implementations/ciphers/ciphercommon_gcm.c
+++ b/providers/implementations/ciphers/ciphercommon_gcm.c
@@ -215,7 +215,7 @@ int ossl_gcm_get_ctx_params(void *vctx, OSSL_PARAM params[])
     if (p.tag != NULL) {
         sz = p.tag->data_size;
         if (!ctx->enc || ctx->taglen == UNINITIALISED_SIZET) {
-            ERR_raise(ERR_LIB_PROV, PROV_R_INVALID_TAG);
+            ERR_raise(ERR_LIB_PROV, PROV_R_TAG_NOT_SET);
             return 0;
         }
         if (p.tag->data != NULL && (sz > EVP_GCM_TLS_TAG_LEN || sz == 0)) {
@@ -263,7 +263,11 @@ int ossl_gcm_set_ctx_params(void *vctx, const OSSL_PARAM params[])
             ERR_raise(ERR_LIB_PROV, PROV_R_FAILED_TO_GET_PARAMETER);
             return 0;
         }
-        if (sz == 0 || ctx->enc) {
+        if (ctx->enc) {
+            ERR_raise(ERR_LIB_PROV, PROV_R_TAG_NOT_NEEDED);
+            return 0;
+        }
+        if (sz == 0) {
             ERR_raise(ERR_LIB_PROV, PROV_R_INVALID_TAG);
             return 0;
         }
diff --git a/test/evp_extra_test.c b/test/evp_extra_test.c
index 236991f1f5..cb18ead4db 100644
--- a/test/evp_extra_test.c
+++ b/test/evp_extra_test.c
@@ -5929,6 +5929,123 @@ err:
     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:
+ *   - supplying a tag value while encrypting must fail
+ *   - reading a tag while decrypting must fail
+ *   - error codes should be consistent across all AEADs
+ */
+static int test_evp_aead_tag_direction(int idx)
+{
+    const EVP_CIPHER_TEST_INFO *info = &cipher_list[idx];
+    EVP_CIPHER_CTX *ctx_enc = NULL; /* set tag while encrypting: must fail */
+    EVP_CIPHER_CTX *ctx_dec = NULL; /* get tag while decrypting: must fail */
+
+    OSSL_PARAM tagparams[2];
+
+    unsigned char key[EVP_MAX_KEY_LENGTH] = { 0 };
+    unsigned char iv[EVP_MAX_IV_LENGTH] = { 0 };
+    unsigned char tag[EVPTEST_TAG_LEN_MAX] = { 0 };
+
+    int i = 0, testresult = 0, expected = 0;
+    char *errmsg = NULL;
+    unsigned long err_code = 0;
+
+    /* 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);
+
+    /*
+     * a tag value supplied while encrypting must be rejected. data is non-NULL
+     * so this is a value-set, not the data == NULL tag-length query.
+     */
+    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;
+    }
+    tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+        tag, info->taglen);
+    tagparams[1] = OSSL_PARAM_construct_end();
+    ERR_set_mark();
+    if (!TEST_false(EVP_CIPHER_CTX_set_params(ctx_enc, tagparams))) {
+        ERR_clear_last_mark();
+        errmsg = "ENC_SET_TAG_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_TAG_NOT_NEEDED)) {
+        ERR_clear_last_mark();
+        expected = PROV_R_TAG_NOT_NEEDED;
+        errmsg = "ENC_SET_TAG_WRONG_REASON";
+        goto err;
+    }
+    ERR_pop_to_mark();
+
+    /* a tag read while decrypting 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;
+    }
+    tagparams[0] = OSSL_PARAM_construct_octet_string(OSSL_CIPHER_PARAM_AEAD_TAG,
+        tag, info->taglen);
+    tagparams[1] = OSSL_PARAM_construct_end();
+    ERR_set_mark();
+    if (!TEST_false(EVP_CIPHER_CTX_get_params(ctx_dec, tagparams))) {
+        ERR_clear_last_mark();
+        errmsg = "DEC_GET_TAG_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_TAG_NOT_SET)) {
+        ERR_clear_last_mark();
+        expected = PROV_R_TAG_NOT_SET;
+        errmsg = "DEC_GET_TAG_WRONG_REASON";
+        goto err;
+    }
+    ERR_pop_to_mark();
+
+    testresult = 1;
+
+err:
+    if (errmsg != NULL) {
+        if (expected != 0)
+            TEST_info("test_evp_aead_tag_direction %d, %s: %s"
+                      " (expected reason %d, got %d)",
+                idx, errmsg, info->name,
+                expected, ERR_GET_REASON(err_code));
+        else
+            TEST_info("test_evp_aead_tag_direction %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
@@ -8937,6 +9054,7 @@ int setup_tests(void)
     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_init_seq, OSSL_NELEM(evp_init_tests));
     ADD_ALL_TESTS(test_evp_reset, OSSL_NELEM(evp_reset_tests));