summaryrefslogtreecommitdiffstatshomepage
path: root/posts
diff options
context:
space:
mode:
Diffstat (limited to 'posts')
-rw-r--r--posts/img/cgit-signatures.pngbin0 -> 59431 bytes
-rw-r--r--posts/signify-cgit.md193
2 files changed, 193 insertions, 0 deletions
diff --git a/posts/img/cgit-signatures.png b/posts/img/cgit-signatures.png
new file mode 100644
index 0000000..bfcfcb1
--- /dev/null
+++ b/posts/img/cgit-signatures.png
Binary files differ
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.
+