#!/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 die() { echo "$@" >&2; exit 1; } 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 exit 1 } 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_committer_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 # ------- # 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 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" T0="$SERVER_NONCE_TIME" T1="$PUSHER_SIGN_TIME" T2="$SERVER_PUSH_TIME" DELTA_T1_T0=$(( T1 - T0 )) DELTA_T2_T0=$(( T2 - T0 )) DELTA_T2_T1=$(( T2 - T1 )) [[ $DELTA_T1_T0 -lt 0 ]] && DELTA_T1_T0=$(( DELTA_T1_T0 * -1 )) [[ $DELTA_T2_T0 -lt 0 ]] && DELTA_T2_T0=$(( DELTA_T2_T0 * -1 )) [[ $DELTA_T2_T1 -lt 0 ]] && DELTA_T2_T1=$(( DELTA_T2_T1 * -1 )) CLOCK_DRIFT_LIMIT=5 PUSH_LIMIT=60 if [[ $DELTA_T1_T0 -ge $CLOCK_DRIFT_LIMIT ]]; then warn "Your system clock is off by $DELTA_T1_T0 seconds (limit $CLOCK_DRIFT_LIMIT)" die "Run NTP, rebase your commits as needed, and push again." fi if [[ $DELTA_T2_T0 -ge $PUSH_LIMIT ]]; then die "Try again! Your push took $DELTA_T2_T0 seconds, (limit $PUSH_LIMIT)." fi } # ---------------------------------------------------------------------- # Send info to syslog for debugging log_git_push case ${VERIFY_SIGS} in gentoo-devs) if [[ ${GL_USER} != *@gentoo.org ]]; then echo "*** Pusher address is not @gentoo.org" >&2 echo " (it is ${GL_USER})" >&2 echo "*** Please report this to infra" >&2 exit 1 fi # find key fingerprints in LDAP KEY_FPS=( $(ldapsearch "uid=${GL_USER%@gentoo.org}" -D '' -Z -LLL \ gpgfingerprint -o ldif-wrap=no | \ 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) ;; *) echo "Invalid value of gentoo.verify-signatures" >&2 exit 1 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 user clock as well. verify_committer_clock # All good now exit 0