#! /bin/bash
# common functions for ca-scripts

PROGNAME=$( basename $0 )
CONFFILE="/etc/ca-scripts.conf"
SHAREDIR="/home/alex/code/ca-scripts/tpl"
CRYPTKEY="-nodes"

INDEXTPL="index-html"
INDEXOUT=""

# ideally, run these scripts as an unprivileged "ssl" user/group
# and place users that need access to ssl certs into that group
# no world-readable stuff here
umask 027

error() {
    usage >&2
    echo -e "ERROR: $1\n" >&2
    exit 1
}

ca_check_var() {
    local varname vartest

    varname="$1"
    vartest="$2"
    eval "if [ ! $vartest \"\$$varname\" ]; then
              echo '$varname value \"\$$varname\" failed \"$vartest\" test'
          fi"
}

ca_override_conf() {
    local varname

    varname="${1#USER_}"
    eval "$varname=\"\$USER_$varname\""
}

ca_set_default() {
    local varname vardef
    
    varname="$1"
    vardef="$2"
    eval "if [ -z \"\$$varname\" ]; then
              $varname=\"$vardef\"
          fi"
}

ca_load_conf() {
    local varname vartest varerr vardef error ca_name
    if [ ! -r "$CONFFILE" ]; then
        error "Unable to find $CONFFILE."
    fi
    # XXX: seems like . <file> doesn't work if it's not relative to a directory
    #      look this up on the internet sometime to work out why...
    if [ "$CONFFILE" = "$( basename $CONFFILE )" ]; then
        CONFFILE="./$CONFFILE"
    fi
    . "$CONFFILE"
    
    error=""
    while read vartest varname; do
        varerr=$( ca_check_var "$varname" "$vartest" )
        if [ -n "$varerr" ]; then
            error="$error\n  $varerr"
        fi
    done <<__TESTS__
-d CA_HOME
-n CA_DOMAIN
-n CA_DN_C
-n CA_DN_ST
-n CA_DN_L
-n CA_DN_O
-n CA_DN_OU
-n CA_DN_CN
__TESTS__
    if [ -n "$error" ]; then
        error "Parsing config file $CONFFILE failed:\n$error"
    fi
    
    # check user-provided variables and copy them to CA_ namespace to override
    # any defaults that have potentially been set in configuration file
    # XXX: this is getting really dirty now, perhaps find an alternative?
    set | awk -F\= '/^USER_CA_[A-Z_]*=/{print $1}' | while read user_var; do
        ca_override_conf "$user_var"
    done

    # XXX: and this alternative should probably have better validation ;-)
    case "$CA_CRT_TYPE" in
        server|client|user) :;;
        *) error "Unrecognised certificate type '$CA_CRT_TYPE'!";;
    esac

    # we need to do these first to use them in other default defs
    # NOTE: bash's here-string syntax appends \n which tr turns to _ :(
    ca_set_default CA_NAME  "$( echo -n "$CA_DOMAIN" | tr -c '[:alnum:]@-' _ )"
    ca_set_default CA_EMAIL "ca@$CA_DOMAIN"

    while read varname vardef; do
        ca_set_default "$varname" "$vardef"
    done <<__DEFAULTS__
CA_DESC         $CA_DN_CN
CA_DAYS         3652
CA_PATHLEN      0
CA_CRT_URI      http://$CA_DOMAIN/ca/$CA_NAME.ca.crt
CA_CRL_URI      http://$CA_DOMAIN/ca/$CA_NAME.ca.crl
CA_CRT_DAYS     365
CA_CRL_DAYS     365
CA_CRT_BITS     2048
CA_CRT_TYPE     server
CA_CRT_C        $CA_DN_C
CA_CRT_ST       $CA_DN_ST
CA_CRT_L        $CA_DN_L
CA_CRT_O        $CA_DN_O
CA_CRT_OU       $CA_DN_OU
CA_CRT_E        $CA_EMAIL
__DEFAULTS__
}

ca_sed_cmd() {
    # MD5 in CA_CR[TL]_MD5_FP has a non alphabetic character :(
    # XXX: pretty sure this is a dirty and wrong way of templating vars
    set | awk -F\= '/^CA_[A-Z5_]*=/{print $1}' | while read ca_var; do
        echo "s#%$ca_var%#${!ca_var}#;"
    done
}

ca_template() {
    local template dest

    if [ -r "$1" ]; then 
        template="$1"
    elif [ -r "$SHAREDIR/$1.tpl" ]; then
        template="$SHAREDIR/$1.tpl"
    else
        error "Could not read from template $1"
    fi
    dest="$2"

    sed -e "$(ca_sed_cmd)" <"$template" >"$dest"
}

ca_gen_crl() {
    openssl ca -config "$CA_HOME/cnf/$CA_NAME.ca.cnf" -gencrl -md sha1 \
      -crldays "$CA_CRL_DAYS" -out "$CA_HOME/crl/$CA_NAME.ca.crl"
    openssl crl -in "$CA_HOME/crl/$CA_NAME.ca.crl" \
      -out "$CA_HOME/crl/$CA_NAME.ca.crl.der" -outform DER
}

ca_gen_p12() {
    local cnf_name
    cnf_name="$1"
    openssl pkcs12 -export -descert -out $CA_HOME/p12/$cnf_name.p12 \
      -in       $CA_HOME/crt/$cnf_name.crt \
      -inkey    $CA_HOME/key/$cnf_name.key \
      -certfile $CA_HOME/crt/$CA_NAME.ca.crt
}

ca_checksum() {
    CA_CRT_MD5_FP="$( openssl x509 -in $CA_HOME/crt/$CA_NAME.ca.crt \
      -noout -md5 -fingerprint | cut -d= -f2 )"
    CA_CRT_SHA_FP="$( openssl x509 -in $CA_HOME/crt/$CA_NAME.ca.crt \
      -noout -sha1 -fingerprint | cut -d= -f2 )"
    CA_CRL_MD5_FP="$( openssl crl -in $CA_HOME/crl/$CA_NAME.ca.crl \
      -noout -md5 -fingerprint | cut -d= -f2 )"
    CA_CRL_SHA_FP="$( openssl crl -in $CA_HOME/crl/$CA_NAME.ca.crl \
      -noout -sha1 -fingerprint | cut -d= -f2 )"
}

ca_cnf_name() {
    local crt
    crt="$1"
    # work out what configuration files we should be using from the cert's CN
    echo $( openssl x509 -in "$crt" -noout -nameopt sep_multiline,use_quote \
      -subject | awk -F= '/CN=/{ gsub("[^A-Za-z0-9@-]", "_", $2); print $2}' ) 
      #          grep "CN=" | cut -d= -f2 | tr -c '[:alnum:]@-' _ )
      # originally did this like the above but tried awk instead. awk(1) seems
      # to lie about it's egrep(1) support though as no matter what I tried the
      # tr(1) regex didn't work in the gsub() call above.
}

ca_cnf_type() {
    local crt
    # XXX: dirty hack -- derive type from filename being *.TYPE.crt
    crt="${1%.crt}"
    crt="${crt##*.}"
    case "$crt" in
        server|client|user) echo "$crt";;
        *) echo $CA_CRT_TYPE;;
    esac
}

ca_find_cnf() {
    local name _name _type
    name="$1"

    if [ -f "$name" ]; then
        _name="$(ca_cnf_name $name)"
        _type="$(ca_cnf_type $name)"
        if [ $(basename "$name" .crt) = "${_name}.${_type}" ]; then
            echo "${_name}.${_type}"
            return
        else
            error "Unable to derive config details from certificate '$name'."
        fi
    fi
    
    # XXX: this stil doesn't handle default types. FIXME when it's not 1am.
    _name=$( echo -n "$name" | tr -c '[:alnum:]@-' _ )
    if [ "$CA_CRT_TYPE" = "user" ]; then 
        # user names may have dots etc. in, so use munged version in match
        # check if name is "user@domain", append $CA_DOMAIN if not
        if [ "${_name%%@*}" = "$_name" \
             -a -f "$CA_HOME/crt/${_name}@$CA_NAME.$CA_CRT_TYPE.crt" ];
        then
            # name is not fully-qualified, but a cert exists for it
            echo "${_name}@$CA_NAME.$CA_CRT_TYPE"
        elif [ -f "$CA_HOME/crt/$_name.$CA_CRT_TYPE.crt" ]; then
            # name was fully-qualified and a cert exists
            echo "$_name.$CA_CRT_TYPE"
        else
            error "Could not find $CA_CRT_TYPE certificate configuration matching '$name'"
        fi
    else
        # check if name is fully-qualified -- contains more than 1 dot
        # NOTE: we have to do this test with the unmunged name ...
        if [ "${name%%.*}" = "$name" \
             -a -f "$CA_HOME/crt/${_name}_$CA_NAME.$CA_CRT_TYPE.crt" ];
        then
            # name is not fully-qualified, but a cert exists for it
            echo "${_name}_$CA_NAME.$CA_CRT_TYPE"
        elif [ -f "$CA_HOME/crt/$_name.$CA_CRT_TYPE.crt" ]; then
            # name was fully-qualified and a cert exists
            echo "$_name.$CA_CRT_TYPE"
        else
            error "Could not find $CA_CRT_TYPE certificate configuration matching '$name'"
        fi
    fi
}