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
+127 -112
View File
@@ -407,6 +407,13 @@ get_json_array_value() {
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_json_int_value() {
local filter
@@ -604,75 +611,86 @@ sign_csr() {
fi
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
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
if [[ ${API} -eq 2 ]]; then
# APIv2
# Initial step: Find which authorizations we're dealing with
if [[ ${API} -eq 2 ]]; then
# Request new order and store authorization URIs
for altname in ${altnames}; do
challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")"
done
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}"'}')"
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)"
local idx=0
for uris in ${authorizations}; do
authorization[${idx}]="${uris}"
for uri in ${order_authorizations}; do
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))
done
unset challenge_identifiers authorizations
fi
local idx=0; idy=-1
# Request challenges
for altname in ${altnames}; do
idy=$((idy+1))
if [[ ${API} -eq 1 ]]; then
# Ask the acme-server for new challenge token and extract them from the resulting json block
echo " + Requesting challenge for ${altname}..."
response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}' | clean_json)"
# Check if authorizations are valid and gather challenge information for pending authorizations
local idx=0
for authorization in ${authorizations[*]}; do
if [[ "${API}" -eq 2 ]]; then
# Receive authorization ($authorization is authz uri)
response="$(http_request get "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" | clean_json)"
identifier="$(echo "${response}" | get_json_dict_value identifier | get_json_string_value value)"
echo " + Handling authorization for ${identifier}"
else
echo " + Handling challenge for ${altname}..."
uris="$(<<<"${authorization[${idy}]}" _sed -e 's/\"(.*)".*/\1/')"
response="$(http_request get "${uris}" | clean_json)"
# Request new authorization ($authorization is altname)
identifier="${authorization}"
echo " + Requesting authorization for ${identifier}..."
response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${identifier}"'"}}' | clean_json)"
fi
challenge_status="$(printf '%s' "${response}" | rm_json_arrays | get_json_string_value status)"
if [ "${challenge_status}" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then
echo " + Already validated!"
continue
# Check if authorization has already been validated
if [ "$(echo "${response}" | sed -E 's/"challenges": \[\{.*\}\]//' | get_json_string_value status)" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then
echo " + Found valid authorization for ${identifier}"
continue
fi
challenges="$(printf '%s\n' "${response}" | sed -n 's/.*\("challenges":[^\[]*\[[^]]*]\).*/\1/p')"
challenge="$(<<<"${challenges}" _sed -e 's/^[^\[]+\[(.+)\]$/\1/' -e 's/\}(, (\{)|(\]))/}\'$'\n''\2/g' | grep \""${CHALLENGETYPE}"\")"
# Find challenge in authorization
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')"
if [[ ${API} -eq 1 ]]; then
challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)"
# Gather challenge information
challenge_identifier[${idx}]="${identifier}"
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
challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value url)"
challenge_uris[${idx}]="$(echo "${challenge}" | get_json_string_value uri)"
fi
if [[ -z "${challenge_token}" ]] || [[ -z "${challenge_uri}" ]]; then
_exiterr "Can't retrieve challenges (${response})"
fi
# Challenge response consists of the challenge token and the thumbprint of our public certificate
keyauth="${challenge_token}.${thumbprint}"
# Prepare challenge tokens and deployment parameters
keyauth="${challenge_tokens[${idx}]}.${thumbprint}"
case "${CHALLENGETYPE}" in
"http-01")
# Store challenge response in well-known location and make world-readable (so that a webserver can access it)
printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_token}"
chmod a+r "${WELLKNOWN}/${challenge_token}"
printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_tokens[${idx}]}"
chmod a+r "${WELLKNOWN}/${challenge_tokens[${idx}]}"
keyauth_hook="${keyauth}"
;;
"dns-01")
@@ -680,89 +698,86 @@ sign_csr() {
keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)"
;;
esac
challenge_altnames[${idx}]="${altname}"
challenge_uris[${idx}]="${challenge_uri}"
keyauths[${idx}]="${keyauth}"
challenge_tokens[${idx}]="${challenge_token}"
# Note: assumes args will never have spaces!
deploy_args[${idx}]="${altname} ${challenge_token} ${keyauth_hook}"
deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}"
idx=$((idx+1))
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
if [[ ${challenge_count} -ne 0 ]]; then
# Detect duplicate challenge identifiers
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
[[ -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
# Respond to challenges
reqstatus="valid"
idx=0
if [ ${challenge_count} -ne 0 ]; then
for altname in "${challenge_altnames[@]:0}"; do
challenge_token="${challenge_tokens[${idx}]}"
keyauth="${keyauths[${idx}]}"
# Validate pending challenges
local idx=0
while [ ${idx} -lt ${num_pending_challenges} ]; do
echo " + Responding to challenge for ${challenge_identifier[${idx}]} authorization..."
# Wait for hook script to deploy the challenge if used
# shellcheck disable=SC2086
[[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${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
# Run hook script to deploy the challenge token
if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then
"${HOOK}" "deploy_challenge" ${deploy_args[${idx}]}
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
# Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem