rewrote challenge validation to iterate over authorizations instead of altnames (fixes some acmev2 validation edgecases), also removed broken test-script (for now)

This commit is contained in:
Lukas Schauer
2018-01-28 05:02:18 +01:00
parent 6f3fed496d
commit 0f69481e2b
4 changed files with 138 additions and 392 deletions

View File

@@ -1,14 +0,0 @@
sudo: false
language: shell
os:
- linux
- osx
cache:
directories:
- ngrok
script:
- export CI="true"
- ./test.sh

View File

@@ -407,6 +407,13 @@ get_json_array_value() {
sed -n "${filter}" sed -n "${filter}"
} }
# Get sub-dictionary from json
get_json_dict_value() {
local filter
filter=$(printf 's/.*"%s": *{\([^}]*\)}.*/\\1/p' "$1")
sed -n "${filter}"
}
# Get integer value from json # Get integer value from json
get_json_int_value() { get_json_int_value() {
local filter local filter
@@ -604,75 +611,86 @@ sign_csr() {
fi fi
if [[ -n "${ZSH_VERSION:-}" ]]; then if [[ -n "${ZSH_VERSION:-}" ]]; then
local -A challenge_altnames challenge_uris challenge_tokens keyauths deploy_args authorization local -A challenge_identifiers challenge_uris challenge_tokens authorizations keyauths deploy_args
else else
local -a challenge_altnames challenge_uris challenge_tokens keyauths deploy_args authorization local -a challenge_identifiers challenge_uris challenge_tokens authorizations keyauths deploy_args
fi fi
if [[ ${API} -eq 2 ]]; then # Initial step: Find which authorizations we're dealing with
# APIv2 if [[ ${API} -eq 2 ]]; then
# Request new order and store authorization URIs
for altname in ${altnames}; do for altname in ${altnames}; do
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")" challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
done done
challenge_identifiers="[${challenge_identifiers%, }]" challenge_identifiers="[${challenge_identifiers%, }]"
echo " + Requesting challenges for ${altnames}..." echo " + Requesting new certificate order from CA..."
result="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}')" result="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}')"
authorizations="$(echo ${result} | get_json_array_value authorizations)" order_authorizations="$(echo ${result} | get_json_array_value authorizations)"
finalize="$(echo "${result}" | get_json_string_value finalize)" finalize="$(echo "${result}" | get_json_string_value finalize)"
local idx=0 local idx=0
for uris in ${authorizations}; do for uri in ${order_authorizations}; do
authorization[${idx}]="${uris}" authorizations[${idx}]="$(echo "${uri}" | _sed -e 's/\"(.*)".*/\1/')"
idx=$((idx+1))
done
echo " + Received ${idx} authorizations URLs from the CA"
else
# Copy $altnames to $authorizations (just doing this to reduce duplicate code later on)
local idx=0
for altname in ${altnames}; do
authorizations[${idx}]="${altname}"
idx=$((idx+1)) idx=$((idx+1))
done done
unset challenge_identifiers authorizations
fi fi
local idx=0; idy=-1 # Check if authorizations are valid and gather challenge information for pending authorizations
# Request challenges local idx=0
for altname in ${altnames}; do for authorization in ${authorizations[*]}; do
idy=$((idy+1)) if [[ "${API}" -eq 2 ]]; then
if [[ ${API} -eq 1 ]]; then # Receive authorization ($authorization is authz uri)
# Ask the acme-server for new challenge token and extract them from the resulting json block response="$(http_request get "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" | clean_json)"
echo " + Requesting challenge for ${altname}..." identifier="$(echo "${response}" | get_json_dict_value identifier | get_json_string_value value)"
response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}' | clean_json)" echo " + Handling authorization for ${identifier}"
else else
echo " + Handling challenge for ${altname}..." # Request new authorization ($authorization is altname)
uris="$(<<<"${authorization[${idy}]}" _sed -e 's/\"(.*)".*/\1/')" identifier="${authorization}"
response="$(http_request get "${uris}" | clean_json)" echo " + Requesting authorization for ${identifier}..."
response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${identifier}"'"}}' | clean_json)"
fi fi
challenge_status="$(printf '%s' "${response}" | rm_json_arrays | get_json_string_value status)" # Check if authorization has already been validated
if [ "${challenge_status}" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then if [ "$(echo "${response}" | sed -E 's/"challenges": \[\{.*\}\]//' | get_json_string_value status)" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then
echo " + Already validated!" echo " + Found valid authorization for ${identifier}"
continue continue
fi fi
challenges="$(printf '%s\n' "${response}" | sed -n 's/.*\("challenges":[^\[]*\[[^]]*]\).*/\1/p')" # Find challenge in authorization
challenge="$(<<<"${challenges}" _sed -e 's/^[^\[]+\[(.+)\]$/\1/' -e 's/\}(, (\{)|(\]))/}\'$'\n''\2/g' | grep \""${CHALLENGETYPE}"\")" challenges="$(echo "${response}" | _sed 's/.*"challenges": \[(\{.*\})\].*/\1/')"
challenge="$(<<<"${challenges}" _sed -e 's/^[^\[]+\[(.+)\]$/\1/' -e 's/\}(, (\{)|(\]))/}\'$'\n''\2/g' | grep \""${CHALLENGETYPE}"\" || true)"
if [ -z "${challenge}" ]; then
allowed_validations="$(grep -Eo '"type": "[^"]+"' <<< "${challenges}" | grep -Eo ' "[^"]+"' | _sed -e 's/"//g' -e 's/^ //g')"
_exiterr "Validating this certificate is not possible using ${CHALLENGETYPE}. Possible validation methods are: ${allowed_validations}"
fi
challenge_token="$(printf '%s' "${challenge}" | get_json_string_value token | _sed 's/[^A-Za-z0-9_\-]/_/g')" # Gather challenge information
if [[ ${API} -eq 1 ]]; then challenge_identifier[${idx}]="${identifier}"
challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)" challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)"
if [[ ${API} -eq 2 ]]; then
challenge_uris[${idx}]="$(echo "${challenge}" | get_json_string_value url)"
else else
challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value url)" challenge_uris[${idx}]="$(echo "${challenge}" | get_json_string_value uri)"
fi fi
if [[ -z "${challenge_token}" ]] || [[ -z "${challenge_uri}" ]]; then # Prepare challenge tokens and deployment parameters
_exiterr "Can't retrieve challenges (${response})" keyauth="${challenge_tokens[${idx}]}.${thumbprint}"
fi
# Challenge response consists of the challenge token and the thumbprint of our public certificate
keyauth="${challenge_token}.${thumbprint}"
case "${CHALLENGETYPE}" in case "${CHALLENGETYPE}" in
"http-01") "http-01")
# Store challenge response in well-known location and make world-readable (so that a webserver can access it) # Store challenge response in well-known location and make world-readable (so that a webserver can access it)
printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_token}" printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_tokens[${idx}]}"
chmod a+r "${WELLKNOWN}/${challenge_token}" chmod a+r "${WELLKNOWN}/${challenge_tokens[${idx}]}"
keyauth_hook="${keyauth}" keyauth_hook="${keyauth}"
;; ;;
"dns-01") "dns-01")
@@ -680,89 +698,86 @@ sign_csr() {
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)" keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
;; ;;
esac esac
challenge_altnames[${idx}]="${altname}"
challenge_uris[${idx}]="${challenge_uri}"
keyauths[${idx}]="${keyauth}" keyauths[${idx}]="${keyauth}"
challenge_tokens[${idx}]="${challenge_token}" deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}"
# Note: assumes args will never have spaces!
deploy_args[${idx}]="${altname} ${challenge_token} ${keyauth_hook}"
idx=$((idx+1)) idx=$((idx+1))
done done
challenge_count="${idx}" local num_pending_challenges=${idx}
echo " + ${num_pending_challenges} pending challenge(s)"
# Wait for hook script to deploy the challenges if used # Detect duplicate challenge identifiers
if [[ ${challenge_count} -ne 0 ]]; then if [ "${HOOK_CHAIN}" = "yes" ] && [ -n "$(tr ' ' '\n' <<< "${challenge_identifier[*]}" | sort | uniq -d)" ]; then
echo "!! Disabling HOOK_CHAIN for this certificate (see https://dehydrated.de/docs/hook_chain.md#problem-with-wildcard-certificates for more information)"
HOOK_CHAIN=no
fi
# Deploy challenge tokens using chained hook
if [[ ${num_pending_challenges} -ne 0 ]]; then
# shellcheck disable=SC2068 # shellcheck disable=SC2068
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "deploy_challenge" ${deploy_args[@]} if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then
echo " + Deploying challenge tokens..."
"${HOOK}" "deploy_challenge" ${deploy_args[@]}
fi
fi fi
# Respond to challenges # Validate pending challenges
reqstatus="valid" local idx=0
idx=0 while [ ${idx} -lt ${num_pending_challenges} ]; do
if [ ${challenge_count} -ne 0 ]; then echo " + Responding to challenge for ${challenge_identifier[${idx}]} authorization..."
for altname in "${challenge_altnames[@]:0}"; do
challenge_token="${challenge_tokens[${idx}]}"
keyauth="${keyauths[${idx}]}"
# Wait for hook script to deploy the challenge if used # Run hook script to deploy the challenge token
# shellcheck disable=SC2086 if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]}
# Ask the acme-server to verify our challenge and wait until it is no longer pending
echo " + Responding to challenge for ${altname}..."
if [[ ${API} -eq 1 ]]; then
result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}' | clean_json)"
else
result="$(signed_request "${challenge_uris[${idx}]}" '{"keyAuthorization": "'"${keyauth}"'"}' | clean_json)"
fi
reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
while [[ "${reqstatus}" = "pending" ]]; do
sleep 1
if [[ ${API} -eq 1 ]]; then
result="$(http_request get "${challenge_uris[${idx}]}")"
else
result="$(http_request get "${challenge_uris[${idx}]}")"
fi
reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
done
[[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_token}"
# Wait for hook script to clean the challenge if used
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token}" ]]; then
# shellcheck disable=SC2086
"${HOOK}" "clean_challenge" ${deploy_args[${idx}]}
fi
idx=$((idx+1))
if [[ "${reqstatus}" = "valid" ]]; then
echo " + Challenge is valid!"
else
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "invalid_challenge" "${altname}" "${result}"
break
fi
done
fi
# Wait for hook script to clean the challenges if used
# shellcheck disable=SC2068
if [[ ${challenge_count} -ne 0 ]]; then
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[@]}
fi
if [[ "${reqstatus}" != "valid" ]]; then
# Clean up any remaining challenge_tokens if we stopped early
if [[ "${CHALLENGETYPE}" = "http-01" ]] && [[ ${challenge_count} -ne 0 ]]; then
while [ ${idx} -lt ${#challenge_tokens[@]} ]; do
rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
idx=$((idx+1))
done
fi fi
_exiterr "Challenge is invalid! (returned: ${reqstatus}) (result: ${result})" # Ask the acme-server to verify our challenge and wait until it is no longer pending
if [[ ${API} -eq 1 ]]; then
result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauths[${idx}]}"'"}' | clean_json)"
else
result="$(signed_request "${challenge_uris[${idx}]}" '{"keyAuthorization": "'"${keyauths[${idx}]}"'"}' | clean_json)"
fi
reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
while [[ "${reqstatus}" = "pending" ]]; do
sleep 1
result="$(http_request get "${challenge_uris[${idx}]}")"
reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
done
[[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
# Run hook script to clean the challenge token
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then
# shellcheck disable=SC2086
"${HOOK}" "clean_challenge" ${deploy_args[${idx}]}
fi
idx=$((idx+1))
if [[ "${reqstatus}" = "valid" ]]; then
echo " + Challenge is valid!"
else
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "invalid_challenge" "${altname}" "${result}"
break
fi
done
if [[ ${num_pending_challenges} -ne 0 ]]; then
# Clean challenge tokens using chained hook
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[@]}
# Clean remaining challenge tokens if validation has failed
if [[ "${reqstatus}" != "valid" ]]; then
if [[ "${CHALLENGETYPE}" = "http-01" ]] && [[ ${num_pending_challenges} -ne 0 ]]; then
while [ ${idx} -lt ${num_pending_challenges} ]; do
rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
idx=$((idx+1))
done
fi
_exiterr "Challenge is invalid! (returned: ${reqstatus}) (result: ${result})"
fi
fi fi
# Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem # Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem

View File

@@ -61,3 +61,14 @@ HOOK: deploy_cert lukas.im /etc/dehydrated/certs/lukas.im/privkey.pem /etc/dehyd
+ Done! + Done!
``` ```
# Problem with wildcard certificates
For wildcard certificates the upper level domain is used for verification, e.g.
`*.foo.example.com` will be verified at `foo.example.com`.
In cases where both `foo.example.com` and `*.foo.example.com` would have to be
validated there would be a conflict since both will have different tokens but
both are expected to be resolved under `_acme-challenge.foo.example.com`.
If dehydrated detects this kind of configuration it will automatically fall back
to non-chaining behaviour (until the next certificate).

266
test.sh
View File

@@ -1,266 +0,0 @@
#!/usr/bin/env bash
# Fail early
set -eu -o pipefail
# Check if running in CI environment
if [[ ! "${CI:-false}" == "true" ]]; then
echo "ERROR: Not running in CI environment!"
exit 1
fi
_TEST() {
echo
echo "${1} "
}
_SUBTEST() {
echo -n " + ${1} "
}
_PASS() {
echo -e "[\u001B[32mPASS\u001B[0m]"
}
_FAIL() {
echo -e "[\u001B[31mFAIL\u001B[0m]"
echo
echo "Problem: ${@}"
echo
echo "STDOUT:"
cat tmplog
echo
echo "STDERR:"
cat errorlog
exit 1
}
_CHECK_FILE() {
_SUBTEST "Checking if file '${1}' exists..."
if [[ -e "${1}" ]]; then
_PASS
else
_FAIL "Missing file: ${1}"
fi
}
_CHECK_LOG() {
_SUBTEST "Checking if log contains '${1}'..."
if grep -- "${1}" tmplog > /dev/null; then
_PASS
else
_FAIL "Missing in log: ${1}"
fi
}
_CHECK_NOT_LOG() {
_SUBTEST "Checking if log doesn't contain '${1}'..."
if grep -- "${1}" tmplog > /dev/null; then
_FAIL "Found in log: ${1}"
else
_PASS
fi
}
_CHECK_ERRORLOG() {
_SUBTEST "Checking if errorlog is empty..."
if [[ -z "$(cat errorlog)" ]]; then
_PASS
else
_FAIL "Non-empty errorlog"
fi
}
# If not found (should be cached in travis) download ngrok
if [[ ! -e "ngrok/ngrok" ]]; then
(
mkdir -p ngrok
cd ngrok
if [ "${TRAVIS_OS_NAME}" = "linux" ]; then
wget -O ngrok.zip https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-linux-amd64.zip
elif [ "${TRAVIS_OS_NAME}" = "osx" ]; then
wget -O ngrok.zip https://bin.equinox.io/c/4VmDzA7iaHb/ngrok-stable-darwin-amd64.zip
else
echo "No ngrok for ${TRAVIS_OS_NAME}"
exit 1
fi
unzip ngrok.zip ngrok
chmod +x ngrok
)
fi
# Run ngrok and grab temporary url from logfile
ngrok/ngrok http 8080 --log stdout --log-format logfmt --log-level debug > tmp.log &
ngrok/ngrok http 8080 --log stdout --log-format logfmt --log-level debug > tmp2.log &
ngrok/ngrok http 8080 --log stdout --log-format logfmt --log-level debug > tmp3.log &
sleep 2
TMP_URL="$(grep -Eo "Hostname:[a-z0-9]+.ngrok.io" tmp.log | head -1 | cut -d':' -f2)"
TMP2_URL="$(grep -Eo "Hostname:[a-z0-9]+.ngrok.io" tmp2.log | head -1 | cut -d':' -f2)"
TMP3_URL="$(grep -Eo "Hostname:[a-z0-9]+.ngrok.io" tmp3.log | head -1 | cut -d':' -f2)"
if [[ -z "${TMP_URL}" ]] || [[ -z "${TMP2_URL}" ]] || [[ -z "${TMP3_URL}" ]]; then
echo "Couldn't get an url from ngrok, not a dehydrated bug, tests can't continue."
exit 1
fi
# Run python webserver in .acme-challenges directory to serve challenge responses
mkdir -p .acme-challenges/.well-known/acme-challenge
(
cd .acme-challenges
python -m SimpleHTTPServer 8080 > /dev/null 2> /dev/null
) &
# Generate config and create empty domains.txt
echo 'CA="https://testca.kurz.pw/directory"' > config
echo 'CA_TERMS="https://testca.kurz.pw/terms"' >> config
echo 'WELLKNOWN=".acme-challenges/.well-known/acme-challenge"' >> config
echo 'RENEW_DAYS="14"' >> config
touch domains.txt
# Check if help command is working
_TEST "Checking if help command is working..."
./dehydrated --help > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Default command: help"
_CHECK_LOG "--help (-h)"
_CHECK_LOG "--domain (-d) domain.tld"
_CHECK_ERRORLOG
# Register account key without LICENSE set
_TEST "Register account key without LICENSE set"
./dehydrated --register > tmplog 2> errorlog && _FAIL "Script execution failed"
_CHECK_LOG "To accept these terms"
_CHECK_ERRORLOG
# Register account key and agreeing to terms
_TEST "Register account key without LICENSE set"
./dehydrated --register --accept-terms > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Registering account key"
_CHECK_FILE accounts/*/account_key.pem
_CHECK_ERRORLOG
# Delete accounts and add LICENSE to config for normal operation
rm -rf accounts
echo 'LICENSE="https://testca.kurz.pw/terms/v1"' >> config
# Run in cron mode with empty domains.txt (should only generate private key and exit)
_TEST "First run in cron mode, checking if private key is generated and registered"
./dehydrated --cron > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Registering account key"
_CHECK_FILE accounts/*/account_key.pem
_CHECK_ERRORLOG
# Temporarily move config out of the way and try signing certificate by using temporary config location
_TEST "Try signing using temporary config location and with domain as command line parameter"
mv config tmp_config
./dehydrated --cron --domain "${TMP_URL}" --domain "${TMP2_URL}" --accept-terms -f tmp_config > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_NOT_LOG "Checking domain name(s) of existing cert"
_CHECK_LOG "Generating private key"
_CHECK_LOG "Requesting challenge for ${TMP_URL}"
_CHECK_LOG "Requesting challenge for ${TMP2_URL}"
_CHECK_LOG "Challenge is valid!"
_CHECK_LOG "Creating fullchain.pem"
_CHECK_LOG "Done!"
_CHECK_ERRORLOG
mv tmp_config config
# Add third domain to command-lime, should force renewal.
_TEST "Run in cron mode again, this time adding third domain, should force renewal."
./dehydrated --cron --domain "${TMP_URL}" --domain "${TMP2_URL}" --domain "${TMP3_URL}" > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Domain name(s) are not matching!"
_CHECK_LOG "Forcing renew."
_CHECK_LOG "Generating private key"
_CHECK_LOG "Requesting challenge for ${TMP_URL}"
_CHECK_LOG "Requesting challenge for ${TMP2_URL}"
_CHECK_LOG "Requesting challenge for ${TMP3_URL}"
_CHECK_LOG "Challenge is valid!"
_CHECK_LOG "Creating fullchain.pem"
_CHECK_LOG "Done!"
_CHECK_ERRORLOG
# Prepare domains.txt
# Modify TMP3_URL to be uppercase to check for upper-lower-case mismatch bugs
echo "${TMP_URL} ${TMP2_URL} $(tr 'a-z' 'A-Z' <<<"${TMP3_URL}")" >> domains.txt
# Run in cron mode again (should find a non-expiring certificate and do nothing)
_TEST "Run in cron mode again, this time with domain in domains.txt, should find non-expiring certificate"
./dehydrated --cron > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Checking domain name(s) of existing cert... unchanged."
_CHECK_LOG "Skipping renew"
_CHECK_ERRORLOG
# Disable private key renew
echo 'PRIVATE_KEY_RENEW="no"' >> config
# Run in cron mode one last time, with domain in domains.txt and force-resign (should find certificate, resign anyway, and not generate private key)
_TEST "Run in cron mode one last time, with domain in domains.txt and force-resign"
./dehydrated --cron --force > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Checking domain name(s) of existing cert... unchanged."
_CHECK_LOG "Ignoring because renew was forced!"
_CHECK_NOT_LOG "Generating private key"
_CHECK_LOG "Requesting challenge for ${TMP_URL}"
_CHECK_LOG "Requesting challenge for ${TMP2_URL}"
_CHECK_LOG "Requesting challenge for ${TMP3_URL}"
_CHECK_LOG "Already validated!"
_CHECK_LOG "Creating fullchain.pem"
_CHECK_LOG "Done!"
_CHECK_ERRORLOG
# Check if signcsr command is working
_TEST "Running signcsr command"
./dehydrated --signcsr certs/${TMP_URL}/cert.csr > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "BEGIN CERTIFICATE"
_CHECK_LOG "END CERTIFICATE"
_CHECK_NOT_LOG "ERROR"
# Check if renewal works
_TEST "Run in cron mode again, to check if renewal works"
echo 'RENEW_DAYS="300"' >> config
./dehydrated --cron > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Checking domain name(s) of existing cert... unchanged."
_CHECK_LOG "Renewing!"
_CHECK_ERRORLOG
# Check if certificate is valid in various ways
_TEST "Verifying certificate..."
_SUBTEST "Verifying certificate on its own..."
openssl x509 -in "certs/${TMP_URL}/cert.pem" -noout -text > tmplog 2> errorlog && _PASS || _FAIL
_CHECK_LOG "CN=${TMP_URL}"
_CHECK_LOG "${TMP2_URL}"
_SUBTEST "Verifying file with full chain..."
openssl x509 -in "certs/${TMP_URL}/fullchain.pem" -noout -text > /dev/null 2>> errorlog && _PASS || _FAIL
_SUBTEST "Verifying certificate against CA certificate..."
curl -s https://testca.kurz.pw/acme/issuer-cert | openssl x509 -inform DER -outform PEM > ca.pem
(openssl verify -verbose -CAfile "ca.pem" -purpose sslserver "certs/${TMP_URL}/fullchain.pem" 2>&1 || true) | (grep -v ': OK$' || true) >> errorlog 2>> errorlog && _PASS || _FAIL
_CHECK_ERRORLOG
# Revoke certificate using certificate key
_TEST "Revoking certificate..."
./dehydrated --revoke "certs/${TMP_URL}/cert.pem" --privkey "certs/${TMP_URL}/privkey.pem" > tmplog 2> errorlog || _FAIL "Script execution failed"
REAL_CERT="$(readlink -n "certs/${TMP_URL}/cert.pem")"
_CHECK_LOG "Revoking certs/${TMP_URL}/${REAL_CERT}"
_CHECK_LOG "Done."
_CHECK_FILE "certs/${TMP_URL}/${REAL_CERT}-revoked"
_CHECK_ERRORLOG
# Enable private key renew
echo 'PRIVATE_KEY_RENEW="yes"' >> config
echo 'PRIVATE_KEY_ROLLOVER="yes"' >> config
# Check if Rolloverkey creation works
_TEST "Testing Rolloverkeys..."
_SUBTEST "First Run: Creating rolloverkey"
./dehydrated --cron --domain "${TMP2_URL}" > tmplog 2> errorlog || _FAIL "Script execution failed"
CERT_ROLL_HASH=$(openssl rsa -in certs/${TMP2_URL}/privkey.roll.pem -outform DER -pubout 2>/dev/null | openssl sha -sha256)
_CHECK_LOG "Generating private key"
_CHECK_LOG "Generating private rollover key"
_SUBTEST "Second Run: Force Renew, Use rolloverkey"
./dehydrated --cron --force --domain "${TMP2_URL}" > tmplog 2> errorlog || _FAIL "Script execution failed"
CERT_NEW_HASH=$(openssl rsa -in certs/${TMP2_URL}/privkey.pem -outform DER -pubout 2>/dev/null | openssl sha -sha256)
_CHECK_LOG "Generating private key"
_CHECK_LOG "Moving Rolloverkey into position"
_SUBTEST "Verifying Hash Rolloverkey and private key second run"
[[ "${CERT_ROLL_HASH}" = "${CERT_NEW_HASH}" ]] && _PASS || _FAIL
_CHECK_ERRORLOG
# Test cleanup command
_TEST "Cleaning up certificates"
./dehydrated --cleanup > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Moving unused file to archive directory: ${TMP_URL}/cert-"
_CHECK_LOG "Moving unused file to archive directory: ${TMP_URL}/chain-"
_CHECK_LOG "Moving unused file to archive directory: ${TMP_URL}/fullchain-"
_CHECK_ERRORLOG
# All done
exit 0