title: Hosting signify signatures on cgit date: 2021-05-16 author: Wolfgang Müller A seemingly overlooked[^1] feature of [cgit](https://git.zx2c4.com/cgit/about/) is its ability to host detached signatures of the snapshots it generates. Add a signature of a snapshot to the Git repository, and cgit will automatically offer it right next to the corresponding snapshot:
Detached signatures
	linked alongside compressed tarballs
Detached signatures in their natural habitat
But how does cgit know which signature corresponds to which snapshot, and how are signatures stored in the Git repository itself? Enter [`git-notes(1)`](https://git-scm.com/docs/git-notes), another feature that is probably overlooked. Intended to facilitate adding additional notes to commit messages without changing the commit itself, `git-notes(1)` lends itself well to attaching textual data to any Git object such as a commit or a tag. ## Duly noted Internally, Git stores the contents of a note as a [blob](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects), meaning it is saved to the object database and referred to by its hash. But Git must also store which commit a note belongs to. For this it uses a [tree](https://git-scm.com/book/en/v2/Git-Internals-Git-Objects#_tree_objects). A tree is Git's way of encoding a directory: it contains a list of paths, and maps them to blobs (files) or other trees (subdirectories). We can look at those trees using [`git-cat-file(1)`](https://git-scm.com/docs/git-cat-file). For example, the following is the tree for [this commit](https://git.oriole.systems/quarg/commit/?id=7cd07362c4c13bfd01afa85a4666c491a020f330): ``` $ git cat-file -p 6a81213f2e88a33835f2fb94015bda5dc04a397c 100644 blob 3feb78adc667d7bf4ad2cec4fb780b29ced25302 .gitignore 100644 blob 2149bd82c51e65c34d8aee87f96c4f1c1af8f6c1 LICENSE 100644 blob d397eaf5a75d0d30c4d9d2ffcd1007b44acc842a quarg.1 040000 tree eee3c892c769bad3b7d883f0a162500406f67c3b quarg 100644 blob 1a861ec51a39cbefa0f5027995c358af2224ebbb setup.py ``` The tree object that links notes, however, uses the commit blob itself as a "path". In the following tree (for [this](https://git.oriole.systems/quarg/commit/?h=refs/notes/signatures/tar.gz&id=f12709c0843a89b5be37fe499b9928a8c11cbcd9) commit) the note blob is on the left side whilst the commit blob ("path") is on the right: ``` $ git cat-file -p 3a5fd837cae7e478b9a230f6c301c93efda7c1e2 100644 blob 5c7ec832f83342aa460bb27b0b75e12695a98a53 43c9fb13e063cebfd08e741b67b9ec2317aed4b9 ``` Let's see which commit that is: ``` $ git show -s --pretty=oneline 43c9fb13e063cebfd08e741b67b9ec2317aed4b9 43c9fb13e063cebfd08e741b67b9ec2317aed4b9 (tag: 0.1.2) Prepare for release of 0.1.2 ``` The note tree is then linked in a normal [commit object](https://git.oriole.systems/quarg/commit/?h=refs/notes/signatures/tar.gz&id=f12709c0843a89b5be37fe499b9928a8c11cbcd9) that a special [ref](https://git-scm.com/book/en/v2/Git-Internals-Git-References) points to. By default, that is `refs/notes/commits`. One may think of that ref as pointing to a special branch holding all the information on notes "published" to that branch. Instead of that default location, cgit looks in `refs/notes/signatures/` for any signatures. For example, signatures for a gzip-compressed tarball are stored in `refs/notes/signatures/tar.gz`. If cgit finds a signature attached to a tag and can generate a matching tarball, it will automatically link the contents of the note blob as a `.asc`[^2] file. ## Snap, shot! With the Git internals out of the way, let's break down the process of hosting a signature on cgit. Before we get to the actual signing process, a quick but important detour. Since we will be publishing signatures of the snapshot itself, we have to make sure that its contents are stable. Furthermore, we have to be absolutely certain that cgit is generating the same snapshot that we used when we made the signature. We could of course tag a release, download the snapshot via cgit ourselves, verify that it contains the correct files, and then sign it, but that seems backwards; we just blindly trusted the cgit machinery to give us what we expect. What if our cgit host had been compromised? We should make sure that we can create the same snapshot locally. Thankfully, this is a trivial thing to do. We can use [`git-archive(1)`](https://git-scm.com/docs/git-archive) to create a stable archive[^3] from any tag. This is, in fact, also what cgit does internally. By default, `git-archive(1)` does not prefix the files in the archive with the project title and tag, so to make sure that we get a sane[^4] tarball, we have to pass the right prefix -- just as cgit does: ``` $ git archive --prefix=quarg-0.1.2/ -o quarg-0.1.2.tar.gz -- 0.1.2 ``` The resulting file should be exactly what you get when you download a snapshot for the same version from cgit. ## Signed, Sealed, Delivered Now that we have the actual snapshot, we can get to signing it. The example in `cgitrc(5)` uses the dreaded `gpg(1)` interface to generate a signature, but since notes are just textual objects, we can use any utility that generates a signature in text form. I will be using OpenBSD's [signify](https://www.openbsd.org/papers/bsdcan-signify.html), a tool I have been recommending for a long time given its simplicity and ease of use[^5]. To make things more straightforward and give people who do not want to use signify a way of verifying the integrity of the download, we do not sign the snapshot itself, but its checksum. Conveniently, signify supports verifying the signature and checksum in one invocation[^6]. Since signify expects BSD-style checksums from OpenBSD's [`sha256(1)`](https://man.openbsd.org/sha256), we have to make sure to pass the `--tag` option to its GNU counterpart, [`sha256sum(1)`](https://manpages.debian.org/buster/coreutils/sha256sum.1.en.html): ``` $ sha256sum --tag quarg-0.1.2.tar.gz > quarg-0.1.2.tar.gz.SHA256 ``` Finally, the following invocation of signify cryptographically signs the checksum file using our secret key and writes the signature to `quarg-0.1.2.tar.gz.SHA256.sig`: ``` $ signify -Ses release.sec -m quarg-0.1.2.tar.gz.SHA256 ``` ## Final assembly Now all that is left is to store the signature in Git's object database using [`git-hash-object(1)`](https://git-scm.com/docs/git-hash-object) and tell `git-notes(1)` to link that blob to the `0.1.2` release tag: ``` $ git notes --ref=signatures/tar.gz add -C "$(git hash-object -w quarg-0.1.2.tar.gz.SHA256.sig)" 0.1.2 ``` Let's take a look at the signature we just stored: ``` $ git notes --ref=signatures/tar.gz show 0.1.2 untrusted comment: verify with release.pub RWRyR8jRAxhmZ8C5e7Vxkaed4Tg5Po+Qg4J+0LvjfRRfzch1MqUL8nzkrtGEB8fLG1+DwRYkzYdcZ7qjcYSPx048lTSVpqjSAAc= SHA256 (quarg-0.1.2.tar.gz) = 1b6610c2417f36b5b1df5208c3c641b8b2ac3283dae87f453801cdc8c4ffb80a ``` Looks good. Let's verify it before publishing: ``` $ git notes --ref=signatures/tar.gz show 0.1.2 | signify -Cp release.pub -x - Signature Verified quarg-0.1.2.tar.gz: OK ``` Great, this is ready to be published. Git will not include `refs/notes/*` by default when pushing, so to update the upstream repository with the new signatures, we have to push the ref manually: ``` $ git push origin refs/notes/signatures/tar.gz ``` Similarly, a regular clone of the repository will not download any note objects by default. If you want to have a look at the signatures for [weltschmerz](https://git.oriole.systems/weltschmerz/) or [quarg](https://git.oriole.systems/quarg/), for example, you have to fetch them manually like so: ``` $ git fetch origin refs/notes/signatures/tar.gz:refs/notes/signatures/tar.gz ``` Git also supports showing notes in [`git-log(1)`](https://git-scm.com/docs/git-log) directly, by use of the `--notes` option. The following will display any signatures for the tar.gz format inline after the commit message[^7]: ``` $ git log --notes=signatures/tar.gz ``` ## When in doubt, automate Of course doing all of this every time one wants to carve a new release is a bit tedious, so I ended up writing a [small helper script](https://git.oriole.systems/git-helpers/tree/git-sign-for-cgit). For now it hardcodes the invocation of signify, but it should be easily extensible to accommodate other tools. [^1]: It is mentioned briefly in [`cgitrc(5)`](https://git.zx2c4.com/cgit/tree/cgitrc.5.txt?id=892ba8c3cc0617d2087a2337d8c6e71524d7b49c#n777) but I have yet to see it be deployed widely. Of course it might also be that signatures are not valued that much anymore these days. [^2]: Whilst I'd prefer `.sig` for signify signatures specifically, this would need patching in cgit. So for now I have to concede and accept that Firefox calls it a 'detached OpenPGP signature' even though it isn't. [^3]: An interesting side effect that I only [recently](https://git.oriole.systems/site/commit/?id=0da8a6e3d85a98e38b1abc2499f7e7b3fe5d9534) noticed is that by signing tarballs generated by `git-archive(1)` one also implicitly signs a commit. Read more [here](https://oriole.systems/posts/verify-with-signify#Verification%20of%20the%20corresponding%20commit). [^4]: The worst sin a tarball can commit is having all files saved at toplevel, polluting the directory it is extracted into. [^5]: A portable version is available [here](https://github.com/aperezdc/signify). [^6]: This is so that you can verify a whole set of files with just one signature. [^7]: Note that we do not have to type out the full ref here. `git-log(1)` will make sure to [form the full name](https://git-scm.com/docs/git-log#Documentation/git-log.txt---notesltrefgt). `git-notes` also supports these short forms.