Blog-like notes

Using an offline GnuPG master key

OpenPGP subkeys have many benefits (well summarized on this Debian wiki page), one of them being that if you have both an encryption subkey and a signing subkey (at least), you don’t need your master private key for your daily usage of OpenPGP—you will only need it for signing someone else’s key or to modify your own key (adding or revoking User IDs, adding or revoking subkeys, updating the preferences, etc.).

Consequently, your private master key can be put offline most of the time, and brought back to your computer only on those few occasions where it is required.

There are already many pages on the web describing how to do that. However, most of them, if not all of them, predate the recent release of GnuPG 2.1, so I thought an update on that topic would be worthwhile.

How to remove the master private key from your keyring

Step 1: Back up the original keyring

Let’s assume you have the following private keyring:

$ gpg2 --list-secret-keys
/home/alice/.gnupg/pubring.kbx
------------------------------
sec   rsa4096/CB2F38F25B491A54 2014-12-31 [SC] [expires: 2017-12-30]
uid               [ultimate] Alice <alice@example.org>
ssb   rsa2048/04BB7F8FDEC5E5D9 2014-12-31 [S] [expires: 2015-12-31]
ssb   rsa2048/BBB6B86627C2D43A 2014-12-31 [E] [expires: 2015-12-31]

Export the private master key and its private subkeys:

$ gpg2 --armor --output alice-private-keys.asc --export-secret-key alice@example.org

Store the exported private keys in a secure place (not on your computer). You may also print a hardcopy with a tool like Paperkey:

$ gpg2 --export-secret-key alice@example.org | paperkey | lpr

Step 2: Remove the private master key from the keyring

The “classic” method for doing that is to export your private subkeys, then delete all your private keys (the master private key and the private subkeys), and import back the private subkeys (because GnuPG prior to version 2.1 had no option to delete the master key only).

With GnuPG 2.1 however, there is a more direct method. First, find the keygrip of your master key:

$ gpg2 --list-secret-keys --with-keygrip
/home/alice/.gnupg/pubring.kbx
------------------------------
sec   rsa4096/CB2F38F25B491A54 2014-12-31 [SC] [expires: 2017-12-30]
      Keygrip = D4DF0C35D3E22FA6AC37DA2E54FB03F73616A3CB
uid               [ultimate] Alice <alice@example.org>
ssb   rsa2048/04BB7F8FDEC5E5D9 2014-12-31 [S] [expires: 2015-12-31]
      Keygrip = 21B2EDF018D7CAF0B45644FDB753DD42307C4425
ssb   rsa2048/BBB6B86627C2D43A 2014-12-31 [E] [expires: 2015-12-31]
      Keygrip = 2E149DA9C5E46E0DECC6A17EFD8B5FB1DF1E1BAB

Then send a DELETE_KEY command to the GnuPG Agent:

$ gpg-connect-agent "DELETE_KEY D4DF0C35D3E22FA6AC37DA2E54FB03F73616A3CB" /bye

Confirm that you want to delete the key when the agent ask you so.

For reference, the “classic” method (which still works with GnuPG 2.1) is as follows:

First, export the subkeys:

$ gpg2 --armor --output private-subkeys.asc --export-secret-subkeys alice@example.org

Then delete the private master key and its subkeys:

$ gpg2 --delete-secret-key alice@example.org

sec  rsa4096/CB2F38F25B491A54 2014-12-31 Alice <alice@example.org>

Delete this key from the keyring? (y/N) y
This is a secret key! - really delete? (y/N) y

Then import back the subkeys you have just exported:

$ gpg2 --import private-subkeys.asc
gpg: key CB2F38F25B491A54: "Alice <alice@example.org>" not changed
gpg: key CB2F38F25B491A54: secret key imported
gpg: Total number processed: 4
gpg:              unchanged: 1
gpg:       secret keys read: 4
gpg:   secret keys imported: 2

Step 3: Ensure that the private master key has been removed

List the contents of your private keyring again:

$ gpg2 --list-secret-keys
/home/alice/.gnupg/pubring.kbx
------------------------------
sec#  rsa4096/CB2F38F25B491A54 2014-12-31 [SC] [expires: 2017-12-30]
uid               [ultimate] Alice <alice@example.org>
ssb   rsa2048/04BB7F8FDEC5E5D9 2014-12-31 [S] [expires: 2015-12-31]
ssb   rsa2048/BBB6B86627C2D43A 2014-12-31 [E] [expires: 2015-12-31]
ssb   rsa2048/7D2233B8833E70AF 2014-12-31 [A] [expires: 2015-12-31]

No matter which method above you have used, you should see the # symbol after the sec keyword, confirming that the private master key is not usable.

If you want to be really sure, try to do something that requires the private master key, such as adding a new dummy User ID to your keyring:

$ gpg2 --edit-key alice@example.org
Secret key is available.

pub  rsa4096/CB2F38F25B491A54
     created: 2014-12-31  expires: 2017-12-30  usage: SC  
     trust: ultimate      validity: ultimate
sub  rsa2048/04BB7F8FDEC5E5D9
     created: 2014-12-31  expires: 2015-12-31  usage: S   
sub  rsa2048/BBB6B86627C2D43A
     created: 2014-12-31  expires: 2015-12-31  usage: E   
sub  rsa2048/7D2233B8833E70AF
     created: 2014-12-31  expires: 2015-12-31  usage: A   
[ultimate] (1). Alice <alice@example.org>

gpg> adduid
Real name: Dummy
Email address: dummy@example.org
Comment: 
You selected this USER-ID:
    "Dummy <dummy@example.org>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
gpg: signing failed: No secret key
gpg: signing failed: No secret key

gpg> quit

Note the signing failed: No secret key messages.

How to use the offline master key

There are several ways to make GnuPG temporarily use the offline master key. The method I am currently using goes along the following lines.

Let’s assume you have stored the alice-private-keys.asc file (generated in step 1 above) on a USB stick, and that, when the stick is mounted, the file is available at a location such as /run/media/alice/mystick/alice-private-keys.asc.

Create a temporary directory to use as a temporary GnuPG home:

$ mkdir ~/gpgtmp
$ chmod 0700 ~/gpgtmp

Import your complete private keyring on that directory from your backup file:

$ gpg2 --homedir ~/gpgtmp --import /run/media/alice/mystick/alice-private-keys.asc
gpg: keybox ‘gpgtmp/pubring.kbx’ created
gpg: gpgtmp/trustdb.gpg: trustdb created
gpg: key 5B491A54: public key "Alice <alice@example.org>" imported
gpg: key 5B491A54: secret key imported
gpg: Total number processed: 4
gpg:               imported: 1
gpg:       secret keys read: 4
gpg:   secret keys imported: 3

You may now unmount your USB stick and put it back in its safe place.

Now edit the key you want to sign. Use the temporary directory as GnuPG home, but add the public keyring from your normal GnuPG home directory:

$ gpg2 --homedir ~/gpgtmp --keyring ~/.gnupg/pubring.kbx --edit-key bob@example.com

If you were using GnuPG 2.0 prior to GnuPG 2.1, your public keyring is probably still in the “legacy” format and will be called pubring.gpg instead of pubring.kbx.

When you have finished, terminate the GnuPG Agent which was started in the temporary directory, and wipe out that directory:

$ gpg-connect-agent --homedir ~/gpgtmp KILLAGENT /bye
$ rm -rf ~/gpgtmp

A drawback of this method is that since you are not using your normal GnuPG’s home directory, any configuration file in that directory will be ignored. If that is a concern, you may copy or symlink your $GNUPGHOME/gpg.conf file into the temporary directory before invoking GnuPG.

I strongly suggest using a small script to automatize the above commands—such as this one​1:

#!/bin/bash

# The DOS label of your USB stick
LABEL=mystick

# The pathname to the file containing your private keys
# on that stick
KEYFILE=alice-private-keys.asc

# Identify the device file corresponding to your USB stick
device=$(/sbin/findfs $LABEL)

if [ -n "$device" ]; then
    # Mount the stick
    udisksctl mount --block-device $device

    # Create temporary GnuPG home directory
    tmpdir=$(mktemp -d -p $XDG_RUNTIME_DIR gpg.XXXXXX)

    # Import the private keys
    gpg2 --homedir $tmpdir --import /run/media/$USER/$LABEL/$KEYFILE

    # Unmount the stick
    udisksctl unmount --block-device $device

    # Launch GnuPG from the temporary directory,
    # with the default public keyring
    # and with any arguments given to us on the command line
    gpg2 --homedir $tmpdir --keyring ${GNUPGHOME:-$HOME/.gnupg}/pubring.kbx $@

    # Cleaning up
    [ -f $tmpdir/S.gpg-agent ] && gpg-connect-agent --homedir $tmpdir KILLAGENT /bye
    rm -rf $tmpdir
fi

Some further thoughts

Splitting the master key in parts

If you don’t like the idea to keep your offline master key on a single USB stick,2 you may want to use a N-of-M secret sharing scheme to split the backup file in M parts, in such a way that N parts at least are needed to reconstruct the file.

For example, using libgfshare’s implementation of Shamir’s Secret Sharing Scheme:

$ gfsplit -n 2 -m 3 alice-private-keys.asc

This will produce three files of the form alice-private-keys.asc.NNN, any two of which will be needed to reconstruct the original file. You may then store each of them on a different support.

Another method: Manipulating GnuPG Agent’s key files directly

Currently (with GnuPG 2.1), GnuPG Agent stores each private key in a separate file $GNUPGHOME/private-keys-v1.d/NNN…NNN.key, where NNN…NNN is the 20-bytes long keygrip of the key.

This allows a different and much simpler method to put the master private key offline, and bringing it back only when needed.

Back up the private key and delete it from the private keyring in a single step simply by moving the corresponding file from the $GNUPGHOME/private-keys-v1.d directory (once you know its keygrip):

$ mv ~/.gnupg/private-keys-v1.d/D4DF0C35D3E22FA6AC37DA2E54FB03F73616A3CB.key \
  /run/media/alice/mystick/

To use the master key when needed, copy or symlink it from your storage device back into the private keys directory, perform the operation requiring the master key, then delete the file or symlink. No more need to use a temporary GnuPG home directory.

A few remarks regarding this method:

  1. My own script is actually a bit more complicated, but it boils down to the same logic.
  2. Keep in mind, however, that your exported private key is not unprotected: with GnuPG’s default settings, the key is encrypted with a 128-bit AES key, which is itself derived by n iterations of SHA1 on your passphrase—with n automatically defined so that the entire computation takes about 100ms.