#!/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()
{
    echo "Usage: $0 [options] MNTNER"
    echo 'Generic options:'
    echo ' --pgp,    sign using your PGP key'
    echo ' --ssh,    sign using your ssh key'
    echo ' --push,   force push result'
    echo ' --verify, check existing signature is correct'
    echo ' --commit, verify this specific commit'
    echo ' --help,   display this message'
    echo 'SSH specific options:'
    echo ' --key,    (required for signing) specify SSH private key file to use'
}

##########################################################################
# defaults

DO_PUSH=0
AUTH_METHOD=''
MNTNER=''
SSH_KEYFILE=''
VERIFY_ONLY=0
COMMIT_SHA=''

##########################################################################
# parse arguments

while [ -n "$1" ]
do
    case "$1" in

        --pgp)
            AUTH_METHOD='pgp'
            ;;
        --ssh)
            AUTH_METHOD='ssh'
            ;;
        --force)
            DO_PUSH=1
            ;;
        --key)
            shift
            SSH_KEYFILE="$1"
            ;;
        --verify)
            VERIFY_ONLY=1
            ;;
        --commit)
            shift
            COMMIT_SHA="$1"
            ;;
        --help)
            usage
            exit 0
            ;;
        *)
            if [ -z "$MNTNER" ]
            then
                MNTNER=$1
            else
                echo "ERROR: Unknown option: $1"
                usage
                exit 1
            fi
            ;;
        
    esac

    shift
done

##########################################################################
# initial sanity checks

# 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
    echo "ERROR: mntner '${MNTNER}' not found"
    exit 1
fi

if [ "$VERIFY_ONLY" -ne 1 ]
then

    # check for untracked or uncommitted changes
    if [ -n "$(git status --porcelain)" ]
    then
        echo "ERROR: git worktree has unstaged or uncommitted changes"
        echo "This script can only be run once your commit is completed"
        echo "---"
        git status
        exit 1
    fi
    
    # check that the commit has been squashed
    ./squash-my-commits --verify 
    if [ $? -ne 0 ]
    then
        echo "ERROR: Ensure your commits are squashed before signing"
        echo "Run the included script: ./squash-my-commits"
        exit 1
    fi
    
    # check for an existing signature
    git log -n 1 --format=format:%B 2>&1 | grep '^### DN42 Signature' > /dev/null
    if [ $? -eq 0 ]
    then
        echo "ERROR: The last commit appears to already be signed"
        echo "---"
        git log -n 1 --show-signature
        exit 1
    fi
    
fi

##########################################################################
# helper functions

guess_auth_method()
{
    # look for the first auth method in the mntner object
    method=$(grep '^auth' "data/mntner/${MNTNER}" | \
                 head -n 1 | tr -s ' ' | cut -d' ' -f2 | cut -d'-' -f1)
    # didn't find anything ?
    if [ -z "$method" ]
    then
        echo "Unable to find mntner auth method for ${MNTNER}"
        echo "Try specifying the method manually"
        usage
        exit 1
    fi

    case "$method" in
        pgp)
            AUTH_METHOD='pgp'
            ;;
        PGPKEY)
            AUTH_METHOD='pgp'
            ;;
        ssh)
            AUTH_METHOD='ssh'
            ;;
        *)
            echo "ERROR: Auth method is unknown or unimplemented"
            exit 1
    esac
}


##########################################################################
# PGP signing function

sign_pgp()
{
    # check first if there is already a signature
    git log -n 1 --show-signature | grep "^gpg" > /dev/null 2>&1
    if [ $? -eq 0 ]
    then
        echo "ERROR: The last commit appears to already be signed."
        echo "---"
        git log -n 1 --show-signature
        exit 1
    fi

    # 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
    git commit --amend --no-edit -S -m "$comment"

    # assuming it's actually configured properly ....
    if [ $? -ne 0 ]
    then
        echo "ERROR: failed to sign commit"
        echo "Have you configured git with your PGP key ?"
        echo "For example, to configure your key globally:"
        echo " - Find your key using: gpg --list-keys"
        echo " - Then add it to git:  " \
             "git config --global user.signingkey <FPRINT>"
        exit 1
    fi
}

##########################################################################
# verify PGP signature

verify_pgp()
{
    echo "Verifying PGP signature"

    # find the current commit hash
    hash=$(git log ${COMMIT_SHA} -n 1 --format=format:%H)

    # requires git 2.5
    git verify-commit "$hash"
    if [ $? -ne 0 ]
    then
        echo "ERROR: failed to verify PGP signature"
        exit 1
    fi
    echo " - PGP signature verified ok"

    # extract the fingerprint of the key that was successful
    prints=$(git verify-commit --raw "$hash" 2>&1 | \
                 grep "VALIDSIG" | cut -f3,12 -d' ')
    for print in $prints
    do
        grep "^auth" data/mntner/${MNTNER} | grep -i $print > /dev/null 2>&1
        if [ $? -eq 0 ]
        then
            echo " - matched with auth attribute for $MNTNER"
            echo "Successfully verified PGP signature"
            return
        fi
    done

    echo "ERROR: unable to match key fingerprint with mntner: $MNTNER"
    exit 1
}

##########################################################################
# SSH signing function

sign_ssh()
{
    # check for ssh-keygen signing capability
    ssh-keygen -Y sign 2>&1 | grep 'missing namespace' > /dev/null
    if [ $? -ne 0 ]
    then
        echo "ERROR: This script requires the key signing capability " \
             "from OpenSSH ssh-keygen > version 8"
        echo "---"
        ssh -V
        exit 1
    fi

    if [ -z "$SSH_KEYFILE" ]
    then
        echo "ERROR: You must specify your SSH private key " \
             "using --key"
        exit 1
    fi
    
    # find the current commit hash
    hash=$(git log -n 1 --format=format:%H)

    # create the signature
    sig=$(echo "$hash" | ssh-keygen -Y sign -n dn42 -f "$SSH_KEYFILE")

    comment="$(git log -n 1 --format=format:%B)

### DN42 Signature
### method: ssh
### mntner: $MNTNER
### text: $hash 
$sig
"

    # update the commit with the sig
    git commit --amend --no-edit -m "$comment"
}

##########################################################################
# verify SSH signature

verify_ssh()
{
    echo "Verifying SSH signature"

    # create a temporary files for the 'allowed' keys file and signature
    afile=$(mktemp)
    sfile=$(mktemp)
    
    # extract and reformat the temp keys in to the allowed file
    grep '^auth' "data/mntner/${MNTNER}" | tr -s ' ' | cut -d' ' -f2- | \
        grep '^ssh-' | sed "s/^/${MNTNER} /" > "$afile"

    # extract the signed text from the git comment
    text=$(git log ${COMMIT_SHA} -n 1 --format=format:%B | grep '^### text:' |
               cut -d':' -f2 | tr -d ' ')

    # extract the SSH signature from the comment
    begin="-----BEGIN SSH SIGNATURE-----"
    end="-----END SSH SIGNATURE-----"
    git log ${COMMIT_SHA} -n 1 --format=format:%B | \
        sed "/^$begin\$/,/^$end\$/!d" > "$sfile"

    # and finally verify
    echo "$text" | ssh-keygen -Y verify -f "$afile" \
                              -n dn42 -I $MNTNER -s "$sfile"
    # grab the result and then clean up
    result=$?
    rm -f "$afile" "$sfile"

    # did it work ?
    if [ $result -eq 0 ]
    then
        echo "Successfully verified SSH sigature"
    else
        echo "ERROR: signature verification failed"
        exit 1
    fi
}


##########################################################################
# body of the script starts here

if [ -z "$AUTH_METHOD" ]
then
    echo "Attempting to guess auth method from the mntner object"
    guess_auth_method
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)
        if [ "$VERIFY_ONLY" -ne 1 ]
        then
            echo "Signing using SSH key"
            sign_ssh
        fi
        verify_ssh
    ;;
    *)
        echo "ERROR: Unknown or unimplemented auth method: $AUTH_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 'Remember to push your changes using: git push --force'
fi

exit 0

##########################################################################
# end of file
