2021-07-28 08:18:33 -04:00
|
|
|
#!/usr/bin/env bash
|
|
|
|
|
|
|
|
# Unofficial Bash strict mode
|
|
|
|
set \
|
|
|
|
-o errexit \
|
|
|
|
-o errtrace \
|
|
|
|
-o noglob \
|
|
|
|
-o nounset \
|
|
|
|
-o pipefail
|
|
|
|
IFS=$'\n\t'
|
|
|
|
shopt -s inherit_errexit
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
determine_colored_output() {
|
|
|
|
declare -gl COLORED_STDOUT COLORED_STDERR
|
|
|
|
readonly GREEN='\033[0;32m'
|
|
|
|
readonly RED='\033[0;31m'
|
|
|
|
readonly COLOR_DEFAULT='\033[0m'
|
|
|
|
|
|
|
|
if [[ -v NO_COLOR || ${TERM-} == dumb ]]; then
|
|
|
|
COLORED_STDOUT=false COLORED_STDERR=false
|
|
|
|
else
|
|
|
|
[[ -t 1 ]] || COLORED_STDOUT=false
|
|
|
|
[[ -t 2 ]] || COLORED_STDERR=false
|
|
|
|
fi
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2021-07-28 08:18:33 -04:00
|
|
|
exit_with_error() {
|
2023-07-09 18:16:59 -04:00
|
|
|
local error_prefix=error:$'\t\t'
|
|
|
|
|
|
|
|
[[ ${COLORED_STDERR-} != false ]] &&
|
|
|
|
local -r COLORED_ERROR_MSG=${RED}${error_prefix}${*}${COLOR_DEFAULT}
|
|
|
|
|
|
|
|
# We will have closed file descriptor 2 unless verbosity was requested, so we
|
|
|
|
# will try to use FD5 (the FD that stderr was likely redirected to), and
|
|
|
|
# fallback to FD2 if FD5 wasn't opened yet.
|
|
|
|
if [[ -f /dev/fd/5 ]]; then
|
|
|
|
exec >&5
|
|
|
|
else
|
|
|
|
exec >&2
|
|
|
|
fi
|
|
|
|
printf '%b\n' "${COLORED_ERROR_MSG:-${error_prefix}${@}}"
|
|
|
|
|
2021-07-28 08:18:33 -04:00
|
|
|
exit 1
|
|
|
|
}
|
|
|
|
|
|
|
|
check_for_dependencies() {
|
2023-07-09 18:16:59 -04:00
|
|
|
if ((BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] < 3 || BASH_VERSINFO[0] < 4)); then
|
|
|
|
exit_with_error "${0##*/} requires Bash 4.3+."
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
|
|
|
|
if ! { command -v openssl >&- &&
|
2023-07-09 18:16:59 -04:00
|
|
|
[[ $(openssl version || true) =~ ^OpenSSL\ ([[:digit:]]+)\.([[:digit:]]+) ]] &&
|
|
|
|
((BASH_REMATCH[1] == 1 && BASH_REMATCH[2] >= 1 || BASH_REMATCH[1] > 1)); }; then
|
2021-07-28 08:18:33 -04:00
|
|
|
# shellcheck disable=2016
|
|
|
|
exit_with_error \
|
2023-07-09 18:16:59 -04:00
|
|
|
"${0##*/} requires OpenSSL 1.1.0+," \
|
2021-07-28 08:18:33 -04:00
|
|
|
'but it is not available on $PATH.'
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
parse_cli_options() {
|
|
|
|
local -r cli_options="
|
|
|
|
Usage: ${0} [-c/--certbot-dir DIRECTORY] [-f/--force-update] \\
|
|
|
|
[-h/--help] [-l/--no-color] [-n/--cert-name NAME[,NAME...] \\
|
|
|
|
[-u/--ocsp-responder URL]] [-o/--output-dir DIRECTORY] \\
|
|
|
|
[-q/--quiet|-v/--verbose] [-w/--no-reload-webserver]
|
|
|
|
"
|
|
|
|
|
|
|
|
print_option_error() {
|
|
|
|
local reason=${1} option=${2}
|
|
|
|
shift 2
|
|
|
|
local option_error="${option}: "
|
|
|
|
|
|
|
|
case ${reason} in
|
|
|
|
--conflict)
|
|
|
|
local second_option=${1}
|
|
|
|
shift
|
|
|
|
option_error+="This option cannot be combined with the option ${second_option}."
|
|
|
|
;;
|
|
|
|
--duplicate)
|
|
|
|
option_error+="This option cannot be specified multiple times."
|
|
|
|
;;
|
|
|
|
--unknown)
|
|
|
|
option_error+="Invalid option."
|
|
|
|
;;
|
|
|
|
--value)
|
|
|
|
option_error+="This option requires a value."
|
|
|
|
;;
|
|
|
|
*)
|
|
|
|
exit 1
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
|
|
|
|
exit_with_error "${option_error}" "${cli_options}"
|
|
|
|
}
|
2021-07-28 08:18:33 -04:00
|
|
|
|
|
|
|
declare -gl ERROR_ENCOUNTERED
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
declare -gi VERBOSITY=${VERBOSITY:-1}
|
2021-07-28 08:18:33 -04:00
|
|
|
|
|
|
|
while ((${#} > 0)); do
|
|
|
|
local parameter=${1}
|
|
|
|
|
|
|
|
case ${parameter} in
|
|
|
|
-[^-]?*)
|
|
|
|
set -- "-${parameter:1:1}" "-${parameter:2}" "${@:2}"
|
|
|
|
;;
|
|
|
|
-c | --certbot-dir | --certbot-dir=?*)
|
|
|
|
if [[ -v CERTBOT_DIR ]]; then
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --duplicate "${parameter}"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
|
|
|
|
if [[ ${parameter} =~ --certbot-dir=(.+) ]]; then
|
|
|
|
CERTBOT_DIR=${BASH_REMATCH[1]}
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
if [[ -n ${2-} ]]; then
|
2021-07-28 08:18:33 -04:00
|
|
|
CERTBOT_DIR=${2}
|
|
|
|
shift
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --value "${parameter}"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
fi
|
|
|
|
|
|
|
|
CERTBOT_DIR=$(
|
|
|
|
realpath \
|
|
|
|
--canonicalize-missing \
|
|
|
|
--relative-base . \
|
|
|
|
-- "${CERTBOT_DIR}"
|
|
|
|
echo x
|
|
|
|
)
|
|
|
|
CERTBOT_DIR=${CERTBOT_DIR%??}
|
|
|
|
shift
|
|
|
|
;;
|
|
|
|
-f | --force-update)
|
|
|
|
if [[ ! -v FORCE_UPDATE ]]; then
|
|
|
|
declare -glr FORCE_UPDATE=true
|
|
|
|
fi
|
|
|
|
shift
|
|
|
|
;;
|
|
|
|
-h | --help)
|
2023-07-09 18:16:59 -04:00
|
|
|
{
|
|
|
|
printf '%s\n' certbot-ocsp-fetcher
|
|
|
|
printf '%s\n' "${cli_options}"
|
|
|
|
local absolute_tool_path
|
|
|
|
absolute_tool_path=$(realpath --no-symlinks -- "${0}")
|
|
|
|
readonly absolute_tool_path
|
|
|
|
cat <<EOSTRING
|
|
|
|
|
|
|
|
certbot-ocsp-fetcher helps you setup OCSP stapling in nginx. The tool primes
|
|
|
|
nginx's OCSP cache to work around nginx's flawed OCSP stapling implementation.
|
|
|
|
The tool does this by fetching and saving OCSP responses for TLS certificates
|
|
|
|
issued with Certbot.
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
Example:
|
|
|
|
|
|
|
|
1. Fetch OCSP responses for all certificates managed by Certbot, and save
|
|
|
|
them in the current working directory. This should usually be run on a
|
|
|
|
schedule, e.g. as a cronjob or systemd timer.
|
|
|
|
|
|
|
|
$ ${0}
|
|
|
|
|
|
|
|
2. Add the path(s) to the resulting OCSP response(s) as the value of the
|
|
|
|
ssl_stapling_file directive in the corresponding vhosts in Nginx. Don't
|
|
|
|
forget to reload Nginx afterwards.
|
|
|
|
|
|
|
|
3. Re-issue all certificates managed by Certbot, to add the OCSP Must-Staple
|
|
|
|
flag to the certs and automatically run certbot-ocsp-fetcher during renewals:
|
|
|
|
|
|
|
|
$ certbot renew --deploy-hook ${absolute_tool_path} --force-renewal --must-staple
|
|
|
|
|
|
|
|
---
|
|
|
|
|
|
|
|
See the online README for an explanation of all the CLI options:
|
|
|
|
https://github.com/tomwassenberg/certbot-ocsp-fetcher/blob/main/README.md
|
|
|
|
EOSTRING
|
|
|
|
} >&2
|
2021-07-28 08:18:33 -04:00
|
|
|
exit
|
|
|
|
;;
|
2023-07-09 18:16:59 -04:00
|
|
|
-l | --no-color)
|
|
|
|
readonly COLORED_STDOUT=false COLORED_STDERR=false
|
|
|
|
;;
|
2021-07-28 08:18:33 -04:00
|
|
|
-n | --cert-name | --cert-name=?*)
|
|
|
|
if [[ ${parameter} =~ --cert-name=(.+) ]]; then
|
|
|
|
local cert_lineages_value=${BASH_REMATCH[1]}
|
|
|
|
shift
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
if [[ -n ${2-} ]]; then
|
2021-07-28 08:18:33 -04:00
|
|
|
local cert_lineages_value=${2}
|
|
|
|
shift 2
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --value "${parameter}"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
fi
|
|
|
|
|
|
|
|
# Loop over any lineages passed in the same value of --cert-name.
|
|
|
|
OLDIFS=${IFS}
|
|
|
|
IFS=,
|
|
|
|
declare -Ag CERT_LINEAGES
|
|
|
|
# Check if a hardcoded OCSP responder was specified for this set of
|
|
|
|
# lineages.
|
2023-07-09 18:16:59 -04:00
|
|
|
case ${1-} in
|
2021-07-28 08:18:33 -04:00
|
|
|
-u | --ocsp-responder)
|
2023-07-09 18:16:59 -04:00
|
|
|
if [[ -n ${2-} ]]; then
|
2021-07-28 08:18:33 -04:00
|
|
|
for lineage_name in ${cert_lineages_value}; do
|
|
|
|
CERT_LINEAGES["${lineage_name}"]=${2}
|
|
|
|
done
|
|
|
|
shift
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --value "${parameter}"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
shift
|
|
|
|
;;
|
|
|
|
--ocsp-responder=?*)
|
|
|
|
[[ ${1} =~ --ocsp-responder=(.+) ]]
|
|
|
|
for lineage_name in ${cert_lineages_value}; do
|
|
|
|
CERT_LINEAGES["${lineage_name}"]=${BASH_REMATCH[1]}
|
|
|
|
done
|
|
|
|
shift
|
|
|
|
;;
|
|
|
|
*)
|
|
|
|
# If no OCSP responder was specified, just save the lineage
|
|
|
|
# name as the key, with an empty value.
|
|
|
|
for lineage_name in ${cert_lineages_value}; do
|
|
|
|
CERT_LINEAGES["${lineage_name}"]=
|
|
|
|
done
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
unset lineage_name cert_lineages_value
|
|
|
|
IFS=${OLDIFS}
|
|
|
|
;;
|
|
|
|
-o | --output-dir | --output-dir=?*)
|
|
|
|
if [[ -v OUTPUT_DIR ]]; then
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --duplicate "${parameter}"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
|
|
|
|
if [[ ${parameter} =~ --output-dir=(.+) ]]; then
|
|
|
|
OUTPUT_DIR=${BASH_REMATCH[1]}
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
if [[ -n ${2-} ]]; then
|
2021-07-28 08:18:33 -04:00
|
|
|
OUTPUT_DIR=${2}
|
|
|
|
shift
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --value "${parameter}"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
fi
|
|
|
|
|
|
|
|
OUTPUT_DIR=$(
|
|
|
|
realpath \
|
|
|
|
--canonicalize-missing \
|
|
|
|
--relative-base . \
|
|
|
|
-- "${OUTPUT_DIR}"
|
|
|
|
echo x
|
|
|
|
)
|
|
|
|
OUTPUT_DIR=${OUTPUT_DIR%??}
|
|
|
|
shift
|
|
|
|
;;
|
|
|
|
-q | --quiet)
|
|
|
|
if ((VERBOSITY != 1)); then
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --conflict "${parameter}" -v/--verbose
|
2021-07-28 08:18:33 -04:00
|
|
|
else
|
|
|
|
readonly VERBOSITY=0
|
|
|
|
shift
|
|
|
|
fi
|
|
|
|
;;
|
|
|
|
-v | --verbose)
|
|
|
|
if ((VERBOSITY == 0)); then
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --conflict "${parameter}" -q/--quiet
|
2021-07-28 08:18:33 -04:00
|
|
|
else
|
|
|
|
VERBOSITY+=1
|
|
|
|
shift
|
|
|
|
fi
|
|
|
|
;;
|
|
|
|
-w | --no-reload-webserver)
|
|
|
|
if [[ ! -v RELOAD_WEBSERVER ]]; then
|
|
|
|
declare -glr RELOAD_WEBSERVER=false
|
|
|
|
fi
|
|
|
|
shift
|
|
|
|
;;
|
|
|
|
*)
|
2023-07-09 18:16:59 -04:00
|
|
|
print_option_error --unknown "${parameter}"
|
2021-07-28 08:18:33 -04:00
|
|
|
;;
|
|
|
|
esac
|
|
|
|
done
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
# Respect the common "DEBUG" environment variable if set, unless the --quiet
|
|
|
|
# or --verbose flag has been passed as well.
|
|
|
|
if ((${DEBUG:-0} >= 1)) && ((VERBOSITY == 1)); then
|
|
|
|
# We set VERBOSITY to 0 in case of --quiet, so use the value of $DEBUG
|
|
|
|
# incremented with 1 to match it with $VERBOSITY.
|
|
|
|
VERBOSITY=$((DEBUG + 1))
|
|
|
|
fi
|
|
|
|
|
2021-07-28 08:18:33 -04:00
|
|
|
# When not parsed, the stdout and/or stderr output of all external commands
|
|
|
|
# we call in the script is redirected to file descriptor 3. Depending on the
|
|
|
|
# desired verbosity, we redirect this file descriptor to either stderr or to
|
|
|
|
# /dev/null.
|
|
|
|
if ((VERBOSITY >= 2)); then
|
|
|
|
exec 3>&2
|
|
|
|
else
|
|
|
|
exec 3>/dev/null
|
|
|
|
fi
|
2023-07-09 18:16:59 -04:00
|
|
|
|
|
|
|
# First copy file descriptor 2 to a new FD, so stderr can still be used
|
|
|
|
# (unconditionally) in the exit_with_error function.
|
|
|
|
exec 5>&2
|
|
|
|
if ((VERBOSITY < 1)); then
|
|
|
|
exec 2>/dev/null
|
|
|
|
fi
|
2021-07-28 08:18:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
# Set output directory if necessary and check if it's writeable
|
|
|
|
prepare_output_dir() {
|
|
|
|
if [[ -v OUTPUT_DIR ]]; then
|
|
|
|
if [[ ! -e ${OUTPUT_DIR} ]]; then
|
|
|
|
# Don't yet fail if it's not possible to create the directory, so we can
|
|
|
|
# exit with a custom error down below
|
|
|
|
mkdir \
|
|
|
|
--parents \
|
|
|
|
-- "${OUTPUT_DIR}" || true
|
|
|
|
fi
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
# Use $CACHE_DIRECTORY if set (e.g. when run as a systemd service),
|
|
|
|
# otherwise the working directory
|
|
|
|
readonly OUTPUT_DIR=${CACHE_DIRECTORY:-.}
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
|
|
|
|
if [[ ! -w ${OUTPUT_DIR} ]]; then
|
2023-07-09 18:16:59 -04:00
|
|
|
exit_with_error "no write access to output directory (\"${OUTPUT_DIR}\")"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
|
|
|
start_in_correct_mode() {
|
|
|
|
# Create temporary directory to store OCSP staple file,
|
|
|
|
# before having checked the certificate status in the response
|
|
|
|
local temp_output_dir
|
|
|
|
temp_output_dir=$(mktemp --directory)
|
|
|
|
readonly temp_output_dir
|
|
|
|
trap "rm -r -- ""${temp_output_dir}" EXIT
|
|
|
|
|
|
|
|
declare -A lineages_processed
|
|
|
|
|
|
|
|
# These two environment variables are set if this script is invoked by Certbot
|
|
|
|
if [[ ! -v RENEWED_DOMAINS || ! -v RENEWED_LINEAGE ]]; then
|
|
|
|
run_standalone
|
|
|
|
else
|
|
|
|
run_as_deploy_hook
|
|
|
|
fi
|
|
|
|
|
|
|
|
print_and_handle_result
|
|
|
|
}
|
|
|
|
|
|
|
|
# Run in "check one or all certificate lineage(s) managed by Certbot" mode
|
|
|
|
# $1 - Path to temporary output directory
|
|
|
|
run_standalone() {
|
2023-07-09 18:16:59 -04:00
|
|
|
printf >&2 '%s\n\n' "Running in stand-alone mode..."
|
|
|
|
|
2021-07-28 08:18:33 -04:00
|
|
|
readonly CERTBOT_DIR=${CERTBOT_DIR:-/etc/letsencrypt}
|
|
|
|
|
|
|
|
if [[ ! -r ${CERTBOT_DIR} || (-d ${CERTBOT_DIR}/live && ! -r ${CERTBOT_DIR}/live) ]]; then
|
2023-07-09 18:16:59 -04:00
|
|
|
exit_with_error "can't access ${CERTBOT_DIR}/live"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
|
|
|
|
# Check specific lineage if passed on CLI,
|
|
|
|
# or otherwise all lineages in Certbot's dir
|
2023-07-09 18:16:59 -04:00
|
|
|
if [[ -n ${!CERT_LINEAGES[*]} ]]; then
|
2021-07-28 08:18:33 -04:00
|
|
|
for lineage_name in "${!CERT_LINEAGES[@]}"; do
|
|
|
|
if [[ -r ${CERTBOT_DIR}/live/${lineage_name} ]]; then
|
|
|
|
fetch_ocsp_response \
|
2023-07-09 18:16:59 -04:00
|
|
|
--standalone \
|
2021-07-28 08:18:33 -04:00
|
|
|
"${temp_output_dir}" \
|
|
|
|
"${lineage_name}" \
|
|
|
|
"${CERT_LINEAGES["${lineage_name}"]}"
|
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
exit_with_error "can't access ${CERTBOT_DIR}/live/${lineage_name}"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
done
|
|
|
|
else
|
|
|
|
set +f
|
|
|
|
shopt -s nullglob
|
|
|
|
for lineage_dir in "${CERTBOT_DIR}"/live/*; do
|
|
|
|
set -f
|
|
|
|
|
|
|
|
# Skip non-directories, like Certbot's README file
|
|
|
|
[[ -d ${lineage_dir} ]] || continue
|
|
|
|
|
|
|
|
fetch_ocsp_response \
|
2023-07-09 18:16:59 -04:00
|
|
|
--standalone "${temp_output_dir}" "${lineage_dir##*/}"
|
2021-07-28 08:18:33 -04:00
|
|
|
done
|
|
|
|
unset lineage_dir
|
|
|
|
fi
|
|
|
|
}
|
|
|
|
|
|
|
|
# Run in deploy-hook mode, only processing the passed lineage
|
|
|
|
# $1 - Path to temporary output directory
|
|
|
|
run_as_deploy_hook() {
|
2023-07-09 18:16:59 -04:00
|
|
|
printf >&2 '%s\n\n' "Running as a deploy hook of Certbot..."
|
|
|
|
|
2021-07-28 08:18:33 -04:00
|
|
|
if [[ -v CERTBOT_DIR ]]; then
|
|
|
|
# The directory is already inferred from the environment variable that
|
|
|
|
# Certbot passes
|
|
|
|
exit_with_error \
|
2023-07-09 18:16:59 -04:00
|
|
|
"-c/--certbot-dir cannot be passed" \
|
2021-07-28 08:18:33 -04:00
|
|
|
"when run as Certbot hook"
|
|
|
|
fi
|
|
|
|
|
|
|
|
if [[ -v FORCE_UPDATE ]]; then
|
|
|
|
# When run as deploy hook the behavior of this flag is used by default.
|
|
|
|
# Therefore passing this flag would not have any effect.
|
|
|
|
exit_with_error \
|
2023-07-09 18:16:59 -04:00
|
|
|
"-f/--force-update cannot be passed" \
|
2021-07-28 08:18:33 -04:00
|
|
|
"when run as Certbot hook"
|
|
|
|
fi
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
if [[ -n ${!CERT_LINEAGES[*]} ]]; then
|
2021-07-28 08:18:33 -04:00
|
|
|
# The certificate lineage is already inferred from the environment
|
|
|
|
# variable that Certbot passes
|
2023-07-09 18:16:59 -04:00
|
|
|
exit_with_error "-n/--cert-name cannot be passed when run as Certbot hook"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
|
|
|
|
fetch_ocsp_response \
|
|
|
|
--deploy_hook "${temp_output_dir}" "${RENEWED_LINEAGE##*/}"
|
|
|
|
}
|
|
|
|
|
|
|
|
# Check if it's necessary to fetch a new OCSP response
|
|
|
|
check_for_existing_ocsp_staple_file() {
|
|
|
|
[[ -f ${OUTPUT_DIR}/${lineage_name}.der ]] || return 1
|
|
|
|
|
|
|
|
# Validate and verify the existing local OCSP staple file
|
|
|
|
local existing_ocsp_response
|
|
|
|
set +e
|
|
|
|
existing_ocsp_response=$(openssl ocsp \
|
|
|
|
-no_nonce \
|
|
|
|
-issuer "${lineage_dir}/chain.pem" \
|
|
|
|
-cert "${lineage_dir}/cert.pem" \
|
|
|
|
-verify_other "${lineage_dir}/chain.pem" \
|
|
|
|
-respin "${OUTPUT_DIR}/${lineage_name}.der" 2>&3)
|
|
|
|
local -ir existing_ocsp_response_rc=${?}
|
|
|
|
set -e
|
|
|
|
readonly existing_ocsp_response
|
|
|
|
|
|
|
|
((existing_ocsp_response_rc == 0)) || return 1
|
|
|
|
|
|
|
|
for existing_ocsp_response_line in ${existing_ocsp_response}; do
|
|
|
|
if [[ ${existing_ocsp_response_line} =~ ^[[:blank:]]*"This Update: "(.+)$ ]]; then
|
|
|
|
local -r this_update=${BASH_REMATCH[1]}
|
|
|
|
elif [[ ${existing_ocsp_response_line} =~ ^[[:blank:]]*"Next Update: "(.+)$ ]]; then
|
|
|
|
local -r next_update=${BASH_REMATCH[1]}
|
|
|
|
fi
|
|
|
|
done
|
2023-07-09 18:16:59 -04:00
|
|
|
[[ -n ${this_update-} && -n ${next_update-} ]] || return 1
|
2021-07-28 08:18:33 -04:00
|
|
|
|
|
|
|
# Only continue fetching OCSP response if existing response expires within
|
|
|
|
# half of its lifetime.
|
2023-07-09 18:16:59 -04:00
|
|
|
{
|
|
|
|
# The command substitutions here don't respect `set -o errexit`, but in
|
|
|
|
# case any of them fail, the total command still fails unless both
|
|
|
|
# substitutions print an integer. This seems very unlikely to occur, so
|
|
|
|
# let's ignore this.
|
|
|
|
# shellcheck disable=2312
|
|
|
|
local -ri response_lifetime_in_seconds=$(($(date +%s --date "${next_update}") - $(date +%s --date "${this_update}")))
|
|
|
|
|
|
|
|
# `set -o errexit` isn't respected here either, but we default to renewing
|
|
|
|
# the OCSP response, so this is fine.
|
|
|
|
# shellcheck disable=2312
|
|
|
|
(($(date +%s) < $(date +%s --date "${this_update}") + response_lifetime_in_seconds / 2)) || return 1
|
|
|
|
}
|
2021-07-28 08:18:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
# Generate file used by ssl_stapling_file in nginx config of websites
|
|
|
|
# $1 - Whether to run as a deploy hook for Certbot, or standalone
|
|
|
|
# $2 - Path to temporary output directory
|
|
|
|
# $3 - Name of certificate lineage
|
|
|
|
# $4 - OCSP endpoint (if specified on command line)
|
|
|
|
fetch_ocsp_response() {
|
|
|
|
local -r temp_output_dir=${2}
|
|
|
|
local -r lineage_name=${3}
|
2023-07-09 18:16:59 -04:00
|
|
|
|
|
|
|
# This validation should be revisited once
|
|
|
|
# https://github.com/certbot/certbot/issues/6127 is fixed.
|
|
|
|
if [[ ${lineage_name} =~ ($'\n')|($'\t') ]]; then
|
|
|
|
ERROR_ENCOUNTERED=true
|
|
|
|
exit_with_error \
|
|
|
|
"Unsupported characters encountered in the following" \
|
|
|
|
"lineage name: ${lineage_name}$'\n\n'" \
|
|
|
|
"Lineage names with embedded tabs or newlines are not supported," \
|
|
|
|
"because Certbot (as of version 1.18.0) does not have well-defined" \
|
|
|
|
'behavior on handling any "unconventional" lineage names.'
|
|
|
|
fi
|
|
|
|
|
2021-07-28 08:18:33 -04:00
|
|
|
case ${1} in
|
|
|
|
--standalone)
|
|
|
|
local -r lineage_dir=${CERTBOT_DIR}/live/${lineage_name}
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
# `set -o errexit` is not respected here, but in case of failure we still
|
|
|
|
# err on the safe side by renewing the OCSP staple file.
|
|
|
|
# shellcheck disable=2310
|
|
|
|
if [[ ${FORCE_UPDATE-} != true ]] &&
|
2021-07-28 08:18:33 -04:00
|
|
|
check_for_existing_ocsp_staple_file; then
|
|
|
|
lineages_processed["${lineage_name}"]="not updated"$'\t'"valid staple file on disk"
|
|
|
|
return
|
|
|
|
fi
|
|
|
|
;;
|
|
|
|
--deploy_hook)
|
|
|
|
local -r lineage_dir=${RENEWED_LINEAGE}
|
|
|
|
;;
|
|
|
|
*)
|
|
|
|
return 1
|
|
|
|
;;
|
|
|
|
esac
|
|
|
|
shift 3
|
|
|
|
|
|
|
|
# Verify that the leaf certificate is still valid. If the certificate is
|
|
|
|
# expired, we don't have to request a (new) OCSP response.
|
|
|
|
local cert_expiry_output
|
|
|
|
set +e
|
|
|
|
cert_expiry_output=$(openssl x509 \
|
|
|
|
-in "${lineage_dir}/cert.pem" \
|
|
|
|
-checkend 0 \
|
|
|
|
-noout 2>&3)
|
|
|
|
local -ri cert_expiry_rc=${?}
|
|
|
|
set -e
|
|
|
|
if ((cert_expiry_rc != 0)); then
|
|
|
|
ERROR_ENCOUNTERED=true
|
2023-07-09 18:16:59 -04:00
|
|
|
lineages_processed["${lineage_name}"]="failed to update"
|
2021-07-28 08:18:33 -04:00
|
|
|
if [[ ${cert_expiry_output} == "Certificate will expire" ]]; then
|
|
|
|
lineages_processed["${lineage_name}"]+=$'\t'"leaf certificate expired"
|
|
|
|
fi
|
|
|
|
return
|
|
|
|
fi
|
|
|
|
|
|
|
|
local ocsp_endpoint
|
|
|
|
if [[ -n ${1-} ]]; then
|
|
|
|
ocsp_endpoint=${1}
|
|
|
|
else
|
|
|
|
ocsp_endpoint=$(openssl x509 \
|
|
|
|
-noout \
|
|
|
|
-ocsp_uri \
|
|
|
|
-in "${lineage_dir}/cert.pem" \
|
|
|
|
2>&3)
|
|
|
|
fi
|
|
|
|
|
|
|
|
# Request, verify and temporarily save the actual OCSP response,
|
|
|
|
# and check whether the certificate status is "good"
|
|
|
|
local ocsp_call_output
|
|
|
|
set +e
|
|
|
|
ocsp_call_output=$(openssl ocsp \
|
|
|
|
-no_nonce \
|
|
|
|
-url "${ocsp_endpoint}" \
|
|
|
|
-issuer "${lineage_dir}/chain.pem" \
|
|
|
|
-cert "${lineage_dir}/cert.pem" \
|
|
|
|
-verify_other "${lineage_dir}/chain.pem" \
|
|
|
|
-respout "${temp_output_dir}/${lineage_name}.der" 2>&3)
|
|
|
|
local -ir ocsp_call_rc=${?}
|
|
|
|
set -e
|
2023-07-09 18:16:59 -04:00
|
|
|
readonly ocsp_call_output=${ocsp_call_output#"${lineage_dir}"/cert.pem: }
|
2021-07-28 08:18:33 -04:00
|
|
|
local -r cert_status=${ocsp_call_output%%$'\n'*}
|
|
|
|
|
|
|
|
if [[ ${ocsp_call_rc} != 0 || ${cert_status} != good ]]; then
|
|
|
|
ERROR_ENCOUNTERED=true
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
lineages_processed["${lineage_name}"]="failed to update"
|
2021-07-28 08:18:33 -04:00
|
|
|
if ((VERBOSITY >= 2)); then
|
|
|
|
lineages_processed["${lineage_name}"]+=$'\t'"${ocsp_call_output//[[:space:]]/ }"
|
|
|
|
else
|
|
|
|
lineages_processed["${lineage_name}"]+=$'\t'"${cert_status}"
|
|
|
|
fi
|
|
|
|
|
|
|
|
return
|
|
|
|
fi
|
|
|
|
|
|
|
|
# If arrived here status was good, so move OCSP staple file to definitive
|
|
|
|
# folder
|
|
|
|
mv "${temp_output_dir}/${lineage_name}.der" "${OUTPUT_DIR}/"
|
|
|
|
|
|
|
|
lineages_processed["${lineage_name}"]=updated
|
|
|
|
}
|
|
|
|
|
|
|
|
print_and_handle_result() {
|
|
|
|
local -r header=LINEAGE$'\t'RESULT$'\t'REASON
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
local lineages_processed_marked_up
|
2021-07-28 08:18:33 -04:00
|
|
|
for lineage_name in "${!lineages_processed[@]}"; do
|
2023-07-09 18:16:59 -04:00
|
|
|
lineages_processed_marked_up+=$'\n'"${lineage_name}"$'\t'
|
|
|
|
if [[ ${COLORED_STDOUT-} != false ]]; then
|
|
|
|
if [[ ${lineages_processed["${lineage_name}"]} =~ ^updated ]]; then
|
|
|
|
lineages_processed_marked_up+=${GREEN}
|
|
|
|
elif [[ ${lineages_processed["${lineage_name}"]} =~ ^"failed to update" ]]; then
|
|
|
|
lineages_processed_marked_up+=${RED}
|
|
|
|
fi
|
|
|
|
lineages_processed_marked_up+=${lineages_processed["${lineage_name}"]}${COLOR_DEFAULT}
|
|
|
|
else
|
|
|
|
lineages_processed_marked_up+=${lineages_processed["${lineage_name}"]}
|
|
|
|
fi
|
2021-07-28 08:18:33 -04:00
|
|
|
done
|
|
|
|
unset lineage_name
|
2023-07-09 18:16:59 -04:00
|
|
|
lineages_processed_marked_up=$(sort <<<"${lineages_processed_marked_up-}")
|
|
|
|
readonly lineages_processed_marked_up
|
2021-07-28 08:18:33 -04:00
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
if [[ ${RELOAD_WEBSERVER-} != false ]]; then
|
2021-07-28 08:18:33 -04:00
|
|
|
reload_webserver
|
|
|
|
fi
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
local output=${header}${lineages_processed_marked_up-}${nginx_status-}
|
2021-07-28 08:18:33 -04:00
|
|
|
|
|
|
|
if ((VERBOSITY >= 1)); then
|
2023-07-09 18:16:59 -04:00
|
|
|
local output_table
|
|
|
|
# shellcheck disable=2016
|
|
|
|
output_table=$(column \
|
|
|
|
--output-separator $'\t' \
|
|
|
|
--separator $'\t' \
|
|
|
|
--table \
|
|
|
|
<<<"${output}" \
|
|
|
|
2>/dev/null) ||
|
|
|
|
output_table=$(column -s$'\t' -t <<<"${output}" 2>/dev/null) ||
|
|
|
|
local -r column_error=($'\n'
|
|
|
|
'Install the BSD utility `column` for properly formatted output.'
|
|
|
|
'If the version of `column` supports the `--output-separator` flag,'
|
|
|
|
'the output will be formatted as TSV.'
|
|
|
|
$'\n'
|
|
|
|
)
|
|
|
|
readonly output=${output_table:-${output}}
|
|
|
|
unset output_table
|
|
|
|
|
|
|
|
# Extract header to direct it to stderr
|
|
|
|
printf '%s\n' "${output%%$'\n'*}" >&2
|
|
|
|
# Remove header before printing everything else to stdout
|
|
|
|
[[ -n ${!lineages_processed[*]} ]] && printf '%b\n' "${output#*$'\n'}"
|
|
|
|
|
|
|
|
if [[ ${COLORED_STDERR-} != false ]]; then
|
|
|
|
printf %b "${RED}${column_error[*]-}${COLOR_DEFAULT}" >&2
|
2021-07-28 08:18:33 -04:00
|
|
|
else
|
2023-07-09 18:16:59 -04:00
|
|
|
printf %b "${column_error[*]-}" >&2
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
|
|
|
fi
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
[[ ${ERROR_ENCOUNTERED-} != true ]]
|
2021-07-28 08:18:33 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
reload_webserver() {
|
|
|
|
for lineage_name in "${!lineages_processed[@]}"; do
|
|
|
|
if [[ ${lineages_processed["${lineage_name}"]} == updated ]]; then
|
2023-07-09 18:16:59 -04:00
|
|
|
local nginx_status
|
2021-07-28 08:18:33 -04:00
|
|
|
if nginx -s reload >&3 2>&1; then
|
2023-07-09 18:16:59 -04:00
|
|
|
[[ ${COLORED_STDERR-} != false ]] && nginx_status=${GREEN}
|
2021-07-28 08:18:33 -04:00
|
|
|
# The last line includes a leading space, to workaround the lack of the
|
|
|
|
# `-n` flag in later versions of `column`.
|
2023-07-09 18:16:59 -04:00
|
|
|
nginx_status+=$'\n\n \t'"nginx reloaded"
|
2021-07-28 08:18:33 -04:00
|
|
|
else
|
|
|
|
ERROR_ENCOUNTERED=true
|
2023-07-09 18:16:59 -04:00
|
|
|
[[ ${COLORED_STDERR-} != false ]] && nginx_status=${RED}
|
|
|
|
nginx_status=$'\n\n \t'"nginx not reloaded"$'\t'"unable to reload nginx service, try manually"
|
2021-07-28 08:18:33 -04:00
|
|
|
fi
|
2023-07-09 18:16:59 -04:00
|
|
|
[[ ${COLORED_STDERR-} != false ]] &&
|
|
|
|
readonly nginx_status+=${COLOR_DEFAULT}
|
2021-07-28 08:18:33 -04:00
|
|
|
break
|
|
|
|
fi
|
|
|
|
done
|
|
|
|
unset lineage_name
|
|
|
|
}
|
|
|
|
|
|
|
|
main() {
|
|
|
|
check_for_dependencies
|
|
|
|
|
2023-07-09 18:16:59 -04:00
|
|
|
determine_colored_output
|
|
|
|
|
|
|
|
parse_cli_options "${@}"
|
2021-07-28 08:18:33 -04:00
|
|
|
|
|
|
|
prepare_output_dir
|
|
|
|
|
|
|
|
start_in_correct_mode
|
|
|
|
}
|
|
|
|
|
|
|
|
main "${@}"
|