git, in its default operation, does not allow developers to repudiate their commits. This default allows bad actors an opportunity to install malicious code, then cover their tracks or deny responsibility.
This document demonstrates how trivial malicious operations are, and details how to use GPG signatures to overcome the default insecurity of git.
It's trivial to spoof authorship of git commits, because git does not authenticate the author settings of the local git config. This “feature” allows bad actors to install malicious code, cover their tracks, and lay blame on others:
$ whoami mallory $ git config --global user.email "alice.victim@php.net" $ git config --global user.name "Alice Victim" $ vi ... # malicious change $ git add . $ git commit -m 'Fix typo' $ git push origin master Counting objects: 3, done. ... To https://github.com/php/php-src.git 45cf77a9..529c33c master -> master
After this sequence, Alice appears as the author, while Mallory is nowhere to be seen in the git history:
$ whoami bob $ git pull origin master ... $ git log commit 529c33c51d94a9fc88d389fb88b60ff4f1d1bf63 Author: Alice Victim <alice.victim@php.net> Date: Tue Aug 25 16:23:20 EDT 2020 Fix typo
Remotely, in GitHub, the author information will likewise show that of Alice.
When someone finally notices the attack and traces its origins, Alice will receive the blame because Alice is unable to repudiate this commit. In fact, nowhere will anyone be able to see that Mallory created and pushed this commit in Alice's name: git simply does not store that proof.
Signing commits is a simple means to prove authorship of code changes. To use it, a simple one-time setup and periodic password entry is all that's needed. This section walks through the configuration and verification of commit signing. It's written in a choose-your-own-adventure style: for each step, follow the sub-heading that matches your environment and git use.
This is a living document: if you don't see instructions for your specific setup, or if these instructions don't work for you, please mail suggested changes to internals@php.net.
Before you begin, ensure that:
Modern versions of Git, versions 2.2 or higher, use GPG for signing commits and tags. So we'll need to install GPG.
Use the package manager available in your OS. See also the GPG binaries download page for direct downloads.
OS / Distribution | Command(s) |
---|---|
macOS with Homebrew | brew install gpg |
Ubuntu, Debian, Mint, Kali | sudo apt install gnupg |
CentOS, Fedora, RHEL | sudo yum install gnupg |
OS | Command(s) |
---|---|
Ubuntu 18.04LTS | sudo add-apt-repository ppa:git-core/ppa && sudo apt update && sudo apt install git |
These instructions assume you're using Zsh. If you're using a different shell, replace .zshrc
with your shell's startup file for interactivity (e.g., .bash_profile
in Linux, macOS accounts that haven't migrated to Zsh or in the Terminal.app). It's been reported that iTerm2 in macOS Big Sur needs .zshenv
.
$ mkdir "${HOME}"/.gnupg $ chmod 700 "${HOME}"/.gnupg $ >> "${HOME}"/.zshrc echo 'GPG_TTY="$(tty)" && export GPG_TTY || echo "Could not determine TTY: $?" >&2'
Start a new terminal, then verify gpg is setup correctly:
$ gpg --version | head -2 gpg (GnuPG) 2.2.21 libgcrypt 1.8.6 $ gpg-connect-agent /bye && echo 'GPG ok' || echo 'ERROR: GPG not running' GPG ok $ [ -r "${GPG_TTY}" ] && echo 'TTY ok' || echo 'ERROR: TTY not found' TTY ok
If you see an output indicating “ERROR”, double check the installation commands.
With GPG installed and operational, the next step is to create a key.
In the examples that follow, replace references to “Your Name” with your full name; replace “you@php.net” with your PHP.net email address.
You will be prompted to enter a strong passphrase. Remember to store that in a secure location, like a password manager.
$ gpg --batch --generate-key <(echo ' Key-Type: RSA Key-Length: 4096 Expire-Date: 0 Name-Real: Your Name Name-Email: you@php.net ')
Note that this creates an immortal key. See the section “Frequently Asked Questions” for a discussion of the pros and cons to this.
You need to know your new key's ID, which is a hexadecimal value unique to your system. Note that the key ID is not a fingerprint or an ASCII-armored value.
List your keys. Copy the value after “rsa4096/” and store it in a temporary environment variable for later convenience. In this example, the key ID is 02783663:
$ gpg -K --keyid-format SHORT sec rsa4096/02783663 2020-08-26 [SCEA] 79694216A0DECA5B53E94E96910A1F8402783663 uid [ultimate] Your Name <you@php.net> $ export GPG_KEYID=02783663
GPG produces an “ASCII-armored” version of keys for easy portability. You'll need that now.
$ gpg --armor --export "${GPG_KEYID}" | pbcopy
We need to tell git to use that key ID and to always sign commits and tags to PHP repositories.
The examples that follow assume you want to sign all commits and tags. If you have multiple git configuration files, and only want signing to apply to specific configurations, alter the commands to affect only those specific files. See also the “Advanced Setup” section.
$ git config --global --replace user.signingkey "${GPG_KEYID}" $ git config --global --replace commit.gpgsign true $ git config --global --replace tag.gpgsign true
Note you can replace --global
with --file /path/to/a/git/config
to effect only a specific git configuration file. See also the “Advanced Setup” section.
Open the GitHub SSH and GPG keys page, scroll down and click “New GPG key”. In the “Key” text box, paste the exported key from your clipboard. Then click “Add GPG key”. Answer any credential challenges GitHub presents.
Hooray, you made it to the last step. Now we'll verify commits and tags made to any configured PHP repository is signed by your key and visible in GitHub.
From a PHP repository working directory, confirm that the following commands have outputs that match the pattern of this example.
$ cd /path/to/php/php-src $ echo "${GPG_KEYID}" # from earlier, the ID of the key you setup 02783663 $ git config --get user.signingkey # should match what git sees 02783663 $ git config --get commit.gpgsign true $ git config --get tag.gpgsign true
Now, we'll make a signed, temporary tag:
$ export TEMP_TAG="$(whoami)-$(date +%s)-${RANDOM}-signature-test" $ git tag -m "Temporary tag for testing signing" "${TEMP_TAG}"
What happened?
git tag -s -m “Explicit sign” “${TEMP_TAG}”
. If that prompts you for your password, you're not using git version 2.23 or later.Now, we have a signed tag. We can verify that in the git log:
$ git show --show-signature "${TEMP_TAG}" tag myusername-1599166378-6517-signature-test Tagger: Your Name <you@php.net> Date: Tue Aug 25 16:42:17 EDT 2020 Temporary tag for testing signing -----BEGIN PGP SIGNATURE----- ABCD...=== -----END PGP SIGNATURE-----
We'll push this temporary tag to GitHub to check that GitHub recognizes the signature:
$ git push origin "${TEMP_TAG}" Enumerating objects: 1, done. Counting objects: 100% (1/1), done. Writing objects: 100% (1/1), 839 bytes | 839.00 KiB/s, done. Total 1 (delta 0), reused 0 (delta 0) To github.com:php/php-src.git * [new tag] myusername-1599166378-6517-signature-test -> myusername-1599166378-6517-signature-test
Now, navigate to the repository's tag page.
Near the top of the list you'll see the recently pushed tag and off to the right there should be green text of “Verified”. Click on the “Verified” text and a pop-up appears showing your information and key.
Congratulations, your key now affirmatively signs tags!
When you next commit code, double check GitHub that your commit appears as “Verified”.
If you have any trouble with these changes, then check the “Troubleshooting” section below.
Finally, clean up that temporary tag:
$ git push --delete origin "${TEMP_TAG}" $ git tag --delete "${TEMP_TAG}"
Several things can go wrong here.
Check that you have a recent git version:
$ git --version git version 2.24.3 (Apple Git-128)
If this doesn't say at least 2.23.0, then you need to upgrade git. If you can't upgrade git, you can manually force git to sign as follows:
git -S
git -s
Note the difference in capitalization. You can simplify having to type these options with a git alias. The recommended and best solution is to upgrade git to the latest version.
git does not verify its options for presence or spelling, so it's possible to forget to add, or add with a misspelling, the git options. Check that git is configured correctly:
$ cd /path/to/php/php-src $ git config --get user.signingkey 02783663 $ git config --get commit.gpgsign true $ git config --get tag.gpgsign true
Note that git options are case-insensitive. Refer to step 5 for details on setting up git.
If the GPG agent isn't running, or is running but isn't configured correctly, you'll see errors like:
error: gpg failed to sign the data fatal: failed to write commit object # or: gpg: signing failed: Inappropriate ioctl for device gpg: [stdin]: clear-sign failed: Inappropriate ioctl for device
Check that gpg diagnostic output matches the following pattern:
$ gpg-connect-agent /bye && echo 'GPG ok' || echo 'GPG not running' GPG ok $ echo "${GPG_TTY}" /dev/ttys002
Refer to step 1 for details on setting up gpg. Also, try a reboot and ensure gpg is configured to start.
Check that you have copied the key correctly to GitHub (step 6). In particular, watch out for spurious or trailing characters, including white space.
Ask on internals@php.net, and someone will help you. If possible, please provide output of the following commands:
cd /path/to/php/php-src gpg --version gpg-connect-agent /bye && echo 'GPG ok' || echo 'GPG not running' gpg -K --keyid-format SHORT date | gpg --clearsign git --version git config --list
Yes, probably. These IDE just pass down the git responsibility to the underlying tool chain, so if git is configured properly on the command line, it should work in your favorite IDE. However, you may need to configure GPG to use the native key chain instead of its default agent (pinentry
), so that the IDE can prompt for and remember the password.
Laptop stolen. Accidentally rm
the file. Whatever the case may be, the simplest way to revoke authorization is to open the GitHub SSH and GPG keys page, find the key, and remove it. Any future use of that key will not appear as valid in GitHub. Then follow this guide to add a new key.
Yes. If your private key falls into the wrong hands, they'll be unable to use the key without the passphrase.
This guide created an immortal key (see step 2), because for this use case key expiration does not add any security while it increases development friction.
As part of regular security hygiene, however, consider creating a new key and replacing the old one at GitHub annually.
This guide created a unique key in batch mode, to simplify working through the steps. However, more key options are available, which can be entered interactively:
$ gpg --full-generate-key
When doing this, make sure to select a key type of RSA
and a key length of 4096
, as GitHub sets that as a minimum requirement.
By default, when you enter your signing key's password using the text entry prompt that's configured in this guide, GPG agent remembers your password for 10 minutes. If you find yourself entering your password a lot, you can increase it through configuration:
$ cat "${HOME}"/.gnupg/gpg-agent.conf default-cache-ttl 28800 max-cache-ttl 28800
That will set the password caching to 8 hours (or 28,800 seconds).
You don't have to one signing key for all repositories. You can have one per repository. Or one per all repositories in a directory. Git offers unlimited configuration choices. Consider this example:
$ nl "${HOME}"/.gitconfig 1 [user] 2 name = Your Name 3 email = me@example.com 4 user = your_github_username 5 signingkey = 02783663 6 [push] 7 followTags = true 8 [commit] 9 gpgsign = true 10 [tag] 11 gpgSign = true 12 13 [includeIf "gitdir/i:~/code/php/"] 14 path = ~/.gitconfig.d/php $ nl "${HOME}"/.gitconfig.d/php 1 [user] 2 email = you@php.net 3 signingkey = 7DB08A14
The .gitconfig
file, lines 1 through 11 set a global signing key, while lines 13 and 14 use a different configuration for all repositories under the $HOME/code/php
directory. For those repos, it uses the signing key 7DB08A14
.
To always display signatures in commit logs, you can configure git to always display them by default
git config --global log.showSignature true
This guide was adapted, with permission, from internal developer documentation at LifeOmic.