diff options
-rw-r--r-- | posts/img/cgit-signatures.png | bin | 0 -> 59431 bytes | |||
-rw-r--r-- | posts/signify-cgit.md | 193 |
2 files changed, 193 insertions, 0 deletions
diff --git a/posts/img/cgit-signatures.png b/posts/img/cgit-signatures.png Binary files differnew file mode 100644 index 0000000..bfcfcb1 --- /dev/null +++ b/posts/img/cgit-signatures.png diff --git a/posts/signify-cgit.md b/posts/signify-cgit.md new file mode 100644 index 0000000..bf0e48e --- /dev/null +++ b/posts/signify-cgit.md @@ -0,0 +1,193 @@ +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: + +<figure> + <img class="round" src="img/cgit-signatures.png" width="60%" alt="Detached signatures + linked alongside compressed tarballs"/> + <figcaption>Detached signatures in their natural habitat</figcaption> +</figure> + +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). +Normally, a tree maps blobs (or other trees) to paths in the repository. The +tree object that links notes, however, uses the commit blob as a "path". In the +following tree printed by +[`git-cat-file(1)`](https://git-scm.com/docs/git-cat-file), the note blobs are +on the left side whilst the commit blobs are on the right: + +``` +100644 blob 6e3356af25efc1012279eb5e4c1bc5a31be16c31 22b910ba4985c6f1d91eaf9ac6c88e6fcb555115 +100644 blob c2daf0183bca1757216a792e9f770e8637db0d1b 36e553d961cc114c2f676108bdc28c7473a81fdc +100644 blob bc82006e27fc0c79b282976703d97f78fbf4ebef ebdc5a9bc9d5e303c08fb4a2126de02946b80d08 +100644 blob fc26da013cd8da9e95dd65433b9c8e423c20fbea f5121f6db3a18421f857d84c1e85bbc7c45cbd24 +``` + +The tree itself is then linked in a normal commit object 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/<format>` +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 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[^3] tarball, we have to +pass the right prefix -- just as cgit does: + +``` +$ git archive --prefix=project-1.0.0/ -o project-1.0.0.tar.gz -- 1.0.0 +``` + +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[^4]. + +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[^5]. + +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 project-1.0.0.tar.gz > project-1.0.0.tar.gz.SHA256 +``` + +Finally, the following invocation of signify cryptographically signs the +checksum file using our secret key and writes the signature to +`project-1.0.0.tar.gz.SHA256.sig`: + +``` +$ signify -Ses release.sec -m project-1.0.0.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 `1.0.0` release tag: + +``` +$ git notes --ref=signatures/tar.gz add -C "$(git hash-object -w project-1.0.0.tar.gz.SHA256.sig)" 1.0.0 +``` + +Let's take a look at the signature we just stored: + +``` +$ git notes --ref=signatures/tar.gz show 1.0.0 +untrusted comment: verify with release.pub +RWRyR8jRAxhmZ/xwxq1/oPEJ1BUZa+sYj/UKP+px+KdkT/fHrHYSXCoHmoCKqCpy3Iv2hekCyK/36fi30Leti53J+QVvkGeT2Qc= +SHA256 (project-1.0.0.tar.gz) = 2fdc6078b432dbc513fc9f21cd90d87e9458e7c4fea9507d58b4560a00e0399c +``` + +Looks good. Let's verify it before publishing: + +``` +$ git notes --ref=signatures/tar.gz show 1.0.0 | signify -Cp release.pub -x - +Signature Verified +project-1.0.0.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[^6]: + +``` +$ 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]: The worst sin a tarball can commit is having all files saved at toplevel, + polluting the directory it is extracted into. +[^4]: A portable version is available + [here](https://github.com/aperezdc/signify). +[^5]: This is so that you can verify a whole set of files with just one + signature. +[^6]: 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. + |