Signing Git Commits

I always knew you could sign Git commits but never had a need. It's easier than you think.

Signing Git Commits

I was recently helping Sergio (from NI) set up GitHub workflows for the LabVIEW Icon Editor. It was interesting and fun work and I definitely learned something. I thought I would share it with you all.

As part of our testing, I created a fork and made a simple commit and submitted a merge request. My merge request got rejected because my commit wasn't signed. I knew you could sign commits. I remembered reading about it in one of the many Git books out there, but hadn't really given it much thought. I had never had a need.

I'll describe what signing is first and how it works for those who don't know. If you want to skip right to the instructions click here.

What is signing?

Signing a commit is a cryptographic operation. Disclaimer: I am not a cryptologist and I did not study exactly how this is implemented in Git. I do have a pretty good understanding of the basics of how this works in general. We'll take a very brief tour here of a small part of cryptography because I find it interesting and perhaps not all of you are knowledgeable about it. If I got something wrong, please correct me. Keep in mind, the intended audience are not security professionals, so it is intentionally somewhat simplified.

Hashing

Generally, when you cryptographically sign something like a document or an executable, you take the file and hash it using a cryptographic hash. A hash function takes in a file, digests it, and spits out a number/value of a fixed size. It's a one-way process. It's repeatable. And most importantly for these purposes with a cryptographic hash function, if you change 1 bit in the input there's a very high probability that half the outputs change in an unpredictable manner. So any small change in the output produces a wildly different hashed value.

Key Pairs

You also need a private/public key pair. With a private/public key pair, anything encrypted by one key can only be decrypted by the other, which will become important shortly. Also, another very important point is that knowing one key, you cannot derive the other. This allows you to make one public and still keep the other one private. You take the hashed value and encrypt it with your private key and the result is your signature. When you send the file to someone else, you can also send along your signature, so they can validate it. They'll also need access to your public key as we'll see in a minute.

Why sign?

The point of this whole process is that the receiver can then validate your signature. How do they do that? First they take the file and hash it using the same function you did. If no bits have changed, they should get the same result. Next they take your public key and decrypt your signature, the result they get should match the hashed value. If the hashed value derived from your signature using your public key and the hashed value of the file match, then the signature is valid.

What does valid mean?

The question to ask ourselves is what does it mean that a signature is valid or invalid? What are the implications?

If a signature is valid, we can say that "Whoever signed this file was in possession of the corresponding private key (couldn't have been signed with any other key) and the file has not changed since they signed it (or the hash would have changed)." If it is invalid, we can conclude that either "It was signed by a different key (than the one corresponding to the public key we have) OR the file was tampered with or corrupted (resulting in a different hash).

Having signed commits helps you to link a specific change to a specific person - because presumably they are the only ones who have access to their private key. It also provides a level of non-repudiation in that if someone makes a signed commit that includes some malware, if the signature is valid, it's kind of hard for them to deny it. The only person who could have signed that commit is someone with access to their private key. It's not 100% perfect because private keys can be stolen (and collisions can be a thing for nation-state actors), but it's a pretty good attestation and certainly higher than simply saying the git commit message says it was sam@sasworkshops.com. That's just a simple entry in your git config file. It's easy to change to whatever you want and impersonate someone - signed commits prevent that.

Why would NI care?

In this case I do think it is very appropriate for NI to care. The Icon Editor ships with LabVIEW. LabVIEW is installed in a lot of sensitive places and often controls dangerous equipment. Nation State Actors and various hackers would love to get malware on some of those systems. If something were to sneak into the Icon Editor, NI (and the feds) would very much be interested in figuring out where it came from. Signed commits would definitely help with that. If you don't think this type of attack is something to worry about, check out this article: https://english.ncsc.nl/latest/weblog/weblog/2024/the-xz-factor-social-vulnerabilities-in-open-source-projects

How to do it?

If you followed the above discussion, you are probably thinking: "Sam, that all sounds great. I understood the basic idea. The steps all make sense, but how do I do all this?" The good news is Git and GitHub will do most of this for you, if you set it up correctly.

The nice thing about signing things in Git is WE don't have to perform any hashing. Git already does plenty of that for us. If you look at the way Git stores data, it's all hashed. You may have noticed that Git revisions aren't numbered sequentially and you may have heard someone mention the term commit hash. The commit hash is a hashed value that represents the state of your working directory and its entire history. Git calculates that automatically for us. We can just sign that commit hash.

Generating Keys

Before we can sign anything, we need a set of keys. You may already be in luck. If you already use SSH to clone repositories from GitHub/GitLab or if you already have ssh keys for sshing into servers, you can just reuse that keypair and skip to the next section.

If you don't have keys generated or want to use a different key for signing, then you need to use ssh-keygen as follows.

ssh-keygen -t ed25519

Uploading Keys

Next you need to upload your public key to GitHub or GitLab. This is so GitHub can validate your signature. You can use the same key for authentication and signing. Authentication just means you can use the key to push or pull using the ssh protocol. If you use GitLab and have already uploaded your SSH key, you can probably skip this step since by default GitLab let's you use your SSH keys for both signing and authentication. If you use GitHub, you'll have to upload your key again as a signing key.

First you need to get your public key (don't forget the .pub extension - the other file is your private key!)

cd ~/.ssh
cat id_ed25519.pub

Copy whatever that spits out. Log into GitHub or GitLab and go to your profile page. Find the page for uploading keys. Click the appropriate button to add the key, paste in what you copied from the terminal. On GitLab there is just one SSH Keys page under User Settings. You have the option of using it for Signing, Authentication or both. The default is both. On GitHub you need to go to the Settings and then SSH and GPG Keys. On GitHub when you upload the key you have to pick either signing or authentication. If you want to use the same key for both, you just upload the same key twice, selecting authentication one time and signing the next.

Setting up Git

Now you need to tell your local git client to sign your commits. I'm going to show you how to do it from the CLI. Any of the major GUI clients should pick this up.

# tell git to use ssh for signing instead of GPG
git config --global gpg.format ssh 
# tell git which ssh key to use for signing
git config --global user.signingkey ~/.ssh/id_ed25519
# tell git to always sign commits
git config --global commit.gpgsign true
# tell git to always sign tags
git config --global tag.gpgsign true
# autosigning is optional you can always add a -S to git commit to sign just a single commit. There really is no downside to signing things though.

Once you have all this set up, you should make a commit and if you push it to GitLab or GitHub you should see an indication. In either you should get a green Verified Icon next to the commit. If you don't make sure that you are using the same e-mail for your git commit as your GitHub/GitLab account. If you need to change it, here is how.

git config --global user.email "your@email.com"

Should I Sign My Commits?

If you run a team, org, or open source project maybe forcing all your developers to sign their commits might be a bit much - it depends on your threat model. As an individual, why not? I don't really see a downside to signing commits. As you can see it's not terribly hard to do. It takes all of 5 minutes to set up. Turn on auto-signing and you won't have to think about it again.

Need Help With Secure Development Practices?

If you need help with setting up secure development practices, let's talk!