mirror of
https://github.com/dehydrated-io/dehydrated.git
synced 2026-04-20 15:31:21 +02:00
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:
14
.travis.yml
14
.travis.yml
@@ -1,14 +0,0 @@
|
|||||||
sudo: false
|
|
||||||
language: shell
|
|
||||||
|
|
||||||
os:
|
|
||||||
- linux
|
|
||||||
- osx
|
|
||||||
|
|
||||||
cache:
|
|
||||||
directories:
|
|
||||||
- ngrok
|
|
||||||
|
|
||||||
script:
|
|
||||||
- export CI="true"
|
|
||||||
- ./test.sh
|
|
||||||
239
dehydrated
239
dehydrated
@@ -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
|
||||||
|
|||||||
@@ -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
266
test.sh
@@ -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
|
|
||||||
Reference in New Issue
Block a user