Encrypted message malleability checks are incorrectly enforced causing plaintext recovery attacks
A flaw in GnuPG’s enforcement of integrity protections allows practical malleability of encrypted messages. Specifically, GnuPG violates specified requirements for Modification Detection Code (MDC) verification, permitting attackers to manipulate encrypted packets in ways that can lead to plaintext recovery attacks under realistic conditions.
Impact
A user might be tricked into decrypting and publishing a secret encrypted message, e.g. by changing packet types of a secret message to look like a public key packet.
Details
GnuPG defaults to using a MDC for proving integrity. RFC 9580 specifies the MDC format as:
Two constant octets with the values 0xD3 and 0x14 are appended to the plaintext. Then, the plaintext of the data to be encrypted is passed through the SHA-1 hash function. The input to the hash function is comprised of the prefix data described above and all of the plaintext, including the trailing constant octets 0xD3, 0x14. The 20 octets of the SHA-1 hash are then appended to the plaintext (after the constant octets 0xD3, 0x14) and encrypted along with the plaintext using the same CFB context. This trailing checksum is known as the Modification Detection Code (MDC).
During decryption, the plaintext data should be hashed with SHA-1, including the prefix data as well as the trailing constant octets 0xD3, 0x14, but excluding the last 20 octets containing the SHA-1 hash. The computed SHA-1 hash is then compared with the last 20 octets of plaintext. A mismatch of the hash indicates that the message has been modified and MUST be treated as a security problem. Any failure SHOULD be reported to the user.
GnuPG violates this requirement in two dangerous ways:
- Packets can be modified by an attacker to output a failure that appears harmless to the user, such as truncation, and
- it does not discard inputs known to be a security problem and continues processing the data.
In decrypt-data.c’s decrypt_data function, this code can set the return code to invalid packet when an irregular end-of-file was seen:
ed->buf = NULL;
if (dfx->eof_seen > 1)
rc = gpg_error(GPG_ERR_INV_PACKET); However, the code handling this return code in mainproc.c’s proc_encrypted function does not adequately handle this case:
result = decrypt_data (c->ctrl, c, pkt->pkt.encrypted, c->dek,
&compl_error);
// ...
} else if (!result || (gpg_err_code(result) == GPG_ERR_BAD_SIGNATURE
&& !pkt->pkt.encrypted->aead_algo
&& opt.ignore_mdc_error)) {
/* All is fine or for an MDC message the MDC failed but the
* --ignore-mdc-error option is active. For compatibility
* reasons we issue GOODMDC also for AEAD messages. */
write_status(STATUS_DECRYPTION_OKAY);
if (opt.verbose > 1)
log_info(_("decryption okay\n"));
if (pkt->pkt.encrypted->aead_algo) {
write_status(STATUS_GOODMDC);
compliance_de_vs |= 4;
} else if (pkt->pkt.encrypted->mdc_method && !result) {
write_status(STATUS_GOODMDC);
compliance_de_vs |= 4;
} else
log_info(_("WARNING: message was not integrity protected\n"));
} else if (gpg_err_code(result) == GPG_ERR_BAD_SIGNATURE
|| gpg_err_code(result) == GPG_ERR_TRUNCATED) {
glo_ctrl.lasterr = result;
log_error(_("WARNING: encrypted message has been manipulated!\n"));
write_status(STATUS_BADMDC);
write_status(STATUS_DECRYPTION_FAILED);
} else {
if (gpg_err_code(result) == GPG_ERR_BAD_KEY
|| gpg_err_code(result) == GPG_ERR_CHECKSUM
|| gpg_err_code(result) == GPG_ERR_CIPHER_ALGO) {
if (c->symkeys)
write_status_text(STATUS_ERROR,
"symkey_decrypt.maybe_error"
" 11_BAD_PASSPHRASE");
if (c->dek && *c->dek->s2k_cacheid != '