31 Commits

Author SHA1 Message Date
Lukas Schauer
992beecbdb release v0.3.0 2016-09-07 21:20:28 +02:00
leonklingele
6192b33ac2 ECDSA is supported since February 10, 2016 (#260)
Let's Encrypt will however sign all ECDSA certs with an RSA intermediate certificate.
https://letsencrypt.org/upcoming-features/
2016-08-22 15:40:37 +02:00
leonklingele
47602dea04 Update staging doc: Let's Encrypt is no longer in beta. (#259) 2016-08-22 15:38:29 +02:00
leonklingele
3a66a7f8d2 Fix default license help text in example config (#254) 2016-08-06 15:32:53 +02:00
Lukas Schauer
624ce4436a updated readme 2016-08-04 00:11:08 +02:00
Lukas Schauer
d81eb58536 Only print full chain on signcsr command if --full-chain/-fc is set (related to #150) 2016-08-04 00:11:02 +02:00
BtbN
117d5d6228 Echo newline after error details 2016-08-03 23:50:59 +02:00
BtbN
34565c193d Add support for --keep-going in cron mode
Fixes #154
2016-08-03 23:50:59 +02:00
Lukas Schauer
ca0249c46c Update staging.md 2016-08-03 23:47:57 +02:00
Lukas Schauer
afabfff06e updated url to letsencrypt license agreement 2016-08-01 20:35:46 +02:00
Lukas Schauer
bd9cc5b0c4 Added option to run letsencrypt.sh without locks
This should only be used when letsencrypt.sh is under control by a
different script which makes sure that no two processes are touching the
same files.
2016-07-21 12:34:40 +02:00
Lukas Schauer
194464b04b Default WELLKNOWN location is now /var/www/letsencrypt
With this change private and public files are now separated by default.
2016-07-20 17:04:25 +02:00
chkhanu
364bcccf74 Added option to select IP version of name to address resolution (#231) 2016-07-20 16:49:04 +02:00
Martin Schut
44aca90cd7 Introduce per cert configuration directory DOMAINS_D (#242) 2016-07-13 13:23:15 +02:00
Lukas Schauer
2042b177c7 modified lighttpd example config 2016-06-10 23:41:59 +02:00
Domen Puncer Kugler
ae98ff6767 Add Lighttpd example to wellknown.md (#224) 2016-06-08 01:49:50 +02:00
Lukas Schauer
194d543fa1 removed ACCOUNT_KEY and ACCOUNT_KEY_JSON from example config 2016-06-04 11:12:30 +02:00
Christian Tacke
df292dece2 Include method and URL in curl error (#214)
In case curl gives an error, it's helpful to know the URL
being tried and the method. In the GET case, one can easily
retry in the shell and debug this.
2016-06-04 04:15:16 +02:00
Lukas Schauer
f4138efab9 changed dependency check for diff to be compatible with openbsd diff (fixes #219) 2016-06-04 04:12:21 +02:00
Lukas Schauer
034ec30c7d added multi-account support (fixes #92, #163) 2016-06-04 04:01:24 +02:00
Lukas Schauer
ec48906992 initial support for configuration on per-certificate base (#105) 2016-05-26 17:11:42 +02:00
Lukas Schauer
64864f5fa6 added signcsr change to CHANGELOG 2016-05-26 16:04:01 +02:00
Lukas Schauer
620c7eb23e output CA cert for signcsr command (fixes #150) 2016-05-26 15:44:59 +02:00
Lukas Schauer
4e8f944b72 added CERTDIR change to CHANGELOG 2016-05-26 15:06:07 +02:00
Lukas Schauer
8e77ba5e02 added option to set csr-flag indicating ocsp stapling to be mandatory 2016-05-26 15:02:23 +02:00
Alex Macleod
0d8b928923 Check that the detected config is a file (#211) 2016-05-26 15:02:15 +02:00
Alex Macleod
785ffa5539 Make certificate output location configurable (#210) 2016-05-26 14:53:01 +02:00
Lukas Schauer
d5b285868e renamed "config.sh" to "config" 2016-05-22 22:34:28 +02:00
Daniel Beyer
722430039b Remove unneded shebang for config.sh.example 2016-05-22 22:25:14 +02:00
Lukas Schauer
41aae07343 Update CHANGELOG 2016-05-22 18:50:17 +02:00
Daniel Beyer
a3e5ed361b Make location of domains.txt configurable (#204)
This is implemented by defining ${DOMAINS_TXT} in config.sh. If not
set in config.sh, it defaults to the previously (hard-coded) location,
which is ${BASEDIR}/domains.txt.
2016-05-22 18:49:17 +02:00
14 changed files with 357 additions and 102 deletions

4
.gitignore vendored
View File

@@ -1,8 +1,8 @@
private_key.pem
private_key.json
domains.txt
config.sh
config
hook.sh
certs/*
archive/*
.acme-challenges/*
accounts/*

View File

@@ -2,6 +2,28 @@
This file contains a log of major changes in letsencrypt.sh
## [x.x.x] - xxxx-xx-xx
## Changed
- ...
## [0.3.0] - 2016-09-07
## Changed
- 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 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
### Changed

View File

@@ -41,11 +41,18 @@ 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.sh Use specified config file
--config (-f) path/to/config Use specified config file
--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
--algo (-a) rsa|prime256v1|secp384r1 Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
```

View File

@@ -4,7 +4,7 @@ This script also supports the new `dns-01`-type verification. This type of verif
You need a hook script that deploys the challenge to your DNS server!
The hook script (indicated in the config.sh file or the --hook/-k command line argument) gets four arguments: an operation name (clean_challenge, deploy_challenge, or deploy_cert) and some operands for that. For deploy_challenge $2 is the domain name for which the certificate is required, $3 is a "challenge token" (which is not needed for dns-01), and $4 is a token which needs to be inserted in a TXT record for the domain.
The hook script (indicated in the config file or the --hook/-k command line argument) gets four arguments: an operation name (clean_challenge, deploy_challenge, or deploy_cert) and some operands for that. For deploy_challenge $2 is the domain name for which the certificate is required, $3 is a "challenge token" (which is not needed for dns-01), and $4 is a token which needs to be inserted in a TXT record for the domain.
Typically, you will need to split the subdomain name in two, the subdomain name and the domain name separately. For example, for "my.example.com", you'll need "my" and "example.com" separately. You then have to prefix "_acme-challenge." before the subdomain name, as in "_acme-challenge.my" and set a TXT record for that on the domain (e.g. "example.com") which has the value supplied in $4

View File

@@ -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.

View File

@@ -1,22 +1,25 @@
#!/usr/bin/env bash
########################################################
# This is the main config file for letsencrypt.sh #
# #
# This file is looked for in the following locations: #
# $SCRIPTDIR/config.sh (next to this script) #
# /usr/local/etc/letsencrypt.sh/config.sh #
# /etc/letsencrypt.sh/config.sh #
# ${PWD}/config.sh (in current working-directory) #
# $SCRIPTDIR/config (next to this script) #
# /usr/local/etc/letsencrypt.sh/config #
# /etc/letsencrypt.sh/config #
# ${PWD}/config (in current working-directory) #
# #
# 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)
#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"
@@ -30,14 +33,17 @@
# Base directory for account key, generated certificates and list of domains (default: $SCRIPTDIR -- uses config directory if undefined)
#BASEDIR=$SCRIPTDIR
# Output directory for challenge-tokens to be served by webserver or deployed in HOOK (default: $BASEDIR/.acme-challenges)
#WELLKNOWN="${BASEDIR}/.acme-challenges"
# File containing the list of domains to request certificates for (default: $BASEDIR/domains.txt)
#DOMAINS_TXT="${BASEDIR}/domains.txt"
# Location of private account key (default: $BASEDIR/private_key.pem)
#ACCOUNT_KEY="${BASEDIR}/private_key.pem"
# Output directory for generated certificates
#CERTDIR="${BASEDIR}/certs"
# Location of private account registration information (default: $BASEDIR/private_key.json)
#ACCOUNT_KEY_JSON="${BASEDIR}/private_key.json"
# 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: /var/www/letsencrypt)
#WELLKNOWN="/var/www/letsencrypt"
# Default keysize for private keys (default: 4096)
#KEYSIZE="4096"
@@ -74,3 +80,6 @@
# Lockfile location, to prevent concurrent access (default: $BASEDIR/lock)
#LOCKFILE="${BASEDIR}/lock"
# Option to add CSR-flag indicating OCSP stapling to be mandatory (default: no)
#OCSP_MUST_STAPLE="no"

View File

@@ -9,7 +9,7 @@ See below for an example on how the calls change:
### HOOK_CHAIN="no" (default behaviour)
```
# INFO: Using main config file /etc/letsencrypt.sh/config.sh
# INFO: Using main config file /etc/letsencrypt.sh/config
Processing lukas.im with alternative names: www.lukas.im
+ Checking domain name(s) of existing cert... unchanged.
+ Checking expire date of existing cert...
@@ -37,7 +37,7 @@ HOOK: deploy_cert lukas.im /etc/letsencrypt.sh/certs/lukas.im/privkey.pem /etc/l
### HOOK_CHAIN="yes"
```
# INFO: Using main config file /etc/letsencrypt.sh/config.sh
# INFO: Using main config file /etc/letsencrypt.sh/config
Processing lukas.im with alternative names: www.lukas.im
+ Checking domain name(s) of existing cert... unchanged.
+ Checking expire date of existing cert...

View File

@@ -0,0 +1,18 @@
# 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

View File

@@ -1,15 +1,12 @@
# Staging
Lets Encrypt has stringent rate limits in place during the public beta period.
Lets 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.
To avoid this, please set the CA property to the Lets Encrypt staging server URL in your `config.sh` file:
To avoid this, please set the CA property to the Lets Encrypt staging server URL in your config file:
```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.

View File

@@ -54,3 +54,15 @@ Alias /.well-known/acme-challenge /var/www/letsencrypt
</IfModule>
</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/"
)
```

View File

@@ -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."
command -v grep > /dev/null 2>&1 || _exiterr "This script requires grep."
_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
set +e
@@ -46,14 +47,57 @@ check_dependencies() {
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
load_config() {
# Check for config in various locations
if [[ -z "${CONFIG:-}" ]]; then
for check_config in "/etc/letsencrypt.sh" "/usr/local/etc/letsencrypt.sh" "${PWD}" "${SCRIPTDIR}"; do
if [[ -e "${check_config}/config.sh" ]]; then
if [[ -f "${check_config}/config" ]]; then
BASEDIR="${check_config}"
CONFIG="${check_config}/config.sh"
CONFIG="${check_config}/config"
break
fi
done
@@ -61,14 +105,16 @@ 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"
RENEW_DAYS="30"
ACCOUNT_KEY=
ACCOUNT_KEY_JSON=
KEYSIZE="4096"
WELLKNOWN=
PRIVATE_KEY_RENEW="yes"
@@ -76,12 +122,14 @@ load_config() {
OPENSSL_CNF="$(openssl version -d | cut -d\" -f2)/openssl.cnf"
CONTACT_EMAIL=
LOCKFILE=
OCSP_MUST_STAPLE="no"
IP_VERSION=
if [[ -z "${CONFIG:-}" ]]; then
echo "#" >&2
echo "# !! WARNING !! No main config file found, using default config!" >&2
echo "#" >&2
elif [[ -e "${CONFIG}" ]]; then
elif [[ -f "${CONFIG}" ]]; then
echo "# INFO: Using main config file ${CONFIG}"
BASEDIR="$(dirname "${CONFIG}")"
# shellcheck disable=SC1090
@@ -115,20 +163,37 @@ load_config() {
# Check BASEDIR and set default variables
[[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}"
[[ -z "${ACCOUNT_KEY}" ]] && ACCOUNT_KEY="${BASEDIR}/private_key.pem"
[[ -z "${ACCOUNT_KEY_JSON}" ]] && ACCOUNT_KEY_JSON="${BASEDIR}/private_key.json"
[[ -z "${WELLKNOWN}" ]] && WELLKNOWN="${BASEDIR}/.acme-challenges"
CAHASH="$(echo "${CA}" | urlbase64)"
[[ -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 "${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}"
[[ "${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
[[ "${KEY_ALGO}" =~ ^(rsa|prime256v1|secp384r1)$ ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... can not continue."
verify_config
store_configvars
}
# Initialize system
@@ -136,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}")"
@@ -152,7 +219,7 @@ init_system() {
_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 CONFIG
export WELLKNOWN BASEDIR CERTDIR CONFIG
# Checking for private key ...
register_new_key="no"
@@ -182,16 +249,21 @@ init_system() {
echo "+ Registering account key with letsencrypt..."
[[ ! -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}"
(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}"
(signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
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
}
# Different sed version for different os types...
@@ -254,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
@@ -271,7 +347,7 @@ http_request() {
set -e
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
if [[ ! "${statuscode:0:1}" = "2" ]]; then
@@ -279,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
@@ -503,19 +581,19 @@ sign_domain() {
fi
# If there is no existing certificate directory => make it
if [[ ! -e "${BASEDIR}/certs/${domain}" ]]; then
echo " + Creating new directory ${BASEDIR}/certs/${domain} ..."
mkdir -p "${BASEDIR}/certs/${domain}"
if [[ ! -e "${CERTDIR}/${domain}" ]]; then
echo " + Creating new directory ${CERTDIR}/${domain} ..."
mkdir -p "${CERTDIR}/${domain}" || _exiterr "Unable to create directory ${CERTDIR}/${domain}"
fi
privkey="privkey.pem"
# 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..."
privkey="privkey-${timestamp}.pem"
case "${KEY_ALGO}" in
rsa) _openssl genrsa -out "${BASEDIR}/certs/${domain}/privkey-${timestamp}.pem" "${KEYSIZE}";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${BASEDIR}/certs/${domain}/privkey-${timestamp}.pem";;
rsa) _openssl genrsa -out "${CERTDIR}/${domain}/privkey-${timestamp}.pem" "${KEYSIZE}";;
prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${CERTDIR}/${domain}/privkey-${timestamp}.pem";;
esac
fi
@@ -530,33 +608,36 @@ sign_domain() {
tmp_openssl_cnf="$(_mktemp)"
cat "${OPENSSL_CNF}" > "${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}"
crt_path="${BASEDIR}/certs/${domain}/cert-${timestamp}.pem"
crt_path="${CERTDIR}/${domain}/cert-${timestamp}.pem"
# 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
echo " + Creating fullchain.pem..."
cat "${crt_path}" > "${BASEDIR}/certs/${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"
if ! grep -q "BEGIN CERTIFICATE" "${BASEDIR}/certs/${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
cat "${crt_path}" > "${CERTDIR}/${domain}/fullchain-${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" "${CERTDIR}/${domain}/chain-${timestamp}.pem"; then
openssl x509 -in "${CERTDIR}/${domain}/chain-${timestamp}.pem" -inform DER -out "${CERTDIR}/${domain}/chain-${timestamp}.pem" -outform PEM
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
[[ "${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 "fullchain-${timestamp}.pem" "${BASEDIR}/certs/${domain}/fullchain.pem"
ln -sf "cert-${timestamp}.csr" "${BASEDIR}/certs/${domain}/cert.csr"
ln -sf "cert-${timestamp}.pem" "${BASEDIR}/certs/${domain}/cert.pem"
ln -sf "chain-${timestamp}.pem" "${CERTDIR}/${domain}/chain.pem"
ln -sf "fullchain-${timestamp}.pem" "${CERTDIR}/${domain}/fullchain.pem"
ln -sf "cert-${timestamp}.csr" "${CERTDIR}/${domain}/cert.csr"
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}" "${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
echo " + Done!"
@@ -570,8 +651,10 @@ command_sign_domains() {
if [[ -n "${PARAM_DOMAIN:-}" ]]; then
DOMAINS_TXT="$(_mktemp)"
printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
elif [[ -e "${BASEDIR}/domains.txt" ]]; then
DOMAINS_TXT="${BASEDIR}/domains.txt"
elif [[ -e "${DOMAINS_TXT}" ]]; then
if [[ ! -r "${DOMAINS_TXT}" ]]; then
_exiterr "domains.txt found but not readable"
fi
else
_exiterr "domains.txt not found and --domain not given"
fi
@@ -580,10 +663,11 @@ command_sign_domains() {
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
reset_configvars
IFS="${ORIGIFS}"
domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
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}"
@@ -593,6 +677,46 @@ command_sign_domains() {
echo "Processing ${domain} with alternative names: ${morenames}"
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
printf " + Checking domain name(s) of existing cert..."
@@ -623,7 +747,7 @@ command_sign_domains() {
else
# Certificate-Names unchanged and cert is still valid
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
fi
else
@@ -632,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
@@ -655,7 +784,33 @@ command_sign_csr() {
_exiterr "Could not read certificate signing request ${csrfile}"
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
}
@@ -702,7 +857,7 @@ command_cleanup() {
fi
# Loop over all certificate directories
for certdir in "${BASEDIR}/certs/"*; do
for certdir in "${CERTDIR}/"*; do
# Skip if entry is not a folder
[[ -d "${certdir}" ]] || continue
@@ -771,7 +926,7 @@ command_help() {
command_env() {
echo "# letsencrypt.sh configuration"
load_config
typeset -p CA LICENSE CHALLENGETYPE 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)
@@ -828,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)
@@ -840,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
@@ -847,6 +1025,18 @@ 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)
PARAM_OCSP_MUST_STAPLE="yes"
;;
# PARAM_Usage: --privkey (-p) path/to/key.pem
# PARAM_Description: Use specified private key instead of account key (useful for revocation)
--privkey|-p)
@@ -855,7 +1045,7 @@ main() {
PARAM_ACCOUNT_KEY="${1}"
;;
# PARAM_Usage: --config (-f) path/to/config.sh
# PARAM_Usage: --config (-f) path/to/config
# PARAM_Description: Use specified config file
--config|-f)
shift 1
@@ -871,6 +1061,14 @@ main() {
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_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
--challenge|-t)

27
test.sh
View File

@@ -96,10 +96,10 @@ mkdir -p .acme-challenges/.well-known/acme-challenge
) &
# Generate config and create empty domains.txt
echo 'CA="https://testca.kurz.pw/directory"' > config.sh
echo 'LICENSE="https://testca.kurz.pw/terms/v1"' >> config.sh
echo 'WELLKNOWN=".acme-challenges/.well-known/acme-challenge"' >> config.sh
echo 'RENEW_DAYS="14"' >> config.sh
echo 'CA="https://testca.kurz.pw/directory"' > config
echo 'LICENSE="https://testca.kurz.pw/terms/v1"' >> config
echo 'WELLKNOWN=".acme-challenges/.well-known/acme-challenge"' >> config
echo 'RENEW_DAYS="14"' >> config
touch domains.txt
# Check if help command is working
@@ -114,13 +114,13 @@ _CHECK_ERRORLOG
_TEST "First run in cron mode, checking if private key is generated and registered"
./letsencrypt.sh --cron > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Registering account key"
_CHECK_FILE "private_key.pem"
_CHECK_FILE accounts/*/account_key.pem
_CHECK_ERRORLOG
# Temporarily move config out of the way and try signing certificate by using temporary config location
_TEST "Try signing using temporary config location and with domain as command line parameter"
mv config.sh tmp_config.sh
./letsencrypt.sh --cron --domain "${TMP_URL}" --domain "${TMP2_URL}" -f tmp_config.sh > tmplog 2> errorlog || _FAIL "Script execution failed"
mv config tmp_config
./letsencrypt.sh --cron --domain "${TMP_URL}" --domain "${TMP2_URL}" -f tmp_config > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_NOT_LOG "Checking domain name(s) of existing cert"
_CHECK_LOG "Generating private key"
_CHECK_LOG "Requesting challenge for ${TMP_URL}"
@@ -129,11 +129,7 @@ _CHECK_LOG "Challenge is valid!"
_CHECK_LOG "Creating fullchain.pem"
_CHECK_LOG "Done!"
_CHECK_ERRORLOG
mv tmp_config.sh config.sh
# Move private key and add new location to config
mv private_key.pem account_key.pem
echo 'PRIVATE_KEY="./account_key.pem"' >> config.sh
mv tmp_config config
# Add third domain to command-lime, should force renewal.
_TEST "Run in cron mode again, this time adding third domain, should force renewal."
@@ -161,7 +157,7 @@ _CHECK_LOG "Skipping renew"
_CHECK_ERRORLOG
# Disable private key renew
echo 'PRIVATE_KEY_RENEW="no"' >> config.sh
echo 'PRIVATE_KEY_RENEW="no"' >> config
# Run in cron mode one last time, with domain in domains.txt and force-resign (should find certificate, resign anyway, and not generate private key)
_TEST "Run in cron mode one last time, with domain in domains.txt and force-resign"
@@ -184,12 +180,9 @@ _CHECK_LOG "BEGIN CERTIFICATE"
_CHECK_LOG "END CERTIFICATE"
_CHECK_NOT_LOG "ERROR"
# Delete account key (not needed anymore)
rm account_key.pem
# Check if renewal works
_TEST "Run in cron mode again, to check if renewal works"
echo 'RENEW_DAYS="300"' >> config.sh
echo 'RENEW_DAYS="300"' >> config
./letsencrypt.sh --cron > tmplog 2> errorlog || _FAIL "Script execution failed"
_CHECK_LOG "Checking domain name(s) of existing cert... unchanged."
_CHECK_LOG "Renewing!"