You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1769 lines
64 KiB

#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# getsslD - Obtain SSL certificates from the letsencrypt.org ACME server.
# Runs in a Docker conatainer.
# Based on the work of https://github.com/srvrco/getssl
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License at <http://www.gnu.org/licenses/> for
# more details.
# For usage, run "getsslD -h" or see
PROGNAME=getsslD
VERSION="1.0"
# Default values, accepts environment variables if set, otherwise default are used
ACCOUNT_KEY_LENGTH=${ACCOUNT_KEY_LENGTH:-"4096"}
ACCOUNT_KEY_TYPE=${ACCOUNT_KEY_TYPE:-"rsa"}
CA=${CA:-"https://acme-staging.api.letsencrypt.org"}
CA_CERT_LOCATION=${CA_CERT_LOCATION:-""}
CHALLENGE_CHECK_TYPE=${CHALLENGE_CHECK_TYPE:-"http"}
CHECK_ALL_AUTH_DNS=${CHECK_ALL_AUTH_DNS:-"false"}
CHECK_REMOTE=${CHECK_REMOTE:-"true"}
CHECK_REMOTE_WAIT=${CHECK_REMOTE_WAIT:-"0"}
CSR_SUBJECT=${CSR_SUBJECT:-"/"}
DEACTIVATE_AUTH=${DEACTIVATE_AUTH:-"false"}
DEFAULT_REVOKE_CA=${DEFAULT_REVOKE_CA:-"https://acme-v01.api.letsencrypt.org"}
DEFAULT_UMASK=${DEFAULT_UMASK:-"u=rx,g=rx,o="}
DNS_EXTRA_WAIT=${DNS_EXTRA_WAIT:-""}
DNS_WAIT=${DNS_WAIT:-"10"}
DOMAIN_KEY_LENGTH=${DOMAIN_KEY_LENGTH:-"4096"}
DUAL_RSA_ECDSA=${DUAL_RSA_ECDSA:-"false"}
GETSSLD_IGNORE_CP_PRESERVE=${GETSSLD_IGNORE_CP_PRESERVE:-"false"}
HTTP_TOKEN_CHECK_WAIT=${HTTP_TOKEN_CHECK_WAIT:-"0"}
IGNORE_DIRECTORY_DOMAIN=${IGNORE_DIRECTORY_DOMAIN:-"false"}
ORIG_UMASK=$(umask)
PREVIOUSLY_VALIDATED=${PREVIOUSLY_VALIDATED:-"true"}
PRIVATE_KEY_ALG=${PRIVATE_KEY_ALG:-"rsa"}
PUBLIC_DNS_SERVER=${PUBLIC_DNS_SERVER:-""}
RELOAD_CMD=${RELOAD_CMD:-""}
RENEW_ALLOW=${RENEW_ALLOW:-"30"}
REUSE_PRIVATE_KEY=${REUSE_PRIVATE_KEY:-"true"}
SERVER_TYPE=${SERVER_TYPE:-"https"}
SKIP_HTTP_TOKEN_CHECK=${SKIP_HTTP_TOKEN_CHECK:-"false"}
SSLCONF=${SSLCONF:-"$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf"}
OCSP_MUST_STAPLE=${OCSP_MUST_STAPLE:-"false"}
TOKEN_USER_ID=${TOKEN_USER_ID:-""}
USE_SINGLE_ACL=${USE_SINGLE_ACL:-"false"}
VALIDATE_VIA_DNS=${VALIDATE_VIA_DNS:-""}
WORKING_DIR=${WORKING_DIR:-~/.getsslD}
_CHECK_ALL=${_CHECK_ALL:-"false"}
_CREATE_CONFIG=${_CREATE_CONFIG:-"false"}
_FORCE_RENEW=${_FORCE_RENEW:-"false"}
_QUIET=${_QUIET:-"false"}
_RECREATE_CSR=${_RECREATE_CSR:-"false"}
_REVOKE=${_REVOKE:-"false"}
_USE_DEBUG=${_USE_DEBUG:-"false"}
config_errors="false"
LANG=C
# Define all functions (in alphabetical order)
cert_archive() {
# Archive certificates files
# Create directory for day, store certs by DOMAIN-YYYY_MM_DD:HH_MM UTC
info "Copying generated certs to archive..."
local date_time
date_time=$(date -u +%Y_%m_%d_%H_%M)
local date=${date_time::10}
local archive_dir="${DOMAIN_DIR}/archive/${date}"
local archive_suffix="${DOMAIN}-${date_time}"
umask "${DEFAULT_UMASK}"
mkdir -p "${archive_dir}"
info " ${archive_dir} created."
cp "${CERT_FILE}" "${archive_dir}/${archive_suffix}.crt"
cp "${DOMAIN_DIR}/${DOMAIN}.csr" "${archive_dir}/${archive_suffix}.csr"
cp "${DOMAIN_DIR}/${DOMAIN}.key" "${archive_dir}/${archive_suffix}.key"
cp "${CA_CERT}" "${archive_dir}/${archive_suffix}-chain.crt"
cat "$CERT_FILE" "$CA_CERT" > "${archive_dir}/${archive_suffix}-fullchain.crt"
info " RSA certs and chains copied."
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
cp "${CERT_FILE::-4}.ec.crt" "${archive_dir}/${archive_suffix}.ec.crt"
cp "${DOMAIN_DIR}/${DOMAIN}.ec.csr" "${archive_dir}/${archive_suffix}.ec.csr"
cp "${DOMAIN_DIR}/${DOMAIN}.ec.key" "${archive_dir}/${archive_suffix}.ec.key"
cp "${CA_CERT::-4}.ec.crt" "${archive_dir}/${archive_suffix}-chain.ec.crt"
cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${archive_dir}/${archive_suffix}-fullchain.ec.crt"
fi
info " EC certs and chains copied."
umask "${ORIG_UMASK}"
# Call purge_archive to clear out old files
info "Purging old getsslD archives"
purge_archive "${DOMAIN_DIR}"
}
check_challenge_completion() { # checks with the ACME server if our challenge is OK
uri=$1
domain=$2
keyauthorization=$3
debug "sending request to ACME server saying we're ready for challenge"
send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}"
# check response from our request to perform challenge
if [[ ! -z "$code" ]] && [[ ! "$code" == '202' ]] ; then
error_exit "$domain:Challenge error: $code"
fi
# 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
status=$(json_get "$response" status)
# If ACME response is valid, then break out of loop
if [[ "$status" == "valid" ]] ; then
info "Verified $domain"
break;
fi
# if ACME response is that their check gave an invalid response, error exit
if [[ "$status" == "invalid" ]] ; then
err_detail=$(json_get "$response" detail)
error_exit "$domain:Verify error:$err_detail"
fi
# if ACME response is pending ( they haven't completed checks yet) then wait and try again.
if [[ "$status" == "pending" ]] ; then
info "Pending"
else
error_exit "$domain:Verify error:$response"
fi
debug "sleep 5 secs before testing verify again"
sleep 5
done
if [[ "$DEACTIVATE_AUTH" == "true" ]]; then
deactivate_url=$(echo "$responseHeaders" | grep "^Link" | awk -F"[<>]" '{print $2}')
deactivate_url_list="$deactivate_url_list $deactivate_url"
debug "adding url to deactivate list - $deactivate_url"
fi
}
check_config() { # check the config files for all obvious errors
debug "checking config"
# check keys
case "$ACCOUNT_KEY_TYPE" in
rsa|prime256v1|secp384r1|secp521r1)
debug "checked ACCOUNT_KEY_TYPE " ;;
*)
info "${DOMAIN}: invalid ACCOUNT_KEY_TYPE - $ACCOUNT_KEY_TYPE"
config_errors=true ;;
esac
if [[ "$ACCOUNT_KEY" == "$DOMAIN_DIR/${DOMAIN}.key" ]]; then
info "${DOMAIN}: ACCOUNT_KEY and domain key ( $DOMAIN_DIR/${DOMAIN}.key ) must be different"
config_errors=true
fi
case "$PRIVATE_KEY_ALG" in
rsa|prime256v1|secp384r1|secp521r1)
debug "checked PRIVATE_KEY_ALG " ;;
*)
info "${DOMAIN}: invalid PRIVATE_KEY_ALG - $PRIVATE_KEY_ALG"
config_errors=true ;;
esac
if [[ "$DUAL_RSA_ECDSA" == "true" ]] && [[ "$PRIVATE_KEY_ALG" == "rsa" ]]; then
info "${DOMAIN}: PRIVATE_KEY_ALG not set to an EC type and DUAL_RSA_ECDSA=\"true\""
config_errors=true
fi
# get all domains
if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
alldomains=${SANS//,/ }
else
alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")
fi
if [[ -z "$alldomains" ]]; then
info "${DOMAIN}: no domains specified"
config_errors=true
fi
if [[ $VALIDATE_VIA_DNS == "true" ]]; then # using dns-01 challenge
if [[ -z "$DNS_ADD_COMMAND" ]]; then
info "${DOMAIN}: DNS_ADD_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")"
config_errors=true
fi
if [[ -z "$DNS_DEL_COMMAND" ]]; then
info "${DOMAIN}: DNS_DEL_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")"
config_errors=true
fi
fi
dn=0
tmplist=$(mktemp)
for d in $alldomains; do # loop over domains (dn is domain number)
debug "checking domain $d"
if [[ "$(grep "^${d}$" "$tmplist")" = "$d" ]]; then
info "${DOMAIN}: $d appears to be duplicated in domain, SAN list"
config_errors=true
else
echo "$d" >> "$tmplist"
fi
if [[ "$USE_SINGLE_ACL" == "true" ]]; then
DOMAIN_ACL="${ACL[0]}"
else
DOMAIN_ACL="${ACL[$dn]}"
fi
if [[ $VALIDATE_VIA_DNS != "true" ]]; then # using http-01 challenge
if [[ -z "${DOMAIN_ACL}" ]]; then
info "${DOMAIN}: ACL location not specified for domain $d in $DOMAIN_DIR/getsslD.cfg"
config_errors=true
fi
# check domain exist
if [[ "$(nslookup -query=AAAA "${d}"|grep -c "^${d}.*has AAAA address")" -ge 1 ]]; then
debug "found IPv6 record for ${d}"
elif [[ "$(nslookup "${d}"| grep -c ^Name)" -ge 1 ]]; then
debug "found IPv4 record for ${d}"
else
info "${DOMAIN}: DNS lookup failed for $d"
config_errors=true
fi
fi # end using http-01 challenge
((dn++))
done
# tidy up
rm -f "$tmplist"
if [[ "$config_errors" == "true" ]]; then
error_exit "${DOMAIN}: exiting due to config errors"
fi
debug "${DOMAIN}: check_config completed - all OK"
}
clean_up() { # Perform pre-exit housekeeping
umask "$ORIG_UMASK"
if [[ $VALIDATE_VIA_DNS == "true" ]]; then
# Tidy up DNS entries if things failed part way though.
shopt -s nullglob
for dnsfile in $TEMP_DIR/dns_verify/*; do
# shellcheck source=/dev/null
. "$dnsfile"
debug "attempting to clean up DNS entry for $d"
eval "$DNS_DEL_COMMAND" "$d" "$auth_key"
done
shopt -u nullglob
fi
if [[ ! -z "$DOMAIN_DIR" ]]; then
rm -rf "${TEMP_DIR:?}"
fi
}
copy_file_to_location() { # copies a file, using scp, sftp or ftp if required.
cert=$1 # descriptive name, just used for display
from=$2 # current file location
to=$3 # location to move file to.
IFS=\; read -r -a copy_locations <<<"$3"
for to in "${copy_locations[@]}"; do
info "copying $cert to $to"
debug "copying from $from to $to"
if [[ "${to:0:4}" == "ssh:" ]] ; then
debug "using scp scp -q $from ${to:4}"
if ! scp -q "$from" "${to:4}" >/dev/null 2>&1 ; then
error_exit "problem copying file to the server using scp.
scp $from ${to:4}"
fi
debug "userid $TOKEN_USER_ID"
if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then
servername=$(echo "$to" | awk -F":" '{print $2}')
tofile=$(echo "$to" | awk -F":" '{print $3}')
debug "servername $servername"
debug "file $tofile"
# shellcheck disable=SC2029
ssh "$servername" "chown $TOKEN_USER_ID $tofile"
fi
elif [[ "${to:0:4}" == "ftp:" ]] ; then
if [[ "$cert" != "challenge token" ]] ; then
error_exit "ftp is not a sercure method for copying certificates or keys"
fi
debug "using ftp to copy the file from $from"
ftpuser=$(echo "$to"| awk -F: '{print $2}')
ftppass=$(echo "$to"| awk -F: '{print $3}')
ftphost=$(echo "$to"| awk -F: '{print $4}')
ftplocn=$(echo "$to"| awk -F: '{print $5}')
ftpdirn=$(dirname "$ftplocn")
ftpfile=$(basename "$ftplocn")
fromdir=$(dirname "$from")
fromfile=$(basename "$from")
debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile"
debug "from dir=$fromdir file=$fromfile"
ftp -n <<- _EOF
open $ftphost
user $ftpuser $ftppass
cd $ftpdirn
lcd $fromdir
put $fromfile
_EOF
elif [[ "${to:0:5}" == "sftp:" ]] ; then
debug "using sftp to copy the file from $from"
ftpuser=$(echo "$to"| awk -F: '{print $2}')
ftppass=$(echo "$to"| awk -F: '{print $3}')
ftphost=$(echo "$to"| awk -F: '{print $4}')
ftplocn=$(echo "$to"| awk -F: '{print $5}')
ftpdirn=$(dirname "$ftplocn")
ftpfile=$(basename "$ftplocn")
fromdir=$(dirname "$from")
fromfile=$(basename "$from")
debug "sftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile"
debug "from dir=$fromdir file=$fromfile"
sshpass -p "$ftppass" sftp "$ftpuser@$ftphost" <<- _EOF
cd $ftpdirn
lcd $fromdir
put $fromfile
_EOF
else
if ! mkdir -p "$(dirname "$to")" ; then
error_exit "cannot create ACL directory $(basename "$to")"
fi
if [[ "$GETSSLD_IGNORE_CP_PRESERVE" == "true" ]]; then
if ! cp "$from" "$to" ; then
error_exit "cannot copy $from to $to"
fi
else
if ! cp -p "$from" "$to" ; then
error_exit "cannot copy $from to $to"
fi
fi
if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then
chown "$TOKEN_USER_ID" "$to"
fi
fi
debug "copied $from to $to"
done
}
create_csr() { # create a csr using a given key (if it doesn't already exist)
csr_file=$1
csr_key=$2
# check if domain csr exists - if not then create it
if [[ -s "$csr_file" ]]; then
debug "domain csr exists at - $csr_file"
# check all domains in config are in csr
if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
alldomains=$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u)
else
alldomains=$(echo "$DOMAIN,$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u)
fi
domains_in_csr=$(openssl req -text -noout -in "$csr_file" \
| sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \
| sort -u)
for d in $alldomains; do
if [[ "$(echo "${domains_in_csr}"| grep "^${d}$")" != "${d}" ]]; then
info "existing csr at $csr_file does not contain ${d} - re-create-csr"\
".... $(echo "${domains_in_csr}"| grep "^${d}$")"
_RECREATE_CSR="true"
fi
done
# check all domains in csr are in config
if [[ "$alldomains" != "$domains_in_csr" ]]; then
info "existing csr at $csr_file does not have the same domains as the config - re-create-csr"
_RECREATE_CSR="true"
fi
fi
# end of ... check if domain csr exists - if not then create it
# if CSR does not exist, or flag set to recreate, then create csr
if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "true" ]]; then
info "creating domain csr - $csr_file"
# create a temporary config file, for portability.
tmp_conf=$(mktemp)
cat "$SSLCONF" > "$tmp_conf"
printf "[SAN]\n%s" "$SANLIST" >> "$tmp_conf"
# add OCSP Must-Staple to the domain csr
# if openssl version >= 1.1.0 one can also use "tlsfeature = status_request"
if [[ "$OCSP_MUST_STAPLE" == "true" ]]; then
printf "\n1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05" >> "$tmp_conf"
fi
openssl req -new -sha256 -key "$csr_key" -subj "$CSR_SUBJECT" -reqexts SAN -config "$tmp_conf" > "$csr_file"
rm -f "$tmp_conf"
fi
}
create_key() { # create a domain key (if it doesn't already exist)
key_type=$1 # domain key type
key_loc=$2 # domain key location
key_len=$3 # domain key length - for rsa keys.
# check if key exists, if not then create it.
if [[ -s "$key_loc" ]]; then
debug "domain key exists at $key_loc - skipping generation"
# ideally need to check validity of domain key
else
umask 077
info "creating key - $key_loc"
case "$key_type" in
rsa)
openssl genrsa "$key_len" > "$key_loc";;
prime256v1|secp384r1|secp521r1)
openssl ecparam -genkey -name "$key_type" > "$key_loc";;
*)
error_exit "unknown private key algorithm type $key_loc";;
esac
umask "$ORIG_UMASK"
# remove csr on generation of new domain key
if [[ -e "${key_loc::-4}.csr" ]]; then
rm -f "${key_loc::-4}.csr"
fi
fi
}
date_epoc() { # convert the date into epoch time
date -D "%b %d %T %Y" -d "$(echo "$1" | awk '{print $1 $2 $3 $4}')" +%s
}
date_fmt() { # format date from epoc time to YYYY-MM-DD
date -d "@$1" +%F
}
date_renew() { # calculates the renewal time in epoch
date_now_s=$( date +%s )
echo "$((date_now_s + RENEW_ALLOW*24*60*60))"
}
debug() { # write out debug info if the debug flag has been set
if [[ ${_USE_DEBUG} == "true" ]]; then
echo " "
echo "$@"
fi
}
error_exit() { # give error message on error exit
echo -e "${PROGNAME}: ${1:-"Unknown Error"}" >&2
clean_up
exit 1
}
error() {
# Write error message to STDERR for log.
echo "$@" >&2
}
get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns )
gad_d="$1" # domain name
gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER
res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" ${gad_s})
if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then
# this is a Non-authoritative server, need to check for an authoritative one.
gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g')
if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then
# if domain name doesn't exist, then find auth servers for next level up
gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }')
gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}')
fi
fi
if [[ -z "$gad_s" ]]; then
res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d")
else
res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" "${gad_s}")
fi
if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then
gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g')
elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then
gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }')
gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}')
fi
all_auth_dns_servers=$(nslookup -type=soa -type=ns "$gad_d" "$gad_s" \
| awk ' $2 ~ "nameserver" {print $4}' \
| sed 's/\.$//g'| tr '\n' ' ')
if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then
primary_ns="$all_auth_dns_servers"
else
primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}')
fi
}
get_certificate() { # get certificate for csr, if all domains validated.
gc_csr=$1 # the csr file
gc_certfile=$2 # The filename for the certificate
gc_cafile=$3 # The filename for the CA certificate
der=$(openssl req -in "$gc_csr" -outform DER | urlbase64)
debug "der $der"
send_signed_request "$URL_new_cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64"
# convert certificate information into correct format and save to file.
CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r')
debug "certdata location = $CertData"
if [[ "$CertData" ]] ; then
echo -----BEGIN CERTIFICATE----- > "$gc_certfile"
curl --silent "$CertData" | openssl base64 -e >> "$gc_certfile"
echo -----END CERTIFICATE----- >> "$gc_certfile"
info "Certificate saved in $CERT_FILE"
fi
# If certificate wasn't a valid certificate, error exit.
if [[ -z "$CertData" ]] ; then
response2=$(echo "$response" | fold -w64 |openssl base64 -d)
debug "response was $response"
error_exit "Sign failed: $(echo "$response2" | grep "detail")"
fi
# get a copy of the CA certificate.
IssuerData=$(grep -i '^Link' "$CURL_HEADER" \
| cut -d " " -f 2\
| cut -d ';' -f 1 \
| sed 's/<//g' \
| sed 's/>//g')
if [[ "$IssuerData" ]] ; then
echo -----BEGIN CERTIFICATE----- > "$gc_cafile"
curl --silent "$IssuerData" | openssl base64 -e >> "$gc_cafile"
echo -----END CERTIFICATE----- >> "$gc_cafile"
info "The intermediate CA cert is in $gc_cafile"
fi
}
get_cr() { # get curl response
url="$1"
debug url "$url"
response=$(curl --silent "$url")
ret=$?
debug response "$response"
code=$(json_get "$response" status)
debug code "$code"
debug "get_cr return code $ret"
return $ret
}
get_signing_params() { # get signing parameters from key
skey=$1
if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key
pub_exp64=$(openssl rsa -in "${skey}" -noout -text \
| grep publicExponent \
| grep -oE "0x[a-f0-9]+" \
| cut -d'x' -f2 \
| hex2bin \
| urlbase64)
pub_mod64=$(openssl rsa -in "${skey}" -noout -modulus \
| cut -d'=' -f2 \
| hex2bin \
| urlbase64)
jwk='{"e":"'"${pub_exp64}"'","kty":"RSA","n":"'"${pub_mod64}"'"}'
jwkalg="RS256"
signalg="sha256"
elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key.
crv="$(openssl ec -in "$skey" -noout -text 2>/dev/null | awk '$2 ~ "CURVE:" {print $3}')"
if [[ -z "$crv" ]]; then
gsp_keytype="$(openssl ec -in "$skey" -noout -text 2>/dev/null \
| grep "^ASN1 OID:" \
| awk '{print $3}')"
case "$gsp_keytype" in
prime256v1) crv="P-256" ;;
secp384r1) crv="P-384" ;;
secp521r1) crv="P-521" ;;
*) error_exit "invalid curve algorithm type $gsp_keytype";;
esac
fi
case "$crv" in
P-256) jwkalg="ES256" ; signalg="sha256" ;;
P-384) jwkalg="ES384" ; signalg="sha384" ;;
P-521) jwkalg="ES512" ; signalg="sha512" ;;
*) error_exit "invalid curve algorithm type $crv";;
esac
pubtext="$(openssl ec -in "$skey" -noout -text 2>/dev/null \
| awk '/^pub:/{p=1;next}/^ASN1 OID:/{p=0}p' \
| tr -d ": \n\r")"
mid=$(( (${#pubtext} -2) / 2 + 2 ))
debug "pubtext = $pubtext"
x64=$(echo "$pubtext" | cut -b 3-$mid | hex2bin | urlbase64)
y64=$(echo "$pubtext" | cut -b $((mid+1))-${#pubtext} | hex2bin | urlbase64)
jwk='{"crv":"'"$crv"'","kty":"EC","x":"'"$x64"'","y":"'"$y64"'"}'
debug "jwk $jwk"
else
error_exit "Invalid key file"
fi
thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)"
debug "jwk alg = $jwkalg"
debug "jwk = $jwk"
debug "thumbprint $thumbprint"
}
graceful_exit() { # normal exit function.
clean_up
exit
}
help_message() { # print out the help message
cat <<- _EOF_
Obtain SSL certificates from the letsencrypt.org ACME server
$(usage)
Options:
-a, --all Check all certificates
-d, --debug Output debug information
-c, --create DOMAIN Create default configuration files
-f, --force Force renewal of cert - override expiry checks
-h, --help Display this help message and exit
-q, --quiet Quiet mode - only outputs on error or success of new cert
-r, --revoke CERT KEY [CA SERVER] Revoke a certificate
_EOF_
}
hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char
echo -e -n "$(cat | sed -r -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
}
info() { # write out info as long as the quiet flag has not been set.
if [[ ${_QUIET} != "true" ]]; then
echo "$@"
fi
}
json_get() { # get the value corresponding to $2 in the JSON passed as $1.
# remove newlines, so it's a single chunk of JSON
json_data=$( echo "$1" | tr '\n' ' ')
# if $3 is defined, this is the section which the item is in.
if [[ ! -z "$3" ]]; then
jg_section=$(echo "$json_data" | awk -F"[}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${3}"'\"/){print $i}}}')
if [[ "$2" == "uri" ]]; then
jg_subsect=$(echo "$jg_section" | awk -F"[,]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i)}}}')
jg_result=$(echo "$jg_subsect" | awk -F'"' '{print $4}')
else
jg_result=$(echo "$jg_section" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}')
fi
else
jg_result=$(echo "$json_data" |awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}')
fi
# check number of quotes
jg_q=${jg_result//[^\"]/}
# if 2 quotes, assume it's a quoted variable and just return the data within the quotes.
if [[ ${#jg_q} -eq 2 ]]; then
echo "$jg_result" | awk -F'"' '{print $2}'
else
echo "$jg_result"
fi
}
purge_archive() { # purge archive of old, invalid, certificates
arcdir="$1/archive"
debug "purging archives in ${arcdir}/"
for padir in $arcdir/????_??_??_??_??; do
# check each directory
if [[ -d "$padir" ]]; then
tstamp=$(basename "$padir"| awk -F"_" '{print $1"-"$2"-"$3" "$4":"$5}')
direpoc=$(date -d "$tstamp" +%s)
current_epoc=$(date "+%s")
# as certs currently valid for 90 days, purge anything older than 100
purgedate=$((current_epoc - 60*60*24*100))
if [[ "$direpoc" -lt "$purgedate" ]]; then
echo "purge $padir"
rm -rf "${padir:?}"
fi
fi
done
}
reload_service() { # Runs a command to reload services ( via ssh if needed)
if [[ ! -z "$RELOAD_CMD" ]]; then
info "reloading SSL services"
if [[ "${RELOAD_CMD:0:4}" == "ssh:" ]] ; then
sshhost=$(echo "$RELOAD_CMD"| awk -F: '{print $2}')
command=${RELOAD_CMD:(( ${#sshhost} + 5))}
debug "running following command to reload cert"
debug "ssh $sshhost ${command}"
# shellcheck disable=SC2029
ssh "$sshhost" "${command}" 1>/dev/null 2>&1
# allow 2 seconds for services to restart
sleep 2
else
debug "running reload command $RELOAD_CMD"
if ! eval "$RELOAD_CMD" ; then
error_exit "error running $RELOAD_CMD"
fi
fi
fi
}
revoke_certificate() { # revoke a certificate
debug "revoking cert $REVOKE_CERT"
debug "using key $REVOKE_KEY"
ACCOUNT_KEY="$REVOKE_KEY"
# need to set the revoke key as "account_key" since it's used in send_signed_request.
get_signing_params "$REVOKE_KEY"
TEMP_DIR=$(mktemp -d)
debug "revoking from $CA"
rcertdata=$(openssl x509 -in "$REVOKE_CERT" -inform PEM -outform DER | urlbase64)
send_signed_request "$URL_revoke" "{\"resource\": \"revoke-cert\", \"certificate\": \"$rcertdata\"}"
if [[ $code -eq "200" ]]; then
info "certificate revoked"
else
error_exit "Revocation failed: $(echo "$response" | grep "detail")"
fi
}
requires() { # check if required function is available
if [[ "$#" -gt 1 ]]; then # if more than 1 value, check list
for i in "$@"; do
if [[ "$i" == "${!#}" ]]; then # if on last variable then exit as not found
error_exit "this script requires one of: ${*:1:$(($#-1))}"
fi
res=$(which "$i" 2>/dev/null)
debug "checking for $i ... $res"
if [[ ! -z "$res" ]]; then # if function found, then set variable to function and return
debug "function $i found at $res - setting ${!#} to $i"
eval "${!#}=\$i"
return
fi
done
else # only one value, so check it.
result=$(which "$1" 2>/dev/null)
debug "checking for required $1 ... $result"
if [[ -z "$result" ]]; then
error_exit "This script requires $1 installed"
fi
fi
}
set_server_type() { # uses SERVER_TYPE to set REMOTE_PORT and REMOTE_EXTRA
if [[ ${SERVER_TYPE} == "https" ]] || [[ ${SERVER_TYPE} == "webserver" ]]; then
REMOTE_PORT=443
elif [[ ${SERVER_TYPE} == "ftp" ]]; then
REMOTE_PORT=21
REMOTE_EXTRA="-starttls ftp"
elif [[ ${SERVER_TYPE} == "ftpi" ]]; then
REMOTE_PORT=990
elif [[ ${SERVER_TYPE} == "imap" ]]; then
REMOTE_PORT=143
REMOTE_EXTRA="-starttls imap"
elif [[ ${SERVER_TYPE} == "imaps" ]]; then
REMOTE_PORT=993
elif [[ ${SERVER_TYPE} == "pop3" ]]; then
REMOTE_PORT=110
REMOTE_EXTRA="-starttls pop3"
elif [[ ${SERVER_TYPE} == "pop3s" ]]; then
REMOTE_PORT=995
elif [[ ${SERVER_TYPE} == "smtp" ]]; then
REMOTE_PORT=25
REMOTE_EXTRA="-starttls smtp"
elif [[ ${SERVER_TYPE} == "smtps_deprecated" ]]; then
REMOTE_PORT=465
elif [[ ${SERVER_TYPE} == "smtps" ]] || [[ ${SERVER_TYPE} == "smtp_submission" ]]; then
REMOTE_PORT=587
REMOTE_EXTRA="-starttls smtp"
elif [[ ${SERVER_TYPE} == "xmpp" ]]; then
REMOTE_PORT=5222
REMOTE_EXTRA="-starttls xmpp"
elif [[ ${SERVER_TYPE} == "xmpps" ]]; then
REMOTE_PORT=5269
elif [[ ${SERVER_TYPE} == "ldaps" ]]; then
REMOTE_PORT=636
elif [[ ${SERVER_TYPE} =~ ^[0-9]+$ ]]; then
REMOTE_PORT=${SERVER_TYPE}
else
info "${DOMAIN}: unknown server type \"$SERVER_TYPE\" in SERVER_TYPE"
config_errors=true
fi
}
send_signed_request() { # Sends a request to the ACME server, signed with your private key.
url=$1
payload=$2
needbase64=$3
debug url "$url"
debug payload "$payload"
CURL_HEADER="$TEMP_DIR/curl.header"
dp="$TEMP_DIR/curl.dump"
CURL="curl --silent --dump-header $CURL_HEADER "
if [[ ${_USE_DEBUG} == "true" ]]; then
CURL="$CURL --trace-ascii $dp "
fi
# convert payload to url base 64
payload64="$(printf '%s' "${payload}" | urlbase64)"
debug payload64 "$payload64"
# get nonce from ACME server
nonceurl="$CA/directory"
nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ')
debug nonce "$nonce"
# Build header with just our public key and algorithm information
header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}'
# Build another header which also contains the previously received nonce and encode it as urlbase64
protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}'
protected64="$(printf '%s' "${protected}" | urlbase64)"
debug protected "$protected"
# Sign header with nonce and our payload with our private key and encode signature as urlbase64
sign_string "$(printf '%s' "${protected64}.${payload64}")" "${ACCOUNT_KEY}" "$signalg"
# Send header + extended header + payload + signature to the acme-server
body="{\"header\": ${header},"
body="${body}\"protected\": \"${protected64}\","
body="${body}\"payload\": \"${payload64}\","
body="${body}\"signature\": \"${signed64}\"}"
debug "header, payload and signature = $body"
code="500"
loop_limit=5
while [[ "$code" -eq 500 ]]; do
if [[ "$needbase64" ]] ; then
response=$($CURL -X POST --data "$body" "$url" | urlbase64)
else
response=$($CURL -X POST --data "$body" "$url")
fi
responseHeaders=$(cat "$CURL_HEADER")
debug responseHeaders "$responseHeaders"
debug response "$response"
code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1)
debug code "$code"
response_status=$(json_get "$response" status \
| head -1| awk -F'"' '{print $2}')
debug "response status = $response_status"
if [[ "$code" -eq 500 ]]; then
info "error on acme server - trying again ...."
sleep 2
loop_limit=$((loop_limit - 1))
if [[ $loop_limit -lt 1 ]]; then
error_exit "500 error from ACME server: $response"
fi
fi
done
}
sign_string() { # sign a string with a given key and algorithm and return urlbase64
# sets the result in variable signed64
str=$1
key=$2
signalg=$3
if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key
signed64="$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" | urlbase64)"
elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key.
signed=$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" -hex | awk '{print $2}')
debug "EC signature $signed"
if [[ "${signed:4:4}" == "0220" ]]; then #sha256
R=$(echo "$signed" | cut -c 9-72)
part2=$(echo "$signed" | cut -c 73-)
elif [[ "${signed:4:4}" == "0221" ]]; then #sha256
R=$(echo "$signed" | cut -c 11-74)
part2=$(echo "$signed" | cut -c 75-)
elif [[ "${signed:4:4}" == "0230" ]]; then #sha384
R=$(echo "$signed" | cut -c 9-104)
part2=$(echo "$signed" | cut -c 105-)
elif [[ "${signed:4:4}" == "0231" ]]; then #sha384
R=$(echo "$signed" | cut -c 11-106)
part2=$(echo "$signed" | cut -c 107-)
elif [[ "${signed:6:4}" == "0241" ]]; then #sha512
R=$(echo "$signed" | cut -c 11-140)
part2=$(echo "$signed" | cut -c 141-)
elif [[ "${signed:6:4}" == "0242" ]]; then #sha512
R=$(echo "$signed" | cut -c 11-142)
part2=$(echo "$signed" | cut -c 143-)
else
error_exit "error in EC signing couldn't get R from $signed"
fi
debug "R $R"
if [[ "${part2:0:4}" == "0220" ]]; then #sha256
S=$(echo "$part2" | cut -c 5-68)
elif [[ "${part2:0:4}" == "0221" ]]; then #sha256
S=$(echo "$part2" | cut -c 7-70)
elif [[ "${part2:0:4}" == "0230" ]]; then #sha384
S=$(echo "$part2" | cut -c 5-100)
elif [[ "${part2:0:4}" == "0231" ]]; then #sha384
S=$(echo "$part2" | cut -c 7-102)
elif [[ "${part2:0:4}" == "0241" ]]; then #sha512
S=$(echo "$part2" | cut -c 5-136)
elif [[ "${part2:0:4}" == "0242" ]]; then #sha512
S=$(echo "$part2" | cut -c 5-136)
else
error_exit "error in EC signing couldn't get S from $signed"
fi
debug "S $S"
signed64=$(printf '%s' "${R}${S}" | hex2bin | urlbase64 )
debug "encoded RS $signed64"
fi
}
signal_exit() { # Handle trapped signals
case $1 in
INT)
error_exit "Program interrupted by user" ;;
TERM)
echo -e "\n$PROGNAME: Program terminated" >&2
graceful_exit ;;
*)
error_exit "$PROGNAME: Terminating on unknown signal" ;;
esac
}
urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_'
openssl base64 -e | tr -d '\n\r' | sed -r -e 's:=*$::g' -e 'y:+/:-_:'
}
usage() { # echos out the program usage
echo "Usage: $PROGNAME [options] [ARGS]"
}
write_domain_template() { # write out a template file for a domain.
cat > "$1" <<- _EOF_domain_
# Uncomment and modify any variables you need
# see https://github.com/dschaper/getsslD/wiki/Config-variables for details
# see https://github.com/dschaper/getsslD/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"
#PRIVATE_KEY_ALG="rsa"
# Additional domains - this could be multiple domains / subdomains in a comma separated list
# Note: this is Additional domains - so should not include the primary domain.
SANS="${EX_SANS}"
# Acme Challenge Location. The first line for the domain, the following ones for each additional domain.
# If these start with ssh: then the next variable is assumed to be the hostname and the rest the location.
# An ssh key will be needed to provide you with access to the remote server.
# Optionally, you can specify a different userid for ssh/scp to use on the remote server before the @ sign.
# If left blank, the username on the local server will be used to authenticate against the remote server.
# If these start with ftp: then the next variables are ftpuserid:ftppassword:servername:ACL_location
# These should be of the form "/path/to/your/website/folder/.well-known/acme-challenge"
# where "/path/to/your/website/folder/" is the path, on your web server, to the web root for your domain.
#ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge'
# 'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge'
# 'ssh:sshuserid@server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge'
# 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge')
#Set USE_SINGLE_ACL="true" to use a single ACL for all checks
#USE_SINGLE_ACL="false"
# Location for all your certs, these can either be on the server (full path name)
# or using ssh /sftp as for the ACL
#DOMAIN_CERT_LOCATION="/etc/ssl/${DOMAIN}.crt"
#DOMAIN_KEY_LOCATION="/etc/ssl/${DOMAIN}.key"
#CA_CERT_LOCATION="/etc/ssl/chain.crt"
#DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert
#DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert
# The command needed to reload apache / nginx or whatever you use
#RELOAD_CMD=""
# Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp,
# smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which
# will be checked for certificate expiry and also will be checked after
# an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true
#SERVER_TYPE="https"
#CHECK_REMOTE="true"
_EOF_domain_
}
write_getsslD_template() { # write out the main template file
cat > "$1" <<- _EOF_getsslD_
# Uncomment and modify any variables you need
# see https://github.com/dschaper/getsslD/wiki/Config-variables for details
#
# The staging server is best for testing (hence set as default)
CA="https://acme-staging.api.letsencrypt.org"
# This server issues full certificates, however has rate limits
#CA="https://acme-v01.api.letsencrypt.org"
#AGREEMENT="$AGREEMENT"
# Set an email address associated with your account - generally set at account level rather than domain.
#ACCOUNT_EMAIL="me@example.com"
ACCOUNT_KEY_LENGTH=4096
ACCOUNT_KEY="$WORKING_DIR/account.key"
PRIVATE_KEY_ALG="rsa"
#REUSE_PRIVATE_KEY="true"
# The command needed to reload apache / nginx or whatever you use
#RELOAD_CMD=""
# The time period within which you want to allow renewal of a certificate
# this prevents hitting some of the rate limits.
RENEW_ALLOW="30"
# 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"
# Use the following 3 variables if you want to validate via DNS
#VALIDATE_VIA_DNS="true"
#DNS_ADD_COMMAND=
#DNS_DEL_COMMAND=
_EOF_getsslD_
}
write_openssl_conf() { # write out a minimal openssl conf
cat > "$1" <<- _EOF_openssl_conf_
# minimal openssl.cnf file
distinguished_name = req_distinguished_name
[ req_distinguished_name ]
[v3_req]
[v3_ca]
_EOF_openssl_conf_
}
# Trap signals
trap "signal_exit TERM" TERM HUP
trap "signal_exit INT" INT
# Parse command-line
while [[ -n ${1+defined} ]]; do
case $1 in
-h | --help)
help_message; graceful_exit ;;
-d | --debug)
_USE_DEBUG="true" ;;
-c | --create)
_CREATE_CONFIG="true" ;;
-f | --force)
_FORCE_RENEW="true" ;;
-a | --all)
_CHECK_ALL="true" ;;
-q | --quiet)
_QUIET="true" ;;
-r | --revoke)
_REVOKE="true"
shift
REVOKE_CERT="$1"
shift
REVOKE_KEY="$1"
shift
REVOKE_CA="$1" ;;
-w)
shift; WORKING_DIR="$1" ;;
-* | --*)
usage
error_exit "Unknown option $1" ;;
*)
if [[ ! -z $DOMAIN ]]; then
error_exit "invalid command line $DOMAIN - it appears to contain more than one domain"
fi
DOMAIN="$1"
if [[ -z $DOMAIN ]]; then
error_exit "invalid command line - it appears to contain a null variable"
fi ;;
esac
shift
done
# Main logic
############
# Revoke a certificate if requested
if [[ $_REVOKE == "true" ]]; then
if [[ -z $REVOKE_CA ]]; then
CA=$DEFAULT_REVOKE_CA
elif [[ "$REVOKE_CA" == "-d" ]]; then
_USE_DEBUG="true"
CA=$DEFAULT_REVOKE_CA
else
CA=$REVOKE_CA
fi
URL_revoke=$(curl "${CA}/directory" 2>/dev/null | grep "revoke-cert" | awk -F'"' '{print $4}')
revoke_certificate
graceful_exit
fi
# get latest agreement from CA (as default)
AGREEMENT=$(curl -I "${CA}/terms" 2>/dev/null | awk '$1 ~ "Location:" {print $2}'|tr -d '\r')
# if nothing in command line, print help and exit.
if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} != "true" ]]; then
error "Domain is required for this option."
help_message
graceful_exit
fi
# if the "working directory" doesn't exist, then create it.
if [[ ! -d "$WORKING_DIR" ]]; then
debug "Making working directory - $WORKING_DIR"
mkdir -p "$WORKING_DIR"
fi
# read any variables from config in working directory
if [[ -s "$WORKING_DIR/getsslD.cfg" ]]; then
debug "reading config from $WORKING_DIR/getsslD.cfg"
# shellcheck source=/dev/null
. "$WORKING_DIR/getsslD.cfg"
fi
# Define defaults for variables not set in the main config.
ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account.key}"
DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}"
DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN"
CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt"
CA_CERT="$DOMAIN_DIR/chain.crt"
TEMP_DIR="$DOMAIN_DIR/tmp"
# Set the OPENSSL_CONF environment variable so openssl knows which config to use
export OPENSSL_CONF=$SSLCONF
# if "-a" option then check other parameters and create run for each domain.
if [[ ${_CHECK_ALL} == "true" ]]; then
info "Check all certificates"
if [[ ${_CREATE_CONFIG} == "true" ]]; then
error_exit "cannot combine -c|--create with -a|--all"
fi
if [[ ${_FORCE_RENEW} == "true" ]]; then
error_exit "cannot combine -f|--force with -a|--all because of rate limits"
fi
if [[ ! -d "$DOMAIN_STORAGE" ]]; then
error_exit "DOMAIN_STORAGE not found - $DOMAIN_STORAGE"
fi
for dir in ${DOMAIN_STORAGE}/*; do
if [[ -d "$dir" ]]; then
debug "Checking $dir"
cmd="$0"
if [[ ${_USE_DEBUG} == "true" ]]; then
cmd="$cmd -d"
fi
if [[ ${_QUIET} == "true" ]]; then
cmd="$cmd -q"
fi
# check if $dir looks like a domain name (contains a period)
if [[ $(basename "$dir") == *.* ]]; then
cmd="$cmd -w $WORKING_DIR $(basename "$dir")"
debug "CMD: $cmd"
eval "$cmd"
fi
fi
done
graceful_exit
fi
# end of "-a" option (looping through all domains)
# if "-c|--create" option used, then create config files.
if [[ ${_CREATE_CONFIG} == "true" ]]; then
# If main config file does not exists then create it.
if [[ ! -s "$WORKING_DIR/getsslD.cfg" ]]; then
info "creating main config file $WORKING_DIR/getsslD.cfg"
if [[ ! -s "$SSLCONF" ]]; then
SSLCONF="$WORKING_DIR/openssl.cnf"
write_openssl_conf "$SSLCONF"
fi
write_getsslD_template "$WORKING_DIR/getsslD.cfg"
fi
# If domain and domain config don't exist then create them.
if [[ ! -d "$DOMAIN_DIR" ]]; then
info "Making domain directory - $DOMAIN_DIR"
mkdir -p "$DOMAIN_DIR"
fi
if [[ -s "$DOMAIN_DIR/getsslD.cfg" ]]; then
info "domain config already exists $DOMAIN_DIR/getsslD.cfg"
else
info "creating domain config file in $DOMAIN_DIR/getsslD.cfg"
# if domain has an existing cert, copy from domain and use to create defaults.
EX_CERT=$(echo \
| openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:443" 2>/dev/null \
| openssl x509 2>/dev/null)
EX_SANS="www.${DOMAIN}"
if [[ ! -z "${EX_CERT}" ]]; then
EX_SANS=$(echo "$EX_CERT" \
| openssl x509 -noout -text 2>/dev/null| grep "Subject Alternative Name" -A2 \
| grep -Eo "DNS:[a-zA-Z 0-9.-]*" | sed "s@DNS:$DOMAIN@@g" | grep -v '^$' | cut -c 5-)
EX_SANS=${EX_SANS//$'\n'/','}
fi
write_domain_template "$DOMAIN_DIR/getsslD.cfg"
fi
TEMP_DIR="$DOMAIN_DIR/tmp"
# end of "-c|--create" option, so exit
graceful_exit
fi
# end of "-c|--create" option to create config file.
# if domain directory doesn't exist, then create it.
if [[ ! -d "$DOMAIN_DIR" ]]; then
debug "Making working directory - $DOMAIN_DIR"
mkdir -p "$DOMAIN_DIR"
fi
# define a temporary directory, and if it doesn't exist, create it.
TEMP_DIR="$DOMAIN_DIR/tmp"
if [[ ! -d "${TEMP_DIR}" ]]; then
debug "Making temp directory - ${TEMP_DIR}"
mkdir -p "${TEMP_DIR}"
fi
# read any variables from config in domain directory
if [[ -s "$DOMAIN_DIR/getsslD.cfg" ]]; then
debug "reading config from $DOMAIN_DIR/getsslD.cfg"
# shellcheck source=/dev/null
. "$DOMAIN_DIR/getsslD.cfg"
fi
# from SERVER_TYPE set REMOTE_PORT and REMOTE_EXTRA
set_server_type
# check config for typical errors.
check_config
if [[ -e "$DOMAIN_DIR/FORCE_RENEWAL" ]]; then
rm -f "$DOMAIN_DIR/FORCE_RENEWAL" || error_exit "problem deleting file $DOMAIN_DIR/FORCE_RENEWAL"
_FORCE_RENEW="true"
info "${DOMAIN}: forcing renewal (due to FORCE_RENEWAL file)"
fi
# Obtain CA resource locations
ca_all_loc=$(curl "${CA}/directory" 2>/dev/null)
URL_new_reg=$(echo "$ca_all_loc" | grep "new-reg" | awk -F'"' '{print $4}')
URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}')
URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}')
# if check_remote is true then connect and obtain the current certificate (if not forcing renewal)
if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW != "true" ]]; then
debug "getting certificate for $DOMAIN from remote server"
# shellcheck disable=SC2086
EX_CERT=$(echo \
| openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \
| openssl x509 2>/dev/null)
if [[ ! -z "$EX_CERT" ]]; then # if obtained a cert
if [[ -s "$CERT_FILE" ]]; then # if local exists
CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null)
else # since local doesn't exist leave empty so that the domain validation will happen
CERT_LOCAL=""
fi
CERT_REMOTE=$(echo "$EX_CERT" | openssl x509 -noout -fingerprint 2>/dev/null)
if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then
debug "certificate on server is same as the local cert"
else
# check if the certificate is for the right domain
EX_CERT_DOMAIN=$(echo "$EX_CERT" | openssl x509 -text \
| sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \
| sort -u | grep "^$DOMAIN\$")
if [[ "$EX_CERT_DOMAIN" == "$DOMAIN" ]]; then
# check renew-date on ex_cert and compare to local ( if local exists)
enddate_ex=$(echo "$EX_CERT" | openssl x509 -noout -enddate 2>/dev/null| cut -d= -f 2-)
enddate_ex_s=$(date_epoc "$enddate_ex")
debug "external cert has enddate $enddate_ex ( $enddate_ex_s ) "
if [[ -s "$CERT_FILE" ]]; then # if local exists
enddate_lc=$(openssl x509 -noout -enddate < "$CERT_FILE" 2>/dev/null| cut -d= -f 2-)
enddate_lc_s=$(date_epoc "$enddate_lc")
debug "local cert has enddate $enddate_lc ( $enddate_lc_s ) "
else
enddate_lc_s=0
debug "local cert doesn't exist"
fi
if [[ "$enddate_ex_s" -eq "$enddate_lc_s" ]]; then
debug "certificates expire at the same time"
elif [[ "$enddate_ex_s" -gt "$enddate_lc_s" ]]; then
# remote has longer to expiry date than local copy.
debug "remote cert has longer to run than local cert - ignoring"
else
info "${DOMAIN}: remote cert expires sooner than local, attempting to upload from local"
copy_file_to_location "domain certificate" \
"$CERT_FILE" \
"$DOMAIN_CERT_LOCATION"
copy_file_to_location "private key" \
"$DOMAIN_DIR/${DOMAIN}.key" \
"$DOMAIN_KEY_LOCATION"
copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION"
cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem"
copy_file_to_location "full pem" \
"$TEMP_DIR/${DOMAIN}_chain.pem" \
"$DOMAIN_CHAIN_LOCATION"
cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem"
copy_file_to_location "private key and domain cert pem" \
"$TEMP_DIR/${DOMAIN}_K_C.pem" \
"$DOMAIN_KEY_CERT_LOCATION"
cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem"
copy_file_to_location "full pem" \
"$TEMP_DIR/${DOMAIN}.pem" \
"$DOMAIN_PEM_LOCATION"
reload_service
fi
else
info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate"
fi
fi
else
info "${DOMAIN}: no certificate obtained from host"
fi
# end of .... if obtained a cert
fi
# end of .... check_remote is true then connect and obtain the current certificate
# if there is an existing certificate file, check details.
if [[ -s "$CERT_FILE" ]]; then
debug "certificate $CERT_FILE exists"
enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-)
debug "local cert is valid until $enddate"
if [[ "$enddate" != "-" ]]; then
enddate_s=$(date_epoc "$enddate")
if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW != "true" ]]; 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
debug "upgradeing from fake cert to real"
else
info "${DOMAIN}: certificate is valid for more than $RENEW_ALLOW days (until $enddate)"
# everything is OK, so exit.
graceful_exit
fi
else
debug "${DOMAIN}: certificate needs renewal"
fi
fi
fi
# end of .... if there is an existing certificate file, check details.
if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then
errmsg="$DOMAIN due for renewal,"
errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config"
error_exit "$errmsg"
fi
# create account key if it doesn't exist.
if [[ -s "$ACCOUNT_KEY" ]]; then
debug "Account key exists at $ACCOUNT_KEY skipping generation"
else
info "creating account key $ACCOUNT_KEY"
create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH"
fi
# if not reusing priavte key, then remove the old keys
if [[ "$REUSE_PRIVATE_KEY" != "true" ]]; then
if [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; then
rm -f "$DOMAIN_DIR/${DOMAIN}.key"
fi
if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then
rm -f "$DOMAIN_DIR/${DOMAIN}.ecs.key"
fi
fi
# create new domain keys if they don't already exist
if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then
create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH"
else
create_key "rsa" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH"
create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.ec.key" "$DOMAIN_KEY_LENGTH"
fi
# End of creating domain keys.
#create SAN
if [[ -z "$SANS" ]]; then
SANLIST="subjectAltName=DNS:${DOMAIN}"
elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
SANLIST="subjectAltName=DNS:${SANS//,/,DNS:}"
else
SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//,/,DNS:}"
fi
debug "created SAN list = $SANLIST"
#create CSR's
if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then
create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key"
else
create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key"
create_csr "$DOMAIN_DIR/${DOMAIN}.ec.csr" "$DOMAIN_DIR/${DOMAIN}.ec.key"
fi
# use account key to register with CA
# currently the code registers every time, and gets an "already registered" back if it has been.
get_signing_params "$ACCOUNT_KEY"
if [[ "$ACCOUNT_EMAIL" ]] ; then
regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}'
else
regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}'
fi
info "Registering account"
# send the request to the ACME server.
send_signed_request "$URL_new_reg" "$regjson"
if [[ "$code" == "" ]] || [[ "$code" == '201' ]] ; then
info "Registered"
echo "$response" > "$TEMP_DIR/account.json"
elif [[ "$code" == '409' ]] ; then
debug "Already registered"
else
error_exit "Error registering account ... $(json_get "$response" detail)"
fi
# end of registering account with CA
# verify each domain
info "Verify each domain"
# loop through domains for cert ( from SANS list)
if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
alldomains=${SANS//,/ }
else
alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")
fi
dn=0
for d in $alldomains; do
# $d is domain in current loop, which is number $dn for ACL
info "Verifying $d"
if [[ "$USE_SINGLE_ACL" == "true" ]]; then
DOMAIN_ACL="${ACL[0]}"
else
DOMAIN_ACL="${ACL[$dn]}"
fi
# request a challenge token from ACME server
request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"$d\"}}"
send_signed_request "$URL_new_authz" "$request"
debug "completed send_signed_request"
# check if we got a valid response and token, if not then error exit
if [[ ! -z "$code" ]] && [[ ! "$code" == '201' ]] ; then
error_exit "new-authz error: $response"
fi
if [[ $response_status == "valid" ]]; then
info "$d is already validated"
if [[ "$DEACTIVATE_AUTH" == "true" ]]; then
deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")"
deactivate_url_list+=" $deactivate_url "
debug "url added to deactivate list ${deactivate_url}"
debug "deactivate list is now $deactivate_url_list"
fi
# increment domain-counter
((dn++))
else
PREVIOUSLY_VALIDATED="false"
if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification
# get the dns component of the ACME response
# get the token from the dns component
token=$(json_get "$response" "token" "dns-01")
debug token "$token"
# get the uri from the dns component
uri=$(json_get "$response" "uri" "dns-01")
debug uri "$uri"
keyauthorization="$token.$thumbprint"
debug keyauthorization "$keyauthorization"
#create signed authorization key from token.
auth_key=$(printf '%s' "$keyauthorization" | openssl dgst -sha256 -binary \
| openssl base64 -e \
| tr -d '\n\r' \
| sed -e 's:=*$::g' -e 'y:+/:-_:')
debug auth_key "$auth_key"
debug "adding dns via command: $DNS_ADD_COMMAND $d $auth_key"
if ! eval "$DNS_ADD_COMMAND" "$d" "$auth_key" ; then
error_exit "DNS_ADD_COMMAND failed for domain $d"
fi
# find a primary / authoritative DNS server for the domain
if [[ -z "$AUTH_DNS_SERVER" ]]; then
get_auth_dns "$d"
else
primary_ns="$AUTH_DNS_SERVER"
fi
debug primary_ns "$primary_ns"
# make a directory to hold pending dns-challenges
if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then
mkdir "$TEMP_DIR/dns_verify"
fi
# generate a file with the current variables for the dns-challenge
cat > "$TEMP_DIR/dns_verify/$d" <<- _EOF_
token="${token}"
uri="${uri}"
keyauthorization="${keyauthorization}"
d="${d}"
primary_ns="${primary_ns}"
auth_key="${auth_key}"
_EOF_
else # set up the correct http token for verification
# get the token from the http component
token=$(json_get "$response" "token" "http-01")
debug token "$token"
# get the uri from the http component
uri=$(json_get "$response" "uri" "http-01")
debug uri "$uri"
#create signed authorization key from token.
keyauthorization="$token.$thumbprint"
debug keyauthorization "$keyauthorization"
# save variable into temporary file
echo -n "$keyauthorization" > "$TEMP_DIR/$token"
chmod 644 "$TEMP_DIR/$token"
# copy to token to acme challenge location
umask 0022
IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL"
for t_loc in "${token_locations[@]}"; do
debug "copying file from $TEMP_DIR/$token to ${t_loc}"
copy_file_to_location "challenge token" \
"$TEMP_DIR/$token" \
"${t_loc}/$token"
done
umask "$ORIG_UMASK"
wellknown_url="${CHALLENGE_CHECK_TYPE}://$d/.well-known/acme-challenge/$token"
debug wellknown_url "$wellknown_url"
if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then
info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly"
else
sleep "$HTTP_TOKEN_CHECK_WAIT"
# check that we can reach the challenge ourselves, if not, then error
if [[ ! "$(curl -k --silent --location "$wellknown_url")" == "$keyauthorization" ]]; then
error_exit "for some reason could not reach $wellknown_url - please check it manually"
fi
fi
check_challenge_completion "$uri" "$d" "$keyauthorization"
debug "remove token from ${DOMAIN_ACL}"
IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL"
for t_loc in "${token_locations[@]}"; do
if [[ "${t_loc:0:4}" == "ssh:" ]] ; then
sshhost=$(echo "${t_loc}"| awk -F: '{print $2}')
command="rm -f ${t_loc:(( ${#sshhost} + 5))}/${token:?}"
debug "running following command to remove token"
debug "ssh $sshhost ${command}"
# shellcheck disable=SC2029
ssh "$sshhost" "${command}" 1>/dev/null 2>&1
rm -f "${TEMP_DIR:?}/${token:?}"
elif [[ "${t_loc:0:4}" == "ftp:" ]] ; then
debug "using ftp to remove token file"
ftpuser=$(echo "${t_loc}"| awk -F: '{print $2}')
ftppass=$(echo "${t_loc}"| awk -F: '{print $3}')
ftphost=$(echo "${t_loc}"| awk -F: '{print $4}')
ftplocn=$(echo "${t_loc}"| awk -F: '{print $5}')
debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost location=$ftplocn"
ftp -n <<- EOF
open $ftphost
user $ftpuser $ftppass
cd $ftplocn
delete ${token:?}
EOF
else
rm -f "${t_loc:?}/${token:?}"
fi
done
fi
# increment domain-counter
((dn++))
fi
done # end of ... loop through domains for cert ( from SANS list)
# perform validation if via DNS challenge
if [[ $VALIDATE_VIA_DNS == "true" ]]; then
# loop through dns-variable files to check if dns has been changed
for dnsfile in $TEMP_DIR/dns_verify/*; do
if [[ -e "$dnsfile" ]]; then
debug "loading DNSfile: $dnsfile"
# shellcheck source=/dev/null
. "$dnsfile"
# check for token at public dns server, waiting for a valid response.
for ns in $primary_ns; do
debug "checking dns at $ns"
ntries=0
check_dns="fail"
while [[ "$check_dns" == "fail" ]]; do
check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \
| grep ^_acme|awk -F'"' '{ print $2}')
debug "expecting $auth_key"
debug "${ns} gave ... $check_result"
if [[ "$check_result" == *"$auth_key"* ]]; then
check_dns="success"
else
if [[ $ntries -lt 100 ]]; then
ntries=$(( ntries + 1 ))
info "checking DNS at ${ns} for ${d}. Attempt $ntries/100 gave wrong result, "\
"waiting $DNS_WAIT secs before checking again"
sleep $DNS_WAIT
else
debug "dns check failed - removing existing value"
error_exit "checking _acme-challenge.${d} gave $check_result not $auth_key"
fi
fi
done
done
fi
done
if [[ "$DNS_EXTRA_WAIT" -gt 0 && "$PREVIOUSLY_VALIDATED" != "true" ]]; then
info "sleeping $DNS_EXTRA_WAIT seconds before asking the ACME-server to check the dns"
sleep "$DNS_EXTRA_WAIT"
fi
# loop through dns-variable files to let the ACME server check the challenges
for dnsfile in $TEMP_DIR/dns_verify/*; do
if [[ -e "$dnsfile" ]]; then
debug "loading DNSfile: $dnsfile"
# shellcheck source=/dev/null
. "$dnsfile"
check_challenge_completion "$uri" "$d" "$keyauthorization"
debug "remove DNS entry"
eval "$DNS_DEL_COMMAND" "$d" "$auth_key"
# remove $dnsfile after each loop.
rm -f "$dnsfile"
fi
done
fi
# end of ... perform validation if via DNS challenge
#end of varify each domain.
# Verification has been completed for all SANS, so request certificate.
info "Verification completed, obtaining certificate."
#obtain the certificate.
get_certificate "$DOMAIN_DIR/${DOMAIN}.csr" \
"$CERT_FILE" \
"$CA_CERT"
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
get_certificate "$DOMAIN_DIR/${DOMAIN}.ec.csr" \
"${CERT_FILE::-4}.ec.crt" \
"${CA_CERT::-4}.ec.crt"
fi
# create Archive of new certs and keys.
cert_archive
debug "Certificates obtained and archived locally, will now copy to specified locations"
# copy certs to the correct location (creating concatenated files as required)
copy_file_to_location "domain certificate" "$CERT_FILE" "$DOMAIN_CERT_LOCATION"
copy_file_to_location "private key" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LOCATION"
copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION"
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
if [[ ! -z "$DOMAIN_CERT_LOCATION" ]]; then
copy_file_to_location "ec domain certificate" \
"${CERT_FILE::-4}.ec.crt" \
"${DOMAIN_CERT_LOCATION::-4}.ec.crt"
fi
if [[ ! -z "$DOMAIN_KEY_LOCATION" ]]; then
copy_file_to_location "ec private key" \
"$DOMAIN_DIR/${DOMAIN}.ec.key" \
"${DOMAIN_KEY_LOCATION::-4}.ec.key"
fi
if [[ ! -z "$CA_CERT_LOCATION" ]]; then
copy_file_to_location "ec CA certificate" \
"${CA_CERT::-4}.ec.crt" \
"${CA_CERT_LOCATION::-4}.ec.crt"
fi
fi
# if DOMAIN_CHAIN_LOCATION is not blank, then create and copy file.
if [[ ! -z "$DOMAIN_CHAIN_LOCATION" ]]; then
if [[ "$(dirname "$DOMAIN_CHAIN_LOCATION")" == "." ]]; then
to_location="${DOMAIN_DIR}/${DOMAIN_CHAIN_LOCATION}"
else
to_location="${DOMAIN_CHAIN_LOCATION}"
fi
cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem"
copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem" "$to_location"
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}_chain.pem.ec"
copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem.ec" "${to_location}.ec"
fi
fi
# if DOMAIN_KEY_CERT_LOCATION is not blank, then create and copy file.
if [[ ! -z "$DOMAIN_KEY_CERT_LOCATION" ]]; then
if [[ "$(dirname "$DOMAIN_KEY_CERT_LOCATION")" == "." ]]; then
to_location="${DOMAIN_DIR}/${DOMAIN_KEY_CERT_LOCATION}"
else
to_location="${DOMAIN_KEY_CERT_LOCATION}"
fi
cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem"
copy_file_to_location "private key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem" "$to_location"
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}_K_C.pem.ec"
copy_file_to_location "private ec key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" "${to_location}.ec"
fi
fi
# if DOMAIN_PEM_LOCATION is not blank, then create and copy file.
if [[ ! -z "$DOMAIN_PEM_LOCATION" ]]; then
if [[ "$(dirname "$DOMAIN_PEM_LOCATION")" == "." ]]; then
to_location="${DOMAIN_DIR}/${DOMAIN_PEM_LOCATION}"
else
to_location="${DOMAIN_PEM_LOCATION}"
fi
cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem"
copy_file_to_location "full key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem" "$to_location"
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "$TEMP_DIR/${DOMAIN}.pem.ec"
copy_file_to_location "full ec key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem.ec" "${to_location}.ec"
fi
fi
# end of copying certs.
# Run reload command to restart apache / nginx or whatever system
reload_service
# deactivate authorizations
if [[ "$DEACTIVATE_AUTH" == "true" ]]; then
debug "in deactivate list is $deactivate_url_list"
for deactivate_url in $deactivate_url_list; do
resp=$(curl "$deactivate_url" 2>/dev/null)
d=$(json_get "$resp" "hostname")
info "deactivating domain $d"
debug "deactivating $deactivate_url"
send_signed_request "$deactivate_url" "{\"resource\": \"authz\", \"status\": \"deactivated\"}"
# check response
if [[ "$code" == "200" ]]; then
debug "Authorization deactivated"
else
error_exit "$domain: Deactivation error: $code"
fi
done
fi
# end of deactivating authorizations
# Check if the certificate is installed correctly
if [[ ${CHECK_REMOTE} == "true" ]]; then
sleep "$CHECK_REMOTE_WAIT"
# shellcheck disable=SC2086
CERT_REMOTE=$(echo \
| openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \
| openssl x509 -noout -fingerprint 2>/dev/null)
CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null)
if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then
info "${DOMAIN} - certificate installed OK on server"
else
error_exit "${DOMAIN} - certificate obtained but certificate on server is different from the new certificate"
fi
fi
# end of Check if the certificate is installed correctly
# To have reached here, a certificate should have been successfully obtained.
# Use echo rather than info so that 'quiet' is ignored.
echo "certificate obtained for ${DOMAIN}"
# gracefully exit ( tidying up temporary files etc).
graceful_exit