Browse Source

Wildcard support in ACMEv2

Fixes #347 and #400
pull/599/head
Tim Kimber 5 years ago
parent
commit
ca666aa474
No known key found for this signature in database GPG Key ID: 3E1804964E76BD18
1 changed files with 128 additions and 145 deletions
  1. +128
    -145
      getssl

+ 128
- 145
getssl View File

@ -469,7 +469,7 @@ check_challenge_completion() { # checks with the ACME server if our challenge is
# if ACME response is that their check gave an invalid response, error exit
if [[ "$status" == "invalid" ]] ; then
err_detail=$(echo "$response" | grep "detail")
#! FIXME need to check for "DNS problem: SERVFAIL looking up CAA ..." and retry
# TODO need to check for "DNS problem: SERVFAIL looking up CAA ..." and retry
error_exit "$domain:Verify error:$err_detail"
fi
@ -491,6 +491,84 @@ check_challenge_completion() { # checks with the ACME server if our challenge is
fi
}
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)
# check for token at public dns server, waiting for a valid response.
for ns in $primary_ns; do
debug "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}" \
| 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}" \
| grep 'IN\WTXT'|awk -F'"' '{ print $2}')
elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then
check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${lower_d}" "${ns}" \
| grep 'descriptive text'|awk -F'"' '{ print $2}')
else
check_result=$(nslookup -type=txt "_acme-challenge.${lower_d}" "${ns}" \
| grep 'text ='|awk -F'"' '{ print $2}')
fi
debug "expecting $auth_key"
debug "${ns} gave ... $check_result"
if [[ "$check_result" == *"$auth_key"* ]]; then
check_dns="success"
else
if [[ $ntries -lt $DNS_WAIT_COUNT ]]; then
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"
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
fi
info "checking DNS at ${ns} for ${lower_d}. 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"
error_exit "checking _acme-challenge.${lower_d} gave $check_result not $auth_key"
fi
fi
done
done
if [[ "$DNS_EXTRA_WAIT" -gt 0 && "$PREVIOUSLY_VALIDATED" != "true" ]]; then
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
check_config() { # check the config files for all obvious errors
debug "checking config"
@ -518,13 +596,13 @@ check_config() { # check the config files for all obvious errors
config_errors=true
fi
# get all domains
# get all domains into an array
if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
alldomains=${SANS//[, ]/ }
read -r -a alldomains <<< "${SANS//[, ]/ }"
else
alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")
read -r -a alldomains <<< "$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")"
fi
if [[ -z "$alldomains" ]]; then
if [[ -z "${alldomains[*]}" ]]; then
info "${DOMAIN}: no domains specified"
config_errors=true
fi
@ -542,11 +620,14 @@ check_config() { # check the config files for all obvious errors
dn=0
tmplist=$(mktemp 2>/dev/null || mktemp -t getssl)
for d in $alldomains; do # loop over domains (dn is domain number)
for d in "${alldomains[@]}"; do # loop over domains (dn is domain number)
debug "checking domain $d"
if [[ "$(grep "^${d}$" "$tmplist")" = "$d" ]]; then
info "${DOMAIN}: $d appears to be duplicated in domain, SAN list"
config_errors=true
elif [[ "$d" != "${d##\*.}" ]] && [[ "$VALIDATE_VIA_DNS" != "true" ]]; then
info "${DOMAIN}: cannot use http-01 validation for wildcard domains"
config_errors=true
else
echo "$d" >> "$tmplist"
fi
@ -682,7 +763,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"
eval "$DNS_DEL_COMMAND" "${d##\*.}" "$auth_key"
done
shopt -u nullglob
fi
@ -809,14 +890,14 @@ create_csr() { # create a csr using a given key (if it doesn't already exist)
debug "domain csr exists at - $csr_file"
# check all domains in config are in csr
if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
alldomains=$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u)
read -r -a alldomains <<< "$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u)"
else
alldomains=$(echo "$DOMAIN,$SANS" | sed -e 's/,/ /g; s/ $//; y/ /\n/' | sort -u)
read -r -a alldomains <<< "$(echo "$DOMAIN,$SANS" | sed -e 's/,/ /g; s/ $//; y/ /\n/' | sort -u)"
fi
domains_in_csr=$(openssl req -text -noout -in "$csr_file" \
| sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \
| sort -u)
for d in $alldomains; do
for d in "${alldomains[@]}"; do
if [[ "$(echo "${domains_in_csr}"| grep "^${d}$")" != "${d}" ]]; then
info "existing csr at $csr_file does not contain ${d} - re-create-csr"\
".... $(echo "${domains_in_csr}"| grep "^${d}$")"
@ -824,7 +905,7 @@ create_csr() { # create a csr using a given key (if it doesn't already exist)
fi
done
# check all domains in csr are in config
if [[ "$alldomains" != "$domains_in_csr" ]]; then
if [[ "${alldomains[*]}" != "$domains_in_csr" ]]; then
info "existing csr at $csr_file does not have the same domains as the config - re-create-csr"
_RECREATE_CSR=1
fi
@ -877,7 +958,7 @@ create_key() { # create a domain key (if it doesn't already exist)
create_order() {
dstring="["
for d in $alldomains; do
for d in "${alldomains[@]}"; do
dstring="${dstring}{\"type\":\"dns\",\"value\":\"$d\"},"
done
dstring="${dstring::${#dstring}-1}]"
@ -893,9 +974,9 @@ create_order() {
if [[ $API -eq 1 ]]; then
dn=0
for d in $alldomains; do
for d in "${alldomains[@]}"; do
# get authorizations link
AuthLink[$dn]=$(json_get "$response" "identifiers" "value" "$d" "authorizations" "x")
AuthLink[$dn]=$(json_get "$response" "identifiers" "value" "${d##\*.}" "authorizations" "x")
debug "authorizations link for $d - ${AuthLink[$dn]}"
((dn++))
done
@ -909,12 +990,14 @@ create_order() {
send_signed_request "$l" ""
# Get domain from response
authdomain=$(json_get "$response" "identifier" "value")
# find array position (This is O(n2) but that doubt we'll see performance issues)
wildcard=$(json_get "$response" "wildcard")
debug wildcard="$wildcard"
# find array position (This is O(n2) but doubt that we'll see performance issues)
dn=0
for d in $alldomains; do
for d in "${alldomains[@]}"; do
# Convert domain to lowercase as response from server will be in lowercase
d=$(echo "$d" | tr "[:upper:]" "[:lower:]")
if [ "$d" == "$authdomain" ]; then
lower_d=$(echo "$d" | tr "[:upper:]" "[:lower:]")
if [[ ( "$lower_d" == "$authdomain" && -z "$wildcard" ) || ( "$lower_d" == "*.${authdomain}" && -n "$wildcard" ) ]]; then
debug "Saving authorization response for $authdomain for domain alldomains[$dn]"
debug "Response = ${response//[$'\t\r\n']}"
AuthLinkResponse[$dn]=$response
@ -1004,7 +1087,7 @@ find_dns_utils() {
fulfill_challenges() {
dn=0
for d in $alldomains; do
for d in "${alldomains[@]}"; do
# $d is domain in current loop, which is number $dn for ACL
info "Verifying $d"
if [[ "$USE_SINGLE_ACL" == "true" ]]; then
@ -1015,7 +1098,7 @@ for d in $alldomains; do
# request a challenge token from ACME server
if [[ $API -eq 1 ]]; then
request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"$d\"}}"
request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"${d##\*.}\"}}"
send_signed_request "$URL_new_authz" "$request"
debug "completed send_signed_request"
@ -1068,7 +1151,7 @@ for d in $alldomains; do
debug auth_key "$auth_key"
# shellcheck disable=SC2018,SC2019
lower_d=$(echo "$d" | tr A-Z a-z)
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"
@ -1082,21 +1165,7 @@ for d in $alldomains; do
fi
debug primary_ns "$primary_ns"
# make a directory to hold pending dns-challenges
if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then
mkdir "$TEMP_DIR/dns_verify"
fi
# generate a file with the current variables for the dns-challenge
cat > "$TEMP_DIR/dns_verify/$d" <<- _EOF_
token="${token}"
uri="${uri}"
keyauthorization="${keyauthorization}"
d="${d}"
primary_ns="${primary_ns}"
auth_key="${auth_key}"
_EOF_
check_challenge_completion_dns "${token}" "${uri}" "${keyauthorization}" "${d}" "${primary_ns}" "${auth_key}"
else # set up the correct http token for verification
if [[ $API -eq 1 ]]; then
# get the token from the http component
@ -1179,101 +1248,7 @@ for d in $alldomains; do
((dn++))
fi
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
if [[ -e "$dnsfile" ]]; then
debug "loading DNSfile: $dnsfile"
# shellcheck source=/dev/null
. "$dnsfile"
# Always use lowercase domain name when querying DNS servers
# shellcheck disable=SC2018,SC2019
lower_d=$(echo "$d" | tr A-Z a-z)
# check for token at public dns server, waiting for a valid response.
for ns in $primary_ns; do
debug "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}" \
| 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}" \
| grep 'IN\WTXT'|awk -F'"' '{ print $2}')
elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then
check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${lower_d}" "${ns}" \
| grep 'descriptive text'|awk -F'"' '{ print $2}')
else
check_result=$(nslookup -type=txt "_acme-challenge.${lower_d}" "${ns}" \
| grep 'text ='|awk -F'"' '{ print $2}')
fi
debug "expecting $auth_key"
debug "${ns} gave ... $check_result"
if [[ "$check_result" == *"$auth_key"* ]]; then
check_dns="success"
else
if [[ $ntries -lt $DNS_WAIT_COUNT ]]; then
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"
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
fi
info "checking DNS at ${ns} for ${lower_d}. 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"
# remove $dnsfile after each loop.
rm -f "$dnsfile"
error_exit "checking _acme-challenge.${lower_d} gave $check_result not $auth_key"
fi
fi
done
done
fi
done
if [[ "$DNS_EXTRA_WAIT" -gt 0 && "$PREVIOUSLY_VALIDATED" != "true" ]]; then
info "sleeping $DNS_EXTRA_WAIT seconds before asking the ACME server to check the dns"
sleep "$DNS_EXTRA_WAIT"
fi
# loop through dns-variable files to let the ACME server check the challenges
for dnsfile in "$TEMP_DIR"/dns_verify/*; do
if [[ -e "$dnsfile" ]]; then
debug "loading DNSfile: $dnsfile"
# shellcheck source=/dev/null
. "$dnsfile"
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"
# remove $dnsfile after each loop.
rm -f "$dnsfile"
fi
done
fi
# end of ... perform validation if via DNS challenge
#end of varify each domain.
#end of verify each domain.
}
get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns )
@ -2557,7 +2532,7 @@ if [[ ${_CHECK_ALL} -eq 1 ]]; then
fi
# check if $dir is a directory with a getssl.cfg in it
if [[ -f "$dir/getssl.cfg" ]]; then
cmd="$cmd -w $WORKING_DIR $(basename "$dir")"
cmd="$cmd -w $WORKING_DIR \"$(basename "$dir")\""
debug "CMD: $cmd"
eval "$cmd"
fi
@ -2590,15 +2565,20 @@ if [[ ${_CREATE_CONFIG} -eq 1 ]]; then
info "creating domain config file in $DOMAIN_DIR/getssl.cfg"
# if domain has an existing cert, copy from domain and use to create defaults.
EX_CERT=$(echo \
| openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:443" 2>/dev/null \
| openssl s_client -servername "${DOMAIN##\*.}" -connect "${DOMAIN##\*.}:443" 2>/dev/null \
| openssl x509 2>/dev/null)
EX_SANS="www.${DOMAIN}"
EX_SANS="www.${DOMAIN##\*.}"
if [[ -n "${EX_CERT}" ]]; then
# Putting this inside the EX_SANS line below doesn't work on Centos7
escaped_d=${DOMAIN/\*/\\\*}
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-)
| grep -Eo "DNS:[a-zA-Z 0-9.-\*]*" | sed "s@DNS:${escaped_d}@@g" | grep -v '^$' | cut -c 5-)
EX_SANS=${EX_SANS//$'\n'/','}
fi
if [[ -n "${EX_SANS}" ]]; then
info "Adding SANS=$EX_SANS from certificate installed on ${DOMAIN##\*.} to new configuration file"
fi
write_domain_template "$DOMAIN_DIR/getssl.cfg"
fi
TEMP_DIR="$DOMAIN_DIR/tmp"
@ -2674,11 +2654,12 @@ 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
debug "getting certificate for $DOMAIN from remote server"
real_d=${DOMAIN##\*.}
debug "getting certificate for $DOMAIN from remote server ($real_d)"
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
# shellcheck disable=SC2086
# check if openssl supports RSA-PSS
if [[ $(echo | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} -sigalgs RSA-PSS+SHA256 2>/dev/null) ]]; then
if [[ $(echo | openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} -sigalgs RSA-PSS+SHA256 2>/dev/null) ]]; then
CIPHER="-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA512"
else
CIPHER="-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512"
@ -2688,7 +2669,7 @@ if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then
fi
# shellcheck disable=SC2086
EX_CERT=$(echo \
| openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${CIPHER} 2>/dev/null \
| openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${CIPHER} 2>/dev/null \
| openssl x509 2>/dev/null)
if [[ -n "$EX_CERT" ]]; then # if obtained a cert
if [[ -s "$CERT_FILE" ]]; then # if local exists
@ -2877,9 +2858,9 @@ info "Verify each domain"
# loop through domains for cert ( from SANS list)
if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
alldomains=${SANS//[, ]/ }
read -r -a alldomains <<< "${SANS//[, ]/ }"
else
alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")
read -r -a alldomains <<< "$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")"
fi
if [[ $API -eq 2 ]]; then
@ -2941,11 +2922,12 @@ fi
# Check if the certificate is installed correctly
if [[ ${CHECK_REMOTE} == "true" ]]; then
real_d=${DOMAIN##\*.}
sleep "$CHECK_REMOTE_WAIT"
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
# shellcheck disable=SC2086
# check if openssl supports RSA-PSS
if [[ $(echo | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} -sigalgs RSA-PSS+SHA256 2>/dev/null) ]]; then
if [[ $(echo | openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} -sigalgs RSA-PSS+SHA256 2>/dev/null) ]]; then
PARAMS=("-sigalgs RSA-PSS+SHA256:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512" "-sigalgs ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512")
else
PARAMS=("-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512" "-sigalgs ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512")
@ -2962,20 +2944,21 @@ if [[ ${CHECK_REMOTE} == "true" ]]; then
for ((i=0; i<${#PARAMS[@]};++i)); do
debug "Checking ${CERTS[i]}"
# shellcheck disable=SC2086
debug openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${PARAMS[i]}
CERT_REMOTE=$(echo \
| openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${PARAMS[i]} 2>/dev/null \
| openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${PARAMS[i]} 2>/dev/null \
| openssl x509 -noout -fingerprint 2>/dev/null)
CERT_LOCAL=$(openssl x509 -noout -fingerprint < "${CERTS[i]}" 2>/dev/null)
debug CERT_LOCAL="${CERT_LOCAL}"
debug CERT_REMOTE="${CERT_REMOTE}"
if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then
info "${DOMAIN} - ${TYPES[i]} certificate installed OK on server"
info "${real_d} - ${TYPES[i]} certificate installed OK on server"
elif [[ "$CERT_REMOTE" == "" ]]; then
info "${CERTS[i]} not returned by server"
error_exit "${DOMAIN} - ${TYPES[i]} certificate obtained but not installed on server"
error_exit "${real_d} - ${TYPES[i]} certificate obtained but not installed on server"
else
info "${CERTS[i]} didn't match server"
error_exit "${DOMAIN} - ${TYPES[i]} certificate obtained but certificate on server is different from the new certificate"
error_exit "${real_d} - ${TYPES[i]} certificate obtained but certificate on server is different from the new certificate"
fi
done
fi


Loading…
Cancel
Save