diff --git a/dehydrated b/dehydrated index cc1e115b..35183f6c 100755 --- a/dehydrated +++ b/dehydrated @@ -236,7 +236,7 @@ _mktemp() { # Check for script dependencies check_dependencies() { # look for required binaries - for binary in grep mktemp diff sed awk curl cut; do + for binary in grep mktemp diff sed awk curl cut; do bin_path="$(command -v "${binary}" 2>/dev/null)" || _exiterr "This script requires ${binary}." [[ -x "${bin_path}" ]] || _exiterr "${binary} found in PATH but it's not executable" done @@ -318,7 +318,7 @@ load_config() { fi # Preset - CA_ZEROSSL="" + CA_ZEROSSL="https://acme.zerossl.com/v2/DV90" CA_LETSENCRYPT="https://acme-v02.api.letsencrypt.org/directory" # Default values @@ -424,6 +424,11 @@ load_config() { # Check BASEDIR and set default variables [[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}" + # Check for ca cli parameter + if [ -n "${PARAM_CA:-}" ]; then + CA="${PARAM_CA}" + fi + # Preset CAs if [ "${CA}" = "letsencrypt" ]; then CA="{$CA_LETSENCRYPT}" @@ -527,7 +532,8 @@ init_system() { 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_TERMS="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value termsOfService)" && + CA_TERMS="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value -p '"meta","termsOfService"')" && + CA_REQUIRES_EAB="$(printf "%s" "${CA_DIRECTORY}" | get_json_bool_value -p '"meta","externalAccountRequired"' || echo false)" && 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 @@ -539,6 +545,7 @@ init_system() { # Checking for private key ... register_new_key="no" + generated="false" if [[ -n "${PARAM_ACCOUNT_KEY:-}" ]]; then # a private key was specified from the command line so use it for this run echo "Using private key ${PARAM_ACCOUNT_KEY} instead of account key" @@ -557,6 +564,7 @@ init_system() { fi echo "+ Generating account key..." + generated="true" local tmp_account_key="$(_mktemp)" _openssl genrsa -out "${tmp_account_key}" "${KEYSIZE}" cat "${tmp_account_key}" > "${ACCOUNT_KEY}" @@ -582,6 +590,35 @@ init_system() { FAILED=true fi + # ZeroSSL special sauce + if [[ "${CA}" = "${CA_ZEROSSL}" ]]; then + if [[ -z "${EAB_KID:-}" ]] || [[ -z "${EAB_HMAC_KEY:-}" ]]; then + if [[ -z "${CONTACT_EMAIL}" ]]; then + echo "ZeroSSL requires contact email to be set or EAB_KID/EAB_HMAC_KEY to be manually configured" + FAILED=true + else + zeroapi="$(curl -s "https://api.zerossl.com/acme/eab-credentials-email" -d "email=${CONTACT_EMAIL}" | jsonsh)" + EAB_KID="$(printf "%s" "${zeroapi}" | get_json_string_value eab_kid)" + EAB_HMAC_KEY="$(printf "%s" "${zeroapi}" | get_json_string_value eab_hmac_key)" + if [[ -z "${EAB_KID:-}" ]] || [[ -z "${EAB_HMAC_KEY:-}" ]]; then + echo "Unknown error retrieving ZeroSSL API credentials" + echo "${zeroapi}" + FAILED=true + fi + fi + fi + fi + + # Check if external account is required + if [[ "${FAILED}" = "false" ]]; then + if [[ "${CA_REQUIRES_EAB}" = "true" ]]; then + if [[ -z "${EAB_KID:-}" ]] || [[ -z "${EAB_HMAC_KEY:-}" ]]; then + FAILED=true + echo "This CA requires an external account but no EAB_KID/EAB_HMAC_KEY has been configured" + fi + fi + fi + # If an email for the contact has been provided then adding it to the registration request if [[ "${FAILED}" = "false" ]]; then if [[ ${API} -eq 1 ]]; then @@ -591,11 +628,26 @@ init_system() { (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"${CA_TERMS}"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true fi else - if [[ -n "${CONTACT_EMAIL}" ]]; then - (signed_request "${CA_NEW_ACCOUNT}" '{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + if [[ -n "${EAB_KID:-}" ]] && [[ -n "${EAB_HMAC_KEY:-}" ]]; then + eab_url="${CA_NEW_ACCOUNT}" + eab_protected64="$(printf '{"alg":"HS256","kid":"%s","url":"%s"}' "${EAB_KID}" "${eab_url}" | urlbase64)" + eab_payload64="$(printf "%s" '{"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}' | urlbase64)" + eab_key="$(printf "%s" "${EAB_HMAC_KEY}" | deurlbase64)" + eab_signed64="$(printf '%s' "${eab_protected64}.${eab_payload64}" | "${OPENSSL}" dgst -binary -sha256 -hmac "${eab_key}" | urlbase64)" + + if [[ -n "${CONTACT_EMAIL}" ]]; then + regjson='{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true, "externalAccountBinding": {"protected": "'"${eab_protected64}"'", "payload": "'"${eab_payload64}"'", "signature": "'"${eab_signed64}"'"}}' + else + regjson='{"termsOfServiceAgreed": true, "externalAccountBinding": {"protected": "'"${eab_protected64}"'", "payload": "'"${eab_payload64}"'", "signature": "'"${eab_signed64}"'"}}' + fi else - (signed_request "${CA_NEW_ACCOUNT}" '{"termsOfServiceAgreed": true}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + if [[ -n "${CONTACT_EMAIL}" ]]; then + regjson='{"contact":["mailto:'"${CONTACT_EMAIL}"'"], "termsOfServiceAgreed": true}' + else + regjson='{"termsOfServiceAgreed": true}' + fi fi + (signed_request "${CA_NEW_ACCOUNT}" "${regjson}" > "${ACCOUNT_KEY_JSON}") || FAILED=true fi fi @@ -603,7 +655,10 @@ init_system() { echo >&2 echo >&2 echo "Error registering account key. See message above for more information." >&2 - rm "${ACCOUNT_KEY}" "${ACCOUNT_KEY_JSON}" + if [[ "${generated}" = "true" ]]; then + rm "${ACCOUNT_KEY}" + fi + rm -f "${ACCOUNT_KEY_JSON}" exit 1 fi elif [[ "${COMMAND:-}" = "register" ]]; then @@ -669,12 +724,27 @@ urlbase64() { "${OPENSSL}" base64 -e | tr -d '\n\r' | _sed -e 's:=*$::g' -e 'y:+/:-_:' } +# Decode data from url-safe formatted base64 +deurlbase64() { + data="$(cat | tr -d ' \n\r')" + modlen=$((${#data} % 4)) + padding="" + if [[ "${modlen}" = "2" ]]; then padding="=="; + elif [[ "${modlen}" = "3" ]]; then padding="="; fi + printf "%s%s" "${data}" "${padding}" | tr -d '\n\r' | _sed -e 'y:-_:+/:' | "${OPENSSL}" base64 -d -A +} + # Convert hex string to binary data hex2bin() { # Remove spaces, add leading zero, escape as hex string and parse with printf printf -- "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" } +# Convert binary data to hex string +bin2hex() { + hexdump | _sed 's/^[^ ]*//' | tr -d ' \n\r' +} + # OpenSSL writes to stderr/stdout even when there are no errors. So just # display the output if the exit code was != 0 to simplify debugging. _openssl() { @@ -1873,6 +1943,15 @@ main() { fi ;; + # PARAM_Usage: --ca url/preset + # PARAM_Description: Use specified CA URL or preset + --ca) + shift 1 + check_parameters "${1:-}" + [[ -n "${PARAM_CA:-}" ]] && _exiterr "CA can only be specified once!" + PARAM_CA="${1}" + ;; + # PARAM_Usage: --alias certalias # PARAM_Description: Use specified name for certificate directory (and per-certificate config) instead of the primary domain (only used if --domain is specified) --alias)