#!/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"