# Patch Analysis: CVE-2026-5479 — wolfSSL EVP ChaCha20-Poly1305 Authentication Tag Verification

## What the Fix Changes

The fix is contained in `wolfcrypt/src/evp.c` inside `wolfSSL_EVP_CipherFinal`, specifically in the `WC_CHACHA20_POLY1305_TYPE` branch (around lines 1501–1515 in v5.9.0-stable, expanded to ~20 lines in v5.9.1-stable).

### Before (v5.9.0-stable)
```c
case WC_CHACHA20_POLY1305_TYPE:
    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;
    }
    break;
```

On the decrypt path (`!ctx->enc`), `wc_ChaCha20Poly1305_Final()` computes the Poly1305 authentication tag and stores it into `ctx->authTag`, **overwriting** the expected tag that was previously supplied by `EVP_CIPHER_CTX_ctrl(EVP_CTRL_AEAD_SET_TAG)`. No comparison between the expected and computed tags is ever performed. Consequently, any forged or mismatched tag is silently accepted.

### After (v5.9.1-stable)
```c
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) {
        /* 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;
        }
    }
    *outl = 0;
    return WOLFSSL_SUCCESS;
}
break;
```

The fix:
1. **Saves** the expected tag (from `ctx->authTag`) into a local `computedTag` buffer before `wc_ChaCha20Poly1305_Final()` overwrites it.
2. Calls `wc_ChaCha20Poly1305_Final()` to compute the tag, which now lands back in `ctx->authTag`.
3. **Compares** the saved expected tag against the freshly computed tag using `wc_ChaCha20Poly1305_CheckTag()`.
4. **Clears** the saved tag from stack via `ForceZero()`.
5. Returns `WOLFSSL_FAILURE` if the tags do not match.

## Fix Assumptions

The fix makes the following assumptions:
- **Assumption 1**: All ChaCha20-Poly1305 decrypt flows through the EVP compatibility layer **must** pass through `wolfSSL_EVP_CipherFinal` (or its macro aliases `EVP_DecryptFinal_ex`, `EVP_CipherFinal`, `EVP_CipherFinal_ex`).
- **Assumption 2**: The expected tag has already been stored in `ctx->authTag` via `EVP_CIPHER_CTX_ctrl(EVP_CTRL_AEAD_SET_TAG)` before `Final` is called.
- **Assumption 3**: The only ChaCha20-Poly1305 code path inside `wolfSSL_EVP_CipherFinal` is the one fixed; other AEAD ciphers (AES-GCM, AES-CCM, ARIA-GCM, SM4-GCM, SM4-CCM) already perform internal tag verification in their respective `wc_*Decrypt()` / `wc_*DecryptFinal()` functions.

## What the Fix Does NOT Cover (Gap Analysis)

1. **No explicit plaintext zeroing on tag failure**: Unlike the low-level `wc_ChaCha20Poly1305_Decrypt()`, which calls `ForceZero(outPlaintext, inCiphertextLen)` on authentication failure, the EVP layer `wolfSSL_EVP_CipherFinal` does not zero out plaintext that was already emitted by prior `EVP_DecryptUpdate` calls. This is consistent with OpenSSL EVP behavior (the caller is responsible for discarding output on `Final` failure), but it means an application that ignores the `Final` failure will still have partially decrypted attacker-controlled data in its buffers.

2. **Other direct callers of `wc_ChaCha20Poly1305_Final`**: The fix only covers the EVP layer. The low-level `wc_ChaCha20Poly1305_Final()` itself still computes-but-does-not-verify the tag. However, the only other non-test callers are `wc_ChaCha20Poly1305_Encrypt()` (which intentionally generates a tag) and `wc_ChaCha20Poly1305_Decrypt()` (which already performs `wc_ChaCha20Poly1305_CheckTag()` after calling `Final`). So there is no other production code path that uses `Final` without verification.

3. **`wolfSSL_EVP_Cipher` one-shot API**: `wolfSSL_EVP_Cipher` does not have a case for `WC_CHACHA20_POLY1305_TYPE` (there is literally a `/* TODO: Chacha??? */` comment and it falls through to `default`, returning `WOLFSSL_FATAL_ERROR`). Therefore it cannot be used to bypass the fix.

4. **Different cipher types**: The fix does not add tag verification to any other cipher type because, as noted above, all other AEAD ciphers in `wolfSSL_EVP_CipherFinal` call decrypt functions that verify the tag internally.

## Comparison: Behavior Before vs. After

| Scenario | v5.9.0 (vulnerable) | v5.9.1 (fixed) |
|---|---|---|
| Bad tag, good ciphertext | `Final` returns success | `Final` returns failure |
| Good tag, modified ciphertext | `Final` returns success | `Final` returns failure |
| Good tag, modified AAD | `Final` returns success | `Final` returns failure |
| No tag set (NULL ptr to SET_TAG) | `Final` returns success, tag stored in `authTag` | `Final` compares computed tag against uninitialized/zeroed `authTag` → failure (correct) |

## Verdict

The fix is **complete and targeted** for the specific vulnerability. There is no bypass through alternate EVP API entry points, because:
- `EVP_DecryptFinal_ex`, `EVP_CipherFinal`, and `EVP_CipherFinal_ex` are all macro aliases for `wolfSSL_EVP_CipherFinal`.
- The QUIC code path (`src/quic.c`) uses `wolfSSL_EVP_CipherFinal` directly and is therefore also fixed.
- No other non-test callers of `wc_ChaCha20Poly1305_Final` exist that omit tag verification.
- The low-level `wc_ChaCha20Poly1305_Decrypt()` already verified tags correctly; the bug was strictly in the EVP compatibility layer.

**No bypass was found.** The fix correctly closes the gap.
