GitHub recently announced support for validating Git commit signatures signed by SSH keys. OpenSSH 8.0 added support for this back in 2019 and Git has supported the functionality since version 2.34 in 2021, so it is great to see GitHub adding support as well. PGP is bad, so I am glad to see continued alternatives come out for traditional GPG-based operations.
Assuming GitHub is what you/your company uses for a code repository, now that you don’t have to use GPG for it to “work” you should absolutely be signing your commits. We’ll look at why using an SSH key for GitHub commit signatures is all you need and improves the developer experience over using GPG. Then we’ll go over the handful of commands to run to hook everything up. We’ll end with looking at exciting future tools in this space from the Sigstore project.
Why You Should Sign Commits With SSH
How do you know who pushed a commit? Well, you take a look at the metadata of the commit and identify the name and email address as provided by the user.name
and user.email
git config values.
But you can set those values to anything!
How do you prove you are the name
and email
associated with that commit?
Well, you sign the commit with something that is publicly associated with your name and email.
In the past, this has been a GPG key published to some key registry, so others can look up your email and find your associated public key.
Now, with GitHub, you can do the same thing with an SSH key.
You already tell GitHub what SSH key you plan to use to push to repositories you own (SSH Authentication Key).
So GitHub knows who you are, but from merely displaying commit metadata no one else working in that repository knows, for sure, that you are the one who pushed that commit.
You have an email address associated with your GitHub account, so GitHub could theoretically match the user.email
from your commit to the email address in your account, and match the SSH authentication key used to the one stored in your account, and transitively infer that, yes, you are validated as the owner of the SSH key -> owner the that email address -> valid user.email
for the commit.
But, this is a brittle, transitive relationship and Git already provides a way to do this directly, through git commit signatures.
So GitHub provides a UI around this native Git functionality.
By uploading an SSH Signing Key to GitHub, GitHub becomes the key registry and can validate signatures you attach to your uploaded commits. They can therefore assert via the UI that yes, you are indeed the person associated with that commit.
As an individual developer working on your own project, you don’t need to sign your commits. You, hopefully, already know you are who you say you are. But for organizations, or open source communities, it is important to know who is pushing commits to a repository. A secure software supply chain is all the rage these days, and for good reason. In Verizon’s 2022 Data Breach Investigations Report (DBIR), they note a substantial rise in supply chains as the source of breaches and incidents this past year. (Although, in the 2023 report with more data they do reverse the claim and say supply chain incidents are pretty much non-existent compared to the more typical methods of company breaches.) Attestation of code commits is one element of a secure software supply chain.
Now that the pain and friction of using GPG for signatures is no longer necessary, I highly recommend all organizations leverage git commit signatures - via SSH, or OIDC (provided by sigstore) - to validate who is pushing commits to their internal and external repositories.
And you can finally toggle this setting in your branch protection rules without adding a ton of friction!
How To Sign GitHub Commits With SSH
The following instructions assume you have already run git config --global user.email <email>
and don’t enter an email for every commit, because why would you want to type it in every time.
If you do not have this value configured, then replace the subshell email=
command below appropriately for your configuration.
First, configure Git locally to sign commits with an SSH key. The “quickstart” commands are below, and I’ll explain what we’re doing afterward.
git config --global gpg.format ssh
# This assumes you have an SSH public key at ~/.ssh/id_ed25519.pub
publickey=$(cat ~/.ssh/id_ed25519.pub)
git config --global user.signingkey "key::$publickey"
git config --global commit.gpgsign true
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
email=$(git config --global --list | grep "user.email" | awk '{split($0, a,"="); print a[2]}')
echo "$email $publickey" >> ~/.ssh/allowed_signers
Commit something, and verify you have hooked everything up correctly locally by running the following command:
git log --show-signature
If you don’t see the Good "git" signature
line, walk back through the above commands and ensure everything is properly configured.
You can also review the explanation for each command below.
On the GitHub side, go to https://github.com/settings/ssh/new and add your SSH key, even if you have already been using this key for authentication to GitHub. Any key used for authenticating to GitHub and pushing commits is an “Authentication Key.” You have to tell GitHub you intend to use the key as a “Signing Key” as well.
Publish your commits as normal, and SSH signatures will automatically be added locally and validated on GitHub!
If you are running on OSX, I recommend storing your SSH keys in the Secure Enclave, instead of in local files on your system. That also enables you to require Touch ID before any process is allowed to use your SSH key.
If this is interesting to you, scroll down to Leveraging Secretive for SSH Commit Signing.
What did I just do?
Let’s walk through the commands we ran above.
git config --global gpg.format ssh
Here we are telling git in our global config to sign commits using SSH keys.
# This assumes you have an SSH public key at ~/.ssh/id_ed25519.pub
publickey=$(cat ~/.ssh/id_ed25519.pub)
git config --global user.signingkey "key::$publickey"
Here we tell git which SSH key to use for signing commits. Your local system will match this public key to your private key and sign commits with your private key.
git config --global commit.gpgsign true
Here we tell git to sign all commits by default, without having to pass the -S
flag each time.
If we stopped here, we could sign commits, but we’d get the following error when reviewing local commit signatures:
Note the error: gpg.ssh.allowedSignersFile needs to be configured and exist for ssh signature verification
error.
That’s a useful error pointing to the next git command to run.
You have to tell your local git what SSH keys are available and allowed to sign commits, similar to updating the setting on GitHub telling that system what SSH keys are allowed to sign commits.
We do that by first telling git where to look for our list of SSH keys allowed to sign commits:
git config --global gpg.ssh.allowedSignersFile ~/.ssh/allowed_signers
And then we populate that file with our SSH public key.
email=$(git config --global --list | grep "user.email" | awk '{split($0, a,"="); print a[2]}')
echo "$email $publickey" >> ~/.ssh/allowed_signers
The format of this file is, per line:
<email address that identifies the ssh key owner> <ssh public key>
If you only run gpg.ssh.allowedSignersFile
and don’t include your matching SSH public key into that file, you will see an error that git is unable to find any matching principal to the SSH key when you are trying to sign with it:
Note that whatever email you enter in your allowed_signers
file is how your local git will identify signatures.
However, this is all configuration local to your machine.
You’ve configured in allowed_signers
that this signature equals this email, so git displays that email in git log --show-signature
.
Uploaded commits to GitHub will be bound to your GitHub user identity, and the verification will always display your GitHub user account.
In this way, a malicious user is unable to tamper with the identity behind a commit, as the signature will either be unverified or point to a different GitHub user.
As commit signature verification becomes more broadly used, I expect to see more typo squatting-type of attacks, where a malicious user will create a GitHub user account with a similar name to a trusted user, and then sign commits with their own SSH key, tricking the PR UI to display the commit as coming from a verified user that looks like they are the trusted member inside the organization.
OIDC-based signature verification could mitigate this risk, and there is current tooling in early stages of development to enable this. I look forward to a point where GitHub supports this signature format in their UI. If you are not on OSX, skip over the next section and learn more about OIDC-based signature verification at the conclusion of this blog post.
Leveraging Secretive for SSH Commit Signing
Secretive is a macOS app that allows you to store SSH keys in the Secure Enclave. It’s pretty neat, and I recommend it for OSX folks. Configuring git to sign keys with Secretive requires a slightly modified setup.
First, ensure that SSH_AUTH_SOCK
is exported in your ZSH/BASH profile.
export SSH_AUTH_SOCK=/Users/<USER>/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
Then open the Secretive app, copy the Public Key Path for the SSH key you want to use for signing, and copy it into the user.signingkey
git command:
git config --global user.signingkey /Users/<USER>/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/PublicKeys/<YOURKEY>.pub
Note that while Secretive allows you to set it up via your global SSH config as well, signing with Git requires the SSH_AUTH_SOCK
env var to be set.
You can set both the SSH_AUTH_SOCK
in your terminal profile of choice and the IdentityAgent
in your SSH config, they are not mutually exclusive.
Host *
IdentityAgent /Users/<USER>/Library/Containers/com.maxgoedjen.Secretive.SecretAgent/Data/socket.ssh
You still need to use the public key itself for your ~/.ssh/allowed_signers
file.
Get it from the Public Key section in Secretive.
email=$(git config --global --list | grep "user.email" | awk '{split($0, a,"="); print a[2]}')
echo "$email <ssh public key>" >> ~/.ssh/allowed_signers
Looking To The Future: Commit Signing with OIDC and Sigstore
Sigstore is an awesome project aiming to provide a suite of easy-to-use tools for signing and verifying software artifacts. Gitsign is one such tool and implements keyless git commit signatures using OIDC. In essence, you can sign your commits without needing to handle any keys by using your GitHub account as a signing authority. Verification is backed by your GitHub identity, similar to uploading an SSH Signing Key, but you don’t need to worry about keys anymore. You trigger an OAuth flow to GitHub when you commit instead.
The gitsign project is still early in its development, and iterative 0.x releases continue to add necessary user experience features, such as a credential cache in 0.2.0 that allows you to reuse an OIDC authentication session for multiple commits without needing to reauth, e.g. when moving through a rebase.
The latest current version, 0.3.0, adds support for configuring gitsign via ~/.gitconfig
parameters.
Once gitsign hits a 1.0 release (and GitHub’s UI supports validating them!), I fully intend to migrate my workflows to using it instead of SSH signing keys. For now, SSH signing keys provide the best experience by enforcing non-repudiation with low friction for companies and OSS organizations of all sizes.