Summary
AuthenticatedSymmetricCipher::DecryptAndVerify(msg, mac, macLength, ...) returns
true and emits decrypted plaintext when macLength == 0, for any
ciphertext, AAD and IV. A zero-length authentication tag is accepted as valid, so
authentication is bypassed for every AEAD mode (GCM, CCM, EAX, ChaCha20Poly1305,
XChaCha20Poly1305). NIST SP 800-38C/38D and RFC 8439 forbid a zero-length tag.
Details
DecryptAndVerify ends with return TruncatedVerify(mac, macSize)
(cryptlib.cpp:256-263), and TruncatedVerify does:
bool HashTransformation::TruncatedVerify(const byte *digest, size_t digestLength)
{
ThrowIfInvalidTruncatedSize(digestLength); // cryptlib.cpp:409
SecByteBlock calculated(digestLength ? digestLength : 1);
TruncatedFinal(calculated, digestLength);
return VerifyBufsEqual(calculated, digest, digestLength); // cryptlib.cpp:412
}
ThrowIfInvalidTruncatedSize only rejects size > DigestSize()
(cryptlib.cpp:415-419):
void HashTransformation::ThrowIfInvalidTruncatedSize(size_t size) const
{
if (size > DigestSize())
throw InvalidArgument(...);
}
For size == 0, 0 > DigestSize() is false, so it passes. Then
VerifyBufsEqual(a, b, 0) returns true unconditionally — both while loops
(count >= 8, count >= 4) never execute, the accumulator stays 0, and the
tail for loop runs zero times (misc.cpp:218-262, return acc8 == 0).
TruncatedFinal(calculated, 0) writes zero bytes, so no tag is ever compared.
Net effect: DecryptAndVerify(..., macLength=0, ...) is true for attacker-
controlled ciphertext and the function still writes the (attacker-influenced)
decrypted plaintext.
PoC
cryptopp-aead-zero-tag-poc/ — builds GCM<AES> and ChaCha20Poly1305 round-trips and
shows (A) a correct 16-byte tag verifies, (B) a corrupted 16-byte tag is rejected,
but (C) a tag length of 0 accepts attacker-controlled ciphertext and emits
plaintext.
Build & run:
cd /path/to/cryptopp && make libcryptopp.a
export CRYPTOPP_DIR=/path/to/cryptopp
cd cryptopp-aead-zero-tag-poc
./build_and_run.sh
Expected output:
[*] GCM<AES> (tag size 16, IV 12)
[check A] correct 16-byte tag verifies : yes
[check B] corrupted 16-byte tag rejected : yes
[check C] tag length = 0 accepts attacker ct : ACCEPTED
[check C] plaintext emitted despite empty tag : yes (28 bytes)
[!] Vulnerability reproduced: GCM<AES> (tag size 16, IV 12) DecryptAndVerify(macLength=0)
returns TRUE for attacker-controlled ciphertext and emits plaintext;
authentication is bypassed.
Impact
This is an authentication-integrity bypass (CWE-347/345): forged AEAD records are
accepted as authentic. It is exploitable when an application uses length-prefixed
AEAD record framing (TLS-/QUIC-like or custom) and forwards an attacker-controlled
on-the-wire tag-length field into the library's macLength; applications that
hard-code a standard tag length (e.g. 16 for GCM/ChaChaPoly) are unaffected — this
precondition is why severity is Medium rather than High. Workaround: hard-code the
standard tag length and never derive macLength from an attacker-controlled field.
Suggested fix: in ThrowIfInvalidTruncatedSize, enforce a minimum tag length
(e.g. add a MinDigestSize() and reject size < MinDigestSize() / size == 0),
and/or have each mode's TruncatedFinal throw when macSize == 0.
cryptopp-aead-zero-tag-poc.tar.gz
Summary
AuthenticatedSymmetricCipher::DecryptAndVerify(msg, mac, macLength, ...)returnstrue and emits decrypted plaintext when
macLength == 0, for anyciphertext, AAD and IV. A zero-length authentication tag is accepted as valid, so
authentication is bypassed for every AEAD mode (GCM, CCM, EAX, ChaCha20Poly1305,
XChaCha20Poly1305). NIST SP 800-38C/38D and RFC 8439 forbid a zero-length tag.
Details
DecryptAndVerifyends withreturn TruncatedVerify(mac, macSize)(
cryptlib.cpp:256-263), andTruncatedVerifydoes:ThrowIfInvalidTruncatedSizeonly rejectssize > DigestSize()(
cryptlib.cpp:415-419):For
size == 0,0 > DigestSize()is false, so it passes. ThenVerifyBufsEqual(a, b, 0)returnstrueunconditionally — bothwhileloops(
count >= 8,count >= 4) never execute, the accumulator stays0, and thetail
forloop runs zero times (misc.cpp:218-262,return acc8 == 0).TruncatedFinal(calculated, 0)writes zero bytes, so no tag is ever compared.Net effect:
DecryptAndVerify(..., macLength=0, ...)istruefor attacker-controlled ciphertext and the function still writes the (attacker-influenced)
decrypted plaintext.
PoC
cryptopp-aead-zero-tag-poc/— builds GCM<AES> and ChaCha20Poly1305 round-trips andshows (A) a correct 16-byte tag verifies, (B) a corrupted 16-byte tag is rejected,
but (C) a tag length of
0accepts attacker-controlled ciphertext and emitsplaintext.
Build & run:
Expected output:
Impact
This is an authentication-integrity bypass (CWE-347/345): forged AEAD records are
accepted as authentic. It is exploitable when an application uses length-prefixed
AEAD record framing (TLS-/QUIC-like or custom) and forwards an attacker-controlled
on-the-wire tag-length field into the library's
macLength; applications thathard-code a standard tag length (e.g. 16 for GCM/ChaChaPoly) are unaffected — this
precondition is why severity is Medium rather than High. Workaround: hard-code the
standard tag length and never derive
macLengthfrom an attacker-controlled field.Suggested fix: in
ThrowIfInvalidTruncatedSize, enforce a minimum tag length(e.g. add a
MinDigestSize()and rejectsize < MinDigestSize()/size == 0),and/or have each mode's
TruncatedFinalthrow whenmacSize == 0.cryptopp-aead-zero-tag-poc.tar.gz