PGP key migration with GPG
This is the second post in a two-part series on GPG. In the first, I shared how I learned to effectively call GPG in scripts.
I started down this path because I wanted to manage my PGP signing keys the way I manage my SSH keys—one unique key per host—and for the same reason: if a host is ever lost or compromised, I can revoke the key for just that host without disturbing my other hosts. The problem I ran into is that, unlike SSH keys, PGP keys must be derived from each other to be useful. SSH does not care if we have ten different identities, but PGP wants them all to be signed by the same primary key. The consequence is that I cannot generate a new PGP key from scratch on a new host. Instead, I must generate it on an old host and copy it to the new host, which complicates the process for bootstrapping my developer environment.
The most popular search results for PGP key migration
recommend a process that copies all of your keys to each new machine.
Ideally, though, one should never copy their secret primary key anywhere.
We can export subkeys only, without the primary key,
using --export-secret-subkeys
, but that includes every subkey.
I really want to export just one specific secret subkey,
the one for signing on the new machine.
One way to select a specific subkey for export is with an export filter.
The idea is to use the drop-subkey
filter to drop every subkey
except the one we want, identified by its fingerprint:
> gpg --export-secret-subkeys --export-filter drop-subkey="fpr <> ${subkey}"
An easier way is to pass the subkey as an argument,
but with an exclamation point (!
):
> gpg --export-secret-subkeys ${subkey}!
Exporting a secret key includes its public key, so I don't need to export it separately, but I do want the public halves of the other subkeys in order to verify signatures from other hosts. Those must be exported with a separate command, but they don't need to go in a separate file—they can just append to the same file for easy transport.
Even though I want a unique signing key per host, I don't delete the subkey after I export it. I keep it around in case I ever need to revoke it, or more likely, update its expiration date.
That means I have one machine, the primary key machine, that has a keyring containing every secret key—the primary key and all of its subkeys—while each development machine has just one unique signing subkey in its keyring, one which no other development machine has.
I want Git to sign commits on multiple machines, but it is annoying
to configure a different user.signingkey
setting on each.
However, I can configure them all to use the primary key,
and GPG will use the first eligibile subkey
if the primary secret key is missing.
But if the primary key machine has every subkey in its keyring,
then how can we make Git use a specific subkey on that machine?
I move the primary keyring with all subkeys to its own GPG home directory
and keep only the unique signing subkey for that machine
in the default home directory (~/.gnupg
).
In the end, my general pattern for bootstrapping a new machine is this:
- Generate a new subkey in the primary keyring, wherever it is.
- Export only that secret subkey, armored.
- Append all public keys.
- Copy that one file to a new machine.
- Import its keys and update their trust level.
I keep these ready-to-copy commands in my bootstrap instructions:
# On your primary key machine:
fingerprint() { awk -F\: '/^fpr/ { print $10 }'; }
export GNUPGHOME=~/gnupg
primary=$(gpg --list-keys --with-colons | head -3 | fingerprint)
# You need your passphrase for this step.
gpg --quick-add-key ${primary} ed25519 sign 2y
subkey=$(gpg --list-keys --with-colons | tail -3 | fingerprint)
# Need passphrase.
gpg --armor --export-secret-subkeys ${subkey}! >subkey.asc
gpg --armor --export >>subkey.asc
scp subkey.asc $(whoami)@${remote}:/home/$(whoami)/
# On your new machine:
# Need passphrase.
gpg --import subkey.asc
gpg --batch --command-fd 0 --edit-key jfreeman08@gmail.com <<EOF
trust
5
y
save
EOF
In order for GitHub to verify signatures, I then need to share my new public subkey with them. In order to update an existing GPG keyring on GitHub, we have to delete the old one and upload the new one.
> gpg --armor --export | xclip -in
When it comes time to update the expiration date of a key, I update it on the primary key machine and then repeat the steps to export it and import it, except that I don't need to update the trust again.