🔥Let's Do DevOps: Cryptographically Signing Your Commits on GitHub with PGP🚀
Aka, was it really you that pushed this commit?
This blog series focuses on presenting complex DevOps projects as simple and approachable via plain language and lots of pictures. You can do it!
Hey all!
“Git” is a nearly universal tool for managing code. It’s incredibly influential and pervasive. It’s also really insecure.
“GitHub” is in a similar position - it’s a nearly universal tool among software engineers, and (unsurprisingly), it relies on “git” a ton. Something that most users don’t recognize is the Author name you see on your PRs on GitHub is what’s configured in your local git install, and that name can be set arbitrarily to anything. Like, anything.
That’s hilarious and really unsafe.
> git config set user.name "Ronald McDonald"
> git touch stuff && git add . && git commit -m "I promise my name is ronald mcdonald" && ggpush
> git config set user.name "37337 H4x0r"
> git touch stuff2 && git add . && git commit -m "Totally a real person I promise" && ggpush
We’ve been playing around with improving our security posture on GitHub, and there are tons of tools and settings you can deploy that’ll improve things for your orgs.
One of those is called Commit Signature Verification, and it’s the process of using a cryptographic operation to “sign” a commit with your private key, which proves that you and only you were the person to construct the code. In theory, this should help prove that the commit wasn’t modified once you generated it, and that you generated it, separate and apart from the key used to push code to GitHub.
Commit verification links your git commit to a GitHub identity - that’s a huge security boon.
On most pages you’ll also see that pretty “Verified” badge on your PRs, and that means that the commit is signed cryptographically with a key linked to a GitHub Profile.
Alright, now that we’ve talked about why this matters, let’s talk about how. It’s surprisingly easy.
Lets Talk .gitconfig
Most of this walk-through is going to be on mac, and should be entirely applicable to any *nix system. If you’re on windows, most commands will work but the process will be slightly different. I’ll try to call out the differences on each step.
The .gitconfig is a file with that exact name (including the leading period) in your user directory. The leading period does two things:
Makes the file show up first alphabetically
Marks the file as “hidden” which means your UI might hide the file by default
Pro tip: You can show all files, including hidden ones, with `ls` by using the -a` flag, like this. The -l shows a list of files (one more line), and -h shows sizes in human readable length. It’s my go-to to look at files in a directory.
ls -lah
The .gitconfig file is read by git and stores defaults that are used if you don’t over-ride the default in your command. When you run commands like this:
> git config set user.email kymidd@gmail.com
The value is stored in your .gitconfig file.
> head -n4 ~/.gitconfig
# This is Git's per-user configuration file.
[user]
name = Kyler Middleton
email = my@email.com
This file is used for all sorts of git stuff, including your name and email, as well as specifying if commits should be signed by default (or just when requested).
Create a GPG Key for Signing
Our first big step here is to create a GPG key to sign our commits with. GPG stands for GnuPG, which in turn stands for Gnu Privacy Guard (*nix programmers love recursive names). It’s a complete implementation of the OpenPGP encryption/hashing/signature RFC (complete RFC here).
What that means in plain english is that it’s a tool which can create a public and private key, which can be used for signing. Here’s an easy chart for whether you should share the files:
Private key: Don’t share. It’s private
Public key: Okay to share. It’s fine if it’s public
If you’ve never done this before, you won’t have one. Let’s make one.
First let’s check your git username and email address, just to check.
> git config --get user.name
Kyler Middleton
> git config --get user.email
my@email.com
Sweet, now it’s time to call the “gpg” command to build us a gpg key:
> gpg --full-generate-key
This wizard will walk us through the options we need to set. Note how I’m not pickin ga selection for the kind of key, or for the elliptical curve I want - I use the defaults by just hitting enter.
I don’t want my local key to expire, so I set “0” for key expiration. Please set the number of days/weeks/months/years you’d like to feel secure.
> gpg --full-generate-key
gpg (GnuPG) 2.4.5; Copyright (C) 2024 g10 Code GmbH
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
Please select what kind of key you want:
(1) RSA and RSA
(2) DSA and Elgamal
(3) DSA (sign only)
(4) RSA (sign only)
(9) ECC (sign and encrypt) *default*
(10) ECC (sign only)
(14) Existing key from card
Your selection?
Please select which elliptic curve you want:
(1) Curve 25519 *default*
(4) NIST P-384
(6) Brainpool P-256
Your selection?
Please specify how long the key should be valid.
0 = key does not expire
<n> = key expires in n days
<n>w = key expires in n weeks
<n>m = key expires in n months
<n>y = key expires in n years
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y
Next up we need to enter our real name and our real email address. The email address and username will need to match what we have git configured to use - it’ll check our local secure gpg store to find a matching key to use for signing.
At the bottom, I hit “O” (The Oh character, as in Octopus) to say that what it shows is “Okay”.
GnuPG needs to construct a user ID to identify your key.
Real name: Kyler Middleton
Email address: my@email.com
Comment:
You selected this USER-ID:
"Kyler Middleton <my@email.com>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O
It’ll then prompt you to enter a passphrase. This passphrase is a great idea - it means that if anyone exports your private key, it will be useless without this phrase, making it much harder to steal.
If you enter a blank passphrase, it will indicate that the key shouldn’t be encrypted further. This is a bad idea. GPG will validate that’s what you meant to do and confirm with you a couple times that’s intended.
Now that GPG has all the info it requires to generate our key, it asks you do some stuff on your computer to improve your entropy (randomness). Then the private and public key are created.
We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.
gpg: revocation certificate stored as '/Users/kyler/.gnupg/openpgp-revocs.d/F2FACE164BF83A84BDB92901F0079EFD5D950CFE.rev'
public and secret key created and signed.
pub ed25519 2024-08-10 [SC]
F2FACE164BF83A84BDB92901F0079EFD5D950CFE
uid Kyler Middleton <my@email.com>
sub cv25519 2024-08-10 [E]
Validate the Key and Upload to Your GitHub User
With an SSH key, like you’ve probably used to create commits and push them to github, those are real files that live on your computer. You can copy and read them with `cat`. This is a little different.
The GPG tool doesn’t create a file on your computer that could be easily stolen, it stores the values in the gpg store on your computer. Let’s take a look at the keys in there.
There’s a few values here, and it can be a little overwhelming. The most important parts to verify and note are:
The username and email address listed here must match exactly with what our .gitconfig shows as our username and email address. If they don’t, the signing process will say a key can’t be found.
There are two lines on the `pub` section here. We want the second one, here: 1B1861C2EEA5C72331C084A7F25591826E390C70. That’s our public key fingerprint, and will be used in a minute.
> gpg --list-keys --keyid-format long
[keyboxd]
---------
pub ed25519/F25591826E390C70 2024-05-01 [SC]
1B1861C2EEA5C72331C084A7F25591826E390C70
uid [ultimate] Kyler Middleton <my@email.com>
sub cv25519/8755D7B6D5950F6E 2024-05-01 [E]
Now we need to export our public gpg key so we can add it to github - that way when we sign commits, GitHub can validate that the signing key matches a github profile to link to.
We use the `gpg —armor —export xxx` command to print out our public gpg key. Copy down this whole block.
Pro tip: Don’t remove any blank lines, and you do need to include the “--- BEGIN…” lines - they’re part of what you should copy.
> gpg --armor --export 1B1861C2EEA5C72331C084A7F25591826E390C70
-----BEGIN PGP PUBLIC KEY BLOCK-----
mDMEZreLgxYJKwYBBAHaRw8BAQdA7wfprLNFtazVi9YYLEA95Jtv9goYwOcjYo/r
(removed a few lines)
AAoJEPAHnv1dlQz++4QBAJ9McZqBw+IIVOQ+EM62jfdkr0IK81QKKkg1TIzIWKKq
AQDzX31/JB9qkY8YgGA1hvM2q7kJzxf1CpXQr+keE4uNAg==
=NWdD
-----END PGP PUBLIC KEY BLOCK-----
Head over to the SSH and GPG Keys section of your GitHub User Profile. You can find it here:
Scroll down a bit to find the “GPG keys” section. Over on the right, click on “New GPG key” green button to start the process of adding a new GPG key to your github user.
Enter a great name for your GPG key - I’d recommend dates, email addresses, real names, stuff like that. But for fun in this demo we’re using “awesome-new-gpg-key”.
Enter the public key exactly as it appears in your terminal. You are required to retain all the blank lines and the beginning and end flags. if you don’t, GitHub will reject it.
Hit Add GPG key and watch out for any error messages. If it all works it’ll take you back to the SSH and GPG Keys page.
Validate that you see your snazzy new GPG key on this page. It’ll list the email address you’ve linked to this key, as well as the key ID/fingerprint.
Let’s Test It!
All that, and we’re not yet signing commits. However, that’s a one-line change, so we’re nearly there. You got this!
So far, we’ve validated our git configuration (username, email), and created a new GPG key with the same username and email. You can test out commit signing with the -s flag (here’s the man page entry), like this.
If you see the “file(s) changed” message like below, it worked.
> touch stuff5
> git add stuff5
> git commit -s -m "stuff5"
[feature/Arbitrary-commiter-git c9a90fe] stuff5
1 file changed, 0 insertions(+), 0 deletions(-)
create mode 100644 stuff5
Here’s what you’ll see if your git information doesn’t match the local library. You’ll want to investigate it by comparing your git info to the names and emails listed in the “gpg --list-keys --keyid-format long” command. Either fix your git config or generate a new GPG key that matches your correct git info.
> git config set user.email not-my@email.com # Note: Doesn't match gpg keys
> git commit -m "stuff5"
error: gpg failed to sign the data:
gpg: skipped "Kyler Middleton <not-my@email.com>": No secret key
[GNUPG:] INV_SGNR 9 Kyler Middleton2 <not-my@email.com>
[GNUPG:] FAILURE sign 17
gpg: signing failed: No secret key
fatal: failed to write commit object
So I Have to Add -s Forever?
There’s actually a super easy way to make git automatically sign all your commits now that you have a GPG key all ready to go.
We can tell git to always use our GPG key to sign all commit messages, forever, with this single command:
# Enable commit signing globally
git config --global commit.gpgsign true
This will add this little stanza in your .gitconfig file that for every commit, to automatically gpg sign.
> cat .gitconfig | tail -n7 | head -n2
[commit]
gpgsign = true
Show it on GitHub
Now that our commits are signed, on GitHub we no longer see the “name” we set in git (which again, could be anything, and could be false), we instead see the GitHub Username that the commit signature is linked to.
Summary
In this write-up we went over a significant security problem with git, which GitHub inherits by supporting git so deeply, and how we can mitigate it with Verified commits signed by a PGP key.
We generated a PGP key, made sure it linked to our git config, and setup our git so it automatically cryptographically signs all our commits from here on out.
I’d love to further research whether we can trick GitHub - can we push with an SSH key linked to a user within a GitHub Org, but with a commit signed by a GPG key for a user that isn’t part of the Org (and could have an arbitrary name)?
I haven’t researched that yet, but I’d love to one day.
Thanks all! Good luck out there.
kyler