To sign or not to sign: Practical vulnerabilities in GPG & friends
49016, Liam
39th Chaos Communication Congress (39C3): Power Cycles · Day 1 · Saal One
Overview
In this revealing talk from 39C3, security researchers Lexi (49016) and Liam delved deep into the often-assumed impenetrable world of Pretty Good Privacy (PGP) and its most prevalent implementation, GNU Privacy Guard (GPG). Their presentation, titled "To sign or not to sign: Practical vulnerabilities in GPG & friends," systematically exposed a staggering array of security flaws across various facets of PGP, ranging from fundamental parsing errors in signature verification to critical memory safety issues and design shortcomings in its trust model. The speakers, both seasoned CTF players, leveraged their expertise in web challenges and binary exploitation to uncover vulnerabilities that challenge the very foundation of digital trust.

Key moments
- 0:00 Practical GPG verification of a Fedora ISO
- 2:32 PGP's layered complexity and potential attack surfaces
- 4:40 How the GPG vulnerability research began
- 6:00 Introduction to PGP cleartext signatures and unverified headers
- 7:20 Code analysis: discovering the 'not-escaped' header bypass
- 8:00 Live demo: injecting unverified text in cleartext signatures
To sign or not to sign: Practical vulnerabilities in GPG & friends
Speakers: 49016 (Lexi), Liam
Conference: 39C3
YouTube: https://www.youtube.com/watch?v=U0ZYOTHrB7I
Overview
In this revealing talk from 39C3, security researchers Lexi (49016) and Liam delved deep into the often-assumed impenetrable world of Pretty Good Privacy (PGP) and its most prevalent implementation, GNU Privacy Guard (GPG). Their presentation, titled "To sign or not to sign: Practical vulnerabilities in GPG & friends," systematically exposed a staggering array of security flaws across various facets of PGP, ranging from fundamental parsing errors in signature verification to critical memory safety issues and design shortcomings in its trust model. The speakers, both seasoned CTF players, leveraged their expertise in web challenges and binary exploitation to uncover vulnerabilities that challenge the very foundation of digital trust.
The talk highlighted that while cryptography relies on robust mathematical principles, its practical application involves complex layers of specification, implementation, and user interaction, all of which present significant attack surfaces. GPG, a tool widely depended upon for secure communication, software integrity, and identity verification—from signing emails and verifying software downloads to securing package managers and SSH authentication—was shown to be susceptible to a variety of practical attacks. These vulnerabilities could lead to serious consequences, including the spoofing of digital identities, the exfiltration of sensitive plaintext, and even arbitrary code execution on user systems.
The implications of these findings are profound for anyone relying on PGP and GPG for security. The research not only details numerous critical bugs but also critically examines how PGP alternatives like Minisign and Age fare, providing a holistic view of the ecosystem's security posture. The speakers' work serves as a stark reminder that even long-standing, widely adopted security tools require continuous scrutiny and robust defensive practices to maintain their integrity against sophisticated adversaries.
Background
▶ Watch: Practical GPG verification of a Fedora ISO (0:00)
The journey into the vulnerabilities of GPG began with Lexi's personal fascination with PGP and a penchant for "shenanigans" with the tool, including hand-crafting PGP public keys and observing GPG's parsing behavior. This curiosity led to the discovery of an initial significant bug, which subsequently snowballed into a comprehensive research effort. The core of their investigation focused on PGP (Pretty Good Privacy), an RFC-standardized protocol defining packet structures, cryptographic specifics, and key exchange mechanisms. The primary target was GPG (GNU Privacy Guard), the most common implementation, written primarily in C, which provides both a command-line interface (CLI) and a library. The researchers also examined alternative implementations like Sequoia PGP (SQ), Age for encryption, and Minisign for file signing.
The attack surface explored was multifaceted, encompassing:
- Signatures: How they are parsed, verified, and whether they genuinely authenticate the intended plaintext. This includes cleartext signatures, full signatures, and detached signatures.
- Authentication and Identity: Ensuring the correct key is used for the right user, and that encrypted messages reach the intended recipient.
- Memory Safety: Given GPG's C codebase, the potential for memory corruption bugs like buffer overflows was a critical area of investigation.
- Safe Usage in Environments: How GPG interacts within various operating environments, particularly on the command line, and its susceptibility to user interface manipulation.
The problem's existence stems from the inherent complexity of the PGP specification, which is a rich and intricate standard. Implementing such a specification securely, especially in a low-level language like C, introduces numerous opportunities for subtle yet critical bugs. Furthermore, the evolution of the standard and its implementations over decades means that legacy design choices and code can harbor vulnerabilities that are difficult to identify and rectify without a deep, adversarial understanding of the system. The speakers' work underscores the perennial challenge of bridging the gap between cryptographic theory and secure, practical software.
Key Findings
▶ Watch: How the GPG vulnerability research began (4:40)
Lexi and Liam uncovered a total of 14 vulnerabilities, with 9 affecting signatures directly, and 9 remaining unpatched at the time of the 39C3 talk. These findings expose a broad spectrum of weaknesses:
- Cleartext Signature Injection (CVE-2023-40590): Attackers can prepend arbitrary, unverified text to cleartext signed messages in GPG by exploiting parsing logic related to hash headers and a C null byte vulnerability. This allows visually spoofing the content of signed messages, rendering cleartext signature verification unreliable.
- Minisign C String Trimming Bug: A similar vulnerability found in Minisign allowed injection of content into signed files by exploiting how carriage returns (
\r) are handled during string trimming, effectively truncating the signed content and allowing unverified data to appear as part of the trusted message. - Cleartext/Full Signature Type Confusion: GPG and Sequoia PGP could be tricked into parsing a cleartext signature as a full signature (and vice-versa) by injecting an invisible Byte Order Mark (BOM) or manipulating the
BEGIN PGPheader. This allows presenting arbitrary unsigned content while a valid signature for different content is verified. - Detached Signature Multiple Plaintext Attack (CVE-2023-40591): A flaw in GPG's parsing of detached signatures allowed an attacker to insert a PGP magic byte marker packet, resetting the "plaintext seen" state. This enabled marking unsigned, malicious content as verified alongside a legitimate signature for different data.
- ANSI Escape Code Clickjacking (CVE-2023-40592): GPG's interaction with the terminal, specifically its interpretation of the
filenamefield in plaintext packets, combined with ANSI escape codes, allowed attackers to overwrite user prompts. This could trick users into unknowingly confirming the decryption of untrusted data to sensitive locations (e.g.,~/.bash_complete), leading to arbitrary code execution. - Age Plugin Path Traversal: The Age encryption tool's plugin system allowed arbitrary path traversals within recipient names, enabling remote code execution by trying to encrypt data to a specially crafted public key (e.g.,
H plugin exploit/../../bin/pwn). This was swiftly patched across all implementations and the specification. - GPG Armor Filter Double Increment (CVE-2023-40593): A critical memory safety bug in GPG's armor filter implementation involved a double increment of a counter variable (
n) within a loop. This led to an integer overflow and allowed returning more data than requested, potentially causing out-of-bounds writes or heap corruption, exploitable via a complex "Schrödinger's payload." - GPG Uninitialized Variable (CVE-2023-40594): An uninitialized variable used to determine if a signature was a "self-signature" could be manipulated. This allowed bypassing restrictions on the use of the SHA-1 digest for third-party signatures, potentially enabling the use of cryptographically weak hashes in contexts where stronger ones are required.
- Message Malleability (Won't Fix): Attackers could manipulate encrypted OpenPGP messages without knowledge of the plaintext. By inserting an irregular EOF packet, GPG's warning for message manipulation (
warning: encrypted message has been manipulated) could be suppressed, while still outputting malformed but exploitable plaintext. This allowed exfiltrating plaintext bytes by redirecting them into a key server URL field. - Trust Packet Signature Cache (CVE-2023-40595): A fundamental flaw in GPG's handling of trust packets allowed an attacker to forge signature validity flags. By importing untrusted trust packets via
--keyringor--import-options restore, an attacker could permanently setcheckedandvalidflags on key signatures. This enabled the forgery of key revocations, the addition of unauthorized signing or authentication subkeys, and ultimately, impersonation of any PGP key holder, including high-profile individuals.
Technical Deep Dive
▶ Watch: Introduction to PGP cleartext signatures and unverified headers (6:00)
The vulnerabilities uncovered by Lexi and Liam expose deep-seated issues in GPG's parsing logic, memory management, and trust model. Let's explore a few of the most impactful in detail.
Cleartext Signature Injection (CVE-2023-40590)
This vulnerability targets cleartext signatures, a common format where the plaintext message is readable, followed by a base64-encoded signature block. The GPG specification explicitly states that headers within the cleartext block (e.g., Hash: SHA512) are not included in the hash for security reasons, to prevent an attacker from faking text at the beginning of a document. GPG attempts to mitigate this by only allowing specific hash headers.
However, the researchers found a critical flaw in the pass_header_line function, which calls pass_hash_header. While pass_hash_header correctly checks for extraneous data after the hash declaration, pass_header_line also permits lines starting with ---not-escaped--- without further validation. This means an attacker could craft a header like Hash: SHA512 ---not-escaped--- injected text and GPG would verify it.
The situation is exacerbated by a classic C string handling error. The header lines are processed as char* strings, which are null-terminated. An attacker can embed a null byte (\0) within the crafted header. For example, Hash: SHA512\0injected content. GPG's parsing functions would interpret the line as ending at the null byte, ignoring everything after it for verification purposes, but the full string would still be displayed to the user.
To make the injected content visually convincing, attackers can use non-newline characters like vertical tabs (\v) and carriage returns (\r). These characters manipulate cursor position on a terminal, making Hash: SHA512\v\rInjected Content appear as if "Injected Content" is on a new line, despite GPG parsing it as part of the original line. This allows for highly deceptive cleartext signature spoofing.
Detached Signature Multiple Plaintext Attack (CVE-2023-40591)
Detached signatures involve two separate files: the message and the signature. GPG's parsing logic for these signatures relies on a seen variable, typically initialized to 0. When GPG processes a packet stream, it updates seen: it remains 0 for non-signature/non-plaintext packets, becomes 1 if a plaintext packet is encountered, and then it expects a signature packet. If a signature is found after plaintext, it's verified.
The vulnerability arises because GPG's parser can be reset. If an attacker injects a PGP magic byte marker packet (simply the string "PGP") into the signature file, GPG processes it. This marker is not a signature and not plaintext, so it doesn't trigger the checks that would invalidate the signature. Crucially, the seen variable, which tracks whether plaintext has been encountered, gets reset to 0 after processing this marker.
The attack sequence is as follows:
- A legitimate message is signed, producing a detached signature.
- An attacker crafts a malicious signature file containing:
- A metadata packet.
- A plaintext packet containing arbitrary malicious text (e.g., "malicious").
- The PGP magic byte marker packet.
- The original, legitimate signature packet.
- When GPG processes this crafted file, it first sees the metadata (seen=0).
- Then, the malicious plaintext packet is processed. The plaintext is copied into a buffer, and
seenis set to 1. - Next, the PGP magic byte marker is encountered. This packet type causes
seento be reset to 0. - Finally, the legitimate signature packet is processed. Because
seenis 0, GPG treats this as a valid detached signature, even though the plaintext it just buffered and displayed was the malicious content, not the originally signed message.
The fix, as suggested by the researchers, is to prevent seen from being reset, for instance, by using seen |= 1 instead of seen = 1 to ensure that once plaintext has been "seen," that state cannot be undone by an intermediate packet.
GPG Armor Filter Double Increment (CVE-2023-40593)
This is a classic memory corruption vulnerability found in GPG's C codebase, specifically within the armor filter implementation responsible for handling Base64-encoded data. The bug lies in a loop that copies data, where the counter n is incremented twice: once in the loop header (for (n = 0; n < len; n++)) and once within the loop body (n++). This results in n increasing by 2 in each iteration.
The immediate consequence is that for half of the inputs, this leads to a one-byte override or an integer overflow of an unsigned int used for the return length. This means the function reports that it has processed and returned more data than it actually has.
Exploiting this requires a "Schrödinger's payload" because the vulnerable code path (input_bypassed == 1) is typically only true for binary data, while the armor filter is exclusively pushed for Base64-encoded (non-binary) data. The researchers devised a complex control flow sketch to trigger this:
- An armor header is pushed, setting
input_bypassedto 0. - A large chunk of Base64-encoded data follows. When decoded, this data is large enough to overflow a 64KB internal buffer in GPG's I/O buffer system.
- This overflow causes the
armor_filterfunction to be called twice with different inputs. - In between these calls, a specially crafted marker packet (e.g., an optional CRC line) is used. This marker packet is processed by a different part of the code that, crucially, reuses the
input_bypassedvariable for a slightly different purpose and resets it to 1. - The second call to the
armor_filterthen encounters the remaining Base64 data, now withinput_bypassedset to 1. This triggers the double-increment loop with non-empty binary data, leading to the integer overflow and potential heap corruption.
The fix involved ensuring that the I/O buffer system always asserts that no more data is returned than requested, and correcting the double increment.
Trust Packet Signature Cache (CVE-2023-40595)
This vulnerability fundamentally undermines GPG's trust model. The PGP RFC defines trust packets to indicate which key holders are trustworthy introducers. Critically, the RFC states these packets "should not be emitted to output and should be ignored on any input." However, GPG violates this by allowing the import of foreign trust packets via --keyring or --import-options restore.
The core issue is that GnuPG uses a separate trust.db.gpg file for its trust model and explicitly comments that "trust value is not yet used" within trust packets. However, just a few lines below this comment, the code reveals that trust packets contain a sig_cache byte. If sig_flags.checked is true for the last processed signature, sig_cache is set to 1. If the signature also has the valid bit set, sig_cache is set to 2.
The critical part is the reverse: GPG's packet.c code actively reads this sig_cache byte from trust packets. If the 1-byte is set, it sets the checked flag on the last signature. If the 2-byte is set, it sets the valid flag on the last signature. This means an attacker can simply craft a trust packet with specific bits set in the sig_cache field and, by importing it, directly manipulate the checked and valid status of any key signature (e.g., subkey additions, revocations, user ID certifications).
This has devastating consequences:
- Adding signing/authentication/encryption subkeys: An attacker can add their own subkeys to a victim's public key, effectively gaining the ability to sign or decrypt on behalf of the victim.
- Signing other keys: Forging certifications.
- Revoking other keys: Disabling legitimate keys.
- Impersonation: The talk demonstrated forging a signature from the German government's Governikus PGP service for Angela Merkel, and even adding a signing subkey to Satoshi Nakamoto's PGP key, allowing the creation of a "signed" message claiming "Bitcoin is backdoored."
This bug bypasses the cryptographic verification of key signatures entirely, replacing it with a simple flag read from a manipulable packet, fundamentally breaking the chain of trust.
Demo / Proof of Concept
▶ Watch: Code analysis: discovering the 'not-escaped' header bypass (7:20)
The talk was rich with practical demonstrations, showcasing the real-world impact of these vulnerabilities.
The presentation opened with a staged demonstration of verifying a Fedora ISO download using QEMU. Following the official instructions, Liam used GPG and Sequoia PGP to verify a checksum file against a Fedora GPG key. The checksum appeared correct, matching the SHA-256 hash. However, this was a setup for a later reveal that the initial verification was subtly compromised by a signature type confusion vulnerability, highlighting the deceptive nature of the attacks.
For the cleartext signature injection, Lexi demonstrated how to prepend arbitrary text to a seemingly legitimate cleartext signature. By crafting a message with embedded null bytes (\0), vertical tabs (\v), and carriage returns (\r) in the header, she showed a "good signature from Cicada 3301" for a message claiming the group was at 35C3 and seeking spoils, despite this being entirely spoofed. The visual deception was striking, making the unverified content appear integral to the signed message.
The Minisign vulnerability was demonstrated by showing a file with a trusted comment that, when verified, displayed a completely different, injected comment. This highlighted that even simpler, "secure by design" alternatives can fall prey to similar C string handling issues.
Liam then presented the detached signature multiple plaintext attack. He showed a detached signature file that, when "decrypted" (verified), displayed the string "malicious" as the content, yet GPG still reported a good signature. This vividly illustrated how arbitrary content could be presented as legitimately signed.
The ANSI escape code clickjacking was perhaps the most alarming for command-line users. Liam demonstrated how a crafted encrypted message, when decrypted, could use ANSI sequences to overwrite the GPG prompt. Instead of seeing a warning about decrypting to ~/.bash_complete, the user would see a harmless-sounding prompt like "Do you want to see this in safe mode? [Y/n]". Pressing enter would then overwrite the .bash_complete file, leading to potential code execution.
The Age plugin path traversal was demonstrated by showing an s-trace output of age attempting to encrypt to a recipient name like exploit/../../bin/pwn. The s-trace clearly showed age executing H plugin exploit/../../bin/pwn, proving arbitrary binary execution.
The trust packet signature cache vulnerabilities were showcased with two high-impact proofs of concept:
- Governikus ID Forgery: By importing a specially crafted key and then using
gpg --check-signatures, Lexi demonstrated GPG confidently asserting that she was "Angela Merkel" and had used the German government's ID card service to notarize her PGP key, despite having done no such thing. This highlighted the ability to forge identity certifications from trusted entities. - Satoshi Nakamoto Subkey Addition: Liam showed how to add a signing subkey to Satoshi Nakamoto's PGP key. He then "decrypted" a message that read "Bitcoin is backdoored, get out while you still can," which GPG reported as having a "good signature from Satoshi Nakamoto." This demonstrated the ability to make any PGP key appear to sign arbitrary messages, with the potential to cause real-world financial panic.
- Permanent Encryption Backdoor: A final demo showed how importing a malicious trust packet could permanently backdoor GPG encryption. Encrypting a message to a user's key would also send a copy of the plaintext to a specified "disclosure inbox" (e.g., for CCC), without the user's knowledge, illustrating silent plaintext exfiltration.
These demonstrations underscored the critical and practical nature of the vulnerabilities, moving beyond theoretical concerns to concrete attack scenarios.
Defensive Implications
▶ Watch: Live demo: injecting unverified text in cleartext signatures (8:00)
The extensive list of vulnerabilities in GPG and related tools necessitates a multi-layered defensive strategy for both users and implementers of PGP-based systems.
For Users:
- Update GPG Regularly: Always use the latest patched versions of GnuPG. Many of the reported issues have received fixes, and staying updated is crucial.
- Avoid Cleartext Signatures: Given the numerous vulnerabilities found in cleartext signatures, users should strongly consider avoiding them. Where possible, favor detached signatures or full signatures as they are generally less susceptible to visual spoofing and parsing ambiguities.
- Always Use
--decrypt: When verifying a signed message, especially cleartext ones, always usegpg --decryptorgpg --verify --output -to explicitly output the parsed and verified plaintext to your terminal. This helps ensure that the content you see is what was actually signed, rather than relying on visual inspection of the raw message. - Treat All GPG Warnings as Errors: The message malleability vulnerability demonstrated that attackers can suppress critical warnings by triggering other, less severe error messages. Users should adopt a policy of treating any warning or error message from GPG as an indication that the message's integrity or authenticity cannot be trusted.
- Never Import Untrusted Trust Packets: Crucially, never use
gpg --import-options restoreorgpg --keyringwith files from untrusted sources. These options allow the import of malicious trust packets that can permanently compromise your GPG keyring's trust model, enabling the forgery of key signatures and subkeys. - Be Wary of CLI Prompts: Exercise extreme caution when GPG or other CLI tools prompt for file paths or confirmations, especially if the prompt appears unusual or if ANSI escape codes might be active. Double-check the exact path and context before pressing Enter.
For Implementers:
- Strict Parsing: Implement PGP parsers with extreme strictness. Any deviation from the expected format should result in an error, not an attempt to "recover" or ignore content. This includes rigid handling of headers, packet types, and string terminations.
- Deprecate Cleartext Signatures: Given the inherent ambiguities and attack surface of cleartext signatures, implementers should seriously consider deprecating them in favor of more robust formats like detached or full signatures.
- Sanitize Plugin Names and Paths: For tools like Age that use plugin systems, strictly validate and sanitize plugin names to prevent path traversals and arbitrary code execution. Only allow alphanumeric characters and explicitly disallow path separators.
- Prioritize Integrity Checks Before Output: When decrypting or verifying content, perform all integrity and authenticity checks before outputting any data to the user. This prevents attackers from displaying malicious content while suppressing warnings or errors.
- Avoid User-Settable Signature Caches: The
sig_cachebug in trust packets highlights the danger of allowing users or untrusted input to directly influence fundamental cryptographic trust flags. Trust decisions should be based solely on cryptographic verification. - Leverage Memory Sanitizers and Static Analysis: For C/C++ codebases like GnuPG, employing comprehensive memory sanitizers (e.g., ASan, MSan) during development and testing, along with static analysis tools, is essential to catch memory safety bugs and uninitialized variables before they reach production.
- Adopt Robust I/O Buffer Management: Implement robust I/O buffer systems that strictly enforce data boundaries and prevent conditions like the double-increment bug. Assertions and runtime checks should be in place to catch discrepancies between requested and returned data.
Key Takeaways
- PGP/GPG is not as secure as widely believed: Despite its long history and widespread use, GnuPG harbors a significant number of practical vulnerabilities across various components.
- Signature parsing is a major attack surface: Cleartext, full, and detached signatures are all susceptible to different types of spoofing, injection, and integrity bypasses due to parsing errors and C string handling issues.
- The GPG trust model is fundamentally flawed in practice: The trust packet
sig_cachevulnerability allows for the complete subversion of key certifications, enabling impersonation and arbitrary key manipulation. - CLI tools introduce unique attack vectors: Command-line interfaces can be exploited via ANSI escape codes for clickjacking, leading to code execution, and insecure plugin systems can enable path traversal attacks.
- C memory safety issues persist in critical software: Classic C bugs like double increments and uninitialized variables can lead to severe memory corruption and integrity bypasses in GPG.
- Vendor response varies significantly: While some projects like Age addressed vulnerabilities swiftly and transparently, GnuPG's response was mixed, with several critical issues remaining unpatched or marked as "won't fix" at the time of the talk.
About the Speaker(s)
The talk was delivered by Lexi (49016) and Liam, a duo who play together for the same Capture The Flag (CTF) team. Lexi primarily focuses on web challenges and describes herself as someone who genuinely enjoys "doing shenanigans with GPG," which ultimately led to the discovery of many of the bugs presented. Liam, on the other hand, specializes in binary exploitation. Their combined expertise across different security domains allowed for a comprehensive and deep analysis of the PGP ecosystem, spanning web-related parsing issues to low-level memory corruption.
All talks from 39th Chaos Communication Congress (39C3): Power Cycles