How to fix GnuPG after importing a poisoned key (without just deleting it)

I spent some time looking into how to fix this, and I published an article about it on my website. Below is a summary of the solution to this question.

1. Identifying the poisoned key

First, we can list the size the public keys in our keyring (in bytes) using the following command (as reported on the GnuPG issue tracker):

user@disp1754:~$ gpg --export | gpg --list-packets | awk -F= -v oldoff=-1 -v keyid=unset '
/^# off=/{ off = $2 + 0 }
/^:public key/{ if (oldoff>-1) { print (off - oldoff) " " keyid }; oldoff = off; keyid = "unset"; }
/keyid:/ {if (keyid == "unset") { keyid = $1; } }
END { print (off - oldoff) " " keyid ; };' | sort -n
7284    keyid: 1DCBDC01B44427C7
119748  keyid: 4E2C6E8793298290
124557  keyid: 403C2657CD994F73
16934647    keyid: F20691179038E5C6
user@disp1754:~$ 

If the above command takes longer than a few seconds to run, then you have a problem. Give it 20 minutes or so, and you'll see problematic keys at the bottom. Anything with 8 digits (>10 MB) is a red flag.

ⓘ Note: In the example above, we see that the public key with id = F20691179038E5C6 has a size of 16934647 bytes = 16M. This is our poisoned key.

The commands that follow in this article use this keyid (F20691179038E5C6) to manipulate the keyring. You should replace this string in the commands below with the corresponding keyid found in the command above on your machine.  

2. Exporting the poisoned key

Now that we've identified the poisoned key, let's export it for safe keeping before we delete it.

user@disp1754:~$ time gpg -a --export 'F20691179038E5C6' > pubkey.asc

real    3m30.950s
user    3m24.430s
sys 0m0.322s
user@disp1754:~$ du -sh pubkey.asc 
22M pubkey.asc
user@disp1754:~$ 

After a few minutes, the command will finish, and you should have a file named pubkey.asc with the contents of the poisoned public key in it. Note that this ASCII armored file containing exactly one public key is 22M!

3. Deleting the poisoned key

Now that we have a safe backup of the poisoned key on disk, let's delete it from our keyring.

user@disp1754:~$ time gpg --delete-key 'F20691179038E5C6'
gpg (GnuPG) 2.1.18; Copyright (C) 2017 Free Software Foundation, Inc.
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.

pub  ed25519/F20691179038E5C6 2019-01-19 Daniel Kahn Gillmor <[email protected]>

Delete this key from the keyring? (y/N) y

real    12m15.265s
user    11m54.242s
sys 0m0.715s
user@disp1754:~$ 

4. Re-importing the cleaned key

To re-import a clean copy of the public key, we'll use the gpg argument --import-filters to drop all signatures (drop-sig) made on the date when the certificate was flooded with signatures.

Given a public key file (like the backup we just exported above), we can list a count of the number of signatures that certificate received for each day with the following command:

user@disp1754:~$ time gpg --list-packets pubkey.asc | grep -i 'sig created ' | sort -n | uniq -c
     11     hashed subpkt 2 len 4 (sig created 2019-01-19)
      2     hashed subpkt 2 len 4 (sig created 2019-01-20)
      4     hashed subpkt 2 len 4 (sig created 2019-01-21)
      2     hashed subpkt 2 len 4 (sig created 2019-01-28)
  14400     hashed subpkt 2 len 4 (sig created 2019-06-17)
  40200     hashed subpkt 2 len 4 (sig created 2019-06-18)

real    0m23.061s
user    0m17.803s
sys 0m0.150s
user@disp1754:~$ 

The above output shows that

  1. 14,400 signatures were made to the key on 2019-06-17 and
  2. 40,200 signatures were made to the key on 2019-06-18

We can import the key while omitting these spammed signatures on these two days as follows (be sure to replace the dates with the corresponding days printed from the above command on your machine):

user@disp1754:~$ time gpg --import-filter drop-sig="sig_created_d=2019-06-17 || sig_created_d=2019-06-18" --import pubkey.asc
gpg: key F20691179038E5C6: 54614 signatures not checked due to missing keys
gpg: key F20691179038E5C6: public key "Daniel Kahn Gillmor <[email protected]>" imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg: no ultimately trusted keys found

real    3m12.091s
user    3m6.991s
sys 0m0.284s
user@disp1754:~$ 

And now things should be much more sane:

user@disp1754:~$ time gpg --export | gpg --list-packets | awk -F= -v oldoff=-1 -v keyid=unset '
> /^# off=/{ off = $2 + 0 }
> /^:public key/{ if (oldoff>-1) { print (off - oldoff) " " keyid }; oldoff = off; keyid = "unset"; }
> /keyid:/ {if (keyid == "unset") { keyid = $1; } }
> END { print (off - oldoff) " " keyid ; };' | sort -n
7284    keyid: 1DCBDC01B44427C7
8930    keyid: F20691179038E5C6
119748  keyid: 4E2C6E8793298290
124557  keyid: 403C2657CD994F73

real    0m0.063s
user    0m0.059s
sys 0m0.016s
user@disp1754:~$ gpg -a --export '403C2657CD994F73' > pubkey2.asc
user@disp1754:~$ du -sh pubkey2.asc 
168K    pubkey2.asc
user@disp1754:~$ 

5. Updating your GnuPG config

As Robert J. Hansen (whoose pgp key was spammed with 149,100 signatures on 2019-06-19) pointed out in their excellent comprehensive gist about this issue, you can prevent your gpg client from breaking itself by:

  1. Removing any lines that start with `keyserver` in your `gpg.conf` file and
  2. Updating `dirmngr.conf` so that it has only 1 `keyserver` = `keyserver hkps://keys.openpgp.org`

That keys.openpgp.org keyserver is a new experimental server (interestingly, it went live just weeks before these poisoned certificates were uploaded) that is more resistant to these attacks. Note that the certificates it serves entirely lack third party signatures, and it also strips the UID packets from the key unless a user explicitly opts-in.

6. Updating your MUA config

You may also need to update your MUA. For example, enigmail in thunderbird may also be configured to update the keys in your keyring.

To prevent enigmail from refreshing your keys from a keyserver, go to your thunderbird preferences -> Advanced -> Config Editor... -> I accept the risk!

And set extensions.enigmail.keyRefreshOn to false

Addendum

Note that the keybox keyring format will refuse to import posioned keys as it has a max key size of 5 MiB, and that users with old installs should consider migrating their keyring to keybox format.

This can be done easily in debian-based systems using the migrate-pubring-from-classic-gpg command


but how can I clean the public key locally without having to download a fresh copy (which introduces unnecessary vectors for tampering)?

Untested method:

  1. Run gpg --edit-key <keyid> clean save.

Another method:

  1. Export the key (or all keys) to a file using --export-options export-clean (which will skip all signatures that can't be verified against trusted keys in your keyring).
  2. Delete the key.
  3. Import the key from the file you made before.

Yet another method:

  1. Write down the key's fingerprint (the full-length one, not just the 16-digit "key ID").
  2. Delete the key.
  3. Download a fresh copy based on the fingerprint (this time using --keyserver-options self-sigs-only if you choose to use a keyserver).