Commit 656044c6 authored by Kazuhiko Shiozaki's avatar Kazuhiko Shiozaki

Merge remote-tracking branch 'upstream/master'

parents c3dda6e3 6192b33a
...@@ -5,4 +5,4 @@ config ...@@ -5,4 +5,4 @@ config
hook.sh hook.sh
certs/* certs/*
archive/* archive/*
.acme-challenges/* accounts/*
...@@ -5,6 +5,21 @@ This file contains a log of major changes in letsencrypt.sh ...@@ -5,6 +5,21 @@ This file contains a log of major changes in letsencrypt.sh
## Changed ## Changed
- Config is now named `config` instead of `config.sh`! - Config is now named `config` instead of `config.sh`!
- Location of domains.txt is now configurable via DOMAINS_TXT config variable - 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 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
## [0.2.0] - 2016-05-22 ## [0.2.0] - 2016-05-22
### Changed ### Changed
......
...@@ -41,11 +41,18 @@ Commands: ...@@ -41,11 +41,18 @@ Commands:
--env (-e) Output configuration variables for use in other scripts --env (-e) Output configuration variables for use in other scripts
Parameters: 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!) --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 --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) --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 --config (-f) path/to/config Use specified config file
--hook (-k) path/to/hook.sh Use specified script for hooks --hook (-k) path/to/hook.sh Use specified script for hooks
--out (-o) certs/directory Output certificates into the specified directory
--challenge (-t) http-01|dns-01 Which challenge should be used? Currently http-01 and dns-01 are supported --challenge (-t) http-01|dns-01 Which challenge should be used? Currently http-01 and dns-01 are supported
--algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1 --algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
``` ```
### Elliptic Curve Cryptography (ECC) ### Elliptic Curve Cryptography (ECC)
This script also supports certificates with Elliptic Curve public keys! 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. Simply set the `KEY_ALGO` variable in one of the config files.
Please read https://community.letsencrypt.org/t/ecdsa-testing-on-staging/8809/ for the current state of ECC support.
...@@ -10,11 +10,16 @@ ...@@ -10,11 +10,16 @@
# Default values of this config are in comments # # Default values of this config are in comments #
######################################################## ########################################################
# Resolve names to addresses of IP version only. (curl)
# supported values: 4, 6
# default: <unset>
#IP_VERSION=
# Path to certificate authority (default: https://acme-v01.api.letsencrypt.org/directory) # Path to certificate authority (default: https://acme-v01.api.letsencrypt.org/directory)
#CA="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) # 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.0.1-July-27-2015.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 # Which challenge should be used? Currently http-01 and dns-01 are supported
#CHALLENGETYPE="http-01" #CHALLENGETYPE="http-01"
...@@ -31,14 +36,14 @@ ...@@ -31,14 +36,14 @@
# File containing the list of domains to request certificates for (default: $BASEDIR/domains.txt) # File containing the list of domains to request certificates for (default: $BASEDIR/domains.txt)
#DOMAINS_TXT="${BASEDIR}/domains.txt" #DOMAINS_TXT="${BASEDIR}/domains.txt"
# Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: $BASEDIR/.acme-challenges) # Output directory for generated certificates
#WELLKNOWN="${BASEDIR}/.acme-challenges" #CERTDIR="${BASEDIR}/certs"
# Location of private account key (default: $BASEDIR/private_key.pem) # Directory for account keys and registration information
#ACCOUNT_KEY="${BASEDIR}/private_key.pem" #ACCOUNTDIR="${BASEDIR}/accounts"
# Location of private account registration information (default: $BASEDIR/private_key.json) # Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: /var/www/letsencrypt)
#ACCOUNT_KEY_JSON="${BASEDIR}/private_key.json" #WELLKNOWN="/var/www/letsencrypt"
# Default keysize for private keys (default: 4096) # Default keysize for private keys (default: 4096)
#KEYSIZE="4096" #KEYSIZE="4096"
...@@ -75,3 +80,6 @@ ...@@ -75,3 +80,6 @@
# Lockfile location, to prevent concurrent access (default: $BASEDIR/lock) # Lockfile location, to prevent concurrent access (default: $BASEDIR/lock)
#LOCKFILE="${BASEDIR}/lock" #LOCKFILE="${BASEDIR}/lock"
# Option to add CSR-flag indicating OCSP stapling to be mandatory (default: no)
#OCSP_MUST_STAPLE="no"
# Config on per-certificate base
letsencrypt.sh allows a few configuration variables to be set on a per-certificate base.
To use this feature create a `config` file in the certificates output directory (e.g. `certs/example.org/config`).
Currently supported options:
- PRIVATE_KEY_RENEW
- KEY_ALGO
- KEYSIZE
- OCSP_MUST_STAPLE
- CHALLENGETYPE
- HOOK
- HOOK_CHAIN
- WELLKNOWN
- OPENSSL_CNF
- RENEW_DAYS
# Staging # 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), If you start testing using the production endpoint (which is the default),
you will quickly hit these limits and find yourself locked out. 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 ...@@ -10,6 +10,3 @@ To avoid this, please set the CA property to the Let’s Encrypt staging server
```bash ```bash
CA="https://acme-staging.api.letsencrypt.org/directory" 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.
...@@ -54,3 +54,15 @@ Alias /.well-known/acme-challenge /var/www/letsencrypt ...@@ -54,3 +54,15 @@ Alias /.well-known/acme-challenge /var/www/letsencrypt
</IfModule> </IfModule>
</Directory> </Directory>
``` ```
### Lighttpd example config
With Lighttpd just add this to your config and it should work in any VHost:
```lighttpd
modules += "alias"
alias.url += (
"/.well-known/acme-challenge/" => "/var/www/letsencrypt/"
)
```
...@@ -35,6 +35,7 @@ check_dependencies() { ...@@ -35,6 +35,7 @@ check_dependencies() {
_sed "" < /dev/null > /dev/null 2>&1 || _exiterr "This script requires sed with support for extended (modern) regular expressions." _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 grep > /dev/null 2>&1 || _exiterr "This script requires grep."
_mktemp -u > /dev/null 2>&1 || _exiterr "This script requires mktemp." _mktemp -u > /dev/null 2>&1 || _exiterr "This script requires mktemp."
diff -u /dev/null /dev/null || _exiterr "This script requires diff."
# curl returns with an error code in some ancient versions so we have to catch that # curl returns with an error code in some ancient versions so we have to catch that
set +e set +e
...@@ -46,12 +47,55 @@ check_dependencies() { ...@@ -46,12 +47,55 @@ check_dependencies() {
fi fi
} }
store_configvars() {
__KEY_ALGO="${KEY_ALGO}"
__OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}"
__PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}"
__KEYSIZE="${KEYSIZE}"
__CHALLENGETYPE="${CHALLENGETYPE}"
__HOOK="${HOOK}"
__WELLKNOWN="${WELLKNOWN}"
__HOOK_CHAIN="${HOOK_CHAIN}"
__OPENSSL_CNF="${OPENSSL_CNF}"
__RENEW_DAYS="${RENEW_DAYS}"
__IP_VERSION="${IP_VERSION}"
}
reset_configvars() {
KEY_ALGO="${__KEY_ALGO}"
OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}"
PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}"
KEYSIZE="${__KEYSIZE}"
CHALLENGETYPE="${__CHALLENGETYPE}"
HOOK="${__HOOK}"
WELLKNOWN="${__WELLKNOWN}"
HOOK_CHAIN="${__HOOK_CHAIN}"
OPENSSL_CNF="${__OPENSSL_CNF}"
RENEW_DAYS="${__RENEW_DAYS}"
IP_VERSION="${__IP_VERSION}"
}
# verify configuration values
verify_config() {
[[ "${CHALLENGETYPE}" =~ (http-01|dns-01) ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... can not continue."
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
_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 # Setup default config values, search for and load configuration files
load_config() { load_config() {
# Check for config in various locations # Check for config in various locations
if [[ -z "${CONFIG:-}" ]]; then if [[ -z "${CONFIG:-}" ]]; then
for check_config in "/etc/letsencrypt.sh" "/usr/local/etc/letsencrypt.sh" "${PWD}" "${SCRIPTDIR}"; do for check_config in "/etc/letsencrypt.sh" "/usr/local/etc/letsencrypt.sh" "${PWD}" "${SCRIPTDIR}"; do
if [[ -e "${check_config}/config" ]]; then if [[ -f "${check_config}/config" ]]; then
BASEDIR="${check_config}" BASEDIR="${check_config}"
CONFIG="${check_config}/config" CONFIG="${check_config}/config"
break break
...@@ -61,15 +105,16 @@ load_config() { ...@@ -61,15 +105,16 @@ load_config() {
# Default values # Default values
CA="https://acme-v01.api.letsencrypt.org/directory" 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" CHALLENGETYPE="http-01"
CONFIG_D= CONFIG_D=
DOMAINS_D=
DOMAINS_TXT= DOMAINS_TXT=
HOOK= HOOK=
HOOK_CHAIN="no" HOOK_CHAIN="no"
RENEW_DAYS="30" RENEW_DAYS="30"
ACCOUNT_KEY=
ACCOUNT_KEY_JSON=
KEYSIZE="4096" KEYSIZE="4096"
WELLKNOWN= WELLKNOWN=
PRIVATE_KEY_RENEW="yes" PRIVATE_KEY_RENEW="yes"
...@@ -77,12 +122,14 @@ load_config() { ...@@ -77,12 +122,14 @@ load_config() {
OPENSSL_CNF="$(openssl version -d | cut -d\" -f2)/openssl.cnf" OPENSSL_CNF="$(openssl version -d | cut -d\" -f2)/openssl.cnf"
CONTACT_EMAIL= CONTACT_EMAIL=
LOCKFILE= LOCKFILE=
OCSP_MUST_STAPLE="no"
IP_VERSION=
if [[ -z "${CONFIG:-}" ]]; then if [[ -z "${CONFIG:-}" ]]; then
echo "#" >&2 echo "#" >&2
echo "# !! WARNING !! No main config file found, using default config!" >&2 echo "# !! WARNING !! No main config file found, using default config!" >&2
echo "#" >&2 echo "#" >&2
elif [[ -e "${CONFIG}" ]]; then elif [[ -f "${CONFIG}" ]]; then
echo "# INFO: Using main config file ${CONFIG}" echo "# INFO: Using main config file ${CONFIG}"
BASEDIR="$(dirname "${CONFIG}")" BASEDIR="$(dirname "${CONFIG}")"
# shellcheck disable=SC1090 # shellcheck disable=SC1090
...@@ -116,21 +163,37 @@ load_config() { ...@@ -116,21 +163,37 @@ load_config() {
# Check BASEDIR and set default variables # Check BASEDIR and set default variables
[[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}" [[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}"
[[ -z "${ACCOUNT_KEY}" ]] && ACCOUNT_KEY="${BASEDIR}/private_key.pem" CAHASH="$(echo "${CA}" | urlbase64)"
[[ -z "${ACCOUNT_KEY_JSON}" ]] && ACCOUNT_KEY_JSON="${BASEDIR}/private_key.json" [[ -z "${ACCOUNTDIR}" ]] && ACCOUNTDIR="${BASEDIR}/accounts"
mkdir -p "${ACCOUNTDIR}/${CAHASH}"
[[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config"
ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem"
ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json"
if [[ -f "${BASEDIR}/private_key.pem" ]] && [[ ! -f "${ACCOUNT_KEY}" ]]; then
echo "! Moving private_key.pem to ${ACCOUNT_KEY}"
mv "${BASEDIR}/private_key.pem" "${ACCOUNT_KEY}"
fi
if [[ -f "${BASEDIR}/private_key.json" ]] && [[ ! -f "${ACCOUNT_KEY_JSON}" ]]; then
echo "! Moving private_key.json to ${ACCOUNT_KEY_JSON}"
mv "${BASEDIR}/private_key.json" "${ACCOUNT_KEY_JSON}"
fi
[[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs"
[[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt" [[ -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" [[ -z "${LOCKFILE}" ]] && LOCKFILE="${BASEDIR}/lock"
[[ -n "${PARAM_NO_LOCK:-}" ]] && LOCKFILE=""
[[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}" [[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}"
[[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}"
[[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}" [[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}"
[[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}" [[ -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}"
[[ "${CHALLENGETYPE}" =~ (http-01|dns-01) ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... can not continue." verify_config
if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then store_configvars
_exiterr "Challenge type dns-01 needs a hook script for deployment... can not continue."
fi
[[ "${KEY_ALGO}" =~ ^(rsa|prime256v1|secp384r1)$ ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... can not continue."
} }
# Initialize system # Initialize system
...@@ -138,11 +201,13 @@ init_system() { ...@@ -138,11 +201,13 @@ init_system() {
load_config load_config
# Lockfile handling (prevents concurrent access) # Lockfile handling (prevents concurrent access)
if [[ -n "${LOCKFILE}" ]]; then
LOCKDIR="$(dirname "${LOCKFILE}")" LOCKDIR="$(dirname "${LOCKFILE}")"
[[ -w "${LOCKDIR}" ]] || _exiterr "Directory ${LOCKDIR} for LOCKFILE ${LOCKFILE} is not writable, aborting." [[ -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." ( set -C; date > "${LOCKFILE}" ) 2>/dev/null || _exiterr "Lock file '${LOCKFILE}' present, aborting."
remove_lock() { rm -f "${LOCKFILE}"; } remove_lock() { rm -f "${LOCKFILE}"; }
trap 'remove_lock' EXIT trap 'remove_lock' EXIT
fi
# Get CA URLs # Get CA URLs
CA_DIRECTORY="$(http_request get "${CA}")" CA_DIRECTORY="$(http_request get "${CA}")"
...@@ -154,7 +219,7 @@ init_system() { ...@@ -154,7 +219,7 @@ init_system() {
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint." _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 some environment variables to be used in hook script
export WELLKNOWN BASEDIR CONFIG export WELLKNOWN BASEDIR CERTDIR CONFIG
# Checking for private key ... # Checking for private key ...
register_new_key="no" register_new_key="no"
...@@ -184,16 +249,21 @@ init_system() { ...@@ -184,16 +249,21 @@ init_system() {
echo "+ Registering account key with letsencrypt..." echo "+ Registering account key with letsencrypt..."
[[ ! -z "${CA_NEW_REG}" ]] || _exiterr "Certificate authority doesn't allow registrations." [[ ! -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 # If an email for the contact has been provided then adding it to the registration request
FAILED=false
if [[ -n "${CONTACT_EMAIL}" ]]; then if [[ -n "${CONTACT_EMAIL}" ]]; then
signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}" (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
else else
signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}" (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
fi fi
if [[ "${FAILED}" = "true" ]]; then
echo
echo
echo "Error registering account key. See message above for more information."
rm "${ACCOUNT_KEY}" "${ACCOUNT_KEY_JSON}"
exit 1
fi fi
if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" ]]; then
_exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
fi fi
} }
# Different sed version for different os types... # Different sed version for different os types...
...@@ -256,15 +326,19 @@ _openssl() { ...@@ -256,15 +326,19 @@ _openssl() {
http_request() { http_request() {
tempcont="$(_mktemp)" tempcont="$(_mktemp)"
if [[ -n "${IP_VERSION:-}" ]]; then
ip_version="-${IP_VERSION}"
fi
set +e set +e
if [[ "${1}" = "head" ]]; then 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="${?}" curlret="${?}"
elif [[ "${1}" = "get" ]]; then 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="${?}" curlret="${?}"
elif [[ "${1}" = "post" ]]; then 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="${?}" curlret="${?}"
else else
set -e set -e
...@@ -273,7 +347,7 @@ http_request() { ...@@ -273,7 +347,7 @@ http_request() {
set -e set -e
if [[ ! "${curlret}" = "0" ]]; then if [[ ! "${curlret}" = "0" ]]; then
_exiterr "Problem connecting to server (curl returned with ${curlret})" _exiterr "Problem connecting to server (${1} for ${2}; curl returned with ${curlret})"
fi fi
if [[ ! "${statuscode:0:1}" = "2" ]]; then if [[ ! "${statuscode:0:1}" = "2" ]]; then
...@@ -281,6 +355,8 @@ http_request() { ...@@ -281,6 +355,8 @@ http_request() {
echo >&2 echo >&2
echo "Details:" >&2 echo "Details:" >&2
cat "${tempcont}" >&2 cat "${tempcont}" >&2
echo >&2
echo >&2
rm -f "${tempcont}" rm -f "${tempcont}"
# Wait for hook script to clean the challenge if used # Wait for hook script to clean the challenge if used
...@@ -505,19 +581,19 @@ sign_domain() { ...@@ -505,19 +581,19 @@ sign_domain() {
fi fi
# If there is no existing certificate directory => make it # If there is no existing certificate directory => make it
if [[ ! -e "${BASEDIR}/certs/${domain}" ]]; then if [[ ! -e "${CERTDIR}/${domain}" ]]; then
echo " + Creating new directory ${BASEDIR}/certs/${domain} ..." echo " + Creating new directory ${CERTDIR}/${domain} ..."
mkdir -p "${BASEDIR}/certs/${domain}" mkdir -p "${CERTDIR}/${domain}" || _exiterr "Unable to create directory ${CERTDIR}/${domain}"
fi fi
privkey="privkey.pem" privkey="privkey.pem"
# generate a new private key if we need or want one # generate a new private key if we need or want one
if [[ ! -r "${BASEDIR}/certs/${domain}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then if [[ ! -r "${CERTDIR}/${domain}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
echo " + Generating private key..." echo " + Generating private key..."
privkey="privkey-${timestamp}.pem" privkey="privkey-${timestamp}.pem"
case "${KEY_ALGO}" in case "${KEY_ALGO}" in
rsa) _openssl genrsa -out "${BASEDIR}/certs/${domain}/privkey-${timestamp}.pem" "${KEYSIZE}";; rsa) _openssl genrsa -out "${CERTDIR}/${domain}/privkey-${timestamp}.pem" "${KEYSIZE}";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${BASEDIR}/certs/${domain}/privkey-${timestamp}.pem";; prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${CERTDIR}/${domain}/privkey-${timestamp}.pem";;
esac esac
fi fi
...@@ -532,33 +608,36 @@ sign_domain() { ...@@ -532,33 +608,36 @@ sign_domain() {
tmp_openssl_cnf="$(_mktemp)" tmp_openssl_cnf="$(_mktemp)"
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}" cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
printf "[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}" printf "[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}"
openssl req -new -sha256 -key "${BASEDIR}/certs/${domain}/${privkey}" -out "${BASEDIR}/certs/${domain}/cert-${timestamp}.csr" -subj "/CN=${domain}/" -reqexts SAN -config "${tmp_openssl_cnf}" if [ "${OCSP_MUST_STAPLE}" = "yes" ]; then
printf "\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >> "${tmp_openssl_cnf}"
fi
openssl req -new -sha256 -key "${CERTDIR}/${domain}/${privkey}" -out "${CERTDIR}/${domain}/cert-${timestamp}.csr" -subj "/CN=${domain}/" -reqexts SAN -config "${tmp_openssl_cnf}"
rm -f "${tmp_openssl_cnf}" rm -f "${tmp_openssl_cnf}"
crt_path="${BASEDIR}/certs/${domain}/cert-${timestamp}.pem" crt_path="${CERTDIR}/${domain}/cert-${timestamp}.pem"
# shellcheck disable=SC2086 # shellcheck disable=SC2086
sign_csr "$(< "${BASEDIR}/certs/${domain}/cert-${timestamp}.csr" )" ${altnames} 3>"${crt_path}" sign_csr "$(< "${CERTDIR}/${domain}/cert-${timestamp}.csr" )" ${altnames} 3>"${crt_path}"
# Create fullchain.pem # Create fullchain.pem
echo " + Creating fullchain.pem..." echo " + Creating fullchain.pem..."
cat "${crt_path}" > "${BASEDIR}/certs/${domain}/fullchain-${timestamp}.pem" cat "${crt_path}" > "${CERTDIR}/${domain}/fullchain-${timestamp}.pem"
http_request get "$(openssl x509 -in "${BASEDIR}/certs/${domain}/cert-${timestamp}.pem" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" http_request get "$(openssl x509 -in "${CERTDIR}/${domain}/cert-${timestamp}.pem" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${CERTDIR}/${domain}/chain-${timestamp}.pem"
if ! grep -q "BEGIN CERTIFICATE" "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem"; then if ! grep -q "BEGIN CERTIFICATE" "${CERTDIR}/${domain}/chain-${timestamp}.pem"; then
openssl x509 -in "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" -inform DER -out "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" -outform PEM openssl x509 -in "${CERTDIR}/${domain}/chain-${timestamp}.pem" -inform DER -out "${CERTDIR}/${domain}/chain-${timestamp}.pem" -outform PEM
fi fi
cat "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" >> "${BASEDIR}/certs/${domain}/fullchain-${timestamp}.pem" cat "${CERTDIR}/${domain}/chain-${timestamp}.pem" >> "${CERTDIR}/${domain}/fullchain-${timestamp}.pem"
# Update symlinks # Update symlinks
[[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${BASEDIR}/certs/${domain}/privkey.pem" [[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${CERTDIR}/${domain}/privkey.pem"
ln -sf "chain-${timestamp}.pem" "${BASEDIR}/certs/${domain}/chain.pem" ln -sf "chain-${timestamp}.pem" "${CERTDIR}/${domain}/chain.pem"
ln -sf "fullchain-${timestamp}.pem" "${BASEDIR}/certs/${domain}/fullchain.pem" ln -sf "fullchain-${timestamp}.pem" "${CERTDIR}/${domain}/fullchain.pem"
ln -sf "cert-${timestamp}.csr" "${BASEDIR}/certs/${domain}/cert.csr" ln -sf "cert-${timestamp}.csr" "${CERTDIR}/${domain}/cert.csr"
ln -sf "cert-${timestamp}.pem" "${BASEDIR}/certs/${domain}/cert.pem" ln -sf "cert-${timestamp}.pem" "${CERTDIR}/${domain}/cert.pem"
# Wait for hook script to clean the challenge and to deploy cert if used # Wait for hook script to clean the challenge and to deploy cert if used
export KEY_ALGO export KEY_ALGO
[[ -n "${HOOK}" ]] && "${HOOK}" "deploy_cert" "${domain}" "${BASEDIR}/certs/${domain}/privkey.pem" "${BASEDIR}/certs/${domain}/cert.pem" "${BASEDIR}/certs/${domain}/fullchain.pem" "${BASEDIR}/certs/${domain}/chain.pem" "${timestamp}" [[ -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 unset challenge_token
echo " + Done!" echo " + Done!"
...@@ -584,10 +663,11 @@ command_sign_domains() { ...@@ -584,10 +663,11 @@ command_sign_domains() {
ORIGIFS="${IFS}" ORIGIFS="${IFS}"
IFS=$'\n' 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' | tr '[:upper:]' '[:lower:]' | _sed -e 's/^[[:space:]]*//g' -e 's/[[:space:]]*$//g' -e 's/[[:space:]]+/ /g' | (grep -vE '^(#|$)' || true)); do
reset_configvars
IFS="${ORIGIFS}" IFS="${ORIGIFS}"
domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)" domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)" morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)"
cert="${BASEDIR}/certs/${domain}/cert.pem" cert="${CERTDIR}/${domain}/cert.pem"
force_renew="${PARAM_FORCE:-no}" force_renew="${PARAM_FORCE:-no}"
...@@ -597,6 +677,46 @@ command_sign_domains() { ...@@ -597,6 +677,46 @@ command_sign_domains() {
echo "Processing ${domain} with alternative names: ${morenames}" echo "Processing ${domain} with alternative names: ${morenames}"
fi fi
# read cert config
# 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 [[ -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'
for cfgline in $(
beforevars="$(_mktemp)"
aftervars="$(_mktemp)"
set > "${beforevars}"
# shellcheck disable=SC1090
. "${certconfig}"
set > "${aftervars}"
diff -u "${beforevars}" "${aftervars}" | grep -E '^\+[^+]'
rm "${beforevars}"
rm "${aftervars}"
); do
config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
config_value="$(echo "${cfgline:1}" | cut -d'=' -f2-)"
case "${config_var}" in
KEY_ALGO|OCSP_MUST_STAPLE|PRIVATE_KEY_RENEW|KEYSIZE|CHALLENGETYPE|HOOK|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS)
echo " + ${config_var} = ${config_value}"
declare -- "${config_var}=${config_value}"
;;
_) ;;
*) echo " ! Setting ${config_var} on a per-certificate base is not (yet) supported"
esac
done
IFS="${ORIGIFS}"
fi
verify_config
if [[ -e "${cert}" ]]; then if [[ -e "${cert}" ]]; then
printf " + Checking domain name(s) of existing cert..." printf " + Checking domain name(s) of existing cert..."
...@@ -627,7 +747,7 @@ command_sign_domains() { ...@@ -627,7 +747,7 @@ command_sign_domains() {
else else
# Certificate-Names unchanged and cert is still valid # Certificate-Names unchanged and cert is still valid
echo "Skipping renew!" echo "Skipping renew!"
[[ -n "${HOOK}" ]] && "${HOOK}" "unchanged_cert" "${domain}" "${BASEDIR}/certs/${domain}/privkey.pem" "${BASEDIR}/certs/${domain}/cert.pem" "${BASEDIR}/certs/${domain}/fullchain.pem" "${BASEDIR}/certs/${domain}/chain.pem" [[ -n "${HOOK}" ]] && "${HOOK}" "unchanged_cert" "${domain}" "${CERTDIR}/${domain}/privkey.pem" "${CERTDIR}/${domain}/cert.pem" "${CERTDIR}/${domain}/fullchain.pem" "${CERTDIR}/${domain}/chain.pem"
continue continue
fi fi
else else
...@@ -636,7 +756,12 @@ command_sign_domains() { ...@@ -636,7 +756,12 @@ command_sign_domains() {
fi fi
# shellcheck disable=SC2086 # shellcheck disable=SC2086
if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
sign_domain ${line} &
wait $! || true
else
sign_domain ${line} sign_domain ${line}
fi
done done
# remove temporary domains.txt file if used # remove temporary domains.txt file if used
...@@ -659,7 +784,33 @@ command_sign_csr() { ...@@ -659,7 +784,33 @@ command_sign_csr() {
_exiterr "Could not read certificate signing request ${csrfile}" _exiterr "Could not read certificate signing request ${csrfile}"
fi fi
sign_csr "$(< "${csrfile}" )" # gen cert
certfile="$(_mktemp)"
sign_csr "$(< "${csrfile}" )" 3> "${certfile}"
# print cert
echo "# CERT #" >&3
cat "${certfile}" >&3
echo >&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}"
exit 0 exit 0
} }
...@@ -706,7 +857,7 @@ command_cleanup() { ...@@ -706,7 +857,7 @@ command_cleanup() {
fi fi
# Loop over all certificate directories # Loop over all certificate directories
for certdir in "${BASEDIR}/certs/"*; do for certdir in "${CERTDIR}/"*; do
# Skip if entry is not a folder # Skip if entry is not a folder
[[ -d "${certdir}" ]] || continue [[ -d "${certdir}" ]] || continue
...@@ -775,7 +926,7 @@ command_help() { ...@@ -775,7 +926,7 @@ command_help() {
command_env() { command_env() {
echo "# letsencrypt.sh configuration" echo "# letsencrypt.sh configuration"
load_config load_config
typeset -p CA LICENSE 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) # Main method (parses script arguments and calls command_* methods)
...@@ -832,6 +983,24 @@ main() { ...@@ -832,6 +983,24 @@ main() {
set_command cleanup 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_Usage: --domain (-d) domain.tld
# PARAM_Description: Use specified domain name(s) instead of domains.txt entry (one certificate!) # PARAM_Description: Use specified domain name(s) instead of domains.txt entry (one certificate!)
--domain|-d) --domain|-d)
...@@ -844,6 +1013,11 @@ main() { ...@@ -844,6 +1013,11 @@ main() {
fi 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_Usage: --force (-x)
# PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS # PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS
...@@ -851,6 +1025,18 @@ main() { ...@@ -851,6 +1025,18 @@ main() {
PARAM_FORCE="yes" 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)
PARAM_OCSP_MUST_STAPLE="yes"
;;
# PARAM_Usage: --privkey (-p) path/to/key.pem # PARAM_Usage: --privkey (-p) path/to/key.pem
# PARAM_Description: Use specified private key instead of account key (useful for revocation) # PARAM_Description: Use specified private key instead of account key (useful for revocation)
--privkey|-p) --privkey|-p)
...@@ -875,6 +1061,14 @@ main() { ...@@ -875,6 +1061,14 @@ main() {
PARAM_HOOK="${1}" PARAM_HOOK="${1}"
;; ;;
# PARAM_Usage: --out (-o) certs/directory
# PARAM_Description: Output certificates into the specified directory
--out|-o)
shift 1
check_parameters "${1:-}"
PARAM_CERTDIR="${1}"
;;
# PARAM_Usage: --challenge (-t) http-01|dns-01 # PARAM_Usage: --challenge (-t) http-01|dns-01
# PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported # PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
--challenge|-t) --challenge|-t)
......
...@@ -114,7 +114,7 @@ _CHECK_ERRORLOG ...@@ -114,7 +114,7 @@ _CHECK_ERRORLOG
_TEST "First run in cron mode, checking if private key is generated and registered" _TEST "First run in cron mode, checking if private key is generated and registered"
./letsencrypt.sh --cron > tmplog 2> errorlog || _FAIL "Script execution failed" ./letsencrypt.sh --cron > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Registering account key" _CHECK_LOG "Registering account key"
_CHECK_FILE "private_key.pem" _CHECK_FILE accounts/*/account_key.pem
_CHECK_ERRORLOG _CHECK_ERRORLOG
# Temporarily move config out of the way and try signing certificate by using temporary config location # Temporarily move config out of the way and try signing certificate by using temporary config location
...@@ -131,10 +131,6 @@ _CHECK_LOG "Done!" ...@@ -131,10 +131,6 @@ _CHECK_LOG "Done!"
_CHECK_ERRORLOG _CHECK_ERRORLOG
mv tmp_config config mv tmp_config config
# Move private key and add new location to config
mv private_key.pem account_key.pem
echo 'PRIVATE_KEY="./account_key.pem"' >> config
# Add third domain to command-lime, should force renewal. # Add third domain to command-lime, should force renewal.
_TEST "Run in cron mode again, this time adding third domain, should force renewal." _TEST "Run in cron mode again, this time adding third domain, should force renewal."
./letsencrypt.sh --cron --domain "${TMP_URL}" --domain "${TMP2_URL}" --domain "${TMP3_URL}" > tmplog 2> errorlog || _FAIL "Script execution failed" ./letsencrypt.sh --cron --domain "${TMP_URL}" --domain "${TMP2_URL}" --domain "${TMP3_URL}" > tmplog 2> errorlog || _FAIL "Script execution failed"
...@@ -184,9 +180,6 @@ _CHECK_LOG "BEGIN CERTIFICATE" ...@@ -184,9 +180,6 @@ _CHECK_LOG "BEGIN CERTIFICATE"
_CHECK_LOG "END CERTIFICATE" _CHECK_LOG "END CERTIFICATE"
_CHECK_NOT_LOG "ERROR" _CHECK_NOT_LOG "ERROR"
# Delete account key (not needed anymore)
rm account_key.pem
# Check if renewal works # Check if renewal works
_TEST "Run in cron mode again, to check if renewal works" _TEST "Run in cron mode again, to check if renewal works"
echo 'RENEW_DAYS="300"' >> config echo 'RENEW_DAYS="300"' >> config
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment