#!/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 --force, override some checks and continue on errors --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' Some parameters can also be set via environment variables, see the defaults section in the script for more details. EOF } ########################################################################## # defaults DO_PUSH=${DO_PUSH:-0} DO_SQUASH=${DO_SQUASH:-1} AUTH_METHOD=${AUTH_METHOD:-''} MNTNER=${MNTNER:-''} SSH_KEYFILE=${SSH_KEYFILE:-''} SSH_METHOD=${SSH_METHOD:-'git'} GPG_PRINT=${GPG_PRINT:-''} VERIFY_ONLY=${VERIFY_ONLY:-0} COMMIT_SHA=${COMMIT_SHA:-''} FORCE=${FORCE:-0} ########################################################################## # 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" ;; --force) FORCE=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 ' ' | tail -n 1) 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 ' ' | tail -n 1) 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: Detected ### DN42 Signature header in commit comment' >&2 echo 'ERROR: The commit appears to be signed already' if [ "$FORCE" -ne 0 ] then >&2 echo '!!! ignoring errors, continuing' else >&2 echo '---' >&2 git log -n 1 --show-signature >&2 echo '---' >&2 echo 'If you want to re-try signing the commit then you must either' >&2 echo 're-run this script again using the --force option, or' >&2 echo 'manually remove the "### DN42 Signature" header from the commit' >&2 echo 'comment (e.g. using git commit --amend)' exit 1 fi 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: Found existing git signature' >&2 echo 'ERROR: The last commit appears to be signed already' if [ "$FORCE" -ne 0 ] then >&2 echo '!!! ignoring errors, continuing' else >&2 echo "---" >&2 git log -n 1 --show-signature >&2 echo "---" >&2 echo "You can use the --force option for re-try signing" exit 1 fi 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 ' ' | tail -n 1) 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 echo '---' verify_pgp ;; ssh|ssh-git) if [ "$VERIFY_ONLY" -ne 1 ] then echo 'Signing using SSH key' sign_ssh fi echo '---' 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