ANT-2026-RSSMAMA7 · wolfssl/wolfssl

crypto-failure high

CVE-2026-5479

Severity Claude high · Security research firm high · Maintainer high

Discovered by Claude Mythos Preview

REPORT

Anthropic's analysis, sealed at approval. Disclosure to the maintainer was performed by Calif.

ANT-2026-RSSMAMA7: wolfSSL EVP_CipherFinal does not verify the Poly1305 tag on ChaCha20-Poly1305 decrypt

In wolfSSL's OpenSSL-compatibility EVP layer, wolfSSL_EVP_CipherFinal() (wolfcrypt/src/evp.c) finalised ChaCha20-Poly1305 decryption by computing the Poly1305 tag into ctx->authTag, overwriting the expected tag the caller had supplied through EVP_CTRL_AEAD_SET_TAG, and then returned success without comparing the two. Any application that decrypts with EVP_chacha20_poly1305() and relies on EVP_DecryptFinal_ex()/EVP_CipherFinal() to reject forgeries, including wolfSSL's own QUIC helper wolfSSL_quic_aead_decrypt() when TLS_CHACHA20_POLY1305_SHA256 is negotiated, therefore accepted tampered or forged ciphertext as authentic. The issue is tracked as CVE-2026-5479 (GHSA-3xr8-r75g-g9c6) and was fixed by commit 1faddd640, which is included in wolfSSL 5.9.1.

Target

Project: wolfSSL
Location: wolfcrypt/src/evp.c:wolfSSL_EVP_CipherFinal
Discovery: static analysis — not yet dynamically reproduced

Technical Details

Root cause. In wolfSSL_EVP_CipherFinal() (wolfcrypt/src/evp.c), the WC_CHACHA20_POLY1305_TYPE case had no ctx->enc branch: for both encryption and decryption it called wc_ChaCha20Poly1305_Final(&ctx->cipher.chachaPoly, ctx->authTag) and returned WOLFSSL_SUCCESS whenever that call succeeded. wc_ChaCha20Poly1305_Final() only generates a tag, so on the decrypt path it overwrote the expected tag that EVP_CIPHER_CTX_ctrl(..., EVP_CTRL_AEAD_SET_TAG, ...) had copied into ctx->authTag. wc_ChaCha20Poly1305_CheckTag() was not called anywhere in evp.c, and wolfSSL_EVP_DecryptFinal_ex() is a plain passthrough to wolfSSL_EVP_CipherFinal(), so no tag comparison took place at any point. The adjacent AES-GCM branch of the same function does verify the tag.

Reach and preconditions. The code is compiled when the EVP compatibility layer is enabled (OPENSSL_EXTRA) together with HAVE_CHACHA/HAVE_POLY1305. It is reached by any caller that follows the standard OpenSSL AEAD pattern: decrypt with EVP_chacha20_poly1305(), set the received tag with EVP_CTRL_AEAD_SET_TAG, and treat a successful EVP_DecryptFinal_ex()/EVP_CipherFinal() as proof of authenticity. wolfSSL's QUIC integration uses exactly this pattern: wolfSSL_quic_get_aead() maps TLS_CHACHA20_POLY1305_SHA256 to EVP_chacha20_poly1305(), and wolfSSL_quic_aead_decrypt() (src/quic.c) sets the tag via EVP_CTRL_AEAD_SET_TAG and relies solely on the return value of wolfSSL_EVP_CipherFinal(). wolfSSL's native TLS record layer calls the wolfCrypt ChaCha20-Poly1305 primitives directly and does not go through this EVP path.

Impact. Loss of AEAD integrity and authenticity for ChaCha20-Poly1305 via the EVP interface: an attacker who can modify or inject ciphertext has forged or tampered messages accepted, and the resulting plaintext is returned to the application as if it had been authenticated. The published advisory (CVE-2026-5479 / GHSA-3xr8-r75g-g9c6, CWE-354) rates the issue High, CVSS 3.1 8.1 (AV:A/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N).

Fix. Commit 1faddd640edc8195c1fb7cb3904876e434ecab87 ("evp: verify Poly1305 tag on ChaCha20-Poly1305 decrypt"), merged via wolfSSL/wolfssl#10102 on 2026-04-06 and included in the wolfSSL 5.9.1 release (the 5.9.0 release does not contain it), saves the expected tag before calling wc_ChaCha20Poly1305_Final() and, on the decrypt path, compares it against the computed tag with wc_ChaCha20Poly1305_CheckTag(), returning WOLFSSL_FAILURE on mismatch. A regression test was added asserting that EVP_DecryptFinal_ex() rejects a forged tag.

case WC_CHACHA20_POLY1305_TYPE:
{
    byte computedTag[CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE];
    if (!ctx->enc) {
        /* Save the expected tag before _Final() overwrites ctx->authTag */
        XMEMCPY(computedTag, ctx->authTag, sizeof(computedTag));
    }
    if (wc_ChaCha20Poly1305_Final(&ctx->cipher.chachaPoly, ctx->authTag) != 0) {
        WOLFSSL_MSG("wc_ChaCha20Poly1305_Final failed");
        return WOLFSSL_FAILURE;
    }
    if (!ctx->enc) {
        int tagErr = wc_ChaCha20Poly1305_CheckTag(computedTag, ctx->authTag);
        ForceZero(computedTag, sizeof(computedTag));
        if (tagErr != 0) {
            WOLFSSL_MSG("ChaCha20-Poly1305 tag mismatch");
            return WOLFSSL_FAILURE;
        }
    }
    *outl = 0;
    return WOLFSSL_SUCCESS;
}

Reproduction

This finding was identified by static analysis and has not yet been dynamically reproduced. The Technical Details section above describes the code path; a trigger input is not included.

[No reproducer or sanitizer output attached — request from security-cvd@anthropic.com if needed.]

Acknowledgement

This vulnerability was discovered by Claude, Anthropic's AI assistant, and triaged by the Anthropic security team in collaboration with Anthropic Research. Please direct questions to security-cvd@anthropic.com and reference ANT-2026-RSSMAMA7.


Reference: ANT-2026-RSSMAMA7
Anthropic CVD Policy: https://www.anthropic.com/coordinated-vulnerability-disclosure

SECURITY RESEARCH FIRM ANALYSIS

Triage and disclosure were performed by Calif.

Verdict
true positive
Severity
high
UPSTREAM FIX

The change that resolved this finding.

diff --git a/tests/api/test_evp_cipher.c b/tests/api/test_evp_cipher.c
index b4e37df7a28..1e88da9979c 100644
--- a/tests/api/test_evp_cipher.c
+++ b/tests/api/test_evp_cipher.c
@@ -1915,6 +1915,7 @@ int test_wolfssl_EVP_chacha20_poly1305(void)
     byte cipherText[sizeof(plainText)];
     byte decryptedText[sizeof(plainText)];
     byte tag[CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE];
+    byte badTag[CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE];
     EVP_CIPHER_CTX* ctx = NULL;
     int outSz;
 
@@ -1979,6 +1980,28 @@ int test_wolfssl_EVP_chacha20_poly1305(void)
     EVP_CIPHER_CTX_free(ctx);
     ctx = NULL;
 
+    /* Negative test: forged (all-zero) tag must be rejected. */
+    XMEMSET(badTag, 0, sizeof(badTag));
+    ExpectNotNull((ctx = EVP_CIPHER_CTX_new()));
+    ExpectIntEQ(EVP_DecryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL,
+                NULL, NULL), WOLFSSL_SUCCESS);
+    ExpectIntEQ(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN,
+                CHACHA20_POLY1305_AEAD_IV_SIZE, NULL), WOLFSSL_SUCCESS);
+    ExpectIntEQ(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG,
+                CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE, badTag),
+                WOLFSSL_SUCCESS);
+    ExpectIntEQ(EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv),
+                WOLFSSL_SUCCESS);
+    ExpectIntEQ(EVP_DecryptUpdate(ctx, NULL, &outSz, aad, sizeof(aad)),
+                WOLFSSL_SUCCESS);
+    ExpectIntEQ(EVP_DecryptUpdate(ctx, decryptedText, &outSz, cipherText,
+                sizeof(cipherText)), WOLFSSL_SUCCESS);
+    /* EVP_DecryptFinal_ex MUST return failure on tag mismatch */
+    ExpectIntNE(EVP_DecryptFinal_ex(ctx, decryptedText, &outSz),
+                WOLFSSL_SUCCESS);
+    EVP_CIPHER_CTX_free(ctx);
+    ctx = NULL;
+
     /* Test partial Inits. CipherInit() allow setting of key and iv
      * in separate calls. */
     ExpectNotNull((ctx = EVP_CIPHER_CTX_new()));
diff --git a/wolfcrypt/src/evp.c b/wolfcrypt/src/evp.c
index fc4f68eb9fc..121d926555f 100644
--- a/wolfcrypt/src/evp.c
+++ b/wolfcrypt/src/evp.c
@@ -1499,16 +1499,33 @@ int wolfSSL_EVP_CipherFinal(WOLFSSL_EVP_CIPHER_CTX *ctx, unsigned char *out,
         * HAVE_FIPS_VERSION >= 2 */
 #if defined(HAVE_CHACHA) && defined(HAVE_POLY1305)
         case WC_CHACHA20_POLY1305_TYPE:
+        {
+            byte computedTag[CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE];
+            if (!ctx->enc) {
+                /* Save the expected tag before _Final() overwrites
+                 * ctx->authTag */
+                XMEMCPY(computedTag, ctx->authTag, sizeof(computedTag));
+            }
             if (wc_ChaCha20Poly1305_Final(&ctx->cipher.chachaPoly,
                                           ctx->authTag) != 0) {
                 WOLFSSL_MSG("wc_ChaCha20Poly1305_Final failed");
                 return WOLFSSL_FAILURE;
             }
-            else {
-                *outl = 0;
-                return WOLFSSL_SUCCESS;
+            if (!ctx->enc) {
+                /* ctx->authTag now holds computed tag; computedTag holds
+                 * expected */
+                int tagErr = wc_ChaCha20Poly1305_CheckTag(computedTag,
+                                                          ctx->authTag);
+                ForceZero(computedTag, sizeof(computedTag));
+                if (tagErr != 0) {
+                    WOLFSSL_MSG("ChaCha20-Poly1305 tag mismatch");
+                    return WOLFSSL_FAILURE;
+                }
             }
-            break;
+            *outl = 0;
+            return WOLFSSL_SUCCESS;
+        }
+        break;
 #endif
 #ifdef WOLFSSL_SM4_GCM
         case WC_SM4_GCM_TYPE:

https://github.com/wolfSSL/wolfssl/commit/1faddd640

TIMELINE

Dates from discovery through public reveal.

  1. 2026-03-29 Reported to tracker
  2. 2026-04-05 Sent to maintainer
  3. 2026-05-07 Patch released
  4. 2026-05-07 Maintainer acknowledged
  5. 2026-05-20 Publicly revealed
PROVENANCE

SHA-3-512 hash:

3d2166ecae422707fc5deddb8399d45cfe7100af81f11c4db5e69c322ab9e357919f53f646abd59e37131b10cd08784ab37185431b9cfd803d3bc0f26297cddf

Committed 2026-04-05 16:37 PT

Revealed 2026-05-20 00:40 PT

Verify (download preimage.json)

Show preimage JSON
{
  "ant_id": "ANT-2026-RSSMAMA7",
  "bug_class": "crypto-failure",
  "claude_severity": "high",
  "commit_sha": null,
  "created_at": "2026-03-29T20:42:34+00:00",
  "description": null,
  "discovered_at": null,
  "location": null,
  "poc_sha256": null,
  "preimage_version": 1,
  "project": "wolfSSL",
  "reproduction": null,
  "technical_details": null,
  "title": "wolfssl evp chacha20 poly1305 poly1305 tag never verifi",
  "vendor_severity": "high"
}