#!/bin/bash

# ------------------------------------------------------------
# Company:  Software- und Organisations-Service GmbH
# Date:     2023-05-10
# Purpose:  sign JS7 workflow file
# Platform: Linux: sh
# ------------------------------------------------------------
#
# Example:  ./js7_sign_workflow.sh \
#    --keystore=/home/sos/signing.p12 \
#    --password=jobscheduler \
#    --hash=sha512 \
#    --dir=.

set -e

# ------------------------------
# Initialize variables
# ------------------------------

sign_dir=.
sign_file=
keystore_file=${JS7_SIGN_KEYSTORE}
cert_file=${JS7_SIGN_CERT}
key_file=${JS7_SIGN_KEY}
password=
hash=sha256

temp_cert_file=

hostname=$(hostname)
log_dir=
show_logs=
start_time=$(date +"%Y-%m-%dT%H-%M-%S")

Usage()
{
    >&2 echo ""
    >&2 echo "Usage: $(basename "$0") [Options] [Switches]"
    >&2 echo ""
    >&2 echo "  Options:"
    >&2 echo "    --dir=<directory>                                 | optional: path to directory with files that should be signed, default: ${sign_dir}"
    >&2 echo "    --file=<file>                                     | optional: path to file that should be signed, default: ${sign_file}"
    >&2 echo "    --keystore=<file>                                 | optional: path to keystore file in PKCS12 format, default: ${keystore_file}"
    >&2 echo "    --key=<private-key-file>                          | optional: path to private key file in .pem format, default: ${key_file}"
    >&2 echo "    --cert=<cert-file>                                | optional: path to certificate file in .pem format, default: ${cert_file}"
    >&2 echo "    --password=<password>                             | optional: password for keystore/private key file"
    >&2 echo "    --hash=<sha256|sha512>                            | optional: hash algorithm such as sha256, sha512, default: ${hash}"
    >&2 echo "    --log-dir=<directory>                             | optional: log directory for log output of this script"
    >&2 echo ""
    >&2 echo "  Switches:"
    >&2 echo "    -h | --help                                       | displays usage"
    >&2 echo "    -p | --password                                   | asks for password"
    >&2 echo "    --show-logs                                       | shows log output of the script"
    >&2 echo ""
}

Log()
{
    if [ -n "${log_file}" ] && [ -f "${log_file}" ]
    then
        echo "$@" >> "${log_file}"
    fi

    if [ -z "${show_logs}" ]
    then
        echo "$@"
    fi
}

LogError()
{
    if [ -n "${log_file}" ] && [ -f "${log_file}" ]
    then
        echo "[ERROR]" "$@" >> "${log_file}"
    fi
    
    >&2 echo "[ERROR]" "$@"
}

AskPassword() {
    password="$(
        exec < /dev/tty || exit
        tty_config=$(stty -g) || exit
        trap 'stty "$tty_config"' EXIT INT TERM
        stty -echo || exit
        printf 'Password: ' > /dev/tty
        IFS= read -r password; rc=$? 2> /dev/tty
        echo > /dev/tty
        printf '%s\n' "$password"
        exit "$rc"
    )"
}

# ------------------------------
# Process command line options
# ------------------------------

Arguments()
{
    for option in "$@"
    do
        case "${option}" in
            -h|--help)              Usage
                                    exit
                                    ;;
            --dir=*)                sign_dir=$(echo "${option}" | sed 's/--dir=//' | sed 's/^"//' | sed 's/"$//' | sed 's/^\(.*\)\/$/\1/')
                                    ;;
            --file=*)               sign_file=$(echo "${option}" | sed 's/--file=//' | sed 's/^"//' | sed 's/"$//' | sed 's/^\(.*\)\/$/\1/')
                                    ;;
            --keystore=*)           keystore_file=$(echo "${option}" | sed 's/--keystore=//' | sed 's/^"//' | sed 's/"$//' | sed 's/^\(.*\)\/$/\1/')
                                    ;;
            --key=*)                key_file=$(echo "${option}" | sed 's/--key=//' | sed 's/^"//' | sed 's/"$//' | sed 's/^\(.*\)\/$/\1/')
                                    ;;
            --cert=*)               cert_file=$(echo "${option}" | sed 's/--cert=//' | sed 's/^"//' | sed 's/"$//' | sed 's/^\(.*\)\/$/\1/')
                                    ;;
            --password=*)           password=$(echo "${option}" | sed 's/--password=//' | sed 's/^"//' | sed 's/"$//' | sed 's/^\(.*\)\/$/\1/')
                                    ;;
            --hash=*)               hash=$(echo "${option}" | sed 's/--hash=//' | sed 's/^"//' | sed 's/"$//' | sed 's/^\(.*\)\/$/\1/')
		                    ;;
            --log-dir=*)            log_dir=$(echo "${option}" | sed 's/--log-dir=//' | sed 's/^"//' | sed 's/"$//' | sed 's/^\(.*\)\/$/\1/')
                                    ;;
            --show-logs)            show_logs=1
                                    ;;
            -p|--password)          AskPassword
                                    ;;
            *)                      >&2 echo "unknown option: ${option}"
                                    Usage
                                    exit 1
                                    ;;
        esac
    done

    if [ -z "${sign_dir}" ] && [ -z "${sign_file}" ]
    then
        LogError "A directory (--dir) holding files or a file (--file) to be signed must be specified"
        Usage
        exit 1
    fi

    if [ -n "${sign_dir}" ] && [ -n "${sign_file}" ]
    then
        if [ "${sign_dir}" = '.' ]
        then
            sign_dir=
        else
            LogError "Only one of a directory (--dir) holding files or a file (--file) to be signed can be specified"
            Usage
            exit 1
        fi
    fi

    if [ -z "${key_file}" ] && [ -z "${keystore_file}" ]
    then
        LogError "A keystore (--keystore) or key file (--key) must be specified"
        Usage
        exit 1
    fi

    if [ -n "${key_file}" ] && [ -n "${keystore_file}" ]
    then
        LogError "Only one of keystore (--keystore) or key file (--key) can be specified"
        Usage
        exit 1
    fi

    if [ -z "${cert_file}" ] && [ -z "${keystore_file}" ]
    then
        LogError "A keystore (--keystore) or certificate file (--cert) must be specified"
        Usage
        exit 1
    fi

    if [ -n "${keystore_file}" ] && [ ! -f "${keystore_file}" ]
    then
        LogError "Keystore not found: --keystore=${keystore_file}"
        Usage
        exit 1
    fi

    if [ -n "${key_file}" ] && [ ! -f "${key_file}" ]
    then
        LogError "Key file not found: --key=${key_file}"
        Usage
        exit 1
    fi

    if [ -n "${cert_file}" ] && [ ! -f "${cert_file}" ]
    then
        LogError "Certificate file not found: --cert=${cert_file}"
        Usage
        exit 1
    fi

    if [ -n "${log_dir}" ]
    then
        if [ ! -d "${log_dir}" ]
        then
            LogError "Log directory not found: --log-dir="
            Usage
            exit 1
        fi
    
        log_file="${log_dir}/js7_sign_workflow.${hostname}.${start_time}.log"
        while [ -f "${log_file}" ]
        do
            sleep 1
            start_time=$(date +"%Y-%m-%dT%H-%M-%S")
            log_file="${log_dir}/js7_sign_workflow.${hostname}.${start_time}.log"
        done
        
        touch "${log_file}"
    fi

    Log "-- begin of log --------------"
    Log "$0" "$(echo "$@" | sed 's/--password=\([^--]*\)//')"
    Log "-- begin of output -----------"
}

# ------------------------------
# Main
# ------------------------------

Sign()
{
    sign_file=$1
    hash=$2
    Log ".. signing file: ${sign_file} using algorithm ${hash}"

    # step 1: create signature from file
    if [ -n "${key_file}" ]
    then
       openssl dgst -r -"${hash}" -sign "${key_file}" -passin pass:"${password}" -out "${sign_file}".sig.bin "${sign_file}"
    else
       openssl dgst -r -"${hash}" -sign "${keystore_file}" -keyform P12 -passin pass:"${password}" -out "${sign_file}".sig.bin "${sign_file}"
    fi

    # step 2: convert signature to base64
    base64 "${sign_file}".sig.bin > "${sign_file}".sig

    # step 3: prepare certificate file
    if [ -z "${cert_file}" ]
    then
        temp_cert_file=/tmp/js7_sign_workflow_$$.crt
        cert_file="${temp_cert_file}"

        if [ -n "${key_file}" ]
        then
            openssl pkcs12 -in "${key_file}" -passin pass:"${password}" -passout pass:"${password}" -nokeys -out "${temp_cert_file}" 2> /dev/null
        else
            openssl pkcs12 -in "${keystore_file}" -passin pass:"${password}" -passout pass:"${password}" -nokeys -out "${temp_cert_file}" 2> /dev/null
        fi
    fi

    # step 4: verify signature from public key
    openssl x509 -in "${cert_file}" -pubkey -noout > "${cert_file}".pub
    Log "$(openssl dgst -${hash} -verify "${cert_file}".pub -signature "${sign_file}".sig.bin "${sign_file}")"
}

Process()
{
    if [ -n "${sign_file}" ]
    then
        Sign "${sign_file}" "${hash}"
    else
        export key_file keystore_file password cert_file log_file show_logs
        export -f Log Sign Cleanup
        find "${sign_dir}" -type f \( -name "*.workflow.json" -o -name "*.jobresource.json" \) -exec /bin/bash -c "set -e; trap 'Cleanup EXIT' EXIT; Sign {} ${hash}" \;
    fi
}

# ------------------------------
# Cleanup temporary resources
# ------------------------------

Cleanup()
{
    if [ -n "${temp_cert_file}" ] && [ -f "${temp_cert_file}" ]
    then
        rm -f "${temp_cert_file}"
    fi
    
    if [ -n "${sign_file}" ] && [ -f "${sign_file}".sig.bin ]
    then
        rm -f "${sign_file}".sig.bin
    fi

    if [ -n "${cert_file}" ] && [ -f "${cert_file}".pub ]
    then
        rm -f "${cert_file}".pub
    fi
}

End()
{
    Cleanup

    if [ "$1" = "EXIT" ]
    then
        Log "-- end of log ----------------"

        if [ -n "${show_logs}" ] && [ -f "${log_file}" ]
        then
            cat "${log_file}"
        fi        
    fi

    unset sign_file
    unset sign_dir
    unset keystore_file
    unset cert_file
    unset key_file
    unset password
    unset hash

    unset hostname
    unset log_dir
    unset show_logs
    
    unset temp_cert_file
        
    set +e    
}

# ------------------------------
# Enable trap and start
# ------------------------------

trap 'End EXIT' EXIT
trap 'End SIGTERM' TERM
trap 'End SIGINT' INT

Arguments "$@"
Process
