Hello community, here is the log from the commit of package dehydrated for openSUSE:Factory checked in at 2019-06-26 16:05:10 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/dehydrated (Old) and /work/SRC/openSUSE:Factory/.dehydrated.new.4615 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "dehydrated" Wed Jun 26 16:05:10 2019 rev:11 rq:712112 version:0.6.5 Changes: -------- --- /work/SRC/openSUSE:Factory/dehydrated/dehydrated.changes 2019-01-24 14:13:30.751355717 +0100 +++ /work/SRC/openSUSE:Factory/.dehydrated.new.4615/dehydrated.changes 2019-06-26 16:05:33.215758401 +0200 @@ -1,0 +2,22 @@ +Wed Jun 26 11:03:27 UTC 2019 - Daniel Molkentin <[email protected]> + +- Update to dehydrated 0.6.5 + * Fixed broken APIv1 compatibility from last update + +------------------------------------------------------------------- +Tue Jun 25 17:29:10 UTC 2019 - Daniel Molkentin <[email protected]> + +- Update to dehydrated 0.6.4 + * Fetch account ID from Location header instead of account json (bsc#1139408) + +- Update to dehydrated 0.6.3 + + * OCSP refresh interval is now configurable + * Implemented POST-as-GET + * Call exit_hook on errors (with error-message as first parameter) + * Initial support for tls-alpn-01 validation + * New hook: sync_cert (for syncing certificate files to disk, see example + hook description) + * Fetch account information after registration to avoid missing account id + +------------------------------------------------------------------- Old: ---- dehydrated-0.6.2.tar.gz dehydrated-0.6.2.tar.gz.asc New: ---- dehydrated-0.6.5.tar.gz dehydrated-0.6.5.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ dehydrated.spec ++++++ --- /var/tmp/diff_new_pack.2YsSO8/_old 2019-06-26 16:05:34.279759907 +0200 +++ /var/tmp/diff_new_pack.2YsSO8/_new 2019-06-26 16:05:34.279759907 +0200 @@ -46,7 +46,7 @@ %endif Name: dehydrated -Version: 0.6.2 +Version: 0.6.5 Release: 0 Summary: A client for signing certificates with an ACME server License: MIT ++++++ dehydrated-0.6.2.tar.gz -> dehydrated-0.6.5.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/CHANGELOG new/dehydrated-0.6.5/CHANGELOG --- old/dehydrated-0.6.2/CHANGELOG 2018-04-25 23:22:40.000000000 +0200 +++ new/dehydrated-0.6.5/CHANGELOG 2019-06-26 12:33:35.000000000 +0200 @@ -1,6 +1,27 @@ # Change Log This file contains a log of major changes in dehydrated +## [0.6.5] - 2019-06-26 +## Fixed +- Fixed broken APIv1 compatibility from last update + +## [0.6.4] - 2019-06-25 +## Changed +- Fetch account ID from Location header instead of account json + +## [0.6.3] - 2019-06-25 +## Changed +- OCSP refresh interval is now configurable +- Implemented POST-as-GET +- Call exit_hook on errors (with error-message as first parameter) + +## Added +- Initial support for tls-alpn-01 validation +- New hook: sync_cert (for syncing certificate files to disk, see example hook description) + +## Fixes +- Fetch account information after registration to avoid missing account id + ## [0.6.2] - 2018-04-25 ## Added - New deploy_ocsp hook diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/README.md new/dehydrated-0.6.5/README.md --- old/dehydrated-0.6.2/README.md 2018-04-25 23:22:40.000000000 +0200 +++ new/dehydrated-0.6.5/README.md 2019-06-26 12:33:35.000000000 +0200 @@ -74,6 +74,7 @@ --config (-f) path/to/config Use specified config file --hook (-k) path/to/hook.sh Use specified script for hooks --out (-o) certs/directory Output certificates into the specified directory + --alpn alpn-certs/directory Output alpn verification certificates into the specified directory --challenge (-t) http-01|dns-01 Which challenge should be used? Currently http-01 and dns-01 are supported --algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 ``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/dehydrated new/dehydrated-0.6.5/dehydrated --- old/dehydrated-0.6.2/dehydrated 2018-04-25 23:22:40.000000000 +0200 +++ new/dehydrated-0.6.5/dehydrated 2019-06-26 12:33:35.000000000 +0200 @@ -17,7 +17,7 @@ exec 3>&- exec 4>&- -VERSION="0.6.2" +VERSION="0.6.5" # Find directory in which this script is stored by traversing all symbolic links SOURCE="${0}" @@ -94,7 +94,7 @@ # verify configuration values verify_config() { - [[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... cannot continue." + [[ "${CHALLENGETYPE}" == "http-01" || "${CHALLENGETYPE}" == "dns-01" || "${CHALLENGETYPE}" == "tls-alpn-01" ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... cannot continue." if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then _exiterr "Challenge type dns-01 needs a hook script for deployment... cannot continue." fi @@ -106,6 +106,7 @@ [[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue." fi [[ "${API}" == "auto" || "${API}" == "1" || "${API}" == "2" ]] || _exiterr "Unsupported API version defined in config: ${API}" + [[ "${OCSP_DAYS}" =~ ^[0-9]+$ ]] || _exiterr "OCSP_DAYS must be a number" } # Setup default config values, search for and load configuration files @@ -125,6 +126,7 @@ CA="https://acme-v02.api.letsencrypt.org/directory" OLDCA= CERTDIR= + ALPNCERTDIR= ACCOUNTDIR= CHALLENGETYPE="http-01" CONFIG_D= @@ -145,6 +147,7 @@ LOCKFILE= OCSP_MUST_STAPLE="no" OCSP_FETCH="no" + OCSP_DAYS=5 IP_VERSION= CHAINCACHE= AUTO_CLEANUP="no" @@ -243,6 +246,7 @@ [[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config" ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem" ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json" + ACCOUNT_ID_JSON="${ACCOUNTDIR}/${CAHASH}/account_id.json" if [[ -f "${BASEDIR}/private_key.pem" ]] && [[ ! -f "${ACCOUNT_KEY}" ]]; then echo "! Moving private_key.pem to ${ACCOUNT_KEY}" @@ -254,6 +258,7 @@ fi [[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs" + [[ -z "${ALPNCERTDIR}" ]] && ALPNCERTDIR="${BASEDIR}/alpn-certs" [[ -z "${CHAINCACHE}" ]] && CHAINCACHE="${BASEDIR}/chains" [[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt" [[ -z "${WELLKNOWN}" ]] && WELLKNOWN="/var/www/dehydrated" @@ -264,6 +269,7 @@ [[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}" [[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}" + [[ -n "${PARAM_ALPNCERTDIR:-}" ]] && ALPNCERTDIR="${PARAM_ALPNCERTDIR}" [[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}" [[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}" [[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}" @@ -319,7 +325,7 @@ fi # Export some environment variables to be used in hook script - export WELLKNOWN BASEDIR CERTDIR CONFIG COMMAND + export WELLKNOWN BASEDIR CERTDIR ALPNCERTDIR CONFIG COMMAND # Checking for private key ... register_new_key="no" @@ -328,6 +334,7 @@ echo "Using private key ${PARAM_ACCOUNT_KEY} instead of account key" ACCOUNT_KEY="${PARAM_ACCOUNT_KEY}" ACCOUNT_KEY_JSON="${PARAM_ACCOUNT_KEY}.json" + ACCOUNT_ID_JSON="${PARAM_ACCOUNT_KEY}_id.json" [ "${COMMAND:-}" = "register" ] && register_new_key="yes" else # Check if private account key exists, if it doesn't exist yet generate a new one (rsa key) @@ -396,10 +403,21 @@ # Read account information or request from CA if missing if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then - ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" if [[ ${API} -eq 1 ]]; then + ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" ACCOUNT_URL="${CA_REG}/${ACCOUNT_ID}" else + if [[ -e "${ACCOUNT_ID_JSON}" ]]; then + ACCOUNT_ID="$(cat "${ACCOUNT_ID_JSON}" | get_json_string_value id)" + else + echo "+ Fetching account ID..." + ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')" + ACCOUNT_ID="${ACCOUNT_URL##*/}" + if [[ -z "${ACCOUNT_ID}" ]]; then + _exiterr "Unknown error on fetching account information" + fi + echo '{"id": "'"${ACCOUNT_ID}"'"}' > "${ACCOUNT_ID_JSON}" + fi ACCOUNT_URL="${CA_ACCOUNT}/${ACCOUNT_ID}" fi else @@ -407,7 +425,7 @@ if [[ ${API} -eq 1 ]]; then _exiterr "This is not implemented for ACMEv1! Consider switching to ACMEv2 :)" else - ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep ^Location: | awk '{print $2}' | tr -d '\r\n')" + ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')" ACCOUNT_INFO="$(signed_request "${ACCOUNT_URL}" '{}')" fi ACCOUNT_ID="${ACCOUNT_URL##*/}" @@ -427,6 +445,7 @@ # Print error message and exit with error _exiterr() { echo "ERROR: ${1}" >&2 + [[ -n "${HOOK:-}" ]] && "${HOOK}" "exit_hook" "${1}" || true exit 1 } @@ -557,7 +576,7 @@ rm -f "${tempheaders}" # remove temporary domains.txt file if used - [[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}" + [[ "${COMMAND:-}" = "sign_domains" && -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}" exit 1 fi fi @@ -577,9 +596,9 @@ # Retrieve nonce from acme-server if [[ ${API} -eq 1 ]]; then - nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" + nonce="$(http_request head "${CA}" | grep -i ^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')" + nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" fi # Build header with just our public key and algorithm information @@ -705,7 +724,7 @@ 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)" + response="$(signed_request "$(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 @@ -752,6 +771,10 @@ # Generate DNS entry content for dns-01 validation keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)" ;; + "tls-alpn-01") + keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $2}')" + generate_alpn_certificate "${identifier}" "${keyauth_hook}" + ;; esac keyauths[${idx}]="${keyauth}" @@ -793,11 +816,16 @@ while [[ "${reqstatus}" = "pending" ]]; do sleep 1 - result="$(http_request get "${challenge_uris[${idx}]}")" + if [[ "${API}" -eq 2 ]]; then + result="$(signed_request "${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_tokens[${idx}]}" + [[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem" if [[ "${reqstatus}" = "valid" ]]; then echo " + Challenge is valid!" @@ -819,6 +847,8 @@ while [ ${idx} -lt ${num_pending_challenges} ]; do # Delete challenge file [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}" + # Delete alpn verification certificates + [[ "${CHALLENGETYPE}" = "tls-alpn-01" ]] && rm -f "${ALPNCERTDIR}/${challenge_names[${idx}]}.crt.pem" "${ALPNCERTDIR}/${challenge_names[${idx}]}.key.pem" # Clean challenge token using non-chained hook [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[${idx}]} idx=$((idx+1)) @@ -838,7 +868,7 @@ 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}")" + crt="$(signed_request "${result}" "")" fi # Try to load the certificate to detect corruption @@ -906,6 +936,27 @@ fi } +# Generate ALPN verification certificate +generate_alpn_certificate() { + local altname="${1}" + local acmevalidation="${2}" + + local alpncertdir="${ALPNCERTDIR}" + if [[ ! -e "${alpncertdir}" ]]; then + echo " + Creating new directory ${alpncertdir} ..." + mkdir -p "${alpncertdir}" || _exiterr "Unable to create directory ${alpncertdir}" + fi + + echo " + Generating ALPN certificate and key for ${1}..." + tmp_openssl_cnf="$(_mktemp)" + cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}" + printf "[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}" + printf "1.3.6.1.5.5.7.1.31=critical,DER:04:20:${acmevalidation}\n" >> "${tmp_openssl_cnf}" + SUBJ="/CN=${altname}/" + [[ "${OSTYPE:0:5}" = "MINGW" ]] && SUBJ="/${SUBJ}" + _openssl req -x509 -new -sha256 -nodes -newkey rsa:2048 -keyout "${alpncertdir}/${altname}.key.pem" -out "${alpncertdir}/${altname}.crt.pem" -subj "${SUBJ}" -extensions SAN -config "${tmp_openssl_cnf}" +} + # Create certificate for domain(s) sign_domain() { local certdir="${1}" @@ -1016,6 +1067,9 @@ rm "${tmpcert}" "${tmpchain}" fi + # Wait for hook script to sync the files before creating the symlinks + [[ -n "${HOOK}" ]] && "${HOOK}" "sync_cert" "${certdir}/privkey-${timestamp}.pem" "${certdir}/cert-${timestamp}.pem" "${certdir}/fullchain-${timestamp}.pem" "${certdir}/chain-${timestamp}.pem" "${certdir}/cert-${timestamp}.csr" + # Update symlinks [[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${certdir}/privkey.pem" @@ -1043,7 +1097,7 @@ revision="$(cd "${SCRIPTDIR}"; git rev-parse HEAD 2>/dev/null || echo "unknown")" echo "GIT-Revision: ${revision}" echo "" - if [[ "${OSTYPE}" = "FreeBSD" ]]; then + if [[ "${OSTYPE}" =~ "BSD" ]]; then echo "OS: $(uname -sr)" else echo "OS: $(cat /etc/issue | grep -v ^$ | head -n1 | _sed 's/\\(r|n|l) .*//g')" @@ -1052,15 +1106,15 @@ [[ -n "${BASH_VERSION:-}" ]] && echo " bash: ${BASH_VERSION}" [[ -n "${ZSH_VERSION:-}" ]] && echo " zsh: ${ZSH_VERSION}" echo " curl: $(curl --version 2>&1 | head -n1 | cut -d" " -f1-2)" - if [[ "${OSTYPE}" = "FreeBSD" ]]; then - echo " awk, sed, mktemp: FreeBSD base system versions" + if [[ "${OSTYPE}" =~ "BSD" ]]; then + echo " awk, sed, mktemp, grep, diff: BSD base system versions" else echo " awk: $(awk -W version 2>&1 | head -n1)" echo " sed: $(sed --version 2>&1 | head -n1)" echo " mktemp: $(mktemp --version 2>&1 | head -n1)" + echo " grep: $(grep --version 2>&1 | head -n1)" + echo " diff: $(diff --version 2>&1 | head -n1)" fi - echo " grep: $(grep --version 2>&1 | head -n1)" - echo " diff: $(diff --version 2>&1 | head -n1)" echo " openssl: $("${OPENSSL}" version 2>&1)" exit 0 @@ -1310,7 +1364,7 @@ if [[ ! -e "${certdir}/ocsp.der" ]]; then update_ocsp="yes" - elif ! ("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respin "${certdir}/ocsp.der" -status_age 432000 2>&1 | grep -q "${cert}: good"); then + elif ! ("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respin "${certdir}/ocsp.der" -status_age $((OCSP_DAYS*24*3600)) 2>&1 | grep -q "${cert}: good"); then update_ocsp="yes" fi @@ -1512,7 +1566,7 @@ command_env() { echo "# dehydrated configuration" load_config - typeset -p CA CERTDIR CHALLENGETYPE DOMAINS_D DOMAINS_TXT HOOK HOOK_CHAIN RENEW_DAYS ACCOUNT_KEY ACCOUNT_KEY_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE + typeset -p CA CERTDIR ALPNCERTDIR CHALLENGETYPE DOMAINS_D DOMAINS_TXT HOOK HOOK_CHAIN RENEW_DAYS ACCOUNT_KEY ACCOUNT_KEY_JSON ACCOUNT_ID_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE } # Main method (parses script arguments and calls command_* methods) @@ -1691,6 +1745,14 @@ PARAM_CERTDIR="${1}" ;; + # PARAM_Usage: --alpn alpn-certs/directory + # PARAM_Description: Output alpn verification certificates into the specified directory + --alpn) + shift 1 + check_parameters "${1:-}" + PARAM_ALPNCERTDIR="${1}" + ;; + # PARAM_Usage: --challenge (-t) http-01|dns-01 # PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported --challenge|-t) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/docs/acme-v1.md new/dehydrated-0.6.5/docs/acme-v1.md --- old/dehydrated-0.6.2/docs/acme-v1.md 1970-01-01 01:00:00.000000000 +0100 +++ new/dehydrated-0.6.5/docs/acme-v1.md 2019-06-26 12:33:35.000000000 +0200 @@ -0,0 +1,19 @@ +## (Future) Removal of API version 1 + +The ACME API version 1 was never really standardized and was only supported by Let's Encrypt. Even though the protocol specification was public, +it wasn't really friendly to be integrated into existing CA systems so initial adoption was basically non-existant. + +ACME version 2 is being designed to overcome these issues by becoming an official IETF standard and supporting a more traditional approach of account +and order management in the backend, making it friendlier to integrate into existing systems centered around those. It has since become a semi-stable IETF +standard draft which only ever got two breaking changes, Content-Type enforcement and `POST-as-GET`, the latter being announced in October 2018 to be enforced +by November 2019. See https://datatracker.ietf.org/wg/acme/documents/ for a better insight into the draft and its changes. + +Next to backend changes that many users won't really care about ACME v2 has all of the features ACME v1 had, but also some additional new features like +e.g. support for [wildcard certificates](domains_txt.md#wildcards). + +Since ACME v2 is basically to be considered stable and ACME v1 has no real benefits over v2, there doesn't seem to be much of a reason to keep the old +protocol around, but since there actually are a few Certificate Authorities and resellers that implemented the v1 protocol and didn't yet make the change +to v2, so dehydrated still supports the old protocol for now. + +Please keep in mind that support for the old ACME protocol version 1 might get removed at any point of bigger inconvenience, e.g. on code changes that +would require a lot of work or ugly workarounds to keep both versions supported. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/docs/examples/config new/dehydrated-0.6.5/docs/examples/config --- old/dehydrated-0.6.2/docs/examples/config 2018-04-25 23:22:40.000000000 +0200 +++ new/dehydrated-0.6.5/docs/examples/config 2019-06-26 12:33:35.000000000 +0200 @@ -31,7 +31,7 @@ # default: https://acme-v01.api.letsencrypt.org/directory #OLDCA="https://acme-v01.api.letsencrypt.org/directory" -# Which challenge should be used? Currently http-01 and dns-01 are supported +# Which challenge should be used? Currently http-01, dns-01 and tls-alpn-01 are supported #CHALLENGETYPE="http-01" # Path to a directory containing additional config files, allowing to override @@ -40,6 +40,11 @@ # default: <unset> #CONFIG_D= +# Directory for per-domain configuration files. +# If not set, per-domain configurations are sourced from each certificates output directory. +# default: <unset> +#DOMAINS_D= + # Base directory for account key, generated certificates and list of domains (default: $SCRIPTDIR -- uses config directory if undefined) #BASEDIR=$SCRIPTDIR @@ -49,6 +54,9 @@ # Output directory for generated certificates #CERTDIR="${BASEDIR}/certs" +# Output directory for alpn verification certificates +#ALPNCERTDIR="${BASEDIR}/alpn-certs" + # Directory for account keys and registration information #ACCOUNTDIR="${BASEDIR}/accounts" @@ -106,6 +114,9 @@ # Fetch OCSP responses (default: no) #OCSP_FETCH="no" +# OCSP refresh interval (default: 5 days) +#OCSP_DAYS=5 + # Issuer chain cache directory (default: $BASEDIR/chains) #CHAINCACHE="${BASEDIR}/chains" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/docs/examples/hook.sh new/dehydrated-0.6.5/docs/examples/hook.sh --- old/dehydrated-0.6.2/docs/examples/hook.sh 2018-04-25 23:22:40.000000000 +0200 +++ new/dehydrated-0.6.5/docs/examples/hook.sh 2019-06-26 12:33:35.000000000 +0200 @@ -37,6 +37,32 @@ # printf 'server 127.0.0.1\nupdate delete _acme-challenge.%s TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key } +sync_cert() { + local KEYFILE="${1}" CERTFILE="${2}" FULLCHAINFILE="${3}" CHAINFILE="${4}" REQUESTFILE="${5}" + + # This hook is called after the certificates have been created but before + # they are symlinked. This allows you to sync the files to disk to prevent + # creating a symlink to empty files on unexpected system crashes. + # + # This hook is not intended to be used for further processing of certificate + # files, see deploy_cert for that. + # + # Parameters: + # - KEYFILE + # The path of the file containing the private key. + # - CERTFILE + # The path of the file containing the signed certificate. + # - FULLCHAINFILE + # The path of the file containing the full certificate chain. + # - CHAINFILE + # The path of the file containing the intermediate certificate(s). + # - REQUESTFILE + # The path of the file containing the certificate signing request. + + # Simple example: sync the files before symlinking them + # sync "${KEYFILE}" "${CERTFILE} "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}" +} + deploy_cert() { local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" @@ -178,13 +204,17 @@ } exit_hook() { + local ERROR="${1:-}" + # This hook is called at the end of the cron command and can be used to # do some final (cleanup or other) tasks. - - : + # + # Parameters: + # - ERROR + # Contains error message if dehydrated exits with error } HANDLER="$1"; shift -if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then +if [[ "${HANDLER}" =~ ^(deploy_challenge|clean_challenge|sync_cert|deploy_cert|deploy_ocsp|unchanged_cert|invalid_challenge|request_failure|generate_csr|startup_hook|exit_hook)$ ]]; then "$HANDLER" "$@" fi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/docs/per-certificate-config.md new/dehydrated-0.6.5/docs/per-certificate-config.md --- old/dehydrated-0.6.2/docs/per-certificate-config.md 2018-04-25 23:22:40.000000000 +0200 +++ new/dehydrated-0.6.5/docs/per-certificate-config.md 2019-06-26 12:33:35.000000000 +0200 @@ -7,6 +7,7 @@ Currently supported options: - PRIVATE_KEY_RENEW +- PRIVATE_KEY_ROLLOVER - KEY_ALGO - KEYSIZE - OCSP_MUST_STAPLE diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/docs/tls-alpn.md new/dehydrated-0.6.5/docs/tls-alpn.md --- old/dehydrated-0.6.2/docs/tls-alpn.md 1970-01-01 01:00:00.000000000 +0100 +++ new/dehydrated-0.6.5/docs/tls-alpn.md 2019-06-26 12:33:35.000000000 +0200 @@ -0,0 +1,106 @@ +# TLS-ALPN-01 + +With `tls-alpn-01`-type verification Let's Encrypt (or the ACME-protocol in general) is checking if you are in control of a domain by accessing +your webserver using a custom ALPN and expecting a specially crafted TLS certificate containing a verification token. +It will do that for any (sub-)domain you want to sign a certificate for. + +Dehydrated generates the required verification certificates, but the delivery is out of its scope. + +### Example nginx config + +On an nginx tcp load-balancer you can use the `ssl_preread` module to map a different port for acme-tls +requests than for e.g. HTTP/2 or HTTP/1.1 requests. + +Your config should look something like this: + +```nginx +stream { + server { + map $ssl_preread_alpn_protocols $tls_port { + ~\bacme-tls/1\b 10443; + default 443; + } + + server { + listen 443; + listen [::]:443; + proxy_pass 10.13.37.42:$tls_port; + ssl_preread on; + } + } +} +``` + +That way https requests are forwarded to port 443 on the backend server, and acme-tls/1 requests are +forwarded to port 10443. + +In the future nginx might support internal routing based on custom ALPNs, but for now you'll have to +use a custom responder for the alpn verification certificates (see below). + +### Example responder + +I hacked together a simple responder in Python, it might not be the best, but it works for me: + +```python +#!/usr/bin/env python3 + +import ssl +import socketserver +import threading +import re +import os + +ALPNDIR="/etc/dehydrated/alpn-certs" +PROXY_PROTOCOL=False + +FALLBACK_CERTIFICATE="/etc/ssl/certs/ssl-cert-snakeoil.pem" +FALLBACK_KEY="/etc/ssl/private/ssl-cert-snakeoil.key" + +class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): + pass + +class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): + def create_context(self, certfile, keyfile, first=False): + ssl_context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH) + ssl_context.set_ciphers('ECDHE+AESGCM') + ssl_context.set_alpn_protocols(["acme-tls/1"]) + ssl_context.options |= ssl.OP_NO_TLSv1 | ssl.OP_NO_TLSv1_1 + if first: + ssl_context.set_servername_callback(self.load_certificate) + ssl_context.load_cert_chain(certfile=certfile, keyfile=keyfile) + return ssl_context + + def load_certificate(self, sslsocket, sni_name, sslcontext): + print("Got request for %s" % sni_name) + if not re.match(r'^(([a-zA-Z]{1})|([a-zA-Z]{1}[a-zA-Z]{1})|([a-zA-Z]{1}[0-9]{1})|([0-9]{1}[a-zA-Z]{1})|([a-zA-Z0-9][-_.a-zA-Z0-9]{0,61}[a-zA-Z0-9]))\.([a-zA-Z]{2,13}|[a-zA-Z0-9-]{2,30}.[a-zA-Z]{2,3})$', sni_name): + return + + certfile = os.path.join(ALPNDIR, "%s.crt.pem" % sni_name) + keyfile = os.path.join(ALPNDIR, "%s.key.pem" % sni_name) + + if not os.path.exists(certfile) or not os.path.exists(keyfile): + return + + sslsocket.context = self.create_context(certfile, keyfile) + + def handle(self): + if PROXY_PROTOCOL: + buf = b"" + while b"\r\n" not in buf: + buf += self.request.recv(1) + + ssl_context = self.create_context(FALLBACK_CERTIFICATE, FALLBACK_KEY, True) + newsock = ssl_context.wrap_socket(self.request, server_side=True) + +if __name__ == "__main__": + HOST, PORT = "0.0.0.0", 10443 + + server = ThreadedTCPServer((HOST, PORT), ThreadedTCPRequestHandler, bind_and_activate=False) + server.allow_reuse_address = True + try: + server.server_bind() + server.server_activate() + server.serve_forever() + except: + server.shutdown() +``` diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.2/docs/troubleshooting.md new/dehydrated-0.6.5/docs/troubleshooting.md --- old/dehydrated-0.6.2/docs/troubleshooting.md 2018-04-25 23:22:40.000000000 +0200 +++ new/dehydrated-0.6.5/docs/troubleshooting.md 2019-06-26 12:33:35.000000000 +0200 @@ -30,3 +30,23 @@ To test this create a file (e.g. `test.txt`) in that directory and try opening it with your browser: `http://example.org/.well-known/acme-challenge/test.txt`. Note that if you have an IPv6 address, the challenge connection will be on IPv6. Be sure that you test HTTP connections on both IPv4 and IPv6. Checking the test file in your browser is often not sufficient because the browser just fails over to IPv4. If you get any error you'll have to fix your web server configuration. + +## DNS invalid challenge since dehydrated 0.6.0 / Why are DNS challenges deployed first and verified later? + +Since Let's Encrypt (and in general the ACMEv2 protocol) now supports wildcard domains there is a situation where DNS caching can become a problem. +If somebody wants to validate a certificate with `example.org` and `*.example.org` there are two tokens that have to be deployed on `_acme-challenge.example.org`. + +If dehydrated would deploy and verify each token on its own the CA would cache the first token on `_acme-challenge.example.org` and the next challenge would simply fail. +Let's Encrypt uses your DNS TTL with a max limit of 5 minutes, but this doesn't seem to be part of the ACME protocol, just some LE specific configuration, +so with other CAs and certain DNS providers who don't allow low TTLs this could potentially take hours. + +Since dehydrated now deploys all challenges first that no longer is a problem. The CA will query and cache both challenges, and both authorizations can be validated. +Some hook-scripts were written in a way that erases the old TXT record rather than adding a new entry, those should be (and many of them already have been) fixed. + +There are certain DNS providers which really only allow one TXT record on a domain. This is really odd and you should probably contact your DNS provider and ask them +to fix this. + +If for whatever reason you can't switch DNS providers and your DNS provider only supports one TXT record and doesn't want to fix that you could try splitting your +certificate into multiple certificates and add a sleep in the `deploy_cert` hook. +If you can't do that or really don't want to please leave a comment on https://github.com/lukas2511/dehydrated/issues/554, +if many people are having this unfixable problem I might try to implement a workaround.
