X-Git-Url: https://git.street.me.uk/andy/dehydrated.git/blobdiff_plain/57197306d7d19d87b3b6b5deadd640d72cbe5f31..HEAD:/dehydrated diff --git a/dehydrated b/dehydrated index cb3bbc8..879a032 100755 --- a/dehydrated +++ b/dehydrated @@ -73,6 +73,7 @@ reset_configvars() { OPENSSL_CNF="${__OPENSSL_CNF}" RENEW_DAYS="${__RENEW_DAYS}" IP_VERSION="${__IP_VERSION}" + ALT_NAMES= } # verify configuration values @@ -81,7 +82,7 @@ verify_config() { if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then _exiterr "Challenge type dns-01 needs a hook script for deployment... can not continue." fi - if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" ]]; then + if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then _exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions." fi [[ "${KEY_ALGO}" =~ ^(rsa|prime256v1|secp384r1)$ ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... can not continue." @@ -105,7 +106,8 @@ load_config() { # Default values CA="https://acme-v01.api.letsencrypt.org/directory" - LICENSE="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf" + CA_TERMS="https://acme-v01.api.letsencrypt.org/terms" + LICENSE= CERTDIR= ACCOUNTDIR= CHALLENGETYPE="http-01" @@ -233,6 +235,24 @@ init_system() { else # Check if private account key exists, if it doesn't exist yet generate a new one (rsa key) if [[ ! -e "${ACCOUNT_KEY}" ]]; then + REAL_LICENSE="$(http_request head "${CA_TERMS}" | (grep Location: || true) | awk -F ': ' '{print $2}' | tr -d '\n\r')" + if [[ -z "${REAL_LICENSE}" ]]; then + printf '\n' + printf 'Error retrieving terms of service from certificate authority.\n' + printf 'Please set LICENSE in config manually.\n' + exit 1 + fi + if [[ ! "${LICENSE}" = "${REAL_LICENSE}" ]]; then + if [[ "${PARAM_ACCEPT_TERMS:-}" = "yes" ]]; then + LICENSE="${REAL_LICENSE}" + else + printf '\n' + printf 'To use dehydrated with this certificate authority you have to agree to their terms of service which you can find here: %s\n\n' "${REAL_LICENSE}" + printf 'To accept these terms of service run `%s --register --accept-terms`.\n' "${0}" + exit 1 + fi + fi + echo "+ Generating account key..." _openssl genrsa -out "${ACCOUNT_KEY}" "${KEYSIZE}" register_new_key="yes" @@ -249,14 +269,22 @@ init_system() { # If we generated a new private key in the step above we have to register it with the acme-server if [[ "${register_new_key}" = "yes" ]]; then echo "+ Registering account key with ACME server..." - [[ ! -z "${CA_NEW_REG}" ]] || _exiterr "Certificate authority doesn't allow registrations." - # If an email for the contact has been provided then adding it to the registration request FAILED=false - 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 + + if [[ -z "${CA_NEW_REG}" ]]; 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 + else + (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true + fi fi + if [[ "${FAILED}" = "true" ]]; then echo echo @@ -264,8 +292,10 @@ init_system() { rm "${ACCOUNT_KEY}" "${ACCOUNT_KEY_JSON}" exit 1 fi + elif [[ "${COMMAND:-}" = "register" ]]; then + echo "+ Account already registered!" + exit 0 fi - } # Different sed version for different os types... @@ -360,29 +390,31 @@ http_request() { fi if [[ ! "${statuscode:0:1}" = "2" ]]; then - echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2 - echo >&2 - echo "Details:" >&2 - cat "${tempcont}" >&2 - echo >&2 - echo >&2 + if [[ ! "${2}" = "${CA_TERMS}" ]] || [[ ! "${statuscode:0:1}" = "3" ]]; then + echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2 + echo >&2 + echo "Details:" >&2 + cat "${tempcont}" >&2 + echo >&2 + echo >&2 - # An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins) - if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then - errtxt=`cat ${tempcont}` - "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" - fi + # An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins) + if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then + errtxt=`cat ${tempcont}` + "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" + fi - rm -f "${tempcont}" + rm -f "${tempcont}" - # Wait for hook script to clean the challenge if used - if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token:+set}" ]]; then - "${HOOK}" "clean_challenge" '' "${challenge_token}" "${keyauth}" - fi + # Wait for hook script to clean the challenge if used + if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token:+set}" ]]; then + "${HOOK}" "clean_challenge" '' "${challenge_token}" "${keyauth}" + fi - # remove temporary domains.txt file if used - [[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}" - exit 1 + # remove temporary domains.txt file if used + [[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}" + exit 1 + fi fi cat "${tempcont}" @@ -425,7 +457,7 @@ extract_altnames() { reqtext="$( <<<"${csr}" openssl req -noout -text )" if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then # SANs used, extract these - altnames="$( <<<"${reqtext}" grep -A1 '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$' | tail -n1 )" + altnames="$( <<<"${reqtext}" awk '/X509v3 Subject Alternative Name:/{print;getline;print;}' | tail -n1 )" # split to one per line: # shellcheck disable=SC1003 altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )" @@ -600,6 +632,51 @@ sign_csr() { echo " + Done!" } +# grep issuer cert uri from certificate +get_issuer_cert_uri() { + certificate="${1}" + openssl x509 -in "${certificate}" -noout -text | (grep 'CA Issuers - URI:' | cut -d':' -f2-) || true +} + +# walk certificate chain, retrieving all intermediate certificates +walk_chain() { + local certificate + certificate="${1}" + + local issuer_cert_uri + issuer_cert_uri="${2:-}" + if [[ -z "${issuer_cert_uri}" ]]; then issuer_cert_uri="$(get_issuer_cert_uri "${certificate}")"; fi + if [[ -n "${issuer_cert_uri}" ]]; then + # create temporary files + local tmpcert + local tmpcert_raw + tmpcert_raw="$(_mktemp)" + tmpcert="$(_mktemp)" + + # download certificate + http_request get "${issuer_cert_uri}" > "${tmpcert_raw}" + + # PEM + if grep -q "BEGIN CERTIFICATE" "${tmpcert_raw}"; then mv "${tmpcert_raw}" "${tmpcert}" + # DER + elif openssl x509 -in "${tmpcert_raw}" -inform DER -out "${tmpcert}" -outform PEM 2> /dev/null > /dev/null; then : + # PKCS7 + elif openssl pkcs7 -in "${tmpcert_raw}" -inform DER -out "${tmpcert}" -outform PEM -print_certs 2> /dev/null > /dev/null; then : + # Unknown certificate type + else _exiterr "Unknown certificate type in chain" + fi + + local next_issuer_cert_uri + next_issuer_cert_uri="$(get_issuer_cert_uri "${tmpcert}")" + if [[ -n "${next_issuer_cert_uri}" ]]; then + printf "\n%s\n" "${issuer_cert_uri}" + cat "${tmpcert}" + walk_chain "${tmpcert}" "${next_issuer_cert_uri}" + fi + rm -f "${tmpcert}" "${tmpcert_raw}" + fi +} + # Create certificate for domain(s) sign_domain() { domain="${1}" @@ -672,14 +749,7 @@ sign_domain() { # Create fullchain.pem echo " + Creating fullchain.pem..." cat "${crt_path}" > "${CERTDIR}/${domain}/fullchain-${timestamp}.pem" - tmpchain="$(_mktemp)" - http_request get "$(openssl x509 -in "${CERTDIR}/${domain}/cert-${timestamp}.pem" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${tmpchain}" - if grep -q "BEGIN CERTIFICATE" "${tmpchain}"; then - mv "${tmpchain}" "${CERTDIR}/${domain}/chain-${timestamp}.pem" - else - openssl x509 -in "${tmpchain}" -inform DER -out "${CERTDIR}/${domain}/chain-${timestamp}.pem" -outform PEM - rm "${tmpchain}" - fi + walk_chain "${crt_path}" > "${CERTDIR}/${domain}/chain-${timestamp}.pem" cat "${CERTDIR}/${domain}/chain-${timestamp}.pem" >> "${CERTDIR}/${domain}/fullchain-${timestamp}.pem" # Update symlinks @@ -697,18 +767,32 @@ sign_domain() { echo " + Done!" } +# Usage: --register +# Description: Register account key +command_register() { + init_system + echo "+ Done!" + exit 0 +} + # Usage: --cron (-c) # Description: Sign/renew non-existant/changed/expiring certificates. command_sign_domains() { + echo "# INFO: Domain signing started: `date`" init_system if [[ -n "${PARAM_DOMAIN:-}" ]]; then DOMAINS_TXT="$(_mktemp)" + tmp_domains="yes" printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}" elif [[ -e "${DOMAINS_TXT}" ]]; then if [[ ! -r "${DOMAINS_TXT}" ]]; then _exiterr "domains.txt found but not readable" fi + elif [[ -n "${DOMAINS_D}" ]]; then + DOMAINS_TXT="$(_mktemp)" + tmp_domains="yes" + find "${DOMAINS_D}" -maxdepth 1 -type f | grep -o '[^/]*$' > "${DOMAINS_TXT}" else _exiterr "domains.txt not found and --domain not given" fi @@ -759,6 +843,9 @@ command_sign_domains() { config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)" config_value="$(echo "${cfgline:1}" | cut -d'=' -f2-)" case "${config_var}" in + ALT_NAMES) + config_value="$(echo "${config_value}" | tr '[:upper:]' '[:lower:]' | _sed -e "s/^'[[:space:]]*//g" -e "s/[[:space:]]*'$//g" -e 's/[[:space:]]+/ /g')" + ;& KEY_ALGO|OCSP_MUST_STAPLE|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS) echo " + ${config_var} = ${config_value}" declare -- "${config_var}=${config_value}" @@ -769,6 +856,16 @@ command_sign_domains() { done IFS="${ORIGIFS}" fi + + if [[ -n "${ALT_NAMES}" ]]; then + if [[ -n "${morenames}" ]]; then + morenames="${morenames} ${ALT_NAMES}" + else + morenames="${ALT_NAMES}" + fi + line="${domain} ${morenames}"; + fi + verify_config export WELLKNOWN CHALLENGETYPE KEY_ALGO PRIVATE_KEY_ROLLOVER @@ -820,7 +917,7 @@ command_sign_domains() { done # remove temporary domains.txt file if used - [[ -n "${PARAM_DOMAIN:-}" ]] && rm -f "${DOMAINS_TXT}" + [[ "${tmp_domains:-}" = "yes" ]] && rm -f "${DOMAINS_TXT}" [[ -n "${HOOK}" ]] && "${HOOK}" "exit_hook" exit 0 @@ -1024,6 +1121,16 @@ main() { set_command sign_domains ;; + --register) + set_command register + ;; + + # PARAM_Usage: --accept-terms + # PARAM_Description: Accept CAs terms of service + --accept-terms) + PARAM_ACCEPT_TERMS="yes" + ;; + --signcsr|-s) shift 1 set_command sign_csr @@ -1166,6 +1273,7 @@ main() { case "${COMMAND}" in env) command_env;; sign_domains) command_sign_domains;; + register) command_register;; sign_csr) command_sign_csr "${PARAM_CSR}";; revoke) command_revoke "${PARAM_REVOKECERT}";; cleanup) command_cleanup;;