pgp on yubikey / refresh expiry
Generally, I try to follow security best practices. This means that I have my PGP signing, authentication and encryption keys on my YubiKey, and I have configured the keys to expire after a year. Unfortunately, refreshing the expiry every year is not quite enough to store how to do that into muscle memory. Here are the steps relevant to my use case.
Putting the keys on the YubiKey in the first place is worth a post of its own. But others have done that well enough, like Andrea Grandi with configuring onfiguring an offline GnuPG master key and subkeys on YubiKey.
First step is noticing that it's that time of the year again
(Yes, XIII is the alias/comment for my key.)
$ gpg --armor --sign -u xiii
gpg: skipped "xiii": Unusable secret key
gpg: signing failed: Unusable secret key
$ gpg --armor --encrypt -r xiii
gpg: xiii: skipped: No public key
gpg: [stdin]: encryption failed: No public key
$ gpg -K xiii
sec# rsa4096 2017-10-10 [SC] [expired: 2020-10-11]
...C170E20E
uid [ expired] Walter Doekes (XIII) <walter@example.com>
ssb> rsa4096 2017-10-10 [S] [expired: 2020-10-11]
ssb> rsa4096 2017-10-10 [E] [expired: 2020-10-11]
ssb> rsa4096 2017-10-10 [A] [expired: 2020-10-11]
Okay. Time to dig up the old master key so we can update the subkeys.
Note the hash mark (#
) in the above listing: the private key for the
master key is not available here.
Note the angle bracket (>
) next to ssb
(Secret SuBkey): that
means those private subkeys are on a smart card.
Update the expiry in a temporary location
We don't want to load the master key into our GPG config. But we do
need it to update the (subkey) expiry values. So, we use a temporary
GNUPGHOME
.
I'll leave the task of fetching the master key (with subkeys) from a
secure storage to you. Assume you've got it in
MASTER_KEY_AND_SUBKEYS
.
$ TEMPHOME=$(mktemp -d '/dev/shm/gnupghome.XXXX' | tee /dev/stderr)
/dev/shm/gnupghome.rYCR
Using /dev/shm
here instead of /tmp
to make it less likely that
decrypted GPG files ever touch the disk.
$ GNUPGHOME=$TEMPHOME gpg --import < MASTER_KEY_AND_SUBKEYS
gpg: keybox '/dev/shm/gnupghome.rYCR/pubring.kbx' created
gpg: /dev/shm/gnupghome.rYCR/trustdb.gpg: trustdb created
gpg: key ...C170E20E: public key "Walter Doekes (XIII) <walter@example.com>" imported
gpg: To migrate 'secring.gpg', with each smartcard, run: gpg --card-status
gpg: key ...C170E20E: secret key imported
gpg: Total number processed: 1
gpg: imported: 1
gpg: secret keys read: 1
gpg: secret keys imported: 1
WARNING: Do not merge any new public data/signatures from your
regular GPG at this point —
gpg --export xiii | GNUPGHOME=$TEMPHOME gpg --import
— as it would
turn all secret subkeys into stubs, and your backup of these keys
would then contain the master key only.
Enter the gpg
console, so we can set a new expire date:
$ GNUPGHOME=$TEMPHOME gpg --edit-key xiii
...
sec rsa4096/...C170E20E
created: 2017-10-10 expired: 2020-10-11 usage: SC
trust: unknown validity: expired
sub rsa4096/...
created: 2017-10-10 expired: 2020-10-11 usage: S
sub rsa4096/...
created: 2017-10-10 expired: 2020-10-11 usage: E
sub rsa4096/...
created: 2017-10-10 expired: 2020-10-11 usage: A
First, set key 1 through 3, to edit the subkeys:
gpg> key 1
gpg> key 2
gpg> key 3
...
sub* rsa4096/...
created: 2017-10-10 expired: 2020-10-11 usage: S
sub* rsa4096/...
created: 2017-10-10 expired: 2020-10-11 usage: E
sub* rsa4096/...
created: 2017-10-10 expired: 2020-10-11 usage: A
Note how an asterisk (*
) appears next to the word sub
.
Set them to expire after a year from now:
gpg> expire
Are you sure you want to change the expiration time for multiple subkeys? (y/N) y
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 1y
Key expires at wo 13 okt 2021 10:34:59 CEST
And do the same for the master key:
gpg> key
(all subkey asterisken are gone again)
gpg> expire
Changing expiration time for the primary key.
gpg: WARNING: no user ID has been marked as primary. This command may
cause a different user ID to become the assumed primary.
...
Key is valid for? (0) 1y
Key expires at wo 13 okt 2021 10:36:51 CEST
At this point, you may want to call clean sigs
here too, which will
remove expired signatures:
- clean sigs
- Remove any signatures that are not usable by the trust calculations. For example, this removes any signature that does not validate. It also removes any signature that is superseded by a later signature, or signatures that were revoked.
(Check gpg --list-sigs xiii
before/afterwards.)
gpg> save
Check and back up this update master and subkeys
Expiry has been fixed.
BEWARE: If you see hash marks (#
) next to the ssb
, your private
subkeys are not available, and you may have erased them in an earlier
step. You may want to go back and fix that, before you overwrite the
private keys in your safe storage.
$ GNUPGHOME=$TEMPHOME gpg -K
/dev/shm/gnupghome.rYCR/pubring.kbx
-------------------------------
sec rsa4096 2017-10-10 [SC] [expires: 2021-10-13]
...C170E20E
uid [ unknown] Walter Doekes (XIII) <walter@example.com>
ssb rsa4096 2017-10-10 [S] [expires: 2021-10-13]
ssb rsa4096 2017-10-10 [E] [expires: 2021-10-13]
ssb rsa4096 2017-10-10 [A] [expires: 2021-10-13]
Export this to your safe place (beyond the scope of this post):
$ GNUPGHOME=$TEMPHOME gpg --armor --export-secret-keys > MASTER_KEY_AND_SUBKEYS_NEW
Import the new public keys
We only need to update the public keys in our regular environment. Do this by exporting directly from the temporary home:
$ GNUPGHOME=$TEMPHOME gpg --armor --export | gpg --import
gpg: key ...C170E20E: "Walter Doekes (XIII) <walter@example.com>" 6 new signatures
gpg: Total number processed: 1
gpg: new signatures: 6
$ gpg --card-status | grep expires
sec# rsa4096/...C170E20E created: 2017-10-10 expires: 2021-10-13
ssb> rsa4096/... created: 2017-10-10 expires: 2021-10-13
ssb> rsa4096/... created: 2017-10-10 expires: 2021-10-13
ssb> rsa4096/... created: 2017-10-10 expires: 2021-10-13
$ gpg --armor --sign -u xiii
test
^D
-----BEGIN PGP MESSAGE-----
...
Yay!
Destroy the temporary files
$ find $TEMPHOME -type f
/dev/shm/gnupghome.rYCR/pubring.kbx~
/dev/shm/gnupghome.rYCR/pubring.kbx
...
$ find $TEMPHOME -type f -print0 | xargs -0 shred -u
$ rmdir $TEMPHOME/* $TEMPHOME
$ pkill gpg-agent
That should free the master key.
Publish the new public key
Publish the new public key/subkeys, so others can use them.
$ for x in keyserver.ubuntu.com keys.gnupg.net pgp.mit.edu; do
gpg --keyserver $x --send-keys C170E20E; done
gpg: sending key ...C170E20E to hkp://keyserver.ubuntu.com
gpg: sending key ...C170E20E to hkp://hkps.pool.sks-keyservers.net
gpg: sending key ...C170E20E to hkp://pgp.mit.edu
Note that it may take a while for the keyservers to propagate this...
elsewhere# gpg --keyserver keyserver.ubuntu.com --recv-keys OTHER_ID C170E20E
gpg: key ...OTHER_ID: "XYZ" <xyz@example.com>" not changed
gpg: key ...C170E20E: "Walter Doekes (XIII) <walter@example.com>" 6 new signatures
gpg: Total number processed: 2
gpg: unchanged: 1
gpg: new signatures: 6
You may also need to import your public key anywhere else where you're signing/encrypting. Also everywhere where you're using GnuPG-Agent forwarding.
Update 2023-10-16
Updated the /tmp
usage example to /dev/shm
, where the chance is
higher that it will not persist to disk.