Cryptography · Modes and Authentication
The Release of Unverified Plaintext Problem in AEAD APIs
Why a decryption function that returns plaintext before the tag is checked reintroduces chosen-ciphertext attacks, and how streaming interfaces force the verification decision into the caller.
Authenticated encryption is usually described as a pair of functions: one that takes a key, nonce, associated data, and plaintext and returns a ciphertext and tag; one that takes the same key and nonce, the ciphertext, the tag, and the associated data, and returns either the plaintext or a failure. Under that interface the security claim is clean — an adversary cannot distinguish ciphertexts from random, and cannot produce a ciphertext that decrypts successfully under a key it has not seen.
Real APIs do not always look like that. Many of them return plaintext and a status that the caller is expected to check, or return plaintext incrementally while the tag is still being computed, or expose a streaming decryptor that yields bytes before the last block has arrived. Each of these shapes is convenient and each one changes the cryptographic contract. The change is not cosmetic: an adversary that can obtain plaintext for ciphertexts whose tags fail has a decryption oracle, and the standard AEAD security definition explicitly denies it that ability.
What the one-shot definition assumes
The standard formulation treats decryption as a single step with two outcomes. For GCM, NIST’s specification phrases the requirement directly: if the tag does not verify, the plaintext is not returned, and nothing derived from it may be used. The security proof for GCM, and for AEAD as a class, relies on this. The argument bounds the adversary’s forgery probability, and that bound says nothing about what the adversary could do with the plaintext of a rejected ciphertext, because the model assumes there is none.
This is not a technicality in the model. Break the assumption and the guarantee disappears, because the decryption of a forged ciphertext with CTR-mode structure is very informative. For GCM, a modified ciphertext block XORs a corresponding plaintext block with a known delta. An adversary that submits a ciphertext, alters it, and learns the plaintext gains $\Delta P$ for a chosen $\Delta C$ — for CTR and GCM this is a known, direct relationship. It does not expose the keystream, but it confirms guesses about the plaintext and enables the construction of further ciphertexts whose behaviour it can predict.
The same shape appears in other modes with different mechanics. In CCM the forgery requires recomputation of a CBC-MAC over the modified plaintext, so a plaintext-releasing CCM implementation behaves differently; in all cases the underlying problem is that the receiver’s output is a function of unauthenticated input and the attacker observes it.
How the leak reappears in practice
Three API shapes are common enough to name.
The first returns a tuple of plaintext and success flag, or writes the plaintext into a buffer and returns an error code. This is safe in principle and unsafe in practice: the plaintext has been written to memory the caller owns, and the caller’s error handling determines whether anything observes it. A bug in a call site — a missing check, a checked error that still writes to the log, a language binding that surfaces the buffer alongside the status — turns the API into an oracle. The pattern is the same as the CBC padding oracle described in the padding oracle article, except the leak is not a single bit of validity but the entire plaintext.
The second shape is a streaming decryptor. The caller feeds ciphertext in chunks and receives plaintext chunks as they become available. This is the natural shape for large transfers, because the alternative is buffering the whole message before releasing a byte. AEAD modes do not permit it in the general case. GCM’s tag covers the entire ciphertext, so no prefix of the plaintext can be authenticated until the last block has been processed. A streaming decryptor that emits bytes must therefore emit unverified plaintext, and the security guarantee becomes conditional on the caller discarding everything if the final tag check fails.
The third shape is worse and more subtle: an interface that authenticates a prefix and then continues. Some protocols need to act on a header before the whole payload has arrived — routing a message, selecting a queue — and the temptation is to verify a tag over the header alone and release it, with the payload authenticated separately. That is legitimate only if the two parts are bound together, so that a valid header cannot be paired with a payload from a different message. Without the binding, the attacker can splice a header from one message onto the payload of another, which is precisely the confusion an AEAD tag over (associated data, ciphertext) is designed to prevent.
Committing the caller to the decision
The engineering answer is to make the verification decision structural rather than procedural. Two approaches are established.
The first is an interface that cannot return unverified plaintext. A one-shot function that either returns a plaintext buffer it allocated, or returns an error with no buffer, makes the mistake impossible to commit. The buffer is not handed to the caller until the tag has verified. This is cheap for bounded messages and is what most language-level AEAD bindings do: Seal and Open in Go’s crypto/cipher.AEAD interface, for example, are specified so that Open returns nil on authentication failure, and the buffer it returns is only present on success.
The second is a two-phase streaming interface, where the caller feeds ciphertext, the implementation holds back release until the tag verifies, and the release is atomic. Implementation-wise this narrows to buffering the plaintext or re-running the decryption after verification, which is affordable for messages up to the size a protocol bounds its records to, and is exactly what a record-oriented protocol does: TLS bounds a record to 16 KB of plaintext, so buffering a record’s plaintext before release is bounded work.
// One-shot boundary: nothing derived from the ciphertext escapes
// unless the tag verified. aead.Open returns nil on failure.
func decryptRecord(aead cipher.AEAD, nonce, sealed []byte) ([]byte, error) {
pt, err := aead.Open(nil, nonce, sealed, nil)
if err != nil {
return nil, err // no plaintext, no partial output
}
return pt, nil
}
The cipher package is imported in the surrounding file; the function is shown to fix the shape of the contract, not as a complete program.
Why release of unverified plaintext is a defined notion
The research literature treats this as a distinct property, not an implementation detail. Hoang, Krovetz, and Rogaway introduced the notion of release of unverified plaintext and used it to motivate a strengthened security definition, sometimes called misuse resistance in the decryption direction or robustness: a scheme should remain secure even when an adversary is given the decryption of forged ciphertexts. They proposed a construction designed to satisfy it.
The property is not free for existing modes. For GCM, releasing the plaintext of a forged ciphertext reveals exactly the XOR delta the attacker chose, which combined with knowledge of the plaintext structure confirms and extends guesses. For a scheme that resists this, the design must be such that a forged ciphertext decrypts to something the adversary cannot distinguish from random — typically by making the plaintext depend on the tag or by using an encryption scheme whose truncation is unpredictable without the key.
Very few deployed modes offer this. The practical posture for GCM, CCM, and ChaCha20-Poly1305 is therefore the conservative one: treat the tag as a gate and put everything behind it, and where the interface cannot do that, treat the API as a hazard that the protocol design must contain.
Where the containment lives
Containment has to happen at the layer that owns the buffer. A protocol that must act on a header before the payload arrives can carry the header inside the associated data of a second, small AEAD invocation and bind the two by including a digest of the payload’s tag in the header’s associated data — so the header cannot be authenticated independently of the message it belongs to. A storage format that decrypts pages independently must include the page index and the file identifier in the associated data, so that a page moved from one position to another fails verification rather than decrypting to the wrong plaintext.
The associated-data binding is also what prevents the subtler splicing attacks, and it is the reason the associated data parameter exists in the AEAD interface at all. The construction of that parameter — what to bind, in what order, encoded how — is a protocol-design question with its own failure modes. GCM’s length block, described in the GHASH tag construction, is one instance of the general rule that the framing of authenticated data must be unambiguous. A header and a payload concatenated without length encoding can be shifted across the boundary, and an attacker who can move bytes between them can turn two valid messages into a third.
Finally, the choice of mode interacts with this. A two-pass construction such as SIV necessarily produces the plaintext before the tag can be checked, so its decryption API faces the release problem in its strongest form — the implementation must compute the synthetic IV over the decrypted plaintext, then compare it, then release. There is no shortcut; the plaintext exists in memory before it is known to be authentic, and the only defence is that it is not observable to the attacker until the comparison passes. That the mode is otherwise misuse-resistant does not exempt it from this requirement, and an implementation that returns the plaintext early has thrown away the property the mode was chosen for.
References
- Robust Authenticated-Encryption AEZ and the Problem That It Solves — Hoang, Krovetz & Rogaway, EUROCRYPT 2015
- NIST SP 800-38D: Recommendation for Block Cipher Modes of Operation — Galois/Counter Mode (GCM) and GMAC — NIST
- RFC 5116: An Interface and Algorithms for Authenticated Encryption — IETF
- The Order of Encryption and Authentication for Protecting Communications — Hugo Krawczyk, CRYPTO 2001