_sed "" < /dev/null > /dev/null 2>&1 || _exiterr "This script requires sed with support for extended (modern) regular expressions."
command -v grep > /dev/null 2>&1 || _exiterr "This script requires grep."
command -v mktemp > /dev/null 2>&1 || _exiterr "This script requires mktemp."
- diff -u /dev/null /dev/null || _exiterr "This script requires diff."
+ command -v diff > /dev/null 2>&1 || _exiterr "This script requires diff."
# curl returns with an error code in some ancient versions so we have to catch that
set +e
OPENSSL_CNF="${__OPENSSL_CNF}"
RENEW_DAYS="${__RENEW_DAYS}"
IP_VERSION="${__IP_VERSION}"
+ ALT_NAMES=
}
# verify configuration values
if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then
_exiterr "Challenge type dns-01 needs a hook script for deployment... can not continue."
fi
- if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" ]]; then
+ if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" && ! "${COMMAND:-}" = "register" ]]; then
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
fi
[[ "${KEY_ALGO}" =~ ^(rsa|prime256v1|secp384r1)$ ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... can not continue."
# Default values
CA="https://acme-v01.api.letsencrypt.org/directory"
- LICENSE="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"
+ CA_TERMS="https://acme-v01.api.letsencrypt.org/terms"
+ LICENSE=
CERTDIR=
ACCOUNTDIR=
CHALLENGETYPE="http-01"
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
# Export some environment variables to be used in hook script
- export WELLKNOWN BASEDIR CERTDIR CONFIG
+ export WELLKNOWN BASEDIR CERTDIR CONFIG COMMAND
# Checking for private key ...
register_new_key="no"
else
# Check if private account key exists, if it doesn't exist yet generate a new one (rsa key)
if [[ ! -e "${ACCOUNT_KEY}" ]]; then
+ REAL_LICENSE="$(http_request head "${CA_TERMS}" | (grep Location: || true) | awk -F ': ' '{print $2}' | tr -d '\n\r')"
+ if [[ -z "${REAL_LICENSE}" ]]; then
+ printf '\n'
+ printf 'Error retrieving terms of service from certificate authority.\n'
+ printf 'Please set LICENSE in config manually.\n'
+ exit 1
+ fi
+ if [[ ! "${LICENSE}" = "${REAL_LICENSE}" ]]; then
+ if [[ "${PARAM_ACCEPT_TERMS:-}" = "yes" ]]; then
+ LICENSE="${REAL_LICENSE}"
+ else
+ printf '\n'
+ printf 'To use dehydrated with this certificate authority you have to agree to their terms of service which you can find here: %s\n\n' "${REAL_LICENSE}"
+ printf 'To accept these terms of service run `%s --register --accept-terms`.\n' "${0}"
+ exit 1
+ fi
+ fi
+
echo "+ Generating account key..."
_openssl genrsa -out "${ACCOUNT_KEY}" "${KEYSIZE}"
register_new_key="yes"
# If we generated a new private key in the step above we have to register it with the acme-server
if [[ "${register_new_key}" = "yes" ]]; then
echo "+ Registering account key with ACME server..."
- [[ ! -z "${CA_NEW_REG}" ]] || _exiterr "Certificate authority doesn't allow registrations."
- # If an email for the contact has been provided then adding it to the registration request
FAILED=false
- if [[ -n "${CONTACT_EMAIL}" ]]; then
- (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
- else
- (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+
+ if [[ -z "${CA_NEW_REG}" ]]; then
+ echo "Certificate authority doesn't allow registrations."
+ FAILED=true
+ fi
+
+ # If an email for the contact has been provided then adding it to the registration request
+ if [[ "${FAILED}" = "false" ]]; then
+ if [[ -n "${CONTACT_EMAIL}" ]]; then
+ (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+ else
+ (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
+ fi
fi
+
if [[ "${FAILED}" = "true" ]]; then
echo
echo
rm "${ACCOUNT_KEY}" "${ACCOUNT_KEY_JSON}"
exit 1
fi
+ elif [[ "${COMMAND:-}" = "register" ]]; then
+ echo "+ Account already registered!"
+ exit 0
fi
-
}
# Different sed version for different os types...
fi
if [[ ! "${statuscode:0:1}" = "2" ]]; then
- echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2
- echo >&2
- echo "Details:" >&2
- cat "${tempcont}" >&2
- echo >&2
- echo >&2
+ if [[ ! "${2}" = "${CA_TERMS}" ]] || [[ ! "${statuscode:0:1}" = "3" ]]; then
+ echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2
+ echo >&2
+ echo "Details:" >&2
+ cat "${tempcont}" >&2
+ echo >&2
+ echo >&2
- # An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins)
- if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then
- errtxt=`cat ${tempcont}`
- "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}"
- fi
+ # An exclusive hook for the {1}-request error might be useful (e.g., for sending an e-mail to admins)
+ if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]]; then
+ errtxt=`cat ${tempcont}`
+ "${HOOK}" "request_failure" "${statuscode}" "${errtxt}" "${1}"
+ fi
- rm -f "${tempcont}"
+ rm -f "${tempcont}"
- # Wait for hook script to clean the challenge if used
- if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token:+set}" ]]; then
- "${HOOK}" "clean_challenge" '' "${challenge_token}" "${keyauth}"
- fi
+ # Wait for hook script to clean the challenge if used
+ if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token:+set}" ]]; then
+ "${HOOK}" "clean_challenge" '' "${challenge_token}" "${keyauth}"
+ fi
- # remove temporary domains.txt file if used
- [[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}"
- exit 1
+ # remove temporary domains.txt file if used
+ [[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}"
+ exit 1
+ fi
fi
cat "${tempcont}"
reqtext="$( <<<"${csr}" openssl req -noout -text )"
if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then
# SANs used, extract these
- altnames="$( <<<"${reqtext}" grep -A1 '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$' | tail -n1 )"
+ altnames="$( <<<"${reqtext}" awk '/X509v3 Subject Alternative Name:/{print;getline;print;}' | tail -n1 )"
# split to one per line:
# shellcheck disable=SC1003
altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )"
echo " + Done!"
}
+# grep issuer cert uri from certificate
+get_issuer_cert_uri() {
+ certificate="${1}"
+ openssl x509 -in "${certificate}" -noout -text | (grep 'CA Issuers - URI:' | cut -d':' -f2-) || true
+}
+
+# walk certificate chain, retrieving all intermediate certificates
+walk_chain() {
+ local certificate
+ certificate="${1}"
+
+ local issuer_cert_uri
+ issuer_cert_uri="${2:-}"
+ if [[ -z "${issuer_cert_uri}" ]]; then issuer_cert_uri="$(get_issuer_cert_uri "${certificate}")"; fi
+ if [[ -n "${issuer_cert_uri}" ]]; then
+ # create temporary files
+ local tmpcert
+ local tmpcert_raw
+ tmpcert_raw="$(_mktemp)"
+ tmpcert="$(_mktemp)"
+
+ # download certificate
+ http_request get "${issuer_cert_uri}" > "${tmpcert_raw}"
+
+ # PEM
+ if grep -q "BEGIN CERTIFICATE" "${tmpcert_raw}"; then mv "${tmpcert_raw}" "${tmpcert}"
+ # DER
+ elif openssl x509 -in "${tmpcert_raw}" -inform DER -out "${tmpcert}" -outform PEM 2> /dev/null > /dev/null; then :
+ # PKCS7
+ elif openssl pkcs7 -in "${tmpcert_raw}" -inform DER -out "${tmpcert}" -outform PEM -print_certs 2> /dev/null > /dev/null; then :
+ # Unknown certificate type
+ else _exiterr "Unknown certificate type in chain"
+ fi
+
+ local next_issuer_cert_uri
+ next_issuer_cert_uri="$(get_issuer_cert_uri "${tmpcert}")"
+ if [[ -n "${next_issuer_cert_uri}" ]]; then
+ printf "\n%s\n" "${issuer_cert_uri}"
+ cat "${tmpcert}"
+ walk_chain "${tmpcert}" "${next_issuer_cert_uri}"
+ fi
+ rm -f "${tmpcert}" "${tmpcert_raw}"
+ fi
+}
+
# Create certificate for domain(s)
sign_domain() {
domain="${1}"
# Create fullchain.pem
echo " + Creating fullchain.pem..."
cat "${crt_path}" > "${CERTDIR}/${domain}/fullchain-${timestamp}.pem"
- tmpchain="$(_mktemp)"
- http_request get "$(openssl x509 -in "${CERTDIR}/${domain}/cert-${timestamp}.pem" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${tmpchain}"
- if grep -q "BEGIN CERTIFICATE" "${tmpchain}"; then
- mv "${tmpchain}" "${CERTDIR}/${domain}/chain-${timestamp}.pem"
- else
- openssl x509 -in "${tmpchain}" -inform DER -out "${CERTDIR}/${domain}/chain-${timestamp}.pem" -outform PEM
- rm "${tmpchain}"
- fi
+ walk_chain "${crt_path}" > "${CERTDIR}/${domain}/chain-${timestamp}.pem"
cat "${CERTDIR}/${domain}/chain-${timestamp}.pem" >> "${CERTDIR}/${domain}/fullchain-${timestamp}.pem"
# Update symlinks
ln -sf "cert-${timestamp}.pem" "${CERTDIR}/${domain}/cert.pem"
# Wait for hook script to clean the challenge and to deploy cert if used
- export KEY_ALGO
[[ -n "${HOOK}" ]] && "${HOOK}" "deploy_cert" "${domain}" "${CERTDIR}/${domain}/privkey.pem" "${CERTDIR}/${domain}/cert.pem" "${CERTDIR}/${domain}/fullchain.pem" "${CERTDIR}/${domain}/chain.pem" "${timestamp}"
unset challenge_token
echo " + Done!"
}
+# Usage: --register
+# Description: Register account key
+command_register() {
+ init_system
+ echo "+ Done!"
+ exit 0
+}
+
# Usage: --cron (-c)
# Description: Sign/renew non-existant/changed/expiring certificates.
command_sign_domains() {
if [[ -n "${PARAM_DOMAIN:-}" ]]; then
DOMAINS_TXT="$(_mktemp)"
+ tmp_domains="yes"
printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
elif [[ -e "${DOMAINS_TXT}" ]]; then
if [[ ! -r "${DOMAINS_TXT}" ]]; then
_exiterr "domains.txt found but not readable"
fi
+ elif [[ -n "${DOMAINS_D}" ]]; then
+ DOMAINS_TXT="$(_mktemp)"
+ tmp_domains="yes"
+ find "${DOMAINS_D}" -maxdepth 1 -type f | grep -o '[^/]*$' > "${DOMAINS_TXT}"
else
_exiterr "domains.txt not found and --domain not given"
fi
# Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire
ORIGIFS="${IFS}"
IFS=$'\n'
- for line in $(<"${DOMAINS_TXT}" tr -d '\r' | tr '[:upper:]' '[:lower:]' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' | (grep -vE '^(#|$)' || true)); do
+ for line in $(<"${DOMAINS_TXT}" tr -d '\r' | awk '{print tolower($0)}' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' | (grep -vE '^(#|$)' || true)); do
reset_configvars
IFS="${ORIGIFS}"
domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
config_value="$(echo "${cfgline:1}" | cut -d'=' -f2-)"
case "${config_var}" in
+ ALT_NAMES)
+ config_value="$(echo "${config_value}" | tr '[:upper:]' '[:lower:]' | _sed -e "s/^'[[:space:]]*//g" -e "s/[[:space:]]*'$//g" -e 's/[[:space:]]+/ /g')"
+ ;&
KEY_ALGO|OCSP_MUST_STAPLE|PRIVATE_KEY_RENEW|PRIVATE_KEY_ROLLOVER|KEYSIZE|CHALLENGETYPE|HOOK|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS)
echo " + ${config_var} = ${config_value}"
declare -- "${config_var}=${config_value}"
done
IFS="${ORIGIFS}"
fi
+
+ if [[ -n "${ALT_NAMES}" ]]; then
+ if [[ -n "${morenames}" ]]; then
+ morenames="${morenames} ${ALT_NAMES}"
+ else
+ morenames="${ALT_NAMES}"
+ fi
+ line="${domain} ${morenames}";
+ fi
+
verify_config
+ export WELLKNOWN CHALLENGETYPE KEY_ALGO PRIVATE_KEY_ROLLOVER
if [[ -e "${cert}" ]]; then
printf " + Checking domain name(s) of existing cert..."
done
# remove temporary domains.txt file if used
- [[ -n "${PARAM_DOMAIN:-}" ]] && rm -f "${DOMAINS_TXT}"
+ [[ "${tmp_domains:-}" = "yes" ]] && rm -f "${DOMAINS_TXT}"
+ [[ -n "${HOOK}" ]] && "${HOOK}" "exit_hook"
exit 0
}
set_command sign_domains
;;
+ --register)
+ set_command register
+ ;;
+
+ # PARAM_Usage: --accept-terms
+ # PARAM_Description: Accept CAs terms of service
+ --accept-terms)
+ PARAM_ACCEPT_TERMS="yes"
+ ;;
+
--signcsr|-s)
shift 1
set_command sign_csr
case "${COMMAND}" in
env) command_env;;
sign_domains) command_sign_domains;;
+ register) command_register;;
sign_csr) command_sign_csr "${PARAM_CSR}";;
revoke) command_revoke "${PARAM_REVOKECERT}";;
cleanup) command_cleanup;;