From bd0e674b1253535da688b98dabebd21e15b3dc25 Mon Sep 17 00:00:00 2001 From: Ian Driver Date: Sat, 14 Dec 2019 16:21:54 +0000 Subject: [PATCH 01/24] Only select first URL returned from acme-v02 --- getssl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/getssl b/getssl index ab560eb..2d3b69d 100755 --- a/getssl +++ b/getssl @@ -2248,7 +2248,7 @@ for d in $alldomains; do 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") + uri=$(json_get "$response" "challenges" "type" "http-01" "url" | head -n1) debug uri "$uri" fi From 7f1b94c6e2a9c1964461ae243681148b5694f631 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Mon, 30 Dec 2019 15:59:58 +0000 Subject: [PATCH 02/24] Fixes for HTTP-01 challenge for POST-as-GET --- getssl | 43 +++++++++++++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 14 deletions(-) diff --git a/getssl b/getssl index 78c10a3..80730c6 100755 --- a/getssl +++ b/getssl @@ -279,7 +279,7 @@ check_challenge_completion() { # checks with the ACME server if our challenge is keyauthorization=$3 debug "sending request to ACME server saying we're ready for challenge" - send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" + send_signed_request "$uri" "{}" # check response from our request to perform challenge if [[ $API -eq 1 ]]; then @@ -294,10 +294,8 @@ check_challenge_completion() { # checks with the ACME server if our challenge is # loop "forever" to keep checking for a response from the ACME server. while true ; do - debug "checking" - if ! get_cr "$uri" ; then - error_exit "$domain:Verify error:$code" - fi + debug "checking if challenge is complete" + send_signed_request "$uri" "" status=$(json_get "$response" status) @@ -853,10 +851,10 @@ get_certificate() { # get certificate for csr, if all domains validated. else # APIv2 send_signed_request "$FinalizeLink" "{\"csr\": \"$der\"}" "needbase64" debug "order link was $OrderLink" - cd=$(curl --user-agent "$CURL_USERAGENT" --silent "$OrderLink") - CertData=$(json_get "$cd" "certificate") + send_signed_request "$OrderLink" "" + CertData=$(json_get "$response" "certificate") debug "CertData is at $CertData" - curl --user-agent "$CURL_USERAGENT" --silent "$CertData" > "$FULL_CHAIN" + 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" @@ -1325,6 +1323,7 @@ set_server_type() { # uses SERVER_TYPE to set REMOTE_PORT and REMOTE_EXTRA REMOTE_PORT=636 elif [[ ${SERVER_TYPE} =~ ^[0-9]+$ ]]; then REMOTE_PORT=${SERVER_TYPE} + REMOTE_EXTRA="CUSTOM-HTTP-PORT" else info "${DOMAIN}: unknown server type \"$SERVER_TYPE\" in SERVER_TYPE" config_errors=true @@ -1335,6 +1334,7 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p url=$1 payload=$2 needbase64=$3 + outfile=$4 # save response into this file (certificate data) debug url "$url" @@ -1374,8 +1374,8 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p # Build another header which also contains the previously received nonce and encode it as urlbase64 if [[ $API -eq 1 ]]; then - protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' - protected64="$(printf '%s' "${protected}" | urlbase64)" + protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' + protected64="$(printf '%s' "${protected}" | urlbase64)" else # APIv2 if [[ -z "$KID" ]]; then debug "KID is blank, so using jwk" @@ -1384,7 +1384,6 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p else debug "using KID=${KID}" protected="{\"alg\": \"$jwkalg\", \"kid\": \"$KID\",\"nonce\": \"${nonce}\", \"url\": \"${url}\"}" - debug "protected = $protected" protected64="$(printf '%s' "${protected}" | urlbase64)" fi fi @@ -1415,13 +1414,22 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p code="500" loop_limit=5 while [[ "$code" -eq 500 ]]; do - if [[ "$needbase64" ]] ; then + if [[ "$outfile" ]] ; then + $CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" > $outfile + response=$(cat "$outfile") + elif [[ "$needbase64" ]] ; then response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" | urlbase64) else response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url") fi responseHeaders=$(cat "$CURL_HEADER") + if [[ "$needbase64" && ${response##*()} != "{"* ]]; then + # response is in base64 too, decode + #!FIXME need to use openssl base64 decoder if it exists + response=$(echo "$response" | base64 -d) + fi + debug responseHeaders "$responseHeaders" debug response "$response" code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1) @@ -1430,7 +1438,9 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p response_status=$(json_get "$response" status \ | head -1| awk -F'"' '{print $2}') else # APIv2 - if [[ ${response##*()} == "{"* ]]; then + if [[ "$output" && "$response" ]]; then + debug "response written to $outfile" + elif [[ ${response##*()} == "{"* ]]; then response_status=$(json_get "$response" status) else debug "response not in json format" @@ -1915,6 +1925,7 @@ else info "unknown API version" graceful_exit fi +debug "Using API v$API" # 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 @@ -2271,7 +2282,11 @@ for d in $alldomains; do done umask "$ORIG_UMASK" - wellknown_url="${CHALLENGE_CHECK_TYPE}://$d/.well-known/acme-challenge/$token" + if [[ "$REMOTE_EXTRA" = "CUSTOM-HTTP-PORT" ]]; then + wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}:${REMOTE_PORT}/.well-known/acme-challenge/$token" + else + wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}/.well-known/acme-challenge/$token" + fi debug wellknown_url "$wellknown_url" if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then From 5df6026c1bf9b9e6bd769e798568b9f171d8a2cd Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Mon, 30 Dec 2019 16:00:32 +0000 Subject: [PATCH 03/24] Test using pebble and docker --- docker-compose.yml | 33 +++++++ test/Dockerfile | 30 +++++++ test/run-test.sh | 9 ++ test/test-config/getssl-ubuntu.cfg | 49 +++++++++++ .../nginx-ubuntu-sites-enabled-default | 88 +++++++++++++++++++ 5 files changed, 209 insertions(+) create mode 100644 docker-compose.yml create mode 100644 test/Dockerfile create mode 100644 test/run-test.sh create mode 100644 test/test-config/getssl-ubuntu.cfg create mode 100644 test/test-config/nginx-ubuntu-sites-enabled-default diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..67e21a6 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3' +services: + pebble: + image: letsencrypt/pebble:latest + # TODO enable -strict + command: pebble -config /test/config/pebble-config.json + environment: + # with Go 1.13.x which defaults TLS 1.3 to on + GODEBUG: "tls13=1" + ports: + - 14000:14000 # HTTPS ACME API + - 15000:15000 # HTTPS Management API + networks: + acmenet: + ipv4_address: 10.30.50.2 + getssl: + build: + context: . + dockerfile: test/Dockerfile + container_name: getssl + volumes: + - .:/getssl + networks: + acmenet: + ipv4_address: 10.30.50.4 + +networks: + acmenet: + driver: bridge + ipam: + driver: default + config: + - subnet: 10.30.50.0/24 diff --git a/test/Dockerfile b/test/Dockerfile new file mode 100644 index 0000000..419d4d0 --- /dev/null +++ b/test/Dockerfile @@ -0,0 +1,30 @@ +FROM ubuntu:bionic +# bionic = latest 18 version + +# Update and install required software +RUN apt-get update +# TODO work out why default version of awk fails +RUN apt-get install -y git curl dnsutils wget linux-libc-dev make gcc binutils nginx-light gawk +RUN apt-get install -y vim dos2unix # for debugging +# TODO test with drill, dig, host + +WORKDIR /root +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-sites-enabled-default /etc/nginx/sites-enabled/default + +# BATS (Bash Automated Testings) +# RUN git clone https://github.com/bats-core/bats-core.git +# RUN bats-core/install.sh /usr/local + +COPY test/test-config/getssl-ubuntu.cfg getssl.cfg + +EXPOSE 80 443 + +# Run eternal loop - for testing +CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] + +# with Pebble +# docker-compose -f "test\docker-compose.yml" up -d --build +# docker exec -it test_getssl /bin/bash +# /getssl/test/run-test.sh diff --git a/test/run-test.sh b/test/run-test.sh new file mode 100644 index 0000000..5e0ba8d --- /dev/null +++ b/test/run-test.sh @@ -0,0 +1,9 @@ +#! /bin/sh + +wget --no-clobber https://raw.githubusercontent.com/letsencrypt/pebble/master/test/certs/pebble.minica.pem +export CURL_CA_BUNDLE=/root/pebble.minica.pem + +service nginx start +/getssl/getssl -c getssl +cp getssl.cfg /root/.getssl/getssl +/getssl/getssl getssl diff --git a/test/test-config/getssl-ubuntu.cfg b/test/test-config/getssl-ubuntu.cfg new file mode 100644 index 0000000..a4db20f --- /dev/null +++ b/test/test-config/getssl-ubuntu.cfg @@ -0,0 +1,49 @@ +# 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 +# +# 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" +SERVER_TYPE="5002" +#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" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="service nginx restart" + +# 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" diff --git a/test/test-config/nginx-ubuntu-sites-enabled-default b/test/test-config/nginx-ubuntu-sites-enabled-default new file mode 100644 index 0000000..fe02c8d --- /dev/null +++ b/test/test-config/nginx-ubuntu-sites-enabled-default @@ -0,0 +1,88 @@ +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# http://wiki.nginx.org/Pitfalls +# http://wiki.nginx.org/QuickStart +# http://wiki.nginx.org/Configuration +# +# Generally, you will want to move this file somewhere, and start with a clean +# file but keep this around for reference. Or just disable in sites-enabled. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +# Default server configuration +# +server { + listen 5002 default_server; + listen [::]:5002 default_server; + + # SSL configuration + # + listen 5001 ssl default_server; + listen [::]:5001 ssl default_server; + # + # Note: You should disable gzip for SSL traffic. + # See: https://bugs.debian.org/773332 + # + # Read up on ssl_ciphers to ensure a secure configuration. + # See: https://bugs.debian.org/765782 + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + # include snippets/snakeoil.conf; + + root /var/www/html; + + # Add index.php to the list if you are using PHP + index index.html index.htm index.nginx-debian.html; + + server_name _; + # ssl_certificate /etc/nginx/pki/server.crt; + # ssl_certificate_key /etc/nginx/pki/private/server.key; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + } + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # include snippets/fastcgi-php.conf; + # + # # With php7.0-cgi alone: + # fastcgi_pass 127.0.0.1:9000; + # # With php7.0-fpm: + # fastcgi_pass unix:/run/php/php7.0-fpm.sock; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} + + +# Virtual Host configuration for example.com +# +# You can move that to a different file under sites-available/ and symlink that +# to sites-enabled/ to enable it. +# +#server { +# listen 80; +# listen [::]:80; +# +# server_name example.com; +# +# root /var/www/example.com; +# index index.html; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} From 5e3d377342c36e7ba391c329539f515b93ce7269 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Mon, 30 Dec 2019 16:41:13 +0000 Subject: [PATCH 04/24] Fix a typo and shellcheck warning --- getssl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/getssl b/getssl index 80730c6..02340b0 100755 --- a/getssl +++ b/getssl @@ -1415,7 +1415,7 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p loop_limit=5 while [[ "$code" -eq 500 ]]; do if [[ "$outfile" ]] ; then - $CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" > $outfile + $CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" > "$outfile" response=$(cat "$outfile") elif [[ "$needbase64" ]] ; then response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" | urlbase64) @@ -1438,7 +1438,7 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p response_status=$(json_get "$response" status \ | head -1| awk -F'"' '{print $2}') else # APIv2 - if [[ "$output" && "$response" ]]; then + if [[ "$outfile" && "$response" ]]; then debug "response written to $outfile" elif [[ ${response##*()} == "{"* ]]; then response_status=$(json_get "$response" status) From 813276ee80df41d934a462ea6a89340d6215e108 Mon Sep 17 00:00:00 2001 From: Tsaukpaetra Date: Tue, 31 Dec 2019 02:53:02 -0700 Subject: [PATCH 05/24] Create dns_freedns.sh Should probably be fixed up, I've only hacked up @dkerr64's version for acme.sh to work on my local instance. It assumes you're using curl, I didn't bother stealing acme.sh's whole library of functions. DNS_ADD_COMMAND="~/.getssl/dns_freedns.sh add" DNS_DEL_COMMAND="~/.getssl/dns_freedns.sh rm" --- dns_scripts/dns_freedns.sh | 702 +++++++++++++++++++++++++++++++++++++ 1 file changed, 702 insertions(+) create mode 100644 dns_scripts/dns_freedns.sh diff --git a/dns_scripts/dns_freedns.sh b/dns_scripts/dns_freedns.sh new file mode 100644 index 0000000..a6571e1 --- /dev/null +++ b/dns_scripts/dns_freedns.sh @@ -0,0 +1,702 @@ +#!/usr/bin/env sh + +#This file name is "dns_freedns.sh" +#So, here must be a method dns_freedns_add() +#Which will be called by acme.sh to add the txt record to your api system. +#returns 0 means success, otherwise error. +# +#Author: David Kerr +#Report Bugs here: https://github.com/dkerr64/acme.sh +#or here... https://github.com/Neilpang/acme.sh/issues/2305 +# +######## Public functions ##################### + +# Export FreeDNS userid and password in following variables... +# FREEDNS_User=username +# FREEDNS_Password=password +# login cookie is saved in acme account config file so userid / pw +# need to be set only when changed. + +#Usage: dns_freedns_add _acme-challenge.www.domain.com "XKrxpRBosdIKFzxW_CT3KLZNf6q0HG9i01zxXp5CPBs" +dns_freedns_add() { + fulldomain="_acme-challenge.$1" + txtvalue="$2" + FREEDNS_COOKIE="$(cat $(dirname "$(readlink -f "$0")")/freednscookie.dat)" + + echo "Info: Add TXT record using FreeDNS" + #echo "Debug: fulldomain: $fulldomain" + #echo "Debug: txtvalue: $txtvalue" + + if [ -z "$FREEDNS_User" ] || [ -z "$FREEDNS_Password" ]; then + FREEDNS_User="" + FREEDNS_Password="" + if [ -z "$FREEDNS_COOKIE" ]; then + echo "ERROR: You did not specify the FreeDNS username and password yet." + echo "ERROR: Please export as FREEDNS_User / FREEDNS_Password and try again." + return 1 + fi + using_cached_cookies="true" + else + FREEDNS_COOKIE="$(_freedns_login "$FREEDNS_User" "$FREEDNS_Password")" + if [ -z "$FREEDNS_COOKIE" ]; then + return 1 + fi + using_cached_cookies="false" + fi + + #echo "Debug: FreeDNS login cookies: $FREEDNS_COOKIE (cached = $using_cached_cookies)" + + echo "$FREEDNS_COOKIE">$(dirname "$(readlink -f "$0")")/freednscookie.dat + + # We may have to cycle through the domain name to find the + # TLD that we own... + i=1 + wmax="$(echo "$fulldomain" | tr '.' ' ' | wc -w)" + while [ "$i" -lt "$wmax" ]; do + # split our full domain name into two parts... + sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")" + i="$(_math "$i" + 1)" + top_domain="$(echo "$fulldomain" | cut -d. -f "$i"-100)" + #echo "Debug: sub_domain: $sub_domain" + #echo "Debug: top_domain: $top_domain" + + DNSdomainid="$(_freedns_domain_id "$top_domain")" + if [ "$?" = "0" ]; then + echo "Info:Domain $top_domain found at FreeDNS, domain_id $DNSdomainid" + break + else + echo "Info:Domain $top_domain not found at FreeDNS, try with next level of TLD" + fi + done + + if [ -z "$DNSdomainid" ]; then + # If domain ID is empty then something went wrong (top level + # domain not found at FreeDNS). + echo "ERROR: Domain $top_domain not found at FreeDNS" + return 1 + fi + + # Add in new TXT record with the value provided + #echo "Debug: Adding TXT record for $fulldomain, $txtvalue" + _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" + return $? +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_freedns_rm() { + fulldomain="_acme-challenge.$1" + txtvalue="$2" + + echo "Info:Delete TXT record using FreeDNS" + #echo "Debug: fulldomain: $fulldomain" + #echo "Debug: txtvalue: $txtvalue" + + # Need to read cookie from conf file again in case new value set + # during login to FreeDNS when TXT record was created. + FREEDNS_COOKIE="$(cat $(dirname "$(readlink -f "$0")")/freednscookie.dat)" + #echo "Debug: FreeDNS login cookies: $FREEDNS_COOKIE" + + TXTdataid="$(_freedns_data_id "$fulldomain" "TXT")" + if [ "$?" != "0" ]; then + echo "Info:Cannot delete TXT record for $fulldomain, record does not exist at FreeDNS" + return 1 + fi + #echo "Debug: Data ID's found, $TXTdataid" + + # now we have one (or more) TXT record data ID's. Load the page + # for that record and search for the record txt value. If match + # then we can delete it. + lines="$(echo "$TXTdataid" | wc -l)" + #echo "Debug: Found $lines TXT data records for $fulldomain" + i=0 + while [ "$i" -lt "$lines" ]; do + i="$(_math "$i" + 1)" + dataid="$(echo "$TXTdataid" | sed -n "${i}p")" + #echo "Debug: $dataid" + + htmlpage="$(_freedns_retrieve_data_page "$FREEDNS_COOKIE" "$dataid")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + echo "ERROR: Has your FreeDNS username and password changed? If so..." + echo "ERROR: Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + echo "$htmlpage" | grep "value=\""$txtvalue"\"" >/dev/null + if [ "$?" = "0" ]; then + # Found a match... delete the record and return + echo "Info:Deleting TXT record for $fulldomain, $txtvalue" + _freedns_delete_txt_record "$FREEDNS_COOKIE" "$dataid" + return $? + fi + done + + # If we get this far we did not find a match + # Not necessarily an error, but log anyway. + echo "Info:Cannot delete TXT record for $fulldomain, $txtvalue. Does not exist at FreeDNS" + return 0 +} + +#################### Private functions below ################################## + +# usage: _freedns_login username password +# print string "cookie=value" etc. +# returns 0 success +_freedns_login() { + export _H1="Accept-Language:en-US" + username="$1" + password="$2" + url="https://freedns.afraid.org/zc.php?step=2" + + #echo "Debug: Login to FreeDNS as user $username" + data="username=$(printf '%s' "$username" | _url_encode)&password=$(printf '%s' "$password" | _url_encode)&submit=Login&action=auth" + #echo "$data" + + if [ -z "$HTTP_HEADER" ] || ! touch "$HTTP_HEADER"; then + HTTP_HEADER="$(_mktemp)" + fi + htmlpage="$(curl -L --silent --dump-header $HTTP_HEADER -X POST -H "$_H1" -H "$_H2" --data "$data" "$url")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS login failed for user $username bad RC from _post" + return 1 + fi + + cookies="$(grep -i '^Set-Cookie.*dns_cookie.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)" + + # if cookies is not empty then logon successful + if [ -z "$cookies" ]; then + #echo "Debug3: htmlpage: $htmlpage" + echo "ERROR: FreeDNS login failed for user $username. Check $HTTP_HEADER file" + return 1 + fi + + printf "%s" "$cookies" + return 0 +} + +# usage _freedns_retrieve_subdomain_page login_cookies +# echo page retrieved (html) +# returns 0 success +_freedns_retrieve_subdomain_page() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + url="https://freedns.afraid.org/subdomain/" + + #echo "Debug: Retrieve subdomain page from FreeDNS" + + htmlpage="$(curl -L --silent -H "$_H1" -H "$_H2" "$url")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS retrieve subdomains failed bad RC from _get" + return 1 + elif [ -z "$htmlpage" ]; then + echo "ERROR: FreeDNS returned empty subdomain page" + return 1 + fi + + #echo "Debug3: htmlpage: $htmlpage" + + printf "%s" "$htmlpage" + return 0 +} + +# usage _freedns_retrieve_data_page login_cookies data_id +# echo page retrieved (html) +# returns 0 success +_freedns_retrieve_data_page() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + data_id="$2" + url="https://freedns.afraid.org/subdomain/edit.php?data_id=$2" + + #echo "Debug: Retrieve data page for ID $data_id from FreeDNS" + + htmlpage="$(curl -L --silent -H "$_H1" -H "$_H2" "$url")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS retrieve data page failed bad RC from _get" + return 1 + elif [ -z "$htmlpage" ]; then + echo "ERROR: FreeDNS returned empty data page" + return 1 + fi + + #echo "Debug3: htmlpage: $htmlpage" + + printf "%s" "$htmlpage" + return 0 +} + +# usage _freedns_add_txt_record login_cookies domain_id subdomain value +# returns 0 success +_freedns_add_txt_record() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + domain_id="$2" + subdomain="$3" + value="$(printf '%s' "$4" | _url_encode)" + url="https://freedns.afraid.org/subdomain/save.php?step=2" + + if [ -z "$HTTP_HEADER" ] || ! touch "$HTTP_HEADER"; then + HTTP_HEADER="$(_mktemp)" + fi + htmlpage="$(curl -L --silent --dump-header $HTTP_HEADER -X POST -H "$_H1" -H "$_H2" --data "type=TXT&domain_id=$domain_id&subdomain=$subdomain&address=%22$value%22&send=Save%21" "$url")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS failed to add TXT record for $subdomain bad RC from _post" + return 1 + elif ! grep "200 OK" "$HTTP_HEADER" >/dev/null; then + #echo "Debug3: htmlpage: $(cat $HTTP_HEADER)" + echo "ERROR: FreeDNS failed to add TXT record for $subdomain. Check $HTTP_HEADER file" + return 1 + elif _contains "$htmlpage" "security code was incorrect"; then + #echo "Debug3: htmlpage: $htmlpage" + echo "ERROR: FreeDNS failed to add TXT record for $subdomain as FreeDNS requested security code" + echo "ERROR: Note that you cannot use automatic DNS validation for FreeDNS public domains" + return 1 + fi + + #echo "Debug3: htmlpage: $htmlpage" + echo "Info:Added acme challenge TXT record for $fulldomain at FreeDNS" + return 0 +} + +# usage _freedns_delete_txt_record login_cookies data_id +# returns 0 success +_freedns_delete_txt_record() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + data_id="$2" + url="https://freedns.afraid.org/subdomain/delete2.php" + + htmlheader="$(curl -L --silent -I -H "$_H1" -H "$_H2" "$url?data_id%5B%5D=$data_id&submit=delete+selected")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS failed to delete TXT record for $data_id bad RC from _get" + return 1 + elif ! _contains "$htmlheader" "200 OK"; then + #echo "Debug2: htmlheader: $htmlheader" + echo "ERROR: FreeDNS failed to delete TXT record $data_id" + return 1 + fi + + echo "Info:Deleted acme challenge TXT record for $fulldomain at FreeDNS" + return 0 +} + +# usage _freedns_domain_id domain_name +# echo the domain_id if found +# return 0 success +_freedns_domain_id() { + # Start by escaping the dots in the domain name + search_domain="$(echo "$1" | sed 's/\./\\./g')" + + # Sometimes FreeDNS does not return the subdomain page but rather + # returns a page regarding becoming a premium member. This usually + # happens after a period of inactivity. Immediately trying again + # returns the correct subdomain page. So, we will try twice to + # load the page and obtain our domain ID + attempts=2 + while [ "$attempts" -gt "0" ]; do + attempts="$(_math "$attempts" - 1)" + + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + echo "ERROR: Has your FreeDNS username and password changed? If so..." + echo "ERROR: Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's//@/g' | tr '@' '\n' \ + | grep "$search_domain\|$search_domain(.*)" \ + | sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' \ + | cut -d = -f 2)" + # The above beauty extracts domain ID from the html page... + # strip out all blank space and new lines. Then insert newlines + # before each table row + # search for the domain within each row (which may or may not have + # a text string in brackets (.*) after it. + # And finally extract the domain ID. + if [ -n "$domain_id" ]; then + printf "%s" "$domain_id" + return 0 + fi + #echo "Debug:Domain $search_domain not found. Retry loading subdomain page ($attempts attempts remaining)" + done + #echo "Debug:Domain $search_domain not found after retry" + return 1 +} + +# usage _freedns_data_id domain_name record_type +# echo the data_id(s) if found +# return 0 success +_freedns_data_id() { + # Start by escaping the dots in the domain name + search_domain="$(echo "$1" | sed 's/\./\\./g')" + record_type="$2" + + # Sometimes FreeDNS does not return the subdomain page but rather + # returns a page regarding becoming a premium member. This usually + # happens after a period of inactivity. Immediately trying again + # returns the correct subdomain page. So, we will try twice to + # load the page and obtain our domain ID + attempts=2 + while [ "$attempts" -gt "0" ]; do + attempts="$(_math "$attempts" - 1)" + + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + echo "ERROR: Has your FreeDNS username and password changed? If so..." + echo "ERROR: Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + data_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's//@/g' | tr '@' '\n' \ + | grep "$record_type" \ + | grep "$search_domain" \ + | sed -n 's/.*\(edit\.php?data_id=[0-9a-zA-Z]*\).*/\1/p' \ + | cut -d = -f 2)" + # The above beauty extracts data ID from the html page... + # strip out all blank space and new lines. Then insert newlines + # before each table row + # search for the record type withing each row (e.g. TXT) + # search for the domain within each row (which is within a + # anchor. And finally extract the domain ID. + if [ -n "$data_id" ]; then + printf "%s" "$data_id" + return 0 + fi + #echo "Debug:Domain $search_domain not found. Retry loading subdomain page ($attempts attempts remaining)" + done + #echo "Debug:Domain $search_domain not found after retry" + return 1 +} + +#### BEGIN things shamefully ripped from https://github.com/Neilpang/acme.sh/blob/master/acme.sh + +#_ascii_hex str +#this can only process ascii chars, should only be used when od command is missing as a backup way. +_ascii_hex() { + _debug2 "Using _ascii_hex" + _str="$1" + _str_len=${#_str} + _h_i=1 + while [ "$_h_i" -le "$_str_len" ]; do + _str_c="$(printf "%s" "$_str" | cut -c "$_h_i")" + printf " %02x" "'$_str_c" + _h_i="$(_math "$_h_i" + 1)" + done +} + +#stdin output hexstr splited by one space +#input:"abc" +#output: " 61 62 63" +_hex_dump() { + if _exists od; then + od -A n -v -t x1 | tr -s " " | sed 's/ $//' | tr -d "\r\t\n" + elif _exists hexdump; then + hexdump -v -e '/1 ""' -e '/1 " %02x" ""' + elif _exists xxd; then + xxd -ps -c 20 -i | sed "s/ 0x/ /g" | tr -d ",\n" | tr -s " " + else + str=$(cat) + _ascii_hex "$str" + fi +} + +#url encode, no-preserved chars +#A B C D E F G H I J K L M N O P Q R S T U V W X Y Z +#41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a + +#a b c d e f g h i j k l m n o p q r s t u v w x y z +#61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a + +#0 1 2 3 4 5 6 7 8 9 - _ . ~ +#30 31 32 33 34 35 36 37 38 39 2d 5f 2e 7e + +#stdin stdout +_url_encode() { + _hex_str=$(_hex_dump) + for _hex_code in $_hex_str; do + #upper case + case "${_hex_code}" in + "41") + printf "%s" "A" + ;; + "42") + printf "%s" "B" + ;; + "43") + printf "%s" "C" + ;; + "44") + printf "%s" "D" + ;; + "45") + printf "%s" "E" + ;; + "46") + printf "%s" "F" + ;; + "47") + printf "%s" "G" + ;; + "48") + printf "%s" "H" + ;; + "49") + printf "%s" "I" + ;; + "4a") + printf "%s" "J" + ;; + "4b") + printf "%s" "K" + ;; + "4c") + printf "%s" "L" + ;; + "4d") + printf "%s" "M" + ;; + "4e") + printf "%s" "N" + ;; + "4f") + printf "%s" "O" + ;; + "50") + printf "%s" "P" + ;; + "51") + printf "%s" "Q" + ;; + "52") + printf "%s" "R" + ;; + "53") + printf "%s" "S" + ;; + "54") + printf "%s" "T" + ;; + "55") + printf "%s" "U" + ;; + "56") + printf "%s" "V" + ;; + "57") + printf "%s" "W" + ;; + "58") + printf "%s" "X" + ;; + "59") + printf "%s" "Y" + ;; + "5a") + printf "%s" "Z" + ;; + + #lower case + "61") + printf "%s" "a" + ;; + "62") + printf "%s" "b" + ;; + "63") + printf "%s" "c" + ;; + "64") + printf "%s" "d" + ;; + "65") + printf "%s" "e" + ;; + "66") + printf "%s" "f" + ;; + "67") + printf "%s" "g" + ;; + "68") + printf "%s" "h" + ;; + "69") + printf "%s" "i" + ;; + "6a") + printf "%s" "j" + ;; + "6b") + printf "%s" "k" + ;; + "6c") + printf "%s" "l" + ;; + "6d") + printf "%s" "m" + ;; + "6e") + printf "%s" "n" + ;; + "6f") + printf "%s" "o" + ;; + "70") + printf "%s" "p" + ;; + "71") + printf "%s" "q" + ;; + "72") + printf "%s" "r" + ;; + "73") + printf "%s" "s" + ;; + "74") + printf "%s" "t" + ;; + "75") + printf "%s" "u" + ;; + "76") + printf "%s" "v" + ;; + "77") + printf "%s" "w" + ;; + "78") + printf "%s" "x" + ;; + "79") + printf "%s" "y" + ;; + "7a") + printf "%s" "z" + ;; + #numbers + "30") + printf "%s" "0" + ;; + "31") + printf "%s" "1" + ;; + "32") + printf "%s" "2" + ;; + "33") + printf "%s" "3" + ;; + "34") + printf "%s" "4" + ;; + "35") + printf "%s" "5" + ;; + "36") + printf "%s" "6" + ;; + "37") + printf "%s" "7" + ;; + "38") + printf "%s" "8" + ;; + "39") + printf "%s" "9" + ;; + "2d") + printf "%s" "-" + ;; + "5f") + printf "%s" "_" + ;; + "2e") + printf "%s" "." + ;; + "7e") + printf "%s" "~" + ;; + #other hex + *) + printf '%%%s' "$_hex_code" + ;; + esac + done +} + +_exists() { + cmd="$1" + if [ -z "$cmd" ]; then + _usage "Usage: _exists cmd" + return 1 + fi + + if eval type type >/dev/null 2>&1; then + eval type "$cmd" >/dev/null 2>&1 + elif command >/dev/null 2>&1; then + command -v "$cmd" >/dev/null 2>&1 + else + which "$cmd" >/dev/null 2>&1 + fi + ret="$?" + #echo "Debug3: $cmd exists=$ret" + return $ret +} + +_head_n() { + head -n "$1" +} + +_mktemp() { + if _exists mktemp; then + if mktemp 2>/dev/null; then + return 0 + elif _contains "$(mktemp 2>&1)" "-t prefix" && mktemp -t "$PROJECT_NAME" 2>/dev/null; then + #for Mac osx + return 0 + fi + fi + if [ -d "/tmp" ]; then + echo "/tmp/${PROJECT_NAME}wefADf24sf.$(_time).tmp" + return 0 + elif [ "$LE_TEMP_DIR" ] && mkdir -p "$LE_TEMP_DIR"; then + echo "/$LE_TEMP_DIR/wefADf24sf.$(_time).tmp" + return 0 + fi + _err "Can not create temp file." +} + +#a + b +_math() { + _m_opts="$@" + printf "%s" "$(($_m_opts))" +} + +_contains() { + _str="$1" + _sub="$2" + echo "$_str" | grep -- "$_sub" >/dev/null 2>&1 +} + +##Now actually do something with that function +case "$1" in + + add) + dns_freedns_add $2 $3 + ;; + rm) + dns_freedns_rm $2 $3 + ;; +esac From 5296a0716f4f3ebbca141785cbb55abdc4bdcb38 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Sat, 4 Jan 2020 10:20:52 +0000 Subject: [PATCH 06/24] Fix for RHEL6 --- getssl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/getssl b/getssl index 02340b0..0f1ef47 100755 --- a/getssl +++ b/getssl @@ -2131,7 +2131,7 @@ if [[ $API -eq 2 ]]; then for d in $alldomains; do dstring="${dstring}{\"type\":\"dns\",\"value\":\"$d\"}," done - dstring="${dstring: : -1}]" + 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)\"" From 2f3e5da3e81f18d95994120586b1d3c199b579e2 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Tue, 7 Jan 2020 13:02:45 +0000 Subject: [PATCH 07/24] Improved test script (http01 and dns01) --- dns_scripts/dns_add_challtestsrv | 7 ++ dns_scripts/dns_del_challtestsrv | 6 ++ docker-compose.yml | 12 ++- getssl | 14 +-- test/Dockerfile-rhel6 | 27 ++++++ test/{Dockerfile => Dockerfile-ubuntu} | 12 +-- test/run-test.sh | 46 +++++++++- test/test-config/getssl-dns01.cfg | 54 +++++++++++ .../{getssl-ubuntu.cfg => getssl-http01.cfg} | 8 +- ...es-enabled-default => nginx-ubuntu-no-ssl} | 9 +- test/test-config/nginx-ubuntu-ssl | 92 +++++++++++++++++++ 11 files changed, 263 insertions(+), 24 deletions(-) create mode 100644 dns_scripts/dns_add_challtestsrv create mode 100644 dns_scripts/dns_del_challtestsrv create mode 100644 test/Dockerfile-rhel6 rename test/{Dockerfile => Dockerfile-ubuntu} (61%) create mode 100644 test/test-config/getssl-dns01.cfg rename test/test-config/{getssl-ubuntu.cfg => getssl-http01.cfg} (91%) rename test/test-config/{nginx-ubuntu-sites-enabled-default => nginx-ubuntu-no-ssl} (93%) create mode 100644 test/test-config/nginx-ubuntu-ssl diff --git a/dns_scripts/dns_add_challtestsrv b/dns_scripts/dns_add_challtestsrv new file mode 100644 index 0000000..601bcfc --- /dev/null +++ b/dns_scripts/dns_add_challtestsrv @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Simple script to update the challtestserv mock DNS server when testing DNS responses + +fulldomain="${1}" +token="${2}" + +curl -X POST -d "{\"host\":\"_acme-challenge.${fulldomain}.\", \"value\": \"${token}\"}" http://10.30.50.3:8055/set-txt diff --git a/dns_scripts/dns_del_challtestsrv b/dns_scripts/dns_del_challtestsrv new file mode 100644 index 0000000..832b136 --- /dev/null +++ b/dns_scripts/dns_del_challtestsrv @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# Simple script to update the challtestserv mock DNS server when testing DNS responses + +fulldomain="${1}" + +curl -X POST -d "{\"host\":\"_acme-challenge.${fulldomain}.\"}" http://10.30.50.3:8055/clear-txt diff --git a/docker-compose.yml b/docker-compose.yml index 67e21a6..b770b44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,7 +3,7 @@ services: pebble: image: letsencrypt/pebble:latest # TODO enable -strict - command: pebble -config /test/config/pebble-config.json + command: pebble -config /test/config/pebble-config.json -dnsserver 10.30.50.3:8053 environment: # with Go 1.13.x which defaults TLS 1.3 to on GODEBUG: "tls13=1" @@ -13,10 +13,18 @@ services: networks: acmenet: ipv4_address: 10.30.50.2 + challtestsrv: + image: letsencrypt/pebble-challtestsrv:latest + command: pebble-challtestsrv -defaultIPv6 "" -defaultIPv4 10.30.50.3 + ports: + - 8055:8055 # HTTP Management API + networks: + acmenet: + ipv4_address: 10.30.50.3 getssl: build: context: . - dockerfile: test/Dockerfile + dockerfile: test/Dockerfile-ubuntu container_name: getssl volumes: - .:/getssl diff --git a/getssl b/getssl index 0f1ef47..991f6d8 100755 --- a/getssl +++ b/getssl @@ -288,7 +288,8 @@ check_challenge_completion() { # checks with the ACME server if our challenge is fi else # APIv2 if [[ -n "$code" ]] && [[ ! "$code" == '200' ]] ; then - error_exit "$domain:Challenge error: $code" + detail=$(json_get "$response" detail) + error_exit "$domain:Challenge error: $code:Detail: $detail" fi fi @@ -1323,7 +1324,6 @@ set_server_type() { # uses SERVER_TYPE to set REMOTE_PORT and REMOTE_EXTRA REMOTE_PORT=636 elif [[ ${SERVER_TYPE} =~ ^[0-9]+$ ]]; then REMOTE_PORT=${SERVER_TYPE} - REMOTE_EXTRA="CUSTOM-HTTP-PORT" else info "${DOMAIN}: unknown server type \"$SERVER_TYPE\" in SERVER_TYPE" config_errors=true @@ -2282,11 +2282,7 @@ for d in $alldomains; do done umask "$ORIG_UMASK" - if [[ "$REMOTE_EXTRA" = "CUSTOM-HTTP-PORT" ]]; then - wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}:${REMOTE_PORT}/.well-known/acme-challenge/$token" - else - wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}/.well-known/acme-challenge/$token" - fi + wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}/.well-known/acme-challenge/$token" debug wellknown_url "$wellknown_url" if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then @@ -2522,6 +2518,8 @@ fi if [[ ${CHECK_REMOTE} == "true" ]]; then sleep "$CHECK_REMOTE_WAIT" # shellcheck disable=SC2086 + debug openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} + CERT_REMOTE=$(echo \ | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \ | openssl x509 -noout -fingerprint 2>/dev/null) @@ -2529,6 +2527,8 @@ if [[ ${CHECK_REMOTE} == "true" ]]; then if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then info "${DOMAIN} - certificate installed OK on server" else + debug Fingerprint on server ${CERT_REMOTE} + debug Fingerprint in file ${CERT_LOCAL} error_exit "${DOMAIN} - certificate obtained but certificate on server is different from the new certificate" fi fi diff --git a/test/Dockerfile-rhel6 b/test/Dockerfile-rhel6 new file mode 100644 index 0000000..5ebb278 --- /dev/null +++ b/test/Dockerfile-rhel6 @@ -0,0 +1,27 @@ +FROM roboxes/rhel6 +# FROM centos:centos6 +# bionic = latest 18 version + +# Update and install required software +RUN yum -y update +RUN yum -y install epel-release +RUN yum -y install git curl dnsutils wget # nginx-light + +WORKDIR /root +#RUN mkdir /etc/nginx/pki +#RUN mkdir /etc/nginx/pki/private +#COPY ./test/test-config/nginx-ubuntu-sites-enabled-default /etc/nginx/sites-enabled/default + +# BATS (Bash Automated Testings) +# RUN git clone https://github.com/bats-core/bats-core.git +# RUN bats-core/install.sh /usr/local + +EXPOSE 80 443 + +# Run eternal loop - for testing +CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] + +# with Pebble +# docker-compose -f "docker-compose.yml" up -d --build +# docker exec -it getssl /bin/bash +# /getssl/test/run-test.sh diff --git a/test/Dockerfile b/test/Dockerfile-ubuntu similarity index 61% rename from test/Dockerfile rename to test/Dockerfile-ubuntu index 419d4d0..7f1a8e5 100644 --- a/test/Dockerfile +++ b/test/Dockerfile-ubuntu @@ -1,30 +1,28 @@ -FROM ubuntu:bionic +FROM ubuntu:xenial # bionic = latest 18 version # Update and install required software RUN apt-get update # TODO work out why default version of awk fails -RUN apt-get install -y git curl dnsutils wget linux-libc-dev make gcc binutils nginx-light gawk +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 WORKDIR /root RUN mkdir /etc/nginx/pki RUN mkdir /etc/nginx/pki/private -COPY ./test/test-config/nginx-ubuntu-sites-enabled-default /etc/nginx/sites-enabled/default +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default # BATS (Bash Automated Testings) # RUN git clone https://github.com/bats-core/bats-core.git # RUN bats-core/install.sh /usr/local -COPY test/test-config/getssl-ubuntu.cfg getssl.cfg - EXPOSE 80 443 # Run eternal loop - for testing CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] # with Pebble -# docker-compose -f "test\docker-compose.yml" up -d --build -# docker exec -it test_getssl /bin/bash +# docker-compose -f "docker-compose.yml" up -d --build +# docker exec -it getssl /bin/bash # /getssl/test/run-test.sh diff --git a/test/run-test.sh b/test/run-test.sh index 5e0ba8d..e93ae89 100644 --- a/test/run-test.sh +++ b/test/run-test.sh @@ -1,9 +1,47 @@ -#! /bin/sh +#! /bin/bash + +set -e + +# Test setup +rm -r /root/.getssl wget --no-clobber https://raw.githubusercontent.com/letsencrypt/pebble/master/test/certs/pebble.minica.pem -export CURL_CA_BUNDLE=/root/pebble.minica.pem +# cat /etc/pki/tls/certs/ca-bundle.crt /root/pebble.minica.pem > /root/pebble-ca-bundle.crt +cat /etc/ssl/certs/ca-certificates.crt /root/pebble.minica.pem > /root/pebble-ca-bundle.crt +export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + +curl -X POST -d '{"host":"getssl", "addresses":["10.30.50.4"]}' http://10.30.50.3:8055/add-a + +# Test #1 - http-01 verification +echo Test \#1 - http-01 verification -service nginx start +cp /getssl/test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default +service nginx restart /getssl/getssl -c getssl -cp getssl.cfg /root/.getssl/getssl +cp /getssl/test/test-config/getssl-http01.cfg /root/.getssl/getssl/getssl.cfg +/getssl/getssl -f getssl + +# Test #2 - http-01 forced renewal +echo Test \#2 - http-01 forced renewal + +sleep 5 # There's a race condition if renew too soon (authlink returns "valid" instead of "pending") +/getssl/getssl getssl -f + +# Test cleanup + +rm -r /root/.getssl + +# Test #3 - dns-01 verification +echo Test \#3 - dns-01 verification + +cp /getssl/test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default +service nginx restart +/getssl/getssl -c getssl +cp /getssl/test/test-config/getssl-dns01.cfg /root/.getssl/getssl/getssl.cfg /getssl/getssl getssl + +# Test #4 - dns-01 forced renewal +echo Test \#4 - dns-01 forced renewal + +sleep 5 # There's a race condition if renew too soon (authlink returns "valid" instead of "pending") +/getssl/getssl getssl -f diff --git a/test/test-config/getssl-dns01.cfg b/test/test-config/getssl-dns01.cfg new file mode 100644 index 0000000..49c58b5 --- /dev/null +++ b/test/test-config/getssl-dns01.cfg @@ -0,0 +1,54 @@ +# 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 +# +# 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" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl /etc/nginx/sites-enabled/default && service nginx restart" + +# 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" diff --git a/test/test-config/getssl-ubuntu.cfg b/test/test-config/getssl-http01.cfg similarity index 91% rename from test/test-config/getssl-ubuntu.cfg rename to test/test-config/getssl-http01.cfg index a4db20f..f3dc5ad 100644 --- a/test/test-config/getssl-ubuntu.cfg +++ b/test/test-config/getssl-http01.cfg @@ -7,7 +7,11 @@ # This server issues full certificates, however has rate limits #CA="https://acme-v01.api.letsencrypt.org" CA="https://pebble:14000/dir" -SERVER_TYPE="5002" + +#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 @@ -39,7 +43,7 @@ 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="service nginx restart" +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl /etc/nginx/sites-enabled/default && service nginx restart" # 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 diff --git a/test/test-config/nginx-ubuntu-sites-enabled-default b/test/test-config/nginx-ubuntu-no-ssl similarity index 93% rename from test/test-config/nginx-ubuntu-sites-enabled-default rename to test/test-config/nginx-ubuntu-no-ssl index fe02c8d..c78d646 100644 --- a/test/test-config/nginx-ubuntu-sites-enabled-default +++ b/test/test-config/nginx-ubuntu-no-ssl @@ -14,13 +14,18 @@ # Default server configuration # server { + listen 80 default_server; listen 5002 default_server; listen [::]:5002 default_server; # SSL configuration # - listen 5001 ssl default_server; - listen [::]:5001 ssl default_server; + listen 443 default_server; + listen [::]:443 default_server; + + listen 5001 default_server; + listen [::]:5001 default_server; + # # Note: You should disable gzip for SSL traffic. # See: https://bugs.debian.org/773332 diff --git a/test/test-config/nginx-ubuntu-ssl b/test/test-config/nginx-ubuntu-ssl new file mode 100644 index 0000000..9f79407 --- /dev/null +++ b/test/test-config/nginx-ubuntu-ssl @@ -0,0 +1,92 @@ +## +# You should look at the following URL's in order to grasp a solid understanding +# of Nginx configuration files in order to fully unleash the power of Nginx. +# http://wiki.nginx.org/Pitfalls +# http://wiki.nginx.org/QuickStart +# http://wiki.nginx.org/Configuration +# +# Generally, you will want to move this file somewhere, and start with a clean +# file but keep this around for reference. Or just disable in sites-enabled. +# +# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. +## + +# Default server configuration +# +server { + listen 80 default_server; + listen 5002 default_server; + listen [::]:5002 default_server; + + # SSL configuration + # + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + listen 5001 ssl default_server; + listen [::]:5001 ssl default_server; + # + # Note: You should disable gzip for SSL traffic. + # See: https://bugs.debian.org/773332 + # + # Read up on ssl_ciphers to ensure a secure configuration. + # See: https://bugs.debian.org/765782 + # + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + # + # include snippets/snakeoil.conf; + + root /var/www/html; + + # Add index.php to the list if you are using PHP + index index.html index.htm index.nginx-debian.html; + + server_name _; + ssl_certificate /etc/nginx/pki/server.crt; + ssl_certificate_key /etc/nginx/pki/private/server.key; + + location / { + # First attempt to serve request as file, then + # as directory, then fall back to displaying a 404. + try_files $uri $uri/ =404; + } + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # include snippets/fastcgi-php.conf; + # + # # With php7.0-cgi alone: + # fastcgi_pass 127.0.0.1:9000; + # # With php7.0-fpm: + # fastcgi_pass unix:/run/php/php7.0-fpm.sock; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} + + +# Virtual Host configuration for example.com +# +# You can move that to a different file under sites-available/ and symlink that +# to sites-enabled/ to enable it. +# +#server { +# listen 80; +# listen [::]:80; +# +# server_name example.com; +# +# root /var/www/example.com; +# index index.html; +# +# location / { +# try_files $uri $uri/ =404; +# } +#} From 3075c30faa4ba3df905ea5df7913c0ff555257b6 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Tue, 7 Jan 2020 13:10:57 +0000 Subject: [PATCH 08/24] Update revision history --- getssl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/getssl b/getssl index 991f6d8..5808f42 100755 --- a/getssl +++ b/getssl @@ -189,6 +189,11 @@ # 2019-10-02 issue #425 Case insensitive processing of agreement url because of HTTP/2 (2.12) # 2019-10-07 update DNS checks to allow use of CNAMEs (2.13) # 2019-11-18 Rebased master onto APIv2 and added Content-Type: application/jose+json (2.14) +# 2019-11-20 #453 and #454 Add User-Agent to all curl requests +# 2019-11-22 #456 Fix shellcheck issues +# 2019-11-23 #459 Fix missing chain.crt +# 2019-12-18 #462 Use POST-as-GET for ACMEv2 endpoints +# 2020-01-07 #464 and #486 "json was blank" (change all curl request to use POST-as-GET) (2.15) # ---------------------------------------------------------------------------------------- PROGNAME=${0##*/} @@ -2518,8 +2523,6 @@ fi if [[ ${CHECK_REMOTE} == "true" ]]; then sleep "$CHECK_REMOTE_WAIT" # shellcheck disable=SC2086 - debug openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} - CERT_REMOTE=$(echo \ | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \ | openssl x509 -noout -fingerprint 2>/dev/null) @@ -2527,8 +2530,6 @@ if [[ ${CHECK_REMOTE} == "true" ]]; then if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then info "${DOMAIN} - certificate installed OK on server" else - debug Fingerprint on server ${CERT_REMOTE} - debug Fingerprint in file ${CERT_LOCAL} error_exit "${DOMAIN} - certificate obtained but certificate on server is different from the new certificate" fi fi From a5313f4f31d20165eaf0e7b2d41c91f26b316b62 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Wed, 8 Jan 2020 21:51:07 +0000 Subject: [PATCH 09/24] error_exit if rate limited and exit if curl returns nothing --- getssl | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/getssl b/getssl index fbfabaa..f0b31f5 100755 --- a/getssl +++ b/getssl @@ -293,7 +293,7 @@ check_challenge_completion() { # checks with the ACME server if our challenge is fi else # APIv2 if [[ -n "$code" ]] && [[ ! "$code" == '200' ]] ; then - detail=$(json_get "$response" detail) + detail=$(echo "$response" | grep "detail" | awk -F\" '{print $4}') error_exit "$domain:Challenge error: $code:Detail: $detail" fi fi @@ -1428,6 +1428,10 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url") fi + if [[ "$response" == "" ]]; then + error_exit "ERROR curl \"$url\" returned nothing" + fi + responseHeaders=$(cat "$CURL_HEADER") if [[ "$needbase64" && ${response##*()} != "{"* ]]; then # response is in base64 too, decode @@ -1462,6 +1466,9 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p error_exit "500 error from ACME server: $response" fi fi + if [[ "$code" -eq 429 ]]; then + error_exit "429 rate limited error from ACME server" + fi done if [[ $response == *"error:badNonce"* ]]; then debug "bad nonce" From 2dbaf3e14d1d65dca263dc49ec235334bab4c022 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Fri, 10 Jan 2020 19:24:57 +0000 Subject: [PATCH 10/24] Update templates, clean up test code --- getssl | 12 +++++++----- test/Dockerfile-rhel6 | 5 ----- test/Dockerfile-ubuntu | 5 ----- test/README.md | 20 ++++++++++++++++++++ test/run-test.sh | 14 ++++++++++---- 5 files changed, 37 insertions(+), 19 deletions(-) create mode 100644 test/README.md diff --git a/getssl b/getssl index f0b31f5..5d46286 100755 --- a/getssl +++ b/getssl @@ -193,7 +193,9 @@ # 2019-11-22 #456 Fix shellcheck issues # 2019-11-23 #459 Fix missing chain.crt # 2019-12-18 #462 Use POST-as-GET for ACMEv2 endpoints -# 2020-01-07 #464 and #486 "json was blank" (change all curl request to use POST-as-GET) (2.15) +# 2020-01-07 #464 and #486 "json was blank" (change all curl request to use POST-as-GET) +# 2020-01-08 Error and exit if rate limited, exit if curl returns nothing +# 2020-01-10 Change domain and getssl templates to v2 (2.15) # ---------------------------------------------------------------------------------------- PROGNAME=${0##*/} @@ -212,7 +214,7 @@ CODE_LOCATION="https://raw.githubusercontent.com/srvrco/getssl/master/getssl" CSR_SUBJECT="/" CURL_USERAGENT="${PROGNAME}/${VERSION}" DEACTIVATE_AUTH="false" -DEFAULT_REVOKE_CA="https://acme-v01.api.letsencrypt.org" +DEFAULT_REVOKE_CA="https://acme-v02.api.letsencrypt.org" DNS_EXTRA_WAIT="" DNS_WAIT=10 DOMAIN_KEY_LENGTH=4096 @@ -1566,7 +1568,7 @@ write_domain_template() { # write out a template file for a domain. # The staging server is best for testing #CA="https://acme-staging-v02.api.letsencrypt.org/directory" # This server issues full certificates, however has rate limits - #CA="https://acme-v01.api.letsencrypt.org" + #CA="https://acme-v02.api.letsencrypt.org" #PRIVATE_KEY_ALG="rsa" @@ -1619,7 +1621,7 @@ write_getssl_template() { # write out the main template file # The staging server is best for testing (hence set as default) CA="https://acme-staging-v02.api.letsencrypt.org/directory" # This server issues full certificates, however has rate limits - #CA="https://acme-v01.api.letsencrypt.org" + #CA="https://acme-v02.api.letsencrypt.org" #AGREEMENT="$AGREEMENT" @@ -2021,7 +2023,7 @@ if [[ -s "$CERT_FILE" ]]; 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-v01.api.letsencrypt.org" ]]; then + 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)" diff --git a/test/Dockerfile-rhel6 b/test/Dockerfile-rhel6 index 5ebb278..019da84 100644 --- a/test/Dockerfile-rhel6 +++ b/test/Dockerfile-rhel6 @@ -20,8 +20,3 @@ EXPOSE 80 443 # Run eternal loop - for testing CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] - -# with Pebble -# docker-compose -f "docker-compose.yml" up -d --build -# docker exec -it getssl /bin/bash -# /getssl/test/run-test.sh diff --git a/test/Dockerfile-ubuntu b/test/Dockerfile-ubuntu index 7f1a8e5..b0f09f8 100644 --- a/test/Dockerfile-ubuntu +++ b/test/Dockerfile-ubuntu @@ -21,8 +21,3 @@ EXPOSE 80 443 # Run eternal loop - for testing CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] - -# with Pebble -# docker-compose -f "docker-compose.yml" up -d --build -# docker exec -it getssl /bin/bash -# /getssl/test/run-test.sh diff --git a/test/README.md b/test/README.md new file mode 100644 index 0000000..9d0aedd --- /dev/null +++ b/test/README.md @@ -0,0 +1,20 @@ +# Testing + +This directory contains a simple test script which tests creating certificates with Pebble (testing version of the LetsEncrypt server) + +Start up pebble, the challdnstest server for DNS challenges +`docker-compose -f "docker-compose.yml" up -d --build` + +Run the tests +`docker exec -it getssl /getssl/test/run-test.sh` + +Debug (need to set CURL_CA_BUNDLE as pebble uses a local certificate, otherwise you get a "unknown API version" error) +`docker exec -it getssl /bin/bash` +`export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt` +`/getssl/getssl -d getssl` + +# TODO +1. Move to BATS (bash automated testing) instead of run-test.sh +2. Test RHEL6, Debian as well +3. Test SSH, SFTP +4. Test wildcards diff --git a/test/run-test.sh b/test/run-test.sh index e93ae89..b983899 100644 --- a/test/run-test.sh +++ b/test/run-test.sh @@ -3,7 +3,9 @@ set -e # Test setup -rm -r /root/.getssl +if [[ -d /root/.getssl ]]; then + rm -r /root/.getssl +fi wget --no-clobber https://raw.githubusercontent.com/letsencrypt/pebble/master/test/certs/pebble.minica.pem # cat /etc/pki/tls/certs/ca-bundle.crt /root/pebble.minica.pem > /root/pebble-ca-bundle.crt @@ -24,11 +26,12 @@ cp /getssl/test/test-config/getssl-http01.cfg /root/.getssl/getssl/getssl.cfg # Test #2 - http-01 forced renewal echo Test \#2 - http-01 forced renewal -sleep 5 # There's a race condition if renew too soon (authlink returns "valid" instead of "pending") +# There's a race condition if renew too soon (authlink returns "valid" instead of "pending") +echo Sleeping 20s to allow previous validation to expire +sleep 20 /getssl/getssl getssl -f # Test cleanup - rm -r /root/.getssl # Test #3 - dns-01 verification @@ -43,5 +46,8 @@ cp /getssl/test/test-config/getssl-dns01.cfg /root/.getssl/getssl/getssl.cfg # Test #4 - dns-01 forced renewal echo Test \#4 - dns-01 forced renewal -sleep 5 # There's a race condition if renew too soon (authlink returns "valid" instead of "pending") +# There's a race condition if renew too soon (authlink returns "valid" instead of "pending") +echo Sleeping 30s to allow previous validation to expire +sleep 30 + /getssl/getssl getssl -f From 9895a211d83beb39bfabffde1f034cfaac7b190c Mon Sep 17 00:00:00 2001 From: srvrco Date: Mon, 13 Jan 2020 09:28:03 +0000 Subject: [PATCH 11/24] correcting version number --- getssl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/getssl b/getssl index 5d46286..e52b3d0 100755 --- a/getssl +++ b/getssl @@ -199,7 +199,7 @@ # ---------------------------------------------------------------------------------------- PROGNAME=${0##*/} -VERSION="2.14" +VERSION="2.15" # defaults ACCOUNT_KEY_LENGTH=4096 From 8203c38b364e28e42dd6951286a70a0df63111d8 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Thu, 16 Jan 2020 11:48:02 +0000 Subject: [PATCH 12/24] Disable auth reuse to fix force-renew tests --- docker-compose.yml | 2 ++ test/run-test.sh | 10 ---------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index b770b44..f4b3567 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,6 +7,8 @@ services: environment: # with Go 1.13.x which defaults TLS 1.3 to on GODEBUG: "tls13=1" + # don't reuse authorizations (breaks testing force renew) + PEBBLE_AUTHZREUSE: 0 ports: - 14000:14000 # HTTPS ACME API - 15000:15000 # HTTPS Management API diff --git a/test/run-test.sh b/test/run-test.sh index b983899..8051922 100644 --- a/test/run-test.sh +++ b/test/run-test.sh @@ -25,10 +25,6 @@ cp /getssl/test/test-config/getssl-http01.cfg /root/.getssl/getssl/getssl.cfg # Test #2 - http-01 forced renewal echo Test \#2 - http-01 forced renewal - -# There's a race condition if renew too soon (authlink returns "valid" instead of "pending") -echo Sleeping 20s to allow previous validation to expire -sleep 20 /getssl/getssl getssl -f # Test cleanup @@ -36,7 +32,6 @@ rm -r /root/.getssl # Test #3 - dns-01 verification echo Test \#3 - dns-01 verification - cp /getssl/test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default service nginx restart /getssl/getssl -c getssl @@ -45,9 +40,4 @@ cp /getssl/test/test-config/getssl-dns01.cfg /root/.getssl/getssl/getssl.cfg # Test #4 - dns-01 forced renewal echo Test \#4 - dns-01 forced renewal - -# There's a race condition if renew too soon (authlink returns "valid" instead of "pending") -echo Sleeping 30s to allow previous validation to expire -sleep 30 - /getssl/getssl getssl -f From f17590af52c0db97fd3105cd66aa187e5240c2ce Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Thu, 16 Jan 2020 11:50:55 +0000 Subject: [PATCH 13/24] Revert ready for challenge for ACME v1 --- getssl | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/getssl b/getssl index e52b3d0..5352e11 100755 --- a/getssl +++ b/getssl @@ -286,14 +286,15 @@ check_challenge_completion() { # checks with the ACME server if our challenge is keyauthorization=$3 debug "sending request to ACME server saying we're ready for challenge" - send_signed_request "$uri" "{}" # check response from our request to perform challenge if [[ $API -eq 1 ]]; then + send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" if [[ -n "$code" ]] && [[ ! "$code" == '202' ]] ; then error_exit "$domain:Challenge error: $code" fi else # APIv2 + send_signed_request "$uri" "{}" if [[ -n "$code" ]] && [[ ! "$code" == '200' ]] ; then detail=$(echo "$response" | grep "detail" | awk -F\" '{print $4}') error_exit "$domain:Challenge error: $code:Detail: $detail" @@ -303,7 +304,13 @@ check_challenge_completion() { # checks with the ACME server if our challenge is # loop "forever" to keep checking for a response from the ACME server. while true ; do debug "checking if challenge is complete" - send_signed_request "$uri" "" + if [[ $API -eq 1 ]]; then + if ! get_cr "$uri" ; then + error_exit "$domain:Verify error:$code" + fi + else # APIv2 + send_signed_request "$uri" "" + fi status=$(json_get "$response" status) From 770277c7ad72b7c78ebcfdec8ba5a7ac4a5089c8 Mon Sep 17 00:00:00 2001 From: Radek SPRTA Date: Thu, 16 Jan 2020 18:27:11 +0100 Subject: [PATCH 14/24] Add support for CloudDNS --- dns_scripts/dns_add_clouddns | 94 ++++++++++++++++++++++++++++++++ dns_scripts/dns_del_clouddns | 101 +++++++++++++++++++++++++++++++++++ 2 files changed, 195 insertions(+) create mode 100755 dns_scripts/dns_add_clouddns create mode 100755 dns_scripts/dns_del_clouddns diff --git a/dns_scripts/dns_add_clouddns b/dns_scripts/dns_add_clouddns new file mode 100755 index 0000000..5236269 --- /dev/null +++ b/dns_scripts/dns_add_clouddns @@ -0,0 +1,94 @@ +#!/usr/bin/env bash +# Need to add your email address and API key to clouddns below or set as env variables +email=${CLOUDDNS_EMAIL:-''} +password=${CLOUDDNS_PASSWORD:-''} +client=${CLOUDDNS_CLIENT:-''} + +# This script adds a token to clouddns DNS for the ACME challenge +# usage dns_add_clouddns "domain name" "token" +# return codes are; +# 0 - success +# 1 - error in input +# 2 - error within internal processing +# 3 - error in result ( domain not found in clouddns etc) + +fulldomain="${1}" +token="${2}" +API='https://admin.vshosting.cloud/clouddns' +LOGIN_API='https://admin.vshosting.cloud/api/public/auth/login' + +# Check initial parameters +if [[ -z "$fulldomain" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi +if [[ -z "$token" ]]; then + echo "DNS script requires challenge token as second parameter" + exit 1 +fi +if [[ -z "$email" ]]; then + echo "CLOUDDNS_EMAIL (email) parameter not set" + exit 1 +fi +if [[ -z "$password" ]]; then + echo "CLOUDDNS_PASSWORD (password) parameter not set" + exit 1 +fi +if [[ -z "$client" ]]; then + echo "CLOUDDNS_CLIENT (id) parameter not set" + exit 1 +fi + +# Login to clouddns to get accessToken +resp=$(curl --silent -X POST -H 'Content-Type: application/json' "$LOGIN_API" \ + --data "{\"email\": \"$email\", \"password\": \"$password\"}") +re='"accessToken":"([^,]*)",' # Match access token +if [[ "${resp// }" =~ $re ]]; then + access_token="${BASH_REMATCH[1]}" +fi +if [[ -z "$access_token" ]]; then + echo 'Could not get access token; check your credentials' + exit 3 +fi +curl_params=( -H "Authorization: Bearer $access_token" -H 'Content-Type: application/json' ) + +# Get main domain +domain_root=$(echo "$fulldomain" | awk -F\. '{print $(NF-1) FS $NF}') + +# Get domain id +resp=$(curl --silent "${curl_params[@]}" -X POST "$API/domain/search" \ + --data "{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$client\"}, {\"name\": \"domainName\", \"operator\": \"eq\", \"value\": \"$domain_root.\"}]}") +re='domainType":"[^"]*","id":"([^,]*)",' # Match domain id +if [[ "${resp//[$'\t\r\n ']}" =~ $re ]]; then + domain_id="${BASH_REMATCH[1]}" +fi + +if [[ -z "$domain_id" ]]; then + echo 'Domain name not found on your CloudDNS account' + exit 3 +fi + +# Add challenge record +txt_record="_acme-challenge.$domain_root." +resp=$(curl --silent "${curl_params[@]}" -X POST "$API/record-txt" \ + --data "{\"type\":\"TXT\",\"name\":\"$txt_record\",\"value\":\"$token\",\"domainId\":\"$domain_id\"}") + +# If adding record failed (error:) then print error message +if [[ "${resp// }" == *'"error"'* ]]; then + if [[ "${resp// }" == *'"code":4136'* ]]; then + echo "DNS challenge token already exists" + exit + fi + re='"message":"([^"]+)"' + if [[ "$resp" =~ $re ]]; then + echo "Error: DNS challenge not added: ${BASH_REMATCH[1]}" + exit 3 + else + echo "Error: DNS challenge not added: unknown error - ${resp}" + exit 3 + fi +fi + +# Publish challenge record +resp=$(curl --silent "${curl_params[@]}" -X PUT "$API/domain/$domain_id/publish" \ + --data "{\"soaTtl\":300}") diff --git a/dns_scripts/dns_del_clouddns b/dns_scripts/dns_del_clouddns new file mode 100755 index 0000000..ddac2b6 --- /dev/null +++ b/dns_scripts/dns_del_clouddns @@ -0,0 +1,101 @@ +#!/usr/bin/env bash +# Need to add your email address and API key to clouddns below or set as env variables +email=${CLOUDDNS_EMAIL:-''} +password=${CLOUDDNS_PASSWORD:-''} +client=${CLOUDDNS_CLIENT:-''} + +# This script adds a token to clouddns DNS for the ACME challenge +# usage dns_add_clouddns "domain name" "token" +# return codes are; +# 0 - success +# 1 - error in input +# 2 - error within internal processing +# 3 - error in result ( domain not found in clouddns etc) + +fulldomain="${1}" +token="${2}" +API='https://admin.vshosting.cloud/clouddns' +LOGIN_API='https://admin.vshosting.cloud/api/public/auth/login' + +# Check initial parameters +if [[ -z "$fulldomain" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi +if [[ -z "$token" ]]; then + echo "DNS script requires challenge token as second parameter" + exit 1 +fi +if [[ -z "$email" ]]; then + echo "CLOUDDNS_EMAIL (email) parameter not set" + exit 1 +fi +if [[ -z "$password" ]]; then + echo "CLOUDDNS_PASSWORD (password) parameter not set" + exit 1 +fi +if [[ -z "$client" ]]; then + echo "CLOUDDNS_CLIENT (id) parameter not set" + exit 1 +fi + +# Login to clouddns to get accessToken +resp=$(curl --silent -X POST -H 'Content-Type: application/json' "$LOGIN_API" \ + --data "{\"email\": \"$email\", \"password\": \"$password\"}") +re='"accessToken":"([^,]*)",' # Match access token +if [[ "${resp// }" =~ $re ]]; then + access_token="${BASH_REMATCH[1]}" +fi +if [[ -z "$access_token" ]]; then + echo 'Could not get access token; check your credentials' + exit 3 +fi +curl_params=( -H "Authorization: Bearer $access_token" -H 'Content-Type: application/json' ) + +# Get main domain and challenge record +domain_root=$(echo "$fulldomain" | awk -F\. '{print $(NF-1) FS $NF}') +txt_record="_acme-challenge.$domain_root." + +# Get domain id +curl_domainid_body="{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$client\"}, {\"name\": \"domainName\", \"operator\": \"eq\", \"value\": \"$domain_root.\"}]}" +resp=$(curl --silent "${curl_params[@]}" -X POST -d "$curl_domainid_body" "$API/domain/search") +re='domainType":"[^"]*","id":"([^,]*)",' # Find result section +if [[ "${resp//[$'\t\r\n ']}" =~ $re ]]; then + domain_id="${BASH_REMATCH[1]}" +fi + +if [[ -z "$domain_id" ]]; then + echo 'Domain name not found on your CloudDNS account' + exit 3 +fi + +# Get challenge record ID +resp=$(curl --silent "${curl_params[@]}" -X GET "$API/domain/$domain_id" ) +re="\"lastDomainRecordList\".*\"id\":\"([^,]*)\"[^}]*\"name\":\"$txt_record\"," # Match domain id +if [[ "${resp//[$'\t\r\n ']}" =~ $re ]]; then + record_id="${BASH_REMATCH[1]}" +fi + +if [[ -z "$record_id" ]]; then + echo 'Challenge record does not exist' + exit 3 +fi + +# Remove challenge record +resp=$(curl --silent "${curl_params[@]}" -X DELETE "$API/record/$record_id") + +# If removing record failed (error:) then print error message +if [[ "${resp// }" == *'"error"'* ]]; then + re='"message":"([^"]+)"' + if [[ "$resp" =~ $re ]]; then + echo "Error: DNS challenge not removed: ${BASH_REMATCH[1]}" + exit 3 + else + echo "Error: DNS challenge not removed: unknown error - ${resp}" + exit 3 + fi +fi + +# Publish challenge record deletion +resp=$(curl --silent "${curl_params[@]}" -X PUT "$API/domain/$domain_id/publish" \ + --data "{\"soaTtl\":300}") From 6c9f0e7655beb760b33a764f60b4ebd5c49ee445 Mon Sep 17 00:00:00 2001 From: Radek SPRTA Date: Thu, 16 Jan 2020 18:30:24 +0100 Subject: [PATCH 15/24] Update changelog --- getssl | 1 + 1 file changed, 1 insertion(+) diff --git a/getssl b/getssl index e52b3d0..f1e2989 100755 --- a/getssl +++ b/getssl @@ -196,6 +196,7 @@ # 2020-01-07 #464 and #486 "json was blank" (change all curl request to use POST-as-GET) # 2020-01-08 Error and exit if rate limited, exit if curl returns nothing # 2020-01-10 Change domain and getssl templates to v2 (2.15) +# 2020-01-16 Add support for CloudDNS # ---------------------------------------------------------------------------------------- PROGNAME=${0##*/} From b8b70f7ceea852bc6194a32cb9959d68c81efeb1 Mon Sep 17 00:00:00 2001 From: Radek SPRTA Date: Thu, 16 Jan 2020 18:49:08 +0100 Subject: [PATCH 16/24] Support certificates with SANs --- dns_scripts/dns_add_clouddns | 2 +- dns_scripts/dns_del_clouddns | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dns_scripts/dns_add_clouddns b/dns_scripts/dns_add_clouddns index 5236269..a4e3f81 100755 --- a/dns_scripts/dns_add_clouddns +++ b/dns_scripts/dns_add_clouddns @@ -69,7 +69,7 @@ if [[ -z "$domain_id" ]]; then fi # Add challenge record -txt_record="_acme-challenge.$domain_root." +txt_record="_acme-challenge.$fulldomain." resp=$(curl --silent "${curl_params[@]}" -X POST "$API/record-txt" \ --data "{\"type\":\"TXT\",\"name\":\"$txt_record\",\"value\":\"$token\",\"domainId\":\"$domain_id\"}") diff --git a/dns_scripts/dns_del_clouddns b/dns_scripts/dns_del_clouddns index ddac2b6..ec22c91 100755 --- a/dns_scripts/dns_del_clouddns +++ b/dns_scripts/dns_del_clouddns @@ -54,7 +54,7 @@ curl_params=( -H "Authorization: Bearer $access_token" -H 'Content-Type: applica # Get main domain and challenge record domain_root=$(echo "$fulldomain" | awk -F\. '{print $(NF-1) FS $NF}') -txt_record="_acme-challenge.$domain_root." +txt_record="_acme-challenge.$fulldomain." # Get domain id curl_domainid_body="{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$client\"}, {\"name\": \"domainName\", \"operator\": \"eq\", \"value\": \"$domain_root.\"}]}" From 197c5f8faa86d146ec69666a58f9f00e0f7ed149 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Fri, 17 Jan 2020 20:34:14 +0000 Subject: [PATCH 17/24] Ignore base64 errors --- getssl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/getssl b/getssl index 5352e11..8269195 100755 --- a/getssl +++ b/getssl @@ -196,10 +196,11 @@ # 2020-01-07 #464 and #486 "json was blank" (change all curl request to use POST-as-GET) # 2020-01-08 Error and exit if rate limited, exit if curl returns nothing # 2020-01-10 Change domain and getssl templates to v2 (2.15) +# 2020-01-17 #473 and #477 Don't use POST-as-GET when sending ready for challenge for ACMEv1 (2.16) # ---------------------------------------------------------------------------------------- PROGNAME=${0##*/} -VERSION="2.15" +VERSION="2.16" # defaults ACCOUNT_KEY_LENGTH=4096 @@ -1444,8 +1445,7 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p responseHeaders=$(cat "$CURL_HEADER") if [[ "$needbase64" && ${response##*()} != "{"* ]]; then # response is in base64 too, decode - #!FIXME need to use openssl base64 decoder if it exists - response=$(echo "$response" | base64 -d) + response=$(echo "$response" | base64 -d 2>&1) fi debug responseHeaders "$responseHeaders" From 6138f4ab1f27908eca837e26f362f9e254a40f9e Mon Sep 17 00:00:00 2001 From: Yannic Haupenthal Date: Tue, 21 Jan 2020 09:42:05 +0100 Subject: [PATCH 18/24] make markdownlint happy --- test/README.md | 28 ++++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/test/README.md b/test/README.md index 9d0aedd..086c58a 100644 --- a/test/README.md +++ b/test/README.md @@ -1,19 +1,31 @@ # Testing -This directory contains a simple test script which tests creating certificates with Pebble (testing version of the LetsEncrypt server) +This directory contains a simple test script which tests creating +certificates with Pebble (testing version of the LetsEncrypt server) Start up pebble, the challdnstest server for DNS challenges -`docker-compose -f "docker-compose.yml" up -d --build` + +```sh +docker-compose -f "docker-compose.yml" up -d --build +``` Run the tests -`docker exec -it getssl /getssl/test/run-test.sh` -Debug (need to set CURL_CA_BUNDLE as pebble uses a local certificate, otherwise you get a "unknown API version" error) -`docker exec -it getssl /bin/bash` -`export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt` -`/getssl/getssl -d getssl` +```sh +docker exec -it getssl /getssl/test/run-test.sh +``` + +Debug (need to set `CURL_CA_BUNDLE` as pebble uses a local certificate, +otherwise you get a "unknown API version" error) + +```sh +docker exec -it getssl /bin/bash +export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +/getssl/getssl -d getssl +``` + +## TODO -# TODO 1. Move to BATS (bash automated testing) instead of run-test.sh 2. Test RHEL6, Debian as well 3. Test SSH, SFTP From 8c7cd703b583e4095c5e41f338388743e8b88c63 Mon Sep 17 00:00:00 2001 From: Yannic Haupenthal Date: Tue, 21 Jan 2020 10:06:47 +0100 Subject: [PATCH 19/24] lint CONTRIBUTING.md --- CONTRIBUTING.md | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 482a7aa..3c8242c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,24 +1,31 @@ # How to contribute -If you are happy writing in bash, please create a PR for any changes you'd like to see included (or bug fixes). +If you are happy writing in bash, please create a PR for any changes +you'd like to see included (or bug fixes). -If you aren't happy writing in bash, please open an issue with as much detail as possible about the issue or what you'd like to see added / improved. +If you aren't happy writing in bash, please open an issue with as much +detail as possible about the issue or what you'd like to see added / +improved. ## Submitting changes -Please update the 'revision history' and version number at the top of the code (without this I can't easily do a merge) +Please update the 'revision history' and version number at the top of +the code (without this I can't easily do a merge) -Please update just one issue per PR. If there are multiple issues, please provide separate PR's one per issue. +Please update just one issue per PR. If there are multiple issues, +please provide separate PR's one per issue. ## Coding conventions -Please see the guidelines at https://github.com/srvrco/getssl/wiki/Bash-Style-guide +Please see the guidelines at ## Testing -Please test with [shellcheck](https://github.com/koalaman/shellcheck), although this will also be tested on github ( via travis) on all PRs. +Please test with [shellcheck](https://github.com/koalaman/shellcheck), +although this will also be tested on github (via travis) on all PRs. -Please remember that the system is used across a wide range of platforms, so if you have access to multiple operating systems, please test on all. +Please remember that the system is used across a wide range of +platforms, so if you have access to multiple operating systems, please +test on all. - -Thanks :) +Thanks :) From 2c1a894224ec57b674d367c5cbd5f0ee3ef70ab7 Mon Sep 17 00:00:00 2001 From: Yannic Haupenthal Date: Tue, 21 Jan 2020 10:07:08 +0100 Subject: [PATCH 20/24] lint bug_report.md --- .github/ISSUE_TEMPLATE/bug_report.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 116d2a5..135ea05 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,6 +12,7 @@ A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: + 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' @@ -21,8 +22,9 @@ Steps to reproduce the behavior: A clear and concise description of what you expected to happen. **Operating system (please complete the following information):** - - OS: [e.g. Debian 9, Ubuntu 18.04, freeBSD ] - - Bash Version [e.g. GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)] + +- OS: [e.g. Debian 9, Ubuntu 18.04, freeBSD ] +- Bash Version [e.g. GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)] **Additional context** Add any other context about the problem here. From 0891d37372bbb86677108aae019c4e454ecf6e7f Mon Sep 17 00:00:00 2001 From: Yannic Haupenthal Date: Tue, 21 Jan 2020 10:07:18 +0100 Subject: [PATCH 21/24] lint README.md (and fix some punctuation) --- README.md | 170 ++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 121 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 8549bda..1d0d3fc 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,76 @@ # getssl -Obtain SSL certificates from the letsencrypt.org ACME server. Suitable for automating the process on remote servers. + +Obtain SSL certificates from the letsencrypt.org ACME server. Suitable +for automating the process on remote servers. ## Features -* **Bash** - It runs on virtually all unix machines, including BSD, most Linux distributions, macOS. -* **Get certificates for remote servers** - The tokens used to provide validation of domain ownership, and the certificates themselves can be automatically copied to remote servers (via ssh, sftp or ftp for tokens). The script doesn't need to run on the server itself. This can be useful if you don't have access to run such scripts on the server itself, e.g. if it's a shared server. -* **Runs as a daily cron** - so certificates will be automatically renewed when required. + +* **Bash** - It runs on virtually all unix machines, including BSD, most + Linux distributions, macOS. +* **Get certificates for remote servers** - The tokens used to provide + validation of domain ownership, and the certificates themselves can be + automatically copied to remote servers (via ssh, sftp or ftp for + tokens). The script doesn't need to run on the server itself. This can + be useful if you don't have access to run such scripts on the server + itself, e.g. if it's a shared server. +* **Runs as a daily cron** - so certificates will be automatically + renewed when required. * **Automatic certificate renewals** -* **Checks certificates are correctly loaded**. After installation of a new certificate it will test the port specified ( see [Server-Types](#server-types) for options ) that the certificate is actually being used correctly. -* **Automatically updates** - The script can automatically update itself with bug fixes etc if required. -* **Extensively configurable** - With a simple configuration file for each certificate it is possible to configure it exactly for your needs, whether a simple single domain or multiple domains across multiple servers on the same certificate. +* **Checks certificates are correctly loaded** - After installation of a + new certificate it will test the port specified ( see + [Server-Types](#server-types) for options ) that the certificate is + actually being used correctly. +* **Automatically updates** - The script can automatically update itself + with bug fixes etc if required. +* **Extensively configurable** - With a simple configuration file for + each certificate it is possible to configure it exactly for your + needs, whether a simple single domain or multiple domains across + multiple servers on the same certificate. * **Supports http and dns challenges** - Full ACME implementation * **Simple and easy to use** -* **Detailed debug info** - Whilst it shouldn't be needed, detailed debug information is available. -* **Reload services** - After a new certificate is obtained then the relevant services (e.g. apache/nginx/postfix) can be reloaded. +* **Detailed debug info** - Whilst it shouldn't be needed, detailed + debug information is available. +* **Reload services** - After a new certificate is obtained then the + relevant services (e.g. apache/nginx/postfix) can be reloaded. * **ACME v1 and V2** - Supports both ACME versions 1 and 2 ## Installation -Since the script is only one file, you can use the following command for a quick installation of GetSSL only: -``` + +Since the script is only one file, you can use the following command for +a quick installation of GetSSL only: + +```sh curl --silent https://raw.githubusercontent.com/srvrco/getssl/master/getssl > getssl ; chmod 700 getssl ``` -This will copy the getssl Bash script to the current location and change the permissions to make it executable for you. -For a more comprehensive installation (e.g. install also helper scripts) use the provided Makefile with each release tarball. Use the `install` target. +This will copy the getssl Bash script to the current location and change +the permissions to make it executable for you. + +For a more comprehensive installation (e.g. install also helper scripts) +use the provided Makefile with each release tarball. Use the `install` +target. You'll find the latest version in the git repository: -``` +```sh git clone https://github.com/srvrco/getssl.git ``` -For Arch Linux there are packages in the AUR, see [here](https://aur.archlinux.org/packages/getssl/) and [there](https://aur.archlinux.org/packages/getssl-git/). +For Arch Linux there are packages in the AUR, see +[here](https://aur.archlinux.org/packages/getssl/) and +[there](https://aur.archlinux.org/packages/getssl-git/). -If you use puppet, there is a [GetSSL Puppet module](https://github.com/dthielking/puppet_getssl) by dthielking +If you use puppet, there is a [GetSSL Puppet +module](https://github.com/dthielking/puppet_getssl) by dthielking ## Overview -GetSSL was written in standard bash ( so it can be run on a server, a desktop computer, or even a virtualbox) and add the checks, and certificates to a remote server ( providing you have a ssh with key, sftp or ftp access to the remote server). +GetSSL was written in standard bash ( so it can be run on a server, a +desktop computer, or even a virtualbox) and add the checks, and +certificates to a remote server ( providing you have a ssh with key, +sftp or ftp access to the remote server). -``` +```getssl getssl ver. 2.02 Obtain SSL certificates from the letsencrypt.org ACME server @@ -63,27 +95,36 @@ Options: Once you have obtained the script (see Installation above), the next step is to use -```./getssl -c yourdomain.com``` +```sh +./getssl -c yourdomain.com +``` -where yourdomain.com is the primary domain name that you want to create a certificate for. This will create the following folders and files. +where yourdomain.com is the primary domain name that you want to create +a certificate for. This will create the following folders and files. -``` +```sh ~/.getssl ~/.getssl/getssl.cfg ~/.getssl/yourdomain.com ~/.getssl/yourdomain.com/getssl.cfg ``` -You can then edit ~/.getssl/getssl.cfg to set the values you want as the default for the majority of your certificates. - -Then edit ~/.getssl/yourdomain.com/getssl.cfg to have the values you want for this specific domain (make sure to uncomment and specify correct `ACL` option, since it is required). +You can then edit `~/.getssl/getssl.cfg` to set the values you want as the +default for the majority of your certificates. -You can then just run; +Then edit `~/.getssl/yourdomain.com/getssl.cfg` to have the values you +want for this specific domain (make sure to uncomment and specify +correct `ACL` option, since it is required). -```getssl yourdomain.com ``` +You can then just run: -and it should run, providing output like; +```sh +getssl yourdomain.com ``` + +and it should run, providing output like: + +```sh Registering account Verify each domain Verifying yourdomain.com @@ -98,30 +139,41 @@ copying private key to ssh:server5:/home/yourdomain/ssl/domain.key copying CA certificate to ssh:server5:/home/yourdomain/ssl/chain.crt reloading SSL services ``` -**This will (by default) use the staging server, so should give you a certificate that isn't trusted ( Fake Let's Encrypt).** + +**This will (by default) use the staging server, so should give you a +certificate that isn't trusted ( Fake Let's Encrypt).** Change the server in your config file to get a fully valid certificate. -**Note:** Verification is done via port 80 (http), port 443 (https) or dns. The certificate can be used (and checked with getssl) on alternate ports. +**Note:** Verification is done via port 80 (http), port 443 (https) or +dns. The certificate can be used (and checked with getssl) on alternate +ports. ## Automating updates I use the following cron -``` + +```cron 23 5 * * * /root/scripts/getssl -u -a -q ``` -The cron will automatically update getssl and renew any certificates, only giving output if there are issues / errors. + +The cron will automatically update getssl and renew any certificates, +only giving output if there are issues / errors. * The -u flag updates getssl if there is a more recent version available. * The -a flag automatically renews any certificates that are due for renewal. -* The -q flag is "quiet" so that it only outputs and emails me if there was an error / issue. +* The -q flag is "quiet" so that it only outputs and emails me if there + was an error / issue. ## Structure -The design aim was to provide flexibility in running the code. The default working directory is ~/.getssl ( which can be modified via the command line) +The design aim was to provide flexibility in running the code. The +default working directory is `~/.getssl` (which can be modified via the +command line). -Within the **working directory** is a config file, getssl.cfg which is a simple bash file containing variables, an example of which is +Within the **working directory** is a config file `getssl.cfg` which is a +simple bash file containing variables, an example of which is: -``` +```getssl # Uncomment and modify any variables you need # The staging server is best for testing (hence set as default) CA="https://acme-staging.api.letsencrypt.org" @@ -143,9 +195,11 @@ RENEW_ALLOW="30" SSLCONF="/usr/lib/ssl/openssl.cnf" ``` -then, within the **working directory** there will be a folder for each certificate (based on its domain name). Within that folder will be a config file (again called getssl.cfg). An example of which is; +then, within the **working directory** there will be a folder for each +certificate (based on its domain name). Within that folder will be a +config file (again called `getssl.cfg`). An example of which is: -``` +```getssl # 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 @@ -195,19 +249,27 @@ RELOAD_CMD="service apache2 reload" #CHECK_REMOTE="true" ``` -If a location for a file starts with ssh: it is assumed the next part of the file is the hostname, followed by a colon, and then the path. -Files will be securely copied using scp, and it assumes that you have a key on the server ( for passwordless access). You can set the user, port etc for the server in your .ssh/config file +If a location for a file starts with `ssh:` it is assumed the next part +of the file is the hostname, followed by a colon, and then the path. +Files will be securely copied using scp, and it assumes that you have a +key on the server (for passwordless access). You can set the user, +port etc for the server in your `.ssh/config` file. -If an ACL starts with ftp: or sftp: it as assumed that the line is in the format "ftp:UserID:Password:servername:/path/to/acme-challenge". sftp requires sshpass. -Note: FTP can be used for copying tokens only and can **not** be used for uploading private key or certificates as it's not a secure method of transfer. +If an ACL starts with `ftp:` or `sftp:` it as assumed that the line is +in the format "ftp:UserID:Password:servername:/path/to/acme-challenge". +sftp requires sshpass. +Note: FTP can be used for copying tokens only +and can **not** be used for uploading private key or certificates as +it's not a secure method of transfer. ssh can also be used for the reload command if using on remote servers. Multiple locations can be defined for a file by separating the locations with a semi-colon. +A typical config file for `example.com` and `www.example.com` on the +same server would be: -A typical config file for example.com and www.example.com on the same server would be -``` +```getssl # uncomment and modify any variables you need # The staging server is best for testing CA="https://acme-staging.api.letsencrypt.org" @@ -231,6 +293,7 @@ RELOAD_CMD="service apache2 reload" ``` ## Server-Types + OpenSSL has built-in support for getting the certificate from a number of SSL services these are available in getssl to check if the certificate is installed correctly @@ -252,23 +315,32 @@ these are available in getssl to check if the certificate is installed correctly | ldaps | 636 | | | port number | | | - ## Revoke a certificate In general revoking a certificate is not required. Usage: `getssl -r path/to/cert path/to/key [CA_server]` -You need to specify both the certificate you want to revoke, and the account or private domain key which was used to sign / obtain the original certificate. The CA_server is an optional parameter and defaults to Let's Encrypt ( "https://acme-v01.api.letsencrypt.org" ) as that is currently the only Certificate Authority using the ACME protocol. - +You need to specify both the certificate you want to revoke, and the +account or private domain key which was used to sign / obtain the +original certificate. The `CA_server` is an optional parameter and +defaults to Let's Encrypt ("") as +that is currently the only Certificate Authority using the ACME +protocol. ## Elliptic curve keys -You can use Elliptic curve keys for both the account key and the domain key (different of course, don't use the same key for both). prime256v1 (NIST P-256) and secp384r1 (NIST P-384) are both fully supported. secp521r1 (NIST P-521) is included in the code, but not currently supported by Let's Encrypt). +You can use Elliptic curve keys for both the account key and the domain +key (different of course, don't use the same key for both). prime256v1 +(NIST P-256) and secp384r1 (NIST P-384) are both fully supported. +secp521r1 (NIST P-521) is included in the code, but not currently +supported by Let's Encrypt). ## Issues / problems / help -If you have any issues, please log them at https://github.com/srvrco/getssl/issues + +If you have any issues, please log them at There are additional help pages on the [wiki](https://github.com/srvrco/getssl/wiki) -If you have any suggestions for improvements then pull requests are welcomed, or raise an issue. +If you have any suggestions for improvements then pull requests are +welcomed, or raise an issue. From b1e177f45ea50e84be5f979cad967d823c37a637 Mon Sep 17 00:00:00 2001 From: Radek SPRTA Date: Wed, 22 Jan 2020 05:26:01 +0100 Subject: [PATCH 22/24] Handle domains of .co.uk type --- dns_scripts/dns_add_clouddns | 11 ++++++++++- dns_scripts/dns_del_clouddns | 11 ++++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/dns_scripts/dns_add_clouddns b/dns_scripts/dns_add_clouddns index a4e3f81..f20d1ab 100755 --- a/dns_scripts/dns_add_clouddns +++ b/dns_scripts/dns_add_clouddns @@ -53,7 +53,16 @@ fi curl_params=( -H "Authorization: Bearer $access_token" -H 'Content-Type: application/json' ) # Get main domain -domain_root=$(echo "$fulldomain" | awk -F\. '{print $(NF-1) FS $NF}') +resp=$(curl --silent "${curl_params[@]}" -X POST "$API/domain/search" \ + --data "{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$client\"}]}") +domain_slice="$fulldomain" +while [[ -z "$domain_root" ]]; do + if [[ "${resp// }" =~ domainName\":\"$domain_slice ]]; then + domain_root="$domain_slice" + _debug domain_root "$domain_root" + fi + domain_slice="${domain_slice#[^\.]*.}" +done # Get domain id resp=$(curl --silent "${curl_params[@]}" -X POST "$API/domain/search" \ diff --git a/dns_scripts/dns_del_clouddns b/dns_scripts/dns_del_clouddns index ec22c91..0b25121 100755 --- a/dns_scripts/dns_del_clouddns +++ b/dns_scripts/dns_del_clouddns @@ -53,7 +53,16 @@ fi curl_params=( -H "Authorization: Bearer $access_token" -H 'Content-Type: application/json' ) # Get main domain and challenge record -domain_root=$(echo "$fulldomain" | awk -F\. '{print $(NF-1) FS $NF}') +resp=$(curl --silent "${curl_params[@]}" -X POST "$API/domain/search" \ + --data "{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$client\"}]}") +domain_slice="$fulldomain" +while [[ -z "$domain_root" ]]; do + if [[ "${resp// }" =~ domainName\":\"$domain_slice ]]; then + domain_root="$domain_slice" + _debug domain_root "$domain_root" + fi + domain_slice="${domain_slice#[^\.]*.}" +done txt_record="_acme-challenge.$fulldomain." # Get domain id From 25ab41135d550c6e7313838a008dad850057807d Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Wed, 22 Jan 2020 22:38:45 +0000 Subject: [PATCH 23/24] Fix json_get for 6 parameters when >9 domains --- getssl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/getssl b/getssl index 8269195..0646ce4 100755 --- a/getssl +++ b/getssl @@ -1192,7 +1192,7 @@ json_get() { # get values from json if [[ -n "$6" ]]; then full=$(json_awk "$1") section=$(echo "$full" | grep "\"$2\"" | grep "\"$3\"" | grep "\"$4\"" | awk -F"," '{print $2}') - echo "$full" | grep "^..${5}\",$section" | awk '{print $2}' | tr -d '"' + echo "$full" | grep "^..${5}\",$section\]" | awk '{print $2}' | tr -d '"' elif [[ -n "$5" ]]; then full=$(json_awk "$1") section=$(echo "$full" | grep "\"$2\"" | grep "\"$3\"" | grep "\"$4\"" | awk -F"," '{print $2}') From 0040d84d22b4b6832c51855a96a78d6642377729 Mon Sep 17 00:00:00 2001 From: Tim Kimber Date: Fri, 24 Jan 2020 19:05:18 +0000 Subject: [PATCH 24/24] Update revision history --- getssl | 1 + 1 file changed, 1 insertion(+) diff --git a/getssl b/getssl index 0646ce4..32df8d1 100755 --- a/getssl +++ b/getssl @@ -197,6 +197,7 @@ # 2020-01-08 Error and exit if rate limited, exit if curl returns nothing # 2020-01-10 Change domain and getssl templates to v2 (2.15) # 2020-01-17 #473 and #477 Don't use POST-as-GET when sending ready for challenge for ACMEv1 (2.16) +# 2020-01-22 #475 and #483 Fix grep regex for >9 subdomains in json_get # ---------------------------------------------------------------------------------------- PROGNAME=${0##*/}