haproxy-ocsp-stapling-updater/hapos-upd

572 lines
16 KiB
Bash
Executable File

#!/bin/bash
# HAProxy OCSP Stapling Updater
# Copyright (c) 2015 Pier Carlo Chiodi - http://www.pierky.com
#
# https://github.com/pierky/haproxy-ocsp-stapling-updater
set -o nounset
VERSION="0.4.1-pre1"
PROGNAME="hapos-upd"
if [ -z ${OPENSSL_BIN+x} ]; then
OPENSSL_BIN="openssl"
fi
SOCAT_BIN="socat"
CERT=""
VAFILE=""
HAPROXY_ADMIN_SOCKET_DEFAULT="/run/haproxy/admin.sock"
HAPROXY_ADMIN_SOCKET="$HAPROXY_ADMIN_SOCKET_DEFAULT"
GOOD_ONLY=0
SYSLOG_PRIO=""
DEBUG=0
KEEP_TEMP=0
OCSP_URL=""
OCSP_HOST=""
VERIFY=1
TMP=""
SKIP_UPDATE=0
PARTIAL_CHAIN=""
function Quit() {
if [ $KEEP_TEMP -eq 0 ]; then
if [ -n "$TMP" ]; then
rm -r $TMP &>/dev/null
fi
fi
exit $1
}
function LogError() {
MSG="$1"
if [ -z "$SYSLOG_PRIO" ]; then
echo "$MSG" >&2
else
logger -p "$SYSLOG_PRIO" -s -- "$PROGNAME - $MSG"
fi
echo "$MSG" >>$TMP/log
}
function Error() {
if [ $1 -eq 9 ]; then
MSG="Error: $2"
else
MSG="Error processing '$CERT': $2"
fi
LogError "$MSG"
if [ $1 -eq 9 ]; then
echo "Run $PROGNAME -h for help" >&2
fi
Quit $1
}
function Debug() {
if [ $DEBUG -eq 1 ]; then
echo "$1"
fi
echo "$1" >>$TMP/log
}
function Trap() {
Debug "Aborting"
Quit 9
}
function Usage() {
echo "
HAProxy OCSP Stapling Updater - $VERSION
Copyright (c) 2015 Pier Carlo Chiodi - http://www.pierky.com
https://github.com/pierky/haproxy-ocsp-stapling-updater
Usage:
$PROGNAME [options] --cert crt_full_path
This script extracts and queries the OCSP server present in a
certificate to obtain its revocation status, then updates HAProxy by
writing the '.issuer' and the '.ocsp' files and by sending it the
'set ssl ocsp-response' command through the local UNIX admin socket.
The crt_full_path argument is the full path to the certificate bundle
used in haproxy 'crt' setting. End-entity (EE) certificate plus any
intermediate CA certificates must be concatenated there.
An OCSP query is sent to the OCSP server given on the command line
(--ocsp-url and --ocsp-host argument); if these arguments are missing,
URL and Host header values are automatically extracted from the
certificate.
If the '.issuer' file already exists it's used to build the OCSP
request, otherwise the chain is extracted from crt_full_path and used
to identify the issuer.
Finally, it writes the related '.issuer' and .'ocsp' files and updates
haproxy, using 'socat' and the local UNIX socket (--socket argument,
default $HAPROXY_ADMIN_SOCKET_DEFAULT).
Exit codes:
0 OK
1 openssl certificates handling error
2 OCSP server URL not found
3 string parsing / PEM manipulation error
4 OCSP error
5 haproxy management error
9 program error (wrong arguments, missing dependencies)
Options:
-d, --debug : don't do anything, print debug messages only.
--keep-temp : keep temporary directory after exiting (for
debug purposes).
-g, --good-only : do not update haproxy if OCSP response
certificate status value is not 'good'.
-l, --syslog priority : log errors to syslog system log module.
The priority may be specified numerically
or as a facility.level pair (e.g.
local7.error).
--ocsp-url url : OCSP server URL; use this instead of the
one in the EE certificate.
--ocsp-host host : OCSP server hostname to be used in the
'Host:' header; use this instead of the one
extracted from the OCSP server URL.
--partial-chain : Allow partial certificate chain if at least one certificate
is in trusted store. Useful when validating an intermediate
certificate without the root CA.
-s, --socket file : haproxy admin socket. If omitted,
$HAPROXY_ADMIN_SOCKET_DEFAULT is used by default.
This script is distributed with only one
method to update haproxy: using 'socat'
with a local admin-level UNIX socket.
Feel free to implement other mechanisms as
needed! The right section in the code is
\"UPDATE HAPROXY\", at the end of the script.
-v, --VAfile file : same as the openssl ocsp -VAfile option
with 'file' as argument. For more details:
'man ocsp'.
If file = \"-\" then the chain extracted
from the certificate's bundle (or .issuer
file) is used (useful for OCSP responses
that don't include the signer certificate).
--noverify : Do not verify OCSP response.
-S, --skip-update : Do not notify haproxy of the new OCSP response.
-h, --help : this help."
}
trap Trap INT TERM
TMP="`mktemp -d -q -t $PROGNAME.XXXXXXXXXX`"
# COMMAND LINE PROCESSING
# ----------------------------------
while [[ $# > 0 ]]
do
case "$1" in
-h|--help)
Usage
Quit 0
;;
-d|--debug)
DEBUG=1
;;
--keep-temp)
KEEP_TEMP=1
;;
-g|--good-only)
GOOD_ONLY=1
;;
--noverify)
VERIFY=0
;;
--partial-chain)
PARTIAL_CHAIN="-partial_chain"
;;
-l|--syslog)
if [ $# -le 1 ]; then
Error 9 "mandatory value is missing for $1 argument"
fi
SYSLOG_PRIO="$2"
shift
;;
--ocsp-url)
if [ $# -le 1 ]; then
Error 9 "mandatory value is missing for $1 argument"
fi
OCSP_URL="$2"
shift
;;
--ocsp-host)
if [ $# -le 1 ]; then
Error 9 "mandatory value is missing for $1 argument"
fi
OCSP_HOST="$2"
shift
;;
-c|--cert)
if [ $# -le 1 ]; then
Error 9 "mandatory value is missing for $1 argument"
fi
CERT="$2"
shift
;;
-v|--VAfile)
if [ $# -le 1 ]; then
Error 9 "mandatory value is missing for $1 argument"
fi
VAFILE="$2"
if [ "$VAFILE" == "-" ]; then
VAFILE="$TMP/chain.pem"
else
if [ ! -e "$VAFILE" ]; then
Error 9 "VAfile does not exists: $VAFILE"
fi
fi
shift
;;
-s|--socket)
if [ $# -le 1 ]; then
Error 9 "mandatory value is missing for $1 argument"
fi
HAPROXY_ADMIN_SOCKET="$2"
shift
;;
-S|--skip-update)
SKIP_UPDATE=1
;;
*)
Error 9 "unknown option: $1"
esac
shift
done
Debug "Temporary directory: $TMP"
$OPENSSL_BIN version | grep OpenSSL &>>$TMP/log
if [ $? -ne 0 ]; then
Error 9 "openssl binary not found; adjust OPENSSL_BIN variable in the script"
fi
$SOCAT_BIN -V | grep socat &>>$TMP/log
if [ $? -ne 0 ]; then
Error 9 "socat binary not found; adjust SOCAT_BIN variable in the script"
fi
if [ -z "$CERT" ]; then
Error 9 "certificate not provided (--cert argument)"
fi
# CURRENT RESPONSE EXPIRED?
# ----------------------------------
ISNEW=1
if [ -e $CERT.ocsp ]; then
ISNEW=0
Debug "An OCSP response already exists: checking its expiration."
$OPENSSL_BIN ocsp -respin $CERT.ocsp -text -noverify | \
grep "Next Update:" &>>$TMP/log
if [ $? -eq 0 ]; then
CURR_EXP=`$OPENSSL_BIN ocsp -respin $CERT.ocsp -text -noverify | grep "Next Update:" | cut -d ':' -f 2-`
CURR_EXP_EPOCH=`date --date="$CURR_EXP" +%s`
if [ $? -ne 0 ]; then
Error 3 "can't parse Next Update from current OCSP response"
fi
if [ $CURR_EXP_EPOCH -lt `date +%s` ]; then
Debug "Current OCSP response expiration: $CURR_EXP - expired"
LogError "current OCSP response is expired: please consider running this script more frequently"
else
Debug "Current OCSP response expiration: $CURR_EXP - NOT expired"
fi
fi
fi
# EXTRACT EE CERTIFICATE INFO
# ----------------------------------
# extract EE certificate
$OPENSSL_BIN x509 -in $CERT -outform PEM -out $TMP/ee.pem &>>$TMP/log
if [ $? -ne 0 ]; then
Error 1 "can't extract EE certificate from $CERT"
fi
# get OCSP server URL
if [ -z "$OCSP_URL" ]; then
OCSP_URL="`$OPENSSL_BIN x509 -in $TMP/ee.pem -ocsp_uri -noout`"
if [ $? -ne 0 ]; then
Error 1 "can't obtain OCSP server URL from $CERT"
fi
if [ -z "$OCSP_URL" ]; then
Error 2 "OCSP server URL not found in the EE certificate"
fi
Debug "OCSP server URL found: $OCSP_URL"
else
Debug "Using OCSP server URL from command line: $OCSP_URL"
fi
# check OCSP server URL format (http:// or https://)
echo "$OCSP_URL" | egrep -i "(http://|https://)" &>/dev/null
if [ $? -ne 0 ]; then
Error 3 "OCSP server URL not in http[s]:// format"
fi
# get OCSP server URL host name
if [ -z "$OCSP_HOST" ]; then
OCSP_HOST="`echo "$OCSP_URL" | egrep -i "(http://|https://)" | cut -d'/' -f 3`"
if [ $? -ne 0 -o -z "$OCSP_HOST" ]; then
Error 3 "can't extract hostname from OCSP server URL $OCSP_URL"
fi
Debug "OCSP server hostname: $OCSP_HOST"
else
Debug "Using OCSP server hostname from command line: $OCSP_HOST"
fi
# EXTRACT CHAIN INFO
# ----------------------------------
if [ -e $CERT.issuer ]; then
Debug "Using existing chain ($CERT.issuer)"
# copy .issuer file to temporary chain.pem
cp $CERT.issuer $TMP/chain.pem &>>$TMP/log
if [ $? -ne 0 ]; then
Error 3 "can't copy current chain from $CERT.issuer"
fi
else
Debug "Extracting chain from certificates bundle"
# get EE certificate's fingerprint
FP_EE="`$OPENSSL_BIN x509 -fingerprint -noout -in $TMP/ee.pem`"
if [ $? -ne 0 -o -z "$FP_EE" ]; then
Error 1 "can't obtain EE certificate's fingerprint"
fi
Debug "EE certificate's fingerprint: $FP_EE"
# get BEGIN CERTIFICATE and END CERTIFICATE separators
PEM_BEGIN_CERT="`head $TMP/ee.pem -n 1`"
PEM_END_CERT="`tail $TMP/ee.pem -n 1`"
# get number of certificates in the bundle file
NUM_OF_CERTS=`cat $CERT | grep -e "$PEM_BEGIN_CERT" | wc -l`
if [ $NUM_OF_CERTS -le 1 ]; then
Error 3 "can't obtain the number of certificates in the chain"
fi
Debug "$NUM_OF_CERTS certificates found in the bundle"
# save each certificate in the bundle into $TMP/chain-X.pem
cat $CERT | \
sed -n -e "/$PEM_BEGIN_CERT/,/$PEM_END_CERT/p" | \
awk "/$PEM_BEGIN_CERT/{x=\"$TMP/chain-\" ++i \".pem\";}{print > x;}" &>>$TMP/log
if [ $? -ne 0 ]; then
Error 3 "can't extract certificates from bundle"
fi
# for each certificate that is extracted from the bundle check if
# it's the EE certificate, otherwise uses it to build the chain file
for c in `seq 1 $NUM_OF_CERTS`;
do
# check fingerprint of current and EE certificates
FP="`$OPENSSL_BIN x509 -fingerprint -noout -in $TMP/chain-$c.pem`"
if [ $? -ne 0 -o -z "$FP" ]; then
Error 1 "can't obtain the fingerprint of the certificate n. $c in the bundle"
else
if [ ! "$FP" == "$FP_EE" ]; then
Debug "Bundle certificate n. $c fingerprint: $FP - it's part of the chain"
# current certificate is not the same as the EE; append to the chain
cat $TMP/chain-$c.pem >> $TMP/chain.pem
else
Debug "Bundle certificate n. $c fingerprint: $FP - EE certificate"
fi
fi
done
fi
# check if the EE certificate validates against the chain
$OPENSSL_BIN verify $PARTIAL_CHAIN -CAfile $TMP/chain.pem $TMP/ee.pem &>>$TMP/log
if [ $? -ne 0 ]; then
if [ -e $CERT.issuer ]; then
Error 1 "can't validate the EE certificate against the existing chain; if it has been changed recently consider removing the current $CERT.issuer file and let this script to figure out a new one"
else
Error 1 "can't validate the EE certificate against the extracted chain"
fi
fi
# OCSP
# ----------------------------------
# query the OCSP server and save its response
$OPENSSL_BIN version | grep "OpenSSL 1.0" &>/dev/null
if [ $? -eq 0 ]; then
# OpenSSL 1.0.x
$OPENSSL_BIN ocsp $PARTIAL_CHAIN -issuer $TMP/chain.pem -cert $TMP/ee.pem \
-respout $TMP/ocsp.der -noverify \
-no_nonce -url $OCSP_URL -header "Host" "$OCSP_HOST" &>>$TMP/log
else
$OPENSSL_BIN ocsp $PARTIAL_CHAIN -issuer $TMP/chain.pem -cert $TMP/ee.pem \
-respout $TMP/ocsp.der -noverify \
-no_nonce -url $OCSP_URL -header "Host=$OCSP_HOST" &>>$TMP/log
fi
if [ $? -ne 0 ]; then
Error 1 "can't receive the OCSP server response"
fi
# process the OCSP response
VERIFYOPT=""
if [ $VERIFY -eq 0 ]; then
VERIFYOPT="-noverify"
fi
if [ -z "$VAFILE" ]; then
$OPENSSL_BIN ocsp $PARTIAL_CHAIN $VERIFYOPT -issuer $TMP/chain.pem -cert $TMP/ee.pem \
-respin $TMP/ocsp.der -no_nonce -CAfile $TMP/chain.pem \
-out $TMP/ocsp.txt &>>$TMP/ocsp-verify.txt
else
$OPENSSL_BIN ocsp $PARTIAL_CHAIN $VERIFYOPT -issuer $TMP/chain.pem -cert $TMP/ee.pem \
-respin $TMP/ocsp.der -no_nonce -CAfile $TMP/chain.pem \
-VAfile $VAFILE \
-out $TMP/ocsp.txt &>>$TMP/ocsp-verify.txt
fi
if [ $? -ne 0 ]; then
Error 1 "can't receive OCSP response"
fi
if [ $VERIFY -eq 1 ]; then
Debug "OCSP response verification results: `cat $TMP/ocsp-verify.txt`"
cat $TMP/ocsp-verify.txt | grep "Response verify OK" &>>$TMP/log
if [ $? -ne 0 ]; then
grep "signer certificate not found" $TMP/ocsp-verify.txt &>/dev/null
if [ $? -eq 0 ]; then
Error 4 "OCSP response verification failure: signer certificate not found; try with '--VAfile -' or '--VAfile OCSP-response-signing-certificate-file' arguments"
else
Error 4 "OCSP response verification failure."
fi
fi
fi
Debug "OCSP response: `cat $TMP/ocsp.txt`"
if [ $GOOD_ONLY -eq 1 ]; then
cat $TMP/ocsp.txt | head -n 1 | grep ": good" &>>$TMP/log
if [ $? -ne 0 ]; then
Error 4 "OCSP response, certificate status not good"
fi
fi
# UPDATE HAPROXY
# ----------------------------------
# Status:
# - $TMP/ocsp.der contains the OCSP response, DER format
# - $TMP/ocsp.txt contains the textual OCSP response as produced
# by openssl
# - the OCSP response has been verified against the chain or
# the --VAfile
if [ $DEBUG -eq 0 ]; then
# update .ocsp and .issuer files
cp $TMP/ocsp.der $CERT.ocsp &>>$TMP/log
if [ $? -ne 0 ]; then
Error 5 "can't update $CERT.ocsp file"
fi
if [ ! -e $CERT.issuer ]; then
cp $TMP/chain.pem $CERT.issuer &>>$TMP/log
if [ $? -ne 0 ]; then
Error 5 "can't update $CERT.issuer file"
fi
fi
if [ $SKIP_UPDATE -eq 0 ]; then
if [ $ISNEW -eq 1 ]; then
# no .ocsp file found, maybe it's an initial run
Debug "Reloading haproxy."
service haproxy reload
if [ $? -ne 0 ]; then
Error 5 "can't reload haproxy with 'service haproxy reload'"
fi
else
# update haproxy via local UNIX socket
Debug "Updating haproxy."
echo "set ssl ocsp-response `base64 -w 0 $TMP/ocsp.der`" | $SOCAT_BIN stdio $HAPROXY_ADMIN_SOCKET &>>$TMP/log
if [ $? -ne 0 ]; then
Error 5 "can't update haproxy ssl ocsp-response using $HAPROXY_ADMIN_SOCKET socket"
fi
fi
else
Debug "Not notifying haproxy because skip-update is set."
fi
else
Debug "Debug mode: haproxy update skipped."
fi
# remove temporary files and quit with success
Quit 0
# vim: set tabstop=4 shiftwidth=4 expandtab: