registry/sign-my-commit

789 lines
21 KiB
Bash
Executable File

#!/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 <<EOF
This script can automatically sign commits using supported SSH or PGP
authentication methods. Remember to push the new signature to the server
after signing (using 'git push --force' or the --push option).
The script will attempt to use the first available auth method, or you
can force it to use PGP or SSH methods with the relevant options.
For SSH signatures the script may be able to find your key via ssh-agent
otherwise you must use the --key option to tell it which key to use.
Usage: $0 [options] YOUR-MNTNER-MNT
Generic options:
--pgp, force signature to use a PGP key
--ssh, force signature to use an SSH key
--push, force push the result after signature and verification
--verify, check an existing signature on the latest commit
--commit <hash>, check the signature on a specific commit
--help, display this message
PGP specific options:
--print <fprint>, specify fingerprint of GPG key to use if you
don't want to use the first available key
SSH specific options:
--key <file>, 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 <type>, 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 <<EOF
ERROR: Unable to identify public key in ssh-agent or home directory
- When auto detecting the public key from you mntner object it
- is required that the private key is available in ssh-agent or
- in your ${HOME}/.ssh/ directory.
- Please add your key to ssh-agent or use the --key option to
- specify where the private key is directly.
---
Pubkey: $pubkey
EOF
exit 1
fi
fi
fi
# try to validate the public key
if ! pubkey=$(ssh-keygen -l -f "$SSH_KEYFILE")
then
>&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 <<EOF
ERROR: This script requires the key signing capability from
OpenSSH ssh-keygen that was introduced in version 8.
If you are unable to upgrade ssh-keygen you must use one of the
manual signing methods detailed in the dn42 wiki:
https://dn42.dev/howto/Registry-Authentication
---
EOF
>&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