-
Notifications
You must be signed in to change notification settings - Fork 39
/
check-commit-signature
executable file
·411 lines (372 loc) · 17 KB
/
check-commit-signature
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
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
#!/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 tags, and merges with unsigned commits
# into master, 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.allowunverifiedsigs
# This boolean sets whether commits/tags signed by
# unverified/untrusted keys 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.allowhotfixonmaster
# The boolean sets whether branches beginning with hotfix-* are allowed on
# master. NOT YET IMPLEMENTED. CURRENTLY IGNORED.
# 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.
# hooks.restrictmergetomaster
# This boolean sets whether restrictions on merging to master should be
# enforced. By default there won't be.
#
# @author Isis Agora Lovecruft, 0x2cdb8b35
# @date 10 March 2013
# @version 0.0.1
##############################################################################
## server config
##################
## $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
## This file has been modified to run off a .gitconfig file in the root of the repo.
## If this file doesn't exist, the hook will exit cleanly.
## Because of this the hook can be included globally and only executes on projects that need it.
## This way, you can add it as a global hook in i.e. Gitlab, and can be configured per-project.
##
## Be aware that this assumes that all collaborators are trustworthy and might
## cause security issues if this isn't the case.
## It's recommended to at least enable the GPG whitelist when using this hook.
git cat-file -e "$rev_old:.gitconfig" &>/dev/null || exit 0;
allowunsignedcommits=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowunsignedcommits)
allowunsignedtags=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowunsignedtags)
allowunverifiedsigs=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowunsignedtags)
allowcommitsonmaster=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowcommitsonmaster)
allowhotfixonmaster=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowhotfixonmaster)
allowdeletebranch=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowdeletebranch)
denycreatebranch=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.denycreatebranch)
allowunannotated=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowunannotated)
allowdeletetag=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowdeletetag)
allowmodifytag=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.allowmodifytag)
restrictmergetomaster=$(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --bool hooks.restrictmergetomaster)
echo ".gitconfig file in repo found, running update hook."
git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --list
## Whitelisted GPG key fingerprints are stored in .gitconfig in the root of the repo.
## To add keys, you either edit the file directly:
##
## $ git config --file .gitconfig -e
## [gpgkeys]
## whitelist = "0A6A 58A1 4B59 46AB DE18 E207 A3AD B67A 2CDB 8B35" # Isis
## whitelist = "0C83 9FFC C5A6 5925 D91B F16C FF52 7312 2C52 C4E4" # Sebastiaan <[email protected]>
##
## Or you can add them through the git config command:
##
## $ git config --file .gitconfig gpgkeys.whitelist --add '0A6A 58A1 4B59 46AB DE18 E207 A3AD B67A 2CDB 8B35' \
## --add '0C83 9FFC C5A6 5925 D91B F16C FF52 7312 2C52 C4E4'
##
## Do note that you cannot add a key description if you do it this way.
##
## Remember to add the public keys to the server, too, or verification will fail.
## If allowunverifiedsigs=false, make sure the public keys are signed by a trusted key as well.
readarray -t collaborators < <(git cat-file blob "$rev_old:.gitconfig" | git config --file /dev/stdin --get-all gpgkeys.whitelist)
if [[ "$allowunsignedcommits" != true ]] && [[ "${#collaborators[@]}" -eq 0 ]]; then
echo "WARNING: allowunsignedcommits is false but no authorized keys have"
echo "WARNING: been added yet. Skipping module."
allowunsignedcommits=true
fi
declare -A trusted_keys
for collab in "${!collaborators[@]}"; do
## As per git version v2.10.0-rc1, git now shows longids by default
fpr="${collaborators["$collab"]}"
longid=$(echo "$fpr" | awk '{print $7$8$9$10}' )
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
}
## 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]{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 originating from branches
## beginning with the name "devel":
is_from_develop=$(git branch --contains "$commit" | grep devel )
## the following returns non-null if $rev_cur is originating from branches
## beginning with the name "release":
is_from_release=$(git branch --contains "$commit" | grep release )
## 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:')
## the following returns non-null if $rev_cur has a signature, and gpg reports
## the signature is good:
verify_sig=$(git verify-commit --raw "$commit" 2>&1)
has_good_sig=$?
if [ $has_good_sig -eq 0 ] && [ "$allowunverifiedsigs" != "true" ]; then
has_verified_sig=false
echo "$verify_sig" | grep -e '^\[GNUPG:\] TRUST_UNDEFINED' >/dev/null || has_verified_sig=true
fi
## 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 -oE "^gpg:\s+ using .* key [0-9A-Fa-f]{16}" | \
sed -e 's/^.* \([0-9A-Fa-f]\{16\}$\)/\1/' )
fpr_signing_keyid=$(gpg --fingerprint "$signing_keyid" 2>/dev/null | \
grep -o -E "([0-9A-F]{4}[[:space:]]{0,2}){10}")
case "$ref","$commit_type" in
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 [ "$has_good_sig" -ne 0 ]; then
echo "*** Bad signature on commit $commit" >&2
exit 1
fi
if [ "$allowunverifiedsigs" != "true" ] && [ "$has_verified_sig" != "true" ]; then
echo "has_verified_sig $has_verified_sig"
echo "*** Unverified 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/master,merge)
## only allow merges to master from release-* and develop/*
if [ "$restrictmergetomaster" = "true" ] && test -z "$is_from_develop" -a -z "$is_from_release"; then
echo "*** Branch master only takes merge commits originating from develop/* or release-* branches" >&2
exit 1
else
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
fi
;;
refs/heads/*,merge)
## merge into non-master branch
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/master,delete)
# delete branch
if [ "$allowdeletebranch" != "true" ]; then
echo "*** Deleting master is not allowed in this repository" >&2
exit 1
fi
;;
refs/heads/master,*)
## kill it with fire
echo "*** Branch master only takes merge commits originating from develop/* or release-* branches" >&2
exit 1
;;
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