From: Martin Strobel Date: Mon, 8 Jan 2018 11:38:01 +0000 (+0100) Subject: ACME v02 Support X-Git-Tag: v0.6.0~44 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=68cb1e066111eb80433ee8f8c1d8c35f84fff9b1;p=thirdparty%2Fdehydrated.git ACME v02 Support --- diff --git a/CHANGELOG b/CHANGELOG index 7de2c4f..304576a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,7 +6,7 @@ This file contains a log of major changes in dehydrated - ... ## Added -- ... +- Support for ACME v02 ## [0.5.0] - 2018-01-13 ## Changed diff --git a/dehydrated b/dehydrated index c2af61c..00dd321 100755 --- a/dehydrated +++ b/dehydrated @@ -138,6 +138,7 @@ load_config() { AUTO_CLEANUP="no" DEHYDRATED_USER= DEHYDRATED_GROUP= + API=1 if [[ -z "${CONFIG:-}" ]]; then echo "#" >&2 @@ -256,14 +257,25 @@ init_system() { # Get CA URLs CA_DIRECTORY="$(http_request get "${CA}")" - CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" && - CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" && - CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" && - # shellcheck disable=SC2015 - CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revoke-cert)" || - _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint." - # Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part - CA_REG=${CA_NEW_REG/new-reg/reg} + if [[ ${API} -eq 1 ]]; then + # shellcheck disable=SC2015 + CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" && + CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" && + CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" && + CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revoke-cert)" || + _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint." + # Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part + CA_REG=${CA_NEW_REG/new-reg/reg} + else + # shellcheck disable=SC2015 + CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" && + CA_NEW_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" && + CA_NEW_ACCOUNT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newAccount)" && + CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revokeCert)" || + _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint." + # Since acct URI is missing from directory we will assume it is the same as CA_NEW_ACCOUNT without the new part + CA_ACCOUNT=${CA_NEW_ACCOUNT/new-acct/acct} + fi # Export some environment variables to be used in hook script export WELLKNOWN BASEDIR CERTDIR CONFIG COMMAND @@ -314,17 +326,25 @@ init_system() { echo "+ Registering account key with ACME server..." FAILED=false - if [[ -z "${CA_NEW_REG}" ]]; then + if [[ ${API} -eq 1 && -z "${CA_NEW_REG}" ]] || [[ ${API} -eq 2 && -z "${CA_NEW_ACCOUNT}" ]]; then echo "Certificate authority doesn't allow registrations." FAILED=true fi # If an email for the contact has been provided then adding it to the registration request if [[ "${FAILED}" = "false" ]]; then - if [[ -n "${CONTACT_EMAIL}" ]]; then - (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + if [[ ${API} -eq 1 ]]; then + if [[ -n "${CONTACT_EMAIL}" ]]; then + (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + else + (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + fi else - (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + if [[ -n "${CONTACT_EMAIL}" ]]; then + (signed_request "${CA_NEW_ACCOUNT}" '{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + else + (signed_request "${CA_NEW_ACCOUNT}" '{"termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + fi fi fi @@ -380,6 +400,13 @@ get_json_string_value() { sed -n "${filter}" } +# Get array value from json dictionary +get_json_array_value() { + local filter + filter=$(printf 's/.*"%s": *\\[\([^]]*\)\\].*/\\1/p' "$1") + sed -n "${filter}" +} + # Get integer value from json get_json_int_value() { local filter @@ -482,20 +509,40 @@ signed_request() { payload64="$(printf '%s' "${2}" | urlbase64)" # Retrieve nonce from acme-server - nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" + if [[ ${API} -eq 1 ]]; then + nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" + else + nonce="$(http_request head "${CA_NEW_NONCE}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" + fi # Build header with just our public key and algorithm information header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}' - # Build another header which also contains the previously received nonce and encode it as urlbase64 - protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}' - protected64="$(printf '%s' "${protected}" | urlbase64)" + if [[ ${API} -eq 1 ]]; then + # Build another header which also contains the previously received nonce and encode it as urlbase64 + protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}' + protected64="$(printf '%s' "${protected}" | urlbase64)" + else + # Build another header which also contains the previously received nonce and url and encode it as urlbase64 + if [[ -e "${ACCOUNT_KEY_JSON}" ]] && [[ -n "$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" ]]; then + REG_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" + protected='{"alg": "RS256", "kid": "'"${CA_ACCOUNT}/${REG_ID}"'", "url": "'"${1}"'", "nonce": "'"${nonce}"'"}' + else + protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "url": "'"${1}"'", "nonce": "'"${nonce}"'"}' + fi + protected64="$(printf '%s' "${protected}" | urlbase64)" + fi # Sign header with nonce and our payload with our private key and encode signature as urlbase64 signed64="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)" - # Send header + extended header + payload + signature to the acme-server - data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' + if [[ ${API} -eq 1 ]]; then + # Send header + extended header + payload + signature to the acme-server + data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' + else + # Send extended header + payload + signature to the acme-server + data='{"protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' + fi http_request post "${1}" "${data}" } @@ -548,22 +595,54 @@ sign_csr() { fi export altnames - if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then + if [[ ${API} -eq 1 ]]; then + if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then + _exiterr "Certificate authority doesn't allow certificate signing" + fi + elif [[ ${API} -eq 2 ]] && [[ -z "${CA_NEW_ORDER}" ]]; then _exiterr "Certificate authority doesn't allow certificate signing" fi - local idx=0 if [[ -n "${ZSH_VERSION:-}" ]]; then - local -A challenge_altnames challenge_uris challenge_tokens keyauths deploy_args + local -A challenge_altnames challenge_uris challenge_tokens keyauths deploy_args authorization else - local -a challenge_altnames challenge_uris challenge_tokens keyauths deploy_args + local -a challenge_altnames challenge_uris challenge_tokens keyauths deploy_args authorization + fi + + if [[ ${API} -eq 2 ]]; then + # APIv2 + for altname in ${altnames}; do + challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")" + done + challenge_identifiers="[${challenge_identifiers%, }]" + + echo " + Requesting challenges for ${altnames}..." + result="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}')" + + 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}" + idx=$((idx+1)) + done + + unset challenge_identifiers authorizations fi + local idx=0; idy=-1 # Request challenges for altname in ${altnames}; do - # 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)" + 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)" + else + uris="$(<<<"${authorization[${idy}]}" _sed -e 's/\"(.*)".*/\1/')" + response="$(http_request get "${uris}" | 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 @@ -575,7 +654,11 @@ sign_csr() { repl=$'\n''{' # fix syntax highlighting in Vim challenge="$(printf "%s" "${challenges//\{/${repl}}" | grep \""${CHALLENGETYPE}"\")" challenge_token="$(printf '%s' "${challenge}" | get_json_string_value token | _sed 's/[^A-Za-z0-9_\-]/_/g')" - challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)" + if [[ ${API} -eq 1 ]]; then + challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)" + else + challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value url)" + fi if [[ -z "${challenge_token}" ]] || [[ -z "${challenge_uri}" ]]; then _exiterr "Can't retrieve challenges (${response})" @@ -627,13 +710,21 @@ sign_csr() { # Ask the acme-server to verify our challenge and wait until it is no longer pending echo " + Responding to challenge for ${altname}..." - result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}' | clean_json)" + 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 - result="$(http_request get "${challenge_uris[${idx}]}")" + 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 @@ -676,8 +767,13 @@ sign_csr() { # Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem echo " + Requesting certificate..." csr64="$( <<<"${csr}" "${OPENSSL}" req -config "${OPENSSL_CNF}" -outform DER | urlbase64)" - crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)" - crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )" + if [[ ${API} -eq 1 ]]; then + crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | "${OPENSSL}" base64 -e)" + crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )" + else + result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | clean_json | get_json_string_value certificate)" + crt="$(http_request get "${result}")" + fi # Try to load the certificate to detect corruption echo " + Checking certificate..." @@ -755,7 +851,11 @@ sign_domain() { export altnames echo " + Signing domains..." - if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then + if [[ ${API} -eq 1 ]]; then + if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then + _exiterr "Certificate authority doesn't allow certificate signing" + fi + elif [[ ${API} -eq 2 ]] && [[ -z "${CA_NEW_ORDER}" ]]; then _exiterr "Certificate authority doesn't allow certificate signing" fi @@ -917,11 +1017,20 @@ command_account() { fi echo "+ Updating registration id: ${REG_ID} contact information..." - # If an email for the contact has been provided then adding it to the registered account - if [[ -n "${CONTACT_EMAIL}" ]]; then - (signed_request "${CA_REG}"/"${REG_ID}" '{"resource": "reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true + if [[ ${API} -eq 1 ]]; then + # If an email for the contact has been provided then adding it to the registered account + if [[ -n "${CONTACT_EMAIL}" ]]; then + (signed_request "${CA_REG}"/"${REG_ID}" '{"resource": "reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true + else + (signed_request "${CA_REG}"/"${REG_ID}" '{"resource": "reg", "contact":[]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true + fi else - (signed_request "${CA_REG}"/"${REG_ID}" '{"resource": "reg", "contact":[]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true + # If an email for the contact has been provided then adding it to the registered account + if [[ -n "${CONTACT_EMAIL}" ]]; then + (signed_request "${CA_ACCOUNT}"/"${REG_ID}" '{"contact":["mailto:'"${CONTACT_EMAIL}"'"]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true + else + (signed_request "${CA_ACCOUNT}"/"${REG_ID}" '{"contact":[]}' > "${NEW_ACCOUNT_KEY_JSON}") || FAILED=true + fi fi if [[ "${FAILED}" = "true" ]]; then @@ -966,7 +1075,7 @@ command_sign_domains() { # Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire ORIGIFS="${IFS}" IFS=$'\n' - for line in $(<"${DOMAINS_TXT}" tr -d '\r' | awk '{print tolower($0)}' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' -e 's/([^ ])>/\1 >/g' -e 's/> />/g' | (grep -vE '^(#|$)' || true)); do + for line in "$(<"${DOMAINS_TXT}" tr -d '\r' | awk '{print tolower($0)}' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' -e 's/([^ ])>/\1 >/g' -e 's/> />/g' | (grep -vE '^(#|$)' || true))"; do reset_configvars IFS="${ORIGIFS}" alias="$(grep -Eo '>[^ ]+' <<< "${line}" || true)" @@ -1194,7 +1303,11 @@ command_revoke() { echo "Revoking ${cert}" cert64="$("${OPENSSL}" x509 -in "${cert}" -inform PEM -outform DER | urlbase64)" - response="$(signed_request "${CA_REVOKE_CERT}" '{"resource": "revoke-cert", "certificate": "'"${cert64}"'"}' | clean_json)" + if [[ ${API} -eq 1 ]]; then + response="$(signed_request "${CA_REVOKE_CERT}" '{"resource": "revoke-cert", "certificate": "'"${cert64}"'"}' | clean_json)" + else + response="$(signed_request "${CA_REVOKE_CERT}" '{"certificate": "'"${cert64}"'"}' | clean_json)" + fi # if there is a problem with our revoke request _request (via signed_request) will report this and "exit 1" out # so if we are here, it is safe to assume the request was successful echo " + Done." @@ -1303,6 +1416,7 @@ main() { fi } + # shellcheck disable=SC2199 [[ -z "${@}" ]] && eval set -- "--help" while (( ${#} )); do diff --git a/docs/examples/config b/docs/examples/config index 7132a68..a14abaf 100644 --- a/docs/examples/config +++ b/docs/examples/config @@ -110,3 +110,6 @@ # Automatic cleanup (default: no) #AUTO_CLEANUP="no" + +# ACME API version (default: 1) +#API=1