Introduction
A version control system like git
doesn't just track changes, it
also provides a record of who made those changes. This information
can be used to check that commits are authorized, which can improve
software supply chain security. In particular, checking a change's
provenance can be used to remove intermediaries like forges, and
package registries from a user's trusted computing base. But,
authorship information can easily be
forged.
An obvious solution to prevent forgeries is to require commits to be digitally signed. But, by itself a digital signature doesn't prevent forgeries. Anyone can generate a certificate with any user ID, and use it to sign commits:
mallory$ sq key generate --userid 'Neal H. Walfield <neal@sequoia-pgp.org>' --own-key --without-password
- ┌ 13914CAD8DA9055E86973BCE16EABCF4A66A228B
└ Neal H. Walfield <neal@sequoia-pgp.org>
- certification created
...
mallory$ emacs main.rs
mallory$ git add main.rs
mallory$ git commit -m 'Clean up the code.'
[main (root-commit) a001000] Clean up the code.
1 file changed, 21 insertions(+)
create mode 100644 main.rs
When someone like Alice verifies the signature, they see that the commit is correctly signed, which it is:
alice$ git log -n1 --pretty=short --show-signature
commit a001000000000000000000000000000000000000
gpg: Signature made Fri Feb 21 08:42:16 2025 +01:00
gpg: using EDDSA key F5FA62C39C1620C0DCE60A53FA23CCD7B28FB8CE
gpg: Good signature from "Neal H. Walfield <neal@sequoia-pgp.org>" [unknown]
gpg: WARNING: This key is not certified with a trusted signature!
gpg: There is no indication that the signature belongs to the owner.
Primary key fingerprint: 1391 4CAD 8DA9 055E 8697 3BCE 16EA BCF4 A66A 228B
Subkey fingerprint: F5FA 62C3 9C16 20C0 DCE6 0A53 FA23 CCD7 B28F B8CE
Author: Neal H. Walfield <neal@sequoia-pgp.org>
Clean up the code.
But as the warning points out, the certificate may not actually belong to the stated owner. And, as the prompt suggests, it isn't.
What is needed is not only a list of entities that are allowed to modify the repository, but also the certificates they use to sign the commits. In other words, to authenticate a commit we need a signing policy, which says what certificates are authorized to make changes.
Creating a policy isn't complicated. But, for an end user it is time consuming, and requires diligently tracking the project to identify when maintainers come and go. That's too much work.
Instead, a project's maintainers could curate a list of entities that are allowed to add commits and update the signing policy, and enumerate the certificates they use to sign them. The tricky part is applying the policy. There are a number of edge cases that need to be handled like how to merge changes from external contributions, who is allowed to change the policy, and how to deal with compromised keys.
The Sequoia git project specifies a set of semantics, defines a policy language and file format, and provides a set of tools to manage a policy file, and authenticate commits.
Using sq-git
is relatively straightforward. You start by adding a
policy file, openpgp-policy.toml
, to your project's repository. (As
shown in the following chapters, sq-git
helps you do that.) The
policy is maintained in-band to allow it to evolve, just like the rest
of the project. The openpgp-policy.toml
file is basically a list of
entities, the type of changes they are authorized to make, and their
respective OpenPGP certificates.
Before you merge a pull request, you check that commits are authorized
by the policy. Locally, this can be done by running sq-git log
on
the range of commits that you want to check. If your project uses CI,
you add a job that automatically checks that all new commits are
authorized.
Downstream users can use sq-git
to check that there is a chain of
trust from an older, known-good version of the software to a new
version. This helps prevent the use of versions that include
modifications that weren't authorized by the project's maintainers.
sq-git
has a relatively simple two-step authentication rule. First,
a commit is considered authorized if one of its parent's policies can
authenticate it. A commit is considered authorized with respect to a
trust root if there is a path from the trust root to the commit where
every commit is authenticated by the parent commit on the path.
The following shows that there are multiple paths from the first commit to the last commit:
bob$ git log --decorate --pretty=short --graph
* commit b009000000000000000000000000000000000000 (HEAD -> main)
|\ Merge: b005000 b006000
| | Author: Bob <bob@example.org>
| |
| | Merge Carol's change
| |
| * commit b006000000000000000000000000000000000000 (carol/vroom)
|/ Author: Carol <carol@example.org>
|
| Use an O(log(n)) algorithm instead of one that takes O(n).
|
* commit b005000000000000000000000000000000000000
| Author: Bob <bob@example.org>
|
| Add support for ACME's frob.
|
* commit b004000000000000000000000000000000000000
| Author: Bob <bob@example.org>
|
| Fix a corner case.
|
* commit b003000000000000000000000000000000000000
| Author: Bob <bob@example.org>
|
| Add a cool new feature.
|
* commit b002000000000000000000000000000000000000
| Author: Alice <alice@example.org>
|
| Authorize Bob to be a release manager.
|
* commit b001000000000000000000000000000000000000
Author: Alice <alice@example.org>
Add a signing policy.
Only one of those paths needs to be authenticated, because when the parent authenticates the merge commit, it implicitly authenticates the changes added by the other parents. In a certain sense, the fact that the other parents are recorded in the git history is purely decorative.
sq-git
distinguishes itself from projects like
sigstore in that all of the information
required to authenticate commits is available locally, and no
third-party authorities are required.