#!/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 certficate.
#
# 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_DN_FILE=$(mktemp)
CANONICAL_ISSUER_DN_FILE=$(mktemp)
CRL_SERIAL_FILE=$(mktemp)

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

# Enhanced error checking for bash
if [ -n "${BASH}" ]
then
    set -o posix
    set -o pipefail
    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.

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
}

check_verify_chain()
{
    # Verify certificate chain in case a CA certificate file/bundle
    # was specified on the command line.
    if [ $# = 1 ]
    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 &&
        openssl verify -crl_download -crl_check -untrusted $2 $1 ||
        exit 1
    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 \
        | sort | grep -v $DNTYPE= > $OUTPUT
}

default_issuer()
{
    cat <<-EOF
    commonName                = International Business Machines Corporation
    countryName               = US
    localityName              = Poughkeepsie
    organizationalUnitName    = Key Signing Service
    organizationName          = International Business Machines Corporation
    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 > $DEF_ISSUER_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_DN_FILE
    then
        echo Incorrect default issuer >&2 && exit 1
    fi
}

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

    if diff $ISSUER_DN_FILE $SUBJECT_DN_FILE
    then
        echo Issuer verification OK
    else
        echo Issuer verification failed >&2 && exit 1
    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 -a $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()
{
    [ $# = 0 ] ||
    [ -e "$1" ] ||
    (echo "File '$1' not found" >&2 && exit 1)
}

# check args
CRL_FILE=
CA_FILE=
CHECK_DEFAULT_ISSUER=1

args=$(getopt -qu "dr:c:h" $*)
if [ $? = 0 ]
then
    set -- $args
    while [ $1 != "" ]
    do
        case $1 in
            -d) CHECK_DEFAULT_ISSUER=0; shift;;
            -r) CRL_FILE=$2; shift 2;;
            -c) CA_FILE=$2; shift 2;;
            -h) usage $0; exit 0;;
            --) shift; break;;
        esac
    done
else
    usage $0 >&2
    exit 1
fi

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

HKD_FILE=$1
HKSK_FILE=$2

# Check whether all specified files exist
check_file $HKD_FILE
check_file $HKSK_FILE
check_file $CA_FILE
check_file $CRL_FILE

# Check trust chain
check_verify_chain $HKSK_FILE $CA_FILE

# Verify host key document signature
echo -n "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
    echo -n "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

    echo -n "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 reqested for \'$HKD_FILE\' were successful
