diff --git a/getssl b/getssl index 066a83c..f522363 100755 --- a/getssl +++ b/getssl @@ -256,7 +256,11 @@ # 2021-02-07 Allow -u --upgrade without any domain, so that one can only update the script (Benno-K)(2.34) # 2021-02-09 Prevent listing the complete file if version tag missing (#637)(softins) # 2021-02-12 Add PREFERRED_CHAIN -# 2021-02-15 ADD ftp explicit SSL with curl for upload the challenge +# 2021-02-15 ADD ftp explicit SSL with curl for upload the challenge (CoolMischa) +# 2021-02-18 Add FULL_CHAIN_INCLUDE_ROOT +# 2021-03-25 Fix DNS challenge completion check if CNAMEs on different NS are used (sideeffect42)(2.35) +# 2021-05-08 Merge from tlhackque/getssl: GoDaddy, split-view, tempfile permissions fixes, --version(2.36) +# 2021-05-26 Fix 'date -j' error on Mac fixed. Mac OS BigSur uses version: date (GNU coreutils) 8.32 (CooMischa) # ---------------------------------------------------------------------------------------- case :$SHELLOPTS: in @@ -265,7 +269,7 @@ esac PROGNAME=${0##*/} PROGDIR="$(cd "$(dirname "$0")" || exit; pwd -P;)" -VERSION="2.34" +VERSION="2.36" # defaults ACCOUNT_KEY_LENGTH=4096 @@ -283,6 +287,7 @@ DEFAULT_REVOKE_CA="https://acme-v02.api.letsencrypt.org" DOMAIN_KEY_LENGTH=4096 DUAL_RSA_ECDSA="false" FTP_OPTIONS="" +FULL_CHAIN_INCLUDE_ROOT="false" GETSSL_IGNORE_CP_PRESERVE="false" HTTP_TOKEN_CHECK_WAIT=0 IGNORE_DIRECTORY_DOMAIN="false" @@ -300,12 +305,13 @@ OCSP_MUST_STAPLE="false" TEMP_UPGRADE_FILE="" TOKEN_USER_ID="" USE_SINGLE_ACL="false" -WORKING_DIR_CANDIDATES=("/etc/getssl/" "${PROGDIR}/conf" "${PROGDIR}/.getssl" "${HOME}/.getssl") +WORKING_DIR_CANDIDATES=("/etc/getssl" "${PROGDIR}/conf" "${PROGDIR}/.getssl" "${HOME}/.getssl") # Variables used when validating using a DNS entry VALIDATE_VIA_DNS="" # Set this to "true" to enable DNS validation -AUTH_DNS_SERVER="" # Use this DNS server to check the challenge token has been set -PUBLIC_DNS_SERVER="" # Use this DNS server to find the authoritative DNS servers for the domain +export AUTH_DNS_SERVER="" # Use this DNS server to check the challenge token has been set +export DNS_CHECK_OPTIONS="" # Options (such as TSIG file) required by DNS_CHECK_FUNC +export PUBLIC_DNS_SERVER="" # Use this DNS server to find the authoritative DNS servers for the domain CHECK_ALL_AUTH_DNS="false" # Check the challenge token has been set on all authoritative DNS servers CHECK_PUBLIC_DNS_SERVER="true" # Check the public DNS server as well as the authoritative DNS servers DNS_ADD_COMMAND="" # Use this command/script to add the challenge token to the DNS entries for the domain @@ -333,7 +339,7 @@ _UPGRADE_CHECK=1 _USE_DEBUG=0 _ONLY_CHECK_CONFIG=0 config_errors="false" -LANG=C +export LANG=C API=1 # store copy of original command in case of upgrading script and re-running @@ -516,48 +522,42 @@ check_challenge_completion() { # checks with the ACME server if our challenge is } check_challenge_completion_dns() { # perform validation via DNS challenge - token=$1 - uri=$2 - keyauthorization=$3 - d=$4 - primary_ns=$5 - auth_key=$6 - - # Always use lowercase domain name when querying DNS servers - # shellcheck disable=SC2018,SC2019 - lower_d=$(echo "${d##\*.}" | tr A-Z a-z) + d=${1} + rr=${2} + primary_ns=${3} + auth_key=${4} # check for token at public dns server, waiting for a valid response. for ns in $primary_ns; do - info "checking dns at $ns" + info "checking DNS at $ns" ntries=0 check_dns="fail" while [[ "$check_dns" == "fail" ]]; do if [[ "$os" == "cygwin" ]]; then - check_result=$(nslookup -type=txt "_acme-challenge.${lower_d}" "${ns}" \ + check_result=$(nslookup -type=txt "${rr}" "${ns}" \ | grep ^_acme -A2\ | grep '"'|awk -F'"' '{ print $2}') elif [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then - debug "$DNS_CHECK_FUNC" TXT "_acme-challenge.${lower_d}" "@${ns}" - check_result=$($DNS_CHECK_FUNC TXT "_acme-challenge.${lower_d}" "@${ns}" \ - | grep -i "^_acme-challenge.${lower_d}" \ + debug "$DNS_CHECK_FUNC" TXT "${rr}" "@${ns}" + check_result=$($DNS_CHECK_FUNC TXT "${rr}" "@${ns}" \ + | grep -i "^${rr}" \ | grep 'IN\WTXT'|awk -F'"' '{ print $2}') debug "check_result=$check_result" if [[ -z "$check_result" ]]; then - debug "$DNS_CHECK_FUNC" ANY "_acme-challenge.${lower_d}" "@${ns}" - check_result=$($DNS_CHECK_FUNC ANY "_acme-challenge.${lower_d}" "@${ns}" \ - | grep -i "^_acme-challenge.${lower_d}" \ + debug "$DNS_CHECK_FUNC" ANY "${rr}" "@${ns}" + check_result=$($DNS_CHECK_FUNC ANY "${rr}" "@${ns}" \ + | grep -i "^${rr}" \ | grep 'IN\WTXT'|awk -F'"' '{ print $2}') debug "check_result=$check_result" fi elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then - check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${lower_d}" "${ns}" \ + check_result=$($DNS_CHECK_FUNC -t TXT "${rr}" "${ns}" \ | grep 'descriptive text'|awk -F'"' '{ print $2}') else - check_result=$(nslookup -type=txt "_acme-challenge.${lower_d}" "${ns}" \ + check_result=$(nslookup -type=txt "${rr}" "${ns}" \ | grep 'text ='|awk -F'"' '{ print $2}') if [[ -z "$check_result" ]]; then - check_result=$(nslookup -type=any "_acme-challenge.${lower_d}" "${ns}" \ + check_result=$(nslookup -type=any "${rr}" "${ns}" \ | grep 'text ='|awk -F'"' '{ print $2}') fi fi @@ -571,22 +571,20 @@ check_challenge_completion_dns() { # perform validation via DNS challenge ntries=$(( ntries + 1 )) if [[ $DNS_WAIT_RETRY_ADD == "true" && $(( ntries % 10 )) == 0 ]]; then - debug "Retrying adding dns via command: $DNS_ADD_COMMAND $lower_d $auth_key" - test_output "Retrying adding dns via command: $DNS_ADD_COMMAND" - eval "$DNS_DEL_COMMAND" "$lower_d" "$auth_key" - if ! eval "$DNS_ADD_COMMAND" "$lower_d" "$auth_key" ; then - error_exit "DNS_ADD_COMMAND failed for domain $d" - fi - + test_output "Deleting DNS RR via command: ${DNS_DEL_COMMAND}" + del_dns_rr "${d}" "${auth_key}" + test_output "Retrying adding DNS via command: ${DNS_ADD_COMMAND}" + add_dns_rr "${d}" "${auth_key}" \ + || error_exit "DNS_ADD_COMMAND failed for domain ${d}" fi - info "checking DNS at ${ns} for ${lower_d}. Attempt $ntries/${DNS_WAIT_COUNT} gave wrong result, "\ + info "checking DNS at ${ns} for ${rr}. Attempt $ntries/${DNS_WAIT_COUNT} gave wrong result, "\ "waiting $DNS_WAIT secs before checking again" sleep $DNS_WAIT else debug "dns check failed - removing existing value" - eval "$DNS_DEL_COMMAND" "$lower_d" "$auth_key" + del_dns_rr "${d}" "${auth_key}" - error_exit "checking _acme-challenge.${lower_d} gave $check_result not $auth_key" + error_exit "checking ${rr} gave $check_result not $auth_key" fi fi done @@ -596,13 +594,6 @@ check_challenge_completion_dns() { # perform validation via DNS challenge info "sleeping $DNS_EXTRA_WAIT seconds before asking the ACME server to check the dns" sleep "$DNS_EXTRA_WAIT" fi - - check_challenge_completion "$uri" "$d" "$keyauthorization" - - debug "remove DNS entry" - # shellcheck disable=SC2018,SC2019 - lower_d=$(echo "${d##\*.}" | tr A-Z a-z) - eval "$DNS_DEL_COMMAND" "$lower_d" "$auth_key" } # end of ... perform validation if via DNS challenge @@ -625,7 +616,7 @@ check_config() { # check the config files for all obvious errors rsa|prime256v1|secp384r1|secp521r1) debug "checked PRIVATE_KEY_ALG " ;; *) - info "${DOMAIN}: invalid PRIVATE_KEY_ALG - $PRIVATE_KEY_ALG" + info "${DOMAIN}: invalid PRIVATE_KEY_ALG - '$PRIVATE_KEY_ALG'" config_errors=true ;; esac if [[ "$DUAL_RSA_ECDSA" == "true" ]] && [[ "$PRIVATE_KEY_ALG" == "rsa" ]]; then @@ -681,32 +672,32 @@ check_config() { # check the config files for all obvious errors config_errors=true fi - # check domain exists using all DNS utilities + # check domain exists using all DNS utilities. DNS_CHECK_OPTIONS may bind IP address or provide TSIG found_ip=false if [[ -n "$HAS_DIG_OR_DRILL" ]]; then - debug "DNS lookup using $HAS_DIG_OR_DRILL ${d}" - if [[ "$($HAS_DIG_OR_DRILL -t SOA "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then + debug "DNS lookup using $HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS ${d}" + if [[ "$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -t SOA "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then found_ip=true - elif [[ "$($HAS_DIG_OR_DRILL -t A "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then + elif [[ "$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -t A "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then found_ip=true - elif [[ "$($HAS_DIG_OR_DRILL -t AAAA "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then + elif [[ "$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -t AAAA "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then found_ip=true fi fi if [[ "$HAS_HOST" == "true" ]]; then debug "DNS lookup using host ${d}" - if [[ "$(host "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then + if [[ "$(host $DNS_CHECK_OPTIONS "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then found_ip=true fi fi if [[ "$HAS_NSLOOKUP" == "true" ]]; then debug "DNS lookup using nslookup -query AAAA ${d}" - if [[ "$(nslookup -query=AAAA "${d}"|grep -c -i "^${d}.*has AAAA address")" -ge 1 ]]; then + if [[ "$(nslookup $DNS_CHECK_OPTIONS -query=AAAA "${d}"|grep -c -i "^${d}.*has AAAA address")" -ge 1 ]]; then debug "found IPv6 record for ${d}" found_ip=true - elif [[ "$(nslookup "${d}"| grep -c ^Name)" -ge 1 ]]; then + elif [[ "$(nslookup $DNS_CHECK_OPTIONS "${d}"| grep -c ^Name)" -ge 1 ]]; then debug "found IPv4 record for ${d}" found_ip=true fi @@ -805,7 +796,7 @@ clean_up() { # Perform pre-exit housekeeping # shellcheck source=/dev/null . "$dnsfile" debug "attempting to clean up DNS entry for $d" - eval "$DNS_DEL_COMMAND" "${d##\*.}" "$auth_key" + del_dns_rr "${d}" "${auth_key}" done shopt -u nullglob fi @@ -1079,8 +1070,9 @@ create_order() { date_epoc() { # convert the date into epoch time if [[ "$os" == "bsd" ]]; then date -j -f "%b %d %T %Y %Z" "$1" +%s - elif [[ "$os" == "mac" ]]; then - date -j -f "%b %d %T %Y %Z" "$1" +%s +# elif [[ "$os" == "mac" ]]; then +# date -j -f "%b %d %T %Y %Z" "$1" +%s +# date -d "$1" +%s elif [[ "$os" == "busybox" ]]; then de_ld=$(echo "$1" | awk '{print $1 " " $2 " " $3 " " $4}') date -D "%b %d %T %Y" -d "$de_ld" +%s @@ -1091,10 +1083,11 @@ date_epoc() { # convert the date into epoch time } date_fmt() { # format date from epoc time to YYYY-MM-DD - if [[ "$os" == "bsd" ]]; then #uses older style date function. - date -j -f "%s" "$1" +%F - elif [[ "$os" == "mac" ]]; then # macOS uses older BSD style date. + if [[ "$os" == "bsd" ]]; then # uses older style date function. date -j -f "%s" "$1" +%F +# elif [[ "$os" == "mac" ]]; then # macOS uses older BSD style date. +# date -j -f "%s" "$1" +%F +# date -d "@$1" +%F else date -d "@$1" +%F fi @@ -1169,6 +1162,26 @@ find_ftp_command() { } +add_dns_rr() { + d=${1} + auth_key=${2} + + # shellcheck disable=SC2018,SC2019 + lower_d=$(printf '%s' "${d#\*.}" | tr 'A-Z' 'a-z') + debug "adding DNS RR via command: ${DNS_ADD_COMMAND} ${lower_d} ${auth_key}" + eval "${DNS_ADD_COMMAND}" "${lower_d}" "${auth_key}" +} + +del_dns_rr() { + d=${1} + auth_key=${2} + + # shellcheck disable=SC2018,SC2019 + lower_d=$(printf '%s' "${d#\*.}" | tr 'A-Z' 'a-z') + debug "removing DNS RR via command: ${DNS_DEL_COMMAND} ${lower_d} ${auth_key}" + eval "${DNS_DEL_COMMAND}" "${lower_d}" "${auth_key}" +} + fulfill_challenges() { dn=0 for d in "${alldomains[@]}"; do @@ -1234,16 +1247,24 @@ for d in "${alldomains[@]}"; do | sed -e 's:=*$::g' -e 'y:+/:-_:') debug auth_key "$auth_key" + add_dns_rr "${d}" "${auth_key}" \ + || error_exit "DNS_ADD_COMMAND failed for domain $d" + # shellcheck disable=SC2018,SC2019 - lower_d=$(echo "${d##\*.}" | tr A-Z a-z) - debug "adding dns via command: $DNS_ADD_COMMAND $lower_d $auth_key" - if ! eval "$DNS_ADD_COMMAND" "$lower_d" "$auth_key" ; then - error_exit "DNS_ADD_COMMAND failed for domain $d" - fi + rr="_acme-challenge.$(printf '%s' "${d#\*.}" | tr 'A-Z' 'a-z')" # find a primary / authoritative DNS server for the domain if [[ -z "$AUTH_DNS_SERVER" ]]; then - get_auth_dns "$d" + # Find authorative dns server for _acme-challenge.{domain} (for CNAMES/acme-dns) + get_auth_dns "${rr}" + if test -n "${cname}"; then + rr=${cname} + fi + + # If no authorative dns server found, try again for {domain} + if [[ -z "$primary_ns" ]]; then + get_auth_dns "$d" + fi elif [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then primary_ns="$AUTH_DNS_SERVER $PUBLIC_DNS_SERVER" else @@ -1251,7 +1272,13 @@ for d in "${alldomains[@]}"; do fi debug set primary_ns = "$primary_ns" - check_challenge_completion_dns "${token}" "${uri}" "${keyauthorization}" "${d}" "${primary_ns}" "${auth_key}" + # internal check + check_challenge_completion_dns "${d}" "${rr}" "${primary_ns}" "${auth_key}" + + # let Let's Encrypt check + check_challenge_completion "${uri}" "${d}" "${keyauthorization}" + + del_dns_rr "${d}" "${auth_key}" else # set up the correct http token for verification if [[ $API -eq 1 ]]; then # get the token from the http component @@ -1364,45 +1391,45 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n gad_s="@$gad_s" fi - # Use SOA +trace to find the name server - if [[ $_TEST_SKIP_SOA_CALL == 0 ]]; then - if [[ "$HAS_DIG_OR_DRILL" == "drill" ]]; then - debug Using "$HAS_DIG_OR_DRILL -T $gad_d $gad_s" to find primary nameserver - test_output "Using $HAS_DIG_OR_DRILL SOA" - res=$($HAS_DIG_OR_DRILL -T SOA "$gad_d" $gad_s 2>/dev/null | grep "IN\WNS\W") - else - debug Using "$HAS_DIG_OR_DRILL SOA +trace +nocomments $gad_d $gad_s" to find primary nameserver - test_output "Using $HAS_DIG_OR_DRILL SOA" - res=$($HAS_DIG_OR_DRILL SOA +trace +nocomments "$gad_d" $gad_s 2>/dev/null | grep "IN\WNS\W") - fi - fi + # Check if domain is a CNAME, first + test_output "Using $HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS CNAME" - # Check if domain is a CNAME - if [[ -z "$res" ]]; then - test_output "Using $HAS_DIG_OR_DRILL CNAME" + # Two options here; either dig CNAME will return the CNAME and the NS or just the CNAME + debug Checking for CNAME using "$HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS CNAME $gad_d $gad_s" + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS CNAME "$gad_d" $gad_s| grep "^$gad_d") + cname=$(echo "$res"| awk '$4 ~ "CNAME" {print $5}' |sed 's/\.$//g') - # Two options here; either dig CNAME will return the CNAME and the NS or just the CNAME - debug Checking for CNAME using "$HAS_DIG_OR_DRILL CNAME $gad_d $gad_s" - res=$($HAS_DIG_OR_DRILL CNAME "$gad_d" $gad_s| grep "^$gad_d") - cname=$(echo "$res"| awk '$4 ~ "CNAME" {print $5}' |sed 's/\.$//g') + if [[ $_TEST_SKIP_CNAME_CALL == 0 ]]; then + debug Checking if CNAME result contains NS records + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS CNAME "$gad_d" $gad_s| grep -E "IN\W(NS|SOA)\W") + else + res= + fi - if [[ $_TEST_SKIP_CNAME_CALL == 0 ]]; then - debug Checking if CNAME result contains NS records - res=$($HAS_DIG_OR_DRILL CNAME "$gad_d" $gad_s| grep -E "IN\W(NS|SOA)\W") - else - res="" - fi + if [[ -n "${cname}" ]]; then + # domain is a CNAME: resolve it and continue with that + debug Domain is a CNAME, actual domain is "$cname" + gad_d=${cname} + fi - if [[ -n "$cname" ]]; then # domain is a CNAME so get main domain - debug Domain is a CNAME, actual domain is "$cname" + # Use SOA +trace to find the name server + if [[ -z "$res" ]] && [[ $_TEST_SKIP_SOA_CALL == 0 ]]; then + if [[ "$HAS_DIG_OR_DRILL" == "drill" ]]; then + debug Using "$HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -T $gad_d $gad_s" to find primary nameserver + test_output "Using $HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS SOA" + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -T SOA "$gad_d" $gad_s 2>/dev/null | grep "IN\WNS\W") + else + debug Using "$HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS SOA +trace +nocomments $gad_d $gad_s" to find primary nameserver + test_output "Using $HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS SOA" + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS SOA +trace +nocomments "$gad_d" $gad_s 2>/dev/null | grep "IN\WNS\W") fi fi # Query for NS records if [[ -z "$res" ]]; then - test_output "Using $HAS_DIG_OR_DRILL NS" - debug Using "$HAS_DIG_OR_DRILL NS $gad_d $gad_s" to find primary nameserver - res=$($HAS_DIG_OR_DRILL NS "$gad_d" $gad_s | grep -E "IN\W(NS|SOA)\W") + test_output "Using $HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS NS" + debug Using "$HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS NS $gad_d $gad_s" to find primary nameserver + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS NS "$gad_d" $gad_s | grep -E "IN\W(NS|SOA)\W") fi if [[ -n "$res" ]]; then @@ -1442,10 +1469,10 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n gad_d="$orig_gad_d" debug Using "host -t NS" to find primary name server for "$gad_d" if [[ -z "$gad_s" ]]; then - res=$(host -t NS "$gad_d"| grep "name server") + res=$(host $DNS_CHECK_OPTIONS -t NS "$gad_d"| grep "name server") else # shellcheck disable=SC2086 - res=$(host -t NS "$gad_d" $gad_s| grep "name server") + res=$(host $DNS_CHECK_OPTIONS -t NS "$gad_d" $gad_s| grep "name server") fi if [[ -n "$res" ]]; then all_auth_dns_servers=$(echo "$res" | awk '{print $4}' | sed 's/\.$//g'|tr '\n' ' ') @@ -1465,9 +1492,9 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n if [[ "$HAS_NSLOOKUP" == "true" ]]; then gad_d="$orig_gad_d" - debug Using "nslookup -debug -type=soa -type=ns $gad_d $gad_s" to find primary name server + debug Using "nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns $gad_d $gad_s" to find primary name server # shellcheck disable=SC2086 - res=$(nslookup -debug -type=soa -type=ns "$gad_d" ${gad_s}) + res=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" ${gad_s}) if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then # this is a Non-authoritative server, need to check for an authoritative one. @@ -1483,7 +1510,7 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n fi # shellcheck disable=SC2086 - res=$(nslookup -debug -type=soa -type=ns "$gad_d" ${gad_s}) + res=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" ${gad_s}) fi if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then @@ -1499,7 +1526,7 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n # shellcheck disable=SC2086 # not quoting gad_s fixes the nslookup: couldn't get address for '': not found warning (#332) - all_auth_dns_servers=$(nslookup -debug -type=soa -type=ns "$gad_d" $gad_s \ + all_auth_dns_servers=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" $gad_s \ | awk '$1 ~ "nameserver" {print $3}' \ | sed 's/\.$//g'| tr '\n' ' ') @@ -1527,6 +1554,7 @@ get_certificate() { # get certificate for csr, if all domains validated. gc_fullchain=$4 # The filename for the fullchain der=$(openssl req -in "$gc_csr" -outform DER | urlbase64) + if [[ $API -eq 1 ]]; then send_signed_request "$URL_new_cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64" # convert certificate information into correct format and save to file. @@ -1580,7 +1608,7 @@ get_certificate() { # get certificate for csr, if all domains validated. cp "$gc_fullchain" "$cert_to_check" i=0 while [[ $i -le ${#alternate_links[@]} ]]; do - cert_issuer=$(openssl crl2pkcs7 -nocrl -certfile "$cert_to_check" | openssl pkcs7 -print_certs -text -noout | grep 'Issuer:' | tail -1 | cut -d= -f2) + cert_issuer=$(openssl crl2pkcs7 -nocrl -certfile "$cert_to_check" | openssl pkcs7 -print_certs -text -noout | grep 'Issuer:' | tail -1 | awk -F"CN=" '{ print $2 }') debug Certificate issued by "$cert_issuer" if [[ $cert_issuer = *${PREFERRED_CHAIN}* ]]; then debug "Found required certificate" @@ -1598,7 +1626,27 @@ get_certificate() { # get certificate for csr, if all domains validated. # tidy up rm -f "$cert_to_check" fi + awk -v CERT_FILE="$gc_certfile" -v CA_CERT="$gc_cafile" 'BEGIN {outfile=CERT_FILE} split_after==1 {outfile=CA_CERT;split_after=0} /-----END CERTIFICATE-----/ {split_after=1} {print > outfile}' "$gc_fullchain" + if [[ "$FULL_CHAIN_INCLUDE_ROOT" = "true" ]]; then + # Some of the code below was copied from zakjan/cert-chain-resolver + + # Download the certificate for the issuer using the "CA Issuers" attribute from the AIA x509 extension + issuer_url=$(openssl x509 -inform pem -noout -text -in "$gc_certfile" | awk 'BEGIN {FS="CA Issuers - URI:"} NF==2 {print $2; exit}') + debug Issuer for "$gc_certfile" is "$issuer_url" + + # Keep downloading issuer certficates until we find the root certificate (which doesn't have a "CA Issuers" attribure) + cp "$gc_certfile" "$gc_fullchain" + while [[ -n "$issuer_url" ]]; do + debug Fetching certificate issuer from "$issuer_url" + issuer_cert=$(curl --user-agent "$CURL_USERAGENT" --silent "$issuer_url" | openssl x509 -inform der -outform pem) + debug Fetched issuer certificate "$(echo "$issuer_cert" | openssl x509 -inform pem -noout -text | awk 'BEGIN {FS="Subject: "} NF==2 {print $2; exit}')" + echo "$issuer_cert" >> "$gc_fullchain" + + # get issuer for the certificate that's just been downloaded + issuer_url=$(echo "$issuer_cert" | openssl x509 -inform pem -noout -text | awk 'BEGIN {FS="CA Issuers - URI:"} NF==2 {print $2; exit}') + done + fi info "Certificate saved in $gc_certfile" fi } @@ -1715,6 +1763,7 @@ help_message() { # print out the help message -u, --upgrade Upgrade getssl if a more recent version is available - can be used with or without domain(s) -k, --keep "#" Maximum number of old getssl versions to keep when upgrading -U, --nocheck Do not check if a more recent version is available + -v --version Display current version of $PROGNAME -w working_dir "Working directory" --preferred-chain "chain" Use an alternate chain for the certificate @@ -2051,16 +2100,18 @@ revoke_certificate() { # revoke a certificate } requires() { # check if required function is available + args=("${@}") + lastarg=${args[${#args[@]}-1]} if [[ "$#" -gt 1 ]]; then # if more than 1 value, check list for i in "$@"; do - if [[ "$i" == "${!#}" ]]; then # if on last variable then exit as not found + if [[ "$i" == "$lastarg" ]]; then # if on last variable then exit as not found error_exit "this script requires one of: ${*:1:$(($#-1))}" fi res=$(command -v "$i" 2>/dev/null) debug "checking for $i ... $res" if [[ -n "$res" ]]; then # if function found, then set variable to function and return - debug "function $i found at $res - setting ${!#} to $i" - eval "${!#}=\$i" + debug "function $i found at $res - setting ${lastarg} to $i" + eval "${lastarg}=\$i" return fi done @@ -2377,10 +2428,14 @@ write_domain_template() { # write out a template file for a domain. # Set USE_SINGLE_ACL="true" to use a single ACL for all checks #USE_SINGLE_ACL="false" - # Preferred Chain - use an different certificate root from the default - # Staging options are: "Fake LE Root X1" and "Fake LE Root X2" - # Production options are: "ISRG Root X1" and "ISRG Root X2" - #PREFERRED_CHAIN="" + # Preferred Chain - use an different certificate root from the default + # This uses wildcard matching so requesting "X1" returns the correct certificate - may need to escape characters + # Staging options are: "(STAGING) Doctored Durian Root CA X3" and "(STAGING) Pretend Pear X1" + # Production options are: "ISRG Root X1" and "ISRG Root X2" + #PREFERRED_CHAIN="\(STAGING\) Pretend Pear X1" + + # Uncomment this if you need the full chain file to include the root certificate (Java keystores, Nutanix Prism) + #FULL_CHAIN_INCLUDE_ROOT="true" # Location for all your certs, these can either be on the server (full path name) # or using ssh /sftp as for the ACL @@ -2436,10 +2491,14 @@ write_getssl_template() { # write out the main template file PRIVATE_KEY_ALG="rsa" #REUSE_PRIVATE_KEY="true" - # Preferred Chain - use an different certificate root from the default - # Staging options are: "Fake LE Root X1" and "Fake LE Root X2" - # Production options are: "ISRG Root X1" and "ISRG Root X2" - #PREFERRED_CHAIN="" + # Preferred Chain - use an different certificate root from the default + # This uses wildcard matching so requesting "X1" returns the correct certificate - may need to escape characters + # Staging options are: "(STAGING) Doctored Durian Root CA X3" and "(STAGING) Pretend Pear X1" + # Production options are: "ISRG Root X1" and "ISRG Root X2" + #PREFERRED_CHAIN="\(STAGING\) Pretend Pear X1" + + # Uncomment this if you need the full chain file to include the root certificate (Java keystores, Nutanix Prism) + #FULL_CHAIN_INCLUDE_ROOT="true" # The command needed to reload apache / nginx or whatever you use. # Several (ssh) commands may be given using a bash array: @@ -2463,6 +2522,19 @@ write_getssl_template() { # write out the main template file #VALIDATE_VIA_DNS="true" #DNS_ADD_COMMAND= #DNS_DEL_COMMAND= + + # Unusual configurations (especially split views) may require these. + # If you have a mixture, these can go in the per-domain getssl.cfg. + # + # If you must use an external DNS Server (e.g. due to split views) + # Specify it here. Otherwise, the default is to find the zone master. + # The default will usually work. + # PUBLIC_DNS_SERVER="8.8.8.8" + + # If getssl is unable to determine the authoritative nameserver for a domain + # it will as you to enter AUTH_DNS_SERVER. This is a server that + # can answer queries for the zone - a master or a slave, not a recursive server. + # AUTH_DNS_SERVER="10.0.0.14" _EOF_getssl_ } @@ -2485,6 +2557,8 @@ while [[ -n ${1+defined} ]]; do case $1 in -h | --help) help_message; graceful_exit ;; + -v | --version) + echo "$PROGNAME V$VERSION"; graceful_exit ;; -d | --debug) _USE_DEBUG=1 ;; -c | --create) @@ -2629,6 +2703,12 @@ if [[ -s "$WORKING_DIR/getssl.cfg" ]]; then . "$WORKING_DIR/getssl.cfg" fi +if [[ -n "$DNS_CHECK_FUNC" ]]; then + requires "${DNS_CHECK_FUNC}" +else + requires nslookup drill dig host DNS_CHECK_FUNC +fi + # Define defaults for variables not set in the main config. ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account.key}" DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}" @@ -2719,6 +2799,7 @@ if [[ ${_CREATE_CONFIG} -eq 1 ]]; then info "Adding SANS=$EX_SANS from certificate installed on ${DOMAIN##\*.} to new configuration file" fi write_domain_template "$DOMAIN_DIR/getssl.cfg" + info "created domain config file in $DOMAIN_DIR/getssl.cfg" fi TEMP_DIR="$DOMAIN_DIR/tmp" # end of "-c|--create" option, so exit @@ -2825,7 +2906,7 @@ if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then else # check if the certificate is for the right domain EX_CERT_DOMAIN=$(echo "$EX_CERT" | openssl x509 -text \ - | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ + | sed -n -e 's/^ *Subject: .*CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ | sort -u | grep "^$DOMAIN\$") if [[ "$EX_CERT_DOMAIN" == "$DOMAIN" ]]; then # check renew-date on ex_cert and compare to local ( if local exists) @@ -2858,18 +2939,26 @@ if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then copy_file_to_location "full pem" \ "$TEMP_DIR/${DOMAIN}_chain.pem" \ "$DOMAIN_CHAIN_LOCATION" + umask 077 cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem" + umask "$ORIG_UMASK" copy_file_to_location "private key and domain cert pem" \ "$TEMP_DIR/${DOMAIN}_K_C.pem" \ "$DOMAIN_KEY_CERT_LOCATION" + umask 077 cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" + umask "$ORIG_UMASK" copy_file_to_location "full pem" \ "$TEMP_DIR/${DOMAIN}.pem" \ "$DOMAIN_PEM_LOCATION" reload_service fi else - info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate" + # Get the domain from the existing certificate for the error message + EX_CERT_DOMAIN=$(echo "$EX_CERT" | openssl x509 -text \ + | sed -n -e 's/^ *Subject: .*CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ + | sort -u | head -1) + info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate ($EX_CERT_DOMAIN != $real_d)" fi fi else