Skip to content

Zero-length AEAD authentication tag is accepted as valid, bypassing authentication #1369

Description

@X1AOxiang

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions