GPG key in TPM

I experimented with storing certificate authority, SSH, and GPG keys in a TPM. This post will be about GPG.

The first thing I tried was following the directions for gpg's built-in tpm support. That page has useful general information about TPMs as well.

But following the directions on Ubuntu 25.04 (gpg 2.4.4) results in this error:

gpg> keytotpm
Really move the primary key? (y/N) y
gpg: error from TPM: Not supported

Moving the master key does work on Fedora 41 (gpg 2.4.5), but I run into trouble when generating a subkey, signing it with the parent key in TPM fails:

gpg: signing failed: Card error
gpg: make_keysig_packeto failed: Card error
gpg: Key generation failed: Card error

Thankfully, that's not the only option for TPM+gpg. I followed Will Rouesnel's directions on generating a key in TPM and importing it. I had some slight changes, and I turned it into a shell script. Take note: this will overwrite your gpg-agent.conf as well as gnupg-pkcs11-scd.conf if you have them. You also will want to comment out the tpm2_ptool init if you already have it setup.

#!/bin/bash

set -xe

# Ubuntu/Debian: apt install libtpm2-pkcs11-1 libtpm2-pkcs11-tools gnupg-pkcs11-scd gnutls-bin libnss3-tools p11-kit
# Fedora: dnf install tpm2-pkcs11 tpm2-pkcs11-tools gnupg-pkcs11-scd gnutls-utils p11-kit

# Initialize a new store. The store retains some data, but will not contain key material.
tpm2_ptool init

# Prompt user for pin numbers - see below for explanation
read -r -s -p "Enter User PIN: " uspin
echo

read -r -s -p "Enter Mgmt PIN: " sopin
echo

# Add a new token to the store
tpm2_ptool addtoken --pid=1 --label="gpg" --userpin=$uspin --sopin=$sopin

# Add a new key to the token - this generates the private key
tpm2_ptool addkey --label="gpg" --key-label="gpg" --userpin=$uspin --algorithm=rsa2048

# Find the path to the tpm2-pkcs11 library
tpm_lib=$(p11-kit list-modules | awk '/^module: / { module=$2} /path: / && module=="tpm2_pkcs11" { print $2}')

token_uri="$(p11tool --list-token-urls | grep token=gpg)"
private_uri="$(p11tool --list-privkeys --login --only-urls --set-pin=${uspin} ${token_uri})"

# Check the private key is accessible - this should display success
p11tool --test-sign --login --set-pin=${uspin} "${private_uri}"

read -r -p "Enter Name (firstname lastname):" name
read -r -p "Enter Email (user@domain):" email

template_ini="$(mktemp template.XXXXXXX.ini)"
cat << EOF > "$template_ini"
cn = "${name}"
serial = $(date --utc +%Y%m%d%H%M%S)
expiration_days = 3650
email = "${email}"
signing_key
encryption_key
cert_signing_key
EOF

GNUTLS_PIN="${uspin}" certtool --generate-self-signed --template "$template_ini" \
    --load-privkey "${private_uri}" --outfile "${name}.crt"

tpm2_ptool addcert --label=gpg --key-label=gpg "${name}.crt"

cat << EOF > "$HOME/.gnupg/gnupg-pkcs11-scd.conf"
providers tpm
provider-tpm-library ${tpm_lib}
pin-cache 20
EOF

cat << EOF >> "$HOME/.gnupg/gpg-agent.conf"
scdaemon-program $(command -v gnupg-pkcs11-scd)
pinentry-program $(command -v pinentry-gnome3)
EOF

systemctl --user restart gpg-agent

gpg --card-status
echo use 14 - existing key from card
gpg --expert --full-generate-key

This again uses tpm2_ptool as well as the tpm2-pkcs11 library.

After all this is done, you can see the "card status" of the TPM, the main thing to see here is the serial number:

$ gpg --card-status
gpg: WARNING: server 'scdaemon' is older than us (0.10.0 < 2.4.4)
gpg: Note: Outdated servers may lack important security fixes.
gpg: Note: Use the command "gpgconf --kill all" to restart them.
Reader ...........: [none]
Application ID ...: D27600012401115031310AEF19C71111
Application type .: OpenPGP
Version ..........: 11.50
Manufacturer .....: ?
Serial number ....: 0AEF19C7
Name of cardholder: [not set]
Language prefs ...: [not set]
Salutation .......: 
URL of public key : [not set]
Login data .......: [not set]
Signature PIN ....: forced
Key attributes ...: rsa48 rsa48 rsa48
Max. PIN lengths .: 0 0 0
PIN retry counter : 0 0 0
Signature counter : 0
Signature key ....: [none]
Encryption key....: [none]
Authentication key: [none]
General key info..: [none]

That serial number shows up on your gpg key:

$ gpg --list-secret-keys
/home/ddrown/.gnupg/pubring.kbx
-----------------------------
sec>  rsa2048 2025-08-19 [SCEAR]
      1549DFC19B59BCD4118B0E28A93503590C9F877F
      Card serial no. = 3131 0AEF19C7
uid           [ultimate] Daniel Drown <gpg@example.com>

The main use I have for a protected gpg key is to use with the "standard unix password manager"