====== Signing Commits ======
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.
===== How to frame another developer =====
[[https://medium.com/@pjbgf/spoofing-git-commits-7bef357d72f0|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
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 [[https://en.wikipedia.org/wiki/Non-repudiation|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.
===== Securing git with signed commits =====
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 .//
==== Pre-requisites ====
Before you begin, ensure that:
* You have installed [[https://stackoverflow.com/a/50647394/2908724|git, version 2.23.0]] or later. (If you can't install this version, read on; there are workarounds.)
* You are able to pull and push to at least one PHP repository.
==== Step 1 of 7: Install GPG ====
Modern versions of Git, versions 2.2 or higher, use [[https://gnupg.org|GPG]] for [[https://git-scm.com/book/en/v2/Git-Tools-Signing-Your-Work|signing commits and tags]]. So we'll need to install GPG.
=== Install the GPG software ===
Use the package manager available in your OS. See also the [[https://gnupg.org/download/index.html|GPG binaries download page]] for direct downloads.
== Recent versions of OS and distributions ==
^ 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'' |
== Older releases ==
^ OS ^ Command(s) ^
| Ubuntu 18.04LTS | ''sudo add-apt-repository ppa:git-core/ppa && sudo apt update && sudo apt install git'' |
=== Update start-up files ===
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 [[https://scriptingosx.com/2017/04/about-bash_profile-and-bashrc-on-macos/|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'
=== Verify installation ===
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.
==== Step 2 of 7: Generate a new, unique GPG signing key ====
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.
=== From a macOS/Linux/POSIX command line ===
$ 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.
==== Step 3 of 7: Get the key ID ====
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.
=== From a macOS/Linux/POSIX command line ===
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
$ export GPG_KEYID=02783663
==== Step 4 of 7: Copy the key in ASCII-friendly format to your clipboard ====
GPG produces an "ASCII-armored" version of keys for easy portability. You'll need that now.
=== From a macOS command line ===
$ gpg --armor --export "${GPG_KEYID}" | pbcopy
==== Step 5 of 7: Configure git to use that key ID ====
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.
=== From a macOS/Linux/POSIX command line ===
$ 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.
==== Step 6 of 7: Configure GitHub to recognize the signature ====
Open the [[https://github.com/settings/keys|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.
==== Step 7 of 7: Verification ====
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 macOS/Linux/POSIX command line ===
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?
* If this is working correctly, you'll be prompted for your password.
* If you're not prompted for your password, try ''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.
* If that doesn't prompt you for your password, your GPG agent is not running. See the "Troubleshooting" section.
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
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 [[https://github.com/php/php-src/tags|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}"
===== Troubleshooting =====
==== I was not prompted to enter my GPG key password? ====
Several things can go wrong here.
=== Outdated version of git ===
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:
* On commits, with ''git -S''
* On tags, with ''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.
=== Missing or incorrectly spelled options to sign all commits ===
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.
=== GPG not running ===
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.
==== I don't see the "Verified" label at GitHub? ====
Check that you have copied the key correctly to GitHub (step 6). In particular, watch out for spurious or trailing characters, including white space.
==== I still can't get it to work! ====
Ask on , 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
===== Frequently Asked Questions =====
==== Can I sign commits using VSCode, Emacs, PHPStorm, vim, or ${other_favorite_editor}? ====
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.
==== What if I lose my key? ====
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.
==== Do I need a passphrase? ====
Yes. If your private key falls into the wrong hands, they'll be unable to use the key without the passphrase.
==== Should my key expire? ====
This guide created an immortal key (see step 2), because //for this use case// [[https://security.stackexchange.com/a/79386/72365|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.
===== Advanced Setup =====
==== Configure more key options ====
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.
==== Extend the lifetime of cached passwords ====
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).
==== Different signing keys for different persona ====
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''.
==== Always show signatures in logs ====
To always display signatures in commit logs, you can configure git to always display them by default
git config --global log.showSignature true
===== Thanks =====
This guide was adapted, with permission, from internal developer documentation at [[https://github.com/lifeomic|LifeOmic]].