diff --git a/docker-compose.yml b/docker-compose.yml index 97bb1e5..09a4264 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,8 +7,6 @@ services: environment: # with Go 1.13.x which defaults TLS 1.3 to on GODEBUG: "tls13=1" - # Don't re-use authorisations (breaks force renew test scripts) - # PEBBLE_AUTHZREUSE: "0" ports: - 14000:14000 # HTTPS ACME API - 15000:15000 # HTTPS Management API @@ -23,33 +21,33 @@ services: networks: acmenet: ipv4_address: 10.30.50.3 - getssl-ubuntu18: + getssl-alpine: build: context: . - dockerfile: test/Dockerfile-ubuntu18 - container_name: getssl-ubuntu18 + dockerfile: test/Dockerfile-alpine + container_name: getssl-alpine volumes: - .:/getssl environment: - GETSSL_HOST: ubuntu18.getssl.test - GETSSL_IP: 10.30.50.4 - NGINX_CONFIG: /etc/nginx/sites-enabled/default + GETSSL_HOST: alpine.getssl.test + GETSSL_IP: 10.30.50.10 + NGINX_CONFIG: /etc/nginx/conf.d/default.conf networks: acmenet: - ipv4_address: 10.30.50.4 + ipv4_address: 10.30.50.10 aliases: - - ubuntu18.getssl.test - - a.ubuntu18.getssl.test - - b.ubuntu18.getssl.test - - c.ubuntu18.getssl.test - - d.ubuntu18.getssl.test - - e.ubuntu18.getssl.test - - f.ubuntu18.getssl.test - - g.ubuntu18.getssl.test - - h.ubuntu18.getssl.test - - i.ubuntu18.getssl.test - - j.ubuntu18.getssl.test - - k.ubuntu18.getssl.test + - alpine.getssl.test + - a.alpine.getssl.test + - b.alpine.getssl.test + - c.alpine.getssl.test + - d.alpine.getssl.test + - e.alpine.getssl.test + - f.alpine.getssl.test + - g.alpine.getssl.test + - h.alpine.getssl.test + - i.alpine.getssl.test + - j.alpine.getssl.test + - k.alpine.getssl.test getssl-centos6: build: context: . @@ -59,11 +57,11 @@ services: - .:/getssl environment: GETSSL_HOST: centos6.getssl.test - GETSSL_IP: 10.30.50.5 + GETSSL_IP: 10.30.50.11 NGINX_CONFIG: /etc/nginx/conf.d/default.conf networks: acmenet: - ipv4_address: 10.30.50.5 + ipv4_address: 10.30.50.11 aliases: - centos6.getssl.test - a.centos6.getssl.test @@ -77,23 +75,89 @@ services: - i.centos6.getssl.test - j.centos6.getssl.test - k.centos6.getssl.test - getssl-ubuntu18-no-gawk: + getssl-debian: + build: + context: . + dockerfile: test/Dockerfile-debian + container_name: getssl-debian + volumes: + - .:/getssl + environment: + GETSSL_HOST: debian.getssl.test + GETSSL_IP: 10.30.50.12 + NGINX_CONFIG: /etc/nginx/sites-enabled/default + networks: + acmenet: + ipv4_address: 10.30.50.12 + aliases: + - debian.getssl.test + - a.debian.getssl.test + - b.debian.getssl.test + - c.debian.getssl.test + - d.debian.getssl.test + - e.debian.getssl.test + - f.debian.getssl.test + - g.debian.getssl.test + - h.debian.getssl.test + - i.debian.getssl.test + - j.debian.getssl.test + - k.debian.getssl.test + getssl-ubuntu: build: context: . - dockerfile: test/Dockerfile-ubuntu18-no-gawk - container_name: getssl-ubuntu18-no-gawk + dockerfile: test/Dockerfile-ubuntu + container_name: getssl-ubuntu volumes: - .:/getssl environment: - GETSSL_HOST: ubuntu18-no-gawk.getssl.test - GETSSL_IP: 10.30.50.6 + GETSSL_HOST: ubuntu.getssl.test + GETSSL_IP: 10.30.50.13 NGINX_CONFIG: /etc/nginx/sites-enabled/default - TEST_AWK: "yes" networks: acmenet: - ipv4_address: 10.30.50.6 + ipv4_address: 10.30.50.13 aliases: - - ubuntu18-no-gawk.getssl.test + - ubuntu.getssl.test + - a.ubuntu.getssl.test + - b.ubuntu.getssl.test + - c.ubuntu.getssl.test + - d.ubuntu.getssl.test + - e.ubuntu.getssl.test + - f.ubuntu.getssl.test + - g.ubuntu.getssl.test + - h.ubuntu.getssl.test + - i.ubuntu.getssl.test + - j.ubuntu.getssl.test + - k.ubuntu.getssl.test + getssl-ubuntu18: + build: + context: . + dockerfile: test/Dockerfile-ubuntu18 + container_name: getssl-ubuntu18 + volumes: + - .:/getssl + environment: + GETSSL_HOST: ubuntu18.getssl.test + GETSSL_IP: 10.30.50.14 + NGINX_CONFIG: /etc/nginx/sites-enabled/default + networks: + acmenet: + ipv4_address: 10.30.50.14 + aliases: + - ubuntu18.getssl.test + - a.ubuntu18.getssl.test + - b.ubuntu18.getssl.test + - c.ubuntu18.getssl.test + - d.ubuntu18.getssl.test + - e.ubuntu18.getssl.test + - f.ubuntu18.getssl.test + - g.ubuntu18.getssl.test + - h.ubuntu18.getssl.test + - i.ubuntu18.getssl.test + - j.ubuntu18.getssl.test + - k.ubuntu18.getssl.test + + networks: acmenet: diff --git a/getssl b/getssl index e5a0f0a..b7b9094 100755 --- a/getssl +++ b/getssl @@ -211,49 +211,37 @@ PROGNAME=${0##*/} VERSION="2.16" # defaults -# ACCOUNT_EMAIL -# ACCOUNT_KEY_ALG ACCOUNT_KEY_LENGTH=4096 ACCOUNT_KEY_TYPE="rsa" -# AGREEMENT? -CA_CERT_LOCATION="" CA="https://acme-staging-v02.api.letsencrypt.org/directory" +CA_CERT_LOCATION="" CHALLENGE_CHECK_TYPE="http" CHECK_ALL_AUTH_DNS="false" -CHECK_REMOTE_WAIT=0 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-v02.api.letsencrypt.org" -# DNS_ADD_COMMAND -# DNS_DEL_COMMAND -# DNS_CHECK_FUNC (internal?) DNS_EXTRA_WAIT="" DNS_WAIT=10 -# DOMAIN_DIR DOMAIN_KEY_LENGTH=4096 DUAL_RSA_ECDSA="false" -# FORCE_RENEWAL (file) GETSSL_IGNORE_CP_PRESERVE="false" HTTP_TOKEN_CHECK_WAIT=0 IGNORE_DIRECTORY_DOMAIN="false" -OCSP_MUST_STAPLE="false" ORIG_UMASK=$(umask) -# PREVENT_NON_INTERACTIVE_RENEWAL PREVIOUSLY_VALIDATED="true" PRIVATE_KEY_ALG="rsa" PUBLIC_DNS_SERVER="" RELOAD_CMD="" RENEW_ALLOW="30" REUSE_PRIVATE_KEY="true" -# REMOTE_EXTRA -# REVOKE_CA SERVER_TYPE="https" SKIP_HTTP_TOKEN_CHECK="false" -# SSH_OPTS SSLCONF="$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf" +OCSP_MUST_STAPLE="false" TEMP_UPGRADE_FILE="" TOKEN_USER_ID="" USE_SINGLE_ACL="false" @@ -343,14 +331,15 @@ 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 - error_exit "$domain:Verify error:$(echo "$response" | grep "detail" | awk -F' "' '{print $3}')" + err_detail=$(json_get "$response" detail) + error_exit "$domain:Verify error:$err_detail" fi # if ACME response is pending ( they haven't completed checks yet) then wait and try again. if [[ "$status" == "pending" ]] ; then info "Pending" else - error_exit "$domain:Verify error:$(echo "$response" | grep "detail")" + error_exit "$domain:Verify error:$response" fi debug "sleep 5 secs before testing verify again" sleep 5 @@ -556,6 +545,7 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. IFS=\; read -r -a copy_locations <<<"$3" for to in "${copy_locations[@]}"; do info "copying $cert to $to" + debug "copying from $from to $to" if [[ "${to:0:4}" == "ssh:" ]] ; then debug "using scp scp -q $from ${to:4}" if ! scp -q "$from" "${to:4}" >/dev/null 2>&1 ; then @@ -720,29 +710,6 @@ create_key() { # create a domain key (if it doesn't already exist) fi } -create_order() { - dstring="[" - for d in $alldomains; do - dstring="${dstring}{\"type\":\"dns\",\"value\":\"$d\"}," - done - dstring="${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") - 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 -} - date_epoc() { # convert the date into epoch time if [[ "$os" == "bsd" ]]; then date -j -f "%b %d %T %Y %Z" "$1" +%s @@ -785,539 +752,282 @@ error_exit() { # give error message on error exit exit 1 } -fulfill_challenges() { -dn=0 -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 - DOMAIN_ACL="${ACL[0]}" - else - DOMAIN_ACL="${ACL[$dn]}" - fi +get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns ) + gad_d="$1" # domain name + gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER - # request a challenge token from ACME server - debug "Requesting challenge tokens" - 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" + if [[ "$os" == "cygwin" ]]; then + all_auth_dns_servers=$(nslookup -type=soa "${d}" ${PUBLIC_DNS_SERVER} 2>/dev/null \ + | grep "primary name server" \ + | awk '{print $NF}') + if [[ -z "$all_auth_dns_servers" ]]; then + error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" + fi + primary_ns="$all_auth_dns_servers" + return + fi - # 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" + if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then + if [[ -z "$gad_s" ]]; then #checking for CNAMEs + res=$($DNS_CHECK_FUNC CNAME "$gad_d"| grep "^$gad_d") + else + res=$($DNS_CHECK_FUNC CNAME "$gad_d" "@$gad_s"| grep "^$gad_d") fi - else - send_signed_request "${AuthLink[$dn]}" "" + 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 + res=$($DNS_CHECK_FUNC NS "$gad_d"| grep "^$gad_d") + else + res=$($DNS_CHECK_FUNC NS "$gad_d" "@$gad_s"| grep "^$gad_d") + fi + if [[ -z "$res" ]]; then + error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" + else + all_auth_dns_servers=$(echo "$res" | awk '$4 ~ "NS" {print $5}' | sed 's/\.$//g'|tr '\n' ' ') + fi + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + fi + return fi - if [[ $response_status == "valid" ]]; then - info "$d is already validated" - if [[ "$DEACTIVATE_AUTH" == "true" ]]; then - deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")" - deactivate_url_list+=" $deactivate_url " - debug "url added to deactivate list ${deactivate_url}" - debug "deactivate list is now $deactivate_url_list" + if [[ "$DNS_CHECK_FUNC" == "host" ]]; then + if [[ -z "$gad_s" ]]; then + res=$($DNS_CHECK_FUNC -t NS "$gad_d"| grep "name server") + else + res=$($DNS_CHECK_FUNC -t NS "$gad_d" "$gad_s"| grep "name server") fi - # increment domain-counter - ((dn++)) - else - PREVIOUSLY_VALIDATED="false" - if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification - 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") - # get the uri from the dns component - uri=$(json_get "$response" "uri" "dns-01") - debug uri "$uri" - else # APIv2 - debug "authlink response = $response" - # get the token from the http-01 component - token=$(json_get "$response" "challenges" "type" "dns-01" "token") - # get the uri from the http component - uri=$(json_get "$response" "challenges" "type" "dns-01" "url") - debug uri "$uri" - fi + if [[ -z "$res" ]]; then + error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" + else + all_auth_dns_servers=$(echo "$res" | awk '{print $4}' | sed 's/\.$//g'|tr '\n' ' ') + fi + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + fi + return + fi - keyauthorization="$token.$thumbprint" - debug keyauthorization "$keyauthorization" + res=$(nslookup -debug -type=soa -type=ns "$gad_d" ${gad_s}) - #create signed authorization key from token. - auth_key=$(printf '%s' "$keyauthorization" | openssl dgst -sha256 -binary \ - | openssl base64 -e \ - | tr -d '\n\r' \ - | sed -e 's:=*$::g' -e 'y:+/:-_:') - debug auth_key "$auth_key" + if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then + # this is a Non-authoritative server, need to check for an authoritative one. + gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') + if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + # if domain name doesn't exist, then find auth servers for next level up + gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }') + gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}') + fi + fi - debug "adding dns via command: $DNS_ADD_COMMAND $d $auth_key" - if ! eval "$DNS_ADD_COMMAND" "$d" "$auth_key" ; then - error_exit "DNS_ADD_COMMAND failed for domain $d" - fi + if [[ -z "$gad_s" ]]; then + res=$(nslookup -debug -type=soa -type=ns "$gad_d") + else + res=$(nslookup -debug -type=soa -type=ns "$gad_d" "${gad_s}") + fi - # find a primary / authoritative DNS server for the domain - if [[ -z "$AUTH_DNS_SERVER" ]]; then - get_auth_dns "$d" - else - primary_ns="$AUTH_DNS_SERVER" - fi - debug primary_ns "$primary_ns" + if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then + gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') + elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }') + gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') + fi - # make a directory to hold pending dns-challenges - if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then - mkdir "$TEMP_DIR/dns_verify" - fi + all_auth_dns_servers=$(nslookup -type=soa -type=ns "$gad_d" "$gad_s" \ + | awk ' $2 ~ "nameserver" {print $4}' \ + | sed 's/\.$//g'| tr '\n' ' ') + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + 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_ +get_certificate() { # get certificate for csr, if all domains validated. + gc_csr=$1 # the csr file + gc_certfile=$2 # The filename for the certificate + gc_cafile=$3 # The filename for the CA certificate - else # set up the correct http token for verification - if [[ $API -eq 1 ]]; then - # get the token from the http component - token=$(json_get "$response" "token" "http-01") - # get the uri from the http component - uri=$(json_get "$response" "uri" "http-01") - debug uri "$uri" - else # APIv2 - send_signed_request "${AuthLink[$dn]}" "" - debug "authlink response = $response" - # get the token from the http-01 component - token=$(json_get "$response" "challenges" "type" "http-01" "token") - # get the uri from the http component - uri=$(json_get "$response" "challenges" "type" "http-01" "url" | head -n1) - debug uri "$uri" - fi + der=$(openssl req -in "$gc_csr" -outform DER | urlbase64) + debug "der $der" + 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 - #create signed authorization key from token. - keyauthorization="$token.$thumbprint" + # 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 - # save variable into temporary file - echo -n "$keyauthorization" > "$TEMP_DIR/$token" - chmod 644 "$TEMP_DIR/$token" + # 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" + send_signed_request "$OrderLink" "" + CertData=$(json_get "$response" "certificate") + debug "CertData is at $CertData" + send_signed_request "$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 +} - # copy to token to acme challenge location - umask 0022 - IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" - for t_loc in "${token_locations[@]}"; do - debug "copying file from $TEMP_DIR/$token to ${t_loc}" - copy_file_to_location "challenge token" \ - "$TEMP_DIR/$token" \ - "${t_loc}/$token" - done - umask "$ORIG_UMASK" - - wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}/.well-known/acme-challenge/$token" - debug wellknown_url "$wellknown_url" +get_cr() { # get curl response + url="$1" + debug url "$url" + response=$(curl --user-agent "$CURL_USERAGENT" --silent "$url") + ret=$? + debug response "$response" + code=$(json_get "$response" status) + debug code "$code" + debug "get_cr return code $ret" + return $ret +} - if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then - info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly" - else - sleep "$HTTP_TOKEN_CHECK_WAIT" - # check that we can reach the challenge ourselves, if not, then error - 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 +get_os() { # function to get the current Operating System + uname_res=$(uname -s) + if [[ $(date -h 2>&1 | grep -ic busybox) -gt 0 ]]; then + os="busybox" + elif [[ ${uname_res} == "Linux" ]]; then + os="linux" + elif [[ ${uname_res} == "FreeBSD" ]]; then + os="bsd" + elif [[ ${uname_res} == "Darwin" ]]; then + os="mac" + elif [[ ${uname_res:0:6} == "CYGWIN" ]]; then + os="cygwin" + elif [[ ${uname_res:0:5} == "MINGW" ]]; then + os="mingw" + else + os="unknown" + fi + debug "detected os type = $os" +} - check_challenge_completion "$uri" "$d" "$keyauthorization" +get_signing_params() { # get signing parameters from key + skey=$1 + if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key + pub_exp64=$(openssl rsa -in "${skey}" -noout -text \ + | grep publicExponent \ + | grep -oE "0x[a-f0-9]+" \ + | cut -d'x' -f2 \ + | hex2bin \ + | urlbase64) + pub_mod64=$(openssl rsa -in "${skey}" -noout -modulus \ + | cut -d'=' -f2 \ + | hex2bin \ + | urlbase64) - debug "remove token from ${DOMAIN_ACL}" - IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" - for t_loc in "${token_locations[@]}"; do - if [[ "${t_loc:0:4}" == "ssh:" ]] ; then - 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 $SSH_OPTS $sshhost ${command}" - # shellcheck disable=SC2029 - # 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" - ftpuser=$(echo "${t_loc}"| awk -F: '{print $2}') - ftppass=$(echo "${t_loc}"| awk -F: '{print $3}') - ftphost=$(echo "${t_loc}"| awk -F: '{print $4}') - ftplocn=$(echo "${t_loc}"| awk -F: '{print $5}') - debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost location=$ftplocn" - ftp -n <<- EOF - open $ftphost - user $ftpuser $ftppass - cd $ftplocn - delete ${token:?} - EOF - else - rm -f "${t_loc:?}/${token:?}" - fi - done + jwk='{"e":"'"${pub_exp64}"'","kty":"RSA","n":"'"${pub_mod64}"'"}' + jwkalg="RS256" + signalg="sha256" + elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. + crv="$(openssl ec -in "$skey" -noout -text 2>/dev/null | awk '$2 ~ "CURVE:" {print $3}')" + if [[ -z "$crv" ]]; then + gsp_keytype="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ + | grep "^ASN1 OID:" \ + | awk '{print $3}')" + case "$gsp_keytype" in + prime256v1) crv="P-256" ;; + secp384r1) crv="P-384" ;; + secp521r1) crv="P-521" ;; + *) error_exit "invalid curve algorithm type $gsp_keytype";; + esac fi - # increment domain-counter - ((dn++)) + case "$crv" in + P-256) jwkalg="ES256" ; signalg="sha256" ;; + P-384) jwkalg="ES384" ; signalg="sha384" ;; + P-521) jwkalg="ES512" ; signalg="sha512" ;; + *) error_exit "invalid curve algorithm type $crv";; + esac + pubtext="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ + | awk '/^pub:/{p=1;next}/^ASN1 OID:/{p=0}p' \ + | tr -d ": \n\r")" + mid=$(( (${#pubtext} -2) / 2 + 2 )) + debug "pubtext = $pubtext" + x64=$(echo "$pubtext" | cut -b 3-$mid | hex2bin | urlbase64) + y64=$(echo "$pubtext" | cut -b $((mid+1))-${#pubtext} | hex2bin | urlbase64) + jwk='{"crv":"'"$crv"'","kty":"EC","x":"'"$x64"'","y":"'"$y64"'"}' + debug "jwk $jwk" + else + error_exit "Invalid key file" 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" - - # 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.${d}" "${ns}" \ - | grep ^_acme -A2\ - | grep '"'|awk -F'"' '{ print $2}') - elif [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then - check_result=$($DNS_CHECK_FUNC TXT "_acme-challenge.${d}" "@${ns}" \ - | grep '300 IN TXT'|awk -F'"' '{ print $2}') - elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then - check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${d}" "${ns}" \ - | grep 'descriptive text'|awk -F'"' '{ print $2}') - else - check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ - | grep 'text ='|awk -F'"' '{ print $2}') - fi - debug "expecting $auth_key" - debug "${ns} gave ... $check_result" + thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)" + debug "jwk alg = $jwkalg" + debug "jwk = $jwk" + debug "thumbprint $thumbprint" +} - if [[ "$check_result" == *"$auth_key"* ]]; then - check_dns="success" - else - if [[ $ntries -lt 100 ]]; then - ntries=$(( ntries + 1 )) - info "checking DNS at ${ns} for ${d}. Attempt $ntries/100 gave wrong result, "\ - "waiting $DNS_WAIT secs before checking again" - sleep $DNS_WAIT - else - debug "dns check failed - removing existing value" - error_exit "checking _acme-challenge.${d} gave $check_result not $auth_key" - fi - fi - done - done - fi - done +graceful_exit() { # normal exit function. + clean_up + exit +} - 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 +help_message() { # print out the help message + cat <<- _EOF_ + $PROGNAME ver. $VERSION + Obtain SSL certificates from the letsencrypt.org ACME server - # 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" + $(usage) - check_challenge_completion "$uri" "$d" "$keyauthorization" + Options: + -a, --all Check all certificates + -d, --debug Output debug information + -c, --create Create default config files + -f, --force Force renewal of cert (overrides expiry checks) + -h, --help Display this help message and exit + -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) + -Q, --mute Like -q, but also mute notification about successful upgrade + -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) + -u, --upgrade Upgrade getssl if a more recent version is available + -k, --keep "#" Maximum number of old getssl versions to keep when upgrading + -U, --nocheck Do not check if a more recent version is available + -w working_dir "Working directory" - debug "remove DNS entry" - eval "$DNS_DEL_COMMAND" "$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. + _EOF_ } -get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns ) - gad_d="$1" # domain name - gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER +hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char +# printf -- "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" + echo -e -n "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" +} - if [[ "$os" == "cygwin" ]]; then - all_auth_dns_servers=$(nslookup -type=soa "${d}" ${PUBLIC_DNS_SERVER} 2>/dev/null \ - | grep "primary name server" \ - | awk '{print $NF}') - if [[ -z "$all_auth_dns_servers" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - fi - primary_ns="$all_auth_dns_servers" - return +info() { # write out info as long as the quiet flag has not been set. + if [[ ${_QUIET} -eq 0 ]]; then + echo "$@" fi - - if [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then - if [[ -z "$gad_s" ]]; then #checking for CNAMEs - res=$($DNS_CHECK_FUNC CNAME "$gad_d"| grep "^$gad_d") - else - res=$($DNS_CHECK_FUNC CNAME "$gad_d" "@$gad_s"| grep "^$gad_d") - fi - 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 - res=$($DNS_CHECK_FUNC NS "$gad_d"| grep "^$gad_d") - else - res=$($DNS_CHECK_FUNC NS "$gad_d" "@$gad_s"| grep "^$gad_d") - fi - if [[ -z "$res" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - else - all_auth_dns_servers=$(echo "$res" | awk '$4 ~ "NS" {print $5}' | sed 's/\.$//g'|tr '\n' ' ') - fi - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') - fi - return - fi - - if [[ "$DNS_CHECK_FUNC" == "host" ]]; then - if [[ -z "$gad_s" ]]; then - res=$($DNS_CHECK_FUNC -t NS "$gad_d"| grep "name server") - else - res=$($DNS_CHECK_FUNC -t NS "$gad_d" "$gad_s"| grep "name server") - fi - if [[ -z "$res" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - else - all_auth_dns_servers=$(echo "$res" | awk '{print $4}' | sed 's/\.$//g'|tr '\n' ' ') - fi - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') - fi - return - fi - - res=$(nslookup -debug=1 -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. - gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') - if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then - # if domain name doesn't exist, then find auth servers for next level up - gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }') - gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}') - fi - fi - - if [[ -z "$gad_s" ]]; then - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d") - else - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" "${gad_s}") - fi - - if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then - gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') - elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then - gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }') - gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') - fi - - all_auth_dns_servers=$(nslookup -type=soa -type=ns "$gad_d" "$gad_s" \ - | awk ' $2 ~ "nameserver" {print $4}' \ - | sed 's/\.$//g'| tr '\n' ' ') - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') - fi -} - -get_certificate() { # get certificate for csr, if all domains validated. - gc_csr=$1 # the csr file - gc_certfile=$2 # The filename for the certificate - gc_cafile=$3 # The filename for the CA certificate - - 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. - CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r') - 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 - - # 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 - info "Requesting Finalize Link" - send_signed_request "$FinalizeLink" "{\"csr\": \"$der\"}" "needbase64" - info Requesting Order Link - debug "order link was $OrderLink" - send_signed_request "$OrderLink" "" - # if ACME response is processing (still creating certificates) then wait and try again. - while [[ "$response_status" == "processing" ]]; do - info "ACME server still Processing certificates" - sleep 5 - send_signed_request "$OrderLink" "" - done - info "Requesting certificate" - CertData=$(json_get "$response" "certificate") - send_signed_request "$CertData" "" "" "$FULL_CHAIN" - info "Full certificate saved in $FULL_CHAIN" - 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}' "$FULL_CHAIN" - info "Certificate saved in $gc_certfile" - fi -} - -get_cr() { # get curl response - url="$1" - debug url "$url" - response=$(curl --user-agent "$CURL_USERAGENT" --silent "$url") - ret=$? - debug response "$response" - code=$(json_get "$response" status) - debug code "$code" - debug "get_cr return code $ret" - return $ret -} - -get_os() { # function to get the current Operating System - uname_res=$(uname -s) - if [[ $(date -h 2>&1 | grep -ic busybox) -gt 0 ]]; then - os="busybox" - elif [[ ${uname_res} == "Linux" ]]; then - os="linux" - elif [[ ${uname_res} == "FreeBSD" ]]; then - os="bsd" - elif [[ ${uname_res} == "Darwin" ]]; then - os="mac" - elif [[ ${uname_res:0:6} == "CYGWIN" ]]; then - os="cygwin" - elif [[ ${uname_res:0:5} == "MINGW" ]]; then - os="mingw" - else - os="unknown" - fi - debug "detected os type = $os" - if [[ -f /etc/issue ]]; then - debug "Running $(cat /etc/issue)" - fi -} - -get_signing_params() { # get signing parameters from key - skey=$1 - if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key - pub_exp64=$(openssl rsa -in "${skey}" -noout -text \ - | grep publicExponent \ - | grep -oE "0x[a-f0-9]+" \ - | cut -d'x' -f2 \ - | hex2bin \ - | urlbase64) - pub_mod64=$(openssl rsa -in "${skey}" -noout -modulus \ - | cut -d'=' -f2 \ - | hex2bin \ - | urlbase64) - - jwk='{"e":"'"${pub_exp64}"'","kty":"RSA","n":"'"${pub_mod64}"'"}' - jwkalg="RS256" - signalg="sha256" - elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. - crv="$(openssl ec -in "$skey" -noout -text 2>/dev/null | awk '$2 ~ "CURVE:" {print $3}')" - if [[ -z "$crv" ]]; then - gsp_keytype="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ - | grep "^ASN1 OID:" \ - | awk '{print $3}')" - case "$gsp_keytype" in - prime256v1) crv="P-256" ;; - secp384r1) crv="P-384" ;; - secp521r1) crv="P-521" ;; - *) error_exit "invalid curve algorithm type $gsp_keytype";; - esac - fi - case "$crv" in - P-256) jwkalg="ES256" ; signalg="sha256" ;; - P-384) jwkalg="ES384" ; signalg="sha384" ;; - P-521) jwkalg="ES512" ; signalg="sha512" ;; - *) error_exit "invalid curve algorithm type $crv";; - esac - pubtext="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ - | awk '/^pub:/{p=1;next}/^ASN1 OID:/{p=0}p' \ - | tr -d ": \n\r")" - mid=$(( (${#pubtext} -2) / 2 + 2 )) - x64=$(echo "$pubtext" | cut -b 3-$mid | hex2bin | urlbase64) - y64=$(echo "$pubtext" | cut -b $((mid+1))-${#pubtext} | hex2bin | urlbase64) - jwk='{"crv":"'"$crv"'","kty":"EC","x":"'"$x64"'","y":"'"$y64"'"}' - else - error_exit "Invalid key file" - fi - thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)" - debug "jwk alg = $jwkalg" -} - -graceful_exit() { # normal exit function. - clean_up - exit -} - -help_message() { # print out the help message - cat <<- _EOF_ - $PROGNAME ver. $VERSION - Obtain SSL certificates from the letsencrypt.org ACME server - - $(usage) - - Options: - -a, --all Check all certificates - -d, --debug Output debug information - -c, --create Create default config files - -f, --force Force renewal of cert (overrides expiry checks) - -h, --help Display this help message and exit - -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) - -Q, --mute Like -q, but also mute notification about successful upgrade - -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) - -u, --upgrade Upgrade getssl if a more recent version is available - -k, --keep "#" Maximum number of old getssl versions to keep when upgrading - -U, --nocheck Do not check if a more recent version is available - -w working_dir "Working directory" - - _EOF_ -} - -hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char -# printf -- "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" - echo -e -n "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" -} - -info() { # write out info as long as the quiet flag has not been set. - if [[ ${_QUIET} -eq 0 ]]; then - echo "$@" - fi -} +} json_awk() { # AWK json converter used for API2 - needs tidying up ;) # shellcheck disable=SC2086 -echo $1 | awk ' +echo "$1" | tr -d '\n' | awk ' { tokenize($0) # while(get_token()) {print TOKEN} if (0 == parse()) { @@ -1463,8 +1173,8 @@ function scream(msg) { } 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) + SPACE="[ \t\n]+" + gsub(/"[^\001-\037"\\]*((\\[^u\001-\037]|\\u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])[^\001-\037"\\]*)*"|-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?|null|false|true|[ \t\n]+|./, "\n&", a1) gsub("\n" SPACE, "\n", a1) sub(/^\n/, "", a1) ITOKENS=0 # get_token() helper @@ -1694,6 +1404,8 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p nonceproblem="true" while [[ "$nonceproblem" == "true" ]]; do + debug nonce "$nonce" + # Build header with just our public key and algorithm information header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}' @@ -1717,17 +1429,23 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p sign_string "$(printf '%s' "${protected64}.${payload64}")" "${ACCOUNT_KEY}" "$signalg" # Send header + extended header + payload + signature to the acme-server - debug "payload = $payload" 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 + 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 code="500" @@ -1756,12 +1474,6 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p debug response "$response" code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1) debug code "$code" - # Check for errors but ignore code 409 (Registration key is already in use) as we always try to register the account - if [[ "$code" == 4* && "$code" != "409" && $response != *"error:badNonce"* ]]; then - detail=$(echo "$response" | grep "detail") - error_exit "ACME server returned error: ${code}: ${detail}" - fi - if [[ $API -eq 1 ]]; then response_status=$(json_get "$response" status \ | head -1| awk -F'"' '{print $2}') @@ -1846,7 +1558,6 @@ sign_string() { # sign a string with a given key and algorithm and return urlbas elif [[ "${part2:0:4}" == "0242" ]]; then #sha512 S=$(echo "$part2" | cut -c 5-136) else - info "print ${str} | openssl dgst -$signalg -sign $key -hex" error_exit "error in EC signing couldn't get S from $signed" fi @@ -1891,10 +1602,7 @@ usage() { # echos out the program usage write_domain_template() { # write out a template file for a domain. cat > "$1" <<- _EOF_domain_ - # This file is read second (and per domain if running with the -a option) - # and overwrites any settings from the first file - # - # Uncomment and modify any variables you need + # Uncomment and modify any variables you need # see https://github.com/srvrco/getssl/wiki/Config-variables for details # see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs # @@ -1925,9 +1633,6 @@ write_domain_template() { # write out a template file for a domain. # 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge' # 'davs:davsuserid:davspassword:{DOMAIN}:443:/web/.well-known/acme-challenge') - # Specify SSH options, e.g. non standard port in SSH_OPTS - # SSH_OPTS=-p 12345 - #Set USE_SINGLE_ACL="true" to use a single ACL for all checks #USE_SINGLE_ACL="false" @@ -1954,9 +1659,7 @@ write_domain_template() { # write out a template file for a domain. write_getssl_template() { # write out the main template file cat > "$1" <<- _EOF_getssl_ - # This file is read first and is common to all domains - # - # Uncomment and modify any variables you need + # Uncomment and modify any variables you need # see https://github.com/srvrco/getssl/wiki/Config-variables for details # # The staging server is best for testing (hence set as default) @@ -2102,7 +1805,7 @@ if [[ $_REVOKE -eq 1 ]]; then fi # get latest agreement from CA (as default) -AGREEMENT=$(curl --user-agent "$CURL_USERAGENT" -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 @@ -2355,147 +2058,420 @@ if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then else info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate" fi - fi - else - info "${DOMAIN}: no certificate obtained from host" - fi - # end of .... if obtained a cert -fi -# end of .... check_remote is true then connect and obtain the current certificate + fi + else + info "${DOMAIN}: no certificate obtained from host" + fi + # end of .... if obtained a cert +fi +# end of .... check_remote is true then connect and obtain the current certificate + +# if there is an existing certificate file, check details. +if [[ -s "$CERT_FILE" ]]; then + debug "certificate $CERT_FILE exists" + enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-) + debug "local cert is valid until $enddate" + if [[ "$enddate" != "-" ]]; then + enddate_s=$(date_epoc "$enddate") + if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW -ne 1 ]]; then + issuer=$(openssl x509 -in "$CERT_FILE" -noout -issuer 2>/dev/null) + if [[ "$issuer" == *"Fake LE Intermediate"* ]] && [[ "$CA" == "https://acme-v02.api.letsencrypt.org" ]]; then + debug "upgrading from fake cert to real" + else + info "${DOMAIN}: certificate is valid for more than $RENEW_ALLOW days (until $enddate)" + # everything is OK, so exit. + graceful_exit + fi + else + debug "${DOMAIN}: certificate needs renewal" + fi + fi +fi +# end of .... if there is an existing certificate file, check details. + +if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then + errmsg="$DOMAIN due for renewal," + errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config" + error_exit "$errmsg" +fi + +# create account key if it doesn't exist. +if [[ -s "$ACCOUNT_KEY" ]]; then + debug "Account key exists at $ACCOUNT_KEY skipping generation" +else + info "creating account key $ACCOUNT_KEY" + create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" +fi + +# if not reusing private key, then remove the old keys +if [[ "$REUSE_PRIVATE_KEY" != "true" ]]; then + if [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; then + rm -f "$DOMAIN_DIR/${DOMAIN}.key" + fi + if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then + rm -f "$DOMAIN_DIR/${DOMAIN}.ecs.key" + fi +fi +# create new domain keys if they don't already exist +if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then + create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" +else + create_key "rsa" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" + create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.ec.key" "$DOMAIN_KEY_LENGTH" +fi +# End of creating domain keys. + +#create SAN +if [[ -z "$SANS" ]]; then + SANLIST="subjectAltName=DNS:${DOMAIN}" +elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then + SANLIST="subjectAltName=DNS:${SANS//,/,DNS:}" +else + SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//,/,DNS:}" +fi +debug "created SAN list = $SANLIST" + +#create CSR's +if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then + create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" +else + create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" + create_csr "$DOMAIN_DIR/${DOMAIN}.ec.csr" "$DOMAIN_DIR/${DOMAIN}.ec.key" +fi + +# use account key to register with CA +# currently the code registers every time, and gets an "already registered" back if it has been. +get_signing_params "$ACCOUNT_KEY" + +info "Registering account" +# send the request to the ACME server. +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 + 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 ...$responseHeaders ... $(json_get "$response" detail)" +fi +# end of registering account with CA + +# verify each domain +info "Verify each domain" + +# loop through domains for cert ( from SANS list) +if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then + 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::${#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 + info "Verifying $d" + if [[ "$USE_SINGLE_ACL" == "true" ]]; then + DOMAIN_ACL="${ACL[0]}" + else + DOMAIN_ACL="${ACL[$dn]}" + fi + + # request a challenge token from ACME server + 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 + send_signed_request "${AuthLink[$dn]}" "" + fi + + if [[ $response_status == "valid" ]]; then + info "$d is already validated" + if [[ "$DEACTIVATE_AUTH" == "true" ]]; then + deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")" + deactivate_url_list+=" $deactivate_url " + debug "url added to deactivate list ${deactivate_url}" + debug "deactivate list is now $deactivate_url_list" + fi + # increment domain-counter + ((dn++)) + else + PREVIOUSLY_VALIDATED="false" + if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification + 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 + 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" + + #create signed authorization key from token. + auth_key=$(printf '%s' "$keyauthorization" | openssl dgst -sha256 -binary \ + | openssl base64 -e \ + | tr -d '\n\r' \ + | sed -e 's:=*$::g' -e 'y:+/:-_:') + debug auth_key "$auth_key" + + debug "adding dns via command: $DNS_ADD_COMMAND $d $auth_key" + if ! eval "$DNS_ADD_COMMAND" "$d" "$auth_key" ; then + error_exit "DNS_ADD_COMMAND failed for domain $d" + fi + + # find a primary / authoritative DNS server for the domain + if [[ -z "$AUTH_DNS_SERVER" ]]; then + get_auth_dns "$d" + else + primary_ns="$AUTH_DNS_SERVER" + 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_ + + else # set up the correct http token for verification + 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 + send_signed_request "${AuthLink[$dn]}" "" + 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" | head -n1) + debug uri "$uri" + fi -# if there is an existing certificate file, check details. -if [[ -s "$CERT_FILE" ]]; then - debug "certificate $CERT_FILE exists" - enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-) - debug "local cert is valid until $enddate" - if [[ "$enddate" != "-" ]]; then - enddate_s=$(date_epoc "$enddate") - if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW -ne 1 ]]; then - issuer=$(openssl x509 -in "$CERT_FILE" -noout -issuer 2>/dev/null) - if [[ "$issuer" == *"Fake LE Intermediate"* ]] && [[ "$CA" == "https://acme-v02.api.letsencrypt.org" ]]; then - debug "upgrading from fake cert to real" + #create signed authorization key from token. + keyauthorization="$token.$thumbprint" + debug keyauthorization "$keyauthorization" + + # save variable into temporary file + echo -n "$keyauthorization" > "$TEMP_DIR/$token" + chmod 644 "$TEMP_DIR/$token" + + # copy to token to acme challenge location + umask 0022 + IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" + for t_loc in "${token_locations[@]}"; do + debug "copying file from $TEMP_DIR/$token to ${t_loc}" + copy_file_to_location "challenge token" \ + "$TEMP_DIR/$token" \ + "${t_loc}/$token" + done + umask "$ORIG_UMASK" + + wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}/.well-known/acme-challenge/$token" + debug wellknown_url "$wellknown_url" + + if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then + info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly" else - info "${DOMAIN}: certificate is valid for more than $RENEW_ALLOW days (until $enddate)" - # everything is OK, so exit. - graceful_exit + sleep "$HTTP_TOKEN_CHECK_WAIT" + # check that we can reach the challenge ourselves, if not, then error + 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 - else - debug "${DOMAIN}: certificate needs renewal" - fi - fi -fi -# end of .... if there is an existing certificate file, check details. - -if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then - errmsg="$DOMAIN due for renewal," - errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config" - error_exit "$errmsg" -fi -# create account key if it doesn't exist. -if [[ -s "$ACCOUNT_KEY" ]]; then - debug "Account key exists at $ACCOUNT_KEY skipping generation" -else - info "creating account key $ACCOUNT_KEY" - create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" -fi + check_challenge_completion "$uri" "$d" "$keyauthorization" -# if not reusing private key, then remove the old keys -if [[ "$REUSE_PRIVATE_KEY" != "true" ]]; then - if [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; then - rm -f "$DOMAIN_DIR/${DOMAIN}.key" - fi - if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then - rm -f "$DOMAIN_DIR/${DOMAIN}.ecs.key" + debug "remove token from ${DOMAIN_ACL}" + IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" + for t_loc in "${token_locations[@]}"; do + if [[ "${t_loc:0:4}" == "ssh:" ]] ; then + 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 $SSH_OPTS $sshhost ${command}" + # shellcheck disable=SC2029 + # 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" + ftpuser=$(echo "${t_loc}"| awk -F: '{print $2}') + ftppass=$(echo "${t_loc}"| awk -F: '{print $3}') + ftphost=$(echo "${t_loc}"| awk -F: '{print $4}') + ftplocn=$(echo "${t_loc}"| awk -F: '{print $5}') + debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost location=$ftplocn" + ftp -n <<- EOF + open $ftphost + user $ftpuser $ftppass + cd $ftplocn + delete ${token:?} + EOF + else + rm -f "${t_loc:?}/${token:?}" + fi + done + fi + # increment domain-counter + ((dn++)) fi -fi -# create new domain keys if they don't already exist -if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then - create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" -else - create_key "rsa" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" - create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.ec.key" "$DOMAIN_KEY_LENGTH" -fi -# End of creating domain keys. +done # end of ... loop through domains for cert ( from SANS list) -#create SAN -if [[ -z "$SANS" ]]; then - SANLIST="subjectAltName=DNS:${DOMAIN}" -elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - SANLIST="subjectAltName=DNS:${SANS//,/,DNS:}" -else - SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//,/,DNS:}" -fi -debug "created SAN list = $SANLIST" +# 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" -#create CSR's -if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then - create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" -else - create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" - create_csr "$DOMAIN_DIR/${DOMAIN}.ec.csr" "$DOMAIN_DIR/${DOMAIN}.ec.key" -fi + # 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.${d}" "${ns}" \ + | grep ^_acme -A2\ + | grep '"'|awk -F'"' '{ print $2}') + elif [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then + check_result=$($DNS_CHECK_FUNC TXT "_acme-challenge.${d}" "@${ns}" \ + | grep '300 IN TXT'|awk -F'"' '{ print $2}') + elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then + check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${d}" "${ns}" \ + | grep 'descriptive text'|awk -F'"' '{ print $2}') + else + check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ + | grep 'text ='|awk -F'"' '{ print $2}') + fi + debug "expecting $auth_key" + debug "${ns} gave ... $check_result" -# use account key to register with CA -# currently the code registers every time, and gets an "already registered" back if it has been. -get_signing_params "$ACCOUNT_KEY" + if [[ "$check_result" == *"$auth_key"* ]]; then + check_dns="success" + else + if [[ $ntries -lt 100 ]]; then + ntries=$(( ntries + 1 )) + info "checking DNS at ${ns} for ${d}. Attempt $ntries/100 gave wrong result, "\ + "waiting $DNS_WAIT secs before checking again" + sleep $DNS_WAIT + else + debug "dns check failed - removing existing value" + error_exit "checking _acme-challenge.${d} gave $check_result not $auth_key" + fi + fi + done + done + fi + done -info "Registering account" -# send the request to the ACME server. -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}' + 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 - 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 - 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 ...$responseHeaders ... $(json_get "$response" detail)" -fi -# end of registering account with CA -# verify each domain -info "Verify each domain" + # 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" -# loop through domains for cert ( from SANS list) -if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=${SANS//,/ } -else - alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") -fi + check_challenge_completion "$uri" "$d" "$keyauthorization" -if [[ $API -eq 2 ]]; then - create_order + debug "remove DNS entry" + eval "$DNS_DEL_COMMAND" "$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. -fulfill_challenges - -# Verification has been completed for all SANS, so request certificate. +# Verification has been completed for all SANS, so request certificate. info "Verification completed, obtaining certificate." #obtain the certificate. @@ -2503,12 +2479,6 @@ get_certificate "$DOMAIN_DIR/${DOMAIN}.csr" \ "$CERT_FILE" \ "$CA_CERT" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then - info "Creating order for EC certificate" - if [[ $API -eq 2 ]]; then - create_order - fulfill_challenges - fi - info "obtaining EC certificate." get_certificate "$DOMAIN_DIR/${DOMAIN}.ec.csr" \ "${CERT_FILE%.*}.ec.crt" \ "${CA_CERT%.*}.ec.crt" diff --git a/test/5-old-awk-error.bats b/test/5-old-awk-error.bats deleted file mode 100644 index 0f234a2..0000000 --- a/test/5-old-awk-error.bats +++ /dev/null @@ -1,24 +0,0 @@ -#! /usr/bin/env bats - -load '/bats-support/load.bash' -load '/bats-assert/load.bash' -load '/getssl/test/test_helper.bash' - - -# This is run for every test -setup() { - export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt -} - - -@test "Check getssl fails if an old version of awk is installed" { - CONFIG_FILE="getssl-http01.cfg" - # Make sure this test only runs on an image running an old version of awk - if [[ "$TEST_AWK" != "" ]]; then - setup_environment - init_getssl - create_certificate - assert_failure - assert_output "getssl: Your version of awk does not work with json_awk (see http://github.com/step-/JSON.awk/issues/6), please install a newer version of mawk or gawk" - fi -} diff --git a/test/Dockerfile-alpine b/test/Dockerfile-alpine new file mode 100644 index 0000000..ff69490 --- /dev/null +++ b/test/Dockerfile-alpine @@ -0,0 +1,22 @@ +FROM alpine:latest + +# Note this image uses busybox awk instead of gawk + +RUN apk --no-cache add supervisor openssl git curl bind-tools wget nginx bash + +WORKDIR /root + +# Create nginx directories in standard places +RUN mkdir /run/nginx +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core +RUN git clone https://github.com/jasonkarns/bats-support /bats-support +RUN git clone https://github.com/jasonkarns/bats-assert-1 /bats-assert +RUN /bats-core/install.sh /usr/local + +# Use supervisord to run nginx in the background +COPY ./test/alpine-supervisord.conf /etc/supervisord.conf +ENTRYPOINT /usr/bin/supervisord -c /etc/supervisord.conf diff --git a/test/Dockerfile-centos6 b/test/Dockerfile-centos6 index f370a0b..9149dad 100644 --- a/test/Dockerfile-centos6 +++ b/test/Dockerfile-centos6 @@ -1,5 +1,7 @@ FROM centos:centos6 +# Note this image uses gawk + # Update and install required software RUN yum -y update RUN yum -y install epel-release @@ -19,4 +21,4 @@ RUN /bats-core/install.sh /usr/local EXPOSE 80 443 # Run eternal loop - for testing -CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] +CMD tail -f /dev/null diff --git a/test/Dockerfile-ubuntu18-no-gawk b/test/Dockerfile-debian similarity index 67% rename from test/Dockerfile-ubuntu18-no-gawk rename to test/Dockerfile-debian index 809708a..c4c88a1 100644 --- a/test/Dockerfile-ubuntu18-no-gawk +++ b/test/Dockerfile-debian @@ -1,11 +1,17 @@ -FROM ubuntu:bionic -# bionic = latest 18 version +FROM debian:latest + +# Note this image uses mawk 1.3 # Update and install required software RUN apt-get update --fix-missing RUN apt-get install -y git curl dnsutils wget nginx-light WORKDIR /root +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private + +# Prevent "Can't load /root/.rnd into RNG" error from openssl +# RUN touch /root/.rnd # BATS (Bash Automated Testings) RUN git clone https://github.com/bats-core/bats-core.git /bats-core @@ -14,4 +20,4 @@ RUN git clone https://github.com/jasonkarns/bats-assert-1 /bats-assert RUN /bats-core/install.sh /usr/local # Run eternal loop - for testing -CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] +CMD tail -f /dev/null diff --git a/test/Dockerfile-ubuntu b/test/Dockerfile-ubuntu new file mode 100644 index 0000000..290100d --- /dev/null +++ b/test/Dockerfile-ubuntu @@ -0,0 +1,23 @@ +FROM ubuntu:latest + +# Note this image uses mawk1.3 + +# Update and install required software +RUN apt-get update --fix-missing +RUN apt-get install -y git curl dnsutils wget nginx-light +RUN apt-get install -y vim dos2unix # for debugging +# TODO test with drill, dig, host + +WORKDIR /root + +# Prevent "Can't load /root/.rnd into RNG" error from openssl +RUN touch /root/.rnd + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core +RUN git clone https://github.com/jasonkarns/bats-support /bats-support +RUN git clone https://github.com/jasonkarns/bats-assert-1 /bats-assert +RUN /bats-core/install.sh /usr/local + +# Run eternal loop - for testing +CMD tail -f /dev/null diff --git a/test/Dockerfile-ubuntu18 b/test/Dockerfile-ubuntu18 index 1b3765c..ebe7607 100644 --- a/test/Dockerfile-ubuntu18 +++ b/test/Dockerfile-ubuntu18 @@ -1,12 +1,11 @@ FROM ubuntu:bionic -# bionic = latest 18 version +# bionic = 18 LTS (long term support) + +# Note this image uses gawk # Update and install required software RUN apt-get update --fix-missing -# TODO work out why default version of awk fails -RUN apt-get install -y git curl dnsutils wget gawk nginx-light # linux-libc-dev make gcc binutils -RUN apt-get install -y vim dos2unix # for debugging -# TODO test with drill, dig, host +RUN apt-get install -y git curl dnsutils wget gawk nginx-light WORKDIR /root RUN mkdir /etc/nginx/pki @@ -25,4 +24,4 @@ RUN /bats-core/install.sh /usr/local EXPOSE 80 443 # Run eternal loop - for testing -CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] +CMD tail -f /dev/null diff --git a/test/alpine-supervisord.conf b/test/alpine-supervisord.conf new file mode 100644 index 0000000..8eec585 --- /dev/null +++ b/test/alpine-supervisord.conf @@ -0,0 +1,14 @@ +[supervisord] +nodaemon=true +logfile=/tmp/supervisord.log +childlogdir=/tmp +pidfile = /tmp/supervisord.pid + +[program:nginx] +command=nginx -g 'daemon off;' +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=false +startretries=0 diff --git a/test/restart-nginx b/test/restart-nginx new file mode 100644 index 0000000..d35f60f --- /dev/null +++ b/test/restart-nginx @@ -0,0 +1,8 @@ +#!/usr/bin/env bash + +if [ "$GETSSL_HOST" = "alpine.getssl.test" ]; then + killall -HUP nginx >&3- + sleep 5 +else + service nginx restart >&3- +fi diff --git a/test/run-all-tests.sh b/test/run-all-tests.sh index 7372e5b..b526c63 100644 --- a/test/run-all-tests.sh +++ b/test/run-all-tests.sh @@ -1,5 +1,7 @@ #!/usr/bin/env bash +docker exec -it getssl-alpine bats /getssl/test docker exec -it getssl-centos6 bats /getssl/test +docker exec -it getssl-debian bats /getssl/test +docker exec -it getssl-ubuntu bats /getssl/test docker exec -it getssl-ubuntu18 bats /getssl/test -docker exec -it getssl-ubuntu18-no-gawk bats /getssl/test/5-old-awk-error.bats diff --git a/test/test-config/getssl-dns01.cfg b/test/test-config/getssl-dns01.cfg index 790dee2..98637b0 100644 --- a/test/test-config/getssl-dns01.cfg +++ b/test/test-config/getssl-dns01.cfg @@ -2,35 +2,17 @@ # see https://github.com/srvrco/getssl/wiki/Config-variables for details # see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs # -# The staging server is best for testing -#CA="https://acme-staging.api.letsencrypt.org" -# This server issues full certificates, however has rate limits -#CA="https://acme-v01.api.letsencrypt.org" CA="https://pebble:14000/dir" VALIDATE_VIA_DNS=true DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" -# AUTH_DNS_SERVER=10.30.50.3 - -#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="" # 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/html/.well-known/acme-challenge') -# 'ssh:server5:/var/www/getssltest.hopto.org/web/.well-known/acme-challenge' -# 'ssh:sshuserid@server5:/var/www/getssltest.hopto.org/web/.well-known/acme-challenge' -# 'ftp:ftpuserid:ftppassword:getssltest.hopto.org:/web/.well-known/acme-challenge') #Set USE_SINGLE_ACL="true" to use a single ACL for all checks USE_SINGLE_ACL="false" @@ -44,11 +26,8 @@ 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="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && service nginx restart >&3-" +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" -# 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" +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-10-hosts.cfg b/test/test-config/getssl-http01-10-hosts.cfg index f521d52..d5c364e 100644 --- a/test/test-config/getssl-http01-10-hosts.cfg +++ b/test/test-config/getssl-http01-10-hosts.cfg @@ -5,7 +5,6 @@ CA="https://pebble:14000/dir" # 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="a.${GETSSL_HOST},b.${GETSSL_HOST},c.${GETSSL_HOST},d.${GETSSL_HOST},e.${GETSSL_HOST},f.${GETSSL_HOST},g.${GETSSL_HOST},h.${GETSSL_HOST},i.${GETSSL_HOST},j.${GETSSL_HOST},k.${GETSSL_HOST}" # Acme Challenge Location. @@ -22,7 +21,8 @@ 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="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && service nginx restart >&3-" +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" -#SERVER_TYPE="https" -#CHECK_REMOTE="true" +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01.cfg b/test/test-config/getssl-http01.cfg index 305dd49..f7d75ea 100644 --- a/test/test-config/getssl-http01.cfg +++ b/test/test-config/getssl-http01.cfg @@ -2,34 +2,13 @@ # see https://github.com/srvrco/getssl/wiki/Config-variables for details # see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs # -# The staging server is best for testing -#CA="https://acme-staging.api.letsencrypt.org" -# This server issues full certificates, however has rate limits -#CA="https://acme-v01.api.letsencrypt.org" CA="https://pebble:14000/dir" -#VALIDATE_VIA_DNS=true -#DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" -#DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" - -#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="" -# 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. +# Acme Challenge Location. ACL=('/var/www/html/.well-known/acme-challenge') -# 'ssh:server5:/var/www/getssltest.hopto.org/web/.well-known/acme-challenge' -# 'ssh:sshuserid@server5:/var/www/getssltest.hopto.org/web/.well-known/acme-challenge' -# 'ftp:ftpuserid:ftppassword:getssltest.hopto.org:/web/.well-known/acme-challenge') #Set USE_SINGLE_ACL="true" to use a single ACL for all checks USE_SINGLE_ACL="false" @@ -43,11 +22,8 @@ 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="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && service nginx restart >&3-" +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" -# 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 +# Define the server type and confirm correct certificate is installed SERVER_TYPE="https" CHECK_REMOTE="true" diff --git a/test/test_helper.bash b/test/test_helper.bash index 6f506fd..b33ee0b 100644 --- a/test/test_helper.bash +++ b/test/test_helper.bash @@ -18,8 +18,8 @@ setup_environment() { fi curl -X POST -d '{"host":"'"$GETSSL_HOST"'", "addresses":["'"$GETSSL_IP"'"]}' http://10.30.50.3:8055/add-a - cp ${CODE_DIR}/test/test-config/nginx-ubuntu-no-ssl ${NGINX_CONFIG} - service nginx restart >&3- + cp ${CODE_DIR}/test/test-config/nginx-ubuntu-no-ssl "${NGINX_CONFIG}" + /getssl/test/restart-nginx } @@ -40,5 +40,4 @@ create_certificate() { # Create certificate cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/${GETSSL_HOST}/getssl.cfg" run ${CODE_DIR}/getssl "$GETSSL_HOST" - #!FIXME test certificate has been placed in the expected location }