Chris Smith 8 years ago
commit
448fba7433
6 changed files with 1057 additions and 0 deletions
  1. 14
    0
      Dockerfile
  2. 23
    0
      LICENCE
  3. 75
    0
      README.md
  4. 11
    0
      config.sh
  5. 918
    0
      letsencrypt.sh
  6. 16
    0
      run.sh

+ 14
- 0
Dockerfile View File

@@ -0,0 +1,14 @@
1
+FROM ubuntu:xenial 
2
+MAINTAINER Chris Smith <chris87@gmail.com> 
3
+
4
+RUN apt-get update \
5
+ && apt-get install -y inotify-tools
6
+
7
+COPY letsencrypt.sh run.sh config.sh /
8
+RUN chmod +x /run.sh /letsencrypt.sh
9
+
10
+VOLUME ["/letsencrypt", "/dns"]
11
+
12
+ENTRYPOINT ["/bin/bash"]
13
+CMD ["/run.sh"]
14
+

+ 23
- 0
LICENCE View File

@@ -0,0 +1,23 @@
1
+The MIT License (MIT)
2
+
3
+letsencrypt.sh copyright (c) 2015 Lukas Schauer
4
+all other content copyright (c) 2016 Chris Smith
5
+
6
+
7
+Permission is hereby granted, free of charge, to any person obtaining a copy
8
+of this software and associated documentation files (the "Software"), to deal
9
+in the Software without restriction, including without limitation the rights
10
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+copies of the Software, and to permit persons to whom the Software is
12
+furnished to do so, subject to the following conditions:
13
+
14
+The above copyright notice and this permission notice shall be included in all
15
+copies or substantial portions of the Software.
16
+
17
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+SOFTWARE.

+ 75
- 0
README.md View File

@@ -0,0 +1,75 @@
1
+# Let's Encrypt Generic DNS Service
2
+
3
+This container uses [letsencrypt.sh](https://github.com/lukas2511/letsencrypt.sh)
4
+to automatically obtain SSL certs from [Let's Encrypt](https://letsencrypt.org/).
5
+
6
+You need to provide a hook that will perform DNS updates on your domains.
7
+If you use a cloud DNS provider, you're probably better off using
8
+[letsencrypt-lexicon](https://github.com/csmith/letsencrypt-lexicon).
9
+
10
+Multiple domains, as well as SANs, are supported. Certificates will be
11
+renewed automatically, and obtained automatically as soon as new domains
12
+are added.
13
+
14
+## Usage
15
+
16
+### Defining domains
17
+
18
+The container defines one volume at `/letsencrypt`, and expects there to be
19
+a list of domains in `/letsencrypt/domains.txt`. Certificates are output to
20
+`/letsencrypt/certs/{domain}`.
21
+
22
+domains.txt should contain one line per certificate. If you want alternate
23
+names on the cert, these should be listed after the primary domain. e.g.
24
+
25
+```
26
+example.com www.example.com
27
+admin.example.com
28
+```
29
+
30
+This will request two certificates: one for example.com with a SAN of
31
+www.example.com, and a separate one for admin.example.com.
32
+
33
+The container uses inotify to monitor the domains.txt file for changes,
34
+so you can update it while the container is running and changes will be
35
+automatically applied.
36
+
37
+### Hook 
38
+
39
+To verify that you own the domain, a TXT record needs to be automatically
40
+created for it. For this container, you must provide a hook that can do
41
+the update. See the documentation for
42
+[letsencrypt.sh](https://github.com/lukas2511/letsencrypt.sh) for details
43
+on the arguments it takes.
44
+
45
+The container expects an executable at `/dns/hook`, and exposes the 
46
+`/dns` volume for you to mount.
47
+
48
+### Other configuration
49
+
50
+For testing purposes, you can set the `STAGING` environment variable to
51
+a non-empty value. This will use the Let's Encrypt staging server, which
52
+has much more relaxed limits.
53
+
54
+You should pass in a contact e-mail address by setting the `EMAIL` env var.
55
+This is passed on to Let's Encrypt, and may be used for important service
56
+announcements.
57
+
58
+### Running
59
+
60
+Here's a full worked example:
61
+
62
+```bash
63
+# The directory we'll use to store the domain list and certificates.
64
+# You could use a docker volume instead.
65
+mkdir /tmp/letsencrypt
66
+echo "domain.com www.domain.com" > /tmp/letsencrypt/domains.txt
67
+
68
+docker run -d --restart=always \
69
+  -e "EMAIL=admin@domain.com" \
70
+  -e "STAGING=true" \
71
+  -v /tmp/letsencrypt:/letsencrypt \
72
+  -v /home/user/my-dns-script:/dns \
73
+  csmith/letsencrypt-generic:latest
74
+```
75
+

+ 11
- 0
config.sh View File

@@ -0,0 +1,11 @@
1
+#!/usr/bin/env bash
2
+
3
+BASEDIR=/letsencrypt
4
+CONTACT_EMAIL=$EMAIL
5
+
6
+if [ -z ${STAGING+-} ]; then
7
+  CA="https://acme-v01.api.letsencrypt.org/directory"
8
+else
9
+  CA="https://acme-staging.api.letsencrypt.org/directory"
10
+fi
11
+

+ 918
- 0
letsencrypt.sh View File

@@ -0,0 +1,918 @@
1
+#!/usr/bin/env bash
2
+
3
+# letsencrypt.sh by lukas2511
4
+# Source: https://github.com/lukas2511/letsencrypt.sh
5
+#
6
+# This script is licensed under The MIT License (see LICENSE for more information).
7
+
8
+set -e
9
+set -u
10
+set -o pipefail
11
+[[ -n "${ZSH_VERSION:-}" ]] && set -o SH_WORD_SPLIT && set +o FUNCTION_ARGZERO
12
+umask 077 # paranoid umask, we're creating private keys
13
+
14
+# Find directory in which this script is stored by traversing all symbolic links
15
+SOURCE="${0}"
16
+while [ -h "$SOURCE" ]; do # resolve $SOURCE until the file is no longer a symlink
17
+  DIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
18
+  SOURCE="$(readlink "$SOURCE")"
19
+  [[ $SOURCE != /* ]] && SOURCE="$DIR/$SOURCE" # if $SOURCE was a relative symlink, we need to resolve it relative to the path where the symlink file was located
20
+done
21
+SCRIPTDIR="$( cd -P "$( dirname "$SOURCE" )" && pwd )"
22
+
23
+BASEDIR="${SCRIPTDIR}"
24
+
25
+# Create (identifiable) temporary files
26
+_mktemp() {
27
+  # shellcheck disable=SC2068
28
+  mktemp ${@:-} "${TMPDIR:-/tmp}/letsencrypt.sh-XXXXXX"
29
+}
30
+
31
+# Check for script dependencies
32
+check_dependencies() {
33
+  # just execute some dummy and/or version commands to see if required tools exist and are actually usable
34
+  openssl version > /dev/null 2>&1 || _exiterr "This script requires an openssl binary."
35
+  _sed "" < /dev/null > /dev/null 2>&1 || _exiterr "This script requires sed with support for extended (modern) regular expressions."
36
+  command -v grep > /dev/null 2>&1 || _exiterr "This script requires grep."
37
+  _mktemp -u > /dev/null 2>&1 || _exiterr "This script requires mktemp."
38
+
39
+  # curl returns with an error code in some ancient versions so we have to catch that
40
+  set +e
41
+  curl -V > /dev/null 2>&1
42
+  retcode="$?"
43
+  set -e
44
+  if [[ ! "${retcode}" = "0" ]] && [[ ! "${retcode}" = "2" ]]; then
45
+    _exiterr "This script requires curl."
46
+  fi
47
+}
48
+
49
+# Setup default config values, search for and load configuration files
50
+load_config() {
51
+  # Check for config in various locations
52
+  if [[ -z "${CONFIG:-}" ]]; then
53
+    for check_config in "/etc/letsencrypt.sh" "/usr/local/etc/letsencrypt.sh" "${PWD}" "${SCRIPTDIR}"; do
54
+      if [[ -e "${check_config}/config.sh" ]]; then
55
+        BASEDIR="${check_config}"
56
+        CONFIG="${check_config}/config.sh"
57
+        break
58
+      fi
59
+    done
60
+  fi
61
+
62
+  # Default values
63
+  CA="https://acme-v01.api.letsencrypt.org/directory"
64
+  LICENSE="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf"
65
+  CHALLENGETYPE="http-01"
66
+  CONFIG_D=
67
+  HOOK=
68
+  HOOK_CHAIN="no"
69
+  RENEW_DAYS="30"
70
+  ACCOUNT_KEY=
71
+  ACCOUNT_KEY_JSON=
72
+  KEYSIZE="4096"
73
+  WELLKNOWN=
74
+  PRIVATE_KEY_RENEW="yes"
75
+  KEY_ALGO=rsa
76
+  OPENSSL_CNF="$(openssl version -d | cut -d\" -f2)/openssl.cnf"
77
+  CONTACT_EMAIL=
78
+  LOCKFILE=
79
+
80
+  if [[ -z "${CONFIG:-}" ]]; then
81
+    echo "#" >&2
82
+    echo "# !! WARNING !! No main config file found, using default config!" >&2
83
+    echo "#" >&2
84
+  elif [[ -e "${CONFIG}" ]]; then
85
+    echo "# INFO: Using main config file ${CONFIG}"
86
+    BASEDIR="$(dirname "${CONFIG}")"
87
+    # shellcheck disable=SC1090
88
+    . "${CONFIG}"
89
+  else
90
+    _exiterr "Specified config file doesn't exist."
91
+  fi
92
+
93
+  if [[ -n "${CONFIG_D}" ]]; then
94
+    if [[ ! -d "${CONFIG_D}" ]]; then
95
+      _exiterr "The path ${CONFIG_D} specified for CONFIG_D does not point to a directory." >&2
96
+    fi
97
+
98
+    for check_config_d in "${CONFIG_D}"/*.sh; do
99
+      if [[ ! -e "${check_config_d}" ]]; then
100
+        echo "# !! WARNING !! Extra configuration directory ${CONFIG_D} exists, but no configuration found in it." >&2
101
+        break
102
+      elif [[ -f "${check_config_d}" ]] && [[ -r "${check_config_d}" ]]; then
103
+        echo "# INFO: Using additional config file ${check_config_d}"
104
+        # shellcheck disable=SC1090
105
+        . "${check_config_d}"
106
+      else
107
+        _exiterr "Specified additional config ${check_config_d} is not readable or not a file at all." >&2
108
+      fi
109
+   done
110
+  fi
111
+
112
+  # Remove slash from end of BASEDIR. Mostly for cleaner outputs, doesn't change functionality.
113
+  BASEDIR="${BASEDIR%%/}"
114
+
115
+  # Check BASEDIR and set default variables
116
+  [[ -d "${BASEDIR}" ]] || _exiterr "BASEDIR does not exist: ${BASEDIR}"
117
+
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"
121
+  [[ -z "${LOCKFILE}" ]] && LOCKFILE="${BASEDIR}/lock"
122
+
123
+  [[ -n "${PARAM_HOOK:-}" ]] && HOOK="${PARAM_HOOK}"
124
+  [[ -n "${PARAM_CHALLENGETYPE:-}" ]] && CHALLENGETYPE="${PARAM_CHALLENGETYPE}"
125
+  [[ -n "${PARAM_KEY_ALGO:-}" ]] && KEY_ALGO="${PARAM_KEY_ALGO}"
126
+
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."
132
+}
133
+
134
+# Initialize system
135
+init_system() {
136
+  load_config
137
+
138
+  # 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
144
+
145
+  # Get CA URLs
146
+  CA_DIRECTORY="$(http_request get "${CA}")"
147
+  CA_NEW_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-cert)" &&
148
+  CA_NEW_AUTHZ="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-authz)" &&
149
+  CA_NEW_REG="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value new-reg)" &&
150
+  # shellcheck disable=SC2015
151
+  CA_REVOKE_CERT="$(printf "%s" "${CA_DIRECTORY}" | get_json_string_value revoke-cert)" ||
152
+  _exiterr "Problem retrieving ACME/CA-URLs, check if your configured CA points to the directory entrypoint."
153
+
154
+  # Export some environment variables to be used in hook script
155
+  export WELLKNOWN BASEDIR CONFIG
156
+
157
+  # Checking for private key ...
158
+  register_new_key="no"
159
+  if [[ -n "${PARAM_ACCOUNT_KEY:-}" ]]; then
160
+    # a private key was specified from the command line so use it for this run
161
+    echo "Using private key ${PARAM_ACCOUNT_KEY} instead of account key"
162
+    ACCOUNT_KEY="${PARAM_ACCOUNT_KEY}"
163
+    ACCOUNT_KEY_JSON="${PARAM_ACCOUNT_KEY}.json"
164
+  else
165
+    # Check if private account key exists, if it doesn't exist yet generate a new one (rsa key)
166
+    if [[ ! -e "${ACCOUNT_KEY}" ]]; then
167
+      echo "+ Generating account key..."
168
+      _openssl genrsa -out "${ACCOUNT_KEY}" "${KEYSIZE}"
169
+      register_new_key="yes"
170
+    fi
171
+  fi
172
+  openssl rsa -in "${ACCOUNT_KEY}" -check 2>/dev/null > /dev/null || _exiterr "Account key is not valid, can not continue."
173
+
174
+  # Get public components from private key and calculate thumbprint
175
+  pubExponent64="$(printf '%x' "$(openssl rsa -in "${ACCOUNT_KEY}" -noout -text | awk '/publicExponent/ {print $2}')" | hex2bin | urlbase64)"
176
+  pubMod64="$(openssl rsa -in "${ACCOUNT_KEY}" -noout -modulus | cut -d'=' -f2 | hex2bin | urlbase64)"
177
+
178
+  thumbprint="$(printf '{"e":"%s","kty":"RSA","n":"%s"}' "${pubExponent64}" "${pubMod64}" | openssl dgst -sha256 -binary | urlbase64)"
179
+
180
+  # If we generated a new private key in the step above we have to register it with the acme-server
181
+  if [[ "${register_new_key}" = "yes" ]]; then
182
+    echo "+ Registering account key with letsencrypt..."
183
+    [[ ! -z "${CA_NEW_REG}" ]] || _exiterr "Certificate authority doesn't allow registrations."
184
+    # If an email for the contact has been provided then adding it to the registration request
185
+    if [[ -n "${CONTACT_EMAIL}" ]]; then
186
+      signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "contact":["mailto:'"${CONTACT_EMAIL}"'"], "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}"
187
+    else
188
+      signed_request "${CA_NEW_REG}" '{"resource": "new-reg", "agreement": "'"$LICENSE"'"}' > "${ACCOUNT_KEY_JSON}"
189
+    fi
190
+  fi
191
+
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
+}
196
+
197
+# Different sed version for different os types...
198
+_sed() {
199
+  if [[ "${OSTYPE}" = "Linux" ]]; then
200
+    sed -r "${@}"
201
+  else
202
+    sed -E "${@}"
203
+  fi
204
+}
205
+
206
+# Print error message and exit with error
207
+_exiterr() {
208
+  echo "ERROR: ${1}" >&2
209
+  exit 1
210
+}
211
+
212
+# Remove newlines and whitespace from json
213
+clean_json() {
214
+  tr -d '\r\n' | _sed -e 's/ +/ /g' -e 's/\{ /{/g' -e 's/ \}/}/g' -e 's/\[ /[/g' -e 's/ \]/]/g'
215
+}
216
+
217
+# Encode data as url-safe formatted base64
218
+urlbase64() {
219
+  # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_'
220
+  openssl base64 -e | tr -d '\n\r' | _sed -e 's:=*$::g' -e 'y:+/:-_:'
221
+}
222
+
223
+# Convert hex string to binary data
224
+hex2bin() {
225
+  # Remove spaces, add leading zero, escape as hex string and parse with printf
226
+  printf -- "$(cat | _sed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
227
+}
228
+
229
+# Get string value from json dictionary
230
+get_json_string_value() {
231
+  local filter
232
+  filter=$(printf 's/.*"%s": *"\([^"]*\)".*/\\1/p' "$1")
233
+  sed -n "${filter}"
234
+}
235
+
236
+# OpenSSL writes to stderr/stdout even when there are no errors. So just
237
+# display the output if the exit code was != 0 to simplify debugging.
238
+_openssl() {
239
+  set +e
240
+  out="$(openssl "${@}" 2>&1)"
241
+  res=$?
242
+  set -e
243
+  if [[ ${res} -ne 0 ]]; then
244
+    echo "  + ERROR: failed to run $* (Exitcode: ${res})" >&2
245
+    echo >&2
246
+    echo "Details:" >&2
247
+    echo "${out}" >&2
248
+    echo >&2
249
+    exit ${res}
250
+  fi
251
+}
252
+
253
+# Send http(s) request with specified method
254
+http_request() {
255
+  tempcont="$(_mktemp)"
256
+
257
+  set +e
258
+  if [[ "${1}" = "head" ]]; then
259
+    statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -I)"
260
+    curlret="${?}"
261
+  elif [[ "${1}" = "get" ]]; then
262
+    statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}")"
263
+    curlret="${?}"
264
+  elif [[ "${1}" = "post" ]]; then
265
+    statuscode="$(curl -s -w "%{http_code}" -o "${tempcont}" "${2}" -d "${3}")"
266
+    curlret="${?}"
267
+  else
268
+    set -e
269
+    _exiterr "Unknown request method: ${1}"
270
+  fi
271
+  set -e
272
+
273
+  if [[ ! "${curlret}" = "0" ]]; then
274
+    _exiterr "Problem connecting to server (curl returned with ${curlret})"
275
+  fi
276
+
277
+  if [[ ! "${statuscode:0:1}" = "2" ]]; then
278
+    echo "  + ERROR: An error occurred while sending ${1}-request to ${2} (Status ${statuscode})" >&2
279
+    echo >&2
280
+    echo "Details:" >&2
281
+    cat "${tempcont}" >&2
282
+    rm -f "${tempcont}"
283
+
284
+    # Wait for hook script to clean the challenge if used
285
+    if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token:+set}" ]]; then
286
+      "${HOOK}" "clean_challenge" '' "${challenge_token}" "${keyauth}"
287
+    fi
288
+
289
+    # remove temporary domains.txt file if used
290
+    [[ -n "${PARAM_DOMAIN:-}" && -n "${DOMAINS_TXT:-}" ]] && rm "${DOMAINS_TXT}"
291
+    exit 1
292
+  fi
293
+
294
+  cat "${tempcont}"
295
+  rm -f "${tempcont}"
296
+}
297
+
298
+# Send signed request
299
+signed_request() {
300
+  # Encode payload as urlbase64
301
+  payload64="$(printf '%s' "${2}" | urlbase64)"
302
+
303
+  # Retrieve nonce from acme-server
304
+  nonce="$(http_request head "${CA}" | grep Replay-Nonce: | awk -F ': ' '{print $2}' | tr -d '\n\r')"
305
+
306
+  # Build header with just our public key and algorithm information
307
+  header='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}}'
308
+
309
+  # Build another header which also contains the previously received nonce and encode it as urlbase64
310
+  protected='{"alg": "RS256", "jwk": {"e": "'"${pubExponent64}"'", "kty": "RSA", "n": "'"${pubMod64}"'"}, "nonce": "'"${nonce}"'"}'
311
+  protected64="$(printf '%s' "${protected}" | urlbase64)"
312
+
313
+  # Sign header with nonce and our payload with our private key and encode signature as urlbase64
314
+  signed64="$(printf '%s' "${protected64}.${payload64}" | openssl dgst -sha256 -sign "${ACCOUNT_KEY}" | urlbase64)"
315
+
316
+  # Send header + extended header + payload + signature to the acme-server
317
+  data='{"header": '"${header}"', "protected": "'"${protected64}"'", "payload": "'"${payload64}"'", "signature": "'"${signed64}"'"}'
318
+
319
+  http_request post "${1}" "${data}"
320
+}
321
+
322
+# Extracts all subject names from a CSR
323
+# Outputs either the CN, or the SANs, one per line
324
+extract_altnames() {
325
+  csr="${1}" # the CSR itself (not a file)
326
+
327
+  if ! <<<"${csr}" openssl req -verify -noout 2>/dev/null; then
328
+    _exiterr "Certificate signing request isn't valid"
329
+  fi
330
+
331
+  reqtext="$( <<<"${csr}" openssl req -noout -text )"
332
+  if <<<"${reqtext}" grep -q '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$'; then
333
+    # SANs used, extract these
334
+    altnames="$( <<<"${reqtext}" grep -A1 '^[[:space:]]*X509v3 Subject Alternative Name:[[:space:]]*$' | tail -n1 )"
335
+    # split to one per line:
336
+    # shellcheck disable=SC1003
337
+    altnames="$( <<<"${altnames}" _sed -e 's/^[[:space:]]*//; s/, /\'$'\n''/g' )"
338
+    # we can only get DNS: ones signed
339
+    if grep -qv '^DNS:' <<<"${altnames}"; then
340
+      _exiterr "Certificate signing request contains non-DNS Subject Alternative Names"
341
+    fi
342
+    # strip away the DNS: prefix
343
+    altnames="$( <<<"${altnames}" _sed -e 's/^DNS://' )"
344
+    echo "${altnames}"
345
+
346
+  else
347
+    # No SANs, extract CN
348
+    altnames="$( <<<"${reqtext}" grep '^[[:space:]]*Subject:' | _sed -e 's/.* CN=([^ /,]*).*/\1/' )"
349
+    echo "${altnames}"
350
+  fi
351
+}
352
+
353
+# Create certificate for domain(s) and outputs it FD 3
354
+sign_csr() {
355
+  csr="${1}" # the CSR itself (not a file)
356
+
357
+  if { true >&3; } 2>/dev/null; then
358
+    : # fd 3 looks OK
359
+  else
360
+    _exiterr "sign_csr: FD 3 not open"
361
+  fi
362
+
363
+  shift 1 || true
364
+  altnames="${*:-}"
365
+  if [ -z "${altnames}" ]; then
366
+    altnames="$( extract_altnames "${csr}" )"
367
+  fi
368
+
369
+  if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then
370
+    _exiterr "Certificate authority doesn't allow certificate signing"
371
+  fi
372
+
373
+  local idx=0
374
+  if [[ -n "${ZSH_VERSION:-}" ]]; then
375
+    local -A challenge_uris challenge_tokens keyauths deploy_args
376
+  else
377
+    local -a challenge_uris challenge_tokens keyauths deploy_args
378
+  fi
379
+
380
+  # Request challenges
381
+  for altname in ${altnames}; do
382
+    # Ask the acme-server for new challenge token and extract them from the resulting json block
383
+    echo " + Requesting challenge for ${altname}..."
384
+    response="$(signed_request "${CA_NEW_AUTHZ}" '{"resource": "new-authz", "identifier": {"type": "dns", "value": "'"${altname}"'"}}' | clean_json)"
385
+
386
+    challenges="$(printf '%s\n' "${response}" | sed -n 's/.*\("challenges":[^\[]*\[[^]]*]\).*/\1/p')"
387
+    repl=$'\n''{' # fix syntax highlighting in Vim
388
+    challenge="$(printf "%s" "${challenges//\{/${repl}}" | grep \""${CHALLENGETYPE}"\")"
389
+    challenge_token="$(printf '%s' "${challenge}" | get_json_string_value token | _sed 's/[^A-Za-z0-9_\-]/_/g')"
390
+    challenge_uri="$(printf '%s' "${challenge}" | get_json_string_value uri)"
391
+
392
+    if [[ -z "${challenge_token}" ]] || [[ -z "${challenge_uri}" ]]; then
393
+      _exiterr "Can't retrieve challenges (${response})"
394
+    fi
395
+
396
+    # Challenge response consists of the challenge token and the thumbprint of our public certificate
397
+    keyauth="${challenge_token}.${thumbprint}"
398
+
399
+    case "${CHALLENGETYPE}" in
400
+      "http-01")
401
+        # Store challenge response in well-known location and make world-readable (so that a webserver can access it)
402
+        printf '%s' "${keyauth}" > "${WELLKNOWN}/${challenge_token}"
403
+        chmod a+r "${WELLKNOWN}/${challenge_token}"
404
+        keyauth_hook="${keyauth}"
405
+        ;;
406
+      "dns-01")
407
+        # Generate DNS entry content for dns-01 validation
408
+        keyauth_hook="$(printf '%s' "${keyauth}" | openssl dgst -sha256 -binary | urlbase64)"
409
+        ;;
410
+    esac
411
+
412
+    challenge_uris[${idx}]="${challenge_uri}"
413
+    keyauths[${idx}]="${keyauth}"
414
+    challenge_tokens[${idx}]="${challenge_token}"
415
+    # Note: assumes args will never have spaces!
416
+    deploy_args[${idx}]="${altname} ${challenge_token} ${keyauth_hook}"
417
+    idx=$((idx+1))
418
+  done
419
+
420
+  # Wait for hook script to deploy the challenges if used
421
+  # shellcheck disable=SC2068
422
+  [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "deploy_challenge" ${deploy_args[@]}
423
+
424
+  # Respond to challenges
425
+  idx=0
426
+  for altname in ${altnames}; do
427
+    challenge_token="${challenge_tokens[${idx}]}"
428
+    keyauth="${keyauths[${idx}]}"
429
+
430
+    # Wait for hook script to deploy the challenge if used
431
+    # shellcheck disable=SC2086
432
+    [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && "${HOOK}" "deploy_challenge" ${deploy_args[${idx}]}
433
+
434
+    # Ask the acme-server to verify our challenge and wait until it is no longer pending
435
+    echo " + Responding to challenge for ${altname}..."
436
+    result="$(signed_request "${challenge_uris[${idx}]}" '{"resource": "challenge", "keyAuthorization": "'"${keyauth}"'"}' | clean_json)"
437
+
438
+    reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
439
+
440
+    while [[ "${reqstatus}" = "pending" ]]; do
441
+      sleep 1
442
+      result="$(http_request get "${challenge_uris[${idx}]}")"
443
+      reqstatus="$(printf '%s\n' "${result}" | get_json_string_value status)"
444
+    done
445
+
446
+    [[ "${CHALLENGETYPE}" = "http-01" ]] && rm -f "${WELLKNOWN}/${challenge_token}"
447
+
448
+    # Wait for hook script to clean the challenge if used
449
+    if [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" != "yes" ]] && [[ -n "${challenge_token}" ]]; then
450
+      # shellcheck disable=SC2086
451
+      "${HOOK}" "clean_challenge" ${deploy_args[${idx}]}
452
+    fi
453
+    idx=$((idx+1))
454
+
455
+    if [[ "${reqstatus}" = "valid" ]]; then
456
+      echo " + Challenge is valid!"
457
+    else
458
+      break
459
+    fi
460
+  done
461
+
462
+  # Wait for hook script to clean the challenges if used
463
+  # shellcheck disable=SC2068
464
+  [[ -n "${HOOK}" ]] && [[ "${HOOK_CHAIN}" = "yes" ]] && "${HOOK}" "clean_challenge" ${deploy_args[@]}
465
+
466
+  if [[ "${reqstatus}" != "valid" ]]; then
467
+    # Clean up any remaining challenge_tokens if we stopped early
468
+    if [[ "${CHALLENGETYPE}" = "http-01" ]]; then
469
+      while [ ${idx} -lt ${#challenge_tokens[@]} ]; do
470
+        rm -f "${WELLKNOWN}/${challenge_tokens[${idx}]}"
471
+        idx=$((idx+1))
472
+      done
473
+    fi
474
+
475
+    _exiterr "Challenge is invalid! (returned: ${reqstatus}) (result: ${result})"
476
+  fi
477
+
478
+  # Finally request certificate from the acme-server and store it in cert-${timestamp}.pem and link from cert.pem
479
+  echo " + Requesting certificate..."
480
+  csr64="$( <<<"${csr}" openssl req -outform DER | urlbase64)"
481
+  crt64="$(signed_request "${CA_NEW_CERT}" '{"resource": "new-cert", "csr": "'"${csr64}"'"}' | openssl base64 -e)"
482
+  crt="$( printf -- '-----BEGIN CERTIFICATE-----\n%s\n-----END CERTIFICATE-----\n' "${crt64}" )"
483
+
484
+  # Try to load the certificate to detect corruption
485
+  echo " + Checking certificate..."
486
+  _openssl x509 -text <<<"${crt}"
487
+
488
+  echo "${crt}" >&3
489
+
490
+  unset challenge_token
491
+  echo " + Done!"
492
+}
493
+
494
+# Create certificate for domain(s)
495
+sign_domain() {
496
+  domain="${1}"
497
+  altnames="${*}"
498
+  timestamp="$(date +%s)"
499
+
500
+  echo " + Signing domains..."
501
+  if [[ -z "${CA_NEW_AUTHZ}" ]] || [[ -z "${CA_NEW_CERT}" ]]; then
502
+    _exiterr "Certificate authority doesn't allow certificate signing"
503
+  fi
504
+
505
+  # 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}"
509
+  fi
510
+
511
+  privkey="privkey.pem"
512
+  # generate a new private key if we need or want one
513
+  if [[ ! -r "${BASEDIR}/certs/${domain}/privkey.pem" ]] || [[ "${PRIVATE_KEY_RENEW}" = "yes" ]]; then
514
+    echo " + Generating private key..."
515
+    privkey="privkey-${timestamp}.pem"
516
+    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";;
519
+    esac
520
+  fi
521
+
522
+  # Generate signing request config and the actual signing request
523
+  echo " + Generating signing request..."
524
+  SAN=""
525
+  for altname in ${altnames}; do
526
+    SAN+="DNS:${altname}, "
527
+  done
528
+  SAN="${SAN%%, }"
529
+  local tmp_openssl_cnf
530
+  tmp_openssl_cnf="$(_mktemp)"
531
+  cat "${OPENSSL_CNF}" > "${tmp_openssl_cnf}"
532
+  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}"
534
+  rm -f "${tmp_openssl_cnf}"
535
+
536
+  crt_path="${BASEDIR}/certs/${domain}/cert-${timestamp}.pem"
537
+  # shellcheck disable=SC2086
538
+  sign_csr "$(< "${BASEDIR}/certs/${domain}/cert-${timestamp}.csr" )" ${altnames} 3>"${crt_path}"
539
+
540
+  # Create fullchain.pem
541
+  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
546
+  fi
547
+  cat "${BASEDIR}/certs/${domain}/chain-${timestamp}.pem" >> "${BASEDIR}/certs/${domain}/fullchain-${timestamp}.pem"
548
+
549
+  # Update symlinks
550
+  [[ "${privkey}" = "privkey.pem" ]] || ln -sf "privkey-${timestamp}.pem" "${BASEDIR}/certs/${domain}/privkey.pem"
551
+
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"
556
+
557
+  # Wait for hook script to clean the challenge and to deploy cert if used
558
+  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}"
560
+
561
+  unset challenge_token
562
+  echo " + Done!"
563
+}
564
+
565
+# Usage: --cron (-c)
566
+# Description: Sign/renew non-existant/changed/expiring certificates.
567
+command_sign_domains() {
568
+  init_system
569
+
570
+  if [[ -n "${PARAM_DOMAIN:-}" ]]; then
571
+    DOMAINS_TXT="$(_mktemp)"
572
+    printf -- "${PARAM_DOMAIN}" > "${DOMAINS_TXT}"
573
+  elif [[ -e "${BASEDIR}/domains.txt" ]]; then
574
+    DOMAINS_TXT="${BASEDIR}/domains.txt"
575
+  else
576
+    _exiterr "domains.txt not found and --domain not given"
577
+  fi
578
+
579
+  # Generate certificates for all domains found in domains.txt. Check if existing certificate are about to expire
580
+  ORIGIFS="${IFS}"
581
+  IFS=$'\n'
582
+  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
583
+    IFS="${ORIGIFS}"
584
+    domain="$(printf '%s\n' "${line}" | cut -d' ' -f1)"
585
+    morenames="$(printf '%s\n' "${line}" | cut -s -d' ' -f2-)"
586
+    cert="${BASEDIR}/certs/${domain}/cert.pem"
587
+
588
+    force_renew="${PARAM_FORCE:-no}"
589
+
590
+    if [[ -z "${morenames}" ]];then
591
+      echo "Processing ${domain}"
592
+    else
593
+      echo "Processing ${domain} with alternative names: ${morenames}"
594
+    fi
595
+
596
+    if [[ -e "${cert}" ]]; then
597
+      printf " + Checking domain name(s) of existing cert..."
598
+
599
+      certnames="$(openssl x509 -in "${cert}" -text -noout | grep DNS: | _sed 's/DNS://g' | tr -d ' ' | tr ',' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//')"
600
+      givennames="$(echo "${domain}" "${morenames}"| tr ' ' '\n' | sort -u | tr '\n' ' ' | _sed 's/ $//' | _sed 's/^ //')"
601
+
602
+      if [[ "${certnames}" = "${givennames}" ]]; then
603
+        echo " unchanged."
604
+      else
605
+        echo " changed!"
606
+        echo " + Domain name(s) are not matching!"
607
+        echo " + Names in old certificate: ${certnames}"
608
+        echo " + Configured names: ${givennames}"
609
+        echo " + Forcing renew."
610
+        force_renew="yes"
611
+      fi
612
+    fi
613
+
614
+    if [[ -e "${cert}" ]]; then
615
+      echo " + Checking expire date of existing cert..."
616
+      valid="$(openssl x509 -enddate -noout -in "${cert}" | cut -d= -f2- )"
617
+
618
+      printf " + Valid till %s " "${valid}"
619
+      if openssl x509 -checkend $((RENEW_DAYS * 86400)) -noout -in "${cert}"; then
620
+        printf "(Longer than %d days). " "${RENEW_DAYS}"
621
+        if [[ "${force_renew}" = "yes" ]]; then
622
+          echo "Ignoring because renew was forced!"
623
+        else
624
+          # Certificate-Names unchanged and cert is still valid
625
+          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"
627
+          continue
628
+        fi
629
+      else
630
+        echo "(Less than ${RENEW_DAYS} days). Renewing!"
631
+      fi
632
+    fi
633
+
634
+    # shellcheck disable=SC2086
635
+    sign_domain ${line}
636
+  done
637
+
638
+  # remove temporary domains.txt file if used
639
+  [[ -n "${PARAM_DOMAIN:-}" ]] && rm -f "${DOMAINS_TXT}"
640
+
641
+  exit 0
642
+}
643
+
644
+# Usage: --signcsr (-s) path/to/csr.pem
645
+# Description: Sign a given CSR, output CRT on stdout (advanced usage)
646
+command_sign_csr() {
647
+  # redirect stdout to stderr
648
+  # leave stdout over at fd 3 to output the cert
649
+  exec 3>&1 1>&2
650
+
651
+  init_system
652
+
653
+  csrfile="${1}"
654
+  if [ ! -r "${csrfile}" ]; then
655
+    _exiterr "Could not read certificate signing request ${csrfile}"
656
+  fi
657
+
658
+  sign_csr "$(< "${csrfile}" )"
659
+
660
+  exit 0
661
+}
662
+
663
+# Usage: --revoke (-r) path/to/cert.pem
664
+# Description: Revoke specified certificate
665
+command_revoke() {
666
+  init_system
667
+
668
+  [[ -n "${CA_REVOKE_CERT}" ]] || _exiterr "Certificate authority doesn't allow certificate revocation."
669
+
670
+  cert="${1}"
671
+  if [[ -L "${cert}" ]]; then
672
+    # follow symlink and use real certificate name (so we move the real file and not the symlink at the end)
673
+    local link_target
674
+    link_target="$(readlink -n "${cert}")"
675
+    if [[ "${link_target}" =~ ^/ ]]; then
676
+      cert="${link_target}"
677
+    else
678
+      cert="$(dirname "${cert}")/${link_target}"
679
+    fi
680
+  fi
681
+  [[ -f "${cert}" ]] || _exiterr "Could not find certificate ${cert}"
682
+
683
+  echo "Revoking ${cert}"
684
+
685
+  cert64="$(openssl x509 -in "${cert}" -inform PEM -outform DER | urlbase64)"
686
+  response="$(signed_request "${CA_REVOKE_CERT}" '{"resource": "revoke-cert", "certificate": "'"${cert64}"'"}' | clean_json)"
687
+  # if there is a problem with our revoke request _request (via signed_request) will report this and "exit 1" out
688
+  # so if we are here, it is safe to assume the request was successful
689
+  echo " + Done."
690
+  echo " + Renaming certificate to ${cert}-revoked"
691
+  mv -f "${cert}" "${cert}-revoked"
692
+}
693
+
694
+# Usage: --cleanup (-gc)
695
+# Description: Move unused certificate files to archive directory
696
+command_cleanup() {
697
+  load_config
698
+
699
+  # Create global archive directory if not existant
700
+  if [[ ! -e "${BASEDIR}/archive" ]]; then
701
+    mkdir "${BASEDIR}/archive"
702
+  fi
703
+
704
+  # Loop over all certificate directories
705
+  for certdir in "${BASEDIR}/certs/"*; do
706
+    # Skip if entry is not a folder
707
+    [[ -d "${certdir}" ]] || continue
708
+
709
+    # Get certificate name
710
+    certname="$(basename "${certdir}")"
711
+
712
+    # Create certitifaces archive directory if not existant
713
+    archivedir="${BASEDIR}/archive/${certname}"
714
+    if [[ ! -e "${archivedir}" ]]; then
715
+      mkdir "${archivedir}"
716
+    fi
717
+
718
+    # Loop over file-types (certificates, keys, signing-requests, ...)
719
+    for filetype in cert.csr cert.pem chain.pem fullchain.pem privkey.pem; do
720
+      # Skip if symlink is broken
721
+      [[ -r "${certdir}/${filetype}" ]] || continue
722
+
723
+      # Look up current file in use
724
+      current="$(basename "$(readlink "${certdir}/${filetype}")")"
725
+
726
+      # Split filetype into name and extension
727
+      filebase="$(echo "${filetype}" | cut -d. -f1)"
728
+      fileext="$(echo "${filetype}" | cut -d. -f2)"
729
+
730
+      # Loop over all files of this type
731
+      for file in "${certdir}/${filebase}-"*".${fileext}"; do
732
+        # Handle case where no files match the wildcard
733
+        [[ -f "${file}" ]] || break
734
+
735
+        # Check if current file is in use, if unused move to archive directory
736
+        filename="$(basename "${file}")"
737
+        if [[ ! "${filename}" = "${current}" ]]; then
738
+          echo "Moving unused file to archive directory: ${certname}/${filename}"
739
+          mv "${certdir}/${filename}" "${archivedir}/${filename}"
740
+        fi
741
+      done
742
+    done
743
+  done
744
+
745
+  exit 0
746
+}
747
+
748
+# Usage: --help (-h)
749
+# Description: Show help text
750
+command_help() {
751
+  printf "Usage: %s [-h] [command [argument]] [parameter [argument]] [parameter [argument]] ...\n\n" "${0}"
752
+  printf "Default command: help\n\n"
753
+  echo "Commands:"
754
+  grep -e '^[[:space:]]*# Usage:' -e '^[[:space:]]*# Description:' -e '^command_.*()[[:space:]]*{' "${0}" | while read -r usage; read -r description; read -r command; do
755
+    if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]] || [[ ! "${command}" =~ ^command_ ]]; then
756
+      _exiterr "Error generating help text."
757
+    fi
758
+    printf " %-32s %s\n" "${usage##"# Usage: "}" "${description##"# Description: "}"
759
+  done
760
+  printf -- "\nParameters:\n"
761
+  grep -E -e '^[[:space:]]*# PARAM_Usage:' -e '^[[:space:]]*# PARAM_Description:' "${0}" | while read -r usage; read -r description; do
762
+    if [[ ! "${usage}" =~ Usage ]] || [[ ! "${description}" =~ Description ]]; then
763
+      _exiterr "Error generating help text."
764
+    fi
765
+    printf " %-32s %s\n" "${usage##"# PARAM_Usage: "}" "${description##"# PARAM_Description: "}"
766
+  done
767
+}
768
+
769
+# Usage: --env (-e)
770
+# Description: Output configuration variables for use in other scripts
771
+command_env() {
772
+  echo "# letsencrypt.sh configuration"
773
+  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
775
+}
776
+
777
+# Main method (parses script arguments and calls command_* methods)
778
+main() {
779
+  COMMAND=""
780
+  set_command() {
781
+    [[ -z "${COMMAND}" ]] || _exiterr "Only one command can be executed at a time. See help (-h) for more information."
782
+    COMMAND="${1}"
783
+  }
784
+
785
+  check_parameters() {
786
+    if [[ -z "${1:-}" ]]; then
787
+      echo "The specified command requires additional parameters. See help:" >&2
788
+      echo >&2
789
+      command_help >&2
790
+      exit 1
791
+    elif [[ "${1:0:1}" = "-" ]]; then
792
+      _exiterr "Invalid argument: ${1}"
793
+    fi
794
+  }
795
+
796
+  [[ -z "${@}" ]] && eval set -- "--help"
797
+
798
+  while (( ${#} )); do
799
+    case "${1}" in
800
+      --help|-h)
801
+        command_help
802
+        exit 0
803
+        ;;
804
+
805
+      --env|-e)
806
+        set_command env
807
+        ;;
808
+
809
+      --cron|-c)
810
+        set_command sign_domains
811
+        ;;
812
+
813
+      --signcsr|-s)
814
+        shift 1
815
+        set_command sign_csr
816
+        check_parameters "${1:-}"
817
+        PARAM_CSR="${1}"
818
+        ;;
819
+
820
+      --revoke|-r)
821
+        shift 1
822
+        set_command revoke
823
+        check_parameters "${1:-}"
824
+        PARAM_REVOKECERT="${1}"
825
+        ;;
826
+
827
+      --cleanup|-gc)
828
+        set_command cleanup
829
+        ;;
830
+
831
+      # PARAM_Usage: --domain (-d) domain.tld
832
+      # PARAM_Description: Use specified domain name(s) instead of domains.txt entry (one certificate!)
833
+      --domain|-d)
834
+        shift 1
835
+        check_parameters "${1:-}"
836
+        if [[ -z "${PARAM_DOMAIN:-}" ]]; then
837
+          PARAM_DOMAIN="${1}"
838
+        else
839
+          PARAM_DOMAIN="${PARAM_DOMAIN} ${1}"
840
+         fi
841
+        ;;
842
+
843
+
844
+      # PARAM_Usage: --force (-x)
845
+      # PARAM_Description: Force renew of certificate even if it is longer valid than value in RENEW_DAYS
846
+      --force|-x)
847
+        PARAM_FORCE="yes"
848
+        ;;
849
+
850
+      # PARAM_Usage: --privkey (-p) path/to/key.pem
851
+      # PARAM_Description: Use specified private key instead of account key (useful for revocation)
852
+      --privkey|-p)
853
+        shift 1
854
+        check_parameters "${1:-}"
855
+        PARAM_ACCOUNT_KEY="${1}"
856
+        ;;
857
+
858
+      # PARAM_Usage: --config (-f) path/to/config.sh
859
+      # PARAM_Description: Use specified config file
860
+      --config|-f)
861
+        shift 1
862
+        check_parameters "${1:-}"
863
+        CONFIG="${1}"
864
+        ;;
865
+
866
+      # PARAM_Usage: --hook (-k) path/to/hook.sh
867
+      # PARAM_Description: Use specified script for hooks
868
+      --hook|-k)
869
+        shift 1
870
+        check_parameters "${1:-}"
871
+        PARAM_HOOK="${1}"
872
+        ;;
873
+
874
+      # PARAM_Usage: --challenge (-t) http-01|dns-01
875
+      # PARAM_Description: Which challenge should be used? Currently http-01 and dns-01 are supported
876
+      --challenge|-t)
877
+        shift 1
878
+        check_parameters "${1:-}"
879
+        PARAM_CHALLENGETYPE="${1}"
880
+        ;;
881
+
882
+      # PARAM_Usage: --algo (-a) rsa|prime256v1|secp384r1
883
+      # PARAM_Description: Which public key algorithm should be used? Supported: rsa, prime256v1 and secp384r1
884
+      --algo|-a)
885
+        shift 1
886
+        check_parameters "${1:-}"
887
+        PARAM_KEY_ALGO="${1}"
888
+        ;;
889
+
890
+      *)
891
+        echo "Unknown parameter detected: ${1}" >&2
892
+        echo >&2
893
+        command_help >&2
894
+        exit 1
895
+        ;;
896
+    esac
897
+
898
+    shift 1
899
+  done
900
+
901
+  case "${COMMAND}" in
902
+    env) command_env;;
903
+    sign_domains) command_sign_domains;;
904
+    sign_csr) command_sign_csr "${PARAM_CSR}";;
905
+    revoke) command_revoke "${PARAM_REVOKECERT}";;
906
+    cleanup) command_cleanup;;
907
+    *) command_help; exit 1;;
908
+  esac
909
+}
910
+
911
+# Determine OS type
912
+OSTYPE="$(uname)"
913
+
914
+# Check for missing dependencies
915
+check_dependencies
916
+
917
+# Run script
918
+main "${@:-}"

+ 16
- 0
run.sh View File

@@ -0,0 +1,16 @@
1
+#!/usr/bin/env bash
2
+
3
+interrupt() {
4
+  echo
5
+  echo "Caught ^C, exiting."
6
+  exit 1
7
+}
8
+
9
+trap interrupt SIGINT
10
+
11
+while true; do
12
+  /letsencrypt.sh --cron --cleanup --hook /dns/hook --challenge dns-01
13
+  inotifywait --timeout 86400 /letsencrypt/domains.txt
14
+  sleep 60
15
+done
16
+

Loading…
Cancel
Save