]>
Commit | Line | Data |
---|---|---|
2e8454b4 | 1 | #!/usr/bin/env bash |
69f3e78b LS |
2 | set -e |
3 | set -u | |
4 | set -o pipefail | |
81882a64 | 5 | umask 077 # paranoid umask, we're creating private keys |
61f0b7ed | 6 | |
16943702 LS |
7 | # Get the directory in which this script is stored |
8 | SCRIPTDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" | |
0e92aba2 MG |
9 | BASEDIR="${SCRIPTDIR}" |
10 | ||
9f66bfdb LS |
11 | check_dependencies() { |
12 | curl -V > /dev/null 2>&1 || _exiterr "This script requires curl." | |
13 | openssl version > /dev/null 2>&1 || _exiterr "This script requres an openssl binary." | |
14 | sed "" < /dev/null > /dev/null 2>&1 || _exiterr "This script requres sed." | |
15 | grep -V > /dev/null 2>&1 || _exiterr "This script requres grep." | |
16 | } | |
17 | ||
ff116396 LS |
18 | # Setup default config values, search for and load configuration files |
19 | load_config() { | |
20 | # Default values | |
21 | CA="https://acme-v01.api.letsencrypt.org/directory" | |
22 | LICENSE="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf" | |
23 | HOOK= | |
24 | RENEW_DAYS="14" | |
25 | PRIVATE_KEY="${BASEDIR}/private_key.pem" | |
26 | KEYSIZE="4096" | |
27 | WELLKNOWN="${BASEDIR}/.acme-challenges" | |
28 | PRIVATE_KEY_RENEW="no" | |
29 | OPENSSL_CNF="$(openssl version -d | cut -d'"' -f2)/openssl.cnf" | |
30 | CONTACT_EMAIL= | |
1e33cfe5 | 31 | LOCKFILE="${BASEDIR}/lock" |
1e33cfe5 | 32 | |
81882a64 LS |
33 | # Check for config in various locations |
34 | if [[ -z "${CONFIG:-}" ]]; then | |
ff116396 | 35 | for check_config in "/etc/letsencrypt.sh" "/usr/local/etc/letsencrypt.sh" "${PWD}" "${SCRIPTDIR}"; do |
81882a64 LS |
36 | if [[ -e "${check_config}/config.sh" ]]; then |
37 | BASEDIR="${check_config}" | |
38 | CONFIG="${check_config}/config.sh" | |
39 | break | |
40 | fi | |
41 | done | |
454c164b | 42 | fi |
454c164b | 43 | |
81882a64 | 44 | if [[ -z "${CONFIG:-}" ]]; then |
ff116396 LS |
45 | echo "#" >&2 |
46 | echo "# !! WARNING !! No config file found, using default config!" >&2 | |
47 | echo "#" >&2 | |
81882a64 | 48 | elif [[ -e "${CONFIG}" ]]; then |
ff116396 | 49 | echo "# INFO: Using config file ${CONFIG}" |
81882a64 LS |
50 | BASEDIR="$(dirname "${CONFIG}")" |
51 | # shellcheck disable=SC1090 | |
52 | . "${CONFIG}" | |
53 | else | |
ff116396 | 54 | echo "Specified config file doesn't exist." >&2 |
81882a64 LS |
55 | exit 1 |
56 | fi | |
61f0b7ed | 57 | |
81882a64 LS |
58 | # Remove slash from end of BASEDIR. Mostly for cleaner outputs, doesn't change functionality. |
59 | BASEDIR="${BASEDIR%%/}" | |
401f5f75 | 60 | |
1e33cfe5 AB |
61 | # Check BASEDIR and set default variables |
62 | if [[ ! -d "${BASEDIR}" ]]; then | |
ff116396 LS |
63 | echo "BASEDIR does not exist: ${BASEDIR}" >&2 |
64 | exit 1 | |
1e33cfe5 | 65 | fi |
ff116396 LS |
66 | } |
67 | ||
93cd114f | 68 | # Initialize system |
ff116396 LS |
69 | init_system() { |
70 | load_config | |
81882a64 | 71 | |
1e33cfe5 | 72 | # Lockfile handling (prevents concurrent access) |
93cd114f LS |
73 | ( set -C; date > "${LOCKFILE}" ) 2>/dev/null || _exiterr "Lock file '${LOCKFILE}' present, aborting." |
74 | remove_lock() { rm -f "${LOCKFILE}"; } | |
81882a64 LS |
75 | trap 'remove_lock' EXIT |
76 | ||
81882a64 | 77 | # Get CA URLs |
3a9e97f9 | 78 | CA_DIRECTORY="$(http_request get "${CA}")" |
81882a64 LS |
79 | CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" && |
80 | CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" && | |
81 | CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" && | |
10d9f342 | 82 | # shellcheck disable=SC2015 |
81882a64 | 83 | CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revoke-cert)" || |
93cd114f | 84 | _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint." |
81882a64 | 85 | |
93cd114f LS |
86 | # Export some environment variables to be used in hook script |
87 | export WELLKNOWN BASEDIR CONFIG | |
0e92aba2 | 88 | |
93cd114f LS |
89 | # Checking for private key ... |
90 | register_new_key="no" | |
0e92aba2 MG |
91 | if [[ -n "${PARAM_PRIVATE_KEY:-}" ]]; then |
92 | # a private key was specified from the command line so use it for this run | |
10d9f342 | 93 | echo "Using private key ${PARAM_PRIVATE_KEY} instead of account key" |
0e92aba2 | 94 | PRIVATE_KEY="${PARAM_PRIVATE_KEY}" |
0e92aba2 MG |
95 | else |
96 | # Check if private account key exists, if it doesn't exist yet generate a new one (rsa key) | |
0e92aba2 | 97 | if [[ ! -e "${PRIVATE_KEY}" ]]; then |
81882a64 | 98 | echo "+ Generating account key..." |
0e92aba2 | 99 | _openssl genrsa -out "${PRIVATE_KEY}" "${KEYSIZE}" |
93cd114f | 100 | register_new_key="yes" |
81882a64 | 101 | fi |
81882a64 | 102 | fi |
93cd114f | 103 | openssl rsa -in "${PRIVATE_KEY}" -check 2>/dev/null > /dev/null || _exiterr "Private key is not valid, can not continue." |
1ab6a436 | 104 | |
81882a64 | 105 | # Get public components from private key and calculate thumbprint |
f70f3048 LS |
106 | pubExponent64="$(openssl rsa -in "${PRIVATE_KEY}" -noout -text | grep publicExponent | grep -oE "0x[a-f0-9]+" | cut -d'x' -f2 | hex2bin | urlbase64)" |
107 | pubMod64="$(openssl rsa -in "${PRIVATE_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)" | |
81882a64 | 108 | |
07149196 | 109 | thumbprint="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}" | openssl sha -sha256 -binary | urlbase64)" |
81882a64 LS |
110 | |
111 | # If we generated a new private key in the step above we have to register it with the acme-server | |
93cd114f | 112 | if [[ "${register_new_key}" = "yes" ]]; then |
81882a64 | 113 | echo "+ Registering account key with letsencrypt..." |
93cd114f LS |
114 | [[ ! -z "${CA_NEW_REG}" ]] || _exiterr "Certificate authority doesn't allow registrations." |
115 | # If an email for the contact has been provided then adding it to the registration request | |
81882a64 LS |
116 | if [[ -n "${CONTACT_EMAIL}" ]]; then |
117 | signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > /dev/null | |
118 | else | |
119 | signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > /dev/null | |
120 | fi | |
121 | fi | |
181dd0ff | 122 | |
93cd114f | 123 | [[ -d "${WELLKNOWN}" ]] || _exiterr "WELLKNOWN directory doesn't exist, please create ${WELLKNOWN} and set appropriate permissions." |
81882a64 | 124 | } |
c24843c6 | 125 | |
9f66bfdb LS |
126 | # Print error message and exit with error |
127 | _exiterr() { | |
128 | echo "ERROR: ${1}" >&2 | |
129 | exit 1 | |
130 | } | |
131 | ||
c6e60302 LS |
132 | anti_newline() { |
133 | tr -d '\n\r' | |
134 | } | |
181dd0ff | 135 | |
61f0b7ed | 136 | urlbase64() { |
c6e60302 | 137 | # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_' |
81cb6ac7 | 138 | openssl base64 -e | anti_newline | sed 's/=*$//g' | tr '+/' '-_' |
61f0b7ed | 139 | } |
91ce50af | 140 | |
16bef17e | 141 | # Convert hex string to binary data |
9fe313d8 | 142 | hex2bin() { |
16bef17e LS |
143 | # Remove spaces, add leading zero, escape as hex string and parse with printf |
144 | printf -- "$(cat | sed -E -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" | |
9fe313d8 | 145 | } |
61f0b7ed | 146 | |
09729186 | 147 | get_json_string_value() { |
760b6894 | 148 | grep -Eo '"'"${1}"'":[[:space:]]*"[^"]*"' | cut -d'"' -f4 |
09729186 LS |
149 | } |
150 | ||
151 | get_json_array() { | |
152 | grep -Eo '"'"${1}"'":[^\[]*\[[^]]*]' | |
153 | } | |
154 | ||
cc605a22 LS |
155 | # OpenSSL writes to stderr/stdout even when there are no errors. So just |
156 | # display the output if the exit code was != 0 to simplify debugging. | |
157 | _openssl() { | |
158 | set +e | |
159 | out="$(openssl "${@}" 2>&1)" | |
160 | res=$? | |
161 | set -e | |
162 | if [[ $res -ne 0 ]]; then | |
163 | echo " + ERROR: failed to run $* (Exitcode: $res)" >&2 | |
164 | echo >&2 | |
165 | echo "Details:" >&2 | |
166 | echo "$out" >&2 | |
167 | exit $res | |
168 | fi | |
169 | } | |
170 | ||
3a9e97f9 | 171 | http_request() { |
3cb292cb LS |
172 | tempcont="$(mktemp)" |
173 | ||
dd5f36e5 | 174 | if [[ "${1}" = "head" ]]; then |
3cb292cb | 175 | statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)" |
dd5f36e5 | 176 | elif [[ "${1}" = "get" ]]; then |
3cb292cb | 177 | statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}")" |
dd5f36e5 | 178 | elif [[ "${1}" = "post" ]]; then |
3cb292cb | 179 | statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -d "${3}")" |
91ce50af | 180 | fi |
dd5f36e5 | 181 | |
3cb292cb | 182 | if [[ ! "${statuscode:0:1}" = "2" ]]; then |
84fac541 | 183 | echo " + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2 |
3cb292cb LS |
184 | echo >&2 |
185 | echo "Details:" >&2 | |
9e79c066 | 186 | cat "${tempcont}" >&2 |
3cb292cb | 187 | rm -f "${tempcont}" |
c24843c6 | 188 | |
189 | # Wait for hook script to clean the challenge if used | |
0b2119c4 | 190 | if [[ -n "${HOOK}" ]] && [[ -n "${challenge_token:+set}" ]]; then |
e32ea24c | 191 | ${HOOK} "clean_challenge" '' "${challenge_token}" "${keyauth}" |
c24843c6 | 192 | fi |
193 | ||
8f6c2328 MG |
194 | # remove temporary domains.txt file if used |
195 | if [[ -n "${PARAM_DOMAIN:-}" ]]; then | |
196 | rm "${DOMAINS_TXT}" | |
197 | fi | |
198 | ||
dd5f36e5 | 199 | exit 1 |
130ea6ab | 200 | fi |
dd5f36e5 | 201 | |
31111265 | 202 | cat "${tempcont}" |
3cb292cb | 203 | rm -f "${tempcont}" |
91ce50af | 204 | } |
81882a64 | 205 | |
61f0b7ed | 206 | signed_request() { |
c6e60302 | 207 | # Encode payload as urlbase64 |
4aa48d33 | 208 | payload64="$(printf '%s' "${2}" | urlbase64)" |
61f0b7ed | 209 | |
c6e60302 | 210 | # Retrieve nonce from acme-server |
3a9e97f9 | 211 | nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | anti_newline)" |
61f0b7ed | 212 | |
c6e60302 | 213 | # Build header with just our public key and algorithm information |
61f0b7ed LS |
214 | header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}' |
215 | ||
c6e60302 | 216 | # Build another header which also contains the previously received nonce and encode it as urlbase64 |
61f0b7ed | 217 | protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}' |
4aa48d33 | 218 | protected64="$(printf '%s' "${protected}" | urlbase64)" |
61f0b7ed | 219 | |
c6e60302 | 220 | # Sign header with nonce and our payload with our private key and encode signature as urlbase64 |
0e92aba2 | 221 | signed64="$(printf '%s' "${protected64}.${payload64}" | openssl dgst -sha256 -sign "${PRIVATE_KEY}" | urlbase64)" |
61f0b7ed | 222 | |
c6e60302 | 223 | # Send header + extended header + payload + signature to the acme-server |
61f0b7ed LS |
224 | data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}' |
225 | ||
3a9e97f9 | 226 | http_request post "${1}" "${data}" |
61f0b7ed LS |
227 | } |
228 | ||
61f0b7ed LS |
229 | sign_domain() { |
230 | domain="${1}" | |
1f65a335 | 231 | altnames="${*}" |
81882a64 | 232 | |
579e2316 | 233 | echo " + Signing domains..." |
09729186 | 234 | if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then |
31111265 | 235 | echo " + ERROR: Certificate authority doesn't allow certificate signing" >&2 |
09729186 LS |
236 | exit 1 |
237 | fi | |
3cc587c2 LS |
238 | timestamp="$(date +%s)" |
239 | ||
3dbbb461 | 240 | # If there is no existing certificate directory => make it |
5b29db97 AJM |
241 | if [[ ! -e "${BASEDIR}/certs/${domain}" ]]; then |
242 | echo " + make directory ${BASEDIR}/certs/${domain} ..." | |
243 | mkdir -p "${BASEDIR}/certs/${domain}" | |
3dbbb461 MG |
244 | fi |
245 | ||
f343dc11 | 246 | privkey="privkey.pem" |
3dbbb461 | 247 | # generate a new private key if we need or want one |
5b29db97 | 248 | if [[ ! -f "${BASEDIR}/certs/${domain}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then |
579e2316 | 249 | echo " + Generating private key..." |
f343dc11 | 250 | privkey="privkey-${timestamp}.pem" |
7f8ea450 | 251 | _openssl genrsa -out "${BASEDIR}/certs/${domain}/privkey-${timestamp}.pem" "${KEYSIZE}" |
61f0b7ed LS |
252 | fi |
253 | ||
c6e60302 LS |
254 | # Generate signing request config and the actual signing request |
255 | SAN="" | |
61f0b7ed | 256 | for altname in $altnames; do |
c6e60302 LS |
257 | SAN+="DNS:${altname}, " |
258 | done | |
cd13a9c2 | 259 | SAN="${SAN%%, }" |
579e2316 | 260 | echo " + Generating signing request..." |
3bc1cf91 LS |
261 | local tmp_openssl_cnf |
262 | tmp_openssl_cnf="$(mktemp)" | |
263 | cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}" | |
9e79c066 LS |
264 | printf "[SAN]\nsubjectAltName=%s" "${SAN}" >> "${tmp_openssl_cnf}" |
265 | 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}" | |
266 | rm -f "${tmp_openssl_cnf}" | |
c6e60302 LS |
267 | |
268 | # Request and respond to challenges | |
269 | for altname in $altnames; do | |
270 | # Ask the acme-server for new challenge token and extract them from the resulting json block | |
579e2316 | 271 | echo " + Requesting challenge for ${altname}..." |
09729186 | 272 | response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}')" |
61f0b7ed | 273 | |
09729186 | 274 | challenges="$(printf '%s\n' "${response}" | get_json_array challenges)" |
526843d6 SR |
275 | repl=$'\n''{' # fix syntax highlighting in Vim |
276 | challenge="$(printf "%s" "${challenges//\{/${repl}}" | grep 'http-01')" | |
09729186 LS |
277 | challenge_token="$(printf '%s' "${challenge}" | get_json_string_value token | sed 's/[^A-Za-z0-9_\-]/_/g')" |
278 | challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)" | |
61f0b7ed | 279 | |
dd5f36e5 | 280 | if [[ -z "${challenge_token}" ]] || [[ -z "${challenge_uri}" ]]; then |
31111265 | 281 | echo " + Error: Can't retrieve challenges (${response})" >&2 |
abb95693 LS |
282 | exit 1 |
283 | fi | |
284 | ||
c6e60302 | 285 | # Challenge response consists of the challenge token and the thumbprint of our public certificate |
61f0b7ed LS |
286 | keyauth="${challenge_token}.${thumbprint}" |
287 | ||
c6e60302 | 288 | # Store challenge response in well-known location and make world-readable (so that a webserver can access it) |
4aa48d33 | 289 | printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_token}" |
2b5df371 | 290 | chmod a+r "${WELLKNOWN}/${challenge_token}" |
61f0b7ed | 291 | |
b33f1288 | 292 | # Wait for hook script to deploy the challenge if used |
c24843c6 | 293 | if [[ -n "${HOOK}" ]]; then |
e32ea24c | 294 | ${HOOK} "deploy_challenge" "${altname}" "${challenge_token}" "${keyauth}" |
b33f1288 SR |
295 | fi |
296 | ||
c6e60302 | 297 | # Ask the acme-server to verify our challenge and wait until it becomes valid |
579e2316 | 298 | echo " + Responding to challenge for ${altname}..." |
61f0b7ed LS |
299 | result="$(signed_request "${challenge_uri}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}')" |
300 | ||
09729186 | 301 | status="$(printf '%s\n' "${result}" | get_json_string_value status)" |
61f0b7ed | 302 | |
cbe1eb2c | 303 | # get status until a result is reached => not pending anymore |
dd5f36e5 | 304 | while [[ "${status}" = "pending" ]]; do |
c6e60302 | 305 | sleep 1 |
3a9e97f9 | 306 | status="$(http_request get "${challenge_uri}" | get_json_string_value status)" |
61f0b7ed LS |
307 | done |
308 | ||
00837b86 | 309 | rm -f "${WELLKNOWN}/${challenge_token}" |
81882a64 | 310 | |
ab301951 E |
311 | # Wait for hook script to clean the challenge if used |
312 | if [[ -n "${HOOK}" ]] && [[ -n "${challenge_token}" ]]; then | |
313 | ${HOOK} "clean_challenge" "${altname}" "${challenge_token}" "${keyauth}" | |
314 | fi | |
81882a64 | 315 | |
76a37834 | 316 | if [[ "${status}" = "valid" ]]; then |
579e2316 | 317 | echo " + Challenge is valid!" |
76a37834 | 318 | else |
31111265 | 319 | echo " + Challenge is invalid! (returned: ${status})" >&2 |
76a37834 MG |
320 | exit 1 |
321 | fi | |
322 | ||
61f0b7ed LS |
323 | done |
324 | ||
b7439a83 | 325 | # Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem |
579e2316 | 326 | echo " + Requesting certificate..." |
f343dc11 | 327 | csr64="$(openssl req -in "${BASEDIR}/certs/${domain}/cert-${timestamp}.csr" -outform DER | urlbase64)" |
09729186 | 328 | crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | openssl base64 -e)" |
85da9090 SR |
329 | crt_path="${BASEDIR}/certs/${domain}/cert-${timestamp}.pem" |
330 | printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" > "${crt_path}" | |
10cf2299 | 331 | # try to load the certificate to detect corruption |
a4e7c43a | 332 | echo " + Checking certificate..." |
10cf2299 | 333 | _openssl x509 -text < "${crt_path}" |
329acb58 LS |
334 | |
335 | # Create fullchain.pem | |
1eb6f6d2 LS |
336 | echo " + Creating fullchain.pem..." |
337 | cat "${crt_path}" > "${BASEDIR}/certs/${domain}/fullchain-${timestamp}.pem" | |
3a9e97f9 | 338 | 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" |
f090fbcf | 339 | if ! grep "BEGIN CERTIFICATE" "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" > /dev/null 2>&1; then |
a733f789 LS |
340 | openssl x509 -in "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" -inform DER -out "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" -outform PEM |
341 | fi | |
342 | ln -sf "chain-${timestamp}.pem" "${BASEDIR}/certs/${domain}/chain.pem" | |
343 | cat "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" >> "${BASEDIR}/certs/${domain}/fullchain-${timestamp}.pem" | |
1eb6f6d2 | 344 | ln -sf "fullchain-${timestamp}.pem" "${BASEDIR}/certs/${domain}/fullchain.pem" |
329acb58 | 345 | |
f343dc11 LS |
346 | # Update remaining symlinks |
347 | if [ ! "${privkey}" = "privkey.pem" ]; then | |
3f6ff8f7 | 348 | ln -sf "privkey-${timestamp}.pem" "${BASEDIR}/certs/${domain}/privkey.pem" |
f343dc11 LS |
349 | fi |
350 | ||
3f6ff8f7 SR |
351 | ln -sf "cert-${timestamp}.csr" "${BASEDIR}/certs/${domain}/cert.csr" |
352 | ln -sf "cert-${timestamp}.pem" "${BASEDIR}/certs/${domain}/cert.pem" | |
f343dc11 | 353 | |
c24843c6 | 354 | # Wait for hook script to clean the challenge and to deploy cert if used |
355 | if [[ -n "${HOOK}" ]]; then | |
e32ea24c | 356 | ${HOOK} "deploy_cert" "${domain}" "${BASEDIR}/certs/${domain}/privkey.pem" "${BASEDIR}/certs/${domain}/cert.pem" "${BASEDIR}/certs/${domain}/fullchain.pem" |
c24843c6 | 357 | fi |
358 | ||
359 | unset challenge_token | |
579e2316 | 360 | echo " + Done!" |
61f0b7ed LS |
361 | } |
362 | ||
f9126627 | 363 | |
0a859a19 | 364 | # Usage: --cron (-c) |
083c6736 | 365 | # Description: Sign/renew non-existant/changed/expiring certificates. |
8f6c2328 | 366 | command_sign_domains() { |
9f66bfdb LS |
367 | init_system |
368 | ||
8f6c2328 | 369 | if [[ -n "${PARAM_DOMAIN:-}" ]]; then |
8f6c2328 | 370 | DOMAINS_TXT="$(mktemp)" |
93cd114f LS |
371 | printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}" |
372 | elif [[ -e "${BASEDIR}/domains.txt" ]]; then | |
373 | DOMAINS_TXT="${BASEDIR}/domains.txt" | |
374 | else | |
375 | _exiterr "domains.txt not found and --domain not given" | |
8f6c2328 | 376 | fi |
93cd114f | 377 | |
81882a64 | 378 | # Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire |
760b6894 | 379 | <"${DOMAINS_TXT}" sed 's/^[[:space:]]*//g;s/[[:space:]]*$//g' | grep -vE '^(#|$)' | while read -r line; do |
81882a64 | 380 | domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)" |
8f6c2328 | 381 | morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)" |
81882a64 | 382 | cert="${BASEDIR}/certs/${domain}/cert.pem" |
f9126627 | 383 | |
2d097c92 MG |
384 | force_renew="${PARAM_FORCE:-no}" |
385 | ||
8f6c2328 MG |
386 | if [[ -z "${morenames}" ]];then |
387 | echo "Processing ${domain}" | |
388 | else | |
93cd114f | 389 | echo "Processing ${domain} with alternative names: ${morenames}" |
8f6c2328 MG |
390 | fi |
391 | ||
81882a64 | 392 | if [[ -e "${cert}" ]]; then |
93cd114f | 393 | printf " + Checking domain name(s) of existing cert..." |
2d097c92 MG |
394 | |
395 | certnames="$(openssl x509 -in "${cert}" -text -noout | grep DNS: | sed 's/DNS://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//')" | |
396 | givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | sed 's/ $//' | sed 's/^ //')" | |
397 | ||
398 | if [[ "${certnames}" = "${givennames}" ]]; then | |
399 | echo " unchanged." | |
400 | else | |
401 | echo " changed!" | |
402 | echo " + Domain name(s) are not matching!" | |
403 | echo " + Names in old certificate: ${certnames}" | |
404 | echo " + Configured names: ${givennames}" | |
405 | echo " + Forcing renew." | |
406 | force_renew="yes" | |
407 | fi | |
408 | fi | |
409 | ||
410 | if [[ -e "${cert}" ]]; then | |
411 | echo " + Checking expire date of existing cert..." | |
81882a64 | 412 | valid="$(openssl x509 -enddate -noout -in "${cert}" | cut -d= -f2- )" |
8221727a | 413 | |
93cd114f | 414 | printf " + Valid till %s " "${valid}" |
81882a64 | 415 | if openssl x509 -checkend $((RENEW_DAYS * 86400)) -noout -in "${cert}"; then |
93cd114f | 416 | printf "(Longer than %d days). " "${RENEW_DAYS}" |
2d097c92 MG |
417 | if [[ "${force_renew}" = "yes" ]]; then |
418 | echo "Ignoring because renew was forced!" | |
8f6c2328 MG |
419 | else |
420 | echo "Skipping!" | |
421 | continue | |
422 | fi | |
423 | else | |
424 | echo "(Less than ${RENEW_DAYS} days). Renewing!" | |
81882a64 | 425 | fi |
81882a64 | 426 | fi |
8221727a | 427 | |
81882a64 | 428 | # shellcheck disable=SC2086 |
93cd114f | 429 | sign_domain ${line} |
e4666acb | 430 | done || true |
f13eaa7f | 431 | |
8f6c2328 | 432 | # remove temporary domains.txt file if used |
93cd114f LS |
433 | [[ -n "${PARAM_DOMAIN:-}" ]] && rm -f "${DOMAINS_TXT}" |
434 | ||
435 | exit 0 | |
81882a64 | 436 | } |
3390080c | 437 | |
0a859a19 LS |
438 | # Usage: --revoke (-r) path/to/cert.pem |
439 | # Description: Revoke specified certificate | |
81882a64 | 440 | command_revoke() { |
9f66bfdb LS |
441 | init_system |
442 | ||
3dcfa8b4 LS |
443 | [[ -n "${CA_REVOKE_CERT}" ]] || _exiterr "Certificate authority doesn't allow certificate revocation." |
444 | ||
81882a64 | 445 | cert="${1}" |
c7018036 MG |
446 | if [[ -L "${cert}" ]]; then |
447 | # follow symlink and use real certificate name (so we move the real file and not the symlink at the end) | |
3bc1cf91 LS |
448 | local link_target |
449 | link_target="$(readlink -n "${cert}")" | |
450 | if [[ "${link_target}" =~ ^/ ]]; then | |
c7018036 MG |
451 | cert="${link_target}" |
452 | else | |
453 | cert="$(dirname "${cert}")/${link_target}" | |
454 | fi | |
455 | fi | |
3dcfa8b4 LS |
456 | [[ -f "${cert}" ]] || _exiterr "Could not find certificate ${cert}" |
457 | ||
81882a64 | 458 | echo "Revoking ${cert}" |
3dcfa8b4 | 459 | |
81882a64 LS |
460 | cert64="$(openssl x509 -in "${cert}" -inform PEM -outform DER | urlbase64)" |
461 | response="$(signed_request "${CA_REVOKE_CERT}" '{"resource": "revoke-cert", "certificate": "'"${cert64}"'"}')" | |
3dcfa8b4 | 462 | # if there is a problem with our revoke request _request (via signed_request) will report this and "exit 1" out |
81882a64 | 463 | # so if we are here, it is safe to assume the request was successful |
3dcfa8b4 LS |
464 | echo " + Done." |
465 | echo " + Renaming certificate to ${cert}-revoked" | |
81882a64 LS |
466 | mv -f "${cert}" "${cert}-revoked" |
467 | } | |
c24843c6 | 468 | |
0a859a19 LS |
469 | # Usage: --help (-h) |
470 | # Description: Show help text | |
81882a64 | 471 | command_help() { |
7727f5ea LS |
472 | printf "Usage: %s [-h] [command [argument]] [parameter [argument]] [parameter [argument]] ...\n\n" "${0}" |
473 | printf "Default command: help\n\n" | |
0a859a19 | 474 | echo "Commands:" |
760b6894 | 475 | grep -e '^[[:space:]]*# Usage:' -e '^[[:space:]]*# Description:' -e '^command_.*()[[:space:]]*{' "${0}" | while read -r usage; read -r description; read -r command; do |
31111265 | 476 | if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]] || [[ ! "${command}" =~ ^command_ ]]; then |
7727f5ea | 477 | _exiterr "Error generating help text." |
0a859a19 | 478 | fi |
7727f5ea | 479 | printf " %-32s %s\n" "${usage##"# Usage: "}" "${description##"# Description: "}" |
0a859a19 | 480 | done |
7727f5ea | 481 | printf -- "\nParameters:\n" |
760b6894 | 482 | grep -E -e '^[[:space:]]*# PARAM_Usage:' -e '^[[:space:]]*# PARAM_Description:' "${0}" | while read -r usage; read -r description; do |
31111265 | 483 | if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]]; then |
7727f5ea | 484 | _exiterr "Error generating help text." |
0a859a19 | 485 | fi |
7727f5ea | 486 | printf " %-32s %s\n" "${usage##"# PARAM_Usage: "}" "${description##"# PARAM_Description: "}" |
0a859a19 | 487 | done |
81882a64 | 488 | } |
063d28a6 | 489 | |
1ab6a436 LS |
490 | # Usage: --env (-e) |
491 | # Description: Output configuration variables for use in other scripts | |
492 | command_env() { | |
493 | echo "# letsencrypt.sh configuration" | |
9f66bfdb LS |
494 | load_config |
495 | typeset -p CA LICENSE HOOK RENEW_DAYS PRIVATE_KEY KEYSIZE WELLKNOWN PRIVATE_KEY_RENEW OPENSSL_CNF CONTACT_EMAIL LOCKFILE | |
1ab6a436 LS |
496 | } |
497 | ||
9f66bfdb LS |
498 | main() { |
499 | COMMAND="" | |
500 | set_command() { | |
501 | [[ -z "${COMMAND}" ]] || _exiterr "Only one command can be executed at a time. See help (-h) for more information." | |
502 | COMMAND="${1}" | |
503 | } | |
504 | ||
505 | check_parameters() { | |
506 | if [[ -z "${1:-}" ]]; then | |
507 | echo "The specified command requires additional parameters. See help:" >&2 | |
31111265 LS |
508 | echo >&2 |
509 | command_help >&2 | |
81882a64 | 510 | exit 1 |
9f66bfdb LS |
511 | elif [[ "${1:0:1}" = "-" ]]; then |
512 | _exiterr "Invalid argument: ${1}" | |
513 | fi | |
514 | } | |
579e2316 | 515 | |
9f66bfdb LS |
516 | while (( "${#}" )); do |
517 | case "${1}" in | |
518 | --help|-h) | |
519 | command_help | |
520 | exit 0 | |
521 | ;; | |
579e2316 | 522 | |
9f66bfdb LS |
523 | --env|-e) |
524 | set_command env | |
525 | ;; | |
579e2316 | 526 | |
9f66bfdb LS |
527 | --cron|-c) |
528 | set_command sign_domains | |
529 | ;; | |
530 | ||
531 | --revoke|-r) | |
532 | shift 1 | |
533 | set_command revoke | |
534 | check_parameters "${1:-}" | |
535 | PARAM_REVOKECERT="${1}" | |
536 | ;; | |
5060dea0 | 537 | |
8f6c2328 | 538 | # PARAM_Usage: --domain (-d) domain.tld |
9f66bfdb LS |
539 | # PARAM_Description: Use specified domain name(s) instead of domains.txt entry (one certificate!) |
540 | --domain|-d) | |
541 | shift 1 | |
542 | check_parameters "${1:-}" | |
543 | if [[ -z "${PARAM_DOMAIN:-}" ]]; then | |
544 | PARAM_DOMAIN="${1}" | |
545 | else | |
546 | PARAM_DOMAIN="${PARAM_DOMAIN} ${1}" | |
547 | fi | |
548 | ;; | |
549 | ||
550 | ||
8f6c2328 | 551 | # PARAM_Usage: --force (-x) |
9f66bfdb LS |
552 | # PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS |
553 | --force|-x) | |
554 | PARAM_FORCE="yes" | |
555 | ;; | |
556 | ||
0a859a19 LS |
557 | # PARAM_Usage: --privkey (-p) path/to/key.pem |
558 | # PARAM_Description: Use specified private key instead of account key (useful for revocation) | |
9f66bfdb LS |
559 | --privkey|-p) |
560 | shift 1 | |
561 | check_parameters "${1:-}" | |
562 | PARAM_PRIVATE_KEY="${1}" | |
563 | ;; | |
564 | ||
565 | # PARAM_Usage: --config (-f) path/to/config.sh | |
566 | # PARAM_Description: Use specified config file | |
567 | --config|-f) | |
568 | shift 1 | |
569 | check_parameters "${1:-}" | |
570 | CONFIG="${1}" | |
571 | ;; | |
572 | ||
573 | *) | |
574 | echo "Unknown parameter detected: ${1}" >&2 | |
575 | echo >&2 | |
576 | command_help >&2 | |
577 | exit 1 | |
578 | ;; | |
579 | esac | |
580 | ||
581 | shift 1 | |
582 | done | |
583 | ||
584 | case "${COMMAND}" in | |
585 | env) command_env;; | |
586 | sign_domains) command_sign_domains;; | |
587 | revoke) command_revoke "${PARAM_REVOKECERT}";; | |
588 | *) command_help; exit1;; | |
81882a64 | 589 | esac |
9f66bfdb | 590 | } |
81882a64 | 591 | |
9f66bfdb LS |
592 | # Check for missing dependencies |
593 | check_dependencies | |
594 | ||
595 | # Run script | |
596 | main "${@:-}" |