|
@@ -35,6 +35,7 @@ check_dependencies() {
|
35
|
35
|
_sed "" < /dev/null > /dev/null 2>&1 || _exiterr "This script requires sed with support for extended (modern) regular expressions."
|
36
|
36
|
command -v grep > /dev/null 2>&1 || _exiterr "This script requires grep."
|
37
|
37
|
_mktemp -u > /dev/null 2>&1 || _exiterr "This script requires mktemp."
|
|
38
|
+ diff -u /dev/null /dev/null || _exiterr "This script requires diff."
|
38
|
39
|
|
39
|
40
|
# curl returns with an error code in some ancient versions so we have to catch that
|
40
|
41
|
set +e
|
|
@@ -46,14 +47,57 @@ check_dependencies() {
|
46
|
47
|
fi
|
47
|
48
|
}
|
48
|
49
|
|
|
50
|
+store_configvars() {
|
|
51
|
+ __KEY_ALGO="${KEY_ALGO}"
|
|
52
|
+ __OCSP_MUST_STAPLE="${OCSP_MUST_STAPLE}"
|
|
53
|
+ __PRIVATE_KEY_RENEW="${PRIVATE_KEY_RENEW}"
|
|
54
|
+ __KEYSIZE="${KEYSIZE}"
|
|
55
|
+ __CHALLENGETYPE="${CHALLENGETYPE}"
|
|
56
|
+ __HOOK="${HOOK}"
|
|
57
|
+ __WELLKNOWN="${WELLKNOWN}"
|
|
58
|
+ __HOOK_CHAIN="${HOOK_CHAIN}"
|
|
59
|
+ __OPENSSL_CNF="${OPENSSL_CNF}"
|
|
60
|
+ __RENEW_DAYS="${RENEW_DAYS}"
|
|
61
|
+ __IP_VERSION="${IP_VERSION}"
|
|
62
|
+}
|
|
63
|
+
|
|
64
|
+reset_configvars() {
|
|
65
|
+ KEY_ALGO="${__KEY_ALGO}"
|
|
66
|
+ OCSP_MUST_STAPLE="${__OCSP_MUST_STAPLE}"
|
|
67
|
+ PRIVATE_KEY_RENEW="${__PRIVATE_KEY_RENEW}"
|
|
68
|
+ KEYSIZE="${__KEYSIZE}"
|
|
69
|
+ CHALLENGETYPE="${__CHALLENGETYPE}"
|
|
70
|
+ HOOK="${__HOOK}"
|
|
71
|
+ WELLKNOWN="${__WELLKNOWN}"
|
|
72
|
+ HOOK_CHAIN="${__HOOK_CHAIN}"
|
|
73
|
+ OPENSSL_CNF="${__OPENSSL_CNF}"
|
|
74
|
+ RENEW_DAYS="${__RENEW_DAYS}"
|
|
75
|
+ IP_VERSION="${__IP_VERSION}"
|
|
76
|
+}
|
|
77
|
+
|
|
78
|
+# verify configuration values
|
|
79
|
+verify_config() {
|
|
80
|
+ [[ "${CHALLENGETYPE}" =~ (http-01|dns-01) ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... can not continue."
|
|
81
|
+ if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then
|
|
82
|
+ _exiterr "Challenge type dns-01 needs a hook script for deployment... can not continue."
|
|
83
|
+ fi
|
|
84
|
+ if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" ]]; then
|
|
85
|
+ _exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
|
|
86
|
+ fi
|
|
87
|
+ [[ "${KEY_ALGO}" =~ ^(rsa|prime256v1|secp384r1)$ ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... can not continue."
|
|
88
|
+ if [[ -n "${IP_VERSION}" ]]; then
|
|
89
|
+ [[ "${IP_VERSION}" = "4" || "${IP_VERSION}" = "6" ]] || _exiterr "Unknown IP version ${IP_VERSION}... can not continue."
|
|
90
|
+ fi
|
|
91
|
+}
|
|
92
|
+
|
49
|
93
|
# Setup default config values, search for and load configuration files
|
50
|
94
|
load_config() {
|
51
|
95
|
# Check for config in various locations
|
52
|
96
|
if [[ -z "${CONFIG:-}" ]]; then
|
53
|
97
|
for check_config in "/etc/letsencrypt.sh" "/usr/local/etc/letsencrypt.sh" "${PWD}" "${SCRIPTDIR}"; do
|
54
|
|
- if [[ -e "${check_config}/config.sh" ]]; then
|
|
98
|
+ if [[ -f "${check_config}/config" ]]; then
|
55
|
99
|
BASEDIR="${check_config}"
|
56
|
|
- CONFIG="${check_config}/config.sh"
|
|
100
|
+ CONFIG="${check_config}/config"
|
57
|
101
|
break
|
58
|
102
|
fi
|
59
|
103
|
done
|
|
@@ -61,14 +105,16 @@ load_config() {
|
61
|
105
|
|
62
|
106
|
# Default values
|
63
|
107
|
CA="https://acme-v01.api.letsencrypt.org/directory"
|
64
|
|
- LICENSE="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
|
|
108
|
+ LICENSE="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf"
|
|
109
|
+ CERTDIR=
|
|
110
|
+ ACCOUNTDIR=
|
65
|
111
|
CHALLENGETYPE="http-01"
|
66
|
112
|
CONFIG_D=
|
|
113
|
+ DOMAINS_D=
|
|
114
|
+ DOMAINS_TXT=
|
67
|
115
|
HOOK=
|
68
|
116
|
HOOK_CHAIN="no"
|
69
|
117
|
RENEW_DAYS="30"
|
70
|
|
- ACCOUNT_KEY=
|
71
|
|
- ACCOUNT_KEY_JSON=
|
72
|
118
|
KEYSIZE="4096"
|
73
|
119
|
WELLKNOWN=
|
74
|
120
|
PRIVATE_KEY_RENEW="yes"
|
|
@@ -76,12 +122,14 @@ load_config() {
|
76
|
122
|
OPENSSL_CNF="$(openssl version -d | cut -d\" -f2)/openssl.cnf"
|
77
|
123
|
CONTACT_EMAIL=
|
78
|
124
|
LOCKFILE=
|
|
125
|
+ OCSP_MUST_STAPLE="no"
|
|
126
|
+ IP_VERSION=
|
79
|
127
|
|
80
|
128
|
if [[ -z "${CONFIG:-}" ]]; then
|
81
|
129
|
echo "#" >&2
|
82
|
130
|
echo "# !! WARNING !! No main config file found, using default config!" >&2
|
83
|
131
|
echo "#" >&2
|
84
|
|
- elif [[ -e "${CONFIG}" ]]; then
|
|
132
|
+ elif [[ -f "${CONFIG}" ]]; then
|
85
|
133
|
echo "# INFO: Using main config file ${CONFIG}"
|
86
|
134
|
BASEDIR="$(dirname "${CONFIG}")"
|
87
|
135
|
# shellcheck disable=SC1090
|
|
@@ -115,20 +163,37 @@ load_config() {
|
115
|
163
|
# Check BASEDIR and set default variables
|
116
|
164
|
[[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}"
|
117
|
165
|
|
118
|
|
- [[ -z "${ACCOUNT_KEY}" ]] && ACCOUNT_KEY="${BASEDIR}/private_key.pem"
|
119
|
|
- [[ -z "${ACCOUNT_KEY_JSON}" ]] && ACCOUNT_KEY_JSON="${BASEDIR}/private_key.json"
|
120
|
|
- [[ -z "${WELLKNOWN}" ]] && WELLKNOWN="${BASEDIR}/.acme-challenges"
|
|
166
|
+ CAHASH="$(echo "${CA}" | urlbase64)"
|
|
167
|
+ [[ -z "${ACCOUNTDIR}" ]] && ACCOUNTDIR="${BASEDIR}/accounts"
|
|
168
|
+ mkdir -p "${ACCOUNTDIR}/${CAHASH}"
|
|
169
|
+ [[ -f "${ACCOUNTDIR}/${CAHASH}/config" ]] && . "${ACCOUNTDIR}/${CAHASH}/config"
|
|
170
|
+ ACCOUNT_KEY="${ACCOUNTDIR}/${CAHASH}/account_key.pem"
|
|
171
|
+ ACCOUNT_KEY_JSON="${ACCOUNTDIR}/${CAHASH}/registration_info.json"
|
|
172
|
+
|
|
173
|
+ if [[ -f "${BASEDIR}/private_key.pem" ]] && [[ ! -f "${ACCOUNT_KEY}" ]]; then
|
|
174
|
+ echo "! Moving private_key.pem to ${ACCOUNT_KEY}"
|
|
175
|
+ mv "${BASEDIR}/private_key.pem" "${ACCOUNT_KEY}"
|
|
176
|
+ fi
|
|
177
|
+ if [[ -f "${BASEDIR}/private_key.json" ]] && [[ ! -f "${ACCOUNT_KEY_JSON}" ]]; then
|
|
178
|
+ echo "! Moving private_key.json to ${ACCOUNT_KEY_JSON}"
|
|
179
|
+ mv "${BASEDIR}/private_key.json" "${ACCOUNT_KEY_JSON}"
|
|
180
|
+ fi
|
|
181
|
+
|
|
182
|
+ [[ -z "${CERTDIR}" ]] && CERTDIR="${BASEDIR}/certs"
|
|
183
|
+ [[ -z "${DOMAINS_TXT}" ]] && DOMAINS_TXT="${BASEDIR}/domains.txt"
|
|
184
|
+ [[ -z "${WELLKNOWN}" ]] && WELLKNOWN="/var/www/letsencrypt"
|
121
|
185
|
[[ -z "${LOCKFILE}" ]] && LOCKFILE="${BASEDIR}/lock"
|
|
186
|
+ [[ -n "${PARAM_NO_LOCK:-}" ]] && LOCKFILE=""
|
122
|
187
|
|
123
|
188
|
[[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}"
|
|
189
|
+ [[ -n "${PARAM_CERTDIR:-}" ]] && CERTDIR="${PARAM_CERTDIR}"
|
124
|
190
|
[[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}"
|
125
|
191
|
[[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
|
|
192
|
+ [[ -n "${PARAM_OCSP_MUST_STAPLE:-}" ]] && OCSP_MUST_STAPLE="${PARAM_OCSP_MUST_STAPLE}"
|
|
193
|
+ [[ -n "${PARAM_IP_VERSION:-}" ]] && IP_VERSION="${PARAM_IP_VERSION}"
|
126
|
194
|
|
127
|
|
- [[ "${CHALLENGETYPE}" =~ (http-01|dns-01) ]] || _exiterr "Unknown challenge type ${CHALLENGETYPE}... can not continue."
|
128
|
|
- if [[ "${CHALLENGETYPE}" = "dns-01" ]] && [[ -z "${HOOK}" ]]; then
|
129
|
|
- _exiterr "Challenge type dns-01 needs a hook script for deployment... can not continue."
|
130
|
|
- fi
|
131
|
|
- [[ "${KEY_ALGO}" =~ ^(rsa|prime256v1|secp384r1)$ ]] || _exiterr "Unknown public key algorithm ${KEY_ALGO}... can not continue."
|
|
195
|
+ verify_config
|
|
196
|
+ store_configvars
|
132
|
197
|
}
|
133
|
198
|
|
134
|
199
|
# Initialize system
|
|
@@ -136,11 +201,13 @@ init_system() {
|
136
|
201
|
load_config
|
137
|
202
|
|
138
|
203
|
# Lockfile handling (prevents concurrent access)
|
139
|
|
- LOCKDIR="$(dirname "${LOCKFILE}")"
|
140
|
|
- [[ -w "${LOCKDIR}" ]] || _exiterr "Directory ${LOCKDIR} for LOCKFILE ${LOCKFILE} is not writable, aborting."
|
141
|
|
- ( set -C; date > "${LOCKFILE}" ) 2>/dev/null || _exiterr "Lock file '${LOCKFILE}' present, aborting."
|
142
|
|
- remove_lock() { rm -f "${LOCKFILE}"; }
|
143
|
|
- trap 'remove_lock' EXIT
|
|
204
|
+ if [[ -n "${LOCKFILE}" ]]; then
|
|
205
|
+ LOCKDIR="$(dirname "${LOCKFILE}")"
|
|
206
|
+ [[ -w "${LOCKDIR}" ]] || _exiterr "Directory ${LOCKDIR} for LOCKFILE ${LOCKFILE} is not writable, aborting."
|
|
207
|
+ ( set -C; date > "${LOCKFILE}" ) 2>/dev/null || _exiterr "Lock file '${LOCKFILE}' present, aborting."
|
|
208
|
+ remove_lock() { rm -f "${LOCKFILE}"; }
|
|
209
|
+ trap 'remove_lock' EXIT
|
|
210
|
+ fi
|
144
|
211
|
|
145
|
212
|
# Get CA URLs
|
146
|
213
|
CA_DIRECTORY="$(http_request get "${CA}")"
|
|
@@ -152,7 +219,7 @@ init_system() {
|
152
|
219
|
_exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
|
153
|
220
|
|
154
|
221
|
# Export some environment variables to be used in hook script
|
155
|
|
- export WELLKNOWN BASEDIR CONFIG
|
|
222
|
+ export WELLKNOWN BASEDIR CERTDIR CONFIG
|
156
|
223
|
|
157
|
224
|
# Checking for private key ...
|
158
|
225
|
register_new_key="no"
|
|
@@ -182,16 +249,21 @@ init_system() {
|
182
|
249
|
echo "+ Registering account key with letsencrypt..."
|
183
|
250
|
[[ ! -z "${CA_NEW_REG}" ]] || _exiterr "Certificate authority doesn't allow registrations."
|
184
|
251
|
# If an email for the contact has been provided then adding it to the registration request
|
|
252
|
+ FAILED=false
|
185
|
253
|
if [[ -n "${CONTACT_EMAIL}" ]]; then
|
186
|
|
- signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}"
|
|
254
|
+ (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
|
187
|
255
|
else
|
188
|
|
- signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}"
|
|
256
|
+ (signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}") || FAILED=true
|
|
257
|
+ fi
|
|
258
|
+ if [[ "${FAILED}" = "true" ]]; then
|
|
259
|
+ echo
|
|
260
|
+ echo
|
|
261
|
+ echo "Error registering account key. See message above for more information."
|
|
262
|
+ rm "${ACCOUNT_KEY}" "${ACCOUNT_KEY_JSON}"
|
|
263
|
+ exit 1
|
189
|
264
|
fi
|
190
|
265
|
fi
|
191
|
266
|
|
192
|
|
- if [[ "${CHALLENGETYPE}" = "http-01" && ! -d "${WELLKNOWN}" ]]; then
|
193
|
|
- _exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions."
|
194
|
|
- fi
|
195
|
267
|
}
|
196
|
268
|
|
197
|
269
|
# Different sed version for different os types...
|
|
@@ -254,15 +326,19 @@ _openssl() {
|
254
|
326
|
http_request() {
|
255
|
327
|
tempcont="$(_mktemp)"
|
256
|
328
|
|
|
329
|
+ if [[ -n "${IP_VERSION:-}" ]]; then
|
|
330
|
+ ip_version="-${IP_VERSION}"
|
|
331
|
+ fi
|
|
332
|
+
|
257
|
333
|
set +e
|
258
|
334
|
if [[ "${1}" = "head" ]]; then
|
259
|
|
- statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
|
|
335
|
+ statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
|
260
|
336
|
curlret="${?}"
|
261
|
337
|
elif [[ "${1}" = "get" ]]; then
|
262
|
|
- statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}")"
|
|
338
|
+ statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}")"
|
263
|
339
|
curlret="${?}"
|
264
|
340
|
elif [[ "${1}" = "post" ]]; then
|
265
|
|
- statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -d "${3}")"
|
|
341
|
+ statuscode="$(curl ${ip_version:-} -s -w "%{http_code}" -o "${tempcont}" "${2}" -d "${3}")"
|
266
|
342
|
curlret="${?}"
|
267
|
343
|
else
|
268
|
344
|
set -e
|
|
@@ -271,7 +347,7 @@ http_request() {
|
271
|
347
|
set -e
|
272
|
348
|
|
273
|
349
|
if [[ ! "${curlret}" = "0" ]]; then
|
274
|
|
- _exiterr "Problem connecting to server (curl returned with ${curlret})"
|
|
350
|
+ _exiterr "Problem connecting to server (${1} for ${2}; curl returned with ${curlret})"
|
275
|
351
|
fi
|
276
|
352
|
|
277
|
353
|
if [[ ! "${statuscode:0:1}" = "2" ]]; then
|
|
@@ -279,6 +355,8 @@ http_request() {
|
279
|
355
|
echo >&2
|
280
|
356
|
echo "Details:" >&2
|
281
|
357
|
cat "${tempcont}" >&2
|
|
358
|
+ echo >&2
|
|
359
|
+ echo >&2
|
282
|
360
|
rm -f "${tempcont}"
|
283
|
361
|
|
284
|
362
|
# Wait for hook script to clean the challenge if used
|
|
@@ -503,19 +581,19 @@ sign_domain() {
|
503
|
581
|
fi
|
504
|
582
|
|
505
|
583
|
# If there is no existing certificate directory => make it
|
506
|
|
- if [[ ! -e "${BASEDIR}/certs/${domain}" ]]; then
|
507
|
|
- echo " + Creating new directory ${BASEDIR}/certs/${domain} ..."
|
508
|
|
- mkdir -p "${BASEDIR}/certs/${domain}"
|
|
584
|
+ if [[ ! -e "${CERTDIR}/${domain}" ]]; then
|
|
585
|
+ echo " + Creating new directory ${CERTDIR}/${domain} ..."
|
|
586
|
+ mkdir -p "${CERTDIR}/${domain}" || _exiterr "Unable to create directory ${CERTDIR}/${domain}"
|
509
|
587
|
fi
|
510
|
588
|
|
511
|
589
|
privkey="privkey.pem"
|
512
|
590
|
# generate a new private key if we need or want one
|
513
|
|
- if [[ ! -r "${BASEDIR}/certs/${domain}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
|
|
591
|
+ if [[ ! -r "${CERTDIR}/${domain}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
|
514
|
592
|
echo " + Generating private key..."
|
515
|
593
|
privkey="privkey-${timestamp}.pem"
|
516
|
594
|
case "${KEY_ALGO}" in
|
517
|
|
- rsa) _openssl genrsa -out "${BASEDIR}/certs/${domain}/privkey-${timestamp}.pem" "${KEYSIZE}";;
|
518
|
|
- prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${BASEDIR}/certs/${domain}/privkey-${timestamp}.pem";;
|
|
595
|
+ rsa) _openssl genrsa -out "${CERTDIR}/${domain}/privkey-${timestamp}.pem" "${KEYSIZE}";;
|
|
596
|
+ prime256v1|secp384r1) _openssl ecparam -genkey -name "${KEY_ALGO}" -out "${CERTDIR}/${domain}/privkey-${timestamp}.pem";;
|
519
|
597
|
esac
|
520
|
598
|
fi
|
521
|
599
|
|
|
@@ -530,33 +608,36 @@ sign_domain() {
|
530
|
608
|
tmp_openssl_cnf="$(_mktemp)"
|
531
|
609
|
cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
|
532
|
610
|
printf "[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}"
|
533
|
|
- 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}"
|
|
611
|
+ if [ "${OCSP_MUST_STAPLE}" = "yes" ]; then
|
|
612
|
+ printf "\n1.3.6.1.5.5.7.1.24=DER:30:03:02:01:05" >> "${tmp_openssl_cnf}"
|
|
613
|
+ fi
|
|
614
|
+ openssl req -new -sha256 -key "${CERTDIR}/${domain}/${privkey}" -out "${CERTDIR}/${domain}/cert-${timestamp}.csr" -subj "/CN=${domain}/" -reqexts SAN -config "${tmp_openssl_cnf}"
|
534
|
615
|
rm -f "${tmp_openssl_cnf}"
|
535
|
616
|
|
536
|
|
- crt_path="${BASEDIR}/certs/${domain}/cert-${timestamp}.pem"
|
|
617
|
+ crt_path="${CERTDIR}/${domain}/cert-${timestamp}.pem"
|
537
|
618
|
# shellcheck disable=SC2086
|
538
|
|
- sign_csr "$(< "${BASEDIR}/certs/${domain}/cert-${timestamp}.csr" )" ${altnames} 3>"${crt_path}"
|
|
619
|
+ sign_csr "$(< "${CERTDIR}/${domain}/cert-${timestamp}.csr" )" ${altnames} 3>"${crt_path}"
|
539
|
620
|
|
540
|
621
|
# Create fullchain.pem
|
541
|
622
|
echo " + Creating fullchain.pem..."
|
542
|
|
- cat "${crt_path}" > "${BASEDIR}/certs/${domain}/fullchain-${timestamp}.pem"
|
543
|
|
- 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"
|
544
|
|
- if ! grep -q "BEGIN CERTIFICATE" "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem"; then
|
545
|
|
- openssl x509 -in "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" -inform DER -out "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" -outform PEM
|
|
623
|
+ cat "${crt_path}" > "${CERTDIR}/${domain}/fullchain-${timestamp}.pem"
|
|
624
|
+ 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"
|
|
625
|
+ if ! grep -q "BEGIN CERTIFICATE" "${CERTDIR}/${domain}/chain-${timestamp}.pem"; then
|
|
626
|
+ openssl x509 -in "${CERTDIR}/${domain}/chain-${timestamp}.pem" -inform DER -out "${CERTDIR}/${domain}/chain-${timestamp}.pem" -outform PEM
|
546
|
627
|
fi
|
547
|
|
- cat "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" >> "${BASEDIR}/certs/${domain}/fullchain-${timestamp}.pem"
|
|
628
|
+ cat "${CERTDIR}/${domain}/chain-${timestamp}.pem" >> "${CERTDIR}/${domain}/fullchain-${timestamp}.pem"
|
548
|
629
|
|
549
|
630
|
# Update symlinks
|
550
|
|
- [[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${BASEDIR}/certs/${domain}/privkey.pem"
|
|
631
|
+ [[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${CERTDIR}/${domain}/privkey.pem"
|
551
|
632
|
|
552
|
|
- ln -sf "chain-${timestamp}.pem" "${BASEDIR}/certs/${domain}/chain.pem"
|
553
|
|
- ln -sf "fullchain-${timestamp}.pem" "${BASEDIR}/certs/${domain}/fullchain.pem"
|
554
|
|
- ln -sf "cert-${timestamp}.csr" "${BASEDIR}/certs/${domain}/cert.csr"
|
555
|
|
- ln -sf "cert-${timestamp}.pem" "${BASEDIR}/certs/${domain}/cert.pem"
|
|
633
|
+ ln -sf "chain-${timestamp}.pem" "${CERTDIR}/${domain}/chain.pem"
|
|
634
|
+ ln -sf "fullchain-${timestamp}.pem" "${CERTDIR}/${domain}/fullchain.pem"
|
|
635
|
+ ln -sf "cert-${timestamp}.csr" "${CERTDIR}/${domain}/cert.csr"
|
|
636
|
+ ln -sf "cert-${timestamp}.pem" "${CERTDIR}/${domain}/cert.pem"
|
556
|
637
|
|
557
|
638
|
# Wait for hook script to clean the challenge and to deploy cert if used
|
558
|
639
|
export KEY_ALGO
|
559
|
|
- [[ -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}"
|
|
640
|
+ [[ -n "${HOOK}" ]] && "${HOOK}" "deploy_cert" "${domain}" "${CERTDIR}/${domain}/privkey.pem" "${CERTDIR}/${domain}/cert.pem" "${CERTDIR}/${domain}/fullchain.pem" "${CERTDIR}/${domain}/chain.pem" "${timestamp}"
|
560
|
641
|
|
561
|
642
|
unset challenge_token
|
562
|
643
|
echo " + Done!"
|
|
@@ -570,8 +651,10 @@ command_sign_domains() {
|
570
|
651
|
if [[ -n "${PARAM_DOMAIN:-}" ]]; then
|
571
|
652
|
DOMAINS_TXT="$(_mktemp)"
|
572
|
653
|
printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
|
573
|
|
- elif [[ -e "${BASEDIR}/domains.txt" ]]; then
|
574
|
|
- DOMAINS_TXT="${BASEDIR}/domains.txt"
|
|
654
|
+ elif [[ -e "${DOMAINS_TXT}" ]]; then
|
|
655
|
+ if [[ ! -r "${DOMAINS_TXT}" ]]; then
|
|
656
|
+ _exiterr "domains.txt found but not readable"
|
|
657
|
+ fi
|
575
|
658
|
else
|
576
|
659
|
_exiterr "domains.txt not found and --domain not given"
|
577
|
660
|
fi
|
|
@@ -580,10 +663,11 @@ command_sign_domains() {
|
580
|
663
|
ORIGIFS="${IFS}"
|
581
|
664
|
IFS=$'\n'
|
582
|
665
|
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
|
|
666
|
+ reset_configvars
|
583
|
667
|
IFS="${ORIGIFS}"
|
584
|
668
|
domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
|
585
|
669
|
morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)"
|
586
|
|
- cert="${BASEDIR}/certs/${domain}/cert.pem"
|
|
670
|
+ cert="${CERTDIR}/${domain}/cert.pem"
|
587
|
671
|
|
588
|
672
|
force_renew="${PARAM_FORCE:-no}"
|
589
|
673
|
|
|
@@ -593,6 +677,46 @@ command_sign_domains() {
|
593
|
677
|
echo "Processing ${domain} with alternative names: ${morenames}"
|
594
|
678
|
fi
|
595
|
679
|
|
|
680
|
+ # read cert config
|
|
681
|
+ # for now this loads the certificate specific config in a subshell and parses a diff of set variables.
|
|
682
|
+ # we could just source the config file but i decided to go this way to protect people from accidentally overriding
|
|
683
|
+ # variables used internally by this script itself.
|
|
684
|
+ if [[ -n "${DOMAINS_D}" ]]; then
|
|
685
|
+ certconfig="${DOMAINS_D}/${domain}"
|
|
686
|
+ else
|
|
687
|
+ certconfig="${CERTDIR}/${domain}/config"
|
|
688
|
+ fi
|
|
689
|
+
|
|
690
|
+ if [ -f "${certconfig}" ]; then
|
|
691
|
+ echo " + Using certificate specific config file!"
|
|
692
|
+ ORIGIFS="${IFS}"
|
|
693
|
+ IFS=$'\n'
|
|
694
|
+ for cfgline in $(
|
|
695
|
+ beforevars="$(_mktemp)"
|
|
696
|
+ aftervars="$(_mktemp)"
|
|
697
|
+ set > "${beforevars}"
|
|
698
|
+ # shellcheck disable=SC1090
|
|
699
|
+ . "${certconfig}"
|
|
700
|
+ set > "${aftervars}"
|
|
701
|
+ diff -u "${beforevars}" "${aftervars}" | grep -E '^\+[^+]'
|
|
702
|
+ rm "${beforevars}"
|
|
703
|
+ rm "${aftervars}"
|
|
704
|
+ ); do
|
|
705
|
+ config_var="$(echo "${cfgline:1}" | cut -d'=' -f1)"
|
|
706
|
+ config_value="$(echo "${cfgline:1}" | cut -d'=' -f2-)"
|
|
707
|
+ case "${config_var}" in
|
|
708
|
+ KEY_ALGO|OCSP_MUST_STAPLE|PRIVATE_KEY_RENEW|KEYSIZE|CHALLENGETYPE|HOOK|WELLKNOWN|HOOK_CHAIN|OPENSSL_CNF|RENEW_DAYS)
|
|
709
|
+ echo " + ${config_var} = ${config_value}"
|
|
710
|
+ declare -- "${config_var}=${config_value}"
|
|
711
|
+ ;;
|
|
712
|
+ _) ;;
|
|
713
|
+ *) echo " ! Setting ${config_var} on a per-certificate base is not (yet) supported"
|
|
714
|
+ esac
|
|
715
|
+ done
|
|
716
|
+ IFS="${ORIGIFS}"
|
|
717
|
+ fi
|
|
718
|
+ verify_config
|
|
719
|
+
|
596
|
720
|
if [[ -e "${cert}" ]]; then
|
597
|
721
|
printf " + Checking domain name(s) of existing cert..."
|
598
|
722
|
|
|
@@ -623,7 +747,7 @@ command_sign_domains() {
|
623
|
747
|
else
|
624
|
748
|
# Certificate-Names unchanged and cert is still valid
|
625
|
749
|
echo "Skipping renew!"
|
626
|
|
- [[ -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"
|
|
750
|
+ [[ -n "${HOOK}" ]] && "${HOOK}" "unchanged_cert" "${domain}" "${CERTDIR}/${domain}/privkey.pem" "${CERTDIR}/${domain}/cert.pem" "${CERTDIR}/${domain}/fullchain.pem" "${CERTDIR}/${domain}/chain.pem"
|
627
|
751
|
continue
|
628
|
752
|
fi
|
629
|
753
|
else
|
|
@@ -632,7 +756,12 @@ command_sign_domains() {
|
632
|
756
|
fi
|
633
|
757
|
|
634
|
758
|
# shellcheck disable=SC2086
|
635
|
|
- sign_domain ${line}
|
|
759
|
+ if [[ "${PARAM_KEEP_GOING:-}" = "yes" ]]; then
|
|
760
|
+ sign_domain ${line} &
|
|
761
|
+ wait $! || true
|
|
762
|
+ else
|
|
763
|
+ sign_domain ${line}
|
|
764
|
+ fi
|
636
|
765
|
done
|
637
|
766
|
|
638
|
767
|
# remove temporary domains.txt file if used
|
|
@@ -655,7 +784,33 @@ command_sign_csr() {
|
655
|
784
|
_exiterr "Could not read certificate signing request ${csrfile}"
|
656
|
785
|
fi
|
657
|
786
|
|
658
|
|
- sign_csr "$(< "${csrfile}" )"
|
|
787
|
+ # gen cert
|
|
788
|
+ certfile="$(_mktemp)"
|
|
789
|
+ sign_csr "$(< "${csrfile}" )" 3> "${certfile}"
|
|
790
|
+
|
|
791
|
+ # print cert
|
|
792
|
+ echo "# CERT #" >&3
|
|
793
|
+ cat "${certfile}" >&3
|
|
794
|
+ echo >&3
|
|
795
|
+
|
|
796
|
+ # print chain
|
|
797
|
+ if [ -n "${PARAM_FULL_CHAIN:-}" ]; then
|
|
798
|
+ # get and convert ca cert
|
|
799
|
+ chainfile="$(_mktemp)"
|
|
800
|
+ http_request get "$(openssl x509 -in "${certfile}" -noout -text | grep 'CA Issuers - URI:' | cut -d':' -f2-)" > "${chainfile}"
|
|
801
|
+
|
|
802
|
+ if ! grep -q "BEGIN CERTIFICATE" "${chainfile}"; then
|
|
803
|
+ openssl x509 -inform DER -in "${chainfile}" -outform PEM -out "${chainfile}"
|
|
804
|
+ fi
|
|
805
|
+
|
|
806
|
+ echo "# CHAIN #" >&3
|
|
807
|
+ cat "${chainfile}" >&3
|
|
808
|
+
|
|
809
|
+ rm "${chainfile}"
|
|
810
|
+ fi
|
|
811
|
+
|
|
812
|
+ # cleanup
|
|
813
|
+ rm "${certfile}"
|
659
|
814
|
|
660
|
815
|
exit 0
|
661
|
816
|
}
|
|
@@ -702,7 +857,7 @@ command_cleanup() {
|
702
|
857
|
fi
|
703
|
858
|
|
704
|
859
|
# Loop over all certificate directories
|
705
|
|
- for certdir in "${BASEDIR}/certs/"*; do
|
|
860
|
+ for certdir in "${CERTDIR}/"*; do
|
706
|
861
|
# Skip if entry is not a folder
|
707
|
862
|
[[ -d "${certdir}" ]] || continue
|
708
|
863
|
|
|
@@ -771,7 +926,7 @@ command_help() {
|
771
|
926
|
command_env() {
|
772
|
927
|
echo "# letsencrypt.sh configuration"
|
773
|
928
|
load_config
|
774
|
|
- 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
|
|
929
|
+ 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
|
775
|
930
|
}
|
776
|
931
|
|
777
|
932
|
# Main method (parses script arguments and calls command_* methods)
|
|
@@ -828,6 +983,24 @@ main() {
|
828
|
983
|
set_command cleanup
|
829
|
984
|
;;
|
830
|
985
|
|
|
986
|
+ # PARAM_Usage: --full-chain (-fc)
|
|
987
|
+ # PARAM_Description: Print full chain when using --signcsr
|
|
988
|
+ --full-chain|-fc)
|
|
989
|
+ PARAM_FULL_CHAIN="1"
|
|
990
|
+ ;;
|
|
991
|
+
|
|
992
|
+ # PARAM_Usage: --ipv4 (-4)
|
|
993
|
+ # PARAM_Description: Resolve names to IPv4 addresses only
|
|
994
|
+ --ipv4|-4)
|
|
995
|
+ PARAM_IP_VERSION="4"
|
|
996
|
+ ;;
|
|
997
|
+
|
|
998
|
+ # PARAM_Usage: --ipv6 (-6)
|
|
999
|
+ # PARAM_Description: Resolve names to IPv6 addresses only
|
|
1000
|
+ --ipv6|-6)
|
|
1001
|
+ PARAM_IP_VERSION="6"
|
|
1002
|
+ ;;
|
|
1003
|
+
|
831
|
1004
|
# PARAM_Usage: --domain (-d) domain.tld
|
832
|
1005
|
# PARAM_Description: Use specified domain name(s) instead of domains.txt entry (one certificate!)
|
833
|
1006
|
--domain|-d)
|
|
@@ -840,6 +1013,11 @@ main() {
|
840
|
1013
|
fi
|
841
|
1014
|
;;
|
842
|
1015
|
|
|
1016
|
+ # PARAM_Usage: --keep-going (-g)
|
|
1017
|
+ # PARAM_Description: Keep going after encountering an error while creating/renewing multiple certificates in cron mode
|
|
1018
|
+ --keep-going|-g)
|
|
1019
|
+ PARAM_KEEP_GOING="yes"
|
|
1020
|
+ ;;
|
843
|
1021
|
|
844
|
1022
|
# PARAM_Usage: --force (-x)
|
845
|
1023
|
# PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS
|
|
@@ -847,6 +1025,18 @@ main() {
|
847
|
1025
|
PARAM_FORCE="yes"
|
848
|
1026
|
;;
|
849
|
1027
|
|
|
1028
|
+ # PARAM_Usage: --no-lock (-n)
|
|
1029
|
+ # PARAM_Description: Don't use lockfile (potentially dangerous!)
|
|
1030
|
+ --no-lock|-n)
|
|
1031
|
+ PARAM_NO_LOCK="yes"
|
|
1032
|
+ ;;
|
|
1033
|
+
|
|
1034
|
+ # PARAM_Usage: --ocsp
|
|
1035
|
+ # PARAM_Description: Sets option in CSR indicating OCSP stapling to be mandatory
|
|
1036
|
+ --ocsp)
|
|
1037
|
+ PARAM_OCSP_MUST_STAPLE="yes"
|
|
1038
|
+ ;;
|
|
1039
|
+
|
850
|
1040
|
# PARAM_Usage: --privkey (-p) path/to/key.pem
|
851
|
1041
|
# PARAM_Description: Use specified private key instead of account key (useful for revocation)
|
852
|
1042
|
--privkey|-p)
|
|
@@ -855,7 +1045,7 @@ main() {
|
855
|
1045
|
PARAM_ACCOUNT_KEY="${1}"
|
856
|
1046
|
;;
|
857
|
1047
|
|
858
|
|
- # PARAM_Usage: --config (-f) path/to/config.sh
|
|
1048
|
+ # PARAM_Usage: --config (-f) path/to/config
|
859
|
1049
|
# PARAM_Description: Use specified config file
|
860
|
1050
|
--config|-f)
|
861
|
1051
|
shift 1
|
|
@@ -871,6 +1061,14 @@ main() {
|
871
|
1061
|
PARAM_HOOK="${1}"
|
872
|
1062
|
;;
|
873
|
1063
|
|
|
1064
|
+ # PARAM_Usage: --out (-o) certs/directory
|
|
1065
|
+ # PARAM_Description: Output certificates into the specified directory
|
|
1066
|
+ --out|-o)
|
|
1067
|
+ shift 1
|
|
1068
|
+ check_parameters "${1:-}"
|
|
1069
|
+ PARAM_CERTDIR="${1}"
|
|
1070
|
+ ;;
|
|
1071
|
+
|
874
|
1072
|
# PARAM_Usage: --challenge (-t) http-01|dns-01
|
875
|
1073
|
# PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
|
876
|
1074
|
--challenge|-t)
|