diff --git a/README.md b/README.md index 42c7cdd..8549bda 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Obtain SSL certificates from the letsencrypt.org ACME server. Suitable for auto * **Simple and easy to use** * **Detailed debug info** - Whilst it shouldn't be needed, detailed debug information is available. * **Reload services** - After a new certificate is obtained then the relevant services (e.g. apache/nginx/postfix) can be reloaded. +* **ACME v1 and V2** - Supports both ACME versions 1 and 2 ## Installation Since the script is only one file, you can use the following command for a quick installation of GetSSL only: diff --git a/dns_scripts/GoDaddy-README.txt b/dns_scripts/GoDaddy-README.txt new file mode 100644 index 0000000..d58ba73 --- /dev/null +++ b/dns_scripts/GoDaddy-README.txt @@ -0,0 +1,60 @@ +Using GoDaddy DNS for LetsEncrypt domain validation. + +Quick guide to setting up getssl for domain validation of +GoDaddy DNS domains. + +There are two prerequisites to using getssl with GoDaddy DNS: + +1) Obtain an API access key from developer.godaddy.com + At first sign-up, you will be required to take a "test" key. + This is NOT what you need. Accept it, then get a "Production" + key. At this writing, there is no charge - but you must have + a GoDaddy customer account. + + You must get the API key for the account which owns the domain + that you want to get certificates for. If the domains that you + manage are owned by more than one account, get a key for each. + + The access key consists of a "Key" and a "Secret". You need + both. + +2) Obtain JSON.sh - https://github.com/dominictarr/JSON.sh + +With those in hand, the installation procedure is: + +1) Put JSON.sh in the getssl DNS scripts directory + Default: /usr/share/getssl/dns_scripts + +2) Open your config file (the global file in ~/.getssl/getssl.cfg + or the per-account file in ~/.getssl/example.net/getssl.cfg + +3) Set the following options: + VALIDATE_VIA_DNS="true" + DNS_ADD_COMMAND="/usr/share/getssl/dns_scripts/dns_add_godaddy" + DNS_DEL_COMMAND="/usr/share/getssl/dns_scripts/dns_del_godaddy" + # The API key for your account/this domain + export GODADDY_KEY="..." GODADDY_SECRET="..." + + 4) Set any other options that you wish (per the standard + directions.) Use the test CA to make sure that + everything is setup correctly. + +That's it. getssl example.net will now validate with DNS. + +To trace record additions and removals, run getssl as +GODADDY_TRACE=Y getssl example.net + +There are additional options, which are documented in the +*godaddy" files and dns_godaddy -h. + +Copyright (2017) Timothe Litt litt at acm _dot org + +This sofware may be freely used providing this notice is included with +all copies. The name of the author may not be used to endorse +any other product or derivative work. No warranty is provided +and the user assumes all responsibility for use of this software. + +Report any issues to https://github.com/tlhackque/getssl/issues. + +Enjoy. + diff --git a/dns_scripts/dns_add_godaddy b/dns_scripts/dns_add_godaddy new file mode 100755 index 0000000..dfd3b3b --- /dev/null +++ b/dns_scripts/dns_add_godaddy @@ -0,0 +1,40 @@ +#!/bin/bash + +# Copyright (2017) Timothe Litt litt at acm _dot org + +# Add token to GoDaddy dns using dns_godaddy + +# You do not have to customize this script. +# +# Obtain the Key and Secret from https://developer.godaddy.com/getstarted +# You must obtain a "Production" key - NOT the "Test" key you're required +# to get first. +# +# Obtain JSON.sh from https://github.com/dominictarr/JSON.sh +# Place it in (or softlink it to) the same directory as $GODADDY_SCRIPT, +# or specify its location with GODADDY_JSON The default is +# /usr/share/getssl/dns_scripts/ +# +# Define GODADDY_KEY and GO_DADDY_SECRET in your account or domain getssl.cfg +# +# See GoDaddy-README.txt for complete instructions. + +fulldomain="$1" +token="$2" + +[ -z "$GODADDY_SCRIPT" ] && GODADDY_SCRIPT="/usr/share/getssl/dns_scripts/dns_godaddy" +[[ "$GODADDY_SCRIPT" =~ ^~ ]] && \ + eval 'GODADDY_SCRIPT=`readlink -nf ' $GODADDY_SCRIPT '`' + +if [ ! -x "$GODADDY_SCRIPT" ]; then + echo "$GODADDY_SCRIPT: not found. Please install, softlink or set GODADDY_SCRIPT to its full path" + echo "See GoDaddy-README.txt for complete instructions." + exit 3 +fi + +# JSON.sh is not (currently) used by add + +export GODADDY_KEY +export GODADDY_SECRET + +$GODADDY_SCRIPT -q add "${fulldomain}" "_acme-challenge.${fulldomain}." "${token}" diff --git a/dns_scripts/dns_add_joker b/dns_scripts/dns_add_joker new file mode 100755 index 0000000..c100886 --- /dev/null +++ b/dns_scripts/dns_add_joker @@ -0,0 +1,44 @@ +#!/bin/bash + +FULLDOMAIN=$1 +TOKEN=$2 +TMPFILE=$(mktemp /tmp/dns_add_joker.XXXXXXX) + +USERNAME="youruser" +PASSWORD="yourpassword" + +# Verify that required parameters are set +if [[ -z "${FULLDOMAIN}" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi + +if [[ -z "${TOKEN}" ]]; then + echo "DNS script requires challenge token as second parameter" + exit 1 +fi + +DOMAIN_ROOT=$(echo "${FULLDOMAIN}" | awk -F\. '{print $(NF-1) FS $NF}') + +SID=$(curl --silent -X POST https://dmapi.joker.com/request/login \ + -H "Accept: application/json" -H "User-Agent: getssl/0.1" \ + -H "application/x-www-form-urlencoded" -d "username=${USERNAME}&password=${PASSWORD}" \ + -i -k 2>/dev/null | grep Auth-Sid | awk '{ print $2 }') + +## put zone data in tempfile +curl --silent -X POST https://dmapi.joker.com/request/dns-zone-get \ + -H "Accept: application/json" -H "User-Agent: getssl/0.1" \ + -H "application/x-www-form-urlencoded" -d "domain=${DOMAIN_ROOT}&auth-sid=${SID}" | \ + tail -n +7 >"${TMPFILE}" + +## add txt record +printf "_acme-challenge.%s. TXT 0 \"%s \" 300\n\n" "${FULLDOMAIN}" "${TOKEN}" >>"${TMPFILE}" + +## generate encoded url data +URLDATA=$(cat "${TMPFILE}" | sed 's/ /%20/g' | sed 's/"/%22/g' | sed ':a;N;$!ba;s/\n/%0A/g') + +## write new zonefile to joker +curl --silent --output /dev/null "https://dmapi.joker.com/request/dns-zone-put?domain=${DOMAIN_ROOT}&zone=${URLDATA}&auth-sid=${SID}" 2>&1 + +## remove tempfile +rm -f "${TMPFILE}" diff --git a/dns_scripts/dns_add_nsupdate b/dns_scripts/dns_add_nsupdate index 891614e..13b0fc9 100755 --- a/dns_scripts/dns_add_nsupdate +++ b/dns_scripts/dns_add_nsupdate @@ -2,9 +2,38 @@ # example of script to add token to local dns using nsupdate -dnskeyfile="path/to/bla.key" - fulldomain="$1" token="$2" -printf "update add _acme-challenge.%s. 300 in TXT \"%s\"\n\n" "${fulldomain}" "${token}" | nsupdate -k "${dnskeyfile}" -v +# VARIABLES: +# +# DNS_NSUPDATE_KEYFILE - path to a TSIG key file, if required +# DNS_NSUPDATE_GETKEY - command to execute if access to the key file requires +# some special action: mounting a disk, decrypting a file.. +# Called with the operation 'add' and action 'open" / 'close' + + +if [ -n "${DNS_NSUPDATE_KEYFILE}" ]; then + if [ -n "${DNS_NSUPDATE_KEY_HOOK}" ] && ! ${DNS_NSUPDATE_KEY_HOOK} 'add' 'open' "${fulldomain}" ; then + exit $(( $? + 128 )) + fi + + options="-k ${DNS_NSUPDATE_KEYFILE}" +fi + +# Note that blank line is a "send" command to nsupdate + +nsupdate "${options}" -v </dev/null | grep Auth-Sid | awk '{ print $2 }') + +## put zone data in tempfile +curl --silent -X POST https://dmapi.joker.com/request/dns-zone-get \ + -H "Accept: application/json" -H "User-Agent: getssl/0.1" \ + -H "application/x-www-form-urlencoded" -d "domain=${DOMAIN_ROOT}&auth-sid=${SID}" | \ + tail -n +7 >"${TMPFILE}" + +## remove txt record +sed -i "/_acme-challenge.${FULLDOMAIN}.*${TOKEN}.*/d" "${TMPFILE}" + +## generate encoded url data +URLDATA=$(cat "${TMPFILE}" | sed 's/ /%20/g' | sed 's/"/%22/g' | sed ':a;N;$!ba;s/\n/%0A/g') + +## write new zonefile to joker +curl --silent --output /dev/null "https://dmapi.joker.com/request/dns-zone-put?domain=${DOMAIN_ROOT}&zone=${URLDATA}&auth-sid=${SID}" 2>&1 + +## remove tempfile +rm -f "${TMPFILE}" diff --git a/dns_scripts/dns_del_nsupdate b/dns_scripts/dns_del_nsupdate index 808b21c..e8442e3 100755 --- a/dns_scripts/dns_del_nsupdate +++ b/dns_scripts/dns_del_nsupdate @@ -1,9 +1,39 @@ #!/bin/bash -# example of script to add token to local dns using nsupdate +# example of script to remove token from local dns using nsupdate -dnskeyfile="path/to/bla.key" fulldomain="$1" token="$2" -printf "update delete _acme-challenge.%s. 300 in TXT \"%s\"\n\n" "${fulldomain}" "${token}" | nsupdate -k "${dnskeyfile}" -v +# VARIABLES: +# +# DNS_NSUPDATE_KEYFILE - path to a TSIG key file, if required +# DNS_NSUPDATE_GETKEY - command to execute if access to the key file requires +# some special action: dismounting a disk, encrypting a +# file... Called with the operation 'del' and action +# 'open" / 'close' + +if [ -n "${DNS_NSUPDATE_KEYFILE}" ]; then + if [ -n "${DNS_NSUPDATE_KEY_HOOK}" ] && ! "${DNS_NSUPDATE_KEY_HOOK}" 'del' 'open' "${fulldomain}" ; then + exit $(( $? + 128 )) + fi + + options="-k ${DNS_NSUPDATE_KEYFILE}" +fi + +# Note that blank line is a "send" command to nsupdate + +nsupdate "${options}" -v <&2 +$0: requires JSON.sh as "$JSON" + +The full path to JSON.sh can be specified with -j, or the +GODADDY_JSON environment variable. + +You can obtain a copy from $GETJSON + +Then place or softlink it to $JSON or set GODADDY_JSON. +EOF + exit 2 +fi + +if [ -z "$GODADDY_KEY" ] || [ -z "$GODADDY_SECRET" ]; then + echo "GODADDY_KEY and GODADDY secret must be defined" >&2 + exit 3 +fi + +[ -n "$DEBUG" ] && VERB="y" +[ -n "$GODADDY_TRACE" ] && VERB="Y" +[ -n "$GODADDY_TFILE" ] && TRACE="$GODADDY_TFILE" + +# Get parameters & validate + +op="$1" +if ! [[ "$op" =~ ^(add|del)$ ]]; then + echo "Operation must be \"add\" or \"del\"" >&2 + exit 3 +fi +domain="$2" +domain="${domain%'.'}" +if [ -z "$domain" ]; then + echo "'domain' parameter is required, see -h" >&2 + exit 3 +fi +name="$3" +if [ -z "$name" ]; then + echo "'name' parameter is required, see -h" >&2 + exit 3 +fi +! [[ "$name" =~ [.]$ ]] && name="${name}.${domain}." +data="$4" +if [ -z "$data" ]; then + echo "'data' parameter is required, see -h" >&2 + exit 3 +fi + +if [ "$op" = 'del' ]; then + ttl= +elif [ -z "$5" ]; then + ttl="600" # GoDaddy minimum TTL is 600 +elif ! [[ "$5" =~ ^[0-9]+$ ]]; then + echo "TTL $5 is not numeric" >&2 + exit 3 +elif [ $5 -lt 600 ]; then + [ -n "$VERB" ] && \ + echo "$5 is less than GoDaddy minimum of 600; increased to 600" >&2 + ttl="600" +else + ttl="$5" +fi + +# --- Done with parameters + +[ -n "$DEBUG" ] && \ + echo "$PROG: $op $domain $name \"$data\" $ttl" >&2 + +# Authorization header has secret and key + +authhdr="Authorization: sso-key $GODADDY_KEY:$GODADDY_SECRET" + +if [ -n "$TRACE" ]; then + function timestamp { local tm="`LC_TIME=C date '+%T.%N'`" + local class="$1"; shift + echo "${tm:0:15} ** ${class}: $*" >>"$TRACE" + } + timestamp 'Info' "$PROG" "V$VERSION" 'Starting new protocol trace' + timestamp 'Args' "$@" + curl --help | grep -q -- --trace-time && CURL_TFLAGS="--trace-time" # 7.14.0 + function curl { + command curl ${CURL_TFLAGS} --trace-ascii % "$@" 2>>"$TRACE" + } + [ -n "$VERB" ] && echo "Appending protocol trace to $TRACE" +fi + +[ -n "$DEBUG" ] && echo "$authhdr" >&2 + +if [ "$op" = "add" ]; then + # May need to retry due to zone cuts + + while [[ "$domain" =~ [^.]+\.[^.]+ ]]; do + + url="$API/$domain/records/TXT/$name" + + request='{"data":"'$data'","ttl":'$ttl'}' + [ -n "$DEBUG" ] && cat >&2 <&2 <&2 + exit $sts + fi + if ! echo "$result" | grep -q '^HTTP/.* 200 '; then + code="`echo "$result" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`" + msg="`echo "$result" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`" + if [ "$code" = "DUPLICATE_RECORD" ]; then + if [ -n "$VERB" ]; then + echo "$msg in $domain" >&2 + fi + exit 0 # Duplicate record is still success + fi + if [ "$code" = 'UNKNOWN_DOMAIN' ]; then + if [[ "$domain" =~ ^([^.]+)\.([^.]+\.[^.]+.*) ]]; then + [ -n "$DEBUG" ] && \ + echo "$domain unknown, trying ${BASH_REMATCH[2]}" >&2 + domain="${BASH_REMATCH[2]}" + continue; + fi + fi + echo "Request failed $msg" >&2 + exit 1 + fi + [ -n "$VERB" ] && echo "$domain: added $name $ttl TXT \"$data\"" >&2 + exit 0 + done +fi + + +# ----- Delete + +# There is no delete API +# But, it is possible to replace all TXT records. +# +# So, first query for all TXT records + +# May need to retry due to zone cuts + +while [[ "$domain" =~ [^.]+\.[^.]+ ]]; do + + url="$API/$domain/records/TXT" + [ -n "$DEBUG" ] && echo "Query for TXT records to: $url" >&2 + + current="$(curl -i -s -X GET --config - "$url" <&2 + exit $sts + fi + [ -n "$DEBUG" ] && cat >&2 <&2 + domain="${BASH_REMATCH[2]}" + continue; + fi + fi + echo "Request failed $msg" >&2 + exit 1 + fi + # Remove headers + + current="$(echo "$current" | sed -e'0,/^\r*$/d')" + break +done + + # The zone cut is known, so the replace can't fail due to UNKNOWN domain + +if [ "$current" = '[]' ]; then # No TXT records in zone + [ -n "$VERB" ] && echo "$domain: $name TXT \"$data\" does not exist" >&2 + [ -n "$DEBUG" ] && echo "No TXT records in $domain" >&2 + exit 1 # Intent was to change, so error status +fi + +[ -n "$DEBUG" ] && echo "Response is valid" + +# Prepare request to replace TXT RRSET + +# Parse JSON and select only the record structures, which are [index] { ...} + +current="$(echo "$current" | $JSON | sed -n -e'/^\[[0-9][0-9]*\]/{ s/^\[[0-9][0-9]*\]//; p}')" +base="$current" + +[ -n "$DEBUG" ] && cat >&2 <&2 + exit 1 # Intent was to change DNS, so this is an error +fi + +# Remove whitespace and insert needed commmas +# +fmtnew="$new" +new=$(echo "$new" | sed -e"s/}/},/g; \$s/},/}/;" | tr -d '\t\n') + +if [ -z "$new" ]; then + [ -n "$VERB" ] && echo "Replacing last TXT record with a dummy (see -h)" >&2 + new='{"type":"TXT","name":"_dummy.record_","data":"_This record is not used_","ttl":601}' + dummy="t" + TAB=$'\t' + fmtnew="${TAB}$new" + if [ "$fmtnew" = "$base" ]; then + [ -n "$VERB" ] && echo "This tool can't delete a placeholder when it is the only TXT record" >&2 + exit 0 # Not really success, but retrying won't help. + fi +fi + +request="[$new]" + +[ -n "$DEBUG" ] && cat >&2 <&2 <&2 <&2 + exit $sts +fi +if ! echo "$result" | grep -q '^HTTP/.* 200 '; then + result="$(echo "$result" | sed -e'0,/^\r*$/d')" + code="`echo "$result" | grep '"code":' | sed -e's/^.*"code":"//; s/\".*$//'`" + msg="`echo "$result" | grep '"message":' | sed -e's/^.*"message":"//; s/\".*$//'`" + echo "Request failed $msg" >&2 + exit 1 +fi + +if [ -n "$VERB" ]; then + if [ -n "$dummy" ]; then + echo "$domain: replaced $name TXT \"$data\" with a placeholder" >&2 + else + echo "$domain: deleted $name TXT \"$data\"" >&2 + fi +fi +exit 0 + diff --git a/getssl b/getssl index 0469281..a6eaac5 100755 --- a/getssl +++ b/getssl @@ -184,18 +184,20 @@ # 2017-01-30 issue #243 compatibility with bash 3.0 (2.08) # 2017-01-30 issue #243 additional compatibility with bash 3.0 (2.09) # 2017-02-18 add OCSP Must-Staple to the domain csr generation (2.10) +# 2018-01-04 updating to use the updated letsencrypt APIv2 # 2019-09-30 issue #423 Use HTTP 1.1 as workaround atm (2.11) # 2019-10-02 issue #425 Case insensitive processing of agreement url because of HTTP/2 (2.12) # 2019-10-07 update DNS checks to allow use of CNAMEs (2.13) +# 2019-11-18 Rebased master onto APIv2 and added Content-Type: application/jose+json (2.14) # ---------------------------------------------------------------------------------------- PROGNAME=${0##*/} -VERSION="2.13" +VERSION="2.14" # defaults ACCOUNT_KEY_LENGTH=4096 ACCOUNT_KEY_TYPE="rsa" -CA="https://acme-staging.api.letsencrypt.org" +CA="https://acme-staging-v02.api.letsencrypt.org/directory" CA_CERT_LOCATION="" CHALLENGE_CHECK_TYPE="http" CHECK_ALL_AUTH_DNS="false" @@ -203,6 +205,7 @@ CHECK_REMOTE="true" CHECK_REMOTE_WAIT=0 CODE_LOCATION="https://raw.githubusercontent.com/srvrco/getssl/master/getssl" CSR_SUBJECT="/" +CURL_USERAGENT="${PROGNAME}/${VERSION}" DEACTIVATE_AUTH="false" DEFAULT_REVOKE_CA="https://acme-v01.api.letsencrypt.org" DNS_EXTRA_WAIT="" @@ -241,6 +244,7 @@ _UPGRADE_CHECK=1 _USE_DEBUG=0 config_errors="false" LANG=C +API=1 # store copy of original command in case of upgrading script and re-running ORIGCMD="$0 $*" @@ -258,11 +262,11 @@ cert_archive() { # Archive certificate file by copying files to dated archive d cp "$CA_CERT" "${DOMAIN_DIR}/archive/${date_time}/chain.crt" cat "$CERT_FILE" "$CA_CERT" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.crt" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cp "${CERT_FILE::-4}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.crt" + cp "${CERT_FILE%.*}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.crt" cp "$DOMAIN_DIR/${DOMAIN}.ec.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.csr" cp "$DOMAIN_DIR/${DOMAIN}.ec.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.key" - cp "${CA_CERT::-4}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/chain.ec.crt" - cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.ec.crt" + cp "${CA_CERT%.*}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/chain.ec.crt" + cat "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.ec.crt" fi umask "$ORIG_UMASK" debug "purging old GetSSL archives" @@ -278,8 +282,14 @@ check_challenge_completion() { # checks with the ACME server if our challenge is send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" # check response from our request to perform challenge - if [[ ! -z "$code" ]] && [[ ! "$code" == '202' ]] ; then - error_exit "$domain:Challenge error: $code" + if [[ $API -eq 1 ]]; then + if [[ -n "$code" ]] && [[ ! "$code" == '202' ]] ; then + error_exit "$domain:Challenge error: $code" + fi + else # APIv2 + if [[ -n "$code" ]] && [[ ! "$code" == '200' ]] ; then + error_exit "$domain:Challenge error: $code" + fi fi # loop "forever" to keep checking for a response from the ACME server. @@ -370,7 +380,7 @@ check_config() { # check the config files for all obvious errors fi dn=0 - tmplist=$(mktemp) + tmplist=$(mktemp 2>/dev/null || mktemp -t getssl) for d in $alldomains; do # loop over domains (dn is domain number) debug "checking domain $d" if [[ "$(grep "^${d}$" "$tmplist")" = "$d" ]]; then @@ -393,7 +403,7 @@ check_config() { # check the config files for all obvious errors fi # check domain exist if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then - if [[ "$($DNS_CHECK_FUNC "${d}" SOA|grep -c "^${d}")" -ge 1 ]]; then + if [[ "$($DNS_CHECK_FUNC "${d}" |grep -c "${d}")" -ge 1 ]]; then debug "found IP for ${d}" else info "${DOMAIN}: DNS lookup failed for ${d}" @@ -428,8 +438,8 @@ check_config() { # check the config files for all obvious errors } check_getssl_upgrade() { # check if a more recent version of code is available available - TEMP_UPGRADE_FILE="$(mktemp)" - curl --silent "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE" + TEMP_UPGRADE_FILE="$(mktemp 2>/dev/null || mktemp -t getssl)" + curl --user-agent "$CURL_USERAGENT" --silent "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE" errcode=$? if [[ $errcode -eq 60 ]]; then error_exit "curl needs updating, your version does not support SNI (multiple SSL domains on a single IP)" @@ -458,12 +468,13 @@ check_getssl_upgrade() { # check if a more recent version of code is available a # Obtain all locally stored old versions in getssl_versions declare -a getssl_versions shopt -s nullglob - for getssl_version in $0.v*; do + for getssl_version in "$0".v*; do getssl_versions[${#getssl_versions[@]}]="$getssl_version" done shopt -u nullglob # Explicitly sort the getssl_versions array to make sure shopt -s -o noglob + # shellcheck disable=SC2207 IFS=$'\n' getssl_versions=($(sort <<< "${getssl_versions[*]}")) shopt -u -o noglob # Remove entries until given number of old versions to keep is reached @@ -489,7 +500,7 @@ clean_up() { # Perform pre-exit housekeeping if [[ $VALIDATE_VIA_DNS == "true" ]]; then # Tidy up DNS entries if things failed part way though. shopt -s nullglob - for dnsfile in $TEMP_DIR/dns_verify/*; do + for dnsfile in "$TEMP_DIR"/dns_verify/*; do # shellcheck source=/dev/null . "$dnsfile" debug "attempting to clean up DNS entry for $d" @@ -497,10 +508,10 @@ clean_up() { # Perform pre-exit housekeeping done shopt -u nullglob fi - if [[ ! -z "$DOMAIN_DIR" ]]; then + if [[ -n "$DOMAIN_DIR" ]]; then rm -rf "${TEMP_DIR:?}" fi - if [[ ! -z "$TEMP_UPGRADE_FILE" ]] && [[ -f "$TEMP_UPGRADE_FILE" ]]; then + if [[ -n "$TEMP_UPGRADE_FILE" ]] && [[ -f "$TEMP_UPGRADE_FILE" ]]; then rm -f "$TEMP_UPGRADE_FILE" fi } @@ -520,13 +531,14 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. scp $from ${to:4}" fi debug "userid $TOKEN_USER_ID" - if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then + if [[ "$cert" == "challenge token" ]] && [[ -n "$TOKEN_USER_ID" ]]; then servername=$(echo "$to" | awk -F":" '{print $2}') tofile=$(echo "$to" | awk -F":" '{print $3}') debug "servername $servername" debug "file $tofile" # shellcheck disable=SC2029 - ssh "$servername" "chown $TOKEN_USER_ID $tofile" + # shellcheck disable=SC2086 + ssh $SSH_OPTS "$servername" "chown $TOKEN_USER_ID $tofile" fi elif [[ "${to:0:4}" == "ftp:" ]] ; then if [[ "$cert" != "challenge token" ]] ; then @@ -580,7 +592,7 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. error_exit "cannot copy $from to $to" fi fi - if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then + if [[ "$cert" == "challenge token" ]] && [[ -n "$TOKEN_USER_ID" ]]; then chown "$TOKEN_USER_ID" "$to" fi fi @@ -622,7 +634,7 @@ create_csr() { # create a csr using a given key (if it doesn't already exist) if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "1" ]]; then info "creating domain csr - $csr_file" # create a temporary config file, for portability. - tmp_conf=$(mktemp) + tmp_conf=$(mktemp 2>/dev/null || mktemp -t getssl) cat "$SSLCONF" > "$tmp_conf" printf "[SAN]\n%s" "$SANLIST" >> "$tmp_conf" # add OCSP Must-Staple to the domain csr @@ -656,8 +668,8 @@ create_key() { # create a domain key (if it doesn't already exist) esac umask "$ORIG_UMASK" # remove csr on generation of new domain key - if [[ -e "${key_loc::-4}.csr" ]]; then - rm -f "${key_loc::-4}.csr" + if [[ -e "${key_loc%.*}.csr" ]]; then + rm -f "${key_loc%.*}.csr" fi fi } @@ -725,7 +737,7 @@ get_auth_dns() { # get the authoritative dns server for a domain (sets primary_n else res=$($DNS_CHECK_FUNC CNAME "$gad_d" "@$gad_s"| grep "^$gad_d") fi - if [[ ! -z "$res" ]]; then # domain is a CNAME so get main domain + if [[ -n "$res" ]]; then # domain is a CNAME so get main domain gad_d=$(echo "$res"| awk '{print $5}' |sed 's/\.$//g') fi if [[ -z "$gad_s" ]]; then #checking for CNAMEs @@ -807,43 +819,54 @@ get_certificate() { # get certificate for csr, if all domains validated. der=$(openssl req -in "$gc_csr" -outform DER | urlbase64) debug "der $der" - send_signed_request "$URL_new_cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64" - - # convert certificate information into correct format and save to file. - CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r') - debug "certdata location = $CertData" - if [[ "$CertData" ]] ; then - echo -----BEGIN CERTIFICATE----- > "$gc_certfile" - curl --silent "$CertData" | openssl base64 -e >> "$gc_certfile" - echo -----END CERTIFICATE----- >> "$gc_certfile" - info "Certificate saved in $CERT_FILE" - fi + 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. + CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r') + debug "certdata location = $CertData" + if [[ "$CertData" ]] ; then + echo -----BEGIN CERTIFICATE----- > "$gc_certfile" + curl --user-agent "$CURL_USERAGENT" --silent "$CertData" | openssl base64 -e >> "$gc_certfile" + echo -----END CERTIFICATE----- >> "$gc_certfile" + info "Certificate saved in $CERT_FILE" + fi - # If certificate wasn't a valid certificate, error exit. - if [[ -z "$CertData" ]] ; then - response2=$(echo "$response" | fold -w64 |openssl base64 -d) - debug "response was $response" - error_exit "Sign failed: $(echo "$response2" | grep "detail")" - fi + # If certificate wasn't a valid certificate, error exit. + if [[ -z "$CertData" ]] ; then + response2=$(echo "$response" | fold -w64 |openssl base64 -d) + debug "response was $response" + error_exit "Sign failed: $(echo "$response2" | grep "detail")" + fi - # get a copy of the CA certificate. - IssuerData=$(grep -i '^Link' "$CURL_HEADER" \ - | cut -d " " -f 2\ - | cut -d ';' -f 1 \ - | sed 's///g') - if [[ "$IssuerData" ]] ; then - echo -----BEGIN CERTIFICATE----- > "$gc_cafile" - curl --silent "$IssuerData" | openssl base64 -e >> "$gc_cafile" - echo -----END CERTIFICATE----- >> "$gc_cafile" - info "The intermediate CA cert is in $gc_cafile" + # get a copy of the CA certificate. + IssuerData=$(grep -i '^Link' "$CURL_HEADER" \ + | cut -d " " -f 2\ + | cut -d ';' -f 1 \ + | sed 's///g') + if [[ "$IssuerData" ]] ; then + echo -----BEGIN CERTIFICATE----- > "$gc_cafile" + curl --user-agent "$CURL_USERAGENT" --silent "$IssuerData" | openssl base64 -e >> "$gc_cafile" + echo -----END CERTIFICATE----- >> "$gc_cafile" + info "The intermediate CA cert is in $gc_cafile" + fi + else # APIv2 + send_signed_request "$FinalizeLink" "{\"csr\": \"$der\"}" "needbase64" + debug "order link was $OrderLink" + cd=$(curl --user-agent "$CURL_USERAGENT" --silent "$OrderLink") + CertData=$(json_get "$cd" "certificate") + debug "CertData is at $CertData" + curl --user-agent "$CURL_USERAGENT" --silent "$CertData" > "$FULL_CHAIN" + info "Full certificate saved in $FULL_CHAIN" + awk -v CERT_FILE="$CERT_FILE" -v CA_CERT="$CA_CERT" 'BEGIN {outfile=CERT_FILE} split_after==1 {outfile=CA_CERT;split_after=0} /-----END CERTIFICATE-----/ {split_after=1} {print > outfile}' "$FULL_CHAIN" + info "Certificate saved in $CERT_FILE" fi } get_cr() { # get curl response url="$1" debug url "$url" - response=$(curl --silent "$url") + response=$(curl --user-agent "$CURL_USERAGENT" --silent "$url") ret=$? debug response "$response" code=$(json_get "$response" status) @@ -966,28 +989,207 @@ info() { # write out info as long as the quiet flag has not been set. fi } -json_get() { # get the value corresponding to $2 in the JSON passed as $1. - # remove newlines, so it's a single chunk of JSON - json_data=$( echo "$1" | tr '\n' ' ') - # if $3 is defined, this is the section which the item is in. - if [[ ! -z "$3" ]]; then - jg_section=$(echo "$json_data" | awk -F"[}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${3}"'\"/){print $i}}}') - if [[ "$2" == "uri" ]]; then - jg_subsect=$(echo "$jg_section" | awk -F"[,]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i)}}}') - jg_result=$(echo "$jg_subsect" | awk -F'"' '{print $4}') +json_awk() { # AWK json converter used for API2 - needs tidying up ;) +# shellcheck disable=SC2086 +echo $1 | awk ' +{ + tokenize($0) # while(get_token()) {print TOKEN} + if (0 == parse()) { + apply(JPATHS, NJPATHS) + } +} + +function apply (ary,size,i) { + for (i=1; i NTOKENS) to = NTOKENS + for (i = from; i < ITOKENS; i++) + context = context sprintf("%s ", TOKENS[i]) + context = context "<<" got ">> " + for (i = ITOKENS + 1; i <= to; i++) + context = context sprintf("%s ", TOKENS[i]) + scream("json_awk expected <" expected "> but got <" got "> at input token " ITOKENS "\n" context) +} + +function reset() { + TOKEN=""; delete TOKENS; NTOKENS=ITOKENS=0 + delete JPATHS; NJPATHS=0 + VALUE="" +} + +function scream(msg) { + FAILS[FILENAME] = FAILS[FILENAME] (FAILS[FILENAME]!="" ? "\n" : "") msg + msg = FILENAME ": " msg + print msg >"/dev/stderr" +} + +function tokenize(a1,pq,pb,ESCAPE,CHAR,STRING,NUMBER,KEYWORD,SPACE) { + SPACE="[[:space:]]+" + gsub(/\"[^[:cntrl:]\"\\]*((\\[^u[:cntrl:]]|\\u[0-9a-fA-F]{4})[^[:cntrl:]\"\\]*)*\"|-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?|null|false|true|[[:space:]]+|./, "\n&", a1) + gsub("\n" SPACE, "\n", a1) + sub(/^\n/, "", a1) + ITOKENS=0 # get_token() helper + return NTOKENS = split(a1, TOKENS, /\n/) +}' +} + +json_get() { # get values from json + if [[ -z "$1" ]] || [[ "$1" == "null" ]]; then + echo "json was blank" + return + fi + if [[ $API = 1 ]]; then + # remove newlines, so it's a single chunk of JSON + json_data=$( echo "$1" | tr '\n' ' ') + # if $3 is defined, this is the section which the item is in. + if [[ -n "$3" ]]; then + jg_section=$(echo "$json_data" | awk -F"[}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${3}"'\"/){print $i}}}') + if [[ "$2" == "uri" ]]; then + jg_subsect=$(echo "$jg_section" | awk -F"[,]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i)}}}') + jg_result=$(echo "$jg_subsect" | awk -F'"' '{print $4}') + else + jg_result=$(echo "$jg_section" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + fi else - jg_result=$(echo "$jg_section" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + jg_result=$(echo "$json_data" |awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + fi + # check number of quotes + jg_q=${jg_result//[^\"]/} + # if 2 quotes, assume it's a quoted variable and just return the data within the quotes. + if [[ ${#jg_q} -eq 2 ]]; then + echo "$jg_result" | awk -F'"' '{print $2}' + else + echo "$jg_result" fi else - jg_result=$(echo "$json_data" |awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') - fi - # check number of quotes - jg_q=${jg_result//[^\"]/} - # if 2 quotes, assume it's a quoted variable and just return the data within the quotes. - if [[ ${#jg_q} -eq 2 ]]; then - echo "$jg_result" | awk -F'"' '{print $2}' - else - echo "$jg_result" + if [[ -n "$6" ]]; then + full=$(json_awk "$1") + section=$(echo "$full" | grep "\"$2\"" | grep "\"$3\"" | grep "\"$4\"" | awk -F"," '{print $2}') + echo "$full" | grep "^..${5}\",$section" | awk '{print $2}' | tr -d '"' + elif [[ -n "$5" ]]; then + full=$(json_awk "$1") + section=$(echo "$full" | grep "\"$2\"" | grep "\"$3\"" | grep "\"$4\"" | awk -F"," '{print $2}') + echo "$full" | grep "^..${2}\",$section" | grep "$5" | awk '{print $2}' | tr -d '"' + elif [[ -n "$3" ]]; then + json_awk "$1" | grep "^..${2}...${3}" | awk '{print $2}' | tr -d '"' + elif [[ -n "$2" ]]; then + json_awk "$1" | grep "^..${2}" | awk '{print $2}' | tr -d '"' + else + json_awk "$1" + fi fi } @@ -1004,7 +1206,7 @@ os_esed() { # Use different sed version for different os types (extended regex) purge_archive() { # purge archive of old, invalid, certificates arcdir="$1/archive" debug "purging archives in ${arcdir}/" - for padir in $arcdir/????_??_??_??_??; do + for padir in "$arcdir"/????_??_??_??_??; do # check each directory if [[ -d "$padir" ]]; then tstamp=$(basename "$padir"| awk -F"_" '{print $1"-"$2"-"$3" "$4":"$5}') @@ -1027,15 +1229,16 @@ purge_archive() { # purge archive of old, invalid, certificates } reload_service() { # Runs a command to reload services ( via ssh if needed) - if [[ ! -z "$RELOAD_CMD" ]]; then + if [[ -n "$RELOAD_CMD" ]]; then info "reloading SSL services" if [[ "${RELOAD_CMD:0:4}" == "ssh:" ]] ; then sshhost=$(echo "$RELOAD_CMD"| awk -F: '{print $2}') command=${RELOAD_CMD:(( ${#sshhost} + 5))} debug "running following command to reload cert" - debug "ssh $sshhost ${command}" + debug "ssh $SSH_OPTS $sshhost ${command}" # shellcheck disable=SC2029 - ssh "$sshhost" "${command}" 1>/dev/null 2>&1 + # shellcheck disable=SC2086 + ssh $SSH_OPTS "$sshhost" "${command}" 1>/dev/null 2>&1 # allow 2 seconds for services to restart sleep 2 else @@ -1053,7 +1256,7 @@ revoke_certificate() { # revoke a certificate ACCOUNT_KEY="$REVOKE_KEY" # need to set the revoke key as "account_key" since it's used in send_signed_request. get_signing_params "$REVOKE_KEY" - TEMP_DIR=$(mktemp -d) + TEMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t getssl) debug "revoking from $CA" rcertdata=$(openssl x509 -in "$REVOKE_CERT" -inform PEM -outform DER | urlbase64) send_signed_request "$URL_revoke" "{\"resource\": \"revoke-cert\", \"certificate\": \"$rcertdata\"}" @@ -1070,16 +1273,16 @@ requires() { # check if required function is available if [[ "$i" == "${!#}" ]]; then # if on last variable then exit as not found error_exit "this script requires one of: ${*:1:$(($#-1))}" fi - res=$(which "$i" 2>/dev/null) + res=$(command -v "$i" 2>/dev/null) debug "checking for $i ... $res" - if [[ ! -z "$res" ]]; then # if function found, then set variable to function and return + 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" return fi done else # only one value, so check it. - result=$(which "$1" 2>/dev/null) + result=$(command -v "$1" 2>/dev/null) debug "checking for required $1 ... $result" if [[ -z "$result" ]]; then error_exit "This script requires $1 installed" @@ -1134,17 +1337,17 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p needbase64=$3 debug url "$url" - debug payload "$payload" CURL_HEADER="$TEMP_DIR/curl.header" dp="$TEMP_DIR/curl.dump" CURL="curl " + # shellcheck disable=SC2072 if [[ "$($CURL -V | head -1 | cut -d' ' -f2 )" > "7.33" ]]; then CURL="$CURL --http1.1 " fi - CURL="$CURL --silent --dump-header $CURL_HEADER " + CURL="$CURL --user-agent $CURL_USERAGENT --silent --dump-header $CURL_HEADER " if [[ ${_USE_DEBUG} -eq 1 ]]; then CURL="$CURL --trace-ascii $dp " @@ -1152,57 +1355,105 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p # convert payload to url base 64 payload64="$(printf '%s' "${payload}" | urlbase64)" - debug payload64 "$payload64" # get nonce from ACME server - nonceurl="$CA/directory" - nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ') - - debug nonce "$nonce" - - # Build header with just our public key and algorithm information - header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}' - - # Build another header which also contains the previously received nonce and encode it as urlbase64 - protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' - protected64="$(printf '%s' "${protected}" | urlbase64)" - debug protected "$protected" - - # Sign header with nonce and our payload with our private key and encode signature as urlbase64 - sign_string "$(printf '%s' "${protected64}.${payload64}")" "${ACCOUNT_KEY}" "$signalg" - - # Send header + extended header + payload + signature to the acme-server - body="{\"header\": ${header}," - body="${body}\"protected\": \"${protected64}\"," - body="${body}\"payload\": \"${payload64}\"," - body="${body}\"signature\": \"${signed64}\"}" - debug "header, payload and signature = $body" - - code="500" - loop_limit=5 - while [[ "$code" -eq 500 ]]; do - if [[ "$needbase64" ]] ; then - response=$($CURL -X POST --data "$body" "$url" | urlbase64) + if [[ $API -eq 1 ]]; then + nonceurl="$CA/directory" + nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ') + else # APIv2 + nonce=$($CURL -I "$URL_newNonce" | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ') + fi + + nonceproblem="true" + while [[ "$nonceproblem" == "true" ]]; do + + debug nonce "$nonce" + + # Build header with just our public key and algorithm information + header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}' + + # Build another header which also contains the previously received nonce and encode it as urlbase64 + if [[ $API -eq 1 ]]; then + protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' + protected64="$(printf '%s' "${protected}" | urlbase64)" + else # APIv2 + if [[ -z "$KID" ]]; then + debug "KID is blank, so using jwk" + protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' + protected64="$(printf '%s' "${protected}" | urlbase64)" + else + debug "using KID=${KID}" + protected="{\"alg\": \"$jwkalg\", \"kid\": \"$KID\",\"nonce\": \"${nonce}\", \"url\": \"${url}\"}" + debug "protected = $protected" + protected64="$(printf '%s' "${protected}" | urlbase64)" + fi + fi + + # Sign header with nonce and our payload with our private key and encode signature as urlbase64 + sign_string "$(printf '%s' "${protected64}.${payload64}")" "${ACCOUNT_KEY}" "$signalg" + + # Send header + extended header + payload + signature to the acme-server + if [[ $API -eq 1 ]]; then + debug "header = $header" + debug "protected = $protected" + debug "payload = $payload" + body="{\"header\": ${header}," + body="${body}\"protected\": \"${protected64}\"," + body="${body}\"payload\": \"${payload64}\"," + body="${body}\"signature\": \"${signed64}\"}" + debug "header, payload and signature = $body" else - response=$($CURL -X POST --data "$body" "$url") + debug "protected = $protected" + debug "payload = $payload" + body="{" + body="${body}\"protected\": \"${protected64}\"," + body="${body}\"payload\": \"${payload64}\"," + body="${body}\"signature\": \"${signed64}\"}" + debug "header, payload and signature = $body" fi - responseHeaders=$(cat "$CURL_HEADER") - debug responseHeaders "$responseHeaders" - debug response "$response" - code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1) - debug code "$code" - response_status=$(json_get "$response" status \ - | head -1| awk -F'"' '{print $2}') - debug "response status = $response_status" - - if [[ "$code" -eq 500 ]]; then - info "error on acme server - trying again ...." - sleep 2 - loop_limit=$((loop_limit - 1)) - if [[ $loop_limit -lt 1 ]]; then - error_exit "500 error from ACME server: $response" + code="500" + loop_limit=5 + while [[ "$code" -eq 500 ]]; do + if [[ "$needbase64" ]] ; then + response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" | urlbase64) + else + response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url") + fi + + responseHeaders=$(cat "$CURL_HEADER") + debug responseHeaders "$responseHeaders" + debug response "$response" + code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1) + debug code "$code" + if [[ $API -eq 1 ]]; then + response_status=$(json_get "$response" status \ + | head -1| awk -F'"' '{print $2}') + else # APIv2 + if [[ ${response##*()} == "{"* ]]; then + response_status=$(json_get "$response" status) + else + debug "response not in json format" + debug "$response" + fi fi + debug "response status = $response_status" + if [[ "$code" -eq 500 ]]; then + info "error on acme server - trying again ...." + debug "loop_limit = $loop_limit" + sleep 5 + loop_limit=$((loop_limit - 1)) + if [[ $loop_limit -lt 1 ]]; then + error_exit "500 error from ACME server: $response" + fi + fi + done + if [[ $response == *"error:badNonce"* ]]; then + debug "bad nonce" + nonce=$(echo "$responseHeaders" | grep -i "^replay-nonce:" | awk '{print $2}' | tr -d '\r\n ') + debug "trying new nonce $nonce" + else + nonceproblem="false" fi done } @@ -1300,45 +1551,46 @@ write_domain_template() { # write out a template file for a domain. # This server issues full certificates, however has rate limits #CA="https://acme-v01.api.letsencrypt.org" - #PRIVATE_KEY_ALG="rsa" - - # Additional domains - this could be multiple domains / subdomains in a comma separated list - # Note: this is Additional domains - so should not include the primary domain. - SANS="${EX_SANS}" - - # Acme Challenge Location. The first line for the domain, the following ones for each additional domain. - # If these start with ssh: then the next variable is assumed to be the hostname and the rest the location. - # An ssh key will be needed to provide you with access to the remote server. - # Optionally, you can specify a different userid for ssh/scp to use on the remote server before the @ sign. - # If left blank, the username on the local server will be used to authenticate against the remote server. - # If these start with ftp: then the next variables are ftpuserid:ftppassword:servername:ACL_location - # These should be of the form "/path/to/your/website/folder/.well-known/acme-challenge" - # where "/path/to/your/website/folder/" is the path, on your web server, to the web root for your domain. - #ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ssh:sshuserid@server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge') - - #Set USE_SINGLE_ACL="true" to use a single ACL for all checks - #USE_SINGLE_ACL="false" - - # Location for all your certs, these can either be on the server (full path name) - # or using ssh /sftp as for the ACL - #DOMAIN_CERT_LOCATION="/etc/ssl/${DOMAIN}.crt" - #DOMAIN_KEY_LOCATION="/etc/ssl/${DOMAIN}.key" - #CA_CERT_LOCATION="/etc/ssl/chain.crt" - #DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert - #DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert - - # The command needed to reload apache / nginx or whatever you use - #RELOAD_CMD="" - - # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, - # smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which - # will be checked for certificate expiry and also will be checked after - # an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true - #SERVER_TYPE="https" - #CHECK_REMOTE="true" + #PRIVATE_KEY_ALG="rsa" + + # Additional domains - this could be multiple domains / subdomains in a comma separated list + # Note: this is Additional domains - so should not include the primary domain. + SANS="${EX_SANS}" + + # Acme Challenge Location. The first line for the domain, the following ones for each additional domain. + # If these start with ssh: then the next variable is assumed to be the hostname and the rest the location. + # An ssh key will be needed to provide you with access to the remote server. + # Optionally, you can specify a different userid for ssh/scp to use on the remote server before the @ sign. + # If left blank, the username on the local server will be used to authenticate against the remote server. + # If these start with ftp: then the next variables are ftpuserid:ftppassword:servername:ACL_location + # These should be of the form "/path/to/your/website/folder/.well-known/acme-challenge" + # where "/path/to/your/website/folder/" is the path, on your web server, to the web root for your domain. + #ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ssh:sshuserid@server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge') + + #Set USE_SINGLE_ACL="true" to use a single ACL for all checks + #USE_SINGLE_ACL="false" + + # Location for all your certs, these can either be on the server (full path name) + # or using ssh /sftp as for the ACL + #DOMAIN_CERT_LOCATION="/etc/ssl/${DOMAIN}.crt" # this is domain cert + #DOMAIN_KEY_LOCATION="/etc/ssl/${DOMAIN}.key" # this is domain key + #CA_CERT_LOCATION="/etc/ssl/chain.crt" # this is CA cert + #DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert + #DOMAIN_PEM_LOCATION="" # this is the domain key, domain cert and CA cert + + # The command needed to reload apache / nginx or whatever you use + #RELOAD_CMD="" + + # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, + # smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which + # will be checked for certificate expiry and also will be checked after + # an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true + #SERVER_TYPE="https" + #CHECK_REMOTE="true" + #CHECK_REMOTE_WAIT="2" # wait 2 seconds before checking the remote server _EOF_domain_ fi } @@ -1349,7 +1601,7 @@ write_getssl_template() { # write out the main template file # see https://github.com/srvrco/getssl/wiki/Config-variables for details # # The staging server is best for testing (hence set as default) - CA="https://acme-staging.api.letsencrypt.org" + CA="https://acme-staging-v02.api.letsencrypt.org/directory" # This server issues full certificates, however has rate limits #CA="https://acme-v01.api.letsencrypt.org" @@ -1430,11 +1682,11 @@ while [[ -n ${1+defined} ]]; do _UPGRADE_CHECK=0 ;; -w) shift; WORKING_DIR="$1" ;; - -* | --*) + -*) usage error_exit "Unknown option $1" ;; *) - if [[ ! -z $DOMAIN ]]; then + if [[ -n $DOMAIN ]]; then error_exit "invalid command line $DOMAIN - it appears to contain more than one domain" fi DOMAIN="$1" @@ -1485,13 +1737,13 @@ if [[ $_REVOKE -eq 1 ]]; then else CA=$REVOKE_CA fi - URL_revoke=$(curl "${CA}/directory" 2>/dev/null | grep "revoke-cert" | awk -F'"' '{print $4}') + URL_revoke=$(curl --user-agent "$CURL_USERAGENT" "${CA}/directory" 2>/dev/null | grep "revoke-cert" | awk -F'"' '{print $4}') revoke_certificate graceful_exit fi # get latest agreement from CA (as default) -AGREEMENT=$(curl -I "${CA}/terms" 2>/dev/null | awk 'tolower($1) ~ "location:" {print $2}'|tr -d '\r') +AGREEMENT=$(curl --user-agent "$CURL_USERAGENT" -I "${CA}/terms" 2>/dev/null | awk 'tolower($1) ~ "location:" {print $2}'|tr -d '\r') # if nothing in command line, print help and exit. if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]]; then @@ -1517,6 +1769,7 @@ ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account.key}" DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}" DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN" CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt" +FULL_CHAIN="$DOMAIN_DIR/fullchain.crt" CA_CERT="$DOMAIN_DIR/chain.crt" TEMP_DIR="$DOMAIN_DIR/tmp" if [[ "$os" == "mingw" ]]; then @@ -1542,7 +1795,7 @@ if [[ ${_CHECK_ALL} -eq 1 ]]; then error_exit "DOMAIN_STORAGE not found - $DOMAIN_STORAGE" fi - for dir in ${DOMAIN_STORAGE}/*; do + for dir in "${DOMAIN_STORAGE}"/*; do if [[ -d "$dir" ]]; then debug "Checking $dir" cmd="$0 -U" # No update checks when calling recursively @@ -1590,7 +1843,7 @@ if [[ ${_CREATE_CONFIG} -eq 1 ]]; then | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:443" 2>/dev/null \ | openssl x509 2>/dev/null) EX_SANS="www.${DOMAIN}" - if [[ ! -z "${EX_CERT}" ]]; then + if [[ -n "${EX_CERT}" ]]; then EX_SANS=$(echo "$EX_CERT" \ | openssl x509 -noout -text 2>/dev/null| grep "Subject Alternative Name" -A2 \ | grep -Eo "DNS:[a-zA-Z 0-9.-]*" | sed "s@DNS:$DOMAIN@@g" | grep -v '^$' | cut -c 5-) @@ -1637,10 +1890,37 @@ if [[ -e "$DOMAIN_DIR/FORCE_RENEWAL" ]]; then fi # Obtain CA resource locations -ca_all_loc=$(curl "${CA}/directory" 2>/dev/null) +ca_all_loc=$(curl --user-agent "$CURL_USERAGENT" "${CA}" 2>/dev/null) +debug "ca_all_loc from ${CA} gives $ca_all_loc" +# APIv1 URL_new_reg=$(echo "$ca_all_loc" | grep "new-reg" | awk -F'"' '{print $4}') URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}') URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}') +#API v2 +URL_newAccount=$(echo "$ca_all_loc" | grep "newAccount" | awk -F'"' '{print $4}') +URL_newNonce=$(echo "$ca_all_loc" | grep "newNonce" | awk -F'"' '{print $4}') +URL_newOrder=$(echo "$ca_all_loc" | grep "newOrder" | awk -F'"' '{print $4}') +if [[ -z "$URL_new_reg" ]] && [[ -z "$URL_newAccount" ]]; then + ca_all_loc=$(curl --user-agent "$CURL_USERAGENT" "${CA}/directory" 2>/dev/null) + debug "ca_all_loc from ${CA}/directory gives $ca_all_loc" + # APIv1 + URL_new_reg=$(echo "$ca_all_loc" | grep "new-reg" | awk -F'"' '{print $4}') + URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}') + URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}') + #API v2 + URL_newAccount=$(echo "$ca_all_loc" | grep "newAccount" | awk -F'"' '{print $4}') + URL_newNonce=$(echo "$ca_all_loc" | grep "newNonce" | awk -F'"' '{print $4}') + URL_newOrder=$(echo "$ca_all_loc" | grep "newOrder" | awk -F'"' '{print $4}') +fi + +if [[ -n "$URL_new_reg" ]]; then + API=1 +elif [[ -n "$URL_newAccount" ]]; then + API=2 +else + info "unknown API version" + graceful_exit +fi # if check_remote is true then connect and obtain the current certificate (if not forcing renewal) if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then @@ -1649,7 +1929,7 @@ if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then EX_CERT=$(echo \ | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \ | openssl x509 2>/dev/null) - if [[ ! -z "$EX_CERT" ]]; then # if obtained a cert + if [[ -n "$EX_CERT" ]]; then # if obtained a cert if [[ -s "$CERT_FILE" ]]; then # if local exists CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null) else # since local doesn't exist leave empty so that the domain validation will happen @@ -1792,23 +2072,42 @@ fi # currently the code registers every time, and gets an "already registered" back if it has been. get_signing_params "$ACCOUNT_KEY" -if [[ "$ACCOUNT_EMAIL" ]] ; then - regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' -else - regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' -fi - info "Registering account" # send the request to the ACME server. -send_signed_request "$URL_new_reg" "$regjson" +if [[ $API -eq 1 ]]; then + if [[ "$ACCOUNT_EMAIL" ]] ; then + regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' + else + regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' + fi + send_signed_request "$URL_new_reg" "$regjson" +elif [[ $API -eq 2 ]]; then + if [[ "$ACCOUNT_EMAIL" ]] ; then + regjson='{"termsOfServiceAgreed": true, "contact": ["mailto: '$ACCOUNT_EMAIL'"]}' + else + regjson='{"termsOfServiceAgreed": true}' + fi + send_signed_request "$URL_newAccount" "$regjson" +else + debug "cant determine account API" + graceful_exit +fi if [[ "$code" == "" ]] || [[ "$code" == '201' ]] ; then info "Registered" + KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') + debug "KID=_$KID}_" echo "$response" > "$TEMP_DIR/account.json" elif [[ "$code" == '409' ]] ; then - debug "Already registered" + KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') + debug responseHeaders "$responseHeaders" + debug "Already registered KID=$KID" +elif [[ "$code" == '200' ]] ; then + KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') + debug responseHeaders "$responseHeaders" + debug "Already registered account, KID=${KID}" else - error_exit "Error registering account ... $(json_get "$response" detail)" + error_exit "Error registering account ...$responseHeaders ... $(json_get "$response" detail)" fi # end of registering account with CA @@ -1817,10 +2116,35 @@ info "Verify each domain" # loop through domains for cert ( from SANS list) if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=${SANS//,/ } + alldomains=${SANS//,/ } else alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") fi + +if [[ $API -eq 2 ]]; then + dstring="[" + for d in $alldomains; do + dstring="${dstring}{\"type\":\"dns\",\"value\":\"$d\"}," + done + dstring="${dstring: : -1}]" + # request NewOrder currently seems to ignore the dates .... + # dstring="${dstring},\"notBefore\": \"$(date -d "-1 hour" --utc +%FT%TZ)\"" + # dstring="${dstring},\"notAfter\": \"$(date -d "2 days" --utc +%FT%TZ)\"" + request="{\"identifiers\": $dstring}" + send_signed_request "$URL_newOrder" "$request" + OrderLink=$(echo "$responseHeaders" | grep -i location | awk '{print $2}'| tr -d '\r\n ') + debug "Order link $OrderLink" + FinalizeLink=$(json_get "$response" "finalize") + debug "finalise link $FinalizeLink" + dn=0 + for d in $alldomains; do + # get authorizations link + AuthLink[$dn]=$(json_get "$response" "identifiers" "value" "$d" "authorizations" "x") + debug "authorizations link for $d - ${AuthLink[$dn]}" + ((dn++)) + done +fi + dn=0 for d in $alldomains; do # $d is domain in current loop, which is number $dn for ACL @@ -1832,13 +2156,17 @@ for d in $alldomains; do fi # request a challenge token from ACME server - request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"$d\"}}" - send_signed_request "$URL_new_authz" "$request" - - debug "completed send_signed_request" - # check if we got a valid response and token, if not then error exit - if [[ ! -z "$code" ]] && [[ ! "$code" == '201' ]] ; then - error_exit "new-authz error: $response" + if [[ $API -eq 1 ]]; then + request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"$d\"}}" + send_signed_request "$URL_new_authz" "$request" + debug "completed send_signed_request" + + # check if we got a valid response and token, if not then error exit + if [[ -n "$code" ]] && [[ ! "$code" == '201' ]] ; then + error_exit "new-authz error: $response" + fi + else + response_status="" fi if [[ $response_status == "valid" ]]; then @@ -1854,13 +2182,24 @@ for d in $alldomains; do else PREVIOUSLY_VALIDATED="false" if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification - # get the dns component of the ACME response - # get the token from the dns component - token=$(json_get "$response" "token" "dns-01") - debug token "$token" - # get the uri from the dns component - uri=$(json_get "$response" "uri" "dns-01") - debug uri "$uri" + if [[ $API -eq 1 ]]; then + # get the dns component of the ACME response + # get the token from the dns component + token=$(json_get "$response" "token" "dns-01") + debug token "$token" + # get the uri from the dns component + uri=$(json_get "$response" "uri" "dns-01") + debug uri "$uri" + else # APIv2 + response=$(curl --user-agent "$CURL_USERAGENT" --silent "${AuthLink[$dn]}" 2>/dev/null) + debug "authlink response = $response" + # get the token from the http-01 component + token=$(json_get "$response" "challenges" "type" "dns-01" "token") + debug token "$token" + # get the uri from the http component + uri=$(json_get "$response" "challenges" "type" "dns-01" "url") + debug uri "$uri" + fi keyauthorization="$token.$thumbprint" debug keyauthorization "$keyauthorization" @@ -1901,12 +2240,23 @@ for d in $alldomains; do _EOF_ else # set up the correct http token for verification - # get the token from the http component - token=$(json_get "$response" "token" "http-01") - debug token "$token" - # get the uri from the http component - uri=$(json_get "$response" "uri" "http-01") - debug uri "$uri" + if [[ $API -eq 1 ]]; then + # get the token from the http component + token=$(json_get "$response" "token" "http-01") + debug token "$token" + # get the uri from the http component + uri=$(json_get "$response" "uri" "http-01") + debug uri "$uri" + else # APIv2 + response=$(curl --user-agent "$CURL_USERAGENT" --silent "${AuthLink[$dn]}" 2>/dev/null) + debug "authlink response = $response" + # get the token from the http-01 component + token=$(json_get "$response" "challenges" "type" "http-01" "token") + debug token "$token" + # get the uri from the http component + uri=$(json_get "$response" "challenges" "type" "http-01" "url") + debug uri "$uri" + fi #create signed authorization key from token. keyauthorization="$token.$thumbprint" @@ -1935,7 +2285,7 @@ for d in $alldomains; do else sleep "$HTTP_TOKEN_CHECK_WAIT" # check that we can reach the challenge ourselves, if not, then error - if [[ ! "$(curl -k --silent --location "$wellknown_url")" == "$keyauthorization" ]]; then + if [[ ! "$(curl --user-agent "$CURL_USERAGENT" -k --silent --location "$wellknown_url")" == "$keyauthorization" ]]; then error_exit "for some reason could not reach $wellknown_url - please check it manually" fi fi @@ -1949,9 +2299,10 @@ for d in $alldomains; do sshhost=$(echo "${t_loc}"| awk -F: '{print $2}') command="rm -f ${t_loc:(( ${#sshhost} + 5))}/${token:?}" debug "running following command to remove token" - debug "ssh $sshhost ${command}" + debug "ssh $SSH_OPTS $sshhost ${command}" # shellcheck disable=SC2029 - ssh "$sshhost" "${command}" 1>/dev/null 2>&1 + # shellcheck disable=SC2086 + ssh $SSH_OPTS "$sshhost" "${command}" 1>/dev/null 2>&1 rm -f "${TEMP_DIR:?}/${token:?}" elif [[ "${t_loc:0:4}" == "ftp:" ]] ; then debug "using ftp to remove token file" @@ -1979,7 +2330,7 @@ done # end of ... loop through domains for cert ( from SANS list) # perform validation if via DNS challenge if [[ $VALIDATE_VIA_DNS == "true" ]]; then # loop through dns-variable files to check if dns has been changed - for dnsfile in $TEMP_DIR/dns_verify/*; do + for dnsfile in "$TEMP_DIR"/dns_verify/*; do if [[ -e "$dnsfile" ]]; then debug "loading DNSfile: $dnsfile" # shellcheck source=/dev/null @@ -2032,7 +2383,7 @@ if [[ $VALIDATE_VIA_DNS == "true" ]]; then fi # loop through dns-variable files to let the ACME server check the challenges - for dnsfile in $TEMP_DIR/dns_verify/*; do + for dnsfile in "$TEMP_DIR"/dns_verify/*; do if [[ -e "$dnsfile" ]]; then debug "loading DNSfile: $dnsfile" # shellcheck source=/dev/null @@ -2059,8 +2410,8 @@ get_certificate "$DOMAIN_DIR/${DOMAIN}.csr" \ "$CA_CERT" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then get_certificate "$DOMAIN_DIR/${DOMAIN}.ec.csr" \ - "${CERT_FILE::-4}.ec.crt" \ - "${CA_CERT::-4}.ec.crt" + "${CERT_FILE%.*}.ec.crt" \ + "${CA_CERT%.*}.ec.crt" fi # create Archive of new certs and keys. @@ -2069,30 +2420,31 @@ cert_archive debug "Certificates obtained and archived locally, will now copy to specified locations" # copy certs to the correct location (creating concatenated files as required) +umask 077 copy_file_to_location "domain certificate" "$CERT_FILE" "$DOMAIN_CERT_LOCATION" copy_file_to_location "private key" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LOCATION" copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - if [[ ! -z "$DOMAIN_CERT_LOCATION" ]]; then + if [[ -n "$DOMAIN_CERT_LOCATION" ]]; then copy_file_to_location "ec domain certificate" \ - "${CERT_FILE::-4}.ec.crt" \ - "${DOMAIN_CERT_LOCATION::-4}.ec.crt" + "${CERT_FILE%.*}.ec.crt" \ + "${DOMAIN_CERT_LOCATION%.*}.ec.crt" fi - if [[ ! -z "$DOMAIN_KEY_LOCATION" ]]; then + if [[ -n "$DOMAIN_KEY_LOCATION" ]]; then copy_file_to_location "ec private key" \ "$DOMAIN_DIR/${DOMAIN}.ec.key" \ - "${DOMAIN_KEY_LOCATION::-4}.ec.key" + "${DOMAIN_KEY_LOCATION%.*}.ec.key" fi - if [[ ! -z "$CA_CERT_LOCATION" ]]; then + if [[ -n "$CA_CERT_LOCATION" ]]; then copy_file_to_location "ec CA certificate" \ - "${CA_CERT::-4}.ec.crt" \ - "${CA_CERT_LOCATION::-4}.ec.crt" + "${CA_CERT%.*}.ec.crt" \ + "${CA_CERT_LOCATION%.*}.ec.crt" fi fi # if DOMAIN_CHAIN_LOCATION is not blank, then create and copy file. -if [[ ! -z "$DOMAIN_CHAIN_LOCATION" ]]; then +if [[ -n "$DOMAIN_CHAIN_LOCATION" ]]; then if [[ "$(dirname "$DOMAIN_CHAIN_LOCATION")" == "." ]]; then to_location="${DOMAIN_DIR}/${DOMAIN_CHAIN_LOCATION}" else @@ -2101,12 +2453,12 @@ if [[ ! -z "$DOMAIN_CHAIN_LOCATION" ]]; then cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem" copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem" "$to_location" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}_chain.pem.ec" + cat "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}_chain.pem.ec" copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem.ec" "${to_location}.ec" fi fi # if DOMAIN_KEY_CERT_LOCATION is not blank, then create and copy file. -if [[ ! -z "$DOMAIN_KEY_CERT_LOCATION" ]]; then +if [[ -n "$DOMAIN_KEY_CERT_LOCATION" ]]; then if [[ "$(dirname "$DOMAIN_KEY_CERT_LOCATION")" == "." ]]; then to_location="${DOMAIN_DIR}/${DOMAIN_KEY_CERT_LOCATION}" else @@ -2115,12 +2467,12 @@ if [[ ! -z "$DOMAIN_KEY_CERT_LOCATION" ]]; then cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem" copy_file_to_location "private key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem" "$to_location" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" + cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" copy_file_to_location "private ec key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" "${to_location}.ec" fi fi # if DOMAIN_PEM_LOCATION is not blank, then create and copy file. -if [[ ! -z "$DOMAIN_PEM_LOCATION" ]]; then +if [[ -n "$DOMAIN_PEM_LOCATION" ]]; then if [[ "$(dirname "$DOMAIN_PEM_LOCATION")" == "." ]]; then to_location="${DOMAIN_DIR}/${DOMAIN_PEM_LOCATION}" else @@ -2129,12 +2481,12 @@ if [[ ! -z "$DOMAIN_PEM_LOCATION" ]]; then cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" copy_file_to_location "full key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem" "$to_location" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}.pem.ec" + cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}.pem.ec" copy_file_to_location "full ec key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem.ec" "${to_location}.ec" fi fi # end of copying certs. - +umask "$ORIG_UMASK" # Run reload command to restart apache / nginx or whatever system reload_service @@ -2142,7 +2494,7 @@ reload_service if [[ "$DEACTIVATE_AUTH" == "true" ]]; then debug "in deactivate list is $deactivate_url_list" for deactivate_url in $deactivate_url_list; do - resp=$(curl "$deactivate_url" 2>/dev/null) + resp=$(curl --user-agent "$CURL_USERAGENT" "$deactivate_url" 2>/dev/null) d=$(json_get "$resp" "hostname") info "deactivating domain $d" debug "deactivating $deactivate_url"