Many organisations are able to generate short-lived certificates. Git 2.34.0 released in November 2021 added support for signing commits with SSH Keys. Let’s see how to use these to introduce commit signing without GPG and with little additional overhead.

What are SSH Certificates

In this article, I will use “certificates” or “SSH Certificates” to refer to SSH User Certificates. These are certificates used to authenticate a user to servers.

If you need an introduction to SSH Certificates and how awesome they are, I invite you to read Mike Malone’s If you’re not using SSH certificates you’re doing SSH wrong. In a single sentence, SSH Certificates are Public Keys stamped by a (trusted) Certificate Authority, they are usually used for authentication and authorization on SSH.

Without certificates, each user has its own private and public key and shares the public key with whoever needs it. By collecting these keys, you create a list of trusted users. Maintaining that list and synchronizing it is tedious as any user can change his key at any time. A solution to this issue is using SSH Certificates. Since Certificates are just the public key of the user stamped by a Certificate Authority, you can decide to trust the Certificate Authority and any certificate created in the future will automatically be trusted.

Just like for X.509 Certificates (used for TLS), the Certificate Authority handles most of the authorization logic before delivering the certificate. To ensure certificates are not misused, the Certificate Authority adds some metadata to it:

  • A validity period
  • A list of Principals, which are the “names” the certificates is valid for
  • A list of Extensions which represents the capabilities allowed for this certificate

How does this relates to signing Git commits

When creating a commit, the author’s identity is attached to it. This identity, called the committer, does not provide any guarantee about who actually made the commit or whether you should trust the change. This is usually not an issue because you trust anyone with write access to your repository to not impersonate another committer.

However, in many cases, you can’t just believe that people are acting in good faith and you need stronger guarantees. This may be for various reasons from audit purposes to personal preferences. In these cases, Git allows attaching a cryptographic signature to commits. Since the Signer is the only person with the private key required to create the signature, this guarantees that signed commits were approved by the Signer (under the condition that the private key isn’t leaked). The Signer and Committer may be different. Signing a commit can be viewed simply as putting a stamp (the signature) on an envelope (the commit content).

When verifying signature, Git verifies that the stamp is from someone you trust. To determine who that someone is, Git looks at the Principals on the signature. To determine whether they are trusted, Git looks at a trust file which associates keys to users. Git hosting services such as GitLab or GitHub additionally verify that the Signer is also the Committer and add a Verified badge on the commit. Other Git objects such as tags and mergetags can also be signed using the same process.

Git currently supports several signature formats: GPG which is the most used, S/MIME which is fairly common in enterprise with X.509 certificates and SSH which is the most recent.

Terminology

Author: The person(s) who wrote the initial code Committer: The person(s) who made the commit Signer: The person who cryptographically signed the commit

Since this article focus around Signatures, the rest of the article uses Author to mean Committer (Author and Committer are the same person in the examples below).

Why use SSH Certificates for signing on Git

Since GPG and SSH Keys are so widely used to sign commits, it makes sense to wonder why we’d like to use SSH Certificates to sign commits. As an individual user, the only way to “prove” your identity is to publish your own public key. This is not the case in many Enterprise settings where a SSH Certificate Authority creates short-lived credentials for employees. In such a setting, individuals don’t want to fetch and refresh the keys of their peers, they want to offload that trust process to the CA.

In other words, anyone who wants to sign with a @mycompany.com email shall have a certificate delivered by a common trusted authority and with a Principal that matches the specific email used to sign the commit.

Putting it all together

If you are reading this article, you probably already have a process to generate certificates for your own identity. For the demonstration, let’s generate a new Certificate Authority, create our own certificate and configure Git.

$ # Generate two key-pairs
$ ssh-keygen -t ed25519 -C "CA key" -f ssh_ca
$ ssh-keygen -t ed25519 -C "User key" -f ssh_sayrus

$ # Generate a Certificate for blog-demo-sayrus
$ ssh-keygen -s ssh_ca -I "Sayrus key" -n "blog-demo-sayrus@sayr.us" ssh_sayrus.pub

$ # Trust the CA for any @sayr.us identity
$ (printf '*@sayr.us cert-authority,namespaces="file,git" '; cat ssh_ca.pub) > allowed_signers

$ # Verify Certificate Identity and Validity
$ ssh-keygen -L -f ssh_sayrus-cert.pub
ssh_sayrus-cert.pub:
        Type: ssh-ed25519-cert-v01@openssh.com user certificate
        Public key: ED25519-CERT SHA256:/W0AkfFf0BTw3uXQz2C3wtV9DMIS7R8Rdmgv7ZjrH5g
        Signing CA: ED25519 SHA256:eMCxVuTpbx6KmM6VdFtTy3r8Eg22ksnJ4hR4qQI8ZiU (using ssh-ed25519)
        Key ID: "Sayrus key"
        Serial: 0
        Valid: forever
        Principals: 
                blog-demo-sayrus@sayr.us
        Critical Options: (none)
        Extensions: 
                permit-X11-forwarding
                permit-agent-forwarding
                permit-port-forwarding
                permit-pty
                permit-user-rc

$ git config gpg.format ssh
$ git config gpg.ssh.allowedSignersFile "${PWD}/allowed_signers"
$ git config user.signingkey "${PWD}/ssh_sayrus-cert.pub"
$ cat allowed_signers
*@sayr.us cert-authority,namespaces="file,git" ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIN+8pFNBOLa0fwNqB9dmIPJGjvOFCQJ+JDSjrbGYUO0w CA key

Signing and verifying commits

To illustrate how Git Signing mechanism works, we will create two commits with different authors but a unique signer. To ensure the Committer matches the Author, I’ll pass configuration parameters using -c.

$ git -c "user.name=Sayrus" -c "user.email=blog-demo-sayrus@sayr.us" commit -m "A commit from myself" --allow-empty
$ git log --show-signature --pretty=full
commit b96d4d050aa8cb0ce4085fac92e6fc7cf0553fc9
Good "git" signature for blog-demo-sayrus@sayr.us with ED25519-CERT key SHA256:/W0AkfFf0BTw3uXQz2C3wtV9DMIS7R8Rdmgv7ZjrH5g
Author: Sayrus <blog-demo-sayrus@sayr.us>
Commit: Sayrus <blog-demo-sayrus@sayr.us>
Date:   Fri Mar 10 14:00:11 2023 +0100

    A commit from myself

$ git -c "user.name=Coworker" -c "user.email=awesome-coworker@sayr.us" commit -m "A commit from someone else" --allow-empty
$ git log --show-signature --pretty=full
commit 4b3034ed0ac5622f96218bfc8caf3191afaf75ac
Good "git" signature for blog-demo-sayrus@sayr.us with ED25519-CERT key SHA256:/W0AkfFf0BTw3uXQz2C3wtV9DMIS7R8Rdmgv7ZjrH5g
Author: Coworker <awesome-coworker@sayr.us>
Commit: Coworker <awesome0coworker@sayr.us>
Date:   Fri Mar 10 14:00:21 2023 +0100

    A commit from someone else

From the log output, we confirm that:

  • Commits are signed by the Principal blog-demo-sayrus@sayr.us
  • Commits are signed by a valid and trusted key from our allowedSignersFile
  • The Principal is allowed for this key (since we allowed *@sayr.us)

Let’s try signing with an expired key:

$ # Generate an expired Certificate for blog-demo-sayrus, this will replace our valid certificate
$ ssh-keygen -s ssh_ca -I "Expired key" -V '-2w:-1w' -n "blog-demo-sayrus@sayr.us" ssh_sayrus.pub
$ git commit -m "A commit from an expired key with a modified commit date" --allow-empty --date="Wed Mar 1 14:00 2023 +0100"
$ git commit -m "A commit from an expired key" --allow-empty
$ git log --show-signature
commit 6e047441f244a8e727ffa6347a6d75554bbfec15
Good "git" signature with ED25519-CERT key SHA256:/W0AkfFf0BTw3uXQz2C3wtV9DMIS7R8Rdmgv7ZjrH5g
/tmp/tmp.iA4TKwFfXG/allowed_signers:1: no valid principals found^M
No principal matched.
Author: Sayrus <blog-demo-sayrus@sayr.us>
Date:   Fri Mar 10 14:18:06 2023 +0100

    A commit from an expired key

commit c08d2a160cac030882a1236889d352be8cfdfefd
Good "git" signature with ED25519-CERT key SHA256:/W0AkfFf0BTw3uXQz2C3wtV9DMIS7R8Rdmgv7ZjrH5g
/tmp/tmp.iA4TKwFfXG/allowed_signers:1: no valid principals found^M
No principal matched.
Author: Sayrus <blog-demo-sayrus@sayr.us>
Date:   Wed Mar 1 14:00:00 2023 +0100

    A commit from an expired key with a modified commit date

From this output, we confirm that the signature is not valid for neither commits. This is because the Signature date is embedded within the signature and does not rely on the commit’s date.

A short note on Signature date

The signature date is entirely controlled by the signer. The same way you can commit with an altered date, it is possible to sign with an altered date. Git Signing with a SSH Certificate alone is not enough to create trusted timestamping.

Verifying that it works for multi-users

$ ssh-keygen -t ed25519 -C "Peer key" -f ssh_peer
$ ssh-keygen -s ssh_ca -I "peer@sayr.us" -n "peer@sayr.us" ssh_peer.pub
$ git config user.signingkey "${PWD}/ssh_peer-cert.pub"
$ # [...]
$ git log --show-signature
commit 19e422127a38ce6f7b7769466490b809f975a71a
Good "git" signature for peer@sayr.us with ED25519-CERT key SHA256:3Mqes9DGMaHi5K96C52QYYFSPQaotVswEgSdJXOTmF4
Author: Sayrus <blog-demo-sayrus@sayr.us>
Date:   Fri Mar 10 16:16:46 2023 +0100

    Signed by someone else

commit 35255c04c3c7999f05c47d40e7f09e3746e0d56f
Good "git" signature for blog-demo-sayrus@sayr.us with ED25519-CERT key SHA256:/W0AkfFf0BTw3uXQz2C3wtV9DMIS7R8Rdmgv7ZjrH5g
Author: Sayrus <blog-demo-sayrus@sayr.us>
Date:   Fri Mar 10 16:16:42 2023 +0100

    Signed by myself

Each commit is signed by a different user and key but they are both trusted since the certificates were created by a trusted authority.

Forge Support

While it works great in the CLI, SSH Certificates are not (yet) supported by GitHub, GitLab or any code hosting solution that I used. This means that if you push a commit signed with a SSH Certificates, your commit will appear as Unverified on GitHub.

GitHub signature for Git CA

Clicking the Unverified tags shows:

GitHub supports GPG and S/MIME signatures. We don’t know what type of signature this is.