#!/bin/sh ########################################################################## # # This script will attempt to sign your commit using one of the # authentication methods in your mntner record. # # If the script fails to work, PRs for fixes are always welcome # and you can always sign your commit manually as detailed in the # DN42 wiki: https://dn42.dev/howto/Registry-Authentication # # do './sign-my-commit --help' to get usage information # ########################################################################## usage() { cat <, check the signature on a specific commit --help, display this message PGP specific options: --print , specify fingerprint of GPG key to use if you don't want to use the first available key SSH specific options: --key , specify SSH key file to use if not using ssh-agent or want to use a different key than the first available (this can be a public or private keyfile) --method , either 'git' or 'comment' to force SSH signatures to use a specific method, defaults to 'git' EOF } ########################################################################## # defaults DO_PUSH=0 DO_SQUASH=1 AUTH_METHOD='' MNTNER='' SSH_KEYFILE='' SSH_METHOD='git' GPG_PRINT='' VERIFY_ONLY=0 COMMIT_SHA='' ########################################################################## # parse arguments while [ -n "$1" ] do case "$1" in --pgp) AUTH_METHOD='pgp' ;; --ssh) AUTH_METHOD='ssh' ;; --push) DO_PUSH=1 ;; --no-squash) DO_SQUASH=0 ;; --verify) VERIFY_ONLY=1 ;; --commit) shift VERIFY_ONLY=1 COMMIT_SHA="$1" ;; --key) shift SSH_KEYFILE="$1" ;; --method) shift SSH_METHOD="$1" ;; --print) shift GPG_PRINT="$1" ;; --help) usage exit 0 ;; *) if [ -z "$MNTNER" ] then MNTNER=$1 else >&2 echo "ERROR: Unknown option: $1" >&2 usage exit 1 fi ;; esac shift done ########################################################################## # perform some initial sanity checks # check working directory if [ ! -d '.git' ] && [ ! -d 'data/mntner' ] then >&2 echo "ERROR: This script must be run in the root directory of a registry clone" exit 1 fi # fill in the last commit if it wasn't specified already if [ -z "$COMMIT_SHA" ] then COMMIT_SHA=$(git log -n 1 --format=format:%H) fi # reset local git configuration git config --local --unset gpg.format git config --local --unset user.signingkey git config --local --unset gpg.ssh.allowedSignersFile # if verifying only, try to guess some info from the existing sig if [ "$VERIFY_ONLY" -eq 1 ] then if [ -z "$MNTNER" ] then MNTNER=$(git log "$COMMIT_SHA" -n 1 --format=format:%B | \ grep '^### mntner:' | \ cut -d':' -f2 | tr -d ' ') if [ -n "$MNTNER" ] then echo "Found mntner $MNTNER from signature" fi fi if [ -z "$AUTH_METHOD" ] then AUTH_METHOD=$(git log "$COMMIT_SHA" -n 1 --format=format:%B | \ grep '^### method:' | \ cut -d':' -f2 | tr -d ' ') if [ -n "$AUTH_METHOD" ] then echo "Using $AUTH_METHOD auth method from signature" fi fi fi # check that a mntner has been specified and exists if [ -z "${MNTNER}" ] then usage exit 1 fi if [ ! -f "data/mntner/${MNTNER}" ] then >&2 echo "ERROR: mntner '${MNTNER}' not found" exit 1 fi # figure out the git version gitv_major=$(git --version | cut -d' ' -f3 | cut -d'.' -f1) gitv_minor=$(git --version | cut -d'.' -f2) # the script needs at least git 2.5 if { [ "$gitv_major" -eq 2 ] && [ "$gitv_minor" -lt 5 ]; } || \ [ "$gitv_major" -lt 2 ] then >&2 echo "ERROR: This script requires a git version 2.5" >&2 echo "---" >&2 git --version exit 1 fi # if signing, check the repo is ready if [ "$VERIFY_ONLY" -ne 1 ] then # check for untracked or uncommitted changes if [ -n "$(git status --porcelain)" ] then >&2 echo "ERROR: git worktree has unstaged or uncommitted changes" >&2 echo "This script can only be run once your commit is completed" >&2 echo "---" >&2 git status exit 1 fi # check that the commit has been squashed if [ "$DO_SQUASH" -eq 1 ] then if ! ./squash-my-commits --verify then >&2 echo "ERROR: Ensure your commits are squashed before signing" >&2 echo "Run the included script: ./squash-my-commits" exit 1 fi fi # check for an existing signature if git log -n 1 --format=format:%B 2>&1 | grep '^### DN42 Signature' > /dev/null then >&2 echo "ERROR: The last commit appears to already be signed" >&2 echo "---" >&2 git log -n 1 --show-signature exit 1 fi fi ########################################################################## # helper functions # guess a signature method based on the first auth attribute in a MNTNER guess_mntner_method() { method=$(grep '^auth:' "data/mntner/${MNTNER}" | head -n 1 | cut -c21- | cut -d' ' -f1) case "$method" in pgp-fingerprint|PGPKEY-*) echo 'pgp' ;; ssh-*|sk-ssh-*|ecdsa-*|sk-ecdsa-*) echo "ssh" ;; '') >&2 echo "ERROR: Unable to find any auth attributes for $MNTNER" exit 1 ;; *) >&2 echo "ERROR: Unknown or unimplemented auth method '$method'" >&2 echo 'Check the auth attribute is actually supported ' >&2 echo 'or specify the signature type manually.' exit 1 ;; esac } ########################################################################## # # PGP Section - functions for signing and verify PGP signatures # ########################################################################## # PGP Helper functions # create a list of authorised PGP fingerprints get_pgp_prints() { pgp_prints=$(mktemp) # cut auth methods from mntner grep '^auth:' "data/mntner/${MNTNER}" | cut -c21- | \ while read -r auth_method auth_data do case "$auth_method" in pgp-fingerprint) # use the fingerprint directly echo "$auth_data" | \ tr '[:lower:]' '[:upper:]' >> "$pgp_prints" ;; PGPKEY-*) if [ ! -f "data/key-cert/$auth_method" ] then >&2 echo "ERROR: failed to find key-cert object: $auth_method" rm "$pgp_prints" exit 1 fi # get the fingerprint from key-cert file grep '^fingerpr:' "data/key-cert/$auth_method" | \ cut -c21- | tr -d ' ' | \ tr '[:lower:]' '[:upper:]' >> "$pgp_prints" ;; esac done if [ ! -s "$pgp_prints" ] then >&2 echo "ERROR: failed to find any pgp fingerprints for $MNTNER" rm "$pgp_prints" exit 1 fi echo "$pgp_prints" } ########################################################################## # PGP signing function sign_pgp() { # check first if there is already a signature if git log -n 1 --show-signature | grep "^gpg" > /dev/null 2>&1 then >&2 echo "ERROR: The last commit appears to already be signed." >&2 echo "---" >&2 git log -n 1 --show-signature exit 1 fi # if the fingerprint wasn't specified, obtain from the MNTNER if [ -z "$GPG_PRINT" ] then pgp_prints=$(get_pgp_prints) GPG_PRINT=$(head -n 1 "$pgp_prints") rm "$pgp_prints" fi echo "PGP signing using fingerprint: $GPG_PRINT" # configure local git for pgp signing git config --local --unset gpg.format git config --local user.signingKey "$GPG_PRINT" # create a new comment with some additional metadata comment="$(git log -n 1 --format=format:%B) ### DN42 Signature ### method: pgp ### mntner: $MNTNER " # PGP signing is straightforward if ! git commit --amend --no-edit -S -m "$comment" then >&2 echo "ERROR: failed to sign commit" exit 1 fi # update the COMMIT_SHA for the verification phase COMMIT_SHA=$(git log -n 1 --format=format:%H) } ########################################################################## # verify PGP signature verify_pgp() { echo "Verifying PGP signature" # requires git 2.5 if ! git verify-commit "$COMMIT_SHA" then >&2 echo "ERROR: failed to verify PGP signature" exit 1 fi echo " - PGP signature verified ok" # create a list of authorised pgp fingerprints valid_prints=$(get_pgp_prints) # extract the fingerprint of the key that was successful prints=$(git verify-commit --raw "$COMMIT_SHA" 2>&1 | \ grep "VALIDSIG" | cut -f3,12 -d' ') for print in $prints do if grep "$print" "$valid_prints" > /dev/null 2>&1 then echo "Matched fingerprint with auth attribute for $MNTNER" echo "Successfully verified PGP signature" rm "$valid_prints" return fi done >&2 echo "ERROR: unable to match key fingerprint with mntner: $MNTNER" rm "$valid_prints" exit 1 } ########################################################################## # # SSH Section - functions for signing and verify SSH signatures # ########################################################################## # SSH helper functions # return only ssh auth methods for mntner filter_ssh_auths() { grep '^auth:' "data/mntner/${MNTNER}" | cut -c21- | \ while read -r line do case "$line" in ssh-*|sk-ssh-*|ecdsa-*|sk-ecdsa-*) echo "$line" ;; esac done } # create an allowed signers file using the mntner auth attributes get_allowed_signers() { allowed=$(mktemp) filter_ssh_auths | sed "s/^/${MNTNER} /" > "$allowed" echo "$allowed" } # try and find a suitable keyfile that we can sign with check_keyfile() { pubkeyfile='' # guess the public key if a keyfile wasn't specified if [ -z "$SSH_KEYFILE" ] then pubkeyfile=$(mktemp) echo "Obtaining public key from $MNTNER auth attributes" # get the public key from mntner auth records filter_ssh_auths | head -n 1 > "$pubkeyfile" if [ ! -s "$pubkeyfile" ] then >&2 echo "ERROR: Unable to auto determine SSH public key" >&2 echo "Try specifying the key directly using --key" rm "$pubkeyfile" exit 1 fi # check if the pubkey is available in agent pubkey=$(tr -s ' ' < "$pubkeyfile" | cut -d' ' -f1,2) if ssh-add -L | grep "^$pubkey" > /dev/null 2>&1 then # key was found in agent ok SSH_KEYFILE="$pubkeyfile" else # no key found in agent, clean up the keyfile first rm "$pubkeyfile" pubkeyfile='' if [ -d "${HOME}/.ssh" ] then # as a last resort, try scanning the 'usual' ssh # directory to find the key in there SSH_KEYFILE=$(grep -l "^$pubkey" "${HOME}"/.ssh/*.pub) fi if [ -n "$SSH_KEYFILE" ] then >&2 echo "Found SSH key in: $SSH_KEYFILE" else # all attempts failed >&2 cat <&2 echo "ERROR: $SSH_KEYFILE doesn't look like a valid SSH key" >&2 echo "Try specifying the public or private key directly using --key" >&2 echo "File contents:" >&2 cat "$SSH_KEYFILE" if [ -n "$pubkeyfile" ]; then rm "$pubkeyfile"; fi exit 1 fi echo "Using: $pubkey" } ########################################################################## # SSH signing function # SSH signature using git signatures sign_ssh_git() { check_keyfile # configure local git signing git config --local gpg.format ssh git config --local user.signingKey "$SSH_KEYFILE" # create a new comment with some additional metadata comment="$(git log -n 1 --format=format:%B) ### DN42 Signature ### method: ssh-git ### mntner: $MNTNER " # the signature is now straightforward git commit --amend --no-edit -S -m "$comment" result=$? # clean up pubkeyfile first if [ -n "$pubkeyfile" ]; then rm "$pubkeyfile"; fi # was there an error ? if [ "$result" -ne 0 ] then >&2 echo "ERROR: failed to sign commit" >&2 echo " - Try specifying your key using --key" >&2 echo " - or adding your key to ssh-agent" exit 1 fi # update the COMMIT_SHA for the verification phase COMMIT_SHA=$(git log -n 1 --format=format:%H) } # SSH signature by adding in to the comment sign_ssh_comment() { check_keyfile # create the signature sig=$(echo "$COMMIT_SHA" | \ ssh-keygen -Y sign -n dn42 -f "$SSH_KEYFILE") result=$? # clean up pubkeyfile first if [ -n "$pubkeyfile" ]; then rm "$pubkeyfile"; fi # check for errors if [ "$result" -ne 0 ] then >&2 echo "ERROR: ssh-keygen signing failed" >&2 echo " - Try specifying your key using --key" >&2 echo " - or adding the key to ssh-agent" if [ -n "$pubkeyfile" ]; then rm "$pubkeyfile"; fi exit 1 fi # create a comment including the signature comment="$(git log -n 1 --format=format:%B) ### DN42 Signature ### method: ssh ### mntner: $MNTNER ### text: $COMMIT_SHA $sig " # update the commit with the sig git commit --amend --no-edit -m "$comment" # update the COMMIT_SHA for the verification phase COMMIT_SHA=$(git log -n 1 --format=format:%H) } sign_ssh() { # check for ssh-keygen signing capability if ! ssh-keygen -Y sign 2>&1 | grep 'missing namespace' > /dev/null then >&2 cat <&2 ssh -V exit 1 fi # if we have git >= 2.34 the commit can be git signed if [ "$SSH_METHOD" != "comment" ] then if { [ "$gitv_major" -eq 2 ] && [ "$gitv_minor" -ge 34 ]; } || \ [ "$gitv_major" -gt 2 ] then echo "Detected git version >= 2.34, using git SSH signature" sign_ssh_git return else echo "Detected git version < 2.34, cannot sign using git" fi fi echo "Defaulting to comment based signature" sign_ssh_comment } ########################################################################## # verify SSH signature # verify a git based SSH signature verify_ssh_git() { echo "Verifying SSH signature in git" # check git version if { [ "$gitv_major" -eq 2 ] && [ "$gitv_minor" -lt 34 ]; } || \ [ "$gitv_major" -lt 2 ] then >&2 echo "Detected git version < 2.34, unable to verify git signatures" >&2 echo "- Upgrade git to at least version 2.34" exit 1 fi # create an allowed signers file and configure it in git allowed=$(get_allowed_signers) git config --local gpg.ssh.allowedSignersFile "$allowed" # signature can now be verified similar to pgp case # find the current commit hash git verify-commit "$COMMIT_SHA" result=$? # clean up allowed signers file before doing anything else git config --local --unset gpg.ssh.allowedSignersFile rm "$allowed" # did the signature successfully validate ? if [ "$result" -ne 0 ] then >&2 echo "ERROR: failed to verify SSH signature" exit 1 fi echo "SSH signature verified ok" } # verify a comment based SSH signature verify_ssh_comment() { echo "Verifying SSH signature comment" # create the allowed signers file allowed=$(get_allowed_signers) # extract the text that was signed from the git comment text=$(git log "$COMMIT_SHA" -n 1 --format=format:%B | \ grep '^### text:' | cut -d':' -f2 | tr -d ' ') # also extract the SSH signature from the comment signature=$(mktemp) begin="-----BEGIN SSH SIGNATURE-----" end="-----END SSH SIGNATURE-----" git log "$COMMIT_SHA" -n 1 --format=format:%B | \ sed "/^$begin\$/,/^$end\$/!d" > "$signature" # now we can verify the signature echo "$text" | ssh-keygen -Y verify -f "$allowed" \ -n dn42 -I "$MNTNER" -s "$signature" # grab the result and clean up before doing anything else result=$? rm "$allowed" "$signature" # did it work ? if [ "$result" -eq 0 ] then echo "Successfully verified SSH sigature" else >&2 echo "ERROR: signature verification failed" exit 1 fi } # SSH verify wrapper verify_ssh() { # determine signature type from log comment method=$(git log "$COMMIT_SHA" -n 1 --format=format:%B | \ grep '^### method:' | cut -d':' -f2 | tr -d ' ') case "$method" in 'ssh') verify_ssh_comment ;; 'ssh-git') verify_ssh_git ;; '') echo "WARNING: No dn42 signature found, attempting git based verification" verify_ssh_git ;; *) >&2 echo "ERROR: commit does not appear to be signed by SSH" >&2 echo "Found signature method: $method" exit 1 ;; esac } ########################################################################## # # Script body - the script resumes here # ########################################################################## if [ -z "$AUTH_METHOD" ] then if [ "$VERIFY_ONLY" -ne 1 ] then echo "Attempting to guess signature method from mntner object" AUTH_METHOD=$(guess_mntner_method) fi fi # decide what to do case "$AUTH_METHOD" in pgp) if [ "$VERIFY_ONLY" -ne 1 ] then echo "Signing using PGP key" sign_pgp fi verify_pgp ;; ssh|ssh-git) if [ "$VERIFY_ONLY" -ne 1 ] then echo "Signing using SSH key" sign_ssh fi verify_ssh ;; '') >&2 echo "ERROR: Unable to automatically determine signing method" >&2 echo "Use the --ssh or --pgp options to force a particular method" exit 1 ;; *) >&2 echo "ERROR: Unknown or unimplemented auth method: $AUTH_METHOD" >&2 echo "Use the --ssh or --pgp options to force a particular method" exit 1 ;; esac ########################################################################## # all done, tidy up if [ "$VERIFY_ONLY" -eq 1 ] then exit 0 fi # push changes if requested if [ "$DO_PUSH" -eq 1 ] then echo 'Force pushing changes' git push --force else echo '---' echo 'Remember to push your changes using: git push --force' echo '---' fi exit 0 ########################################################################## # end of file