aboutsummaryrefslogtreecommitdiff
blob: eef887b48a3a6478609588277051e643cb709371 (plain)
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
#!/bin/bash
# gentoo-infra: infra/githooks.git:local/require-signed-push

VERIFY_SIGS=$(git config --get gentoo.verify-signatures)
: "${VERIFY_SIGS:=gentoo-devs}"

# ----------------------------------------------------------------------
# standard stuff
silent_die() { exit 1; }
die() { echo "$@" >&2; silent_die; }
warn() { echo "$@" >&2; }

fail_signed_push() {
	warn "$@"
	warn "Your push was not signed with a known key."
	warn "You MUST use git push --signed with a known key."
	warn "Known keys are the subkeys of all primary keys in LDAP."
	warn "If you add a new (primary) key to LDAP, please ask Infra to sync gitolite."
	warn "If you modified your key and uploaded to keyservers, please wait 4 hours for sync (SKS pool is slow, keys.gentoo.org pool is faster)"
	warn "If you haven't done either of these things, please see https://wiki.gentoo.org/wiki/Project:Gentoo-keys/Generating_GLEP_63_based_OpenPGP_keys#Next_steps"
	warn "git-receive-pack variables:"
	for var in \
		GIT_PUSH_CERT \
		GIT_PUSH_CERT_KEY \
		GIT_PUSH_CERT_NONCE \
		GIT_PUSH_CERT_NONCE_SLOP \
		GIT_PUSH_CERT_NONCE_STATUS \
		GIT_PUSH_CERT_SIGNER \
		GIT_PUSH_CERT_STATUS \
		; do
			warn "$var='${!var}'"
	done
	if [ -n "${GIT_PUSH_CERT}" ]; then
		warn "A push-cert was found, and follows:"
		warn "====="
		git --no-pager show "$GIT_PUSH_CERT"
		warn "====="
	fi
	silent_die
}

log_git_push() {
	s=""
	for var in \
		GIT_PUSH_CERT \
		GIT_PUSH_CERT_KEY \
		GIT_PUSH_CERT_NONCE \
		GIT_PUSH_CERT_NONCE_SLOP \
		GIT_PUSH_CERT_NONCE_STATUS \
		GIT_PUSH_CERT_SIGNER \
		GIT_PUSH_CERT_STATUS \
		; do
			s="${s} $var='${!var}'"
	done
	logger -t require-signed-push -p info "require-signed-push${s}"
}

verify_pusher_clock() {
	RAW_CERT="$(git show --format='pushtime %ct%nct %ct%nat %at%n%B' "$GIT_PUSH_CERT")"
	# Example inputs
	# Good clock (58 seconds delay on PIN entry to smartcard):
	# -------
	# pushtime 1468598038
	# certificate version 0.1
	# pusher 0x250B7AFED6379D85! 1468597981 +0200
	# nonce 1468597981-660c323b3137c0798711
	# -------
	# Bad clock, ~5 mins slow:
	# -------
	# pushtime 1468596921
	# certificate version 0.1
	# pusher 94BFDF4484AD142F 1468596642 +0200
	# nonce 1468596917-ac5118c996e285ace24e
	# -------
	# Bad clock, ~5 mins fast
	# -------
	# pushtime 1468596921
	# certificate version 0.1
	# pusher 94BFDF4484AD142F 1468597242 +0200
	# nonce 1468596917-ac5118c996e285ace24e
	# -------

	# This is the time, according to server clock, that the server sent out to the user
	# also in GIT_PUSH_CERT_NONCE
	SERVER_NONCE_TIME="$(echo "$RAW_CERT" | awk '/^nonce /{sub("-.*","",$2); print $2}' )"
	# This is the time, according to USER clock, that the response was signed
	# We want the second to last field, because sometimes there are spaces!
	# pusher 94BFDF4484AD142F 1468596642 +0200
	# pusher Michael Palimaka <kensington@gentoo.org> 1468524562 +1000
	PUSHER_SIGN_TIME="$(echo "$RAW_CERT" | awk '/^pusher /{s=$(NF-1); print s}')"
	# This is the time, according to server clock, when the server got the response
	SERVER_PUSH_TIME="$(echo "$RAW_CERT" | awk '/^pushtime /{print $2}')"
	# But it might not be available...
	[[ -z "${SERVER_PUSH_TIME}" ]] && SERVER_PUSH_TIME=$(date +%s)

	[[ -z "$SERVER_NONCE_TIME" ]] && die "require-signed-push: Could not find push nonce"
	[[ -z "$PUSHER_SIGN_TIME" ]] && die "require-signed-push: Could not find pusher identity"

	# Timestamps:
	# T0: nonce generation time (server, trusted input)
	# T1: nonce signature START time (client, untrusted input!)
	# T2: signed-nonce receive time (server, trusted input)
	T0="$SERVER_NONCE_TIME"
	T1="$PUSHER_SIGN_TIME"
	T2="$SERVER_PUSH_TIME"
	# Durations:
	# T1-T0: how long it took the client to get the nonce and sign it (depends on untrusted input)
	# - will be negative if the client (T1) has a clock BEHIND of server (T0),
	#   e.g. client clock is "slow"
	#
	# T2-T0: how long the roundtrip took (only contains trusted inputs)
	# - will only be negative if the server clock jump backwards during the round-trip!
	#
	# T2-T1: how long it took the client to sign their timestamp & nonce and
	#        send it back (depends on untrusted input)
	# - will be negative if the client (T1) has a clock AHEAD of server (T2),
	#   e.g. clock is "fast"
	# - MAY contain delay from smartcards/tokens requiring interaction.
	DELTA_T1_T0=$(( T1 - T0 ))
	DELTA_T2_T0=$(( T2 - T0 ))
	DELTA_T2_T1=$(( T2 - T1 ))

	# Flip the signs, because we care about magnitude, not if they are fast or slow.
	[[ $DELTA_T1_T0 -lt 0 ]] && DELTA_T1_T0=$(( DELTA_T1_T0 * -1 ))
	[[ $DELTA_T2_T1 -lt 0 ]] && DELTA_T2_T1=$(( DELTA_T2_T1 * -1 ))
	# This one should never happen unless the server's clock has gone backwards during the round trip period.
	[[ $DELTA_T2_T0 -lt 0 ]] && die "Server clock moved backwards during process, please report to infra@ and retry!"

	CLOCK_DRIFT_LIMIT=30
	PUSH_LIMIT=60
	_die=0
	# Put the stricter check first, otherwise the weaker check will never be seen.
	if [[ $DELTA_T2_T0 -ge $PUSH_LIMIT ]]; then
		warn "Push roundtrip took too long (push-nonce): $DELTA_T2_T0 sec vs limit $PUSH_LIMIT"
		_die=1
	fi
	if [[ $DELTA_T1_T0 -ge $CLOCK_DRIFT_LIMIT ]]; then
		warn "Push certificate time is too skew (sign-nonce)."
		warn "It's possible your system clock is off by up to $DELTA_T1_T0 seconds vs limit $CLOCK_DRIFT_LIMIT"
		warn "Run NTP, pull & rebase your commits if needed, and push again."
		_die=1
	fi
	if [[ $_die -eq 1 ]]; then
		warn "---cut-here---"
		git show --format='pushtime %ct%nct %ct%nat %at%n%B' "$GIT_PUSH_CERT" 1>&2
		warn "---cut-here---"
		die "Time issues during git-push"
	fi
}

# ----------------------------------------------------------------------
# Send info to syslog for debugging
log_git_push

case ${VERIFY_SIGS} in
	gentoo-devs)
		if [[ ${GL_USER} != *@gentoo.org ]]; then
			warn "*** Pusher address is not @gentoo.org" >&2
			warn "    (it is ${GL_USER})" >&2
			warn "*** Please report this to infra" >&2
			silent_die
		fi

		# find key fingerprints in LDAP
		mapfile -t KEY_FPS < <( \
			ldapsearch -o ldif-wrap=no -x -D '' -Z -LLL \
					"uid=${GL_USER%@gentoo.org}" \
					gpgfingerprint \
			| sed -n -e '/^gpgfingerprint: /{s/^.*://;s/ //g;p}'\
		)
		# match signing key to the primary key
		PRIMARY_KEY=$(gpg --batch --with-colons --fingerprint "${GIT_PUSH_CERT_KEY}" \
			| sed -n -e '/^pub/{n;/^fpr/p}' | cut -d: -f10)
		if [[ -z ${PRIMARY_KEY} ]]; then
			fail_signed_push "Unable to identify primary key used for push"
		fi

		# primary key must match one of the fingerprints in LDAP
		if [[ " ${KEY_FPS[*]} " != *" ${PRIMARY_KEY} "* ]]; then
			fail_signed_push "Key used to sign the commit does not belong to ${GL_USER}"
		fi
		;;
	no)
		;;
	*)
		die "Invalid value of gentoo.verify-signatures"
esac

# Now validate
# see git-log(1) %G
# 2020/04/06: BGUXYREN
case $GIT_PUSH_CERT_STATUS in
	# Good
	G) ;;

	# signature itself has expired
	X) fail_signed_push "FAIL: push certificate signature is expired" ;;

	# key is expired, but the good signature is otherwise good
	Y) fail_signed_push "FAIL: key used for push certificate is expired" ;;

	# good signature made by an revoked key
	R) fail_signed_push "FAIL: key used for push certiticate is revoked" ;;

	# Bad
	B) fail_signed_push "FAIL: signature on push certificate is bad" ;;

	# Untrusted good
	U) ;; # TODO: deny this later
	#U) fail_signed_push "Good but untrusted signature" ;;

	# No signature
	N)
		if [ -z "$GIT_PUSH_CERT" ]; then
			fail_signed_push "FAIL: no push certificate found"
		else
			fail_signed_push "FAIL: push certificate with no signature" # wtf?
		fi
		;;

	# Can't verify -- usually means unknown key
	E)
		if [[ ${VERIFY_SIGS} != no ]]; then
			fail_signed_push "FAIL: Unknown OpenPGP key used for push certificate"
		fi
		;;

	# Future-proof
	*) fail_signed_push "FAIL: Unknown GIT_PUSH_CERT_STATUS" ;;

esac

# Check the clock on pusher system as well.
verify_pusher_clock

# All good now
exit 0