vcs:commit-signing

This is an old revision of the document!


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:

  • You have installed 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 GPG for signing commits and tags.

MacOS with Homebrew

$ brew install gpg
$ mkdir "${HOME}"/.gnupg
$ chmod 700 "${HOME}"/.gnupg
$ echo 'GPG_TTY="$(tty)" && export GPG_TTY || echo "Could not determine TTY: $?" >&2' >> "${HOME}"/.zshrc

Note that these instructions assume you're using Zsh, the default shell in Catalina (10.15) and higher. If you're using an earlier version of macOS, a different terminal emulator (such as iTerm2), or if you have changed your default shell, replace .zshrc with your shell's startup file for interactivity (e.g., .bash_profile for Terminal.app). It's been reported that iTerm2 in Big Sur needs .zshenv.

Note that macOS may require you to grant additional folder access to the Terminal app. Go to System Preferences > Security & Privacy > Privacy > Full Disk Access and click Terminal.

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 need to choose a strong passphrase and store that in a secure location.

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
Passphrase: enter-a-strong-password-here-and-save-it-in-a-password-manager
')

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?

  • 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'll push this up to GitHub to ensure we can see that it's a verified tag:

$ 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

Let's go check that it's verified on GitHub. 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:

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

Advanced Setup

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.

Thanks

This guide was adapted, with permission, from internal developer documentation at LifeOmic.

vcs/commit-signing.1617251319.txt.gz · Last modified: 2021/04/01 04:28 by bishop