#!/bin/bash
#
# crlpublisher.sh
#
# Copyright (C) 2012, Branko Majic <branko@majic.rs>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

program="crlpublisher.sh"
version="0.1.2"

function usage() {
    cat <<EOF
$program $version, a non-interactive utility for publishing CRL's.

Usage: $program [OPTIONS] crl_file

$program is a non-interactive utility for publishing CRL's. It takes a single
argument pointing to the CRL file which should be published. The CRL file should
be provided either in DER or PEM encoding.

The script is written to support several types of publishers. Currently
supported publishers are:

    - scp - Publishes to a remote server using public-key authentication through
      SSH's scp command.

    - archiver - Archives CRLs to a local directory. This publisher will output
      the CRLs to a specific directory using the filename pattern
      'issuerdn_fulldateutc_crlnumber.format' where issuerdn is replaced by the
      issuer DN of the CRL, fulldateutc is replaced with last update date of CRL
      in format YYYY-MM-DD-HH:MM:SS:TZ (e.g. 2013-01-01-00:00:00:+00:00),
      crlnumber is replaced with the CRL number in decimal format, and format is
      replaced by the format of the CRL file (PEM for OpenSSL-style
      base64-encoded CRLs, DER for binary CRLs).

Publishing options are kept within configuration files. Configuration files
should be placed in the explicitly set configurtion directory (set with the -c
option), or one of the following default locations:

    - /etc/crlpublisher/
    - ~/.crlpublisher/

Configuration files must end with a .conf extension. All other files will be
ignored. Each configuraiton file should contain information for a single
publisher matching a single issuer DN.

Common configuration options

    issuerDn (mandatory) - Specifies the distinguished name of the issuer of CRL
    which should be matched for the particular configuration.

    publisher (mandatory) - Type of publisher this configuration refers to.


Configuration options for 'scp' publisher:

    remoteHost (mandatory) - Remote IP or resolvable hostname/FQDN of the target
    server to which the CRL will be published.
    
    remoteLocation (mandatory) - Full path on the remote server to the directory
    location to which the CRL will be published.

    remoteUser (optional) - Name of the remote user which will be used for
    logging-in. Default is the user executing the script.

    privateKey (optional) - Path to the private key which should be used for
    logging-in onto remote server. Default is ~/.ssh/id_rsa.

    remotePort (optional) - Remote port that should be used for connecting to
    the SSH server on remote host. Default is to use port 22.

Configuration options for 'archiver' publisher:

    acrhiveDir (mandatory) - Directory where the CRLs will be archived.

$program accepts the following options:

    -c dir    Explicit configuration directory from which the publisher
              configuration files should be read.

    -v        show script version and licensing information
    -h        show usage help


Please report bugs and send feature requests to <branko@majic.rs>.
EOF
}

function version() {
        cat <<EOF
$program, version $version

+-----------------------------------------------------------------------+
| Copyright (C) 2012, Branko Majic <branko@majic.rs>                    |
|                                                                       |
| This program is free software: you can redistribute it and/or modify  |
| it under the terms of the GNU General Public License as published by  |
| the Free Software Foundation, either version 3 of the License, or     |
| (at your option) any later version.                                   |
|                                                                       |
| This program is distributed in the hope that it will be useful,       |
| but WITHOUT ANY WARRANTY; without even the implied warranty of        |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         |
| GNU General Public License for more details.                          |
|                                                                       |
| You should have received a copy of the GNU General Public License     |
| along with this program.  If not, see <http://www.gnu.org/licenses/>. |
+-----------------------------------------------------------------------+

EOF
}

#
# Helper function that parses a CRL.
#
# Sets:
#     crlIssuerDn - to the value of the CRL issuer's distinguished name.
#     crlNumber - to the value of CRL number.
#     crlFormat - to the type of CRL (PEM/DER).
#     crlLastUpdate - to the last update time of CRL.
#     crlNextUpdate - to the next update time of CRL.
#
function readCrlInfo() {
    local crlFile="$1"
    unset crlIssuerDn crlNumber crlFormat crlLastUpdate crlNextUpdate
 
    # Detect format of the CRL.
    if openssl crl -noout -inform DER -in "$crlFile" 2>/dev/null; then
        crlFormat="DER"
    elif openssl crl -noout -inform PEM -in "$crlFile" 2>/dev/null; then
        crlFormat="PEM"
    else
        echo "Invalid CRL file '$crlFile'" >&2
        return 1
    fi

    # Read the CRL information
    crlIssuerDn=$(openssl crl -issuer -inform "$crlFormat" -noout -in "$crlFile" | sed -e 's#^issuer=/##;s#/#,#g')
    # @TODO: The -crlnumber option was added only to more recent versions of OpenSSL.
    #crlNumber=$(echo "ibase=16;obase=A;$(openssl crl -crlnumber -inform "$crlFormat" -noout -in "$crlFile" | sed -e 's/crlNumber=//')" | bc)
    crlNumber=$(openssl crl -text -inform "$crlFormat" -noout -in "$crlFile"  | grep -A1 'X509v3 CRL Number' | tail -n1 | grep -o '[[:digit:]]\+')
    crlLastUpdate=$(openssl crl -lastupdate -inform "$crlFormat" -noout -in "$crlFile" | sed -e 's/lastUpdate=//')
    crlNextUpdate=$(openssl crl -nextupdate -inform "$crlFormat" -noout -in "$crlFile" | sed -e 's/nextUpdate=//')

    return 0
}

#
# Helper function for clearing the common parameters. This function output
# commands that should be executed in order to clear the parameters (by using
# the "echo" command for example).
#
function clearCommonParameters() {
    echo "local issuerDn publisher"
}

#
# Helper function for verifying the common parameters
#
function verifyCommonParameters() {
    if [[ -z "$issuerDn" ]]; then
        echo "Publisher ($configFile): missing issuer DN" >&2
        return 1
    fi
    if [[ -z "$publisher" ]]; then
        echo "Publisher ($configFile): missing publisher type." >&2
        return 1
    fi
}

#
# Implementation of scp-based publisher.
#
function publish_through_scp() {
    local configFile="$1" crlFile="$2"
    local remoteUser="$USER" remoteHost remoteLocation privateKey="$HOME/.ssh/id_rsa" remotePort="22"

    $(clearCommonParameters)

    . "$configFile"

    verifyCommonParameters || return 1

    if ! [[ -f $privateKey ]]; then
        echo "SCP publisher ($configFile): invalid private key - '$privateKey'." >&2
        return 1
    elif [[ -z $remoteHost ]]; then
        echo "SCP publisher ($configFile): missing 'remoteHost' option." >&2
        return 1
    elif [[ -z $remoteLocation ]]; then
        echo "SCP publisher ($configFile): missing 'remoteLocation' option." >&2
        return 1
    elif [[ -z "$remotePort" ]]; then
        echo "SCP publisher ($configFile): missing 'remotePort' option." >&2
        return 1
    elif [[ ! $remotePort =~ ^[[:digit:]]+$ ]]; then
        echo "SCP publisher ($configFile): invalid remote port - '$remotePort'." >&2
        return 1
    fi

    if [[ $issuerDn == $crlIssuerDn ]]; then
        if ! scp -P "$remotePort" -i "$privateKey" "$crlFile" "$remoteUser"@"$remoteHost":"$remoteLocation"; then
            echo "SCP publisher ($configFile): failed to publish CRL for '$crlIssuerDn'." >&2
	    return 2
        fi
    fi
}

#
# Implementation of archiver.
#
function publish_through_archiver() {
    local configFile="$1" crlFile="$2"
    local archiveDir crlLastUpdateParsed

    $(clearCommonParameters)

    . "$configFile"

    verifyCommonParameters || return 1

    if ! [[ -d $archiveDir ]]; then
        echo "Archiver publisher ($configFile): invalid archive directory - '$archiveDir'" >&2
        return 1
    fi

    # Get the issuance date/time of the issued CRL.
    crlLastUpdateParsed=$(TZ=UTC date -d "$crlLastUpdate" +%F-%T:%z)

    # Set-up the filename.
    filename="${crlIssuerDn}_${crlLastUpdateParsed}_${crlNumber}.$crlFormat"

    # Copy the file.
    cp "$crlFile" "$archiveDir/$filename"
}

# If no arguments were given, just show usage help.
if [[ -z $1 ]]; then
    usage
    exit 0
fi

# Parse the arguments
while getopts "c:vh" opt; do
    case "$opt" in
        c) configDir="$OPTARG";;
        v) version
           exit 0;;
        h) usage
           exit 0;;
        *) usage
           exit 1;;
    esac
done
i=$OPTIND
shift $(($i-1))

# Figure out which configuration directory to use.
if [[ -n $configDir && ! -d $configDir ]]; then
    echo "Specified configuration directory '$configDir' does not exist." >&2
    exit 2
# If no configuration directory was provided, try one of the default ones.
elif [[ -z $configDir ]]; then
    configDir="/etc/crlpublisher"

    [[ ! -d $configDir ]] && configDir="$HOME/.crlpublisher"

    if [[ ! -d $configDir ]]; then
        cat <<EOF >&2
No configuration directory could be found. Please provide configuration
directory path using the -c option, or create configuration directory and the
necessary configuration files in one of the following locations:

- /etc/crlpublisher/
- $HOME/crlpublisher/
EOF
        exit 2
    fi
fi

# The first argument should be a CRL file
crlFile="$1"

# Obtain the issuer's DN first
readCrlInfo "$crlFile" || exit $?

# Assume that the operation suceeds unless the publisher fails for any reason.
operationResult=0

# Process each configuration file
while read configFile; do
    unset publisher
    eval "$(grep "^publisher=" "$configFile")"
    # Execute operation and if it failed, that's the status the script will
    # return.
    "publish_through_$publisher" "$configFile" "$crlFile" || operationResult="$?"
done < <(find "$configDir" -name "*.conf")

exit "$operationResult"