Setting Up Git Commit Signing with Multiple YubiKeys


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

Setting Up Git Commit Signing with Multiple YubiKeys

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.

What You’ll Build

By the end of this guide, you’ll have:

  • Multiple Yubikeys configured for git commit signing with a primary key and device-specific subkeys
  • A bash function to simplify switching between different Yubikeys
  • Touch policy enforcement requiring physical key tap for each signing operation
  • Properly configured git to sign commits with the currently inserted key
  • Optional: Cryptographic attestation proving keys were generated on hardware and enforce touch policy

Motivation

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.

Technology Used

  • macOS Tahoe
  • Yubikey 5 (multiple form factors)`
  • Github

Prerequisites

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:

  • GnuPG (gpg)
  • pinentry-mac
  • YubiKey Manager (ykman) - Latest version recommended
  • One or more Yubikey devices

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

Process Overview

This guide follows a structured workflow:

  1. Yubikey Initial Setup - Configure security settings, PINs, and key attributes on your first Yubikey
  2. Key Generation - Create the primary key and signing subkeys
    • Generate primary key
    • Set up additional Yubikeys with their own signing subkeys
  3. Touch Policy Configuration - Require physical touch for signing operations
  4. Git Integration - Configure git to use your Yubikey for commit signing
  5. Testing Your Setup - Verify everything works correctly
  6. Verifying Signatures - Learn how others can verify your signed commits
  7. [Optional] Attestation - Generate cryptographic proof of hardware key generation

Part 1: Yubikey Initial Setup

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.

  1. Set Fido pin (unrelated to signing but useful)
  2. Set lock-code to prevent the yubikey being wiped
  3. Change pgp pin and admin pin (no need to set reset code, this is for when use doesn’t know admin pin e.g. set by IT)
  4. Set key attributes (ecc and curve 25519)
  5. Set retry limits: ykman openpgp access set-retries 9 9 9

Compatibility 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

Verifying Your Setup

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:

  • Reader status shows your Yubikey serial number
  • PIN retry counter shows your configured values (9 9 9)
  • Key attributes match what you set (ECC, Curve 25519)

If gpg --card-status fails or shows unexpected values, review the setup steps before proceeding to key generation.

Part 2: Key Generation

Primary Key

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:

  • [User ID Properties] Think about privacy when it comes to real name and email fields, the public key is public when uploaded to Github (https://github.com/username.gpg). e.g utilise Github no-reply email
  • [Private key export] The private key for the master key can be exported (subkeys cannot be exported), for purely signing use cases, I don’t believe it is worth the security overhead of securing backup key. Worst case you will need to regenerate the keys and publish a new key but that is no worse than most current best practice that recommends utilising ssh based signing.
  • [Public key] The public key isn’t stored on the yubikey with the primary secret key, therefore you should export and backup the public key after any key changes.
    • Importing the public key to gpg agent is required to perform any key modifications or use the keys for signing (e.g on a new laptop)

Setting Up Secondary Yubikeys

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:

  1. Complete Part 1 (Initial Setup) on the new Yubikey - set PINs, lock code, and key attributes

  2. 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
    
  3. Export and backup your updated public key:

    gpg --armor --export YOUR_KEY_ID > updated-public-key.asc
    
  4. Upload the updated public key to GitHub or your key server

  5. 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.

Part 3: Setting the Touch Policy

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.

Part 4: Git Integration

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:

Option 1: Manual Configuration

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

Part 5: Testing Your Setup

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):

  1. Use the yk-git-key function to switch to a different Yubikey
  2. Make another test commit
  3. Verify the signature shows the different subkey ID

Troubleshooting:

  • If you don’t get a touch prompt, verify your touch policy with ykman openpgp info
  • If git can’t find the key, ensure gpg --card-status shows your Yubikey
  • If signature fails, check git config --global user.signingKey matches your current key

Part 6: Verifying Signatures

To 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.

Part 7: Yubikey Attestation for GPG key

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

Summary

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.