Repository navigation
Expand file tree
/
Copy pathcheck-commit-signature
More file actions
executable file
·333 lines (303 loc) · 12.5 KB
/
Copy pathcheck-commit-signature
File metadata and controls
executable file
·333 lines (303 loc) · 12.5 KB
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
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
#!/usr/bin/env bash
##############################################################################
#
# check-commit-signature
# ----------------------
# A server-side update git hook for checking the GPG signature of a pushed
# commit.
#
# To enable this hook, rename this file to "update".
#
# Config
# ------
# hooks.allowunsignedcommits
# This boolean sets whether unsigned commits are allowed. By
# default, they are not allowed.
# hooks.allowunsignedtags
# This boolean sets whether unsigned tags are allowed. By default,
# they are not allowed.
# hooks.allowcommitsonmaster
# This boolean sets whether non-merge commits are allowed on master. By
# default, these are not allowed.
# hooks.allowunannotated
# This boolean sets whether unannotated tags will be allowed into the
# repository. By default they won't be.
# hooks.allowdeletetag
# This boolean sets whether deleting tags will be allowed in the
# repository. By default they won't be.
# hooks.allowmodifytag
# This boolean sets whether a tag may be modified after creation. By default
# it won't be.
# hooks.allowdeletebranch
# This boolean sets whether deleting branches will be allowed in the
# repository. By default they won't be.
# hooks.denycreatebranch
# This boolean sets whether remotely creating branches will be denied
# in the repository. By default this is allowed.
#
# @author Isis Agora Lovecruft, 0x2cdb8b35
# @date 10 March 2013
# @version 0.0.1
##############################################################################
## server config
##################
allowunsignedcommits=$(git config --bool hooks.allowunsignedcommits)
allowunsignedtags=$(git config --bool hooks.allowunsignedtags)
allowcommitsonmaster=$(git config --bool hooks.allowcommitsonmaster)
allowdeletebranch=$(git config --bool hooks.allowdeletebranch)
denycreatebranch=$(git config --bool hooks.denycreatebranch)
allowunannotated=$(git config --bool hooks.allowunannotated)
allowdeletetag=$(git config --bool hooks.allowdeletetag)
allowmodifytag=$(git config --bool hooks.allowmodifytag)
## committers and their signing GPG key fingerprints should go in the
## ${collaborators} array:
##
## we hard code the fingerprint to protect against the possibility of the gpg
## on the remote git server updating the keys (if it is configured to do so,
## and also configured to use short keyids) and pulling in a colliding key from
## a keyserver.
##
declare -A collaborators
collaborators['aagbsn']='91D4 3A5B 1016 CEB6 D352 65AE 2C4B 239D D876 C9F6'
collaborators['hellais']='46E5 EF37 DE26 4EA6 8DCF 53EA E3A2 1297 150F E210'
collaborators['ioerror']='228F AD20 3DE9 AE7D 84E2 5265 CF9A 6F91 4193 A197'
collaborators['isis']='0A6A 58A1 4B59 46AB DE18 E207 A3AD B67A 2CDB 8B35'
## get the keyids from the fingerprints above:
declare -A trusted_keys
for collab in "${!collaborators[@]}"; do
fpr="${collaborators["$collab"]}"
shortid=$(echo "$fpr" | cut -d ' ' --output-delimiter='' -f 10,11 )
trusted_keys["$collab"]="$shortid"
## git log --show-signature currently will only give us the shortid
##
## if someday git will give us a way to get the longid of the signing
## key, we should change to:
#longid=$(gpg --list-keys --with-colon "$shortid" | \
# grep ^pub | cut -d ':' -f 5 )
#trusted_keys["$collab"]="$longid"
done
is_allowed_signer() {
signing_keyid=$1
if [ -z "$signing_keyid" ]; then
echo "*** Error: is_allowed_signer(): No key ID supplied" >&2
return 1
fi
fpr_signing_keyid=$(gpg --fingerprint "$signing_keyid" | \
grep -o -E "([0-9A-F]{4}[[:space:]]{0,2}){10}")
allowed_keyid=1
for collab in "${!trusted_keys[@]}"; do
## XXX the $keyid is currently only a short key id
## because git doesn't give us a way to ask for a
## long keyid
keyid="${trusted_keys["$collab"]}"
if [ "$signing_keyid" = "$keyid" ]; then
## check that the fingerprint of the key that
## gave us a good signature matches the
## hardcoded ones:
if [ "$fpr_signing_keyid" = "${collaborators["$collab"]}" ]; then
allowed_keyid=0
break
fi
fi
done
return $allowed_keyid
}
## $1 is the git ref which is being revised
## $2 is the last HEAD
## $3 is the HEAD commit of the series of commits being applied
ref=$1
rev_old=$2
rev_new=$3
## the following is the short reference name for tags, i.e. 'v0.1.2' in
## lieu of 'refs/tags/v0.1.2':
short_ref=${ref##refs/tags/}
## a hash full of zeroes is how Git represents "nothing"
zero="0000000000000000000000000000000000000000"
## get all new commits on branch ref, even if it's a new branch
if [ "$rev_old" = "$zero" ]; then
# list everything reachable from rev_new but not any heads
span=$(git rev-list $rev_new --not --branches='*')
else
span=$(git rev-list ^$rev_old $rev_new)
fi
if [ "$allowcommitsonmaster" != "true" ]; then
## the only commits allowed on master are merges which have rev_old as
## a direct parent of rev_new
##
## we have to check this in a separate step, since "git rev-list
## ^rev_old rev_new" on master at the merge of a feature branch will
## also give us all the commits on the feature branch, and we won't
## have enough context while looping through those commits to do this
## check properly
if [ "$ref" = "refs/heads/master" ]; then
## if rev_new isn't a merge, it will only have one parent and
## this "git show" command will return nothing
parents=$(git show --merges --no-patch --format=%P $rev_new)
old_is_parent_of_new=false
for parent in $parents ; do
if [ "$rev_old" = "$parent" ]; then
old_is_parent_of_new=true
break
fi
done
if [ "$old_is_parent_of_new" != "true" ]; then
echo "*** Master only accepts merges of feature branches" >&2
exit 1
fi
fi
fi
if [ -z "$span" ]; then
## rev_new pointed to something considered by "git rev-list" to be
## already in the commit graph, which could be a commit (if we're
## adding a lightweight tag) or a tag object (if we're adding an
## annotated tag), since "git rev-list" doesn't consider the tag
## object to be separate from the commit it points to
type=$(git cat-file -t $rev_new)
case $type in
commit)
## lightweight tag
if [ "$allowunsignedtags" != "true" -o "$allowunannotated" != "true" ]; then
echo "*** The un-annotated tag $short_ref is not allowed in this repository" >&2
echo "*** Use 'git tag [ -a | -s ]' for tags you want to propagate." >&2
exit 1
fi
;;
tag)
## annotated tag
if [ "$rev_old" != $zero -a "$allowmodifytag" != "true" ]; then
echo "*** Tag $short_ref already exists." >&2
echo "*** Modifying a tag is not allowed in this repository." >&2
exit 1
else
if [ "$allowunsignedtags" != "true" ]; then
result=$(git verify-tag $rev_new 2>&1 >/dev/null)
if ! grep '^gpg: Good signature' <<< "$result"; then
echo "*** Tag $short_ref is not signed" >&2w
exit 1
fi
signing_keyid=$(<<<"$result" grep "^gpg: Signature made" | \
grep -o -E "key ID [0-9A-Fa-f]{8,16}" | \
cut -d ' ' -f 3 )
if is_allowed_signer $signing_keyid; then
echo "*** Good signature on tag $short_ref by signing key $signing_keyid" >&2
else
echo "*** Rejecting tag $short_ref due to lack of a valid GPG signature" >&2w
exit 1
fi
fi
fi
;;
*)
echo "*** No new commits, but the pushed ref $ref is a \"$type\" instead of a tag? I'm confused." >&2
exit 1
;;
esac
fi
## for all the commits in the series, check the type of the commit against the
## commit directly before it:
rev_cur=$rev_old ## set the current rev to the previous HEAD
for commit in $span ; do
## check that the current revison object is a hexidecimal hash of length 40
check_rev=$(git rev-parse --verify "$commit")
if ! grep -q -E '^[0-9A-Fa-f]{40}$' <<< $check_rev; then
echo "*** Commit hash is not 40 hex characters" >&2
exit 1
fi
## get the commit type of the current rev:
## a commit with a hash full of zeros is a deletion of a ref
if [ "$commit" = "$zero" ]; then
commit_type=delete
else
if [ "$rev_cur" = "$zero" ]; then
## there was no previous commit to check against,
## so this is the first commit on a branch
commit_type=$(git cat-file -t "$commit")
else
merge=$(git rev-list -n 1 --merges "$rev_cur".."$commit")
if test -n "$merge"; then
commit_type=merge
else
commit_type=$(git cat-file -t "$commit")
fi
fi
fi
## the following returns non-null if $rev_cur is a signed tag:
is_signed_tag=$(git tag --verify "$ref" 2>&1 >/dev/null | grep '^gpg: Good signature')
## the following returns non-null if $rev_cur has a signature, and gpg reports
## the signature is good:
has_good_sig=$(git show --no-patch --format=%H --show-signature "$commit" | grep "^gpg: Good signature")
## the following extracts the signing keyid (either short or long) from the
## signature on $rev_cur:
signing_keyid=$(git show --no-patch --format=%H --show-signature "$commit" | \
grep -o -E "key ID [0-9A-Fa-f]{8,16}" | \
cut -d ' ' -f 3 )
fpr_signing_keyid=$(gpg --fingerprint "$signing_keyid" | \
grep -o -E "([0-9A-F]{4}[[:space:]]{0,2}){10}")
case "$ref","$commit_type" in
refs/heads/*,merge)
if [ "$allowunsignedcommits" != "true" ]; then
if [ -n "$has_good_sig" -a -n "$signing_keyid" ]; then
if is_allowed_signer $signing_keyid; then
echo "*** Good signature on merge $commit by signing key $signing_keyid" >&2
else
echo "*** Key $signing_keyid is not allowed to sign merge $commit" >&2
exit 1
fi
else
echo "*** Merges must be signed with an authorised key" >&2
exit 1
fi
fi
;;
refs/heads/*,commit)
## commit on any branch
if [ "$rev_old" = "$zero" -a "$denycreatebranch" = "true" ]; then
echo "*** Creating a branch is not allowed in this repository" >&2
exit 1
fi
if [ "$allowunsignedcommits" != "true" ]; then
if [ -z "$has_good_sig" ]; then
echo "*** Bad signature on commit $commit" >&2
exit 1
fi
if is_allowed_signer $signing_keyid; then
echo "*** Good signature on commit $commit by signing key $signing_keyid" >&2
else
echo "*** Key $signing_keyid is not allowed to sign commit $commit" >&2
exit 1
fi
fi
;;
refs/heads/*,delete)
# delete branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a branch is not allowed in this repository" >&2
exit 1
fi
;;
refs/tags/*,delete)
## delete tag
if [ "$allowdeletetag" != "true" ]; then
echo "*** Deleting a tag is not allowed in this repository" >&2
exit 1
fi
;;
refs/remotes/*,commit)
## tracking branch
;;
refs/remotes/*,delete)
## delete tracking branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting a tracking branch is not allowed in this repository" >&2
exit 1
fi
;;
*)
## Anything else (is there anything else?)
echo "*** Unknown type of update to ref $ref of type $commit_type " >&2
exit 1
;;
esac
## increment the current rev to the $commit we just checked:
rev_cur=$commit
done