
Guide on how to set up git commit signing with multiple YubiKeys, including hardware attestation to prove keys were generated on-device with specific touch policy.
Published on November 30, 2025 by Kevin Quill
post security-engineering yubikey pgp supply-chain-security git code signing
15 min READ
This guide demonstrates how to set up git commit signing using multiple YubiKeys. It covers the complete workflow from initial setup through attestation, addressing gaps in existing documentation around multiple signing subkeys and attestation for code signing.
By the end of this guide, you’ll have:
Coming at this from the perspective of publishing open-source tooling and having been a user of open source tooling for critical production use cases. I wanted to give users with high-risk threat models the best option for validating my signing practices and code integrity.
Multiple YubiKeys
Having multiple YubiKeys in different form factors, I couldn’t guarantee I would always have the same key at hand. Therefore I wanted a seamless way to sign code with different keys without impacting the ability for users of my code to verify it. The developer experience for me is also a primary requirement alongside security and whatever the setup looks like it shouldn’t make the process any more inconvenient for me than using a single YubiKey.
Before starting, ensure you have the following tools installed:
Below is an example install of required tooling, but isn’t a comprehensive guide across platforms as that is well covered in information online.
Required Tools:
Installation (macOS):
# Install via Homebrew
brew install gnupg ykman pinentry-mac
# Verify installation
gpg --version
ykman --version
# Setup Pinentry
echo "pinentry-program $(brew --prefix)/bin/pinentry-mac" > ~/.gnupg/gpg-agent.conf
Other Platforms: See GnuPG Downloads and YubiKey Manager Installation
This guide follows a structured workflow:
First time setups tasks based on this Yubico Guide and a blog which I found particularly comprehensive. Before following these steps, see the mentioned guides on installing tooling.
ykman openpgp access set-retries 9 9 9Compatibility issue:
The Yubico Labs guide mentions using RSA4096 however that isn’t supported on firmware < 5.7.0
General list of commands but see the linked guides for more detail if unfamiliar with PGP and Yubikey manager
# Set Fido pin
ykman fido access change-pin
# password protect the yubikey config. Remember to save this code or your yubikey may become useless
ykman config set-lock-code --generate
# Pins can be set with with gpg or ykman apps
# Pin defaults:
gpg --card-edit
admin
passwd
# see output for instructions
ykman openpgp access -h
# Set lockout counts
ykman openpgp access set-retries 9 9 9
# Set default key attributes (cipher and length/algorithm)
gpg --card-edit #starts interactive prompt
admin
key-attr
#... selection
quit
Pin Recommendations:
Pin can be 8 to 127 chars, passphrase is best practice. Due to low retry limits complexity doesn’t need to be as high as online passwords
After completing the initial setup, verify your Yubikey is properly configured:
# Check Yubikey is detected and OpenPGP app is accessible
gpg --card-status
# Expected output should show:
# - PIN retry counters (should show 9 9 9 if you set them)
# - Key attributes showing your chosen algorithm (e.g., ed25519)
What to look for:
If gpg --card-status fails or shows unexpected values, review the setup steps before proceeding to key generation.
In OpenPGP, keys have specific capabilities: [C]ertify (primary key), [S]ign, [E]ncrypt, and [A]uthenticate. For commit signing, you’ll create a primary key with certify capability and one or more signing subkeys with the sign capability.
The Yubico Guide can be followed for this step.
Primary Yubikey With PGP the primary certifying key is higher risk as it can certify other subkeys, therefore while it’s call the primary key, it shouldn’t be on your “primary yubikey” that you intend to use daily. Keep it on a backup YubiKey.
Commands overview:
gpg --card-edit
admin
generate
# follow prompts and see notes below
#exit gpg interactive prompt
quit
# View new key
gpg --list-secret-keys --keyid-format=long
Notes:
Once your primary key is configured, you can generate subkeys on additional Yubikeys for daily use and to provide redundancy.
Process for each additional Yubikey:
Complete Part 1 (Initial Setup) on the new Yubikey - set PINs, lock code, and key attributes
Insert the new Yubikey and generate a new signing subkey linked to your primary key:
# (Should already be there unless on new device)Import your public key
gpg --import /path/to/your/public-key.asc
# Edit the key to add a new signing subkey
gpg --edit-key YOUR_KEY_ID
# At the gpg> prompt:
addcardkey
# Select (1) Signature key
# Follow prompts to generate the subkey on the new Yubikey
save
Export and backup your updated public key:
gpg --armor --export YOUR_KEY_ID > updated-public-key.asc
Upload the updated public key to GitHub or your key server
Set the touch policy on the new subkey (see Part 3)
Important: Each Yubikey will have its own unique signing subkey, but all are certified by the same primary key. This means users only need to import your public key once to verify signatures from any of your Yubikeys.
Following generation of the keys, you need to set the touch policy for the key so as it needs to be touched to complete a signing request.
ykman openpgp keys set-touch sig cached-fixed
Touch Policy Choice I believe the cached-fixed policy offers most pragmatic choice as it limits the exposure window but doesn’t prevent you from performing actions like rebases which otherwise would require constant tapping of the key.
Verifying Touch Policy:
# Check current touch policy settings
ykman openpgp info
# Look for "Touch policy" under signature key section
# Should show: sig cached-fixed
You can also test this works by attempting a test signature - you should need to touch the key to complete the operation.
Git will select the newest signing key in the public key, it does not have any logic to detect which Yubikey is available. For multiple Yubikeys, you have two approaches:
Manually specify which subkey to use by appending ! to the key ID:
# Get your subkey IDs
gpg --list-keys --keyid-format=long
# Configure git to use a specific subkey (use the subkey ID, not the primary key)
git config --global user.signingKey C5CA48564B2395E5!
You’ll need to run this command each time you switch Yubikeys.
Use this bash function to automatically detect and configure the correct subkey:
yk-git-key() {
echo "YubiKeys detected:"
local ykman_output
ykman_output=$(ykman list 2>&1)
echo "$ykman_output"
if [[ -z "$ykman_output" ]]; then
echo "---"
echo "ERROR: No YubiKey detected" >&2
return 1
fi
echo "---"
# Get Application ID
local AppID
AppID=$(gpg --card-status 2>/dev/null | grep "Application ID" | awk '{print $4}' | tr -d '\n\r')
if [[ -z "$AppID" ]]; then
echo "ERROR: No YubiKey detected or OpenPGP app not configured" >&2
echo "Try: gpg --card-status" >&2
return 1
fi
echo "Device AppID: $AppID"
# Validate AppID format
if [[ ! $AppID =~ ^D[A-F0-9]{31}$ ]]; then
echo "ERROR: Invalid Application ID format: $AppID" >&2
return 1
fi
# Find matching signing key
local KeyID
KeyID=$(gpg --list-secret-keys --with-colons | grep "$AppID" | awk -F':' '{print $5}' | tr -d '\n\r')
if [[ -z "$KeyID" ]]; then
echo "ERROR: No signing subkey found for this YubiKey" >&2
echo "Ensure public key is imported: gpg --import public-key.asc" >&2
return 1
fi
echo "Signing Key ID: $KeyID"
# Validate KeyID format
if [[ ! $KeyID =~ ^[A-F0-9]{16}$ ]]; then
echo "ERROR: Invalid Key ID format: $KeyID" >&2
return 1
fi
# Configure git
if ! git config --global user.signingKey "$KeyID!"; then
echo "ERROR: Failed to configure git signing key" >&2
return 1
fi
echo "SUCCESS: Git configured to sign with key $KeyID"
}
Usage: Add this function to your .bashrc or .zshrc, then run yk-git-key whenever you switch Yubikeys.
Note: This function will need updating if you have encryption or authentication keys on the Yubikey
Before relying on your Yubikey for commit signing, verify the complete workflow.
Create a test signed commit:
# Enable commit signing globally (if not already set)
git config --global commit.gpgSign true
# Create a test repository or use existing one
cd /path/to/test-repo
# Make a test commit
git commit --allow-empty -S -m "Test commit signing with Yubikey"
# You should be prompted to touch your Yubikey
Verify the signature:
# Show the signature in git log
git log --show-signature -1
# Expected output should show:
# - "Good signature from <your email>"
# - Your key ID
# - Signature details
Test key switching (if you have multiple Yubikeys):
yk-git-key function to switch to a different YubikeyTroubleshooting:
ykman openpgp infogpg --card-status shows your Yubikeygit config --global user.signingKey matches your current keyTo verify GPG signatures you need to import the commiter’s public key.
Github example:
#Just show public key details
curl https://github.com/kvql.gpg | gpg --show-keys
curl https://github.com/kvql.gpg | gpg --import
# Show signatures in git log
git log --show-signature
# Verify a specific commit
git verify-commit <commit id>
Note: This github export limits the overhead for users/teammates to import your keys even if you have multiple independent primary signing keys.
This section is optional and intended for high-security environments or those wanting to provide cryptographic proof that keys were generated on a hardware device.
Attestation allows you to prove that your signing key was generated directly on a Yubikey and is subject to its touch policy. This provides additional assurance for users with strict threat models. For most use cases, the standard commit signing configured above is sufficient.
As mentioned in this Palantir blog, attestation can be used to verify touch policy enforcement, adding an extra layer of validation beyond standard GPG signature verification.
Yubico documentation on the attestation contents and meaning: https://developers.yubico.com/PGP/Attestation.html
Reading the attestation data:
ykSerial=$(ykman info | awk '/Serial number:/ {print $3}')
# generate and export attestation certificate
# attest command has a question prompt if a cert already exists and
# therefore you can't always pipe directly from that.
ykman openpgp keys attest sig -
ykman openpgp certificates export sig - > sig-attest-$ykSerial.pem
# parse the attestation data
openssl asn1parse -in sig-attest.pem -i
# first 4 char are der encoding
# specific value use above command to get string num e.g
# 251:d=5 hl=2 l= 10 prim: OBJECT :1.3.6.1.4.1.41482.5.8
# 263:d=5 hl=2 l= 3 prim: OCTET STRING [HEX DUMP]:040103
openssl asn1parse -in sig-attest.pem -i -strparse 263
Note: Verifying the attestation signature against yubico public key and the attestation values is a key next step and will be covered in a later post
This guide covers git commit signing from a slightly new perspective with multiple Yubikey and on device generated secret keys for future attestation.
Next steps is to cover the threat model on what values does commit signing bring from the developer, open-source consumer and enterprise perspective. A key area which is often incorrectly represented online is how not all siging implementations are equal and the more basic implemenations mitigate minimal risks vs the platform authentication.
Implementing a reusable workflow for attesting the signing key during PRs is also key to getting the value from the attestation certificate consistently and at scale.