pgp on yubikey / refresh expiry

pgp on yubikey / refresh expiry

  • Written by
    Walter Doekes
  • Published on

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 --importas 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.


Back to overview Newer post: offsite / on-the-fly encrypted backups / gocryptfs Older post: tls / testing certificate chains / easycert