Skip to content

Commit 1faddd6

Browse files
committed
evp: verify Poly1305 tag on ChaCha20-Poly1305 decrypt
EVP_DecryptFinal_ex() called wc_ChaCha20Poly1305_Final() which only computes the Poly1305 tag, writing it into ctx->authTag and overwriting the expected tag stored there by EVP_CTRL_AEAD_SET_TAG. No comparison was ever performed, so any forged tag was accepted. Fix: save the expected tag before calling Final(), then verify with wc_ChaCha20Poly1305_CheckTag() on the decrypt path, mirroring the existing AES-GCM branch. Add a regression test that asserts EVP_DecryptFinal_ex() rejects an all-zero forged tag. Reported-by: Nicholas Carlini (Anthropic) & Bronson Yen (Calif.io)
1 parent 13a0163 commit 1faddd6

2 files changed

Lines changed: 44 additions & 4 deletions

File tree

tests/api/test_evp_cipher.c

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,6 +1915,7 @@ int test_wolfssl_EVP_chacha20_poly1305(void)
19151915
byte cipherText[sizeof(plainText)];
19161916
byte decryptedText[sizeof(plainText)];
19171917
byte tag[CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE];
1918+
byte badTag[CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE];
19181919
EVP_CIPHER_CTX* ctx = NULL;
19191920
int outSz;
19201921

@@ -1979,6 +1980,28 @@ int test_wolfssl_EVP_chacha20_poly1305(void)
19791980
EVP_CIPHER_CTX_free(ctx);
19801981
ctx = NULL;
19811982

1983+
/* Negative test: forged (all-zero) tag must be rejected. */
1984+
XMEMSET(badTag, 0, sizeof(badTag));
1985+
ExpectNotNull((ctx = EVP_CIPHER_CTX_new()));
1986+
ExpectIntEQ(EVP_DecryptInit_ex(ctx, EVP_chacha20_poly1305(), NULL,
1987+
NULL, NULL), WOLFSSL_SUCCESS);
1988+
ExpectIntEQ(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_IVLEN,
1989+
CHACHA20_POLY1305_AEAD_IV_SIZE, NULL), WOLFSSL_SUCCESS);
1990+
ExpectIntEQ(EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_AEAD_SET_TAG,
1991+
CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE, badTag),
1992+
WOLFSSL_SUCCESS);
1993+
ExpectIntEQ(EVP_DecryptInit_ex(ctx, NULL, NULL, key, iv),
1994+
WOLFSSL_SUCCESS);
1995+
ExpectIntEQ(EVP_DecryptUpdate(ctx, NULL, &outSz, aad, sizeof(aad)),
1996+
WOLFSSL_SUCCESS);
1997+
ExpectIntEQ(EVP_DecryptUpdate(ctx, decryptedText, &outSz, cipherText,
1998+
sizeof(cipherText)), WOLFSSL_SUCCESS);
1999+
/* EVP_DecryptFinal_ex MUST return failure on tag mismatch */
2000+
ExpectIntNE(EVP_DecryptFinal_ex(ctx, decryptedText, &outSz),
2001+
WOLFSSL_SUCCESS);
2002+
EVP_CIPHER_CTX_free(ctx);
2003+
ctx = NULL;
2004+
19822005
/* Test partial Inits. CipherInit() allow setting of key and iv
19832006
* in separate calls. */
19842007
ExpectNotNull((ctx = EVP_CIPHER_CTX_new()));

wolfcrypt/src/evp.c

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1499,16 +1499,33 @@ int wolfSSL_EVP_CipherFinal(WOLFSSL_EVP_CIPHER_CTX *ctx, unsigned char *out,
14991499
* HAVE_FIPS_VERSION >= 2 */
15001500
#if defined(HAVE_CHACHA) && defined(HAVE_POLY1305)
15011501
case WC_CHACHA20_POLY1305_TYPE:
1502+
{
1503+
byte computedTag[CHACHA20_POLY1305_AEAD_AUTHTAG_SIZE];
1504+
if (!ctx->enc) {
1505+
/* Save the expected tag before _Final() overwrites
1506+
* ctx->authTag */
1507+
XMEMCPY(computedTag, ctx->authTag, sizeof(computedTag));
1508+
}
15021509
if (wc_ChaCha20Poly1305_Final(&ctx->cipher.chachaPoly,
15031510
ctx->authTag) != 0) {
15041511
WOLFSSL_MSG("wc_ChaCha20Poly1305_Final failed");
15051512
return WOLFSSL_FAILURE;
15061513
}
1507-
else {
1508-
*outl = 0;
1509-
return WOLFSSL_SUCCESS;
1514+
if (!ctx->enc) {
1515+
/* ctx->authTag now holds computed tag; computedTag holds
1516+
* expected */
1517+
int tagErr = wc_ChaCha20Poly1305_CheckTag(computedTag,
1518+
ctx->authTag);
1519+
ForceZero(computedTag, sizeof(computedTag));
1520+
if (tagErr != 0) {
1521+
WOLFSSL_MSG("ChaCha20-Poly1305 tag mismatch");
1522+
return WOLFSSL_FAILURE;
1523+
}
15101524
}
1511-
break;
1525+
*outl = 0;
1526+
return WOLFSSL_SUCCESS;
1527+
}
1528+
break;
15121529
#endif
15131530
#ifdef WOLFSSL_SM4_GCM
15141531
case WC_SM4_GCM_TYPE:

0 commit comments

Comments
 (0)