Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package dehydrated for openSUSE:Factory checked in at 2022-11-01 13:42:48 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/dehydrated (Old) and /work/SRC/openSUSE:Factory/.dehydrated.new.2275 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "dehydrated" Tue Nov 1 13:42:48 2022 rev:25 rq:1032541 version:0.7.1 Changes: -------- --- /work/SRC/openSUSE:Factory/dehydrated/dehydrated.changes 2022-09-08 14:23:53.974702926 +0200 +++ /work/SRC/openSUSE:Factory/.dehydrated.new.2275/dehydrated.changes 2022-11-01 13:42:50.191981722 +0100 @@ -1,0 +2,7 @@ +Sat Oct 29 05:03:26 UTC 2022 - Daniel Molkentin <dan...@molkentin.de> + +- Update to 0.7.1 + * See https://github.com/dehydrated-io/dehydrated/releases/tag/v0.7.1 + * Removes more-examples.patch + +------------------------------------------------------------------- Old: ---- dehydrated-0.7.0.tar.gz dehydrated-0.7.0.tar.gz.asc more-examples.patch New: ---- dehydrated-0.7.1.tar.gz dehydrated-0.7.1.tar.gz.asc ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ dehydrated.spec ++++++ --- /var/tmp/diff_new_pack.iceFQx/_old 2022-11-01 13:42:51.739989957 +0100 +++ /var/tmp/diff_new_pack.iceFQx/_new 2022-11-01 13:42:51.783990191 +0100 @@ -53,7 +53,7 @@ %endif Name: dehydrated -Version: 0.7.0 +Version: 0.7.1 Release: 0 Summary: A client for signing certificates with an ACME server License: MIT @@ -77,7 +77,6 @@ Source18: dehydrated-postrun-hooks.service Source19: dehydrated-postrun-hooks@.service Source20: README.postrun-hooks -Patch: more-examples.patch BuildRequires: %{_apache} Requires: coreutils Requires: curl @@ -172,7 +171,6 @@ %prep %setup -q -%patch -p1 cp %{SOURCE9} . cp %{SOURCE10} . cp %{SOURCE20} . @@ -206,10 +204,12 @@ #!/bin/sh systemctl reload apache2.service EOF +%if %{with nginx} cat > %{buildroot}%{_sysconfdir}/dehydrated/postrun-hooks.d/reload-nginx.sh << EOF #!/bin/sh systemctl reload nginx.service EOF +%endif %if %{with nginx} install -m 0755 -d %{buildroot}%{_sysconfdir}/nginx @@ -280,7 +280,7 @@ %{_bindir}/dehydrated %attr(-,%{_user},root) %dir %{_localstatedir}/lib/acme-challenge %{_mandir}/man1/* -%doc LICENSE README.md docs/*.md docs/*.jpg +%doc LICENSE README.md docs/*.md %doc README.maintainer %if %{defined redhat} %doc README.Fedora ++++++ dehydrated-0.7.0.tar.gz -> dehydrated-0.7.1.tar.gz ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/CHANGELOG new/dehydrated-0.7.1/CHANGELOG --- old/dehydrated-0.7.0/CHANGELOG 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/CHANGELOG 2022-10-31 15:12:38.000000000 +0100 @@ -1,6 +1,21 @@ # Change Log This file contains a log of major changes in dehydrated +## [0.7.1] - 2022-10-31 +## Changed +- `--force` no longer forces domain name revalidation by default, a new argument `--force-validation` has been added for that +- Added support for EC secp521r1 algorithm (works with e.g. zerossl) +- `EC PARAMETERS` are no longer written to privkey.pem (didn't seem necessary and was causing issues with various software) + +## Fixed +- Requests resulting in `badNonce` errors are now automatically retried (fixes operation with LE staging servers) +- Deprecated `egrep` usage has been removed + +## Added +- Implemented EC for account keys +- Domain list now also read from domains.txt.d subdirectory (behaviour might change, see docs) +- Implemented RFC 8738 (validating/signing certificates for IP addresses instead of domain names) support (this will not work with most public CAs, if any!) + ## [0.7.0] - 2020-12-10 ## Added - Support for external account bindings diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/LICENSE new/dehydrated-0.7.1/LICENSE --- old/dehydrated-0.7.0/LICENSE 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/LICENSE 2022-10-31 15:12:38.000000000 +0100 @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2018 Lukas Schauer +Copyright (c) 2015-2021 Lukas Schauer Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/README.md new/dehydrated-0.7.1/README.md --- old/dehydrated-0.7.0/README.md 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/README.md 2022-10-31 15:12:38.000000000 +0100 @@ -1,9 +1,6 @@ # 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. This client supports both ACME v1 and the new ACME v2 including support for wildcard certificates! @@ -17,6 +14,7 @@ - Signing of a custom CSR (either standalone or completely automated using hooks!) - Renewal if a certificate is about to expire or defined set of domains changed - Certificate revocation +- and lots more.. Please keep in mind that this software, the ACME-protocol and all supported CA servers out there are relatively young and there might be a few issues. Feel free to report any issues you find with this script or contribute by submitting a pull request, but please check for duplicates first (feel free to comment on those to get things rolling). @@ -74,6 +72,7 @@ --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 + --force-validation Force revalidation of domain names (used in combination with --force) --no-lock (-n) Don't use lockfile (potentially dangerous!) --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 @@ -84,28 +83,6 @@ --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 + --challenge (-t) http-01|dns-01|tls-alpn-01 Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported --algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 ``` - -## Donate - -I'm a student hacker with a few (unfortunately) quite expensive hobbies (self-hosting, virtualization clusters, routing, -high-speed networking, embedded hardware, etc.). -I'm really having fun playing around with hard- and software and I'm steadily learning new things. -Without those hobbies I probably would never have started working on dehydrated to begin with :) - -I'd really appreciate if you could [donate a bit of money](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=23P9DSJBTY7C8) -so I can buy cool stuff (while still being able to afford food :D). - -If you have hardware laying around that you think I'd enjoy playing with (e.g. decommissioned but still modern-ish servers, -10G networking hardware, enterprise grade routers or APs, interesting ARM/MIPS boards, etc.) and that you would be willing -to ship to me please contact me at `donati...@dehydrated.io` or on Twitter [@lukas2511](https://twitter.com/lukas2511). - -If you want your name to be added to the [donations list](https://dehydrated.io/donations.html) please add a note or send me an -email `donati...@dehydrated.io`. I respect your privacy and won't publish your name without permission. - -Other ways of donating: - - [My Amazon Wishlist](http://www.amazon.de/registry/wishlist/1TUCFJK35IO4Q) - - Monero: 4Kkf4tF4r9DakxLj37HDXLJgmpVfQoFhT7JLDvXwtUZZMTbsK9spsAPXivWPAFcDUj6jHhY8hJSHX8Cb8ndMhKeQHPSkBZZiK89Fx8NTHk - - Bitcoin: 12487bHxcrREffTGwUDnoxF1uYxCA7ztKK diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/dehydrated new/dehydrated-0.7.1/dehydrated --- old/dehydrated-0.7.0/dehydrated 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/dehydrated 2022-10-31 15:12:38.000000000 +0100 @@ -17,7 +17,7 @@ exec 3>&- exec 4>&- -VERSION="0.7.0" +VERSION="0.7.1" # Find directory in which this script is stored by traversing all symbolic links SOURCE="${0}" @@ -31,6 +31,22 @@ BASEDIR="${SCRIPTDIR}" ORIGARGS=("${@}") +noglob_set() { + if [[ -n "${ZSH_VERSION:-}" ]]; then + set +o noglob + else + set +f + fi +} + +noglob_clear() { + if [[ -n "${ZSH_VERSION:-}" ]]; then + set -o noglob + else + set -f + fi +} + # Generate json.sh path matching string json_path() { if [ ! "${1}" = "-p" ]; then @@ -55,7 +71,6 @@ # 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 } @@ -88,7 +103,7 @@ awk_egrep () { local pattern_string=$1 - gawk '{ + awk '{ while ($0) { start=match($0, pattern); token=substr($0, start, RLENGTH); @@ -103,14 +118,15 @@ local ESCAPE local CHAR - if echo "test string" | egrep -ao --color=never "test" >/dev/null 2>&1 + if echo "test string" | grep -Eao --color=never "test" >/dev/null 2>&1 then - GREP='egrep -ao --color=never' + GREP='grep -Eao --color=never' else - GREP='egrep -ao' + GREP='grep -Eao' fi - if echo "test string" | egrep -o "test" >/dev/null 2>&1 + # shellcheck disable=SC2196 + if echo "test string" | grep -Eao "test" >/dev/null 2>&1 then ESCAPE='(\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})' CHAR='[^[:cntrl:]"\\]' @@ -126,10 +142,11 @@ 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 + local is_wordsplit_disabled + is_wordsplit_disabled="$(unsetopt 2>/dev/null | grep -c '^shwordsplit$')" + if [ "${is_wordsplit_disabled}" != "0" ]; then setopt shwordsplit; fi + $GREP "$STRING|$NUMBER|$KEYWORD|$SPACE|." | grep -Ev "^$SPACE$" + if [ "${is_wordsplit_disabled}" != "0" ]; then unsetopt shwordsplit; fi } parse_array () { @@ -194,17 +211,14 @@ } parse_value () { - local jpath="${1:+$1,}${2:-}" isleaf=0 isempty=0 print=0 + local jpath="${1:+$1,}${2:-}" 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 + *) value="${token/\\\///}" # replace solidus ("\/") in json strings with normalized value: "/" - value=$(echo "$value" | sed 's#\\/#/#g') - isleaf=1 - [ "$value" = '""' ] && isempty=1 ;; esac [ "$value" = '' ] && return @@ -227,16 +241,26 @@ tokenize | parse } +# Convert IP addresses to their reverse dns variants. +# Used for ALPN certs as validation for IPs uses this in SNI since IPs aren't allowed there. +ip_to_ptr() { + ip="$(cat)" + if [[ "${ip}" =~ : ]]; then + printf "%sip6.arpa" "$(printf "%s" "${ip}" | awk -F: 'BEGIN {OFS=""; }{addCount = 9 - NF; for(i=1; i<=NF;i++){if(length($i) == 0){ for(j=1;j<=addCount;j++){$i = ($i "0000");} } else { $i = substr(("0000" $i), length($i)+5-4);}}; print}' | rev | sed -e "s/./&./g")" + else + printf "%s.in-addr.arpa" "$(printf "%s" "${ip}" | awk -F. '{print $4"."$3"." $2"."$1}')" + fi +} + # Create (identifiable) temporary files _mktemp() { - # shellcheck disable=SC2068 - mktemp ${@:-} "${TMPDIR:-/tmp}/dehydrated-XXXXXX" + mktemp "${TMPDIR:-/tmp}/dehydrated-XXXXXX" } # Check for script dependencies check_dependencies() { # look for required binaries - for binary in grep mktemp diff sed awk curl cut; do + for binary in grep mktemp diff sed awk curl cut head tail hexdump; 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 @@ -254,7 +278,10 @@ store_configvars() { __KEY_ALGO="${KEY_ALGO}" __OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}" + __OCSP_FETCH="${OCSP_FETCH}" + __OCSP_DAYS="${OCSP_DAYS}" __PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}" + __PRIVATE_KEY_ROLLOVER="${PRIVATE_KEY_ROLLOVER}" __KEYSIZE="${KEYSIZE}" __CHALLENGETYPE="${CHALLENGETYPE}" __HOOK="${HOOK}" @@ -269,7 +296,10 @@ reset_configvars() { KEY_ALGO="${__KEY_ALGO}" OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}" + OCSP_FETCH="${__OCSP_FETCH}" + OCSP_DAYS="${__OCSP_DAYS}" PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}" + PRIVATE_KEY_ROLLOVER="${__PRIVATE_KEY_ROLLOVER}" KEYSIZE="${__KEYSIZE}" CHALLENGETYPE="${__CHALLENGETYPE}" HOOK="${__HOOK}" @@ -298,7 +328,7 @@ 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" || "${KEY_ALGO}" == "prime256v1" || "${KEY_ALGO}" == "secp384r1" ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... cannot continue." + [[ "${KEY_ALGO}" == "rsa" || "${KEY_ALGO}" == "prime256v1" || "${KEY_ALGO}" == "secp384r1" || "${KEY_ALGO}" == "secp521r1" ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... cannot continue." if [[ -n "${IP_VERSION}" ]]; then [[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... cannot continue." fi @@ -332,6 +362,8 @@ CERTDIR= ALPNCERTDIR= ACCOUNTDIR= + ACCOUNT_KEYSIZE="4096" + ACCOUNT_KEY_ALGO=rsa CHALLENGETYPE="http-01" CONFIG_D= CURL_OPTS= @@ -379,7 +411,7 @@ fi # Allow globbing - [[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f + noglob_set for check_config_d in "${CONFIG_D}"/*.sh; do if [[ -f "${check_config_d}" ]] && [[ -r "${check_config_d}" ]]; then @@ -392,7 +424,7 @@ done # Disable globbing - [[ -n "${ZSH_VERSION:-}" ]] && set -o noglob || set -f + noglob_clear fi # Check for missing dependencies @@ -473,6 +505,7 @@ fi fi + # shellcheck disable=SC1090 [[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config" ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem" ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json" @@ -512,6 +545,10 @@ [[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}" [[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}" + if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ] && [ "${PARAM_FORCE:-no}" = "no" ]; then + _exiterr "Argument --force-validation can only be used in combination with --force (-x)" + fi + if [ ! "${1:-}" = "noverify" ]; then verify_config fi @@ -539,8 +576,8 @@ grep -q newOrder <<< "${CA_DIRECTORY}" && API=2 || API=1 fi - if [[ ${API} -eq 1 ]]; then - # shellcheck disable=SC2015 + # shellcheck disable=SC2015 + if [[ "${API}" = "1" ]]; then CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" && CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" && CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" && @@ -551,7 +588,6 @@ # Since reg URI is missing from directory we will assume it is the same as CA_NEW_REG without the new part CA_REG=${CA_NEW_REG/new-reg/reg} else - # shellcheck disable=SC2015 CA_NEW_ORDER="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newOrder)" && CA_NEW_NONCE="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newNonce)" && CA_NEW_ACCOUNT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value newAccount)" && @@ -559,8 +595,6 @@ 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 - CA_ACCOUNT=${CA_NEW_ACCOUNT/new-acct/acct} fi # Export some environment variables to be used in hook script @@ -582,26 +616,63 @@ if [[ ! "${PARAM_ACCEPT_TERMS:-}" = "yes" ]]; then printf '\n' >&2 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' "${CA_TERMS}" >&2 - printf 'To accept these terms of service run `%s --register --accept-terms`.\n' "${0}" >&2 + printf 'To accept these terms of service run "%s --register --accept-terms".\n' "${0}" >&2 exit 1 fi echo "+ Generating account key..." generated="true" - local tmp_account_key="$(_mktemp)" - _openssl genrsa -out "${tmp_account_key}" "${KEYSIZE}" + local tmp_account_key + tmp_account_key="$(_mktemp)" + if [[ ${API} -eq 1 && ! "${ACCOUNT_KEY_ALGO}" = "rsa" ]]; then + _exiterr "ACME API version 1 does not support EC account keys" + fi + case "${ACCOUNT_KEY_ALGO}" in + rsa) _openssl genrsa -out "${tmp_account_key}" "${ACCOUNT_KEYSIZE}";; + prime256v1|secp384r1|secp521r1) _openssl ecparam -genkey -name "${ACCOUNT_KEY_ALGO}" -out "${tmp_account_key}" -noout;; + esac cat "${tmp_account_key}" > "${ACCOUNT_KEY}" rm "${tmp_account_key}" register_new_key="yes" fi fi - "${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null || _exiterr "Account key is not valid, cannot continue." - # Get public components from private key and calculate thumbprint - pubExponent64="$(printf '%x' "$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)" - pubMod64="$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)" + if ("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null); then + # Get public components from private key and calculate thumbprint + pubExponent64="$(printf '%x' "$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)" + pubMod64="$("${OPENSSL}" rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)" + + account_key_info="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}")" + account_key_sigalgo=RS256 + elif ("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null); then + curve="$("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -noout -text 2>/dev/null | grep 'NIST CURVE' | cut -d':' -f2 | tr -d ' ')" + pubkey="$("${OPENSSL}" ec -in "${ACCOUNT_KEY}" -noout -text 2>/dev/null | tr -d '\n ' | grep -Eo 'pub:.*ASN1' | _sed -e 's/^pub://' -e 's/ASN1$//' | tr -d ':')" + + if [ "${curve}" = "P-256" ]; then + account_key_sigalgo="ES256" + elif [ "${curve}" = "P-384" ]; then + account_key_sigalgo="ES384" + elif [ "${curve}" = "P-521" ]; then + account_key_sigalgo="ES512" + else + _exiterr "Unknown account key curve: ${curve}" + fi + + ec_x_offset=2 + ec_x_len=$((${#pubkey}/2 - 1)) + ec_x="${pubkey:$ec_x_offset:$ec_x_len}" + ec_x64="$(printf "%s" "${ec_x}" | hex2bin | urlbase64)" + + ec_y_offset=$((ec_x_offset+ec_x_len)) + ec_y_len=$((${#pubkey}-ec_y_offset)) + ec_y="${pubkey:$ec_y_offset:$ec_y_len}" + ec_y64="$(printf "%s" "${ec_y}" | hex2bin | urlbase64)" - thumbprint="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)" + account_key_info="$(printf '{"crv":"%s","kty":"EC","x":"%s","y":"%s"}' "${curve}" "${ec_x64}" "${ec_y64}")" + else + _exiterr "Account key is not valid, cannot continue." + fi + thumbprint="$(printf '%s' "${account_key_info}" | "${OPENSSL}" dgst -sha256 -binary | urlbase64)" # 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 @@ -654,7 +725,7 @@ 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_payload64="$(printf "%s" "${account_key_info}" | 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)" @@ -692,16 +763,16 @@ # 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}" | jsonsh | get_json_int_value id)" + ACCOUNT_ID="$(jsonsh < "${ACCOUNT_KEY_JSON}" | get_json_int_value id)" ACCOUNT_URL="${CA_REG}/${ACCOUNT_ID}" else if [[ -e "${ACCOUNT_ID_JSON}" ]]; then - ACCOUNT_URL="$(cat "${ACCOUNT_ID_JSON}" | jsonsh | get_json_string_value url)" + ACCOUNT_URL="$(jsonsh < "${ACCOUNT_ID_JSON}" | 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_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')" if [[ -z "${ACCOUNT_URL}" ]]; then _exiterr "Unknown error on fetching account information" fi @@ -713,7 +784,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 -i ^Location: | awk '{print $2}' | tr -d '\r\n')" + ACCOUNT_URL="$(signed_request "${CA_NEW_ACCOUNT}" '{"onlyReturnExisting": true}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')" ACCOUNT_INFO="$(signed_request "${ACCOUNT_URL}" '{}')" fi echo "${ACCOUNT_INFO}" > "${ACCOUNT_KEY_JSON}" @@ -734,7 +805,7 @@ 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) + [[ "${skip_exit_hook:-no}" = "no" ]] && [[ -n "${HOOK:-}" ]] && ("${HOOK}" "exit_hook" "${1:-}" || echo 'exit_hook returned with non-zero exit code!' >&2) exit 1 } @@ -762,12 +833,13 @@ # 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')" + # shellcheck disable=SC2059 + printf "%b" "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" } # Convert binary data to hex string bin2hex() { - hexdump -e '16/1 "%02x"' + hexdump -v -e '/1 "%02x"' } # OpenSSL writes to stderr/stdout even when there are no errors. So just @@ -797,6 +869,7 @@ fi set +e + # shellcheck disable=SC2086 if [[ "${1}" = "head" ]]; then statuscode="$(curl ${ip_version:-} ${CURL_OPTS} -A "dehydrated/${VERSION} curl/${CURL_VERSION}" -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)" curlret="${?}" @@ -826,6 +899,10 @@ elif [[ -n "${CA_REVOKE_CERT:-}" ]] && [[ "${2}" = "${CA_REVOKE_CERT:-}" ]] && [[ "${statuscode}" = "409" ]]; then grep -q "Certificate already revoked" "${tempcont}" && return else + if grep -q "urn:ietf:params:acme:error:badNonce" "${tempcont}"; then + printf "badnonce %s" "$(grep -Eoi "^replay-nonce:.*$" "${tempheaders}" | sed 's/ //' | cut -d: -f2)" + return 0 + fi echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2 echo >&2 echo "Details:" >&2 @@ -836,8 +913,8 @@ # An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins) if [[ -n "${HOOK}" ]]; then - errtxt="$(cat ${tempcont})" - errheaders="$(cat ${tempheaders})" + errtxt="$(cat "${tempcont}")" + errheaders="$(cat "${tempheaders}")" "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}" "${errheaders}" || _exiterr 'request_failure hook returned with non-zero exit code' fi @@ -863,16 +940,17 @@ # Encode payload as urlbase64 payload64="$(printf '%s' "${2}" | urlbase64)" - # Retrieve nonce from acme-server - if [[ ${API} -eq 1 ]]; then - nonce="$(http_request head "${CA}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" + if [ -n "${3:-}" ]; then + nonce="$(printf "%s" "${3}" | tr -d ' \t\n\r')" else - nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')" + # Retrieve nonce from acme-server + if [[ ${API} -eq 1 ]]; then + nonce="$(http_request head "${CA}" | grep -i ^Replay-Nonce: | cut -d':' -f2- | tr -d ' \t\n\r')" + else + nonce="$(http_request head "${CA_NEW_NONCE}" | grep -i ^Replay-Nonce: | cut -d':' -f2- | tr -d ' \t\n\r')" + fi fi - # Build header with just our public key and algorithm information - header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}' - if [[ ${API} -eq 1 ]]; then # Build another header which also contains the previously received nonce and encode it as urlbase64 protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}' @@ -880,17 +958,37 @@ else # Build another header which also contains the previously received nonce and url and encode it as urlbase64 if [[ -n "${ACCOUNT_URL:-}" ]]; then - protected='{"alg": "RS256", "kid": "'"${ACCOUNT_URL}"'", "url": "'"${1}"'", "nonce": "'"${nonce}"'"}' + protected='{"alg": "'"${account_key_sigalgo}"'", "kid": "'"${ACCOUNT_URL}"'", "url": "'"${1}"'", "nonce": "'"${nonce}"'"}' else - protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "url": "'"${1}"'", "nonce": "'"${nonce}"'"}' + protected='{"alg": "'"${account_key_sigalgo}"'", "jwk": '"${account_key_info}"', "url": "'"${1}"'", "nonce": "'"${nonce}"'"}' fi protected64="$(printf '%s' "${protected}" | urlbase64)" fi # Sign header with nonce and our payload with our private key and encode signature as urlbase64 - signed64="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)" + if [[ "${account_key_sigalgo}" = "RS256" ]]; then + signed64="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)" + else + dgstparams="$(printf '%s' "${protected64}.${payload64}" | "${OPENSSL}" dgst -sha${account_key_sigalgo:2} -sign "${ACCOUNT_KEY}" | "${OPENSSL}" asn1parse -inform DER)" + dgst_parm_1="$(echo "$dgstparams" | head -n 2 | tail -n 1 | cut -d':' -f4)" + dgst_parm_2="$(echo "$dgstparams" | head -n 3 | tail -n 1 | cut -d':' -f4)" + + # zero-padding (doesn't seem to be necessary, but other clients are doing this as well... + case "${account_key_sigalgo}" in + "ES256") siglen=64;; + "ES384") siglen=96;; + "ES512") siglen=132;; + esac + while [[ ${#dgst_parm_1} -lt $siglen ]]; do dgst_parm_1="0${dgst_parm_1}"; done + while [[ ${#dgst_parm_2} -lt $siglen ]]; do dgst_parm_2="0${dgst_parm_2}"; done + + signed64="$(printf "%s%s" "${dgst_parm_1}" "${dgst_parm_2}" | hex2bin | urlbase64)" + fi if [[ ${API} -eq 1 ]]; then + # Build header with just our public key and algorithm information + header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}' + # Send header + extended header + payload + signature to the acme-server data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' else @@ -898,7 +996,14 @@ data='{"protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' fi - http_request post "${1}" "${data}" + output="$(http_request post "${1}" "${data}")" + + if grep -qE "^badnonce " <<< "${output}"; then + echo " ! Request failed (badNonce), retrying request..." >&2 + signed_request "${1:-}" "${2:-}" "$(printf "%s" "${output}" | cut -d' ' -f2)" + else + printf "%s" "${output}" + fi } # Extracts all subject names from a CSR @@ -917,23 +1022,23 @@ # split to one per line: # shellcheck disable=SC1003 altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )" - # we can only get DNS: ones signed - if grep -qEv '^(DNS|othername):' <<<"${altnames}"; then - _exiterr "Certificate signing request contains non-DNS Subject Alternative Names" + # we can only get DNS/IP: ones signed + if grep -qEv '^(DNS|IP( Address)*|othername):' <<<"${altnames}"; then + _exiterr "Certificate signing request contains non-DNS/IP Subject Alternative Names" fi - # strip away the DNS: prefix - altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|othername:<unsupported>)//' )" + # strip away the DNS/IP: prefix + altnames="$( <<<"${altnames}" _sed -e 's/^(DNS:|IP( Address)*:|othername:<unsupported>)//' )" printf "%s" "${altnames}" | tr '\n' ' ' else # No SANs, extract CN - altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.* CN ?= ?([^ /,]*).*/\1/' )" + altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.*[ /]CN ?= ?([^ /,]*).*/\1/' )" printf "%s" "${altnames}" 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/' + <<<"${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 @@ -968,12 +1073,16 @@ # Request new order and store authorization URIs local challenge_identifiers="" for altname in ${altnames}; do - challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")" + if [[ "${altname}" =~ ^ip: ]]; then + challenge_identifiers+="$(printf '{"type": "ip", "value": "%s"}, ' "${altname:3}")" + else + challenge_identifiers+="$(printf '{"type": "dns", "value": "%s"}, ' "${altname}")" + fi done challenge_identifiers="[${challenge_identifiers%, }]" echo " + Requesting new certificate order from CA..." - order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | awk '{print $2}' | tr -d '\r\n')" + order_location="$(signed_request "${CA_NEW_ORDER}" '{"identifiers": '"${challenge_identifiers}"'}' 4>&1 | grep -i ^Location: | cut -d':' -f2- | tr -d ' \t\r\n')" result="$(signed_request "${order_location}" "" | jsonsh)" order_authorizations="$(echo "${result}" | get_json_array_values authorizations)" @@ -1001,6 +1110,7 @@ # Receive authorization ($authorization is authz uri) response="$(signed_request "$(echo "${authorization}" | _sed -e 's/\"(.*)".*/\1/')" "" | jsonsh)" identifier="$(echo "${response}" | get_json_string_value -p '"identifier","value"')" + identifier_type="$(echo "${response}" | get_json_string_value -p '"identifier","type"')" echo " + Handling authorization for ${identifier}" else # Request new authorization ($authorization is altname) @@ -1010,9 +1120,13 @@ fi # Check if authorization has already been validated - if [ "$(echo "${response}" | _sed 's/"challenges": \[\{.*\}\]//' | get_json_string_value status)" = "valid" ] && [ ! "${PARAM_FORCE:-no}" = "yes" ]; then - echo " + Found valid authorization for ${identifier}" - continue + if [ "$(echo "${response}" | get_json_string_value status)" = "valid" ]; then + if [ "${PARAM_FORCE_VALIDATION:-no}" = "yes" ]; then + echo " + A valid authorization has been found but will be ignored" + else + echo " + Found valid authorization for ${identifier}" + continue + fi fi # Find challenge in authorization @@ -1025,7 +1139,11 @@ challenge="$(echo "${response}" | get_json_dict_value -p '"challenges",'"${challengeindex}")" # Gather challenge information - challenge_names[${idx}]="${identifier}" + if [ "${identifier_type:-}" = "ip" ] && [ "${CHALLENGETYPE}" = "tls-alpn-01" ] ; then + challenge_names[${idx}]="$(echo "${identifier}" | ip_to_ptr)" + else + challenge_names[${idx}]="${identifier}" + fi challenge_tokens[${idx}]="$(echo "${challenge}" | get_json_string_value token)" if [[ ${API} -eq 2 ]]; then @@ -1052,13 +1170,17 @@ 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}" + keyauth_hook="$(printf '%s' "${keyauth}" | "${OPENSSL}" dgst -sha256 -c -hex | awk '{print $NF}')" + generate_alpn_certificate "${identifier}" "${identifier_type}" "${keyauth_hook}" ;; esac keyauths[${idx}]="${keyauth}" - deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}" + if [ "${identifier_type:-}" = "ip" ] && [ "${CHALLENGETYPE}" = "tls-alpn-01" ]; then + deploy_args[${idx}]="$(echo "${identifier}" | ip_to_ptr) ${challenge_tokens[${idx}]} ${keyauth_hook}" + else + deploy_args[${idx}]="${identifier} ${challenge_tokens[${idx}]} ${keyauth_hook}" + fi idx=$((idx+1)) done @@ -1069,11 +1191,13 @@ if [[ ${num_pending_challenges} -ne 0 ]]; then echo " + Deploying challenge tokens..." if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]]; then + # shellcheck disable=SC2068 "${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 + # shellcheck disable=SC2086 "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]} || _exiterr 'deploy_challenge hook returned with non-zero exit code' idx=$((idx+1)) done @@ -1120,6 +1244,7 @@ echo " + Cleaning challenge tokens..." # Clean challenge tokens using chained hook + # shellcheck disable=SC2068 [[ -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 @@ -1130,6 +1255,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 + # shellcheck disable=SC2086 [[ -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 @@ -1177,8 +1303,8 @@ if [ "${altcn}" = "${PREFERRED_CHAIN}" ]; then foundaltchain=1 fi - if [ "${foundaltchain}" = "0" ]; then - while read altcrturl; do + if [ "${foundaltchain}" = "0" ] && (grep -Ei '^link:' "${resheaders}" | grep -q -Ei 'rel="alternate"'); then + while read -r altcrturl; do if [ "${foundaltchain}" = "0" ]; then altcrt="$(signed_request "${altcrturl}" "")" altcn="$(get_last_cn "${altcrt}")" @@ -1266,7 +1392,8 @@ # Generate ALPN verification certificate generate_alpn_certificate() { local altname="${1}" - local acmevalidation="${2}" + local identifier_type="${2}" + local acmevalidation="${3}" local alpncertdir="${ALPNCERTDIR}" if [[ ! -e "${alpncertdir}" ]]; then @@ -1277,10 +1404,17 @@ 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}" + if [[ "${identifier_type}" = "ip" ]]; then + printf "\n[SAN]\nsubjectAltName=IP:%s\n" "${altname}" >> "${tmp_openssl_cnf}" + else + printf "\n[SAN]\nsubjectAltName=DNS:%s\n" "${altname}" >> "${tmp_openssl_cnf}" + fi + printf "1.3.6.1.5.5.7.1.31=critical,DER:04:20:%s\n" "${acmevalidation}" >> "${tmp_openssl_cnf}" SUBJ="/CN=${altname}/" [[ "${OSTYPE:0:5}" = "MINGW" ]] && SUBJ="/${SUBJ}" + if [[ "${identifier_type}" = "ip" ]]; then + altname="$(echo "${altname}" | ip_to_ptr)" + fi _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}" @@ -1312,10 +1446,11 @@ if [[ ! -r "${certdir}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then echo " + Generating private key..." privkey="privkey-${timestamp}.pem" - local tmp_privkey="$(_mktemp)" + local tmp_privkey + tmp_privkey="$(_mktemp)" case "${KEY_ALGO}" in rsa) _openssl genrsa -out "${tmp_privkey}" "${KEYSIZE}";; - prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${tmp_privkey}";; + prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${tmp_privkey}" -noout;; esac cat "${tmp_privkey}" > "${certdir}/privkey-${timestamp}.pem" rm "${tmp_privkey}" @@ -1332,7 +1467,7 @@ echo " + Generating private rollover key..." case "${KEY_ALGO}" in rsa) _openssl genrsa -out "${certdir}/privkey.roll.pem" "${KEYSIZE}";; - prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${certdir}/privkey.roll.pem";; + prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${certdir}/privkey.roll.pem" -noout;; esac fi # delete rolloverkeys if disabled @@ -1345,17 +1480,25 @@ echo " + Generating signing request..." SAN="" for altname in ${altnames}; do - SAN="${SAN}DNS:${altname}, " + if [[ "${altname}" =~ ^ip: ]]; then + SAN="${SAN}IP:${altname:3}, " + else + SAN="${SAN}DNS:${altname}, " + fi done + if [[ "${domain}" =~ ^ip: ]]; then + SUBJ="/CN=${domain:3}/" + else + SUBJ="/CN=${domain}/" + fi SAN="${SAN%%, }" local tmp_openssl_cnf tmp_openssl_cnf="$(_mktemp)" cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}" - printf "[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}" + printf "\n[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}" if [ "${OCSP_MUST_STAPLE}" = "yes" ]; then printf "\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >> "${tmp_openssl_cnf}" fi - SUBJ="/CN=${domain}/" if [[ "${OSTYPE:0:5}" = "MINGW" ]]; then # The subject starts with a /, so MSYS will assume it's a path and convert # it unless we escape it with another one: @@ -1426,20 +1569,21 @@ revision="$(cd "${SCRIPTDIR}"; git rev-parse HEAD 2>/dev/null || echo "unknown")" echo "GIT-Revision: ${revision}" echo "" - if [[ "${OSTYPE}" =~ "BSD" ]]; then + # shellcheck disable=SC1091 + if [[ "${OSTYPE}" =~ (BSD|Darwin) ]]; 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')" + echo "OS: $(grep -v '^$' /etc/issue | 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}" - if [[ "${OSTYPE}" =~ "BSD" ]]; then + if [[ "${OSTYPE}" =~ (BSD|Darwin) ]]; then echo " awk, sed, mktemp, grep, diff: BSD base system versions" else echo " awk: $(awk -W version 2>&1 | head -n1)" @@ -1518,6 +1662,20 @@ exit 0 } +# Parse contents of domains.txt and domains.txt.d +parse_domains_txt() { + # Allow globbing temporarily + noglob_set + local inputs=("${DOMAINS_TXT}" "${DOMAINS_TXT}.d"/*.txt) + noglob_clear + + cat "${inputs[@]}" | + tr -d '\r' | + awk '{print tolower($0)}' | + _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' -e 's/([^ ])>/\1 >/g' -e 's/> />/g' | + (grep -vE '^(#|$)' || true) +} + # Usage: --cron (-c) # Description: Sign/renew non-existent/changed/expiring certificates. command_sign_domains() { @@ -1535,9 +1693,9 @@ if [[ -n "${PARAM_DOMAIN:-}" ]]; then DOMAINS_TXT="$(_mktemp)" if [[ -n "${PARAM_ALIAS:-}" ]]; then - printf -- "${PARAM_DOMAIN} > ${PARAM_ALIAS}" > "${DOMAINS_TXT}" + printf "%s > %s" "${PARAM_DOMAIN}" "${PARAM_ALIAS}" > "${DOMAINS_TXT}" else - printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}" + printf "%s" "${PARAM_DOMAIN}" > "${DOMAINS_TXT}" fi elif [[ -e "${DOMAINS_TXT}" ]]; then if [[ ! -r "${DOMAINS_TXT}" ]]; then @@ -1550,17 +1708,17 @@ # Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire ORIGIFS="${IFS}" IFS=$'\n' - for line in $(<"${DOMAINS_TXT}" tr -d '\r' | awk '{print tolower($0)}' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' -e 's/([^ ])>/\1 >/g' -e 's/> />/g' | (grep -vE '^(#|$)' || true)); do + for line in $(parse_domains_txt); do reset_configvars IFS="${ORIGIFS}" alias="$(grep -Eo '>[^ ]+' <<< "${line}" || true)" line="$(_sed -e 's/>[^ ]+[ ]*//g' <<< "${line}")" aliascount="$(grep -Eo '>' <<< "${alias}" | awk 'END {print NR}' || true )" - [ ${aliascount} -gt 1 ] && _exiterr "Only one alias per line is allowed in domains.txt!" + [ "${aliascount}" -gt 1 ] && _exiterr "Only one alias per line is allowed in domains.txt!" domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)" morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)" - [ ${aliascount} -lt 1 ] && alias="${domain}" || alias="${alias#>}" + [ "${aliascount}" -lt 1 ] && alias="${domain}" || alias="${alias#>}" export alias if [[ -z "${morenames}" ]];then @@ -1614,6 +1772,8 @@ ); do config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)" config_value="$(echo "${cfgline:1}" | cut -d'=' -f2- | tr -d "'")" + # All settings that are allowed here should also be stored and + # restored in store_configvars() and reset_configvars() case "${config_var}" in 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}" @@ -1646,12 +1806,12 @@ fi # Check domain names of existing certificate - if [[ -e "${cert}" ]]; then + if [[ -e "${cert}" && "${force_renew}" = "no" ]]; then printf " + Checking domain name(s) of existing cert..." - certnames="$("${OPENSSL}" x509 -in "${cert}" -text -noout | grep DNS: | _sed 's/DNS://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')" - givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//' | _sed 's/^ //')" - + certnames="$("${OPENSSL}" x509 -in "${cert}" -text -noout | grep -E '(DNS|IP( Address*)):' | _sed 's/(DNS|IP( Address)*)://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')" + givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ip://g' | _sed 's/ $//' | _sed 's/^ //')" + if [[ "${certnames}" = "${givennames}" ]]; then echo " unchanged." else @@ -1692,13 +1852,14 @@ if [[ ! "${skip}" = "yes" ]]; then update_ocsp="yes" [[ -z "${csr}" ]] || printf "%s" "${csr}" > "${certdir}/cert-${timestamp}.csr" + # shellcheck disable=SC2086 if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then skip_exit_hook=yes - sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} & + sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} & wait $! || exit_with_errorcode=1 skip_exit_hook=no else - sign_domain "${certdir}" ${timestamp} ${domain} ${morenames} + sign_domain "${certdir}" "${timestamp}" "${domain}" ${morenames} fi fi @@ -1744,12 +1905,12 @@ # Usage: --signcsr (-s) path/to/csr.pem # Description: Sign a given CSR, output CRT on stdout (advanced usage) command_sign_csr() { + init_system + # redirect stdout to stderr # leave stdout over at fd 3 to output the cert exec 3>&1 1>&2 - init_system - # load csr csrfile="${1}" if [ ! -r "${csrfile}" ]; then @@ -1762,6 +1923,7 @@ # gen cert certfile="$(_mktemp)" + # shellcheck disable=SC2086 sign_csr "${csr}" ${altnames} 3> "${certfile}" # print cert @@ -1866,7 +2028,7 @@ fi # Allow globbing - [[ -n "${ZSH_VERSION:-}" ]] && set +o noglob || set +f + noglob_set # Loop over all certificate directories for certdir in "${CERTDIR}/"*; do @@ -1907,7 +2069,6 @@ # Check if current file is in use, if unused move to archive directory filename="$(basename "${file}")" if [[ ! "${filename}" = "${current}" ]] && [[ -f "${certdir}/${filename}" ]]; then - echo "${filename}" if [[ "${PARAM_CLEANUPDELETE:-}" = "yes" ]]; then echo "Deleting unused file: ${certname}/${filename}" rm "${certdir}/${filename}" @@ -1980,8 +2141,7 @@ fi } - # shellcheck disable=SC2199 - [[ -z "${@}" ]] && eval set -- "--help" + [[ -z "${*}" ]] && eval set -- "--help" while (( ${#} )); do case "${1}" in @@ -2107,6 +2267,12 @@ PARAM_FORCE="yes" ;; + # PARAM_Usage: --force-validation + # PARAM_Description: Force revalidation of domain names (used in combination with --force) + --force-validation) + PARAM_FORCE_VALIDATION="yes" + ;; + # PARAM_Usage: --no-lock (-n) # PARAM_Description: Don't use lockfile (potentially dangerous!) --no-lock|-n) @@ -2183,8 +2349,8 @@ 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 + # PARAM_Usage: --challenge (-t) http-01|dns-01|tls-alpn-01 + # PARAM_Description: Which challenge should be used? Currently http-01, dns-01, and tls-alpn-01 are supported --challenge|-t) shift 1 check_parameters "${1:-}" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/docs/domains_txt.md new/dehydrated-0.7.1/docs/domains_txt.md --- old/dehydrated-0.7.0/docs/domains_txt.md 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/docs/domains_txt.md 2022-10-31 15:12:38.000000000 +0100 @@ -34,6 +34,30 @@ example.net www.example.net wiki.example.net > certalias ``` +This allows to set per certificates options. The options you can change are +explained in [Per Certificate Config](per-certificate-config.md). + +If you want to create different certificate types for the same domain +you can use: + +```text +*.service.example.org service.example.org > star_service_example_org_rsa +*.service.example.org service.example.org > star_service_example_org_ecdsa +``` + +Then add a config file `certs/star_service_example_org_rsa/config` with +the value + +``` +KEY_ALGO="rsa" +``` + +or respectively + +``` +KEY_ALGO="ecdsa" +``` + ### Wildcards Support for wildcards was added by the ACME v2 protocol. @@ -70,3 +94,14 @@ **Note:** The first certificate is valid for both `service.example.com` and for `*.service.example.com` which can be a useful way to create wildcard certificates. + +### Drop-in directory + +If a directory named `domains.txt.d` exists in the same location as +`domains.txt`, the contents of `*.txt` files in that directory are appended to +the list of domains, in alphabetical order of the filenames. This is useful for +automation, as it doesn't require editing an existing file to add new domains. + +Warning: Behaviour of this might change as the naming between `domains.txt.d` +and the `DOMAINS_D` config variable (which is used for per-certificate +configuration) is a bit confusing. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/docs/examples/domains.txt new/dehydrated-0.7.1/docs/examples/domains.txt --- old/dehydrated-0.7.0/docs/examples/domains.txt 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/docs/examples/domains.txt 2022-10-31 15:12:38.000000000 +0100 @@ -24,6 +24,15 @@ # NOTE: It is a certificate for 'service.example.org' *.service.example.org service.example.org > star_service_example_org +# Optionally you can also append the certificate algorithm here to create +# multiple certificate types for the same domain. +# +# This allows to set per certificates options. How to do this is +# explained in [domains.txt documentation](domains_txt.md). +# +*.service.example.org service.example.org > star_service_example_org_rsa +*.service.example.org service.example.org > star_service_example_org_ecdsa + # Create a certificate for 'service.example.net' with an alternative name of # '*.service.example.net' (which is a wildcard domain) and store it in the # directory ${CERTDIR}/service.example.net diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/docs/examples/hook.sh new/dehydrated-0.7.1/docs/examples/hook.sh --- old/dehydrated-0.7.0/docs/examples/hook.sh 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/docs/examples/hook.sh 2022-10-31 15:12:38.000000000 +0100 @@ -1,199 +1,199 @@ #!/usr/bin/env bash deploy_challenge() { - local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" - # This hook is called once for every domain that needs to be - # validated, including any alternative names you may have listed. - # - # Parameters: - # - DOMAIN - # The domain name (CN or subject alternative name) being - # validated. - # - TOKEN_FILENAME - # The name of the file containing the token to be served for HTTP - # validation. Should be served by your web server as - # /.well-known/acme-challenge/${TOKEN_FILENAME}. - # - TOKEN_VALUE - # The token value that needs to be served for validation. For DNS - # validation, this is what you want to put in the _acme-challenge - # TXT record. For HTTP validation it is the value that is expected - # be found in the $TOKEN_FILENAME file. + # This hook is called once for every domain that needs to be + # validated, including any alternative names you may have listed. + # + # Parameters: + # - DOMAIN + # The domain name (CN or subject alternative name) being + # validated. + # - TOKEN_FILENAME + # The name of the file containing the token to be served for HTTP + # validation. Should be served by your web server as + # /.well-known/acme-challenge/${TOKEN_FILENAME}. + # - TOKEN_VALUE + # The token value that needs to be served for validation. For DNS + # validation, this is what you want to put in the _acme-challenge + # TXT record. For HTTP validation it is the value that is expected + # be found in the $TOKEN_FILENAME file. - # Simple example: Use nsupdate with local named - # printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key + # Simple example: Use nsupdate with local named + # printf 'server 127.0.0.1\nupdate add _acme-challenge.%s 300 IN TXT "%s"\nsend\n' "${DOMAIN}" "${TOKEN_VALUE}" | nsupdate -k /var/run/named/session.key } clean_challenge() { - local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" + local DOMAIN="${1}" TOKEN_FILENAME="${2}" TOKEN_VALUE="${3}" - # This hook is called after attempting to validate each domain, - # whether or not validation was successful. Here you can delete - # files or DNS records that are no longer needed. - # - # The parameters are the same as for deploy_challenge. + # This hook is called after attempting to validate each domain, + # whether or not validation was successful. Here you can delete + # files or DNS records that are no longer needed. + # + # The parameters are the same as for deploy_challenge. - # Simple example: Use nsupdate with local named - # 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 + # Simple example: Use nsupdate with local named + # 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}" + 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. + # 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}" + # 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}" + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" TIMESTAMP="${6}" - # This hook is called once for each certificate that has been - # produced. Here you might, for instance, copy your new certificates - # to service-specific locations and reload the service. - # - # Parameters: - # - DOMAIN - # The primary domain name, i.e. the certificate common - # name (CN). - # - 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). - # - TIMESTAMP - # Timestamp when the specified certificate was created. - - # Simple example: Copy file to nginx config - # cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl - # systemctl reload nginx + # This hook is called once for each certificate that has been + # produced. Here you might, for instance, copy your new certificates + # to service-specific locations and reload the service. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - 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). + # - TIMESTAMP + # Timestamp when the specified certificate was created. + + # Simple example: Copy file to nginx config + # cp "${KEYFILE}" "${FULLCHAINFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl + # systemctl reload nginx } deploy_ocsp() { - local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}" + local DOMAIN="${1}" OCSPFILE="${2}" TIMESTAMP="${3}" - # This hook is called once for each updated ocsp stapling file that has - # been produced. Here you might, for instance, copy your new ocsp stapling - # files to service-specific locations and reload the service. - # - # Parameters: - # - DOMAIN - # The primary domain name, i.e. the certificate common - # name (CN). - # - OCSPFILE - # The path of the ocsp stapling file - # - TIMESTAMP - # Timestamp when the specified ocsp stapling file was created. - - # Simple example: Copy file to nginx config - # cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl - # systemctl reload nginx + # This hook is called once for each updated ocsp stapling file that has + # been produced. Here you might, for instance, copy your new ocsp stapling + # files to service-specific locations and reload the service. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - OCSPFILE + # The path of the ocsp stapling file + # - TIMESTAMP + # Timestamp when the specified ocsp stapling file was created. + + # Simple example: Copy file to nginx config + # cp "${OCSPFILE}" /etc/nginx/ssl/; chown -R nginx: /etc/nginx/ssl + # systemctl reload nginx } unchanged_cert() { - local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" + local DOMAIN="${1}" KEYFILE="${2}" CERTFILE="${3}" FULLCHAINFILE="${4}" CHAINFILE="${5}" - # This hook is called once for each certificate that is still - # valid and therefore wasn't reissued. - # - # Parameters: - # - DOMAIN - # The primary domain name, i.e. the certificate common - # name (CN). - # - 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). + # This hook is called once for each certificate that is still + # valid and therefore wasn't reissued. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - 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). } invalid_challenge() { - local DOMAIN="${1}" RESPONSE="${2}" + local DOMAIN="${1}" RESPONSE="${2}" - # This hook is called if the challenge response has failed, so domain - # owners can be aware and act accordingly. - # - # Parameters: - # - DOMAIN - # The primary domain name, i.e. the certificate common - # name (CN). - # - RESPONSE - # The response that the verification server returned + # This hook is called if the challenge response has failed, so domain + # owners can be aware and act accordingly. + # + # Parameters: + # - DOMAIN + # The primary domain name, i.e. the certificate common + # name (CN). + # - RESPONSE + # The response that the verification server returned - # Simple example: Send mail to root - # printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root + # Simple example: Send mail to root + # printf "Subject: Validation of ${DOMAIN} failed!\n\nOh noez!" | sendmail root } request_failure() { - local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}" + local STATUSCODE="${1}" REASON="${2}" REQTYPE="${3}" HEADERS="${4}" - # This hook is called when an HTTP request fails (e.g., when the ACME - # server is busy, returns an error, etc). It will be called upon any - # response code that does not start with '2'. Useful to alert admins - # about problems with requests. - # - # Parameters: - # - STATUSCODE - # The HTML status code that originated the error. - # - REASON - # The specified reason for the error. - # - REQTYPE - # The kind of request that was made (GET, POST...) - # - HEADERS - # HTTP headers returned by the CA + # This hook is called when an HTTP request fails (e.g., when the ACME + # server is busy, returns an error, etc). It will be called upon any + # response code that does not start with '2'. Useful to alert admins + # about problems with requests. + # + # Parameters: + # - STATUSCODE + # The HTML status code that originated the error. + # - REASON + # The specified reason for the error. + # - REQTYPE + # The kind of request that was made (GET, POST...) + # - HEADERS + # HTTP headers returned by the CA - # Simple example: Send mail to root - # printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root + # Simple example: Send mail to root + # printf "Subject: HTTP request failed failed!\n\nA http request failed with status ${STATUSCODE}!" | sendmail root } generate_csr() { - local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}" + local DOMAIN="${1}" CERTDIR="${2}" ALTNAMES="${3}" - # 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 certificate signing request formatted as PEM. - # - # Parameters: - # - DOMAIN - # The primary domain as specified in domains.txt. This does not need to - # match with the domains in the CSR, it's basically just the directory name. - # - CERTDIR - # Certificate output directory for this particular certificate. Can be used - # for storing additional files. - # - ALTNAMES - # All domain names for the current certificate as specified in domains.txt. - # Again, this doesn't need to match with the CSR, it's just there for convenience. - - # Simple example: Look for pre-generated CSRs - # if [ -e "${CERTDIR}/pre-generated.csr" ]; then - # cat "${CERTDIR}/pre-generated.csr" - # fi + # 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 certificate signing request formatted as PEM. + # + # Parameters: + # - DOMAIN + # The primary domain as specified in domains.txt. This does not need to + # match with the domains in the CSR, it's basically just the directory name. + # - CERTDIR + # Certificate output directory for this particular certificate. Can be used + # for storing additional files. + # - ALTNAMES + # All domain names for the current certificate as specified in domains.txt. + # Again, this doesn't need to match with the CSR, it's just there for convenience. + + # Simple example: Look for pre-generated CSRs + # if [ -e "${CERTDIR}/pre-generated.csr" ]; then + # cat "${CERTDIR}/pre-generated.csr" + # fi } startup_hook() { Binary files old/dehydrated-0.7.0/docs/logo.jpg and new/dehydrated-0.7.1/docs/logo.jpg differ Binary files old/dehydrated-0.7.0/docs/logo.png and new/dehydrated-0.7.1/docs/logo.png differ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/docs/per-certificate-config.md new/dehydrated-0.7.1/docs/per-certificate-config.md --- old/dehydrated-0.7.0/docs/per-certificate-config.md 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/docs/per-certificate-config.md 2022-10-31 15:12:38.000000000 +0100 @@ -11,6 +11,8 @@ - KEY_ALGO - KEYSIZE - OCSP_MUST_STAPLE +- OCSP_FETCH +- OCSP_DAYS - CHALLENGETYPE - HOOK - HOOK_CHAIN diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/docs/staging.md new/dehydrated-0.7.1/docs/staging.md --- old/dehydrated-0.7.0/docs/staging.md 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/docs/staging.md 2022-10-31 15:12:38.000000000 +0100 @@ -8,10 +8,7 @@ To avoid this, please set the CA property to the Let???s Encrypt staging server URL in your config file: ```bash -CA="https://acme-staging.api.letsencrypt.org/directory" +CA="https://acme-staging-v02.api.letsencrypt.org/directory" ``` -# ACMEv2 staging - -You can use `CA="https://acme-staging-v02.api.letsencrypt.org/directory"` to test dehydrated with -the ACMEv2 staging endpoint. +Alternatively you can define the CA using the CLI argument `--ca letsencrypt-test` (`letsencrypt-test` is an integrated preset-CA corresponding to the URL above). diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/dehydrated-0.7.0/docs/tls-alpn.md new/dehydrated-0.7.1/docs/tls-alpn.md --- old/dehydrated-0.7.0/docs/tls-alpn.md 2020-12-10 16:54:26.000000000 +0100 +++ new/dehydrated-0.7.1/docs/tls-alpn.md 2022-10-31 15:12:38.000000000 +0100 @@ -6,6 +6,26 @@ Dehydrated generates the required verification certificates, but the delivery is out of its scope. +### Example lighttpd config + +lighttpd can be configured to recognize ALPN `acme-tls/1` and to respond to such +requests using the specially crafted TLS certificates generated by dehydrated. +Configure lighttpd and dehydrated to use the same path for these certificates. +(Be sure to allow read access to the user account under which the lighttpd +server is running.) `mkdir -p /etc/dehydrated/alpn-certs` + +lighttpd.conf: +``` +ssl.acme-tls-1 = "/etc/dehydrated/alpn-certs" +``` + +When renewing certificates, specify `-t tls-alpn-01` and `--alpn /etc/dehydrated/alpn-certs` to dehydrated, e.g. +``` +dehydrated -t tls-alpn-01 --alpn /etc/dehydrated/alpn-certs -c --out /etc/lighttpd/certs -d www.example.com +# gracefully reload lighttpd to use the new certificates by sending lighttpd pid SIGUSR1 +systemctl reload lighttpd +``` + ### Example nginx config On an nginx tcp load-balancer you can use the `ssl_preread` module to map a different port for acme-tls