GPG key in TPM

Using GPG with a TPM protected private key

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.

This wasn't working for me on previous versions, but Ubuntu 26.04 (gpg 2.4.8) works after sudo apt install tpm2daemon && sudo usermod -a -G tss $USER

gpg> keytotpm
Really move the primary key? (y/N) y

sec  rsa2048/43C361483B0C7FC6
     created: 2026-06-16  expires: never       usage: SC  
     card-no: TPM-Protected
     trust: ultimate      validity: ultimate
ssb  rsa2048/DC68A8D7788AE201
     created: 2026-06-16  expires: never       usage: E   
[ultimate] (1). Dan Drown

gpg> key 1

sec  rsa2048/43C361483B0C7FC6
     created: 2026-06-16  expires: never       usage: SC  
     card-no: TPM-Protected
     trust: ultimate      validity: ultimate
ssb* rsa2048/DC68A8D7788AE201
     created: 2026-06-16  expires: never       usage: E   
[ultimate] (1). Dan Drown

gpg> keytotpm

sec  rsa2048/43C361483B0C7FC6
     created: 2026-06-16  expires: never       usage: SC  
     card-no: TPM-Protected
     trust: ultimate      validity: ultimate
ssb* rsa2048/DC68A8D7788AE201
     created: 2026-06-16  expires: never       usage: E   
     card-no: TPM-Protected
[ultimate] (1). Dan Drown

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"

Questions? Comments? Contact information