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"