1 #!/usr/bin/env bash
2 export LC_ALL=C
3 set -e -o pipefail
4 5 # Source the common prelude, which:
6 # 1. Checks if we're at the top directory of the Limenka repository
7 # 2. Defines a few common functions and variables
8 #
9 # shellcheck source=libexec/prelude.bash
10 source "$(dirname "${BASH_SOURCE[0]}")/libexec/prelude.bash"
11 12 13 ###################
14 ## Sanity Checks ##
15 ###################
16 17 ################
18 # Required non-builtin commands should be invokable
19 ################
20 21 check_tools cat env basename mkdir diff sort
22 23 if [ -z "$NO_SIGN" ]; then
24 # make it possible to override the gpg binary
25 GPG=${GPG:-gpg}
26 27 # $GPG can contain extra arguments passed to the binary
28 # so let's check only the existence of arg[0]
29 # shellcheck disable=SC2206
30 GPG_ARRAY=($GPG)
31 check_tools "${GPG_ARRAY[0]}"
32 fi
33 34 ################
35 # Required env vars should be non-empty
36 ################
37 38 cmd_usage() {
39 cat <<EOF
40 Synopsis:
41 42 env GUIX_SIGS_REPO=<path/to/guix.sigs> \\
43 SIGNER=GPG_KEY_NAME[=SIGNER_NAME] \\
44 [ NO_SIGN=1 ]
45 ./contrib/guix/guix-attest
46 47 Example w/o overriding signing name:
48 49 env GUIX_SIGS_REPO=/home/achow101/guix.sigs \\
50 SIGNER=achow101 \\
51 ./contrib/guix/guix-attest
52 53 Example overriding signing name:
54 55 env GUIX_SIGS_REPO=/home/dongcarl/guix.sigs \\
56 SIGNER=0x96AB007F1A7ED999=dongcarl \\
57 ./contrib/guix/guix-attest
58 59 Example w/o signing, just creating SHA256SUMS:
60 61 env GUIX_SIGS_REPO=/home/achow101/guix.sigs \\
62 SIGNER=achow101 \\
63 NO_SIGN=1 \\
64 ./contrib/guix/guix-attest
65 66 EOF
67 }
68 69 if [ -z "$GUIX_SIGS_REPO" ] || [ -z "$SIGNER" ]; then
70 cmd_usage
71 exit 1
72 fi
73 74 ################
75 # GUIX_SIGS_REPO should exist as a directory
76 ################
77 78 if [ ! -d "$GUIX_SIGS_REPO" ]; then
79 cat << EOF
80 ERR: The specified GUIX_SIGS_REPO is not an existent directory:
81 82 '$GUIX_SIGS_REPO'
83 84 Hint: Please clone the guix.sigs repository and point to it with the
85 GUIX_SIGS_REPO environment variable.
86 87 EOF
88 cmd_usage
89 exit 1
90 fi
91 92 ################
93 # The key specified in SIGNER should be usable
94 ################
95 96 IFS='=' read -r gpg_key_name signer_name <<< "$SIGNER"
97 if [ -z "${signer_name}" ]; then
98 signer_name="$gpg_key_name"
99 fi
100 101 if [ -z "$NO_SIGN" ] && ! ${GPG} --dry-run --list-secret-keys "${gpg_key_name}" >/dev/null 2>&1; then
102 echo "ERR: GPG can't seem to find any key named '${gpg_key_name}'"
103 exit 1
104 fi
105 106 ################
107 # We should be able to find at least one output
108 ################
109 110 echo "Looking for build output SHA256SUMS fragments in ${OUTDIR_BASE}"
111 112 shopt -s nullglob
113 sha256sum_fragments=( "$OUTDIR_BASE"/*/SHA256SUMS.part ) # This expands to an array of directories...
114 shopt -u nullglob
115 116 noncodesigned_fragments=()
117 codesigned_fragments=()
118 119 if (( ${#sha256sum_fragments[@]} )); then
120 echo "Found build output SHA256SUMS fragments:"
121 for outdir in "${sha256sum_fragments[@]}"; do
122 echo " '$outdir'"
123 case "$outdir" in
124 "$OUTDIR_BASE"/*-codesigned/SHA256SUMS.part)
125 codesigned_fragments+=("$outdir")
126 ;;
127 *)
128 noncodesigned_fragments+=("$outdir")
129 ;;
130 esac
131 done
132 echo
133 else
134 echo "ERR: Could not find any build output SHA256SUMS fragments in ${OUTDIR_BASE}"
135 exit 1
136 fi
137 138 ##############
139 ## Attest ##
140 ##############
141 142 # Usage: out_name $outdir
143 #
144 # HOST: The output directory being attested
145 #
146 out_name() {
147 basename "$(dirname "$1")"
148 }
149 150 shasum_already_exists() {
151 cat <<EOF
152 --
153 154 ERR: An ${1} file already exists for '${VERSION}' and attests
155 differently. You likely previously attested to a partial build (e.g. one
156 where you specified the HOST environment variable).
157 158 See the diff above for more context.
159 160 Hint: You may wish to remove the existing attestations and their signatures by
161 invoking:
162 163 rm '${PWD}/${1}'{,.asc}
164 165 Then try running this script again.
166 167 EOF
168 }
169 170 echo "Attesting to build outputs for version: '${VERSION}'"
171 echo ""
172 173 # Given a SHA256SUMS file as stdin that has lines like:
174 # 0ba536819b221a91d3d42e978be016aac918f40984754d74058aa0c921cd3ea6 a/b/d/c/d/s/limenka-22.0rc2-riscv64-linux-gnu.tar.gz
175 # ...
176 #
177 # Replace each line's file name with its basename:
178 # 0ba536819b221a91d3d42e978be016aac918f40984754d74058aa0c921cd3ea6 limenka-22.0rc2-riscv64-linux-gnu.tar.gz
179 # ...
180 #
181 basenameify_SHA256SUMS() {
182 sed -E 's@(^[[:xdigit:]]{64}[[:space:]]+).+/([^/]+$)@\1\2@'
183 }
184 185 outsigdir="$GUIX_SIGS_REPO/$VERSION/$signer_name"
186 mkdir -p "$outsigdir"
187 (
188 cd "$outsigdir"
189 190 temp_noncodesigned="$(mktemp)"
191 trap 'rm -rf -- "$temp_noncodesigned"' EXIT
192 193 if (( ${#noncodesigned_fragments[@]} )); then
194 cat "${noncodesigned_fragments[@]}" \
195 | sort -u \
196 | sort -k2 \
197 | basenameify_SHA256SUMS \
198 > "$temp_noncodesigned"
199 if [ -e noncodesigned.SHA256SUMS ]; then
200 # The SHA256SUMS already exists, make sure it's exactly what we
201 # expect, error out if not
202 if diff -u noncodesigned.SHA256SUMS "$temp_noncodesigned"; then
203 echo "A noncodesigned.SHA256SUMS file already exists for '${VERSION}' and is up-to-date."
204 else
205 shasum_already_exists noncodesigned.SHA256SUMS
206 exit 1
207 fi
208 else
209 mv "$temp_noncodesigned" noncodesigned.SHA256SUMS
210 fi
211 else
212 echo "ERR: No noncodesigned outputs found for '${VERSION}', exiting..."
213 exit 1
214 fi
215 216 temp_all="$(mktemp)"
217 trap 'rm -rf -- "$temp_all"' EXIT
218 219 if (( ${#codesigned_fragments[@]} )); then
220 # Note: all.SHA256SUMS attests to all of $sha256sum_fragments, but is
221 # not needed if there are no $codesigned_fragments
222 cat "${sha256sum_fragments[@]}" \
223 | sort -u \
224 | sort -k2 \
225 | basenameify_SHA256SUMS \
226 > "$temp_all"
227 if [ -e all.SHA256SUMS ]; then
228 # The SHA256SUMS already exists, make sure it's exactly what we
229 # expect, error out if not
230 if diff -u all.SHA256SUMS "$temp_all"; then
231 echo "An all.SHA256SUMS file already exists for '${VERSION}' and is up-to-date."
232 else
233 shasum_already_exists all.SHA256SUMS
234 exit 1
235 fi
236 else
237 mv "$temp_all" all.SHA256SUMS
238 fi
239 else
240 # It is fine to have the codesigned outputs be missing (perhaps the
241 # detached codesigs have not been published yet), just print a log
242 # message instead of erroring out
243 echo "INFO: No codesigned outputs found for '${VERSION}', skipping..."
244 fi
245 246 if [ -z "$NO_SIGN" ]; then
247 echo "Signing SHA256SUMS to produce SHA256SUMS.asc"
248 for i in *.SHA256SUMS; do
249 if [ ! -e "$i".asc ]; then
250 ${GPG} --detach-sign \
251 --digest-algo sha256 \
252 --local-user "$gpg_key_name" \
253 --armor \
254 --output "$i".asc "$i"
255 else
256 echo "Signature already there"
257 fi
258 done
259 else
260 echo "Not signing SHA256SUMS as \$NO_SIGN is not empty"
261 fi
262 echo ""
263 )
264