diff --git a/.gitignore b/.gitignore index 26422e13..abc69ff0 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,3 @@ hook.sh certs/* archive/* accounts/* -.acme-challenges/* diff --git a/CHANGELOG b/CHANGELOG index e6cd3433..20769e11 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,13 +6,17 @@ This file contains a log of major changes in letsencrypt.sh - Config is now named `config` instead of `config.sh`! - Location of domains.txt is now configurable via DOMAINS_TXT config variable - Location of certs directory is now configurable via CERTDIR config variable -- signcsr command now also outputs chain certificate +- signcsr command now also outputs chain certificate if --full-chain/-fc is set - Location of account-key(s) changed +- Default WELLKNOWN location is now `/var/www/letsencrypt` +- New version of Let's Encrypt Subscriber Agreement ## Added - Added option to add CSR-flag indicating OCSP stapling to be mandatory - Initial support for configuration on per-certificate base - Support for per-CA account keys and custom config for output cert directory, license, etc. +- Added option to select IP version of name to address resolution +- Added option to run letsencrypt.sh without locks ## Fixed - letsencrypt.sh no longer stores account keys from invalid registrations diff --git a/README.md b/README.md index 65632430..81bbd10c 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,13 @@ Commands: --env (-e) Output configuration variables for use in other scripts Parameters: + --full-chain (-fc) Print full chain when using --signcsr + --ipv4 (-4) Resolve names to IPv4 addresses only + --ipv6 (-6) Resolve names to IPv6 addresses only --domain (-d) domain.tld Use specified domain name(s) instead of domains.txt entry (one certificate!) + --keep-going (-g) Keep going after encountering an error while creating/renewing multiple certificates in cron mode --force (-x) Force renew of certificate even if it is longer valid than value in RENEW_DAYS + --no-lock (-n) Don't use lockfile (potentially dangerous!) --ocsp Sets option in CSR indicating OCSP stapling to be mandatory --privkey (-p) path/to/key.pem Use specified private key instead of account key (useful for revocation) --config (-f) path/to/config Use specified config file diff --git a/docs/ecc.md b/docs/ecc.md index bbe99166..87d52ba4 100644 --- a/docs/ecc.md +++ b/docs/ecc.md @@ -1,5 +1,4 @@ ### Elliptic Curve Cryptography (ECC) This script also supports certificates with Elliptic Curve public keys! -Be aware that at the moment this is not available on the production servers from letsencrypt. -Please read https://community.letsencrypt.org/t/ecdsa-testing-on-staging/8809/ for the current state of ECC support. +Simply set the `KEY_ALGO` variable in one of the config files. diff --git a/docs/examples/config b/docs/examples/config index 298eb04e..a836a4e2 100644 --- a/docs/examples/config +++ b/docs/examples/config @@ -10,11 +10,16 @@ # Default values of this config are in comments # ######################################################## +# Resolve names to addresses of IP version only. (curl) +# supported values: 4, 6 +# default: +#IP_VERSION= + # Path to certificate authority (default: https://acme-v01.api.letsencrypt.org/directory) #CA="https://acme-v01.api.letsencrypt.org/directory" -# Path to license agreement (default: https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf) -#LICENSE="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf" +# Path to license agreement (default: https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf) +#LICENSE="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf" # Which challenge should be used? Currently http-01 and dns-01 are supported #CHALLENGETYPE="http-01" @@ -37,8 +42,8 @@ # Directory for account keys and registration information #ACCOUNTDIR="${BASEDIR}/accounts" -# Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: $BASEDIR/.acme-challenges) -#WELLKNOWN="${BASEDIR}/.acme-challenges" +# Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: /var/www/letsencrypt) +#WELLKNOWN="/var/www/letsencrypt" # Default keysize for private keys (default: 4096) #KEYSIZE="4096" diff --git a/docs/staging.md b/docs/staging.md index ec18445b..297db580 100644 --- a/docs/staging.md +++ b/docs/staging.md @@ -1,6 +1,6 @@ # Staging -Let’s Encrypt has stringent rate limits in place during the public beta period. +Let’s Encrypt has stringent rate limits in place. If you start testing using the production endpoint (which is the default), you will quickly hit these limits and find yourself locked out. @@ -10,6 +10,3 @@ To avoid this, please set the CA property to the Let’s Encrypt staging server ```bash CA="https://acme-staging.api.letsencrypt.org/directory" ``` - -Please keep in mind that at the time of writing this letsencrypt.sh doesn't have support for registration management, -so if you change CA you'll have to move your `private_key.pem` (and, if you care, `private_key.json`) out of the way. diff --git a/letsencrypt.sh b/letsencrypt.sh index 4efa5705..787c31f5 100755 --- a/letsencrypt.sh +++ b/letsencrypt.sh @@ -58,6 +58,7 @@ store_configvars() { __HOOK_CHAIN="${HOOK_CHAIN}" __OPENSSL_CNF="${OPENSSL_CNF}" __RENEW_DAYS="${RENEW_DAYS}" + __IP_VERSION="${IP_VERSION}" } reset_configvars() { @@ -71,6 +72,7 @@ reset_configvars() { HOOK_CHAIN="${__HOOK_CHAIN}" OPENSSL_CNF="${__OPENSSL_CNF}" RENEW_DAYS="${__RENEW_DAYS}" + IP_VERSION="${__IP_VERSION}" } # verify configuration values @@ -83,6 +85,9 @@ verify_config() { _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." + if [[ -n "${IP_VERSION}" ]]; then + [[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... can not continue." + fi } # Setup default config values, search for and load configuration files @@ -100,11 +105,12 @@ load_config() { # Default values CA="https://acme-v01.api.letsencrypt.org/directory" - LICENSE="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf" + LICENSE="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf" CERTDIR= ACCOUNTDIR= CHALLENGETYPE="http-01" CONFIG_D= + DOMAINS_D= DOMAINS_TXT= HOOK= HOOK_CHAIN="no" @@ -117,6 +123,7 @@ load_config() { CONTACT_EMAIL= LOCKFILE= OCSP_MUST_STAPLE="no" + IP_VERSION= if [[ -z "${CONFIG:-}" ]]; then echo "#" >&2 @@ -174,14 +181,16 @@ load_config() { [[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs" [[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt" - [[ -z "${WELLKNOWN}" ]] && WELLKNOWN="${BASEDIR}/.acme-challenges" + [[ -z "${WELLKNOWN}" ]] && WELLKNOWN="/var/www/letsencrypt" [[ -z "${LOCKFILE}" ]] && LOCKFILE="${BASEDIR}/lock" + [[ -n "${PARAM_NO_LOCK:-}" ]] && LOCKFILE="" [[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}" [[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}" [[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}" [[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}" [[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}" + [[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}" verify_config store_configvars @@ -192,11 +201,13 @@ init_system() { load_config # Lockfile handling (prevents concurrent access) - LOCKDIR="$(dirname "${LOCKFILE}")" - [[ -w "${LOCKDIR}" ]] || _exiterr "Directory ${LOCKDIR} for LOCKFILE ${LOCKFILE} is not writable, aborting." - ( set -C; date > "${LOCKFILE}" ) 2>/dev/null || _exiterr "Lock file '${LOCKFILE}' present, aborting." - remove_lock() { rm -f "${LOCKFILE}"; } - trap 'remove_lock' EXIT + if [[ -n "${LOCKFILE}" ]]; then + LOCKDIR="$(dirname "${LOCKFILE}")" + [[ -w "${LOCKDIR}" ]] || _exiterr "Directory ${LOCKDIR} for LOCKFILE ${LOCKFILE} is not writable, aborting." + ( set -C; date > "${LOCKFILE}" ) 2>/dev/null || _exiterr "Lock file '${LOCKFILE}' present, aborting." + remove_lock() { rm -f "${LOCKFILE}"; } + trap 'remove_lock' EXIT + fi # Get CA URLs CA_DIRECTORY="$(http_request get "${CA}")" @@ -315,15 +326,19 @@ _openssl() { http_request() { tempcont="$(_mktemp)" + if [[ -n "${IP_VERSION:-}" ]]; then + ip_version="-${IP_VERSION}" + fi + set +e if [[ "${1}" = "head" ]]; then - statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)" + statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)" curlret="${?}" elif [[ "${1}" = "get" ]]; then - statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}")" + statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}")" curlret="${?}" elif [[ "${1}" = "post" ]]; then - statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -d "${3}")" + statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}" -d "${3}")" curlret="${?}" else set -e @@ -340,6 +355,8 @@ http_request() { echo >&2 echo "Details:" >&2 cat "${tempcont}" >&2 + echo >&2 + echo >&2 rm -f "${tempcont}" # Wait for hook script to clean the challenge if used @@ -664,7 +681,13 @@ command_sign_domains() { # for now this loads the certificate specific config in a subshell and parses a diff of set variables. # we could just source the config file but i decided to go this way to protect people from accidentally overriding # variables used internally by this script itself. - if [ -f "${CERTDIR}/${domain}/config" ]; then + if [[ -n "${DOMAINS_D}" ]]; then + certconfig="${DOMAINS_D}/${domain}" + else + certconfig="${CERTDIR}/${domain}/config" + fi + + if [ -f "${certconfig}" ]; then echo " + Using certificate specific config file!" ORIGIFS="${IFS}" IFS=$'\n' @@ -673,7 +696,7 @@ command_sign_domains() { aftervars="$(_mktemp)" set > "${beforevars}" # shellcheck disable=SC1090 - . "${CERTDIR}/${domain}/config" + . "${certconfig}" set > "${aftervars}" diff -u "${beforevars}" "${aftervars}" | grep -E '^\+[^+]' rm "${beforevars}" @@ -733,7 +756,12 @@ command_sign_domains() { fi # shellcheck disable=SC2086 - sign_domain ${line} + if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then + sign_domain ${line} & + wait $! || true + else + sign_domain ${line} + fi done # remove temporary domains.txt file if used @@ -760,24 +788,29 @@ command_sign_csr() { certfile="$(_mktemp)" sign_csr "$(< "${csrfile}" )" 3> "${certfile}" - # get and convert ca cert - chainfile="$(_mktemp)" - http_request get "$(openssl x509 -in "${certfile}" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${chainfile}" - - if ! grep -q "BEGIN CERTIFICATE" "${chainfile}"; then - openssl x509 -inform DER -in "${chainfile}" -outform PEM -out "${chainfile}" - fi - - # output full chain + # print cert echo "# CERT #" >&3 cat "${certfile}" >&3 echo >&3 - echo "# CHAIN #" >&3 - cat "${chainfile}" >&3 + + # print chain + if [ -n "${PARAM_FULL_CHAIN:-}" ]; then + # get and convert ca cert + chainfile="$(_mktemp)" + http_request get "$(openssl x509 -in "${certfile}" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${chainfile}" + + if ! grep -q "BEGIN CERTIFICATE" "${chainfile}"; then + openssl x509 -inform DER -in "${chainfile}" -outform PEM -out "${chainfile}" + fi + + echo "# CHAIN #" >&3 + cat "${chainfile}" >&3 + + rm "${chainfile}" + fi # cleanup rm "${certfile}" - rm "${chainfile}" exit 0 } @@ -893,7 +926,7 @@ command_help() { command_env() { echo "# letsencrypt.sh configuration" load_config - typeset -p CA LICENSE CERTDIR CHALLENGETYPE DOMAINS_TXT HOOK HOOK_CHAIN RENEW_DAYS ACCOUNT_KEY ACCOUNT_KEY_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE + typeset -p CA LICENSE CERTDIR CHALLENGETYPE DOMAINS_D DOMAINS_TXT HOOK HOOK_CHAIN RENEW_DAYS ACCOUNT_KEY ACCOUNT_KEY_JSON KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE } # Main method (parses script arguments and calls command_* methods) @@ -950,6 +983,24 @@ main() { set_command cleanup ;; + # PARAM_Usage: --full-chain (-fc) + # PARAM_Description: Print full chain when using --signcsr + --full-chain|-fc) + PARAM_FULL_CHAIN="1" + ;; + + # PARAM_Usage: --ipv4 (-4) + # PARAM_Description: Resolve names to IPv4 addresses only + --ipv4|-4) + PARAM_IP_VERSION="4" + ;; + + # PARAM_Usage: --ipv6 (-6) + # PARAM_Description: Resolve names to IPv6 addresses only + --ipv6|-6) + PARAM_IP_VERSION="6" + ;; + # PARAM_Usage: --domain (-d) domain.tld # PARAM_Description: Use specified domain name(s) instead of domains.txt entry (one certificate!) --domain|-d) @@ -962,6 +1013,11 @@ main() { fi ;; + # PARAM_Usage: --keep-going (-g) + # PARAM_Description: Keep going after encountering an error while creating/renewing multiple certificates in cron mode + --keep-going|-g) + PARAM_KEEP_GOING="yes" + ;; # PARAM_Usage: --force (-x) # PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS @@ -969,6 +1025,12 @@ main() { PARAM_FORCE="yes" ;; + # PARAM_Usage: --no-lock (-n) + # PARAM_Description: Don't use lockfile (potentially dangerous!) + --no-lock|-n) + PARAM_NO_LOCK="yes" + ;; + # PARAM_Usage: --ocsp # PARAM_Description: Sets option in CSR indicating OCSP stapling to be mandatory --ocsp)