Table of Contents

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

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.

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 internals@php.net.

Pre-requisites

Before you begin, ensure that:

Step 1 of 7: Install GPG

Modern versions of Git, versions 2.2 or higher, use GPG for 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 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 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 <you@php.net>

$ 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 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?

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}"

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:

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 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

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 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 LifeOmic.