1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
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.
|