Hello community, here is the log from the commit of package dehydrated for openSUSE:Factory checked in at 2020-12-10 18:19:56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/dehydrated (Old) and /work/SRC/openSUSE:Factory/.dehydrated.new.2328 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "dehydrated" Thu Dec 10 18:19:56 2020 rev:20 rq:854627 version:0.7.0 Changes: -------- --- /work/SRC/openSUSE:Factory/dehydrated/dehydrated.changes 2020-11-23 10:25:03.472994975 +0100 +++ /work/SRC/openSUSE:Factory/.dehydrated.new.2328/dehydrated.changes 2020-12-10 18:19:59.271097432 +0100 @@ -1,0 +2,31 @@ +Thu Dec 10 16:01:01 UTC 2020 - Daniel Molkentin <daniel.molken...@suse.com> + +- Update to dehydrated 0.7.0 (JSC#SLE-15909) + + Added + + Support for external account bindings + Special support for ZeroSSL + Support presets for some CAs instead of requiring URLs + Allow requesting preferred chain (--preferred-chain) + Added method to show CAs current terms of service (--display-terms) + Allow setting path to domains.txt using cli arguments (--domains-txt) + Added new cli command --cleanupdelete which deletes old files instead of archiving them + + Fixed + + No more silent failures on broken hook-scripts + Better error-handling with KEEP_GOING enabled + Check actual order status instead of assuming it's valid + Don't include keyAuthorization in challenge validation (RFC compliance) + + Changed + + Using EC secp384r1 as default certificate type + Use JSON.sh to parse JSON + Use account URL instead of account ID (RFC compliance) + Dehydrated now has a new home: https://github.com/dehydrated-io/dehydrated + Added OCSP_FETCH and OCSP_DAYS to per-certificate configurable options + Cleanup now also removes dangling symlinks + +------------------------------------------------------------------- Old: ---- dehydrated-0.6.5.tar.gz dehydrated-0.6.5.tar.gz.asc New: ---- dehydrated-0.7.0.tar.gz dehydrated-0.7.0.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ dehydrated.spec ++++++ --- /var/tmp/diff_new_pack.rxunEq/_old 2020-12-10 18:20:00.091099492 +0100 +++ /var/tmp/diff_new_pack.rxunEq/_new 2020-12-10 18:20:00.091099492 +0100 @@ -45,7 +45,7 @@ %endif Name: dehydrated -Version: 0.6.5 +Version: 0.7.0 Release: 0 Summary: A client for signing certificates with an ACME server License: MIT ++++++ dehydrated-0.6.5.tar.gz -> dehydrated-0.7.0.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/.github/FUNDING.yml new/dehydrated-0.7.0/.github/FUNDING.yml --- old/dehydrated-0.6.5/.github/FUNDING.yml 1970-01-01 01:00:00.000000000 +0100 +++ new/dehydrated-0.7.0/.github/FUNDING.yml 2020-12-10 16:54:26.000000000 +0100 @@ -0,0 +1,2 @@ +github: lukas2511 +custom: ["https://paypal.me/lukas2511", "http://www.amazon.de/registry/wishlist/1TUCFJK35IO4Q"] diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/CHANGELOG new/dehydrated-0.7.0/CHANGELOG --- old/dehydrated-0.6.5/CHANGELOG 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/CHANGELOG 2020-12-10 16:54:26.000000000 +0100 @@ -1,6 +1,30 @@ # Change Log This file contains a log of major changes in dehydrated +## [0.7.0] - 2020-12-10 +## Added +- Support for external account bindings +- Special support for ZeroSSL +- Support presets for some CAs instead of requiring URLs +- Allow requesting preferred chain (`--preferred-chain`) +- Added method to show CAs current terms of service (`--display-terms`) +- Allow setting path to domains.txt using cli arguments (`--domains-txt`) +- Added new cli command `--cleanupdelete` which deletes old files instead of archiving them + +## Fixed +- No more silent failures on broken hook-scripts +- Better error-handling with KEEP_GOING enabled +- Check actual order status instead of assuming it's valid +- Don't include keyAuthorization in challenge validation (RFC compliance) + +## Changed +- Using EC secp384r1 as default certificate type +- Use JSON.sh to parse JSON +- Use account URL instead of account ID (RFC compliance) +- Dehydrated now has a new home: https://github.com/dehydrated-io/dehydrated +- Added `OCSP_FETCH` and `OCSP_DAYS` to per-certificate configurable options +- Cleanup now also removes dangling symlinks + ## [0.6.5] - 2019-06-26 ## Fixed - Fixed broken APIv1 compatibility from last update diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/README.md new/dehydrated-0.7.0/README.md --- old/dehydrated-0.6.5/README.md 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/README.md 2020-12-10 16:54:26.000000000 +0100 @@ -1,5 +1,8 @@ # dehydrated [](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8) +Quick note: dehydrated moved, the license will NOT change, and I will still take care of the project. +See https://lukas.im/2020/01/30/selling-dehydrated/index.html for more details. +  Dehydrated is a client for signing certificates with an ACME-server (e.g. Let's Encrypt) implemented as a relatively simple (zsh-compatible) bash-script. @@ -49,12 +52,15 @@ Commands: --version (-v) Print version information + --display-terms Display current terms of service --register Register account key --account Update account contact information --cron (-c) Sign/renew non-existent/changed/expiring certificates. --signcsr (-s) path/to/csr.pem Sign a given CSR, output CRT on stdout (advanced usage) --revoke (-r) path/to/cert.pem Revoke specified certificate + --deactivate Deactivate account --cleanup (-gc) Move unused certificate files to archive directory + --cleanup-delete (-gcd) Deletes (!) unused certificate files --help (-h) Show help text --env (-e) Output configuration variables for use in other scripts @@ -64,6 +70,7 @@ --ipv4 (-4) Resolve names to IPv4 addresses only --ipv6 (-6) Resolve names to IPv6 addresses only --domain (-d) domain.tld Use specified domain name(s) instead of domains.txt entry (one certificate!) + --ca url/preset Use specified CA URL or preset --alias certalias Use specified name for certificate directory (and per-certificate config) instead of the primary domain (only used if --domain is specified) --keep-going (-g) Keep going after encountering an error while creating/renewing multiple certificates in cron mode --force (-x) Force renew of certificate even if it is longer valid than value in RENEW_DAYS @@ -71,8 +78,10 @@ --lock-suffix example.com Suffix lockfile name with a string (useful for with -d) --ocsp Sets option in CSR indicating OCSP stapling to be mandatory --privkey (-p) path/to/key.pem Use specified private key instead of account key (useful for revocation) + --domains-txt path/to/domains.txt Use specified domains.txt instead of default/configured one --config (-f) path/to/config Use specified config file --hook (-k) path/to/hook.sh Use specified script for hooks + --preferred-chain issuer-cn Use alternative certificate chain identified by issuer CN --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 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/dehydrated new/dehydrated-0.7.0/dehydrated --- old/dehydrated-0.6.5/dehydrated 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/dehydrated 2020-12-10 16:54:26.000000000 +0100 @@ -17,7 +17,7 @@ exec 3>&- exec 4>&- -VERSION="0.6.5" +VERSION="0.7.0" # Find directory in which this script is stored by traversing all symbolic links SOURCE="${0}" @@ -29,7 +29,203 @@ SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )" BASEDIR="${SCRIPTDIR}" -ORIGARGS="$@" +ORIGARGS=("${@}") + +# Generate json.sh path matching string +json_path() { + if [ ! "${1}" = "-p" ]; then + printf '"%s"' "${1}" + else + printf '%s' "${2}" + fi +} + +# Get string value from json dictionary +get_json_string_value() { + local filter + filter="$(printf 's/.*\[%s\][[:space:]]*"\([^"]*\)"/\\1/p' "$(json_path "${1:-}" "${2:-}")")" + sed -n "${filter}" +} + +# Get array values from json dictionary +get_json_array_values() { + grep -E '^\['"$(json_path "${1:-}" "${2:-}")"',[0-9]*\]' | sed -e 's/\[[^\]*\][[:space:]]*//g' -e 's/^"//' -e 's/"$//' +} + +# Get sub-dictionary from json +get_json_dict_value() { + local filter + echo "$(json_path "${1:-}" "${2:-}")" + filter="$(printf 's/.*\[%s\][[:space:]]*\(.*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")" + sed -n "${filter}" | jsonsh +} + +# Get integer value from json +get_json_int_value() { + local filter + filter="$(printf 's/.*\[%s\][[:space:]]*\([^"]*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")" + sed -n "${filter}" +} + +# Get boolean value from json +get_json_bool_value() { + local filter + filter="$(printf 's/.*\[%s\][[:space:]]*\([^"]*\)/\\1/p' "$(json_path "${1:-}" "${2:-}")")" + sed -n "${filter}" +} + +# JSON.sh JSON-parser +# Modified from https://github.com/dominictarr/JSON.sh +# Original Copyright (c) 2011 Dominic Tarr +# Licensed under The MIT License +jsonsh() { + + throw() { + echo "$*" >&2 + exit 1 + } + + awk_egrep () { + local pattern_string=$1 + + gawk '{ + while ($0) { + start=match($0, pattern); + token=substr($0, start, RLENGTH); + print token; + $0=substr($0, start+RLENGTH); + } + }' pattern="$pattern_string" + } + + tokenize () { + local GREP + local ESCAPE + local CHAR + + if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1 + then + GREP='egrep -ao --color=never' + else + GREP='egrep -ao' + fi + + if echo "test string" | egrep -o "test" >/dev/null 2>&1 + then + ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' + CHAR='[^[:cntrl:]"\\]' + else + GREP=awk_egrep + ESCAPE='(\\\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' + CHAR='[^[:cntrl:]"\\\\]' + fi + + local STRING="\"$CHAR*($ESCAPE$CHAR*)*\"" + local NUMBER='-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?' + local KEYWORD='null|false|true' + local SPACE='[[:space:]]+' + + # Force zsh to expand $A into multiple words + local is_wordsplit_disabled=$(unsetopt 2>/dev/null | grep -c '^shwordsplit$') + if [ $is_wordsplit_disabled != 0 ]; then setopt shwordsplit; fi + $GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | egrep -v "^$SPACE$" + if [ $is_wordsplit_disabled != 0 ]; then unsetopt shwordsplit; fi + } + + parse_array () { + local index=0 + local ary='' + read -r token + case "$token" in + ']') ;; + *) + while : + do + parse_value "$1" "$index" + index=$((index+1)) + ary="$ary""$value" + read -r token + case "$token" in + ']') break ;; + ',') ary="$ary," ;; + *) throw "EXPECTED , or ] GOT ${token:-EOF}" ;; + esac + read -r token + done + ;; + esac + value=$(printf '[%s]' "$ary") || value= + : + } + + parse_object () { + local key + local obj='' + read -r token + case "$token" in + '}') ;; + *) + while : + do + case "$token" in + '"'*'"') key=$token ;; + *) throw "EXPECTED string GOT ${token:-EOF}" ;; + esac + read -r token + case "$token" in + ':') ;; + *) throw "EXPECTED : GOT ${token:-EOF}" ;; + esac + read -r token + parse_value "$1" "$key" + obj="$obj$key:$value" + read -r token + case "$token" in + '}') break ;; + ',') obj="$obj," ;; + *) throw "EXPECTED , or } GOT ${token:-EOF}" ;; + esac + read -r token + done + ;; + esac + value=$(printf '{%s}' "$obj") || value= + : + } + + parse_value () { + local jpath="${1:+$1,}${2:-}" isleaf=0 isempty=0 print=0 + case "$token" in + '{') parse_object "$jpath" ;; + '[') parse_array "$jpath" ;; + # At this point, the only valid single-character tokens are digits. + ''|[!0-9]) throw "EXPECTED value GOT ${token:-EOF}" ;; + *) value=$token + # replace solidus ("\/") in json strings with normalized value: "/" + value=$(echo "$value" | sed 's#\\/#/#g') + isleaf=1 + [ "$value" = '""' ] && isempty=1 + ;; + esac + [ "$value" = '' ] && return + [ -z "$jpath" ] && return # do not print head + + printf "[%s]\t%s\n" "$jpath" "$value" + : + } + + parse () { + read -r token + parse_value + read -r token || true + case "$token" in + '') ;; + *) throw "EXPECTED EOF GOT $token" ;; + esac + } + + tokenize | parse +} # Create (identifiable) temporary files _mktemp() { @@ -39,21 +235,20 @@ # Check for script dependencies check_dependencies() { - # just execute some dummy and/or version commands to see if required tools exist and are actually usable + # look for required binaries + 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 + + # just execute some dummy and/or version commands to see if required tools are actually usable "${OPENSSL}" version > /dev/null 2>&1 || _exiterr "This script requires an openssl binary." _sed "" < /dev/null > /dev/null 2>&1 || _exiterr "This script requires sed with support for extended (modern) regular expressions." - command -v grep > /dev/null 2>&1 || _exiterr "This script requires grep." - command -v mktemp > /dev/null 2>&1 || _exiterr "This script requires mktemp." - command -v diff > /dev/null 2>&1 || _exiterr "This script requires diff." # curl returns with an error code in some ancient versions so we have to catch that set +e CURL_VERSION="$(curl -V 2>&1 | head -n1 | awk '{print $2}')" - retcode="$?" set -e - if [[ ! "${retcode}" = "0" ]] && [[ ! "${retcode}" = "2" ]]; then - _exiterr "This script requires curl." - fi } store_configvars() { @@ -63,6 +258,7 @@ __KEYSIZE="${KEYSIZE}" __CHALLENGETYPE="${CHALLENGETYPE}" __HOOK="${HOOK}" + __PREFERRED_CHAIN="${PREFERRED_CHAIN}" __WELLKNOWN="${WELLKNOWN}" __HOOK_CHAIN="${HOOK_CHAIN}" __OPENSSL_CNF="${OPENSSL_CNF}" @@ -77,6 +273,7 @@ KEYSIZE="${__KEYSIZE}" CHALLENGETYPE="${__CHALLENGETYPE}" HOOK="${__HOOK}" + PREFERRED_CHAIN="${__PREFERRED_CHAIN}" WELLKNOWN="${__WELLKNOWN}" HOOK_CHAIN="${__HOOK_CHAIN}" OPENSSL_CNF="${__OPENSSL_CNF}" @@ -88,7 +285,7 @@ # Hook scripts should ignore any hooks they don't know. # Calling a random hook to make this clear to the hook script authors... if [[ -n "${HOOK}" ]]; then - "${HOOK}" "this_hookscript_is_broken__dehydrated_is_working_fine__please_ignore_unknown_hooks_in_your_script" + "${HOOK}" "this_hookscript_is_broken__dehydrated_is_working_fine__please_ignore_unknown_hooks_in_your_script" || _exiterr "Please check your hook script, it should exit cleanly without doing anything on unknown/new hooks." fi } @@ -122,8 +319,15 @@ done fi + # Preset + CA_ZEROSSL="https://acme.zerossl.com/v2/DV90" + CA_LETSENCRYPT="https://acme-v02.api.letsencrypt.org/directory" + CA_LETSENCRYPT_TEST="https://acme-staging-v02.api.letsencrypt.org/directory" + CA_BUYPASS="https://api.buypass.com/acme/directory" + CA_BUYPASS_TEST="https://api.test4.buypass.no/acme/directory" + # Default values - CA="https://acme-v02.api.letsencrypt.org/directory" + CA="letsencrypt" OLDCA= CERTDIR= ALPNCERTDIR= @@ -134,13 +338,14 @@ DOMAINS_D= DOMAINS_TXT= HOOK= + PREFERRED_CHAIN= HOOK_CHAIN="no" RENEW_DAYS="30" KEYSIZE="4096" WELLKNOWN= PRIVATE_KEY_RENEW="yes" PRIVATE_KEY_ROLLOVER="no" - KEY_ALGO=rsa + KEY_ALGO=secp384r1 OPENSSL=openssl OPENSSL_CNF= CONTACT_EMAIL= @@ -190,46 +395,71 @@ [[ -n "${ZSH_VERSION:-}" ]] && set -o noglob || set -f fi + # Check for missing dependencies + check_dependencies + + has_sudo() { + command -v sudo > /dev/null 2>&1 || _exiterr "DEHYDRATED_USER set but sudo not available. Please install sudo." + } + # Check if we are running & are allowed to run as root if [[ -n "$DEHYDRATED_USER" ]]; then - command -v sudo > /dev/null 2>&1 || _exiterr "DEHYDRATED_USER set but sudo not available. Please install sudo." command -v getent > /dev/null 2>&1 || _exiterr "DEHYDRATED_USER set but getent not available. Please install getent." - TARGET_UID="$(getent passwd "${DEHYDRATED_USER}" | cut -d':' -f3)" + TARGET_UID="$(getent passwd "${DEHYDRATED_USER}" | cut -d':' -f3)" || _exiterr "DEHYDRATED_USER ${DEHYDRATED_USER} is invalid" if [[ -z "${DEHYDRATED_GROUP}" ]]; then if [[ "${EUID}" != "${TARGET_UID}" ]]; then echo "# INFO: Running $0 as ${DEHYDRATED_USER}" - exec sudo -u "${DEHYDRATED_USER}" "${0}" ${ORIGARGS} + has_sudo && exec sudo -u "${DEHYDRATED_USER}" "${0}" "${ORIGARGS[@]}" fi else - TARGET_GID="$(getent group "${DEHYDRATED_GROUP}" | cut -d':' -f3)" + TARGET_GID="$(getent group "${DEHYDRATED_GROUP}" | cut -d':' -f3)" || _exiterr "DEHYDRATED_GROUP ${DEHYDRATED_GROUP} is invalid" if [[ -z "${EGID:-}" ]]; then command -v id > /dev/null 2>&1 || _exiterr "DEHYDRATED_GROUP set, don't know current gid and 'id' not available... Please provide 'id' binary." EGID="$(id -g)" fi if [[ "${EUID}" != "${TARGET_UID}" ]] || [[ "${EGID}" != "${TARGET_GID}" ]]; then echo "# INFO: Running $0 as ${DEHYDRATED_USER}/${DEHYDRATED_GROUP}" - exec sudo -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" ${ORIGARGS} + has_sudo && exec sudo -u "${DEHYDRATED_USER}" -g "${DEHYDRATED_GROUP}" "${0}" "${ORIGARGS[@]}" fi fi elif [[ -n "${DEHYDRATED_GROUP}" ]]; then _exiterr "DEHYDRATED_GROUP can only be used in combination with DEHYDRATED_USER." fi - # Check for missing dependencies - check_dependencies - # Remove slash from end of BASEDIR. Mostly for cleaner outputs, doesn't change functionality. [[ "$BASEDIR" != "/" ]] && BASEDIR="${BASEDIR%%/}" # 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}" + elif [ "${CA}" = "letsencrypt-test" ]; then + CA="${CA_LETSENCRYPT_TEST}" + elif [ "${CA}" = "zerossl" ]; then + CA="${CA_ZEROSSL}" + elif [ "${CA}" = "buypass" ]; then + CA="${CA_BUYPASS}" + elif [ "${CA}" = "buypass-test" ]; then + CA="${CA_BUYPASS_TEST}" + fi + if [[ -z "${OLDCA}" ]] && [[ "${CA}" = "https://acme-v02.api.letsencrypt.org/directory" ]]; then OLDCA="https://acme-v01.api.letsencrypt.org/directory" fi # Create new account directory or symlink to account directory from old CA + # dev note: keep in mind that because of the use of 'echo' instead of 'printf' or + # similar there is a newline encoded in the directory name. not going to fix this + # since it's a non-issue and trying to fix existing installations would be too much + # trouble CAHASH="$(echo "${CA}" | urlbase64)" [[ -z "${ACCOUNTDIR}" ]] && ACCOUNTDIR="${BASEDIR}/accounts" if [[ ! -e "${ACCOUNTDIR}/${CAHASH}" ]]; then @@ -247,6 +477,11 @@ ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem" ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json" ACCOUNT_ID_JSON="${ACCOUNTDIR}/${CAHASH}/account_id.json" + ACCOUNT_DEACTIVATED="${ACCOUNTDIR}/${CAHASH}/deactivated" + + if [[ -f "${ACCOUNT_DEACTIVATED}" ]]; then + _exiterr "Account has been deactivated. Remove account and create a new one using --register." + fi if [[ -f "${BASEDIR}/private_key.pem" ]] && [[ ! -f "${ACCOUNT_KEY}" ]]; then echo "! Moving private_key.pem to ${ACCOUNT_KEY}" @@ -268,6 +503,8 @@ [[ -n "${PARAM_NO_LOCK:-}" ]] && LOCKFILE="" [[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}" + [[ -n "${PARAM_DOMAINS_TXT:-}" ]] && DOMAINS_TXT="${PARAM_DOMAINS_TXT}" + [[ -n "${PARAM_PREFERRED_CHAIN:-}" ]] && PREFERRED_CHAIN="${PARAM_PREFERRED_CHAIN}" [[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}" [[ -n "${PARAM_ALPNCERTDIR:-}" ]] && ALPNCERTDIR="${PARAM_ALPNCERTDIR}" [[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}" @@ -295,7 +532,7 @@ fi # Get CA URLs - CA_DIRECTORY="$(http_request get "${CA}")" + CA_DIRECTORY="$(http_request get "${CA}" | jsonsh)" # Automatic discovery of API version if [[ "${API}" = "auto" ]]; then @@ -308,6 +545,7 @@ 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_TERMS="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value terms-of-service)" && + CA_REQUIRES_EAB="false" && 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 @@ -317,7 +555,8 @@ 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 @@ -329,6 +568,7 @@ # 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" @@ -347,6 +587,7 @@ 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}" @@ -372,6 +613,35 @@ 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 @@ -381,11 +651,26 @@ (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 | bin2hex)" + eab_signed64="$(printf '%s' "${eab_protected64}.${eab_payload64}" | "${OPENSSL}" dgst -binary -sha256 -mac HMAC -macopt "hexkey:${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 @@ -393,7 +678,10 @@ 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 @@ -404,21 +692,21 @@ # Read account information or request from CA if missing if [[ -e "${ACCOUNT_KEY_JSON}" ]]; then if [[ ${API} -eq 1 ]]; then - ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" + ACCOUNT_ID="$(cat "${ACCOUNT_KEY_JSON}" | jsonsh | 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="$(cat "${ACCOUNT_ID_JSON}" | jsonsh | get_json_string_value url)" + fi + # if account URL is not storred, fetch it from the CA + if [[ -z "${ACCOUNT_URL:-}" ]]; then + echo "+ Fetching account URL..." 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 + if [[ -z "${ACCOUNT_URL}" ]]; then _exiterr "Unknown error on fetching account information" fi - echo '{"id": "'"${ACCOUNT_ID}"'"}' > "${ACCOUNT_ID_JSON}" + echo '{"url":"'"${ACCOUNT_URL}"'"}' > "${ACCOUNT_ID_JSON}" # store the URL for next time fi - ACCOUNT_URL="${CA_ACCOUNT}/${ACCOUNT_ID}" fi else echo "Fetching missing account information from CA..." @@ -428,7 +716,6 @@ 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##*/}" echo "${ACCOUNT_INFO}" > "${ACCOUNT_KEY_JSON}" fi } @@ -444,8 +731,10 @@ # Print error message and exit with error _exiterr() { - echo "ERROR: ${1}" >&2 - [[ -n "${HOOK:-}" ]] && "${HOOK}" "exit_hook" "${1}" || true + if [ -n "${1:-}" ]; then + echo "ERROR: ${1}" >&2 + fi + [[ "${skip_exit_hook:-no}" = "no" ]] && [[ -n "${HOOK:-}" ]] && ("${HOOK}" "exit_hook" "${1}" || echo 'exit_hook returned with non-zero exit code!' >&2) exit 1 } @@ -460,45 +749,25 @@ "${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')" } -# Get string value from json dictionary -get_json_string_value() { - local filter - filter=$(printf 's/.*"%s": *"\([^"]*\)".*/\\1/p' "$1") - 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 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 - filter=$(printf 's/.*"%s": *\([0-9]*\).*/\\1/p' "$1") - sed -n "${filter}" -} - -rm_json_arrays() { - local filter - filter='s/\[[^][]*\]/null/g' - # remove three levels of nested arrays - sed -e "${filter}" -e "${filter}" -e "${filter}" +# Convert binary data to hex string +bin2hex() { + hexdump -e '16/1 "%02x"' } # OpenSSL writes to stderr/stdout even when there are no errors. So just @@ -514,7 +783,7 @@ echo "Details:" >&2 echo "${out}" >&2 echo >&2 - exit ${res} + exit "${res}" fi } @@ -530,8 +799,8 @@ set +e if [[ "${1}" = "head" ]]; then statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)" - touch "${tempheaders}" curlret="${?}" + touch "${tempheaders}" elif [[ "${1}" = "get" ]]; then statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -L -s -w "%{http_code}" -o "${tempcont}" -D "${tempheaders}" "${2}")" curlret="${?}" @@ -569,7 +838,7 @@ if [[ -n "${HOOK}" ]]; then errtxt="$(cat ${tempcont})" errheaders="$(cat ${tempheaders})" - "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}" + "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}" || _exiterr 'request_failure hook returned with non-zero exit code' fi rm -f "${tempcont}" @@ -577,7 +846,7 @@ # remove temporary domains.txt file if used [[ "${COMMAND:-}" = "sign_domains" && -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}" - exit 1 + _exiterr fi fi @@ -662,6 +931,11 @@ fi } +# Get last issuer CN in certificate chain +get_last_cn() { + <<<"${1}" _sed 'H;/-----BEGIN CERTIFICATE-----/h;$!d;x' | "${OPENSSL}" x509 -noout -issuer | head -n1 | _sed -e 's/.* CN ?= ?([^/,]*).*/\1/' +} + # Create certificate for domain(s) and outputs it FD 3 sign_csr() { csr="${1}" # the CSR itself (not a file) @@ -699,14 +973,15 @@ challenge_identifiers="[${challenge_identifiers%, }]" echo " + Requesting new certificate order from CA..." - result="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}')" + order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')" + result="$(signed_request "${order_location}" "" | jsonsh)" - order_authorizations="$(echo ${result} | get_json_array_value authorizations)" + order_authorizations="$(echo "${result}" | get_json_array_values authorizations)" finalize="$(echo "${result}" | get_json_string_value finalize)" local idx=0 for uri in ${order_authorizations}; do - authorizations[${idx}]="$(echo "${uri}" | _sed -e 's/\"(.*)".*/\1/')" + authorizations[${idx}]="${uri}" idx=$((idx+1)) done echo " + Received ${idx} authorizations URLs from the CA" @@ -724,14 +999,14 @@ for authorization in ${authorizations[*]}; do if [[ "${API}" -eq 2 ]]; then # Receive authorization ($authorization is authz uri) - response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | clean_json)" - identifier="$(echo "${response}" | get_json_dict_value identifier | get_json_string_value value)" + response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | jsonsh)" + identifier="$(echo "${response}" | get_json_string_value -p '"identifier","value"')" echo " + Handling authorization for ${identifier}" else # 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)" + response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${identifier}"'"}}' | jsonsh)" fi # Check if authorization has already been validated @@ -741,20 +1016,25 @@ fi # 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')" + challengeindex="$(echo "${response}" | grep -E '^\["challenges",[0-9]+,"type"\][[:space:]]+"'"${CHALLENGETYPE}"'"' | cut -d',' -f2 || true)" + + if [ -z "${challengeindex}" ]; then + allowed_validations="$(echo "${response}" | grep -E '^\["challenges",[0-9]+,"type"\]' | sed -e 's/\[[^\]*\][[:space:]]*//g' -e 's/^"//' -e 's/"$//' | tr '\n' ' ')" _exiterr "Validating this certificate is not possible using ${CHALLENGETYPE}. Possible validation methods are: ${allowed_validations}" fi + challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")" # Gather challenge information challenge_names[${idx}]="${identifier}" challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)" + if [[ ${API} -eq 2 ]]; then - challenge_uris[${idx}]="$(echo "${challenge}" | _sed 's/"validationRecord": ?\[[^]]+\]//g' | get_json_string_value url)" + challenge_uris[${idx}]="$(echo "${challenge}" | get_json_string_value url)" else - challenge_uris[${idx}]="$(echo "${challenge}" | _sed 's/"validationRecord": ?\[[^]]+\]//g' | get_json_string_value uri)" + if [[ "$(echo "${challenge}" | get_json_string_value type)" = "urn:acme:error:unauthorized" ]]; then + _exiterr "Challenge unauthorized: $(echo "${challenge}" | get_json_string_value detail)" + fi + challenge_uris[${idx}]="$(echo "${challenge}" | get_json_dict_value validationRecord | get_json_string_value uri)" fi # Prepare challenge tokens and deployment parameters @@ -789,12 +1069,12 @@ if [[ ${num_pending_challenges} -ne 0 ]]; then echo " + Deploying challenge tokens..." if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then - "${HOOK}" "deploy_challenge" ${deploy_args[@]} + "${HOOK}" "deploy_challenge" ${deploy_args[@]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' elif [[ -n "${HOOK}" ]]; then # Run hook script to deploy the challenge token local idx=0 while [ ${idx} -lt ${num_pending_challenges} ]; do - "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} + "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' idx=$((idx+1)) done fi @@ -807,21 +1087,21 @@ # 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)" + result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauths[${idx}]}"'"}' | jsonsh)" else - result="$(signed_request "${challenge_uris[${idx}]}" '{"keyAuthorization": "'"${keyauths[${idx}]}"'"}' | clean_json)" + result="$(signed_request "${challenge_uris[${idx}]}" '{}' | jsonsh)" fi - reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)" + reqstatus="$(echo "${result}" | get_json_string_value status)" - while [[ "${reqstatus}" = "pending" ]]; do + while [[ "${reqstatus}" = "pending" ]] || [[ "${reqstatus}" = "processing" ]]; do sleep 1 if [[ "${API}" -eq 2 ]]; then - result="$(signed_request "${challenge_uris[${idx}]}" "")" + result="$(signed_request "${challenge_uris[${idx}]}" "" | jsonsh)" else - result="$(http_request get "${challenge_uris[${idx}]}")" + result="$(http_request get "${challenge_uris[${idx}]}" | jsonsh)" fi - reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)" + reqstatus="$(echo "${result}" | get_json_string_value status)" done [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}" @@ -830,7 +1110,7 @@ if [[ "${reqstatus}" = "valid" ]]; then echo " + Challenge is valid!" else - [[ -n "${HOOK}" ]] && "${HOOK}" "invalid_challenge" "${altname}" "${result}" + [[ -n "${HOOK}" ]] && ("${HOOK}" "invalid_challenge" "${altname}" "${result}" || _exiterr 'invalid_challenge hook returned with non-zero exit code') break fi idx=$((idx+1)) @@ -840,7 +1120,7 @@ echo " + Cleaning challenge tokens..." # Clean challenge tokens using chained hook - [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[@]} + [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[@]} || _exiterr 'clean_challenge hook returned with non-zero exit code') # Clean remaining challenge tokens if validation has failed local idx=0 @@ -850,7 +1130,7 @@ # 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}]} + [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && ("${HOOK}" "clean_challenge" ${deploy_args[${idx}]} || _exiterr 'clean_challenge hook returned with non-zero exit code') idx=$((idx+1)) done @@ -867,8 +1147,55 @@ 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="$(signed_request "${result}" "")" + result="$(signed_request "${finalize}" '{"csr": "'"${csr64}"'"}' | jsonsh)" + while :; do + orderstatus="$(echo "${result}" | get_json_string_value status)" + case "${orderstatus}" + in + "processing" | "pending") + echo " + Order is ${orderstatus}..." + sleep 2; + ;; + "valid") + break; + ;; + *) + _exiterr "Order in status ${orderstatus}" + ;; + esac + result="$(signed_request "${order_location}" "" | jsonsh)" + done + + resheaders="$(_mktemp)" + certificate="$(echo "${result}" | get_json_string_value certificate)" + crt="$(signed_request "${certificate}" "" 4>"${resheaders}")" + + if [ -n "${PREFERRED_CHAIN:-}" ]; then + foundaltchain=0 + altcn="$(get_last_cn "${crt}")" + altoptions="${altcn}" + if [ "${altcn}" = "${PREFERRED_CHAIN}" ]; then + foundaltchain=1 + fi + if [ "${foundaltchain}" = "0" ]; then + while read altcrturl; do + if [ "${foundaltchain}" = "0" ]; then + altcrt="$(signed_request "${altcrturl}" "")" + altcn="$(get_last_cn "${altcrt}")" + altoptions="${altoptions}, ${altcn}" + if [ "${altcn}" = "${PREFERRED_CHAIN}" ]; then + foundaltchain=1 + crt="${altcrt}" + fi + fi + done <<< "$(grep -Ei '^link:' "${resheaders}" | grep -Ei 'rel="alternate"' | cut -d'<' -f2 | cut -d'>' -f1)" + fi + if [ "${foundaltchain}" = "0" ]; then + _exiterr "Alternative chain with CN = ${PREFERRED_CHAIN} not found, available options: ${altoptions}" + fi + echo " + Using preferred chain with CN = ${altcn}" + fi + rm -f "${resheaders}" fi # Try to load the certificate to detect corruption @@ -955,6 +1282,8 @@ 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}" + chmod g+r "${alpncertdir}/${altname}.key.pem" "${alpncertdir}/${altname}.crt.pem" + rm -f "${tmp_openssl_cnf}" } # Create certificate for domain(s) @@ -1068,7 +1397,7 @@ 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" + [[ -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" || _exiterr 'sync_cert hook returned with non-zero exit code') # Update symlinks [[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${certdir}/privkey.pem" @@ -1079,7 +1408,7 @@ ln -sf "cert-${timestamp}.pem" "${certdir}/cert.pem" # Wait for hook script to clean the challenge and to deploy cert if used - [[ -n "${HOOK}" ]] && "${HOOK}" "deploy_cert" "${domain}" "${certdir}/privkey.pem" "${certdir}/cert.pem" "${certdir}/fullchain.pem" "${certdir}/chain.pem" "${timestamp}" + [[ -n "${HOOK}" ]] && ("${HOOK}" "deploy_cert" "${domain}" "${certdir}/privkey.pem" "${certdir}/cert.pem" "${certdir}/fullchain.pem" "${certdir}/chain.pem" "${timestamp}" || _exiterr 'deploy_cert hook returned with non-zero exit code') unset challenge_token echo " + Done!" @@ -1099,13 +1428,17 @@ echo "" if [[ "${OSTYPE}" =~ "BSD" ]]; then echo "OS: $(uname -sr)" + elif [[ -e /etc/os-release ]]; then + ( . /etc/os-release && echo "OS: $PRETTY_NAME" ) + elif [[ -e /usr/lib/os-release ]]; then + ( . /usr/lib/os-release && echo "OS: $PRETTY_NAME" ) else echo "OS: $(cat /etc/issue | grep -v ^$ | head -n1 | _sed 's/\\(r|n|l) .*//g')" fi echo "Used software:" [[ -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)" + echo " curl: ${CURL_VERSION}" if [[ "${OSTYPE}" =~ "BSD" ]]; then echo " awk, sed, mktemp, grep, diff: BSD base system versions" else @@ -1120,6 +1453,15 @@ exit 0 } +# Usage: --display-terms +# Description: Display current terms of service +command_terms() { + init_system + echo "The current terms of service: $CA_TERMS" + echo "+ Done!" + exit 0 +} + # Usage: --register # Description: Register account key command_register() { @@ -1136,12 +1478,12 @@ NEW_ACCOUNT_KEY_JSON="$(_mktemp)" - # Check if we have the registration id - if [[ -z "${ACCOUNT_ID}" ]]; then - _exiterr "Error retrieving registration id." + # Check if we have the registration url + if [[ -z "${ACCOUNT_URL}" ]]; then + _exiterr "Error retrieving registration url." fi - echo "+ Updating registration id: ${ACCOUNT_ID} contact information..." + echo "+ Updating registration url: ${ACCOUNT_URL} contact information..." 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 @@ -1183,7 +1525,7 @@ hookscript_bricker_hook # Call startup hook - [[ -n "${HOOK}" ]] && "${HOOK}" "startup_hook" + [[ -n "${HOOK}" ]] && ("${HOOK}" "startup_hook" || _exiterr 'startup_hook hook returned with non-zero exit code') if [ ! -d "${CHAINCACHE}" ]; then echo " + Creating chain cache directory ${CHAINCACHE}" @@ -1271,9 +1613,9 @@ rm "${aftervars}" ); do config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)" - config_value="$(echo "${cfgline:1}" | cut -d'=' -f2-)" + config_value="$(echo "${cfgline:1}" | cut -d'=' -f2- | tr -d "'")" case "${config_var}" in - KEY_ALGO|OCSP_MUST_STAPLE|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS) + KEY_ALGO|OCSP_MUST_STAPLE|OCSP_FETCH|OCSP_DAYS|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|PREFERRED_CHAIN|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS) echo " + ${config_var} = ${config_value}" declare -- "${config_var}=${config_value}" ;; @@ -1292,7 +1634,7 @@ # Allow for external CSR generation local csr="" if [[ -n "${HOOK}" ]]; then - csr="$("${HOOK}" "generate_csr" "${domain}" "${certdir}" "${domain} ${morenames}")" + csr="$("${HOOK}" "generate_csr" "${domain}" "${certdir}" "${domain} ${morenames}")" || _exiterr 'generate_csr hook returned with non-zero exit code' if grep -qE "\-----BEGIN (NEW )?CERTIFICATE REQUEST-----" <<< "${csr}"; then altnames="$(extract_altnames "${csr}")" domain="$(cut -d' ' -f1 <<< "${altnames}")" @@ -1328,14 +1670,14 @@ valid="$("${OPENSSL}" x509 -enddate -noout -in "${cert}" | cut -d= -f2- )" printf " + Valid till %s " "${valid}" - if "${OPENSSL}" x509 -checkend $((RENEW_DAYS * 86400)) -noout -in "${cert}"; then + if ("${OPENSSL}" x509 -checkend $((RENEW_DAYS * 86400)) -noout -in "${cert}" > /dev/null 2>&1); then printf "(Longer than %d days). " "${RENEW_DAYS}" if [[ "${force_renew}" = "yes" ]]; then echo "Ignoring because renew was forced!" else # Certificate-Names unchanged and cert is still valid echo "Skipping renew!" - [[ -n "${HOOK}" ]] && "${HOOK}" "unchanged_cert" "${domain}" "${certdir}/privkey.pem" "${certdir}/cert.pem" "${certdir}/fullchain.pem" "${certdir}/chain.pem" + [[ -n "${HOOK}" ]] && ("${HOOK}" "unchanged_cert" "${domain}" "${certdir}/privkey.pem" "${certdir}/cert.pem" "${certdir}/fullchain.pem" "${certdir}/chain.pem" || _exiterr 'unchanged_cert hook returned with non-zero exit code') skip="yes" fi else @@ -1351,8 +1693,10 @@ update_ocsp="yes" [[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr" if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then + skip_exit_hook=yes sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} & - wait $! || true + wait $! || exit_with_errorcode=1 + skip_exit_hook=no else sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} fi @@ -1371,15 +1715,15 @@ if [[ "${update_ocsp}" = "yes" ]]; then echo " + Updating OCSP stapling file" ocsp_timestamp="$(date +%s)" - if grep -qE "^(0|(1\.0))\." <<< "$(${OPENSSL} version | awk '{print $2}')"; then + if grep -qE "^(openssl (0|(1\.0))\.)|(libressl (1|2|3)\.)" <<< "$(${OPENSSL} version | awk '{print tolower($0)}')"; then ocsp_log="$("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respout "${certdir}/ocsp-${ocsp_timestamp}.der" -url "${ocsp_url}" -header "HOST" "$(echo "${ocsp_url}" | _sed -e 's/^http(s?):\/\///' -e 's/\/.*$//g')" 2>&1)" || _exiterr "Error while fetching OCSP information: ${ocsp_log}" else ocsp_log="$("${OPENSSL}" ocsp -no_nonce -issuer "${chain}" -verify_other "${chain}" -cert "${cert}" -respout "${certdir}/ocsp-${ocsp_timestamp}.der" -url "${ocsp_url}" 2>&1)" || _exiterr "Error while fetching OCSP information: ${ocsp_log}" fi ln -sf "ocsp-${ocsp_timestamp}.der" "${certdir}/ocsp.der" - [[ -n "${HOOK}" ]] && altnames="${domain} ${morenames}" "${HOOK}" "deploy_ocsp" "${domain}" "${certdir}/ocsp.der" "${ocsp_timestamp}" + [[ -n "${HOOK}" ]] && (altnames="${domain} ${morenames}" "${HOOK}" "deploy_ocsp" "${domain}" "${certdir}/ocsp.der" "${ocsp_timestamp}" || _exiterr 'deploy_ocsp hook returned with non-zero exit code') else - echo " + OSCP stapling file is still valid (skipping update)" + echo " + OCSP stapling file is still valid (skipping update)" fi fi done @@ -1388,12 +1732,13 @@ # remove temporary domains.txt file if used [[ -n "${PARAM_DOMAIN:-}" ]] && rm -f "${DOMAINS_TXT}" - [[ -n "${HOOK}" ]] && "${HOOK}" "exit_hook" + [[ -n "${HOOK}" ]] && ("${HOOK}" "exit_hook" || echo 'exit_hook returned with non-zero exit code!' >&2) if [[ "${AUTO_CLEANUP}" == "yes" ]]; then echo "+ Running automatic cleanup" command_cleanup noinit fi - exit 0 + + exit "${exit_with_errorcode}" } # Usage: --signcsr (-s) path/to/csr.pem @@ -1484,6 +1829,28 @@ mv -f "${cert}" "${cert}-revoked" } +# Usage: --deactivate +# Description: Deactivate account +command_deactivate() { + init_system + + echo "Deactivating account ${ACCOUNT_URL}" + + if [[ ${API} -eq 1 ]]; then + echo "Deactivation for ACMEv1 is not implemented" + else + response="$(signed_request "${ACCOUNT_URL}" '{"status": "deactivated"}' | clean_json)" + deactstatus=$(echo "$response" | jsonsh | get_json_string_value "status") + if [[ "${deactstatus}" = "deactivated" ]]; then + touch "${ACCOUNT_DEACTIVATED}" + else + _exiterr "Account deactivation failed!" + fi + fi + + echo " + Done." +} + # Usage: --cleanup (-gc) # Description: Move unused certificate files to archive directory command_cleanup() { @@ -1491,9 +1858,11 @@ load_config fi - # Create global archive directory if not existent - if [[ ! -e "${BASEDIR}/archive" ]]; then - mkdir "${BASEDIR}/archive" + if [[ ! "${PARAM_CLEANUPDELETE:-}" = "yes" ]]; then + # Create global archive directory if not existent + if [[ ! -e "${BASEDIR}/archive" ]]; then + mkdir "${BASEDIR}/archive" + fi fi # Allow globbing @@ -1508,18 +1877,26 @@ certname="$(basename "${certdir}")" # Create certificates archive directory if not existent - archivedir="${BASEDIR}/archive/${certname}" - if [[ ! -e "${archivedir}" ]]; then - mkdir "${archivedir}" + if [[ ! "${PARAM_CLEANUPDELETE:-}" = "yes" ]]; then + archivedir="${BASEDIR}/archive/${certname}" + if [[ ! -e "${archivedir}" ]]; then + mkdir "${archivedir}" + fi fi # Loop over file-types (certificates, keys, signing-requests, ...) for filetype in cert.csr cert.pem chain.pem fullchain.pem privkey.pem ocsp.der; do - # Skip if symlink is broken - [[ -r "${certdir}/${filetype}" ]] || continue - - # Look up current file in use - current="$(basename "$(readlink "${certdir}/${filetype}")")" + # Delete all if symlink is broken + if [[ -r "${certdir}/${filetype}" ]]; then + # Look up current file in use + current="$(basename "$(readlink "${certdir}/${filetype}")")" + else + if [[ -h "${certdir}/${filetype}" ]]; then + echo "Removing broken symlink: ${certdir}/${filetype}" + rm -f "${certdir}/${filetype}" + fi + current="" + fi # Split filetype into name and extension filebase="$(echo "${filetype}" | cut -d. -f1)" @@ -1529,17 +1906,30 @@ for file in "${certdir}/${filebase}-"*".${fileext}" "${certdir}/${filebase}-"*".${fileext}-revoked"; do # Check if current file is in use, if unused move to archive directory filename="$(basename "${file}")" - if [[ ! "${filename}" = "${current}" ]]; then - echo "Moving unused file to archive directory: ${certname}/${filename}" - mv "${certdir}/${filename}" "${archivedir}/${filename}" + if [[ ! "${filename}" = "${current}" ]] && [[ -f "${certdir}/${filename}" ]]; then + echo "${filename}" + if [[ "${PARAM_CLEANUPDELETE:-}" = "yes" ]]; then + echo "Deleting unused file: ${certname}/${filename}" + rm "${certdir}/${filename}" + else + echo "Moving unused file to archive directory: ${certname}/${filename}" + mv "${certdir}/${filename}" "${archivedir}/${filename}" + fi fi done done done - exit 0 + exit "${exit_with_errorcode}" +} + +# Usage: --cleanup-delete (-gcd) +# Description: Deletes (!) unused certificate files +command_cleanupdelete() { + command_cleanup } + # Usage: --help (-h) # Description: Show help text command_help() { @@ -1571,6 +1961,8 @@ # Main method (parses script arguments and calls command_* methods) main() { + exit_with_errorcode=0 + skip_exit_hook=no COMMAND="" set_command() { [[ -z "${COMMAND}" ]] || _exiterr "Only one command can be executed at a time. See help (-h) for more information." @@ -1620,6 +2012,10 @@ PARAM_ACCEPT_TERMS="yes" ;; + --display-terms) + set_command terms + ;; + --signcsr|-s) shift 1 set_command sign_csr @@ -1634,6 +2030,10 @@ PARAM_REVOKECERT="${1}" ;; + --deactivate) + set_command deactivate + ;; + --version|-v) set_command version ;; @@ -1642,6 +2042,11 @@ set_command cleanup ;; + --cleanup-delete|-gcd) + set_command cleanupdelete + PARAM_CLEANUPDELETE="yes" + ;; + # PARAM_Usage: --full-chain (-fc) # PARAM_Description: Print full chain when using --signcsr --full-chain|-fc) @@ -1672,6 +2077,15 @@ 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) @@ -1721,6 +2135,14 @@ PARAM_ACCOUNT_KEY="${1}" ;; + # PARAM_Usage: --domains-txt path/to/domains.txt + # PARAM_Description: Use specified domains.txt instead of default/configured one + --domains-txt) + shift 1 + check_parameters "${1:-}" + PARAM_DOMAINS_TXT="${1}" + ;; + # PARAM_Usage: --config (-f) path/to/config # PARAM_Description: Use specified config file --config|-f) @@ -1737,6 +2159,14 @@ PARAM_HOOK="${1}" ;; + # PARAM_Usage: --preferred-chain issuer-cn + # PARAM_Description: Use alternative certificate chain identified by issuer CN + --preferred-chain) + shift 1 + check_parameters "${1:-}" + PARAM_PREFERRED_CHAIN="${1}" + ;; + # PARAM_Usage: --out (-o) certs/directory # PARAM_Description: Output certificates into the specified directory --out|-o) @@ -1768,7 +2198,6 @@ check_parameters "${1:-}" PARAM_KEY_ALGO="${1}" ;; - *) echo "Unknown parameter detected: ${1}" >&2 echo >&2 @@ -1787,10 +2216,15 @@ account) command_account;; sign_csr) command_sign_csr "${PARAM_CSR}";; revoke) command_revoke "${PARAM_REVOKECERT}";; + deactivate) command_deactivate;; cleanup) command_cleanup;; + terms) command_terms;; + cleanupdelete) command_cleanupdelete;; version) command_version;; *) command_help; exit 1;; esac + + exit "${exit_with_errorcode}" } # Determine OS type @@ -1800,3 +2234,5 @@ # Run script main "${@:-}" fi + +# vi: expandtab sw=2 ts=2 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/docs/dns-verification.md new/dehydrated-0.7.0/docs/dns-verification.md --- old/dehydrated-0.6.5/docs/dns-verification.md 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/docs/dns-verification.md 2020-12-10 16:54:26.000000000 +0100 @@ -28,4 +28,4 @@ You can delete the TXT record when called with operation `clean_challenge`, when $2 is also the domain name. -Here are some examples: [Examples for DNS-01 hooks](https://github.com/lukas2511/dehydrated/wiki/Examples-for-DNS-01-hooks) +Here are some examples: [Examples for DNS-01 hooks](https://github.com/dehydrated-io/dehydrated/wiki) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/docs/examples/config new/dehydrated-0.7.0/docs/examples/config --- old/dehydrated-0.6.5/docs/examples/config 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/docs/examples/config 2020-12-10 16:54:26.000000000 +0100 @@ -10,10 +10,10 @@ # Default values of this config are in comments # ######################################################## -# Which user should dehydrated run as? This will be implictly enforced when running as root +# Which user should dehydrated run as? This will be implicitly enforced when running as root #DEHYDRATED_USER= -# Which group should dehydrated run as? This will be implictly enforced when running as root +# Which group should dehydrated run as? This will be implicitly enforced when running as root #DEHYDRATED_GROUP= # Resolve names to addresses of IP version only. (curl) @@ -21,8 +21,10 @@ # default: <unset> #IP_VERSION= -# Path to certificate authority (default: https://acme-v02.api.letsencrypt.org/directory) -#CA="https://acme-v02.api.letsencrypt.org/directory" +# URL to certificate authority or internal preset +# Presets: letsencrypt, letsencrypt-test, zerossl, buypass, buypass-test +# default: letsencrypt +#CA="letsencrypt" # Path to old certificate authority # Set this value to your old CA value when upgrading from ACMEv1 to ACMEv2 under a different endpoint. @@ -100,7 +102,7 @@ #PRIVATE_KEY_ROLLOVER="no" # Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 -#KEY_ALGO=rsa +#KEY_ALGO=secp384r1 # E-mail to use during the registration (default: <unset>) #CONTACT_EMAIL= @@ -125,3 +127,6 @@ # ACME API version (default: auto) #API=auto + +# Preferred issuer chain (default: <unset> -> uses default chain) +#PREFERRED_CHAIN= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/docs/examples/hook.sh new/dehydrated-0.7.0/docs/examples/hook.sh --- old/dehydrated-0.6.5/docs/examples/hook.sh 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/docs/examples/hook.sh 2020-12-10 16:54:26.000000000 +0100 @@ -60,7 +60,7 @@ # The path of the file containing the certificate signing request. # Simple example: sync the files before symlinking them - # sync "${KEYFILE}" "${CERTFILE} "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}" + # sync "${KEYFILE}" "${CERTFILE}" "${FULLCHAINFILE}" "${CHAINFILE}" "${REQUESTFILE}" } deploy_cert() { @@ -177,7 +177,7 @@ # This hook is called before any certificate signing operation takes place. # It can be used to generate or fetch a certificate signing request with external # tools. - # The output should be just the cerificate signing request formatted as PEM. + # The output should be just the certificate signing request formatted as PEM. # # Parameters: # - DOMAIN diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/docs/import-from-official-client.md new/dehydrated-0.7.0/docs/import-from-official-client.md --- old/dehydrated-0.6.5/docs/import-from-official-client.md 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/docs/import-from-official-client.md 1970-01-01 01:00:00.000000000 +0100 @@ -1,3 +0,0 @@ -# Import - -If you want to import existing keys from the official letsencrypt client have a look at [Import from official letsencrypt client](https://github.com/lukas2511/dehydrated/wiki/Import-from-official-letsencrypt-client). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/docs/per-certificate-config.md new/dehydrated-0.7.0/docs/per-certificate-config.md --- old/dehydrated-0.6.5/docs/per-certificate-config.md 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/docs/per-certificate-config.md 2020-12-10 16:54:26.000000000 +0100 @@ -17,6 +17,7 @@ - WELLKNOWN - OPENSSL_CNF - RENEW_DAYS +- PREFERRED_CHAIN ## DOMAINS_D diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.6.5/docs/tls-alpn.md new/dehydrated-0.7.0/docs/tls-alpn.md --- old/dehydrated-0.6.5/docs/tls-alpn.md 2019-06-26 12:33:35.000000000 +0200 +++ new/dehydrated-0.7.0/docs/tls-alpn.md 2020-12-10 16:54:26.000000000 +0100 @@ -15,18 +15,16 @@ ```nginx stream { - server { - map $ssl_preread_alpn_protocols $tls_port { - ~\bacme-tls/1\b 10443; - default 443; - } + 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; - } + server { + listen 443; + listen [::]:443; + proxy_pass 10.13.37.42:$tls_port; + ssl_preread on; } } ``` _______________________________________________ openSUSE Commits mailing list -- commit@lists.opensuse.org To unsubscribe, email commit-le...@lists.opensuse.org List Netiquette: https://en.opensuse.org/openSUSE:Mailing_list_netiquette List Archives: https://lists.opensuse.org/archives/list/commit@lists.opensuse.org