Skip to main content

Commit signing

Git allows anyone to configure any name and email on their commits, which means bad actors can spoof commits and impersonate other contributors. Digitally signing commits proves they came from someone with access to a specific private key.

To protect against commit spoofing, all Bitwarden contributors are encouraged to digitally sign their commits.

Choosing a signing method

GitHub supports commit signing with SSH, GPG, and S/MIME. This guide covers SSH, which GitHub recommends as the simplest approach for individual users.

There are three ways to set up SSH commit signing, in order of recommendation:

  1. Hardware-backed SSH key: private key lives on a FIDO2 security key (e.g., YubiKey 5+). Each commit requires a PIN and a physical touch. This is the most secure option.
  2. Standard SSH key: private key lives on disk, protected by a passphrase. Simpler to set up, but the key material is extractable.
  3. Bitwarden SSH agent: import an SSH key into Bitwarden Desktop and use the built-in SSH agent. Keys are available while your vault is unlocked.

Pick one path below, then continue to Add the key to GitHub.

Hardware-backed SSH key

Hardware-backed keys using FIDO2 security keys provide the strongest protection. Private keys are generated on and never leave the hardware, and every signing operation requires physical interaction with the device.

Prerequisites

  • macOS (Apple Silicon) with Homebrew
  • A FIDO2-compatible security key (must support ed25519-sk; older U2F-only keys only support ecdsa-sk)

Install dependencies

The system OpenSSH (8.2+) technically supports ed25519-sk, but lacks the FIDO middleware needed to talk to hardware keys. Install Homebrew's OpenSSH, which has FIDO support compiled in, along with libfido2:

brew install openssh libfido2

Ensure Homebrew's binaries take precedence. Add to ~/.zshrc if not already present:

export PATH="/opt/homebrew/bin:$PATH"

Restart your shell (open a new terminal tab, or run source ~/.zshrc) and verify the correct binary is active:

which ssh-keygen # should print /opt/homebrew/bin/ssh-keygen

If it points to /usr/bin/, your PATH isn't set correctly. Fix that before continuing; the system binary will silently fail at later steps.

Set a FIDO2 PIN

If you haven't already, set a PIN on your security key:

brew install ykman
ykman fido access change-pin

Generate the key

Ensure your git email is configured, then plug in your security key and generate the key:

git config --global user.email "you@example.com"
ssh-keygen -t ed25519-sk -O verify-required -C "$(git config --global --get user.email)"

-O verify-required ensures every SSH operation requires both a PIN and a physical touch. You'll be prompted to touch the key during generation. The passphrase prompt is optional, since the hardware key itself is the primary security factor.

To create a resident key (stored on the hardware, portable across machines via ssh-keygen -K), add -O resident:

ssh-keygen -t ed25519-sk -O resident -O verify-required -C "$(git config --global --get user.email)"

Confirm the key was created:

ls -la ~/.ssh/id_ed25519_sk*

You should see both id_ed25519_sk (private key handle) and id_ed25519_sk.pub (public key).

Configure SSH

Edit ~/.ssh/config:

IgnoreUnknown UseKeychain,AddKeysToAgent

Host github.com
IdentityFile ~/.ssh/id_ed25519_sk
IdentityAgent none

IgnoreUnknown lets this config work with both the system SSH (which supports Apple-specific options like UseKeychain) and Homebrew's SSH (which doesn't). IdentityAgent none bypasses the macOS SSH agent, which can cache the FIDO key handle and then fail on subsequent auth attempts. Disabling it forces SSH to talk to the YubiKey directly every time, which is the correct behavior for verify-required keys.

Configure Git

git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519_sk.pub
git config --global commit.gpgSign true
git config --global tag.gpgSign true

Set up an allowed signers file for local signature verification. This snippet is idempotent, so it's safe to run more than once:

SIGNER_ENTRY="$(git config user.email) $(cat ~/.ssh/id_ed25519_sk.pub)"
touch ~/.ssh/allowed_signers
grep -qF "$SIGNER_ENTRY" ~/.ssh/allowed_signers || echo "$SIGNER_ENTRY" >> ~/.ssh/allowed_signers
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers

Fix the PIN prompt for commit signing

Git calls ssh-keygen -Y sign internally without connecting it to your terminal, so the PIN prompt gets swallowed and the commit appears to hang. The fix is a wrapper script that redirects stdin/stderr to /dev/tty:

cat > ~/.ssh/ssh-keygen-sign-wrapper <<'EOF'
#!/bin/bash
exec /opt/homebrew/bin/ssh-keygen "$@" < /dev/tty 2> /dev/tty
EOF
chmod +x ~/.ssh/ssh-keygen-sign-wrapper

git config --global gpg.ssh.program ~/.ssh/ssh-keygen-sign-wrapper

Now continue to Add the key to GitHub.

Fallback strategy

caution

Make sure you have a recovery plan in case you lose your hardware key:

  • Register multiple hardware keys with GitHub
  • Keep a traditional SSH key as a secure backup for emergencies
  • If using resident keys, back up the key handle with ssh-keygen -K on a second machine

Standard SSH key

A standard ed25519 SSH key stored on disk, protected by a passphrase.

  1. Ensure your git email is configured:

    git config --global user.email "you@example.com"
  2. Generate an SSH key for commit signing:

    ssh-keygen -t ed25519 -f ~/.ssh/bw-signing -C "$(git config --global --get user.email)"
    tip

    Protect the key with a strong passphrase. For stronger protection, consider a hardware-backed key instead.

  3. Configure Git to sign using SSH:

    git config --global gpg.format ssh
    git config --global user.signingkey ~/.ssh/bw-signing.pub
    git config --global commit.gpgSign true
    git config --global tag.gpgSign true
  4. (Optional) Store your passphrase in the macOS Keychain so you don't have to enter it on every commit. See Store your passphrase in the macOS Keychain.

Now continue to Add the key to GitHub.

Bitwarden SSH agent

Import your SSH key into Bitwarden Desktop, then follow the Bitwarden SSH agent guide to configure the desktop app for SSH authentication and Git commit signing. Your SSH keys will be available while your vault is unlocked.

After completing that guide, continue to Add the key to GitHub.

Add the key to GitHub

Copy your public key (adjust the path if you used a custom filename):

# Hardware-backed key
pbcopy < ~/.ssh/id_ed25519_sk.pub

# Standard key
pbcopy < ~/.ssh/bw-signing.pub

In GitHub → Settings → SSH and GPG keys → New SSH key, add the same public key twice: once as an Authentication Key and once as a Signing Key.

Test authentication

ssh -T git@github.com

You should see:

Hi <username>! You've successfully authenticated, but GitHub does not provide shell access.

For hardware keys, you'll be prompted for your FIDO2 PIN and a physical touch. If this doesn't work, stop here and troubleshoot before continuing.

Verify commit signing

git commit --allow-empty -m "test signed commit"
git log --show-signature -1

git log should show "Good signature." Push the commit to GitHub and confirm the Verified badge appears next to the commit.

Git client setup

VS Code

  1. Open Preferences → Settings
  2. Search for "commit signing"
  3. Enable Git: Enable Commit Signing

SourceTree

Refer to Atlassian's guide: Setup GPG to sign commits within SourceTree.

What to expect day-to-day

Hardware keys: Every git push and git commit will prompt for your FIDO2 PIN and a physical touch. This is by design; no one can push or sign commits with your identity without physical access to your security key and knowledge of the PIN. The prompts become second nature quickly, but expect each operation to take a couple of extra seconds.

Standard SSH keys: If you stored your passphrase in the macOS Keychain, commits and pushes will work without any prompts. Otherwise, you'll be prompted for your passphrase when the key is first used in a session.

Store your passphrase in the macOS Keychain

This section applies to standard SSH keys only (not hardware-backed keys, which require a touch on every operation by design).

Add your SSH key to the ssh-agent and store your passphrase in the Keychain:

ssh-add --apple-use-keychain ~/.ssh/bw-signing
ssh-add -l # verify the key was added

This needs to be re-run after every restart. To automate it, create a LaunchAgent:

  1. Create the LaunchAgents directory if it doesn't exist:

    mkdir -p ~/Library/LaunchAgents
  2. Create the plist file:

    USER_NAME=$(whoami)

    cat > ~/Library/LaunchAgents/com.ssh-add-bw-signing.plist <<PLIST
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
    <key>Label</key>
    <string>com.ssh-add-bw-signing</string>
    <key>ProgramArguments</key>
    <array>
    <string>/usr/bin/ssh-add</string>
    <string>--apple-use-keychain</string>
    <string>/Users/${USER_NAME}/.ssh/bw-signing</string>
    </array>
    <key>RunAtLoad</key>
    <true />
    </dict>
    </plist>
    PLIST
  3. Validate, enable, and test:

    plutil -lint ~/Library/LaunchAgents/com.ssh-add-bw-signing.plist

    launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.ssh-add-bw-signing.plist
    launchctl enable gui/$(id -u)/com.ssh-add-bw-signing

    # Kick-start it now (only works if the key isn't loaded yet)
    launchctl kickstart -k gui/$(id -u)/com.ssh-add-bw-signing
    ssh-add -l # verify the key was added
  4. Restart your computer and run ssh-add -l to confirm the key loads automatically.

Cleanup

Remove any old SSH keys from GitHub that are no longer in use. Compare the SHA256 fingerprints shown in GitHub's UI against your local keys:

for f in ~/.ssh/*.pub; do echo "=== $f ==="; ssh-keygen -l -f "$f"; done

Troubleshooting

Hardware key issues

Commit hangs with no PIN prompt. Git is using the system ssh-keygen instead of Homebrew's. Run which ssh-keygen; it should print /opt/homebrew/bin/ssh-keygen. If not, fix your PATH and re-source your shell. Also confirm the wrapper script at ~/.ssh/ssh-keygen-sign-wrapper exists and is executable.

ssh -T git@github.com returns "Permission denied." Check that ~/.ssh/config has the correct IdentityFile path. Run ssh -vT git@github.com to see which keys SSH is offering; the verbose output will show whether your FIDO key is being tried.

"Key enrollment failed" or no touch prompt during key generation. The YubiKey may not be recognized. Try unplugging and re-plugging it, then run ykman fido info to confirm the FIDO2 application is accessible. If ykman can't see it, try a different USB port or check for interfering processes (gpg-agent, yubikey-agent).

Authentication works but suddenly stops. The macOS SSH agent may have cached a stale FIDO key handle. Confirm IdentityAgent none is set in ~/.ssh/config for the github.com host. If the problem persists, clear the agent with ssh-add -D and retry.

YubiKey PIN locked out. After 8 consecutive wrong PIN attempts, the FIDO2 PIN locks. Recovery requires a full FIDO reset (ykman fido reset), which wipes all resident credentials on the key. You'll need to generate a new key pair and re-register it on GitHub.