On 12/29/25 10:57, Lexi Groves (49016) wrote:
Hi! Thanks for the comment. Some clarifications from us:

> Overall, I have one very major point of disagreement here: the OpenPGP clearsign format is useful precisely because it enables the signed message to be easily viewed with other tools, thus complicating attacks and making large-scale attacks much more likely to be detected.  (It only takes one user who looks at a clearsigned digest list with less(1) and sees a bunch of control sequences to raise an alarm.)

This affects two of the vulnerabilities, and while we do agree that we focused exploitation on naive viewers, one can also leave out the "creative formatting"; especially the `NotDashEscaped:` header can also be used without any control sequences. These two specific attacks do have their limitations, but are still worth reporting.

I agree that they are worth reporting, but I am not yet convinced that they are fatal flaws in the OpenPGP clearsign format.

> Item 1:  Multiple Plaintext Attack on Detached PGP Signatures in GnuPG
>
> Exploitation requires a very odd use of signatures.  Bob knows that he has a detached signature, and ordinarily a detached signature is simply used to verify the signed file, after which the signed file is used directly.

This was found especially in response to the clearsig advice being to use an option to print what was *actually* verified, e.g. `--decrypt`, to avoid plaintext confusion attacks. This fools exactly that mechanism that was used to prevent exploitation in the previous attacks.

In other words, the correct ways to verify a detached signature and a clearsigned message are different.  Since which of those you should have is obvious, that is only a matter of user education.

[...]

We assumed that the manual was the source of truth and assumed that using `--decrypt` was the standard way to do this; we may have been biased here, because apparently the common knowledge about this (according to some other documentation that we did not see) was using `--output/-o`. However, due to the nature of the attack, setting the wrong output file while hashing the correct file, `--output` works the same way:

```
$ gpg --output x --verify msg.txt.sig msg.txt
gpg: Signature made Mon 29 Dec 2025 02:59:11 PM CET
gpg:                using EDDSA key EE6EADB4CBB063887A3BE2B413AEBEC571BA1447
gpg: Good signature from "39c3 demo <[email protected]>" [ultimate]
$ cat msg.txt
asdf
$ cat x
Malicious
```

Now that I look at this again, I see a logic error in GPG:  GPG *should* barf if directed to verify a detached signature (which it knows because it was given the name of the file to verify on the command line) but the "detached signature" contains a message in the OpenPGP packet stream.

There is another logic error here also:  GPG can be tricked into emitting output that is *not* the signed message.  Do I now understand correctly that GPG will *verify* the original signature but *output* the contents of the injected Literal Packet?

In other words, there is a workaround for careful users:  use `--decrypt`/`--output` *only* when reading a signed message, and only `--verify` when checking a detached signature.  Is this correct?

[...]

> Item 2:  GnuPG Accepts Path Separators and Path Traversals in Literal Data "Filename" Field
>
> While this is a potentially serious bug, as it enables an attacker to potentially overwrite any file if the attacker can guess the file name, it also relies more on a social-engineering attack.  While a naive user might use the suggested command, a more-experienced user should immediately smell a rat.

Yes. This probably wouldn't fool a hardcore cypherpunk, but to be honest, it'd get me. Reaper compared this to a clickjacking attack in web exploitation, and we completely agree with this classification: this is an exploit chain that abuses the naivety of the user to trigger a *technical* issue.  Nevertheless, I believe that software should always do its best to protect against human error.

I agree that GPG should force extracted files visibly into the current directory unless explicitly directed otherwise.  This is logically easy on POSIX:  s!/!_!g and s!^[.]!_! on the filename. Of course, if the user specifies `--output` then write to exactly the name given.

> The PoC uses ANSI escapes to cover up (erase, move left) the hash mark that makes the fake message viable as a shell script, the actual payload command (set as invisible text), and the prompt from GPG about writing the output file (which appears to be sent to the window title).  I am uncertain how the user is supposed to see the next prompt, as the invisible text mode does not appear to be reset.

This was a slight miscommunication; this does indeed swallow the next bash prompt. Assuming a user doesn't then recover their terminal but just restarts the shell, it spawns a new Bash process, which sources the newly written file. This was just one example of exploitation; there probably is a way to turn an arbitrary write into direct execution on most systems without relying on bash.

Perhaps GPG's "write file" prompt could be made dynamic, with a nonce string generated by GPG that the user must type to approve writing the file?   An exploit that hides the GPG prompt thus prevents the user from approving the file write.

> Item 3:  Cleartext Signature Plaintext Truncated for Hash Calculation
>
> GPG uses in-band signaling if a line is too long.  Comments in the GPG sources acknowledge that this is a hack.
>
> The exploit does have a problem in that it reports an error about "invalid armor" due to a line being too long.  Also, while it works if the message is fed directly to a terminal, I suspect that reading the message with less(1) would show interesting anomalies.

There would indeed be an anomaly; there have to be 30,000 bytes of *something*. An experienced user might suspect something is wrong, but this is a serious issue regardless.

Agreed.  This should be correctly handled, as the GNU Coding Standards are very clear about avoiding arbitrary limits.

> Item 4:  Encrypted message malleability checks are incorrectly enforced causing plaintext recovery attacks
>
> This item admits not actually having a complete attack, and I am unsure how exactly this leads to recovering plaintext, even if Bob cooperates.

While we have not chained all bugs together, we have gotten outputs like [this](https://keys.openpgp.org/[email protected]) on production builds of GnuPG 2.4.8 that contain the plaintext of an encrypted file (viewed with Sequoia to highlight the issue):

```
$ gpg --export [email protected] | sq packet dump

Public-Key Packet, new CTB, 2 header bytes + 51 bytes
    Curve: Ed25519
    Fingerprint: 06993EC337C276ECFD8C598AB613D42DB68D9E1D
    [...]

Signature Packet, new CTB, 3 header bytes + 371 bytes
    Type: DirectKey
    Unhashed area:   Here's the zlib header! ↓
      Preferred keyserver: "http://localhost:9999/upload-key?x=�\u{14}G\"\u{16}��>�\u{15}fd�x\u{15}D\u{15}D�\u{2}x�\u{1}H\0���Fb\0iHP                             ��#�5 That is plaintext w�W���w\u{605}���r\u{1c}�&\u{f}ǟ�\r�^\u{e}��D}�,/�n_���{�Jv�!��\u{14}�\t\u{7}��;�jB2D�!6�a��\"�"
```

> (User willingness to sign a random key received by email without verifying it breaks Web-of-Trust badly.)

The attack does require some user interaction, and a plausible one is always a bit of a challenge; the attack scenario we were thinking of was telling a victim to decrypt and upload a key to a public keyserver *without signing it*, just to publish it (for example to circumvent censorship). We got this slightly wrong in the writeup, apologies for that mistake. We have a correct but brief explanation in the talk (at gpg.fail) as well.

I agree that that is much more plausible.

> If there is a bug here, it seems to be an out-of-bounds read in GPG's Inflate implementation:  the decrypted ciphertext is in the memory buffer prior to the altered packets and thus prior to the beginning of the compressed stream.
>
> I am unsure how the garbled comment packet is produced, unless it is the result of interpreting the decrypted data as a compressed stream and "inflating" it.

This is *not* a memory safety issue, and explaining the bug a bit further in-depth hopefully clears this up. Excuse my potentially incorrect language, I am only a hobby cryptographer for fun, but I will try to break this down.

I now see what I missed last night (explained below).  While looking at this again, I found an error in your original write-up:  you also mentioned "working with AES-256-CBC" (but note that CBC is *also* malleable).

PGP uses the [Cipher FeedBack/CFB mode of operation](https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_feedback_(CFB)). Decryption here is the thing that interests us, which works the following way:

1. Take the ciphertext of the last block, or when initializing the IV block
2. Encrypt (yes, *en*crypt) the result of 1. with the key
3. XOR the current ciphertext with the result of 2.

A malleability attack, where an attacker tries to manipulate the output of this operation, is possible by simply XORing bits into the current ciphertext block. This will break the next block, but we can inject data into the current block as long as we can guess the plaintext of the current block.

This is practically exploitable since GPG defaults to ZLib compression, which has a quite predictable header, and since the attack only requires guessing 7 bytes from the ZLib header to set up a "trampoline", this is practically doable.

Said "trampoline" does the following:

- Since the malleation corrupts the block following it, we need to "catch" that error, like a `try/catch` statement; for that we (ab)use the PGP comment packet, which is meant for adding comments to PGP packet streams and thus ignores all input. - We also need deflate, specifically because since it does not have a header like ZLib, it is short enough to fit in there, and for a trick up our sleeve later.

So what we insert via XORing is:

```
      a3 01                                      # PGP Compressed packet, algorithm=DEFLATE             00 NN NN ~N  ~N                      # Deflate store (e.g. no compression) block of len=NN                             d0 NN                # PGP comment packet, len=NN                                   00 00 00 00 00 # garbage till end of block
```

This sets us up so our PGP packet stream does not get corrupted.

Here's what you probably missed: The CFB mode is essentially a state machine that only knows the last block of ciphertext, the current block of ciphertext, and the stateless encryption function. *We can reset this state to the initial position* by just putting a block filled with zeroes to reset the state, and then *repeat the entire ciphertext*, which a correct PGP implementation then decrypts into that comment packet. *This is a fundamental issue with CFB, and not a PGP or GPG issue*, but we *also* broke the checksum handling of GPG to obfuscate malleability attacks; more on this in a bit.

But if that is it, how did you reset the read pointer to get the ciphertext decrypted a second time?  I *now* see what I missed then:  you did not reset the read pointer; you copied the entire original ciphertext where it will be decrypted and fed into Inflate, in a context where Inflate is expecting literal data which will go into an OpenPGP comment packet and be discarded.

You exploited CFB's malleability to convert the ZLIB header into the start of a DEFLATE stream to set up that context and the reset the keystream so the copy of the ciphertext will be correctly decrypted.  (A similar attack would also work on CBC.)

Clever, very clever.  :-)

Now that we have our plaintext in the decompression buffer, we abuse *another* bug to remove the decryption IOBUF filter to be able to freely write our payload. We still have the decompression filter on the stack, so we can just write a normal public key packet, and inside the public key packet use decompression features to repeat earlier contents of the buffer to recover the plaintext in fields like the keyserver field.

You then exploit a logic error in GPG to stop decrypting while continuing with the same Inflate context, allowing you to simply write the key packet headers and copy the decrypted data out of the decompression buffer into its place in your fake pubkey template.

(note to self:  when designing a chunked data format, compression wrappers should allow *exactly* one---and only one---compressed payload chunk)

This works as long as the encrypted message is small, 64 bytes in your example.  What is the upper limit for the message length that can be revealed with this method?

[...]

> Item 8:  OpenPGP Cleartext Signature Framework Susceptible to Format Confusion
>
> Another logical solution to this issue would be to recognize when processing something that looks like a clearsigned message and reject a one-pass signature if a clearsigned message header has been seen.

This is not that easy; defining what a "header" is is pretty hard, as the attack demonstrates purposefully breaking the header. One way to fix this would be to only allow whitespace instead of arbitrary text before and after the parsed data.

It should be fairly simple:  only clearsigned messages have separate "BEGIN PGP SIGNED MESSAGE" and "BEGIN PGP SIGNATURE" lines and they do not contain One-Pass Signature Packets.  If "BEGIN PGP SIGNED MESSAGE"/"BEGIN PGP SIGNATURE" lines are seen before encountering a One-Pass Signature Packet, barf.

Generating a big fat WARNING if the input is found to match /^-{3,4}BEGIN PGP/ might also be a good idea, since the correct marker lines contain five hyphens in each group.

[...]

> Item 12:  GnuPG may downgrade digest algorithm to SHA1 during key signature checking
>
> The root of this is another out-of-bounds read.  There is a simple fix to this:  always, *always*, *ALWAYS* initialize stack-resident local variables.
>
> I am also unsure about the actual insecurity of SHA1 in general.  Have there been more attacks since the first actual collision?

As far as we know, this does not have any direct impact for a user without huge amounts of compute, but regardless should be fixed as a downgrade to SHA1 is significantly reducing security.

I am not sure about that.  As I understand, OpenPGP (and Git, for another example) only needs second preimage resistance, unlike X.509 which needs absolute collision resistance, and the closest attack on SHA-1 is still only a chosen-prefix collision.

The SHA-1 sky has not fallen, yet.  It may be getting a bit creaky, but it is not falling.  :-)  (Yet...)  :-/

This logic error should still be fixed, of course.

> Item 13:  GnuPG Trust Packet Parsing Enables Adding Arbitrary Subkeys
>
> Keyrings are trusted stores, so this is more of a documentation problem.
>
> The report is right that caching signature checks is probably a bad idea, although it may have been justifiable in the past due to limited computing power.

What should be highlighted especially here are the manual page entries:

```
--keyring file
    Add  file  to  the  current list of keyrings. If file begins with a tilde and a slash,     these are replaced by the $HOME directory. If the filename does not contain  a  slash,     it  is  assumed  to  be  in  the  GnuPG home directory ("~/.gnupg" unless --homedir or
    $GNUPGHOME is used).

    Note that this adds a keyring to the current list. If the intent is to use the  speci-
    fied keyring alone, use --keyring along with --no-default-keyring.

    If the option --no-keyring has been used no keyrings will be used at all.

    Note  that if the option use-keyboxd is enabled in 'common.conf', no keyrings are used     at all and keys are all maintained by the keyboxd process in its own database.

--import-options restore
    Import in key restore mode.  This imports all data which  is usually  skipped  during     import;  including  all  GnuPG  specific  data.  All other contradicting  options  are
    overridden.
```

`restore` vaguely implies that something that usually should not be imported is imported, but especially `--keyring` does not mention *at all* that this *effectively disables key signature verification*. As a user, I would expect `--keyring` to actually *use* the provided public keys instead of just skipping signatures when requested. We would not consider this a bug if this was called `--INSECURE-allow-importing-signature-cache-values` with a huge warning on the manual page, but especially since `--keyring` is a common option, this is a very real security risk in our opinion. Importing and using untrusted public keys (without giving them a trust value) should be a safe operation in cryptography software.

There should probably be some changes to the documentation.  For example, making clear that "GnuPG specific data" includes trust values.

The `--keyring` option is supposed to only affect the GPG invocation to which it is given.  Is it possible to add a key or trust packet to the default keyring merely by using a keyring containing that packet in an operation other than `--import`?

Also, the GPG developers seem intent on deprecating keyrings and moving to their newer "keybox" format.  (I think it is an SQLite database, but have not looked into it yet.)

[...]

Best, Lexi Groves (49016)

-- Jacob

Reply via email to