#!/bin/sh
#
# check_hostkeydoc - Verify an IBM Secure Execution host key document
#
# Sample script to verify that a host key document is genuine by
# verifying the issuer, the validity date and the signature.
# Optionally verify the full trust chain using a CA certificate.
#
# Sample invocation:
#
# ./check_hostkeydoc HKD1234.crt ibm-z-host-key-signing.crt -c DigiCertCA.crt -r ibm-z-host-key.crl
#
# Copyright IBM Corp. 2020
#
# s390-tools is free software; you can redistribute it and/or modify
# it under the terms of the MIT license. See LICENSE for details.

# Allocate temporary files
ISSUER_PUBKEY_FILE=$(mktemp)
SIGNATURE_FILE=$(mktemp)
BODY_FILE=$(mktemp)
ISSUER_DN_FILE=$(mktemp)
SUBJECT_DN_FILE=$(mktemp)
DEF_ISSUER_ARMONK_DN_FILE=$(mktemp)
DEF_ISSUER_POUGHKEEPSIE_DN_FILE=$(mktemp)
CANONICAL_ISSUER_DN_FILE=$(mktemp)
CRL_SERIAL_FILE=$(mktemp)
CRL_ISSUER_FILE=$(mktemp)

# Cleanup on exit
cleanup()
{
    rm -f "$ISSUER_PUBKEY_FILE" "$SIGNATURE_FILE" "$BODY_FILE" \
	"$ISSUER_DN_FILE" "$SUBJECT_DN_FILE" "$DEF_ISSUER_ARMONK_DN_FILE" "$DEF_ISSUER_POUGHKEEPSIE_DN_FILE" \
	"$CANONICAL_ISSUER_DN_FILE" "$CRL_SERIAL_FILE" "$CRL_ISSUER_FILE"
}
trap cleanup EXIT

# Enhanced error checking for bash
if [ -n "${BASH}" ]; then
    # shellcheck disable=SC3040
    set -o posix
    # shellcheck disable=SC3040
    set -o pipefail
    # shellcheck disable=SC3040
    set -o nounset
fi
set -e

# Usage
usage()
{
    cat <<-EOF
Usage: $(basename "$1") [-d] [-c CA-cert] [-r CRL] host-key-doc signing-key-cert

Verify an IBM Secure Execution host key document against a signing key. Use for
resolving issues only. Use the built-in functions from pvimg, pvsecret, or
pvattest directly to verify the host-key documents. This script should only be
used as a last resort for when the distribution provided binaries have
unresolved issues regarding the host-key verification. In that case ensure the
latest version of this script is used.
Find the latest version here https://github.com/ibm-s390-linux/s390-tools

Options:
-d         disable default issuer check of host-key-doc
-c CA-cert trusted CA certificate
-r CRL     list of revoked host-key-docs

Note that in order to have the full trust chain verified
it is necessary to provide the issuing CA's certificate.
The default issuer check may be disabled if a non-default
signing key certificate needs to be verified against the
CA certificate.

EOF
}

curl_crl_by_crt()
{
	CRT="$1"
	OFFSET=$2
	CRL_URL="$(openssl x509 -noout -text -in "$CRT" \
		|grep 'URI:http.*\.crl' | sed -n "$OFFSET"p | xargs)"

	# no CRL at this offset
	if [ -z "$CRL_URL" ];
	then
		return 5
	fi
	curl --silent --output "${CRL_ISSUER_FILE}" -- "${CRL_URL#URI:}"
}

check_verify_chain()
{
    # Verify certificate chain in case a CA certificate file/bundle
    # was specified on the command line.
    if [ -z "$2" ]; then
	cat >&2 <<-EOF
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
No CA certificate specified! Skipping trust chain verification.
Make sure that '$1' is a valid certificate.
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
EOF
    else
	openssl verify -crl_download -crl_check "$2" || exit 1

	if ! stderr=$(openssl verify -crl_download -crl_check -untrusted "$2" "$1" 3>&2 2>&1 1>&3 3>&-); then
	    if ! printf '%s' "${stderr}" | grep -q 'max resp len exceeded'; then
		printf '%s\n' "${stderr}"
		exit 1
	    fi
	# Turn off exit-on-error locally to be able to retry on invalid URIs
	set +e
	off=0;
	# Search for a CRL in the CRT. If the first link does not work try
	# again with the next one until either no more URIS are available or a
	# CRL was found.
	while [ $off -le 10 ]; do
	    i=$(( i + 1 ))

	    CRL=curl_crl_by_crt "$3" $i
	    SUCCESS=$?
	    # No more CRLS available
	    if [ $SUCCESS -eq 5 ]; then
		    exit 1
	    fi
	    # URI was invalid/not reachable
	    if [ $SUCCESS -ne 0 ]; then
		    continue;
	    fi
	    # Found one
	    break
	done
	openssl verify -CRLfile "${CRL_ISSUER_FILE}" -crl_check -untrusted "$2" "$1" || exit 1
	fi
	set -e
    fi
}

extract_pubkey()
{
    openssl x509 -in "$1" -pubkey -noout >"$2"
}

extract_signature()
{
    # Assuming that the last field is the signature
    SIGOFFSET=$(openssl asn1parse -in "$1" | tail -1 | cut -d : -f 1)

    openssl asn1parse -in "$1" -out "$2" -strparse "$SIGOFFSET" -noout
}

extract_body()
{
    # Assuming that the first field is the full cert body
    SIGOFFSET=$(openssl asn1parse -in "$1" | head -2 | tail -1 | cut -d : -f 1)

    openssl asn1parse -in "$1" -out "$2" -strparse "$SIGOFFSET" -noout
}

verify_signature()
{
    # Assuming that the signature algorithm is SHA512 with RSA
    openssl sha512 -verify "$1" -signature "$2" "$3"
}

canonical_dn()
{
    OBJTYPE=$1
    OBJ=$2
    DNTYPE=$3
    OUTPUT=$4

    openssl "$OBJTYPE" -in "$OBJ" -"$DNTYPE" -noout -nameopt multiline |
	LC_ALL=C sort | grep -v "$DNTYPE"= >"$OUTPUT"
}

default_issuer_armonk()
{
    cat <<-EOF
    commonName                = International Business Machines Corporation
    countryName               = US
    localityName              = Armonk
    organizationName          = International Business Machines Corporation
    organizationalUnitName    = Key Signing Service
    stateOrProvinceName       = New York
EOF
}

default_issuer_pougkeepsie()
{
    cat <<-EOF
    commonName                = International Business Machines Corporation
    countryName               = US
    localityName              = Poughkeepsie
    organizationName          = International Business Machines Corporation
    organizationalUnitName    = Key Signing Service
    stateOrProvinceName       = New York
EOF
}

# As organizationalUnitName can have an arbitrary prefix but must
# end with "Key Signing Service" let's normalize the OU name by
# stripping off the prefix
verify_default_issuer()
{
    default_issuer_pougkeepsie >"$DEF_ISSUER_POUGHKEEPSIE_DN_FILE"
    default_issuer_armonk >"$DEF_ISSUER_ARMONK_DN_FILE"

    sed "s/\(^[ ]*organizationalUnitName[ ]*=[ ]*\).*\(Key Signing Service$\)/\1\2/" \
	"$ISSUER_DN_FILE" >"$CANONICAL_ISSUER_DN_FILE"

    if ! {
	diff "$CANONICAL_ISSUER_DN_FILE" "$DEF_ISSUER_POUGHKEEPSIE_DN_FILE" ||
	    diff "$CANONICAL_ISSUER_DN_FILE" "$DEF_ISSUER_ARMONK_DN_FILE"
    } >/dev/null 2>&1; then
	echo Incorrect default issuer >&2 && exit 1
    fi
}

verify_issuer_files()
{
    if [ "$1" -eq 1 ]; then
	verify_default_issuer
    fi
}

cert_time()
{
    DATE=$(openssl x509 -in "$1" -"$2" -noout | sed "s/^.*=//")

    date -d "$DATE" +%s
}

crl_time()
{
    DATE=$(openssl crl -in "$1" -"$2" -noout | sed "s/^.*=//")

    date -d "$DATE" +%s
}

verify_dates()
{
    START="$1"
    END="$2"
    MSG="${3:-Certificate}"
    NOW=$(date +%s)

    if [ "$START" -le "$NOW" ] && [ "$NOW" -le "$END" ]; then
	echo "${MSG} dates are OK"
    else
	echo "${MSG} date verification failed" >&2 && exit 1
    fi
}

crl_serials()
{
    openssl crl -in "$1" -text -noout |
	grep "Serial Number" >"$CRL_SERIAL_FILE"
}

check_serial()
{
    CERT_SERIAL=$(openssl x509 -in "$1" -noout -serial | cut -d = -f 2)

    grep -q "$CERT_SERIAL" "$CRL_SERIAL_FILE"
}

check_file()
{
    [ -e "$1" ] ||
	(echo "File '$1' not found" >&2 && exit 1)
}

# check args
CRL_FILE=
CA_FILE=
CHECK_DEFAULT_ISSUER=1

while getopts 'dr:c:h' opt; do
    case $opt in
    d) CHECK_DEFAULT_ISSUER=0 ;;
    r) CRL_FILE=$OPTARG ;;
    c) CA_FILE=$OPTARG ;;
    h)
	usage "$0"
	exit 0
	;;
    ?)
	usage "$0"
	exit 1
	;;
    esac
done
shift "$((OPTIND - 1))"

if [ $# -ne 2 ]; then
    usage "$0" >&2
    exit 1
fi

HKD_FILE=$1
HKSK_FILE=$2

printf "DEPRECATED SCRIPT. Use pvimg, pvattest, or pvsecret directly.\n"
printf "This script is intended for resolving issues only.\n"
printf "This script may be inaccessible in the future.\n"

# Check whether all specified files exist
check_file "$HKD_FILE"
check_file "$HKSK_FILE"
# CA and CRL are optional arguments
[ -n "$CA_FILE" ] && check_file "$CA_FILE"
[ -n "$CRL_FILE" ] && check_file "$CRL_FILE"

# Check trust chain
check_verify_chain "$HKSK_FILE" "$CA_FILE" "$HKSK_FILE"

# Verify host key document signature
printf "Checking host key document signature: "
extract_pubkey "$HKSK_FILE" "$ISSUER_PUBKEY_FILE" &&
    extract_signature "$HKD_FILE" "$SIGNATURE_FILE" &&
    extract_body "$HKD_FILE" "$BODY_FILE" &&
    verify_signature "$ISSUER_PUBKEY_FILE" "$SIGNATURE_FILE" "$BODY_FILE" ||
    exit 1

# Verify the issuer
canonical_dn x509 "$HKD_FILE" issuer "$ISSUER_DN_FILE"
canonical_dn x509 "$HKSK_FILE" subject "$SUBJECT_DN_FILE"
verify_issuer_files $CHECK_DEFAULT_ISSUER

# Verify dates
verify_dates "$(cert_time "$HKD_FILE" startdate)" "$(cert_time "$HKD_FILE" enddate)"

# Check CRL if specified
if [ -n "$CRL_FILE" ]; then
    printf "Checking CRL signature: "
    extract_signature "$CRL_FILE" "$SIGNATURE_FILE" &&
	extract_body "$CRL_FILE" "$BODY_FILE" &&
	verify_signature "$ISSUER_PUBKEY_FILE" "$SIGNATURE_FILE" "$BODY_FILE" ||
	exit 1

    printf "CRL "
    canonical_dn crl "$CRL_FILE" issuer "$ISSUER_DN_FILE"
    canonical_dn x509 "$HKSK_FILE" subject "$SUBJECT_DN_FILE"
    verify_issuer_files $CHECK_DEFAULT_ISSUER

    verify_dates "$(crl_time "$CRL_FILE" lastupdate)" "$(crl_time "$CRL_FILE" nextupdate)" 'CRL'

    crl_serials "$CRL_FILE"
    check_serial "$HKD_FILE" &&
	echo "Certificate is revoked, do not use it anymore!" >&2 &&
	exit 1
fi

# We made it
echo All checks requested for \'"$HKD_FILE"\' were successful
