GPG Signing with Ledger Hardware Wallet
This document describes how to use your Ledger hardware wallet for GPG signing, including git commit signatures, message signing, and file encryption.
Table of Contents
Section titled “Table of Contents”- Overview
- How It Works
- Prerequisites
- Your GPG Key
- Git Commit Signing
- Manual GPG Operations
- GitHub/GitLab Integration
- Agent Management
- Troubleshooting
- Key Management
- Configuration Details
- Security Benefits
- Related Documentation
Overview
Section titled “Overview”The Ledger hardware wallet can be used for GPG operations:
- GPG key generation and storage - Keys never leave the device
- Git commit signing - Cryptographic proof of authorship
- Message signing - Sign any text or file
- Encryption/decryption - Secure message handling
- SSH authentication - See SSH Authentication
All operations require physical confirmation on your Ledger device, providing hardware-level security.
How It Works
Section titled “How It Works”Architecture
Section titled “Architecture”┌─────────────────────────────────────────────────────────────┐│ Commit Signing Flow │├─────────────────────────────────────────────────────────────┤│ ││ git commit ││ │ ││ ▼ ││ gpg-ledger wrapper script ││ │ ││ ├─> Check if ledger-gpg-agent running ││ │ If not: start agent ││ │ ││ ▼ ││ gpg --homedir ~/.gnupg-ledger ││ │ ││ ▼ ││ ledger-gpg-agent ││ │ ││ ▼ ││ Ledger device (sign with hardware key) ││ │ ││ ▼ ││ User confirms on device ││ │ ││ ▼ ││ Signed commit ││ │└─────────────────────────────────────────────────────────────┘Key Components
Section titled “Key Components”- Ledger device - Stores private key, performs signing operations
- ledger-gpg-agent - Bridges GPG and Ledger device
- gpg-ledger wrapper - Auto-manages agent lifecycle
- Git configuration - Uses wrapper for all GPG operations
Prerequisites
Section titled “Prerequisites”- Ledger Nano S device
- SSH/GPG Agent app installed on Ledger (via Ledger Live)
-
ledger-agentpackage (included in this config) - GPG key initialized on Ledger
If you haven’t completed setup, see Ledger Setup Guide.
Your GPG Key
Section titled “Your GPG Key”This configuration uses a GPG key stored on your Ledger:
- Key ID:
D2A7EC63E350CC488197CB2ED369B07E00FB233E - Identity: Luc Chartier luc@distorted.media
- Algorithm: ECDSA (NIST P-256 curve)
- Key Location: Ledger hardware wallet
- Keyring:
~/.gnupg-ledger/
Initialize GPG Identity (First Time)
Section titled “Initialize GPG Identity (First Time)”If you need to initialize your GPG key on the Ledger:
ledger-gpg init "Your Name <your.email@example.com>" --homedir ~/.gnupg-ledgerImportant:
- Use
--time=0flag to regenerate the same key deterministically - Keys are stored on Ledger and never exposed to computer
- Recoverable using your Ledger’s 24-word recovery seed
Export Public Key
Section titled “Export Public Key”# To filegpg --homedir ~/.gnupg-ledger --armor --export luc@distorted.media > public-key.asc
# To clipboard (macOS)gpg --homedir ~/.gnupg-ledger --armor --export luc@distorted.media | pbcopyGit Commit Signing
Section titled “Git Commit Signing”Configuration (Already Set Up)
Section titled “Configuration (Already Set Up)”Your configuration automatically signs all commits:
✅ GPG key configured: D2A7EC63E350CC488197CB2ED369B07E00FB233E
✅ Sign by default: true
✅ Wrapper script: Auto-manages ledger-gpg-agent
✅ Both gpg.program and gpg.openpgp.program configured
Configuration in nix/profiles/hardware-security.nix:
programs.git = { signing = { key = "D2A7EC63E350CC488197CB2ED369B07E00FB233E"; signByDefault = true; };
extraConfig = { gpg = { program = "...gpg-ledger wrapper..."; openpgp.program = "...gpg-ledger wrapper..."; }; };};Making Signed Commits
Section titled “Making Signed Commits”Commits are automatically signed:
git add .git commit -m "your commit message"Your Ledger will:
- Display “Sign message” on the screen
- Wait for you to press the button to confirm
- Complete the signature and return to git
Verifying Signatures
Section titled “Verifying Signatures”Check the signature on the last commit:
git log --show-signature -1Expected output:
gpg: Signature made Fri Oct 3 06:24:44 2025 PDTgpg: using ECDSA key D2A7EC63E350CC488197CB2ED369B07E00FB233Egpg: Good signature from "Luc Chartier <luc@distorted.media>"Viewing Signed Commits
Section titled “Viewing Signed Commits”Show commits with signature info:
git log --show-signatureShow compact signature status:
git log --pretty=format:"%h %G? %s"Legend:
G= Good signatureB= Bad signatureU= Good signature, unknown validityN= No signature
Manual Signing
Section titled “Manual Signing”To sign a specific commit without auto-signing:
# Disable auto-signing for this repogit config commit.gpgsign false
# Sign a specific commit manuallygit commit -S -m "manually signed commit"
# Or sign the last commitgit commit --amend -S --no-editManual GPG Operations
Section titled “Manual GPG Operations”Sign a Message
Section titled “Sign a Message”echo "test message" | gpg --homedir ~/.gnupg-ledger --clearsignOutput:
-----BEGIN PGP SIGNED MESSAGE-----Hash: SHA256
test message-----BEGIN PGP SIGNATURE-----...signature...-----END PGP SIGNATURE-----Sign a File
Section titled “Sign a File”gpg --homedir ~/.gnupg-ledger --sign file.txtCreates file.txt.gpg (binary signature).
Detached Signature
Section titled “Detached Signature”gpg --homedir ~/.gnupg-ledger --detach-sign file.txtCreates file.txt.sig (separate signature file).
Verify Signature
Section titled “Verify Signature”gpg --homedir ~/.gnupg-ledger --verify file.txt.sig file.txtEncrypt Message
Section titled “Encrypt Message”echo "secret message" | gpg --homedir ~/.gnupg-ledger --encrypt --recipient luc@distorted.mediaDecrypt Message
Section titled “Decrypt Message”gpg --homedir ~/.gnupg-ledger --decrypt encrypted.gpgGitHub/GitLab Integration
Section titled “GitHub/GitLab Integration”Adding Your Public Key to GitHub
Section titled “Adding Your Public Key to GitHub”-
Export your public key:
Terminal window gpg --homedir ~/.gnupg-ledger --armor --export luc@distorted.media | pbcopy -
Add to GitHub:
- Go to Settings → SSH and GPG keys
- Click “New GPG key”
- Paste your public key
- Click “Add GPG key”
-
Verify on GitHub:
- Your commits will show a “Verified” badge
- Click the badge to see signature details
Adding to GitLab
Section titled “Adding to GitLab”- Export public key (same as above)
- Go to Preferences → GPG Keys
- Click “Add new key”
- Paste and save
Signature Status
Section titled “Signature Status”- ✅ Verified: Signature valid, key trusted by platform
- ❓ Unverified: Signature valid but key not added to platform
- ❌ Invalid: Signature verification failed
Agent Management
Section titled “Agent Management”Starting the Agent
Section titled “Starting the Agent”The git wrapper automatically starts the agent, but you can start it manually:
ledger-gpg-agent --homedir ~/.gnupg-ledger --server --verbose &Note: Use --server mode (not --daemon). The --daemon mode has issues.
Checking Agent Status
Section titled “Checking Agent Status”# Check if agent is runningpgrep -f ledger-gpg-agent
# View agent logstail -f ~/.local/share/ledger-gpg-agent.logtail -f ~/.local/share/ledger-gpg-agent.error.logStopping the Agent
Section titled “Stopping the Agent”# Kill all GPG agentskillall ledger-gpg-agent gpg-agent
# Or kill specific processpkill -f "ledger-gpg-agent.*--homedir.*\.gnupg-ledger"Auto-Start on Login
Section titled “Auto-Start on Login”The configuration includes a launchd service (macOS) that automatically starts ledger-gpg-agent on login:
# From nix/profiles/hardware-security.nixlaunchd.agents.ledger-gpg-agent = { enable = true; config = { ProgramArguments = [ "...ledger-gpg-agent wrapper..." ]; RunAtLoad = true; };};Troubleshooting
Section titled “Troubleshooting””No secret key” Error
Section titled “”No secret key” Error”Problem: Git commit fails with gpg: skipped "KEYID": No secret key
Solution:
-
Check if agent is running:
Terminal window pgrep -f ledger-gpg-agent -
Start the agent manually:
Terminal window ledger-gpg-agent --homedir ~/.gnupg-ledger --server --verbose & -
Test signing:
Terminal window echo "test" | gpg --homedir ~/.gnupg-ledger --clearsign
“End of file” Error
Section titled ““End of file” Error”Problem: Agent starts but can’t communicate with Ledger
Solution:
-
Ensure Ledger is ready:
- Ledger unlocked (PIN entered)
- SSH/GPG Agent app is open
- Screen shows “SSH/GPG Agent is ready”
-
Kill and restart agents:
Terminal window killall ledger-gpg-agent gpg-agentledger-gpg-agent --homedir ~/.gnupg-ledger --server --verbose & -
Check logs:
Terminal window tail -f ~/.local/share/ledger-gpg-agent.error.log
Git Using Wrong GPG
Section titled “Git Using Wrong GPG”Problem: Git isn’t using the Ledger wrapper
Solution:
Check configuration:
# Should show wrapper script path, not direct gnupg pathgit config --get gpg.programgit config --get gpg.openpgp.programIf wrong, rebuild your configuration:
darwin-rebuild switch --flake .#wikigen-macLedger Not Responding
Section titled “Ledger Not Responding”Checklist:
- Ledger connected via USB
- Ledger unlocked (PIN entered)
- SSH/GPG Agent app is open on device
- Screen shows “SSH/GPG Agent is ready”
- Agent process is running (
pgrep -f ledger-gpg-agent)
Test:
echo "test" | gpg --homedir ~/.gnupg-ledger --clearsignAgent Not Daemonizing
Section titled “Agent Not Daemonizing”Problem: Agent doesn’t stay in background
Note: The --daemon mode for ledger-gpg-agent has issues. Always use --server mode with &:
ledger-gpg-agent --homedir ~/.gnupg-ledger --server --verbose &Key Management
Section titled “Key Management”List Keys
Section titled “List Keys”# List public keysgpg --homedir ~/.gnupg-ledger --list-keys
# List secret keys (will show Ledger keys)gpg --homedir ~/.gnupg-ledger --list-secret-keys
# Show key fingerprintgpg --homedir ~/.gnupg-ledger --list-keys --fingerprintExport Public Key
Section titled “Export Public Key”# ASCII armored formatgpg --homedir ~/.gnupg-ledger --armor --export luc@distorted.media
# To filegpg --homedir ~/.gnupg-ledger --armor --export luc@distorted.media > public-key.asc
# Specific key by IDgpg --homedir ~/.gnupg-ledger --armor --export D2A7EC63E350CC488197CB2ED369B07E00FB233ERegenerate Key
Section titled “Regenerate Key”If you need to regenerate the exact same key (using recovery seed):
ledger-gpg init "Luc Chartier <luc@distorted.media>" --time=0 --homedir ~/.gnupg-ledgerThe --time=0 flag ensures the timestamp is deterministic, producing the same key.
Key Locations
Section titled “Key Locations”- Public keys:
~/.gnupg-ledger/pubring.kbx - Private keys: Stored on Ledger device (never on computer)
- Trust database:
~/.gnupg-ledger/trustdb.gpg - Agent socket:
~/.gnupg-ledger/S.gpg-agent
Configuration Details
Section titled “Configuration Details”The GPG Wrapper Script
Section titled “The GPG Wrapper Script”Located in nix/profiles/hardware-security.nix, the wrapper:
- Checks if
ledger-gpg-agentis running - Starts the agent in
--servermode if not running - Waits 2 seconds for agent to initialize
- Calls GPG with
--homedir ~/.gnupg-ledgerflag - Passes all arguments through to GPG
pkgs.writeShellScript "gpg-ledger" '' # Ensure ledger-gpg-agent is running if ! pgrep -f "ledger-gpg-agent.*--homedir.*\.gnupg-ledger" > /dev/null; then PATH="${pkgs.gnupg}/bin:$PATH" ${pkgs.ledger-agent}/bin/ledger-gpg-agent \ --homedir $HOME/.gnupg-ledger --server --verbose & sleep 2 fi
# Use --homedir flag instead of GNUPGHOME exec ${pkgs.gnupg}/bin/gpg --homedir $HOME/.gnupg-ledger "$@"''Why Both gpg.program and gpg.openpgp.program?
Section titled “Why Both gpg.program and gpg.openpgp.program?”Git checks gpg.openpgp.program first when gpg.format=openpgp (Home Manager default). We override both to ensure our wrapper is always used.
Environment Variables
Section titled “Environment Variables”Set in nix/profiles/hardware-security.nix:
home.sessionVariables = { GPG_TTY = "$(tty)"; SSH_AUTH_SOCK = "$(gpgconf --list-dirs agent-ssh-socket)";};Also set system-wide (for SOPS) in nix/modules/secrets/sops.nix:
environment.variables = { GNUPGHOME = "/Users/wikigen/.gnupg-ledger";};Security Benefits
Section titled “Security Benefits”Hardware Protection
Section titled “Hardware Protection”- Private key never leaves device - Signing happens on Ledger
- Physical confirmation required - Button press for every operation
- Non-exportable - Keys cannot be copied or stolen from computer
- Tamper-proof - Signatures prove commit authenticity and integrity
Recovery
Section titled “Recovery”- Recoverable from seed - Keys regenerated from 24-word recovery phrase
- Deterministic generation - Same seed = same key (with
--time=0)
Operational Security
Section titled “Operational Security”- Separate keyring - Uses
~/.gnupg-ledgerto avoid conflicts - Audit trail - All signed commits visible in Git history
- Platform verification - GitHub/GitLab verify signatures
Threat Model
Section titled “Threat Model”What Ledger protects against:
- ✅ Key extraction from computer
- ✅ Malware stealing keys from disk
- ✅ Unauthorized signing/commits
- ✅ Key theft via remote attacks
What Ledger doesn’t protect against:
- ❌ Physical theft of device (PIN provides basic protection)
- ❌ Compromise if seed phrase is leaked
- ❌ Social engineering (tricking user to sign malicious commits)
- ❌ Side-channel attacks on device itself
Related Documentation
Section titled “Related Documentation”- Ledger Setup Guide - Complete hardware wallet setup
- Ledger Deep Dive - Comprehensive hardware security guide
- SSH Authentication - SSH with hardware wallet
- SOPS Guide - Secrets management with Ledger GPG
- Architecture Overview - Configuration structure
External References
Section titled “External References”- Git Commit Signing - Official Git documentation
- GitHub GPG Verification - GitHub docs
- GitLab GPG Signatures - GitLab docs
- trezor-agent GPG Documentation - Agent documentation
- Ledger SSH/GPG Agent App - Ledger app source
Quick Reference
Section titled “Quick Reference”Common Commands
Section titled “Common Commands”# Start agentledger-gpg-agent --homedir ~/.gnupg-ledger --server --verbose &
# Check agent statuspgrep -f ledger-gpg-agent
# Make signed commit (automatic)git commit -m "message"
# View signaturegit log --show-signature -1
# Export public keygpg --homedir ~/.gnupg-ledger --armor --export luc@distorted.media
# Test signingecho "test" | gpg --homedir ~/.gnupg-ledger --clearsign
# List keysgpg --homedir ~/.gnupg-ledger --list-keys
# Kill agentskillall ledger-gpg-agent gpg-agentHappy signing! 🔐