Project

General

Profile

Download (114 KB) Statistics
| Branch: | Revision:
1
#!/usr/bin/env bash
2
# ---------------------------------------------------------------------------
3
# getssl - Obtain SSL certificates from the letsencrypt.org ACME server
4

    
5
# This program is free software: you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation, either version 3 of the License, or
8
# (at your option) any later version.
9

    
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
# GNU General Public License at <http://www.gnu.org/licenses/> for
14
# more details.
15

    
16
# For usage, run "getssl -h" or see https://github.com/srvrco/getssl
17

    
18
# ACMEv2 process is documented at https://tools.ietf.org/html/rfc8555#section-7.4
19

    
20
# Revision history:
21
# 2016-01-08 Created (v0.1)
22
# 2016-01-11 type correction and upload to github (v0.2)
23
# 2016-01-11 added import of any existing cert on -c  option (v0.3)
24
# 2016-01-12 corrected formatting of imported certificate (v0.4)
25
# 2016-01-12 corrected error on removal of token in some instances (v0.5)
26
# 2016-01-18 corrected issue with removing tmp if run as root with the -c option (v0.6)
27
# 2016-01-18 added option to upload a single PEN file ( used by cpanel) (v0.7)
28
# 2016-01-23 added dns challenge option (v0.8)
29
# 2016-01-24 create the ACL directory if it does not exist. (v0.9) - dstosberg
30
# 2016-01-26 correcting a couple of small bugs and allow curl to follow redirects (v0.10)
31
# 2016-01-27 add a very basic openssl.cnf file if it doesn't exist and tidy code slightly (v0.11)
32
# 2016-01-28 Typo corrections, quoted file variables and fix bug on DNS_DEL_COMMAND (v0.12)
33
# 2016-01-28 changed DNS checks to use nslookup and allow hyphen in domain names (v0.13)
34
# 2016-01-29 Fix ssh-reload-command, extra waiting for DNS-challenge,
35
# 2016-01-29 add error_exit and cleanup help message (v0.14)
36
# 2016-01-29 added -a|--all option to renew all configured certificates (v0.15)
37
# 2016-01-29 added option for elliptic curve keys (v0.16)
38
# 2016-01-29 added server-type option to use and check cert validity from website (v0.17)
39
# 2016-01-30 added --quiet option for running in cron (v0.18)
40
# 2016-01-31 removed usage of xxd to make script more compatible across versions (v0.19)
41
# 2016-01-31 removed usage of base64 to make script more compatible across platforms (v0.20)
42
# 2016-01-31 added option to safe a full chain certificate (v0.21)
43
# 2016-02-01 commented code and added option for copying concatenated certs to file (v0.22)
44
# 2016-02-01 re-arrange flow for DNS-challenge, to reduce time taken (v0.23)
45
# 2016-02-04 added options for other server types (ldaps, or any port) and check_remote (v0.24)
46
# 2016-02-04 added short sleep following service restart before checking certs (v0.25)
47
# 2016-02-12 fix challenge token location when directory doesn't exist (v0.26)
48
# 2016-02-17 fix sed -E issue, and reduce length of renew check to 365 days for older systems (v0.27)
49
# 2016-04-05 Ensure DNS cleanup on error exit. (0.28) - pecigonzalo
50
# 2016-04-15 Remove NS Lookup of A record when using dns validation (0.29) - pecigonzalo
51
# 2016-04-17 Improving the wording in a couple of comments and info statements. (0.30)
52
# 2016-05-04 Improve check for if DNS_DEL_COMMAND is blank. (0.31)
53
# 2016-05-06 Setting umask to 077 for security of private keys etc. (0.32)
54
# 2016-05-20 update to reflect changes in staging ACME server json (0.33)
55
# 2016-05-20 tidying up checking of json following ACME changes. (0.34)
56
# 2016-05-21 added AUTH_DNS_SERVER to getssl.cfg as optional definition of authoritative DNS server (0.35)
57
# 2016-05-21 added DNS_WAIT to getssl.cfg as (default = 10 seconds as before) (0.36)
58
# 2016-05-21 added PUBLIC_DNS_SERVER option, for forcing use of an external DNS server (0.37)
59
# 2016-05-28 added FTP method of uploading tokens to remote server (blocked for certs as not secure) (0.38)
60
# 2016-05-28 added FTP method into the default config notes. (0.39)
61
# 2016-05-30 Add sftp with password to copy files (0.40)
62
# 2016-05-30 Add version check to see if there is a more recent version of getssl (0.41)
63
# 2016-05-30 Add [-u|--upgrade] option to automatically upgrade getssl (0.42)
64
# 2016-05-30 Added backup when auto-upgrading (0.43)
65
# 2016-05-30 Improvements to auto-upgrade (0.44)
66
# 2016-05-31 Improved comments - no structural changes
67
# 2016-05-31 After running for nearly 6 months, final testing prior to a 1.00 stable version. (0.90)
68
# 2016-06-01 Reorder functions alphabetically as part of code tidy. (0.91)
69
# 2016-06-03 Version 1.0 of code for release (1.00)
70
# 2016-06-09 bugfix of issue 44, and add success statement (ignoring quiet flag) (1.01)
71
# 2016-06-13 test return status of DNS_ADD_COMMAND and error_exit if a problem (hadleyrich) (1.02)
72
# 2016-06-13 bugfix of issue 45, problem with SERVER_TYPE when it's just a port number (1.03)
73
# 2016-06-13 bugfix issue 47 - DNS_DEL_COMMAND cleanup was run when not required. (1.04)
74
# 2016-06-15 add error checking on RELOAD_CMD (1.05)
75
# 2016-06-20 updated sed and date functions to run on MAC OS X (1.06)
76
# 2016-06-20 added CHALLENGE_CHECK_TYPE variable to allow checks direct on https rather than http (1.07)
77
# 2016-06-21 updated grep functions to run on MAC OS X (1.08)
78
# 2016-06-11 updated to enable running on windows with cygwin (1.09)
79
# 2016-07-02 Corrections to work with older slackware issue #56 (1.10)
80
# 2016-07-02 Updating help info re ACL in config file (1.11)
81
# 2016-07-04 adding DOMAIN_STORAGE as a variable to solve for issue #59 (1.12)
82
# 2016-07-05 updated order to better handle non-standard DOMAIN_STORAGE location (1.13)
83
# 2016-07-06 added additional comments about SANS in example template (1.14)
84
# 2016-07-07 check for duplicate domains in domain / SANS (1.15)
85
# 2016-07-08 modified to be used on older bash for issue #64 (1.16)
86
# 2016-07-11 added -w to -a option and comments in domain template (1.17)
87
# 2016-07-18 remove / regenerate csr when generating new private domain key (1.18)
88
# 2016-07-21 add output of combined private key and domain cert (1.19)
89
# 2016-07-21 updated typo (1.20)
90
# 2016-07-22 corrected issue in nslookup debug option - issue #74 (1.21)
91
# 2016-07-26 add more server-types based on openssl s_client (1.22)
92
# 2016-08-01 updated agreement for letsencrypt (1.23)
93
# 2016-08-02 updated agreement for letsencrypt to update automatically (1.24)
94
# 2016-08-03 improve messages on test of certificate installation (1.25)
95
# 2016-08-04 remove carriage return from agreement - issue #80 (1.26)
96
# 2016-08-04 set permissions for token folders - issue #81 (1.27)
97
# 2016-08-07 allow default chained file creation - issue #85 (1.28)
98
# 2016-08-07 use copy rather than move when archiving certs - issue #86 (1.29)
99
# 2016-08-07 enable use of a single ACL for all checks (if USE_SINGLE_ACL="true" (1.30)
100
# 2016-08-23 check for already validated domains (issue #93) - (1.31)
101
# 2016-08-23 updated already validated domains (1.32)
102
# 2016-08-23 included better force_renew and template for USE_SINGLE_ACL (1.33)
103
# 2016-08-23 enable insecure certificate on https token check #94 (1.34)
104
# 2016-08-23 export OPENSSL_CONF so it's used by all openssl commands (1.35)
105
# 2016-08-25 updated defaults for ACME agreement (1.36)
106
# 2016-09-04 correct issue #101 when some domains already validated (1.37)
107
# 2016-09-12 Checks if which is installed (1.38)
108
# 2016-09-13 Don't check for updates, if -U parameter has been given (1.39)
109
# 2016-09-17 Improved error messages from invalid certs (1.40)
110
# 2016-09-19 remove update check on recursive calls when using -a (1.41)
111
# 2016-09-21 changed shebang for portability (1.42)
112
# 2016-09-21 Included option to Deactivate an Authorization (1.43)
113
# 2016-09-22 retry on 500 error from ACME server (1.44)
114
# 2016-09-22 added additional checks and retry on 500 error from ACME server (1.45)
115
# 2016-09-24 merged in IPv6 support (1.46)
116
# 2016-09-27 added additional debug info issue #119 (1.47)
117
# 2016-09-27 removed IPv6 switch in favour of checking both IPv4 and IPv6 (1.48)
118
# 2016-09-28 Add -Q, or --mute, switch to mute notifications about successfully upgrading getssl (1.49)
119
# 2016-09-30 improved portability to work natively on FreeBSD, Slackware and Mac OS X (1.50)
120
# 2016-09-30 comment out PRIVATE_KEY_ALG from the domain template Issue #125 (1.51)
121
# 2016-10-03 check remote certificate for right domain before saving to local (1.52)
122
# 2016-10-04 allow existing CSR with domain name in subject (1.53)
123
# 2016-10-05 improved the check for CSR with domain in subject (1.54)
124
# 2016-10-06 prints update info on what was included in latest updates (1.55)
125
# 2016-10-06 when using -a flag, ignore folders in working directory which aren't domains (1.56)
126
# 2016-10-12 allow multiple tokens in DNS challenge (1.57)
127
# 2016-10-14 added CHECK_ALL_AUTH_DNS option to check all DNS servers, not just one primary server (1.58)
128
# 2016-10-14 added archive of chain and private key for each cert, and purge old archives (1.59)
129
# 2016-10-17 updated info comment on failed cert due to rate limits. (1.60)
130
# 2016-10-17 fix error messages when using 1.0.1e-fips  (1.61)
131
# 2016-10-20 set secure permissions when generating account key (1.62)
132
# 2016-10-20 set permissions to 700 for getssl script during upgrade (1.63)
133
# 2016-10-20 add option to revoke a certificate (1.64)
134
# 2016-10-21 set revocation server default to acme-v01.api.letsencrypt.org (1.65)
135
# 2016-10-21 bug fix for revocation on different servers. (1.66)
136
# 2016-10-22 Tidy up archive code for certificates and reduce permissions for security
137
# 2016-10-22 Add EC signing for secp384r1 and secp521r1 (the latter not yet supported by Let's  Encrypt
138
# 2016-10-22 Add option to create a new private key for every cert (REUSE_PRIVATE_KEY="true" by default)
139
# 2016-10-22 Combine EC signing, Private key reuse and archive permissions (1.67)
140
# 2016-10-25 added CHECK_REMOTE_WAIT option ( to pause before final remote check)
141
# 2016-10-25 Added EC account key support ( prime256v1, secp384r1 ) (1.68)
142
# 2016-10-25 Ignore DNS_EXTRA_WAIT if all domains already validated (issue #146) (1.69)
143
# 2016-10-25 Add option for dual ESA / EDSA certs (1.70)
144
# 2016-10-25 bug fix Issue #141 challenge error 400 (1.71)
145
# 2016-10-26 check content of key files, not just recreate if missing.
146
# 2016-10-26 Improvements on portability (1.72)
147
# 2016-10-26 Date formatting for busybox (1.73)
148
# 2016-10-27 bug fix - issue #157 not recognising EC keys on some versions of openssl (1.74)
149
# 2016-10-31 generate EC account keys and tidy code.
150
# 2016-10-31 fix warning message if cert doesn't exist (1.75)
151
# 2016-10-31 remove only specified DNS token #161 (1.76)
152
# 2016-11-03 Reduce long lines, and remove echo from update (1.77)
153
# 2016-11-05 added TOKEN_USER_ID (to set ownership of token files )
154
# 2016-11-05 updated style to work with latest shellcheck (1.78)
155
# 2016-11-07 style updates
156
# 2016-11-07 bug fix DOMAIN_PEM_LOCATION starting with ./ #167
157
# 2016-11-08 Fix for openssl 1.1.0  #166 (1.79)
158
# 2016-11-08 Add and comment optional sshuserid for ssh ACL (1.80)
159
# 2016-11-09 Add SKIP_HTTP_TOKEN_CHECK option (Issue #170) (1.81)
160
# 2016-11-13 bug fix DOMAIN_KEY_CERT generation (1.82)
161
# 2016-11-17 add PREVENT_NON_INTERACTIVE_RENEWAL option (1.83)
162
# 2016-12-03 add HTTP_TOKEN_CHECK_WAIT option (1.84)
163
# 2016-12-03 bugfix CSR renewal when no SANS and when using MINGW (1.85)
164
# 2016-12-16 create CSR_SUBJECT variable - Issue #193
165
# 2016-12-16 added fullchain to archive (1.86)
166
# 2016-12-16 updated DOMAIN_PEM_LOCATION when using DUAL_RSA_ECDSA (1.87)
167
# 2016-12-19 allow user to ignore permission preservation with nfsv3 shares (1.88)
168
# 2016-12-19 bug fix for CA (1.89)
169
# 2016-12-19 included IGNORE_DIRECTORY_DOMAIN option (1.90)
170
# 2016-12-22 allow copying files to multiple locations (1.91)
171
# 2016-12-22 bug fix for copying tokens to multiple locations (1.92)
172
# 2016-12-23 tidy code - place default variables in alphabetical order.
173
# 2016-12-27 update checks to work with openssl in FIPS mode (1.93)
174
# 2016-12-28 fix leftover tmpfiles in upgrade routine (1.94)
175
# 2016-12-28 tidied up upgrade tmpfile handling (1.95)
176
# 2017-01-01 update comments
177
# 2017-01-01 create stable release 2.0 (2.00)
178
# 2017-01-02 Added option to limit number of old versions to keep (2.01)
179
# 2017-01-03 Created check_config function to list all obvious config issues (2.02)
180
# 2017-01-10 force renew if FORCE_RENEWAL file exists (2.03)
181
# 2017-01-12 added drill, dig or host as alternatives to nslookup (2.04)
182
# 2017-01-18 bugfix issue #227 - error deleting csr if doesn't exist
183
# 2017-01-18 issue #228 check private key and account key are different (2.05)
184
# 2017-01-21 issue #231 mingw bugfix and typos in debug messages (2.06)
185
# 2017-01-29 issue #232 use neutral locale for date formatting (2.07)
186
# 2017-01-30 issue #243 compatibility with bash 3.0 (2.08)
187
# 2017-01-30 issue #243 additional compatibility with bash 3.0 (2.09)
188
# 2017-02-18 add OCSP Must-Staple to the domain csr generation (2.10)
189
# 2018-01-04 updating to use the updated letsencrypt APIv2
190
# 2019-09-30 issue #423 Use HTTP 1.1 as workaround atm (2.11)
191
# 2019-10-02 issue #425 Case insensitive processing of agreement url because of HTTP/2 (2.12)
192
# 2019-10-07 update DNS checks to allow use of CNAMEs (2.13)
193
# 2019-11-18 Rebased master onto APIv2 and added Content-Type: application/jose+json (2.14)
194
# 2019-11-20 #453 and #454 Add User-Agent to all curl requests
195
# 2019-11-22 #456 Fix shellcheck issues
196
# 2019-11-23 #459 Fix missing chain.crt
197
# 2019-12-18 #462 Use POST-as-GET for ACMEv2 endpoints
198
# 2020-01-07 #464 and #486 "json was blank" (change all curl request to use POST-as-GET)
199
# 2020-01-08 Error and exit if rate limited, exit if curl returns nothing
200
# 2020-01-10 Change domain and getssl templates to v2 (2.15)
201
# 2020-01-17 #473 and #477 Don't use POST-as-GET when sending ready for challenge for ACMEv1 (2.16)
202
# 2020-01-22 #475 and #483 Fix grep regex for >9 subdomains in json_get
203
# 2020-01-24 Add support for CloudDNS
204
# 2020-01-24 allow file transfer using WebDAV over HTTPS
205
# 2020-01-26 Use urlbase64_decode() instead of base64 -d
206
# 2020-01-26 Fix "already verified" error for ACMEv2
207
# 2020-01-29 Check awk new enough to support json_awk
208
# 2020-02-05 Fix epoch_date for busybox
209
# 2020-02-06 Bugfixes for json_awk and nslookup to support old awk versions (2.17)
210
# 2020-02-11 Add SCP_OPTS and SFTP_OPTS
211
# 2020-02-12 Fix for DUAL_RSA_ECDSA not working with ACMEv2 (#334, #474, #502)
212
# 2020-02-12 Fix #424 - Sporadic "error in EC signing couldn't get R from ..." (2.18)
213
# 2020-02-12 Fix "Registration key already in use" (2.19)
214
# 2020-02-13 Fix bug with copying to all locations when creating RSA and ECDSA certs (2.20)
215
# 2020-02-22 Change sign_string to use openssl asn1parse (better fix for #424)
216
# 2020-02-23 Add dig to config check for systems without drill (ubuntu)
217
# 2020-03-11 Use dig +trace to find primary name server and improve dig parsing of CNAME
218
# 2020-03-12 Fix bug with DNS validation and multiple domains (#524)
219
# 2020-03-24 Find primary ns using all dns utils (dig, host, nslookup)
220
# 2020-03-23 Fix staging server URL in domain template (2.21)
221
# 2020-03-30 Fix error message find_dns_utils from over version of "command"
222
# 2020-03-30 Fix problems if domain name isn't in lowercase (2.22)
223
# 2020-04-16 Add alternative working dirs '/etc/getssl/' '${PROGDIR}/conf' '${PROGDIR}/.getssl'
224
# 2020-04-16 Add -i|--install command line option (2.23)
225
# 2020-04-19 Remove dependency on seq, ensure clean_up doesn't try to delete /tmp (2.24)
226
# 2020-04-20 Check for domain using all DNS utilities (2.25)
227
# 2020-04-22 Fix HAS_HOST and HAS_NSLOOKUP checks - wolfaba
228
# 2020-04-22 Fix domain case conversion for different locales - glynge (2.26)
229
# 2020-04-26 Fixed ipv4 confirmation with nslookup - Cyber1000
230
# 2020-04-29 Fix ftp/sftp problems if challenge starts with a dash
231
# 2020-05-06 Fix missing fullchain.ec.crt when creating dual certificates (2.27)
232
# 2020-05-14 Add --notify-valid option (exit 2 if certificate is valid)
233
# 2020-05-23 Fix --revoke (didn't work with ACMEv02) (2.28)
234
# 2020-06-06 Fix missing URL_revoke definition when no CA directory suffix (#566)
235
# 2020-06-18 Fix CHECK_REMOTE for DUAL_RSA_ECDSA (#570)
236
# 2020-07-14 Support space separated SANS (#574) (2.29)
237
# 2020-08-06 Use -sigalgs instead of -cipher when checking remote for tls1.3 (#570)
238
# 2020-08-31 Fix slow fork bomb when directory containing getssl isn't writeable (#440)
239
# 2020-09-01 Use RSA-PSS when checking remote for DUAL_RSA_ECDSA (#570)
240
# 2020-09-02 Fix issue when SANS is space and comma separated (#579) (2.30)
241
# 2020-10-02 Various fixes to get_auth_dns and changes to support unit tests (#308)
242
# 2020-10-04 Add CHECK_PUBLIC_DNS_SERVER to check the DNS challenge has been updated there
243
# ----------------------------------------------------------------------------------------
244

    
245
PROGNAME=${0##*/}
246
PROGDIR="$(cd "$(dirname "$0")" || exit; pwd -P;)"
247
VERSION="2.30"
248

    
249
# defaults
250
ACCOUNT_KEY_LENGTH=4096
251
ACCOUNT_KEY_TYPE="rsa"
252
CA="https://acme-staging-v02.api.letsencrypt.org/directory"
253
CA_CERT_LOCATION=""
254
CHALLENGE_CHECK_TYPE="http"
255
CHECK_REMOTE="true"
256
CHECK_REMOTE_WAIT=0
257
CODE_LOCATION="https://raw.githubusercontent.com/srvrco/getssl/master/getssl"
258
CSR_SUBJECT="/"
259
CURL_USERAGENT="${PROGNAME}/${VERSION}"
260
DEACTIVATE_AUTH="false"
261
DEFAULT_REVOKE_CA="https://acme-v02.api.letsencrypt.org"
262
DOMAIN_KEY_LENGTH=4096
263
DUAL_RSA_ECDSA="false"
264
GETSSL_IGNORE_CP_PRESERVE="false"
265
HTTP_TOKEN_CHECK_WAIT=0
266
IGNORE_DIRECTORY_DOMAIN="false"
267
ORIG_UMASK=$(umask)
268
PREVIOUSLY_VALIDATED="true"
269
PRIVATE_KEY_ALG="rsa"
270
RELOAD_CMD=""
271
RENEW_ALLOW="30"
272
REUSE_PRIVATE_KEY="true"
273
SERVER_TYPE="https"
274
SKIP_HTTP_TOKEN_CHECK="false"
275
SSLCONF="$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf"
276
OCSP_MUST_STAPLE="false"
277
TEMP_UPGRADE_FILE=""
278
TOKEN_USER_ID=""
279
USE_SINGLE_ACL="false"
280
WORKING_DIR_CANDIDATES=("/etc/getssl/" "${PROGDIR}/conf" "${PROGDIR}/.getssl" "${HOME}/.getssl")
281

    
282
# Variables used when validating using a DNS entry
283
VALIDATE_VIA_DNS=""             # Set this to "true" to enable DNS validation
284
AUTH_DNS_SERVER=""              # Use this DNS server to check the challenge token has been set
285
PUBLIC_DNS_SERVER=""            # Use this DNS server to find the authoritative DNS servers for the domain
286
CHECK_ALL_AUTH_DNS="false"      # Check the challenge token has been set on all authoritative DNS servers
287
CHECK_PUBLIC_DNS_SERVER="true"  # Check the public DNS server as well as the authoritative DNS servers
288
DNS_ADD_COMMAND=""              # Use this command/script to add the challenge token to the DNS entries for the domain
289
DNS_DEL_COMMAND=""              # Use this command/script to remove the challenge token from the DNS entries for the domain
290
DNS_WAIT_COUNT=100              # How many times to wait for the DNS record to update
291
DNS_WAIT=10                     # How long to wait before checking the DNS record again
292
DNS_EXTRA_WAIT=60               # How long to wait after the DNS entries are visible to us before telling the ACME server to check.
293
DNS_WAIT_RETRY_ADD="false"      # Try the dns_add_command again if the DNS record hasn't updated
294

    
295
# Private variables
296
_CHECK_ALL=0
297
_CREATE_CONFIG=0
298
_FORCE_RENEW=0
299
_KEEP_VERSIONS=""
300
_MUTE=0
301
_NOTIFY_VALID=0
302
_QUIET=0
303
_RECREATE_CSR=0
304
_REVOKE=0
305
_RUNNING_TEST=0
306
_TEST_SKIP_CNAME_CALL=0
307
_TEST_SKIP_SOA_CALL=0
308
_UPGRADE=0
309
_UPGRADE_CHECK=1
310
_USE_DEBUG=0
311
_ONLY_CHECK_CONFIG=0
312
config_errors="false"
313
LANG=C
314
API=1
315

    
316
# store copy of original command in case of upgrading script and re-running
317
ORIGCMD="$0 $*"
318

    
319
# Define all functions (in alphabetical order)
320

    
321
auto_upgrade_v2() {  # Automatically update clients to v2
322
  if [[ "${CA}" == *"acme-v01."* ]] || [[ "${CA}" == *"acme-staging."* ]]; then
323
    OLDCA=${CA}
324
    # shellcheck disable=SC2001
325
    CA=$(echo "${OLDCA}" | sed "s/v01/v02/g")
326
    # shellcheck disable=SC2001
327
    CA=$(echo "${CA}" | sed "s/staging/staging-v02/g")
328
    info "Upgraded to v2 (changed ${OLDCA} to ${CA})"
329
  fi
330
  debug "Using certificate issuer: ${CA}"
331
}
332

    
333
cert_archive() {  # Archive certificate file by copying files to dated archive dir.
334
  debug "creating an archive copy of current new certs"
335
  date_time=$(date +%Y_%m_%d_%H_%M)
336
  mkdir -p "${DOMAIN_DIR}/archive/${date_time}"
337
  umask 077
338
  cp "$CERT_FILE" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.crt"
339
  cp "$DOMAIN_DIR/${DOMAIN}.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.csr"
340
  cp "$DOMAIN_DIR/${DOMAIN}.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.key"
341
  cp "$CA_CERT" "${DOMAIN_DIR}/archive/${date_time}/chain.crt"
342
  cat "$CERT_FILE" "$CA_CERT" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.crt"
343
  if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
344
    cp "${CERT_FILE%.*}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.crt"
345
    cp "$DOMAIN_DIR/${DOMAIN}.ec.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.csr"
346
    cp "$DOMAIN_DIR/${DOMAIN}.ec.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.key"
347
    cp "${CA_CERT%.*}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/chain.ec.crt"
348
    cat "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.ec.crt"
349
  fi
350
  umask "$ORIG_UMASK"
351
  debug "purging old GetSSL archives"
352
  purge_archive "$DOMAIN_DIR"
353
}
354

    
355
cert_install() {  # copy certs to the correct location (creating concatenated files as required)
356
  umask 077
357

    
358
  copy_file_to_location "domain certificate" "$CERT_FILE" "$DOMAIN_CERT_LOCATION"
359
  copy_file_to_location "private key" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LOCATION"
360
  copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION"
361
  if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
362
    if [[ -n "$DOMAIN_CERT_LOCATION" ]]; then
363
      copy_file_to_location "ec domain certificate" \
364
                            "${CERT_FILE%.*}.ec.crt" \
365
                            "${DOMAIN_CERT_LOCATION}" \
366
                            "ec"
367
    fi
368
    if [[ -n "$DOMAIN_KEY_LOCATION" ]]; then
369
      copy_file_to_location "ec private key" \
370
                            "$DOMAIN_DIR/${DOMAIN}.ec.key" \
371
                            "${DOMAIN_KEY_LOCATION}" \
372
                            "ec"
373
    fi
374
    if [[ -n "$CA_CERT_LOCATION" ]]; then
375
      copy_file_to_location "ec CA certificate" \
376
                            "${CA_CERT%.*}.ec.crt" \
377
                            "${CA_CERT_LOCATION%.*}.crt" \
378
                            "ec"
379
    fi
380
  fi
381

    
382
  # if DOMAIN_CHAIN_LOCATION is not blank, then create and copy file.
383
  if [[ -n "$DOMAIN_CHAIN_LOCATION" ]]; then
384
    if [[ "$(dirname "$DOMAIN_CHAIN_LOCATION")" == "." ]]; then
385
      to_location="${DOMAIN_DIR}/${DOMAIN_CHAIN_LOCATION}"
386
    else
387
      to_location="${DOMAIN_CHAIN_LOCATION}"
388
    fi
389
    cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem"
390
    copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem"  "$to_location"
391
    if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
392
      cat "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}_chain.pem.ec"
393
      copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem.ec"  "${to_location}" "ec"
394
    fi
395
  fi
396
  # if DOMAIN_KEY_CERT_LOCATION is not blank, then create and copy file.
397
  if [[ -n "$DOMAIN_KEY_CERT_LOCATION" ]]; then
398
    if [[ "$(dirname "$DOMAIN_KEY_CERT_LOCATION")" == "." ]]; then
399
      to_location="${DOMAIN_DIR}/${DOMAIN_KEY_CERT_LOCATION}"
400
    else
401
      to_location="${DOMAIN_KEY_CERT_LOCATION}"
402
    fi
403
    cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem"
404
    copy_file_to_location "private key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem"  "$to_location"
405
    if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
406
      cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}_K_C.pem.ec"
407
      copy_file_to_location "private ec key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" "${to_location}" "ec"
408
    fi
409
  fi
410
  # if DOMAIN_PEM_LOCATION is not blank, then create and copy file.
411
  if [[ -n "$DOMAIN_PEM_LOCATION" ]]; then
412
    if [[ "$(dirname "$DOMAIN_PEM_LOCATION")" == "." ]]; then
413
      to_location="${DOMAIN_DIR}/${DOMAIN_PEM_LOCATION}"
414
    else
415
      to_location="${DOMAIN_PEM_LOCATION}"
416
    fi
417
    cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem"
418
    copy_file_to_location "full key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem"  "$to_location"
419
    if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
420
      cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}.pem.ec"
421
      copy_file_to_location "full ec key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem.ec"  "${to_location}" "ec"
422
    fi
423
  fi
424
  # end of copying certs.
425
  umask "$ORIG_UMASK"
426
}
427

    
428
check_challenge_completion() { # checks with the ACME server if our challenge is OK
429
  uri=$1
430
  domain=$2
431
  keyauthorization=$3
432

    
433
  info "sending request to ACME server saying we're ready for challenge"
434

    
435
  # check response from our request to perform challenge
436
  if [[ $API -eq 1 ]]; then
437
    send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}"
438

    
439
    if [[ -n "$code" ]] && [[ ! "$code" == '202' ]] ; then
440
      error_exit "$domain:Challenge error: $code"
441
    fi
442
  else # APIv2
443
    send_signed_request "$uri" "{}"
444
    if [[ -n "$code" ]] && [[ ! "$code" == '200' ]] ; then
445
      detail=$(echo "$response" | grep "detail" | awk -F\" '{print $4}')
446
      error_exit "$domain:Challenge error: $code:Detail: $detail"
447
    fi
448
  fi
449

    
450
  # loop "forever" to keep checking for a response from the ACME server.
451
  while true ; do
452
    info "checking if challenge is complete"
453
    if [[ $API -eq 1 ]]; then
454
      if ! get_cr "$uri" ; then
455
        error_exit "$domain:Verify error:$code"
456
      fi
457
    else # APIv2
458
      send_signed_request "$uri" ""
459
    fi
460

    
461
    status=$(json_get "$response" status)
462

    
463
    # If ACME response is valid, then break out of loop
464
    if [[ "$status" == "valid" ]] ; then
465
      info "Verified $domain"
466
      break;
467
    fi
468

    
469
    # if ACME response is that their check gave an invalid response, error exit
470
    if [[ "$status" == "invalid" ]] ; then
471
      err_detail=$(echo "$response" | grep "detail")
472
      #! FIXME need to check for "DNS problem: SERVFAIL looking up CAA ..." and retry
473
      error_exit "$domain:Verify error:$err_detail"
474
    fi
475

    
476
    # if ACME response is pending ( they haven't completed checks yet) then wait and try again.
477
    if [[ "$status" == "pending" ]] ; then
478
      info "Pending"
479
    else
480
      err_detail=$(echo "$response" | grep "detail")
481
      error_exit "$domain:Verify error:$status:$err_detail"
482
    fi
483
    debug "sleep 5 secs before testing verify again"
484
    sleep 5
485
  done
486

    
487
  if [[ "$DEACTIVATE_AUTH" == "true" ]]; then
488
    deactivate_url=$(echo "$responseHeaders" | grep "^Link" | awk -F"[<>]" '{print $2}')
489
    deactivate_url_list="$deactivate_url_list $deactivate_url"
490
    debug "adding url to deactivate list - $deactivate_url"
491
  fi
492
}
493

    
494
check_config() { # check the config files for all obvious errors
495
  debug "checking config"
496

    
497
  # check keys
498
  case "$ACCOUNT_KEY_TYPE" in
499
    rsa|prime256v1|secp384r1|secp521r1)
500
      debug "checked ACCOUNT_KEY_TYPE " ;;
501
    *)
502
      info "${DOMAIN}: invalid ACCOUNT_KEY_TYPE - $ACCOUNT_KEY_TYPE"
503
      config_errors=true ;;
504
  esac
505
  if [[ "$ACCOUNT_KEY" == "$DOMAIN_DIR/${DOMAIN}.key" ]]; then
506
    info "${DOMAIN}: ACCOUNT_KEY and domain key ( $DOMAIN_DIR/${DOMAIN}.key ) must be different"
507
    config_errors=true
508
  fi
509
  case "$PRIVATE_KEY_ALG" in
510
    rsa|prime256v1|secp384r1|secp521r1)
511
      debug "checked PRIVATE_KEY_ALG " ;;
512
    *)
513
      info "${DOMAIN}: invalid PRIVATE_KEY_ALG - $PRIVATE_KEY_ALG"
514
      config_errors=true ;;
515
  esac
516
  if [[ "$DUAL_RSA_ECDSA" == "true" ]] && [[ "$PRIVATE_KEY_ALG" == "rsa" ]]; then
517
    info "${DOMAIN}: PRIVATE_KEY_ALG not set to an EC type and DUAL_RSA_ECDSA=\"true\""
518
    config_errors=true
519
  fi
520

    
521
  # get all domains
522
  if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
523
    alldomains=${SANS//[, ]/ }
524
  else
525
    alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")
526
  fi
527
  if [[ -z "$alldomains" ]]; then
528
    info "${DOMAIN}: no domains specified"
529
    config_errors=true
530
  fi
531

    
532
  if [[ $VALIDATE_VIA_DNS == "true" ]]; then # using dns-01 challenge
533
    if [[ -z "$DNS_ADD_COMMAND" ]]; then
534
      info "${DOMAIN}: DNS_ADD_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")"
535
      config_errors=true
536
    fi
537
    if [[ -z "$DNS_DEL_COMMAND" ]]; then
538
      info "${DOMAIN}: DNS_DEL_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")"
539
      config_errors=true
540
    fi
541
  fi
542

    
543
  dn=0
544
  tmplist=$(mktemp 2>/dev/null || mktemp -t getssl)
545
  for d in $alldomains; do # loop over domains (dn is domain number)
546
    debug "checking domain $d"
547
    if [[ "$(grep "^${d}$" "$tmplist")" = "$d" ]]; then
548
      info "${DOMAIN}: $d appears to be duplicated in domain, SAN list"
549
      config_errors=true
550
    else
551
      echo "$d" >> "$tmplist"
552
    fi
553

    
554
    if [[ "$USE_SINGLE_ACL" == "true" ]]; then
555
      DOMAIN_ACL="${ACL[0]}"
556
    else
557
      DOMAIN_ACL="${ACL[$dn]}"
558
    fi
559

    
560
    if [[ $VALIDATE_VIA_DNS != "true" ]]; then # using http-01 challenge
561
      if [[ -z "${DOMAIN_ACL}" ]]; then
562
        info "${DOMAIN}: ACL location not specified for domain $d in $DOMAIN_DIR/getssl.cfg"
563
        config_errors=true
564
      fi
565

    
566
      # check domain exists using all DNS utilities
567
      found_ip=false
568
      if [[ -n "$HAS_DIG_OR_DRILL" ]]; then
569
        debug "DNS lookup using $HAS_DIG_OR_DRILL ${d}"
570
        if [[ "$($HAS_DIG_OR_DRILL -t SOA "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then
571
          found_ip=true
572
        elif [[ "$($HAS_DIG_OR_DRILL -t A "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then
573
          found_ip=true
574
        elif [[ "$($HAS_DIG_OR_DRILL -t AAAA "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then
575
          found_ip=true
576
        fi
577
      fi
578

    
579
      if [[ "$HAS_HOST" == "true" ]]; then
580
        debug "DNS lookup using host ${d}"
581
        if [[ "$(host "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then
582
          found_ip=true
583
        fi
584
      fi
585

    
586
      if [[ "$HAS_NSLOOKUP" == "true" ]]; then
587
        debug "DNS lookup using nslookup -query AAAA ${d}"
588
        if [[ "$(nslookup -query=AAAA "${d}"|grep -c -i "^${d}.*has AAAA address")" -ge 1 ]]; then
589
          debug "found IPv6 record for ${d}"
590
          found_ip=true
591
        elif [[ "$(nslookup "${d}"| grep -c ^Name)" -ge 1 ]]; then
592
          debug "found IPv4 record for ${d}"
593
          found_ip=true
594
        fi
595
      fi
596

    
597
      if [[ "$found_ip" == "false" ]]; then
598
        info "${DOMAIN}: DNS lookup failed for $d"
599
        config_errors=true
600
      fi
601
    fi # end using dns-01 challenge
602
    ((dn++))
603
  done
604

    
605
  # tidy up
606
  rm -f "$tmplist"
607

    
608
  if [[ "$config_errors" == "true" ]]; then
609
    error_exit "${DOMAIN}: exiting due to config errors"
610
  fi
611
  debug "${DOMAIN}: check_config completed  - all OK"
612
}
613

    
614
check_getssl_upgrade() { # check if a more recent version of code is available available
615
  TEMP_UPGRADE_FILE="$(mktemp 2>/dev/null || mktemp -t getssl)"
616
  curl --user-agent "$CURL_USERAGENT" --silent "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE"
617
  errcode=$?
618
  if [[ $errcode -eq 60 ]]; then
619
    error_exit "curl needs updating, your version does not support SNI (multiple SSL domains on a single IP)"
620
  elif [[ $errcode -gt 0 ]]; then
621
    error_exit "curl error : $errcode"
622
  fi
623
  latestversion=$(awk -F '"' '$1 == "VERSION=" {print $2}' "$TEMP_UPGRADE_FILE")
624
  latestvdec=$(echo "$latestversion"| tr -d '.')
625
  localvdec=$(echo "$VERSION"| tr -d '.' )
626
  debug "current code is version ${VERSION}"
627
  debug "Most recent version is  ${latestversion}"
628
  # use a default of 0 for cases where the latest code has not been obtained.
629
  if [[ "${latestvdec:-0}" -gt "$localvdec" ]]; then
630
    if [[ ${_UPGRADE} -eq 1 ]]; then
631
      if ! install "$0" "${0}.v${VERSION}"; then
632
	 error_exit "problem renaming old version while updating, check permissions"
633
      fi
634
      if ! install -m 700 "$TEMP_UPGRADE_FILE" "$0"; then
635
	 error_exit "problem installing new version while updating, check permissions"
636
      fi
637
      if [[ ${_MUTE} -eq 0 ]]; then
638
        echo "Updated getssl from v${VERSION} to v${latestversion}"
639
        echo "these update notification can be turned off using the -Q option"
640
        echo ""
641
        echo "Updates are;"
642
        awk "/\(${VERSION}\)$/ {s=1} s; /\(${latestversion}\)$/ {s=0}" "$TEMP_UPGRADE_FILE" | awk '{if(NR>1)print}'
643
        echo ""
644
      fi
645
      if [[ -n "$_KEEP_VERSIONS" ]] && [[ "$_KEEP_VERSIONS" =~ ^[0-9]+$ ]]; then
646
        # Obtain all locally stored old versions in getssl_versions
647
        declare -a getssl_versions
648
        shopt -s nullglob
649
        for getssl_version in "$0".v*; do
650
          getssl_versions[${#getssl_versions[@]}]="$getssl_version"
651
        done
652
        shopt -u nullglob
653
        # Explicitly sort the getssl_versions array to make sure
654
        shopt -s -o noglob
655
        # shellcheck disable=SC2207
656
        IFS=$'\n' getssl_versions=($(sort <<< "${getssl_versions[*]}"))
657
        shopt -u -o noglob
658
        # Remove entries until given number of old versions to keep is reached
659
        while [[ ${#getssl_versions[@]} -gt $_KEEP_VERSIONS ]]; do
660
          debug "removing old version ${getssl_versions[0]}"
661
          rm "${getssl_versions[0]}"
662
          getssl_versions=("${getssl_versions[@]:1}")
663
        done
664
      fi
665
      eval "$ORIGCMD"
666
      graceful_exit
667
    else
668
      info ""
669
      info "A more recent version (v${latestversion}) of getssl is available, please update"
670
      info "the easiest way is to use the -u or --upgrade flag"
671
      info ""
672
    fi
673
  fi
674
}
675

    
676
clean_up() { # Perform pre-exit housekeeping
677
  umask "$ORIG_UMASK"
678
  if [[ $VALIDATE_VIA_DNS == "true" ]]; then
679
    # Tidy up DNS entries if things failed part way though.
680
    shopt -s nullglob
681
    for dnsfile in "$TEMP_DIR"/dns_verify/*; do
682
      # shellcheck source=/dev/null
683
      . "$dnsfile"
684
      debug "attempting to clean up DNS entry for $d"
685
      eval "$DNS_DEL_COMMAND" "$d" "$auth_key"
686
    done
687
    shopt -u nullglob
688
  fi
689
  if [[ -n "$DOMAIN_DIR" ]]; then
690
    if [ "${TEMP_DIR}" -ef "/tmp" ]; then
691
        info "Not going to delete TEMP_DIR ${TEMP_DIR} as it appears to be /tmp"
692
    else
693
        rm -rf "${TEMP_DIR:?}"
694
    fi
695
  fi
696
  if [[ -n "$TEMP_UPGRADE_FILE" ]] && [[ -f "$TEMP_UPGRADE_FILE" ]]; then
697
    rm -f "$TEMP_UPGRADE_FILE"
698
  fi
699
}
700

    
701
copy_file_to_location() { # copies a file, using scp, sftp or ftp if required.
702
  cert=$1   # descriptive name, just used for display
703
  from=$2   # current file location
704
  to=$3     # location to move file to.
705
  suffix=$4 # (optional) optional suffix for DUAL_RSA_ECDSA, i.e. save to private.key becomes save to private.ec.key
706
  IFS=\; read -r -a copy_locations <<<"$3"
707
  for to in "${copy_locations[@]}"; do
708
    if [[ -n "$suffix" ]]; then
709
      to="${to%.*}.${suffix}.${to##*.}"
710
    fi
711
    info "copying $cert to $to"
712
    if [[ "${to:0:4}" == "ssh:" ]] ; then
713
      debug "using scp -q $SCP_OPTS $from ${to:4}"
714
      # shellcheck disable=SC2086
715
      if ! scp -q $SCP_OPTS "$from" "${to:4}" >/dev/null 2>&1 ; then
716
        error_exit "problem copying file to the server using scp.
717
        scp $from ${to:4}"
718
      fi
719
      debug "userid $TOKEN_USER_ID"
720
      if [[ "$cert" == "challenge token" ]] && [[ -n "$TOKEN_USER_ID" ]]; then
721
        servername=$(echo "$to" | awk -F":" '{print $2}')
722
        tofile=$(echo "$to" | awk -F":" '{print $3}')
723
        debug "servername $servername"
724
        debug "file $tofile"
725
        # shellcheck disable=SC2029
726
        # shellcheck disable=SC2086
727
        ssh $SSH_OPTS "$servername" "chown $TOKEN_USER_ID $tofile"
728
      fi
729
    elif [[ "${to:0:4}" == "ftp:" ]] ; then
730
      if [[ "$cert" != "challenge token" ]] ; then
731
        error_exit "ftp is not a secure method for copying certificates or keys"
732
      fi
733
      debug "using ftp to copy the file from $from"
734
      ftpuser=$(echo "$to"| awk -F: '{print $2}')
735
      ftppass=$(echo "$to"| awk -F: '{print $3}')
736
      ftphost=$(echo "$to"| awk -F: '{print $4}')
737
      ftplocn=$(echo "$to"| awk -F: '{print $5}')
738
      ftpdirn=$(dirname "$ftplocn")
739
      ftpfile=$(basename "$ftplocn")
740
      fromdir=$(dirname "$from")
741
      fromfile=$(basename "$from")
742
      debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile"
743
      debug "from dir=$fromdir  file=$fromfile"
744
      ftp -n <<- _EOF
745
			open $ftphost
746
			user $ftpuser $ftppass
747
			cd $ftpdirn
748
			lcd $fromdir
749
			put ./$fromfile
750
			_EOF
751
    elif [[ "${to:0:5}" == "sftp:" ]] ; then
752
      debug "using sftp to copy the file from $from"
753
      ftpuser=$(echo "$to"| awk -F: '{print $2}')
754
      ftppass=$(echo "$to"| awk -F: '{print $3}')
755
      ftphost=$(echo "$to"| awk -F: '{print $4}')
756
      ftplocn=$(echo "$to"| awk -F: '{print $5}')
757
      ftpdirn=$(dirname "$ftplocn")
758
      ftpfile=$(basename "$ftplocn")
759
      fromdir=$(dirname "$from")
760
      fromfile=$(basename "$from")
761
      debug "sftp $SFTP_OPTS user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile"
762
      debug "from dir=$fromdir  file=$fromfile"
763
      # shellcheck disable=SC2086
764
      sshpass -p "$ftppass" sftp $SFTP_OPTS "$ftpuser@$ftphost" <<- _EOF
765
			cd $ftpdirn
766
			lcd $fromdir
767
			put ./$fromfile
768
			_EOF
769
    elif [[ "${to:0:5}" == "davs:" ]] ; then
770
      debug "using davs to copy the file from $from"
771
      davsuser=$(echo "$to"| awk -F: '{print $2}')
772
      davspass=$(echo "$to"| awk -F: '{print $3}')
773
      davshost=$(echo "$to"| awk -F: '{print $4}')
774
      davsport=$(echo "$to"| awk -F: '{print $5}')
775
      davslocn=$(echo "$to"| awk -F: '{print $6}')
776
      davsdirn=$(dirname "$davslocn")
777
      davsfile=$(basename "$davslocn")
778
      fromdir=$(dirname "$from")
779
      fromfile=$(basename "$from")
780
      debug "davs user=$davsuser - pass=$davspass - host=$davshost port=$davsport dir=$davsdirn file=$davsfile"
781
      debug "from dir=$fromdir  file=$fromfile"
782
      curl -u "${davsuser}:${davspass}" -T "${fromdir}/${fromfile}" "https://${davshost}:${davsport}${davsdirn}/${davsfile}"
783
    else
784
      if ! mkdir -p "$(dirname "$to")" ; then
785
        error_exit "cannot create ACL directory $(basename "$to")"
786
      fi
787
      if [[ "$GETSSL_IGNORE_CP_PRESERVE" == "true" ]]; then
788
        if ! cp "$from" "$to" ; then
789
          error_exit "cannot copy $from to $to"
790
        fi
791
      else
792
        if ! cp -p "$from" "$to" ; then
793
          error_exit "cannot copy $from to $to"
794
        fi
795
      fi
796
      if [[ "$cert" == "challenge token" ]] && [[ -n "$TOKEN_USER_ID" ]]; then
797
        chown "$TOKEN_USER_ID" "$to"
798
      fi
799
    fi
800
    debug "copied $from to $to"
801
  done
802
}
803

    
804
create_csr() { # create a csr using a given key (if it doesn't already exist)
805
  csr_file=$1
806
  csr_key=$2
807
  # check if domain csr exists - if not then create it
808
  if [[ -s "$csr_file" ]]; then
809
    debug "domain csr exists at - $csr_file"
810
    # check all domains in config are in csr
811
    if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
812
      alldomains=$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u)
813
    else
814
      alldomains=$(echo "$DOMAIN,$SANS" | sed -e 's/,/ /g; s/ $//; y/ /\n/' | sort -u)
815
    fi
816
    domains_in_csr=$(openssl req -text -noout -in "$csr_file" \
817
        | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \
818
        | sort -u)
819
    for d in $alldomains; do
820
      if [[ "$(echo "${domains_in_csr}"| grep "^${d}$")" != "${d}" ]]; then
821
        info "existing csr at $csr_file does not contain ${d} - re-create-csr"\
822
             ".... $(echo "${domains_in_csr}"| grep "^${d}$")"
823
        _RECREATE_CSR=1
824
      fi
825
    done
826
    # check all domains in csr are in config
827
    if [[ "$alldomains" != "$domains_in_csr" ]]; then
828
      info "existing csr at $csr_file does not have the same domains as the config - re-create-csr"
829
      _RECREATE_CSR=1
830
    fi
831
  fi
832
  # end of ... check if domain csr exists - if not then create it
833

    
834
  # if CSR does not exist, or flag set to recreate, then create csr
835
  if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "1" ]]; then
836
    info "creating domain csr - $csr_file"
837
    # create a temporary config file, for portability.
838
    tmp_conf=$(mktemp 2>/dev/null || mktemp -t getssl)
839
    cat "$SSLCONF" > "$tmp_conf"
840
    printf "[SAN]\n%s" "$SANLIST" >> "$tmp_conf"
841
    # add OCSP Must-Staple to the domain csr
842
    # if openssl version >= 1.1.0 one can also use "tlsfeature = status_request"
843
    if [[ "$OCSP_MUST_STAPLE" == "true" ]]; then
844
      printf "\n1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05" >> "$tmp_conf"
845
    fi
846
    openssl req -new -sha256 -key "$csr_key" -subj "$CSR_SUBJECT" -reqexts SAN -config "$tmp_conf" > "$csr_file"
847
    rm -f "$tmp_conf"
848
  fi
849
}
850

    
851
create_key() { # create a domain key (if it doesn't already exist)
852
  key_type=$1 # domain key type
853
  key_loc=$2  # domain key location
854
  key_len=$3  # domain key length - for rsa keys.
855
  # check if key exists, if not then create it.
856
  if [[ -s "$key_loc" ]]; then
857
    debug "domain key exists at $key_loc - skipping generation"
858
    # ideally need to check validity of domain key
859
  else
860
    umask 077
861
    info "creating key - $key_loc"
862
    case "$key_type" in
863
      rsa)
864
        openssl genrsa "$key_len" > "$key_loc";;
865
      prime256v1|secp384r1|secp521r1)
866
        openssl ecparam -genkey -name "$key_type" > "$key_loc";;
867
      *)
868
        error_exit "unknown private key algorithm type $key_loc";;
869
    esac
870
    umask "$ORIG_UMASK"
871
    # remove csr on generation of new domain key
872
    if [[ -e "${key_loc%.*}.csr" ]]; then
873
      rm -f "${key_loc%.*}.csr"
874
    fi
875
  fi
876
}
877

    
878
create_order() {
879
  dstring="["
880
  for d in $alldomains; do
881
    dstring="${dstring}{\"type\":\"dns\",\"value\":\"$d\"},"
882
  done
883
  dstring="${dstring::${#dstring}-1}]"
884
  # request NewOrder currently seems to ignore the dates ....
885
  #  dstring="${dstring},\"notBefore\": \"$(date -d "-1 hour" --utc +%FT%TZ)\""
886
  #  dstring="${dstring},\"notAfter\": \"$(date -d "2 days" --utc +%FT%TZ)\""
887
  request="{\"identifiers\": $dstring}"
888
  send_signed_request "$URL_newOrder" "$request"
889
  OrderLink=$(echo "$responseHeaders" | grep -i location | awk '{print $2}'| tr -d '\r\n ')
890
  debug "Order link $OrderLink"
891
  FinalizeLink=$(json_get "$response" "finalize")
892

    
893
  if [[ $API -eq 1 ]]; then
894
    dn=0
895
    for d in $alldomains; do
896
      # get authorizations link
897
      AuthLink[$dn]=$(json_get "$response" "identifiers" "value" "$d" "authorizations" "x")
898
      debug "authorizations link for $d - ${AuthLink[$dn]}"
899
      ((dn++))
900
    done
901
  else
902
    # Authorization links are unsorted, so fetch the authorization link, find the domain, save response in the correct array position
903
    AuthLinks=$(json_get "$response" "authorizations")
904
    AuthLinkResponse=()
905
    AuthLinkResponseHeader=()
906
    for l in $AuthLinks; do
907
      debug "Requesting authorizations link for $l"
908
      send_signed_request "$l" ""
909
      # Get domain from response
910
      authdomain=$(json_get "$response" "identifier" "value")
911
      # find array position (This is O(n2) but that doubt we'll see performance issues)
912
      dn=0
913
      for d in $alldomains; do
914
        # Convert domain to lowercase as response from server will be in lowercase
915
        d=$(echo "$d" | tr "[:upper:]" "[:lower:]")
916
        if [ "$d" == "$authdomain" ]; then
917
          debug "Saving authorization response for $authdomain for domain alldomains[$dn]"
918
          AuthLinkResponse[$dn]=$response
919
          AuthLinkResponseHeader[$dn]=$responseHeaders
920
        fi
921
        ((dn++))
922
      done
923
    done
924
  fi
925
}
926

    
927
date_epoc() { # convert the date into epoch time
928
  if [[ "$os" == "bsd" ]]; then
929
    date -j -f "%b %d %T %Y %Z" "$1" +%s
930
  elif [[ "$os" == "mac" ]]; then
931
    date -j -f "%b %d %T %Y %Z" "$1" +%s
932
  elif [[ "$os" == "busybox" ]]; then
933
    de_ld=$(echo "$1" | awk '{print $1 " " $2 " " $3 " " $4}')
934
    date -D "%b %d %T %Y" -d "$de_ld" +%s
935
  else
936
    date -d "$1" +%s
937
  fi
938

    
939
}
940

    
941
date_fmt() { # format date from epoc time to YYYY-MM-DD
942
  if [[ "$os" == "bsd" ]]; then #uses older style date function.
943
    date -j -f "%s" "$1" +%F
944
  elif [[ "$os" == "mac" ]]; then # macOS uses older BSD style date.
945
    date -j -f "%s" "$1" +%F
946
  else
947
    date -d "@$1" +%F
948
  fi
949
}
950

    
951
date_renew() { # calculates the renewal time in epoch
952
  date_now_s=$( date +%s )
953
  echo "$((date_now_s + RENEW_ALLOW*24*60*60))"
954
}
955

    
956
debug() { # write out debug info if the debug flag has been set
957
  if [[ ${_USE_DEBUG} -eq 1 ]]; then
958
    # If running tests then output in TAP format (for debugging tests)
959
    if [[ ${_RUNNING_TEST} -eq 1 ]]; then
960
      echo "#" "$@" >&3
961
    else
962
      echo " "
963
      echo "$@"
964
    fi
965
  fi
966
}
967

    
968
test_output() { # write out debug output for testing
969
  if [[ ${_RUNNING_TEST} -eq 1 ]]; then
970
    echo "#" "$@"
971
  fi
972
}
973

    
974
error_exit() { # give error message on error exit
975
  echo -e "${PROGNAME}: ${1:-"Unknown Error"}" >&2
976
  clean_up
977
  exit 1
978
}
979

    
980
find_dns_utils() {
981
    HAS_NSLOOKUP=false
982
    HAS_DIG_OR_DRILL=""
983
    HAS_HOST=false
984
    if [[ -n "$(command -v nslookup 2>/dev/null)" ]]; then
985
        debug "HAS NSLOOKUP=true"
986
        HAS_NSLOOKUP=true
987
    fi
988

    
989
    if [[ -n "$(command -v drill 2>/dev/null)" ]]; then
990
        debug "HAS DIG_OR_DRILL=drill"
991
        HAS_DIG_OR_DRILL="drill"
992
    elif [[ -n "$(command -v dig 2>/dev/null)" ]]; then
993
        debug "HAS DIG_OR_DRILL=dig"
994
        HAS_DIG_OR_DRILL="dig"
995
    fi
996

    
997
    if [[ -n "$(command -v host 2>/dev/null)" ]]; then
998
        debug "HAS HOST=true"
999
        HAS_HOST=true
1000
    fi
1001
}
1002

    
1003
fulfill_challenges() {
1004
dn=0
1005
for d in $alldomains; do
1006
  # $d is domain in current loop, which is number $dn for ACL
1007
  info "Verifying $d"
1008
  if [[ "$USE_SINGLE_ACL" == "true" ]]; then
1009
    DOMAIN_ACL="${ACL[0]}"
1010
  else
1011
    DOMAIN_ACL="${ACL[$dn]}"
1012
  fi
1013

    
1014
  # request a challenge token from ACME server
1015
  if [[ $API -eq 1 ]]; then
1016
    request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"$d\"}}"
1017
    send_signed_request "$URL_new_authz" "$request"
1018
    debug "completed send_signed_request"
1019

    
1020
    # check if we got a valid response and token, if not then error exit
1021
    if [[ -n "$code" ]] && [[ ! "$code" == '201' ]] ; then
1022
      error_exit "new-authz error: $response"
1023
    fi
1024
  else
1025
    response=${AuthLinkResponse[$dn]}
1026
    responseHeaders=${AuthLinkResponseHeader[$dn]}
1027
    response_status=$(json_get "$response" status)
1028
  fi
1029

    
1030
  if [[ $response_status == "valid" ]]; then
1031
    info "$d is already validated"
1032
    if [[ "$DEACTIVATE_AUTH" == "true" ]]; then
1033
      deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")"
1034
      deactivate_url_list+=" $deactivate_url "
1035
      debug "url added to deactivate list ${deactivate_url}"
1036
      debug "deactivate list is now $deactivate_url_list"
1037
    fi
1038
    # increment domain-counter
1039
    ((dn++))
1040
  else
1041
    PREVIOUSLY_VALIDATED="false"
1042
    if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification
1043
      if [[ $API -eq 1 ]]; then
1044
        # get the dns component of the ACME response
1045
        # get the token and uri from the dns component
1046
        token=$(json_get "$response" "token" "dns-01")
1047
        uri=$(json_get "$response" "uri" "dns-01")
1048
        debug uri "$uri"
1049
      else # APIv2
1050
        debug "authlink response = $response"
1051
        # get the token and uri from the dns-01 component
1052
        token=$(json_get "$response" "challenges" "type" "dns-01" "token")
1053
        uri=$(json_get "$response" "challenges" "type" "dns-01" "url")
1054
        debug uri "$uri"
1055
      fi
1056

    
1057
      keyauthorization="$token.$thumbprint"
1058
      debug keyauthorization "$keyauthorization"
1059

    
1060
      #create signed authorization key from token.
1061
      auth_key=$(printf '%s' "$keyauthorization" | openssl dgst -sha256 -binary \
1062
                 | openssl base64 -e \
1063
                 | tr -d '\n\r' \
1064
                 | sed -e 's:=*$::g' -e 'y:+/:-_:')
1065
      debug auth_key "$auth_key"
1066

    
1067
      # shellcheck disable=SC2018,SC2019
1068
      lower_d=$(echo "$d" | tr A-Z a-z)
1069
      debug "adding dns via command: $DNS_ADD_COMMAND $lower_d $auth_key"
1070
      if ! eval "$DNS_ADD_COMMAND" "$lower_d" "$auth_key" ; then
1071
        error_exit "DNS_ADD_COMMAND failed for domain $d"
1072
      fi
1073

    
1074
      # find a primary / authoritative DNS server for the domain
1075
      if [[ -z "$AUTH_DNS_SERVER" ]]; then
1076
        get_auth_dns "$d"
1077
      else
1078
        primary_ns="$AUTH_DNS_SERVER"
1079
      fi
1080
      debug primary_ns "$primary_ns"
1081

    
1082
      # make a directory to hold pending dns-challenges
1083
      if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then
1084
        mkdir "$TEMP_DIR/dns_verify"
1085
      fi
1086

    
1087
      # generate a file with the current variables for the dns-challenge
1088
      cat > "$TEMP_DIR/dns_verify/$d" <<- _EOF_
1089
			token="${token}"
1090
			uri="${uri}"
1091
			keyauthorization="${keyauthorization}"
1092
			d="${d}"
1093
			primary_ns="${primary_ns}"
1094
			auth_key="${auth_key}"
1095
			_EOF_
1096

    
1097
    else      # set up the correct http token for verification
1098
      if [[ $API -eq 1 ]]; then
1099
        # get the token from the http component
1100
        token=$(json_get "$response" "token" "http-01")
1101
        # get the uri from the http component
1102
        uri=$(json_get "$response" "uri" "http-01")
1103
        debug uri "$uri"
1104
      else # APIv2
1105
        debug "authlink response = $response"
1106
        # get the token from the http-01 component
1107
        token=$(json_get "$response" "challenges" "type" "http-01" "token")
1108
        # get the uri from the http component
1109
        uri=$(json_get "$response" "challenges" "type" "http-01" "url" | head -n1)
1110
        debug uri "$uri"
1111
      fi
1112

    
1113
      #create signed authorization key from token.
1114
      keyauthorization="$token.$thumbprint"
1115

    
1116
      # save variable into temporary file
1117
      echo -n "$keyauthorization" > "$TEMP_DIR/$token"
1118
      chmod 644 "$TEMP_DIR/$token"
1119

    
1120
      # copy to token to acme challenge location
1121
      umask 0022
1122
      IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL"
1123
      for t_loc in "${token_locations[@]}"; do
1124
        debug "copying file from $TEMP_DIR/$token to ${t_loc}"
1125
        copy_file_to_location "challenge token" \
1126
                              "$TEMP_DIR/$token" \
1127
                              "${t_loc}/$token"
1128
      done
1129
      umask "$ORIG_UMASK"
1130

    
1131
      wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}/.well-known/acme-challenge/$token"
1132
      debug wellknown_url "$wellknown_url"
1133

    
1134
      if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then
1135
        info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly"
1136
      else
1137
        sleep "$HTTP_TOKEN_CHECK_WAIT"
1138
        # check that we can reach the challenge ourselves, if not, then error
1139
        if [[ ! "$(curl --user-agent "$CURL_USERAGENT" -k --silent --location "$wellknown_url")" == "$keyauthorization" ]]; then
1140
          error_exit "for some reason could not reach $wellknown_url - please check it manually"
1141
        fi
1142
      fi
1143

    
1144
      check_challenge_completion "$uri" "$d" "$keyauthorization"
1145

    
1146
      debug "remove token from ${DOMAIN_ACL}"
1147
      IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL"
1148
      for t_loc in "${token_locations[@]}"; do
1149
        if [[ "${t_loc:0:4}" == "ssh:" ]] ; then
1150
          sshhost=$(echo "${t_loc}"| awk -F: '{print $2}')
1151
          command="rm -f ${t_loc:(( ${#sshhost} + 5))}/${token:?}"
1152
          debug "running following command to remove token"
1153
          debug "ssh $SSH_OPTS $sshhost ${command}"
1154
          # shellcheck disable=SC2029 disable=SC2086
1155
          ssh $SSH_OPTS "$sshhost" "${command}" 1>/dev/null 2>&1
1156
          rm -f "${TEMP_DIR:?}/${token:?}"
1157
        elif [[ "${t_loc:0:4}" == "ftp:" ]] ; then
1158
          debug "using ftp to remove token file"
1159
          ftpuser=$(echo "${t_loc}"| awk -F: '{print $2}')
1160
          ftppass=$(echo "${t_loc}"| awk -F: '{print $3}')
1161
          ftphost=$(echo "${t_loc}"| awk -F: '{print $4}')
1162
          ftplocn=$(echo "${t_loc}"| awk -F: '{print $5}')
1163
          debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost location=$ftplocn"
1164
          ftp -n <<- EOF
1165
					open $ftphost
1166
					user $ftpuser $ftppass
1167
					cd $ftplocn
1168
					delete ${token:?}
1169
					EOF
1170
        else
1171
          rm -f "${t_loc:?}/${token:?}"
1172
        fi
1173
      done
1174
    fi
1175
    # increment domain-counter
1176
    ((dn++))
1177
  fi
1178
done # end of ... loop through domains for cert ( from SANS list)
1179

    
1180
# perform validation if via DNS challenge
1181
if [[ $VALIDATE_VIA_DNS == "true" ]]; then
1182
  # loop through dns-variable files to check if dns has been changed
1183
  for dnsfile in "$TEMP_DIR"/dns_verify/*; do
1184
    if [[ -e "$dnsfile" ]]; then
1185
      debug "loading DNSfile: $dnsfile"
1186
      # shellcheck source=/dev/null
1187
      . "$dnsfile"
1188

    
1189
      # Always use lowercase domain name when querying DNS servers
1190
      # shellcheck disable=SC2018,SC2019
1191
      lower_d=$(echo "$d" | tr A-Z a-z)
1192

    
1193
      # check for token at public dns server, waiting for a valid response.
1194
      for ns in $primary_ns; do
1195
        debug "checking dns at $ns"
1196
        ntries=0
1197
        check_dns="fail"
1198
        while [[ "$check_dns" == "fail" ]]; do
1199
          if [[ "$os" == "cygwin" ]]; then
1200
            check_result=$(nslookup -type=txt "_acme-challenge.${lower_d}" "${ns}" \
1201
                           | grep ^_acme -A2\
1202
                           | grep '"'|awk -F'"' '{ print $2}')
1203
          elif [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then
1204
            debug "$DNS_CHECK_FUNC" TXT "_acme-challenge.${lower_d}" "@${ns}"
1205
            check_result=$($DNS_CHECK_FUNC TXT "_acme-challenge.${lower_d}" "@${ns}" \
1206
                           | grep 'IN\WTXT'|awk -F'"' '{ print $2}')
1207
          elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then
1208
            check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${lower_d}" "${ns}" \
1209
                           | grep 'descriptive text'|awk -F'"' '{ print $2}')
1210
          else
1211
            check_result=$(nslookup -type=txt "_acme-challenge.${lower_d}" "${ns}" \
1212
                           | grep 'text ='|awk -F'"' '{ print $2}')
1213
          fi
1214
          debug "expecting  $auth_key"
1215
          debug "${ns} gave ... $check_result"
1216

    
1217
          if [[ "$check_result" == *"$auth_key"* ]]; then
1218
            check_dns="success"
1219
          else
1220
            if [[ $ntries -lt $DNS_WAIT_COUNT ]]; then
1221
              ntries=$(( ntries + 1 ))
1222

    
1223
              if [[ $DNS_WAIT_RETRY_ADD == "true" && $(( ntries % 10 == 0 )) ]]; then
1224
                debug "Retrying adding dns via command: $DNS_ADD_COMMAND $lower_d $auth_key"
1225
                if ! eval "$DNS_ADD_COMMAND" "$lower_d" "$auth_key" ; then
1226
                  error_exit "DNS_ADD_COMMAND failed for domain $d"
1227
                fi
1228

    
1229
              fi
1230
              info "checking DNS at ${ns} for ${lower_d}. Attempt $ntries/${DNS_WAIT_COUNT} gave wrong result, "\
1231
                "waiting $DNS_WAIT secs before checking again"
1232
              sleep $DNS_WAIT
1233
            else
1234
              debug "dns check failed - removing existing value"
1235
              eval "$DNS_DEL_COMMAND" "$lower_d" "$auth_key"
1236
              # remove $dnsfile after each loop.
1237
              rm -f "$dnsfile"
1238

    
1239
              error_exit "checking _acme-challenge.${lower_d} gave $check_result not $auth_key"
1240
            fi
1241
          fi
1242
        done
1243
      done
1244
    fi
1245
  done
1246

    
1247
  if [[ "$DNS_EXTRA_WAIT" -gt 0 && "$PREVIOUSLY_VALIDATED" != "true" ]]; then
1248
    info "sleeping $DNS_EXTRA_WAIT seconds before asking the ACME server to check the dns"
1249
    sleep "$DNS_EXTRA_WAIT"
1250
  fi
1251

    
1252
  # loop through dns-variable files to let the ACME server check the challenges
1253
  for dnsfile in "$TEMP_DIR"/dns_verify/*; do
1254
    if [[ -e "$dnsfile" ]]; then
1255
      debug "loading DNSfile: $dnsfile"
1256
      # shellcheck source=/dev/null
1257
      . "$dnsfile"
1258

    
1259
      check_challenge_completion "$uri" "$d" "$keyauthorization"
1260

    
1261
      debug "remove DNS entry"
1262
      # shellcheck disable=SC2018,SC2019
1263
      lower_d=$(echo "$d" | tr A-Z a-z)
1264
      eval "$DNS_DEL_COMMAND" "$lower_d" "$auth_key"
1265
      # remove $dnsfile after each loop.
1266
      rm -f "$dnsfile"
1267
    fi
1268
  done
1269
fi
1270
# end of ... perform validation if via DNS challenge
1271
#end of varify each domain.
1272
}
1273

    
1274
get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns )
1275
  orig_gad_d="$1" # domain name
1276
  gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER
1277
  if [[ -n "$gad_s" ]]; then
1278
    gad_s="@$gad_s"
1279
  fi
1280

    
1281
  if [[ "$os" == "cygwin" ]]; then
1282
    gad_d="$orig_gad_d"
1283
    # shellcheck disable=SC2086
1284
    all_auth_dns_servers=$(nslookup -type=soa "${d}" ${PUBLIC_DNS_SERVER} 2>/dev/null \
1285
                          | grep "primary name server" \
1286
                          | awk '{print $NF}')
1287
    if [[ -z "$all_auth_dns_servers" ]]; then
1288
      error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config"
1289
    fi
1290
    if [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then
1291
      primary_ns="$all_auth_dns_servers $PUBLIC_DNS_SERVER"
1292
    else
1293
      primary_ns="$all_auth_dns_servers"
1294
    fi
1295
    return
1296
  fi
1297

    
1298
  if [[ -n "$HAS_DIG_OR_DRILL" ]]; then
1299
    gad_d="$orig_gad_d"
1300
    # Use SOA +trace to find the name server
1301
    if [[ $_TEST_SKIP_SOA_CALL == 0 ]]; then
1302
      if [[ "$HAS_DIG_OR_DRILL" == "dig" ]]; then
1303
        debug Using "$HAS_DIG_OR_DRILL SOA +trace +nocomments $gad_d $gad_s" to find primary nameserver
1304
        test_output "Using $HAS_DIG_OR_DRILL SOA"
1305
        res=$($HAS_DIG_OR_DRILL SOA +trace +nocomments "$gad_d" "$gad_s" 2>/dev/null | grep "IN\WNS\W")
1306
      else
1307
        debug Using "$HAS_DIG_OR_DRILL -T $gad_d $gad_s" to find primary nameserver
1308
        test_output "Using $HAS_DIG_OR_DRILL SOA"
1309
        res=$($HAS_DIG_OR_DRILL -T SOA "$gad_d" "$gad_s" 2>/dev/null | grep "IN\WNS\W")
1310
      fi
1311
    fi
1312

    
1313
    # Check if domain is a CNAME
1314
    if [[ -z "$res" ]]; then
1315
      test_output "Using $HAS_DIG_OR_DRILL CNAME"
1316

    
1317
      # Two options here; either dig CNAME will return the CNAME and the NS or just the CNAME
1318
      debug Checking for CNAME using "$HAS_DIG_OR_DRILL CNAME $gad_d $gad_s"
1319
      res=$($HAS_DIG_OR_DRILL CNAME "$gad_d" "$gad_s"| grep "^$gad_d")
1320
      cname=$(echo "$res"| awk '$4 ~ "CNAME" {print $5}' |sed 's/\.$//g')
1321

    
1322
      if [[ $_TEST_SKIP_CNAME_CALL == 0 ]]; then
1323
        debug Checking if CNAME result contains NS records
1324
        res=$($HAS_DIG_OR_DRILL CNAME "$gad_d" "$gad_s"| grep -E "IN\W(NS|SOA)\W")
1325
      else
1326
        res=""
1327
      fi
1328

    
1329
      if [[ -n "$cname" ]]; then # domain is a CNAME so get main domain
1330
        debug Domain is a CNAME, actual domain is "$cname"
1331
      fi
1332
    fi
1333

    
1334
    # Query for NS records
1335
    if [[ -z "$res" ]]; then
1336
      test_output "Using $HAS_DIG_OR_DRILL NS"
1337
      debug Using "$HAS_DIG_OR_DRILL NS $gad_d $gad_s" to find primary nameserver
1338
      res=$($HAS_DIG_OR_DRILL NS "$gad_d" $gad_s | grep -E "IN\W(NS|SOA)\W")
1339
    fi
1340

    
1341
    if [[ -n "$res" ]]; then
1342
      # Convert dig output into an array of nameservers
1343
      IFS=$'\n' read -r -d '' -a ns_servers < <(echo "$res" | awk '$4 ~ "(NS|SOA)" {print $5}' | sed 's/\.$//g')
1344

    
1345
      # Nameservers from SOA +trace includes root and all intermediate servers, so just use all the ones with the same domain as the last name server
1346
      # i.e. if we have root, google, duckdns1, duckdns2 then return all the duckdns servers
1347
      ns_domain=${ns_servers[${#ns_servers[@]} -1 ]#*.}
1348
      all_auth_dns_servers=""
1349
      for i in "${ns_servers[@]}"; do
1350
        if [[ $i =~ $ns_domain ]]; then
1351
          all_auth_dns_servers="$all_auth_dns_servers $i"
1352
        fi
1353
      done
1354

    
1355
      if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then
1356
        primary_ns="$all_auth_dns_servers"
1357
      else
1358
        primary_ns=$(echo "$all_auth_dns_servers" | awk '{print " " $1}')
1359
      fi
1360

    
1361
      if [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then
1362
        primary_ns="$primary_ns $PUBLIC_DNS_SERVER"
1363
      fi
1364

    
1365
      debug set primary_ns = "$primary_ns"
1366
      test_output set primary_ns ="$primary_ns"
1367
      return
1368
    fi
1369
  fi
1370

    
1371
  if [[ "$HAS_HOST" == "true" ]]; then
1372
    gad_d="$orig_gad_d"
1373
    debug Using "host -t NS" to find primary name server for "$gad_d"
1374
    if [[ -z "$gad_s" ]]; then
1375
      res=$(host -t NS "$gad_d"| grep "name server")
1376
    else
1377
      res=$(host -t NS "$gad_d" "$gad_s"| grep "name server")
1378
    fi
1379
    if [[ -n "$res" ]]; then
1380
      all_auth_dns_servers=$(echo "$res" | awk '{print $4}' | sed 's/\.$//g'|tr '\n' ' ')
1381
      if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then
1382
        primary_ns="$all_auth_dns_servers"
1383
      else
1384
        primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}')
1385
      fi
1386

    
1387
      if [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then
1388
        primary_ns="$primary_ns $PUBLIC_DNS_SERVER"
1389
      fi
1390

    
1391
      return
1392
    fi
1393
  fi
1394

    
1395
  if [[ "$HAS_NSLOOKUP" == "true" ]]; then
1396
    gad_d="$orig_gad_d"
1397
    debug Using "nslookup -debug -type=soa -type=ns $gad_d $gad_s" to find primary name server
1398
    # shellcheck disable=SC2086
1399
    res=$(nslookup -debug -type=soa -type=ns "$gad_d" ${gad_s})
1400

    
1401
    if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then
1402
      # this is a Non-authoritative server, need to check for an authoritative one.
1403
      gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g')
1404
      if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then
1405
        # if domain name doesn't exist, then find auth servers for next level up
1406
        gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }')
1407
        gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}')
1408
        # handle scenario where awk returns nothing
1409
        if [[ -z "$gad_d" ]]; then
1410
          gad_d="$orig_gad_d"
1411
        fi
1412
      fi
1413

    
1414
      # shellcheck disable=SC2086
1415
      res=$(nslookup -debug -type=soa -type=ns "$gad_d" ${gad_s})
1416
    fi
1417

    
1418
    if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then
1419
      gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g')
1420
    elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then
1421
      gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }')
1422
      gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}')
1423
      # handle scenario where awk returns nothing
1424
      if [[ -z "$gad_d" ]]; then
1425
        gad_d="$orig_gad_d"
1426
      fi
1427
    fi
1428

    
1429
    # shellcheck disable=SC2086
1430
    # not quoting gad_s fixes the nslookup: couldn't get address for '': not found warning (#332)
1431
    all_auth_dns_servers=$(nslookup -debug -type=soa -type=ns "$gad_d" $gad_s \
1432
                          | awk '$1 ~ "nameserver" {print $3}' \
1433
                          | sed 's/\.$//g'| tr '\n' ' ')
1434

    
1435
    if [[ -n "$all_auth_dns_servers" ]]; then
1436
      if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then
1437
        primary_ns="$all_auth_dns_servers"
1438
      else
1439
        primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}')
1440
      fi
1441

    
1442
      if [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then
1443
        primary_ns="$primary_ns $PUBLIC_DNS_SERVER"
1444
      fi
1445
      return
1446
    fi
1447
  fi
1448

    
1449
  # nslookup on alpine/ubuntu containers doesn't support -debug, print a warning in this case
1450
  # This means getssl cannot check that the DNS record has been updated on the primary name server
1451
  info "Warning: Couldn't find primary DNS server - please set PUBLIC_DNS_SERVER or AUTH_DNS_SERVER in config"
1452
  info "This means getssl cannot check the DNS entry has been updated"
1453
}
1454

    
1455
get_certificate() { # get certificate for csr, if all domains validated.
1456
  gc_csr=$1         # the csr file
1457
  gc_certfile=$2    # The filename for the certificate
1458
  gc_cafile=$3      # The filename for the CA certificate
1459
  gc_fullchain=$4   # The filename for the fullchain
1460

    
1461
  der=$(openssl req -in "$gc_csr" -outform DER | urlbase64)
1462
  if [[ $API -eq 1 ]]; then
1463
    send_signed_request "$URL_new_cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64"
1464
    # convert certificate information into correct format and save to file.
1465
    CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r')
1466
    if [[ "$CertData" ]] ; then
1467
      echo -----BEGIN CERTIFICATE----- > "$gc_certfile"
1468
      curl --user-agent "$CURL_USERAGENT" --silent "$CertData" | openssl base64 -e  >> "$gc_certfile"
1469
      echo -----END CERTIFICATE-----  >> "$gc_certfile"
1470
      info "Certificate saved in $CERT_FILE"
1471
    fi
1472

    
1473
    # If certificate wasn't a valid certificate, error exit.
1474
    if [[ -z "$CertData" ]] ; then
1475
      response2=$(echo "$response" | fold -w64 |openssl base64 -d)
1476
      debug "response was $response"
1477
      error_exit "Sign failed: $(echo "$response2" | grep "detail")"
1478
    fi
1479

    
1480
    # get a copy of the CA certificate.
1481
    IssuerData=$(grep -i '^Link' "$CURL_HEADER" \
1482
                | cut -d " " -f 2\
1483
                | cut -d ';' -f 1 \
1484
                | sed 's/<//g' \
1485
                | sed 's/>//g')
1486
    if [[ "$IssuerData" ]] ; then
1487
      echo -----BEGIN CERTIFICATE----- > "$gc_cafile"
1488
      curl --user-agent "$CURL_USERAGENT" --silent "$IssuerData" | openssl base64 -e  >> "$gc_cafile"
1489
      echo -----END CERTIFICATE-----  >> "$gc_cafile"
1490
      info "The intermediate CA cert is in $gc_cafile"
1491
    fi
1492
  else # APIv2
1493
    info "Requesting Finalize Link"
1494
    send_signed_request "$FinalizeLink" "{\"csr\": \"$der\"}" "needbase64"
1495
    info Requesting Order Link
1496
    debug "order link was $OrderLink"
1497
    send_signed_request "$OrderLink" ""
1498
    # if ACME response is processing (still creating certificates) then wait and try again.
1499
    while [[ "$response_status" == "processing" ]]; do
1500
      info "ACME server still Processing certificates"
1501
      sleep 5
1502
      send_signed_request "$OrderLink" ""
1503
    done
1504
    info "Requesting certificate"
1505
    CertData=$(json_get "$response" "certificate")
1506
    send_signed_request "$CertData" "" "" "$gc_fullchain"
1507
    info "Full certificate saved in $gc_fullchain"
1508
    awk -v CERT_FILE="$gc_certfile" -v CA_CERT="$gc_cafile" 'BEGIN {outfile=CERT_FILE} split_after==1 {outfile=CA_CERT;split_after=0} /-----END CERTIFICATE-----/ {split_after=1} {print > outfile}' "$gc_fullchain"
1509
    info "Certificate saved in $gc_certfile"
1510
  fi
1511
}
1512

    
1513
get_cr() { # get curl response
1514
  url="$1"
1515
  debug url "$url"
1516
  response=$(curl --user-agent "$CURL_USERAGENT" --silent "$url")
1517
  ret=$?
1518
  debug response  "$response"
1519
  code=$(json_get "$response" status)
1520
  debug code "$code"
1521
  debug "get_cr return code $ret"
1522
  return $ret
1523
}
1524

    
1525
get_os() { # function to get the current Operating System
1526
  uname_res=$(uname -s)
1527
  if [[ $(date -h 2>&1 | grep -ic busybox) -gt 0 ]]; then
1528
    os="busybox"
1529
  elif [[ ${uname_res} == "Linux" ]]; then
1530
    os="linux"
1531
  elif [[ ${uname_res} == "FreeBSD" ]]; then
1532
    os="bsd"
1533
  elif [[ ${uname_res} == "Darwin" ]]; then
1534
    os="mac"
1535
  elif [[ ${uname_res:0:6} == "CYGWIN" ]]; then
1536
    os="cygwin"
1537
  elif [[ ${uname_res:0:5} == "MINGW" ]]; then
1538
    os="mingw"
1539
  else
1540
    os="unknown"
1541
  fi
1542
  debug "detected os type = $os"
1543
  if [[ -f /etc/issue ]]; then
1544
    debug "Running $(cat /etc/issue)"
1545
  fi
1546
}
1547

    
1548
get_signing_params() { # get signing parameters from key
1549
  skey=$1
1550
  if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key
1551
    pub_exp64=$(openssl rsa -in "${skey}" -noout -text \
1552
                | grep publicExponent \
1553
                | grep -oE "0x[a-f0-9]+" \
1554
                | cut -d'x' -f2 \
1555
                | hex2bin \
1556
                | urlbase64)
1557
    pub_mod64=$(openssl rsa -in "${skey}" -noout -modulus \
1558
                | cut -d'=' -f2 \
1559
                | hex2bin \
1560
                | urlbase64)
1561

    
1562
    jwk='{"e":"'"${pub_exp64}"'","kty":"RSA","n":"'"${pub_mod64}"'"}'
1563
    jwkalg="RS256"
1564
    signalg="sha256"
1565
  elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key.
1566
    crv="$(openssl ec -in  "$skey" -noout -text 2>/dev/null | awk '$2 ~ "CURVE:" {print $3}')"
1567
    if [[ -z "$crv" ]]; then
1568
      gsp_keytype="$(openssl ec -in  "$skey" -noout -text 2>/dev/null \
1569
                     | grep "^ASN1 OID:" \
1570
                     | awk '{print $3}')"
1571
      case "$gsp_keytype" in
1572
        prime256v1) crv="P-256" ;;
1573
        secp384r1) crv="P-384" ;;
1574
        secp521r1) crv="P-521" ;;
1575
        *) error_exit "invalid curve algorithm type $gsp_keytype";;
1576
      esac
1577
    fi
1578
    case "$crv" in
1579
      P-256) jwkalg="ES256" ; signalg="sha256" ;;
1580
      P-384) jwkalg="ES384" ; signalg="sha384" ;;
1581
      P-521) jwkalg="ES512" ; signalg="sha512" ;;
1582
      *) error_exit "invalid curve algorithm type $crv";;
1583
    esac
1584
    pubtext="$(openssl ec  -in "$skey"  -noout -text 2>/dev/null \
1585
               | awk '/^pub:/{p=1;next}/^ASN1 OID:/{p=0}p' \
1586
               | tr -d ": \n\r")"
1587
    mid=$(( (${#pubtext} -2) / 2 + 2 ))
1588
    x64=$(echo "$pubtext" | cut -b 3-$mid | hex2bin | urlbase64)
1589
    y64=$(echo "$pubtext" | cut -b $((mid+1))-${#pubtext} | hex2bin | urlbase64)
1590
    jwk='{"crv":"'"$crv"'","kty":"EC","x":"'"$x64"'","y":"'"$y64"'"}'
1591
  else
1592
    error_exit "Invalid key file"
1593
  fi
1594
  thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)"
1595
  debug "jwk alg = $jwkalg"
1596
}
1597

    
1598
graceful_exit() { # normal exit function.
1599
  exit_code=$1
1600
  clean_up
1601
  # shellcheck disable=SC2086
1602
  exit $exit_code
1603
}
1604

    
1605
help_message() { # print out the help message
1606
  cat <<- _EOF_
1607
	$PROGNAME ver. $VERSION
1608
	Obtain SSL certificates from the letsencrypt.org ACME server
1609

    
1610
	$(usage)
1611

    
1612
	Options:
1613
	  -a, --all          Check all certificates
1614
	  -d, --debug        Output debug information
1615
	  -c, --create       Create default config files
1616
	  -f, --force        Force renewal of cert (overrides expiry checks)
1617
	  -h, --help         Display this help message and exit
1618
	  -i, --install      Install certificates and reload service
1619
	  -q, --quiet        Quiet mode (only outputs on error, success of new cert, or getssl was upgraded)
1620
	  -Q, --mute         Like -q, but also mute notification about successful upgrade
1621
	  -r, --revoke   "cert" "key" [CA_server] Revoke a certificate (the cert and key are required)
1622
	  -u, --upgrade      Upgrade getssl if a more recent version is available
1623
	  -k, --keep     "#" Maximum number of old getssl versions to keep when upgrading
1624
	  -U, --nocheck      Do not check if a more recent version is available
1625
	  -w working_dir "Working directory"
1626

    
1627
	_EOF_
1628
}
1629

    
1630
hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char
1631
#  printf -- "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
1632
  echo -e -n "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')"
1633
}
1634

    
1635
info() { # write out info as long as the quiet flag has not been set.
1636
  if [[ ${_QUIET} -eq 0 ]]; then
1637
    echo "$@"
1638
  fi
1639
}
1640

    
1641
json_awk() { # AWK json converter used for API2 - needs tidying up ;)
1642
# shellcheck disable=SC2086
1643
echo "$1" | tr -d '\n' | awk '
1644
{
1645
  tokenize($0) # while(get_token()) {print TOKEN}
1646
  if (0 == parse()) {
1647
    apply(JPATHS, NJPATHS)
1648
  }
1649
}
1650

    
1651
function apply (ary,size,i) {
1652
  for (i=1; i<size; i++)
1653
    print ary[i]
1654
}
1655

    
1656
function get_token() {
1657
  TOKEN = TOKENS[++ITOKENS] # for internal tokenize()
1658
  return ITOKENS < NTOKENS
1659
}
1660

    
1661
function parse_array(a1,idx,ary,ret) {
1662
  idx=0
1663
  ary=""
1664
  get_token()
1665
  if (TOKEN != "]") {
1666
    while (1) {
1667
      if (ret = parse_value(a1, idx)) {
1668
        return ret
1669
      }
1670
      idx=idx+1
1671
      ary=ary VALUE
1672
      get_token()
1673
      if (TOKEN == "]") {
1674
        break
1675
      } else if (TOKEN == ",") {
1676
        ary = ary ","
1677
      } else {
1678
        report(", or ]", TOKEN ? TOKEN : "EOF")
1679
        return 2
1680
      }
1681
      get_token()
1682
    }
1683
  }
1684
  VALUE=""
1685
  return 0
1686
}
1687

    
1688
function parse_object(a1,key,obj) {
1689
  obj=""
1690
  get_token()
1691
  if (TOKEN != "}") {
1692
    while (1) {
1693
      if (TOKEN ~ /^".*"$/) {
1694
        key=TOKEN
1695
      } else {
1696
        report("string", TOKEN ? TOKEN : "EOF")
1697
        return 3
1698
      }
1699
      get_token()
1700
      if (TOKEN != ":") {
1701
        report(":", TOKEN ? TOKEN : "EOF")
1702
        return 4
1703
      }
1704
      get_token()
1705
      if (parse_value(a1, key)) {
1706
        return 5
1707
      }
1708
      obj=obj key ":" VALUE
1709
      get_token()
1710
      if (TOKEN == "}") {
1711
        break
1712
      } else if (TOKEN == ",") {
1713
        obj=obj ","
1714
      } else {
1715
        report(", or }", TOKEN ? TOKEN : "EOF")
1716
        return 6
1717
      }
1718
      get_token()
1719
    }
1720
  }
1721
  VALUE=""
1722
  return 0
1723
}
1724

    
1725

    
1726
function parse_value(a1, a2,   jpath,ret,x) {
1727
  jpath=(a1!="" ? a1 "," : "") a2 # "${1:+$1,}$2"
1728
  if (TOKEN == "{") {
1729
    if (parse_object(jpath)) {
1730
      return 7
1731
    }
1732
  } else if (TOKEN == "[") {
1733
    if (ret = parse_array(jpath)) {
1734
      return ret
1735
    }
1736
  } else if (TOKEN == "") { #test case 20150410 #4
1737
    report("value", "EOF")
1738
    return 9
1739
  } else if (TOKEN ~ /^([^0-9])$/) {
1740
    # At this point, the only valid single-character tokens are digits.
1741
    report("value", TOKEN)
1742
    return 9
1743
  } else {
1744
    VALUE=TOKEN
1745
  }
1746
  if (! ("" == jpath || "" == VALUE)) {
1747
    x=sprintf("[%s]\t%s", jpath, VALUE)
1748
    print x
1749
  }
1750
  return 0
1751
}
1752

    
1753
function parse(   ret) {
1754
  get_token()
1755
  if (ret = parse_value()) {
1756
    return ret
1757
  }
1758
  if (get_token()) {
1759
    report("EOF", TOKEN)
1760
    return 11
1761
  }
1762
  return 0
1763
}
1764

    
1765
function report(expected, got,   i,from,to,context) {
1766
  from = ITOKENS - 10; if (from < 1) from = 1
1767
  to = ITOKENS + 10; if (to > NTOKENS) to = NTOKENS
1768
  for (i = from; i < ITOKENS; i++)
1769
    context = context sprintf("%s ", TOKENS[i])
1770
  context = context "<<" got ">> "
1771
  for (i = ITOKENS + 1; i <= to; i++)
1772
    context = context sprintf("%s ", TOKENS[i])
1773
  scream("json_awk expected <" expected "> but got <" got "> at input token " ITOKENS "\n" context)
1774
}
1775

    
1776
function reset() {
1777
  TOKEN=""; delete TOKENS; NTOKENS=ITOKENS=0
1778
  delete JPATHS; NJPATHS=0
1779
  VALUE=""
1780
}
1781

    
1782
function scream(msg) {
1783
  FAILS[FILENAME] = FAILS[FILENAME] (FAILS[FILENAME]!="" ? "\n" : "") msg
1784
  msg = FILENAME ": " msg
1785
  print msg >"/dev/stderr"
1786
}
1787

    
1788
function tokenize(a1,pq,pb,ESCAPE,CHAR,STRING,NUMBER,KEYWORD,SPACE) {
1789
  SPACE="[ \t\n]+"
1790
  gsub(/"[^\001-\037"\\]*((\\[^u\001-\037]|\\u[0-9a-fA-F][0-9a-fA-F][0-9a-fA-F][0-9a-fA-F])[^\001-\037"\\]*)*"|-?(0|[1-9][0-9]*)([.][0-9]*)?([eE][+-]?[0-9]*)?|null|false|true|[ \t\n]+|./, "\n&", a1)
1791
  gsub("\n" SPACE, "\n", a1)
1792
  sub(/^\n/, "", a1)
1793
  ITOKENS=0 # get_token() helper
1794
  return NTOKENS = split(a1, TOKENS, /\n/)
1795
}'
1796
}
1797

    
1798
json_get() { # get values from json
1799
  if [[ -z "$1" ]] || [[ "$1" == "null" ]]; then
1800
    echo "json was blank"
1801
    return
1802
  fi
1803
  if [[ $API = 1 ]]; then
1804
    # remove newlines, so it's a single chunk of JSON
1805
    json_data=$( echo "$1" | tr '\n' ' ')
1806
    # if $3 is defined, this is the section which the item is in.
1807
    if [[ -n "$3" ]]; then
1808
      jg_section=$(echo "$json_data" | awk -F"[}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${3}"'\"/){print $i}}}')
1809
      if [[ "$2" == "uri" ]]; then
1810
        jg_subsect=$(echo "$jg_section" | awk -F"[,]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i)}}}')
1811
        jg_result=$(echo "$jg_subsect" | awk -F'"' '{print $4}')
1812
      else
1813
        jg_result=$(echo "$jg_section" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}')
1814
      fi
1815
    else
1816
      jg_result=$(echo "$json_data" |awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}')
1817
    fi
1818
    # check number of quotes
1819
    jg_q=${jg_result//[^\"]/}
1820
    # if 2 quotes, assume it's a quoted variable and just return the data within the quotes.
1821
    if [[ ${#jg_q} -eq 2 ]]; then
1822
      echo "$jg_result" | awk -F'"' '{print $2}'
1823
    else
1824
      echo "$jg_result"
1825
    fi
1826
  else
1827
    if [[ -n "$6" ]]; then
1828
      full=$(json_awk "$1")
1829
      section=$(echo "$full" | grep "\"$2\"" | grep "\"$3\"" | grep "\"$4\"" | awk -F"," '{print $2}')
1830
      echo "$full" | grep "^..${5}\",$section\]" | awk '{print $2}' | tr -d '"'
1831
    elif [[ -n "$5" ]]; then
1832
      full=$(json_awk "$1")
1833
      section=$(echo "$full" | grep "\"$2\"" | grep "\"$3\"" | grep "\"$4\"" | awk -F"," '{print $2}')
1834
      echo "$full" | grep "^..${2}\",$section" | grep "$5" | awk '{print $2}' | tr -d '"'
1835
    elif [[ -n "$3" ]]; then
1836
      json_awk "$1" | grep "^..${2}...${3}" | awk '{print $2}' | tr -d '"'
1837
    elif [[ -n "$2" ]]; then
1838
      json_awk "$1" | grep "^..${2}" | awk '{print $2}' | tr -d '"'
1839
    else
1840
      json_awk "$1"
1841
    fi
1842
  fi
1843
}
1844

    
1845
obtain_ca_resource_locations()
1846
{
1847
  for suffix in "" "/directory" "/dir";
1848
  do
1849
    # Obtain CA resource locations
1850
    ca_all_loc=$(curl --user-agent "$CURL_USERAGENT" "${CA}${suffix}" 2>/dev/null)
1851
    debug "ca_all_loc from ${CA}${suffix} gives $ca_all_loc"
1852
    # APIv1
1853
    URL_new_reg=$(echo "$ca_all_loc" | grep "new-reg" | awk -F'"' '{print $4}')
1854
    URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}')
1855
    URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}')
1856
    #API v2
1857
    URL_newAccount=$(echo "$ca_all_loc" | grep "newAccount" | awk -F'"' '{print $4}')
1858
    URL_newNonce=$(echo "$ca_all_loc" | grep "newNonce" | awk -F'"' '{print $4}')
1859
    URL_newOrder=$(echo "$ca_all_loc" | grep "newOrder" | awk -F'"' '{print $4}')
1860
    URL_revoke=$(echo "$ca_all_loc" | grep "revokeCert" | awk -F'"' '{print $4}')
1861

    
1862
    if [[ -n "$URL_new_reg" ]] || [[ -n "$URL_newAccount" ]]; then
1863
      break
1864
    fi
1865
  done
1866

    
1867
  if [[ -n "$URL_new_reg" ]]; then
1868
    API=1
1869
  elif [[ -n "$URL_newAccount" ]]; then
1870
    API=2
1871
  else
1872
    error_exit "unknown API version"
1873
  fi
1874
  debug "Using API v$API"
1875
}
1876

    
1877
os_esed() { # Use different sed version for different os types (extended regex)
1878
  if [[ "$os" == "bsd" ]]; then # BSD requires -E flag for extended regex
1879
    sed -E "${@}"
1880
  elif [[ "$os" == "mac" ]]; then # MAC uses older BSD style sed.
1881
    sed -E "${@}"
1882
  else
1883
    sed -r "${@}"
1884
  fi
1885
}
1886

    
1887
purge_archive() { # purge archive of old, invalid, certificates
1888
  arcdir="$1/archive"
1889
  debug "purging archives in ${arcdir}/"
1890
  for padir in "$arcdir"/????_??_??_??_??; do
1891
    # check each directory
1892
    if [[ -d "$padir" ]]; then
1893
      tstamp=$(basename "$padir"| awk -F"_" '{print $1"-"$2"-"$3" "$4":"$5}')
1894
      if [[ "$os" == "bsd" ]]; then
1895
        direpoc=$(date -j -f "%F %H:%M" "$tstamp" +%s)
1896
      elif [[ "$os" == "mac" ]]; then
1897
        direpoc=$(date -j -f "%F %H:%M" "$tstamp" +%s)
1898
      else
1899
        direpoc=$(date -d "$tstamp" +%s)
1900
      fi
1901
      current_epoc=$(date "+%s")
1902
      # as certs currently valid for 90 days, purge anything older than 100
1903
      purgedate=$((current_epoc - 60*60*24*100))
1904
      if [[ "$direpoc" -lt "$purgedate" ]]; then
1905
        echo "purge $padir"
1906
        rm -rf "${padir:?}"
1907
      fi
1908
    fi
1909
  done
1910
}
1911

    
1912
reload_service() {  # Runs a command to reload services ( via ssh if needed)
1913
  if [[ -n "$RELOAD_CMD" ]]; then
1914
    info "reloading SSL services"
1915
    if [[ "${RELOAD_CMD:0:4}" == "ssh:" ]] ; then
1916
      sshhost=$(echo "$RELOAD_CMD"| awk -F: '{print $2}')
1917
      command=${RELOAD_CMD:(( ${#sshhost} + 5))}
1918
      debug "running following command to reload cert"
1919
      debug "ssh $SSH_OPTS $sshhost ${command}"
1920
      # shellcheck disable=SC2029
1921
      # shellcheck disable=SC2086
1922
      ssh $SSH_OPTS "$sshhost" "${command}" 1>/dev/null 2>&1
1923
      # allow 2 seconds for services to restart
1924
      sleep 2
1925
    else
1926
      debug "running reload command $RELOAD_CMD"
1927
      if ! eval "$RELOAD_CMD" ; then
1928
        error_exit "error running $RELOAD_CMD"
1929
      fi
1930
    fi
1931
  fi
1932
}
1933

    
1934
revoke_certificate() { # revoke a certificate
1935
  debug "revoking cert $REVOKE_CERT"
1936
  debug "using key $REVOKE_KEY"
1937
  ACCOUNT_KEY="$REVOKE_KEY"
1938
  # need to set the revoke key as "account_key" since it's used in send_signed_request.
1939
  get_signing_params "$REVOKE_KEY"
1940
  TEMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t getssl)
1941
  debug "revoking from $URL_revoke"
1942
  rcertdata=$(sed '1d;$d' "$REVOKE_CERT" | tr -d "\r\n" | tr '/+' '_-' | tr -d '= ')
1943
  send_signed_request "$URL_revoke" "{\"certificate\": \"$rcertdata\",\"reason\": $REVOKE_REASON}"
1944
  if [[ $code -eq "200" ]]; then
1945
    info "certificate revoked"
1946
  else
1947
    error_exit "Revocation failed: $(echo "$response" | grep "detail")"
1948
  fi
1949
}
1950

    
1951
requires() { # check if required function is available
1952
  if [[ "$#" -gt 1 ]]; then # if more than 1 value, check list
1953
    for i in "$@"; do
1954
      if [[ "$i" == "${!#}" ]]; then # if on last variable then exit as not found
1955
        error_exit "this script requires one of: ${*:1:$(($#-1))}"
1956
      fi
1957
      res=$(command -v "$i" 2>/dev/null)
1958
      debug "checking for $i ... $res"
1959
      if [[ -n "$res" ]]; then # if function found, then set variable to function and return
1960
        debug "function $i found at $res  - setting ${!#} to $i"
1961
        eval "${!#}=\$i"
1962
        return
1963
      fi
1964
    done
1965
  else # only one value, so check it.
1966
    result=$(command -v "$1" 2>/dev/null)
1967
    debug "checking for required $1 ... $result"
1968
    if [[ -z "$result" ]]; then
1969
      error_exit "This script requires $1 installed"
1970
    fi
1971
  fi
1972
}
1973

    
1974
set_server_type() { # uses SERVER_TYPE to set REMOTE_PORT and REMOTE_EXTRA
1975
  if [[ ${SERVER_TYPE} == "https" ]] || [[ ${SERVER_TYPE} == "webserver" ]]; then
1976
    REMOTE_PORT=443
1977
  elif [[ ${SERVER_TYPE} == "ftp" ]]; then
1978
    REMOTE_PORT=21
1979
    REMOTE_EXTRA="-starttls ftp"
1980
  elif [[ ${SERVER_TYPE} == "ftpi" ]]; then
1981
    REMOTE_PORT=990
1982
  elif [[ ${SERVER_TYPE} == "imap" ]]; then
1983
    REMOTE_PORT=143
1984
    REMOTE_EXTRA="-starttls imap"
1985
  elif [[ ${SERVER_TYPE} == "imaps" ]]; then
1986
    REMOTE_PORT=993
1987
  elif [[ ${SERVER_TYPE} == "pop3" ]]; then
1988
    REMOTE_PORT=110
1989
    REMOTE_EXTRA="-starttls pop3"
1990
  elif [[ ${SERVER_TYPE} == "pop3s" ]]; then
1991
    REMOTE_PORT=995
1992
  elif [[ ${SERVER_TYPE} == "smtp" ]]; then
1993
    REMOTE_PORT=25
1994
    REMOTE_EXTRA="-starttls smtp"
1995
  elif [[ ${SERVER_TYPE} == "smtps_deprecated" ]]; then
1996
    REMOTE_PORT=465
1997
  elif [[ ${SERVER_TYPE} == "smtps" ]] || [[ ${SERVER_TYPE} == "smtp_submission" ]]; then
1998
    REMOTE_PORT=587
1999
    REMOTE_EXTRA="-starttls smtp"
2000
  elif [[ ${SERVER_TYPE} == "xmpp" ]]; then
2001
    REMOTE_PORT=5222
2002
    REMOTE_EXTRA="-starttls xmpp"
2003
  elif [[ ${SERVER_TYPE} == "xmpps" ]]; then
2004
    REMOTE_PORT=5269
2005
  elif [[ ${SERVER_TYPE} == "ldaps" ]]; then
2006
    REMOTE_PORT=636
2007
  elif [[ ${SERVER_TYPE} =~ ^[0-9]+$ ]]; then
2008
    REMOTE_PORT=${SERVER_TYPE}
2009
  else
2010
    info "${DOMAIN}: unknown server type \"$SERVER_TYPE\" in SERVER_TYPE"
2011
    config_errors=true
2012
  fi
2013
}
2014

    
2015
send_signed_request() { # Sends a request to the ACME server, signed with your private key.
2016
  url=$1
2017
  payload=$2
2018
  needbase64=$3
2019
  outfile=$4 # save response into this file (certificate data)
2020

    
2021
  debug url "$url"
2022

    
2023
  CURL_HEADER="$TEMP_DIR/curl.header"
2024
  dp="$TEMP_DIR/curl.dump"
2025

    
2026
  CURL="curl "
2027
  # shellcheck disable=SC2072
2028
  if [[ "$($CURL -V | head -1 | cut -d' ' -f2 )" > "7.33" ]]; then
2029
    CURL="$CURL --http1.1 "
2030
  fi
2031

    
2032
  CURL="$CURL --user-agent $CURL_USERAGENT --silent --dump-header $CURL_HEADER "
2033

    
2034
  if [[ ${_USE_DEBUG} -eq 1 ]]; then
2035
    CURL="$CURL --trace-ascii $dp "
2036
  fi
2037

    
2038
  # convert payload to url base 64
2039
  payload64="$(printf '%s' "${payload}" | urlbase64)"
2040

    
2041
  # get nonce from ACME server
2042
  if [[ $API -eq 1 ]]; then
2043
    nonceurl="$CA/directory"
2044
    nonce=$($CURL -I "$nonceurl" | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ')
2045
  else # APIv2
2046
    nonce=$($CURL -I "$URL_newNonce" | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ')
2047
  fi
2048

    
2049
  nonceproblem="true"
2050
  while [[ "$nonceproblem" == "true" ]]; do
2051

    
2052
    # Build header with just our public key and algorithm information
2053
    header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}'
2054

    
2055
    # Build another header which also contains the previously received nonce and encode it as urlbase64
2056
    if [[ $API -eq 1 ]]; then
2057
      protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}'
2058
      protected64="$(printf '%s' "${protected}" | urlbase64)"
2059
    else # APIv2
2060
      if [[ -z "$KID" ]]; then
2061
        debug "KID is blank, so using jwk"
2062
        protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}'
2063
        protected64="$(printf '%s' "${protected}" | urlbase64)"
2064
      else
2065
        debug "using KID=${KID}"
2066
        protected="{\"alg\": \"$jwkalg\", \"kid\": \"$KID\",\"nonce\": \"${nonce}\", \"url\": \"${url}\"}"
2067
        protected64="$(printf '%s' "${protected}" | urlbase64)"
2068
      fi
2069
    fi
2070

    
2071
    # Sign header with nonce and our payload with our private key and encode signature as urlbase64
2072
    sign_string "$(printf '%s' "${protected64}.${payload64}")"  "${ACCOUNT_KEY}" "$signalg"
2073

    
2074
    # Send header + extended header + payload + signature to the acme-server
2075
    debug "payload = $payload"
2076
    if [[ $API -eq 1 ]]; then
2077
      body="{\"header\": ${header},"
2078
      body="${body}\"protected\": \"${protected64}\","
2079
      body="${body}\"payload\": \"${payload64}\","
2080
      body="${body}\"signature\": \"${signed64}\"}"
2081
    else
2082
      body="{"
2083
      body="${body}\"protected\": \"${protected64}\","
2084
      body="${body}\"payload\": \"${payload64}\","
2085
      body="${body}\"signature\": \"${signed64}\"}"
2086
    fi
2087

    
2088
    code="500"
2089
    loop_limit=5
2090
    while [[ "$code" -eq 500 ]]; do
2091
      if [[ "$outfile" ]] ; then
2092
        $CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" > "$outfile"
2093
        errcode=$?
2094
        response=$(cat "$outfile")
2095
      elif [[ "$needbase64" ]] ; then
2096
        response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" | urlbase64)
2097
        errcode=$?
2098
      else
2099
        response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url")
2100
        errcode=$?
2101
      fi
2102

    
2103
      if [[ $errcode -gt 0 || ( "$response" == "" && $url != *"revoke"* ) ]]; then
2104
        error_exit "ERROR curl \"$url\" failed with $errcode and returned $response"
2105
      fi
2106

    
2107
      responseHeaders=$(cat "$CURL_HEADER")
2108
      if [[ "$needbase64" && ${response##*()} != "{"* ]]; then
2109
        # response is in base64 too, decode
2110
        response=$(urlbase64_decode "$response")
2111
      fi
2112

    
2113
      debug responseHeaders "$responseHeaders"
2114
      debug response  "$response"
2115
      code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1)
2116
      debug code "$code"
2117
      if [[ "$code" == 4* && $response != *"error:badNonce"* && "$code" != 409 ]]; then
2118
        detail=$(echo "$response" | grep "detail")
2119
        error_exit "ACME server returned error: ${code}: ${detail}"
2120
      fi
2121

    
2122
      if [[ $API -eq 1 ]]; then
2123
        response_status=$(json_get "$response" status \
2124
                        | head -1| awk -F'"' '{print $2}')
2125
      else # APIv2
2126
        if [[ "$outfile" && "$response" ]]; then
2127
          debug "response written to $outfile"
2128
        elif [[ ${response##*()} == "{"* ]]; then
2129
          response_status=$(json_get "$response" status)
2130
        else
2131
          debug "response not in json format"
2132
          debug "$response"
2133
        fi
2134
      fi
2135
      debug "response status = $response_status"
2136
      if [[ "$code" -eq 500 ]]; then
2137
        info "error on acme server - trying again ...."
2138
        debug "loop_limit = $loop_limit"
2139
        sleep 5
2140
        loop_limit=$((loop_limit - 1))
2141
        if [[ $loop_limit -lt 1 ]]; then
2142
          error_exit "500 error from ACME server:  $response"
2143
        fi
2144
      fi
2145
    done
2146
    if [[ $response == *"error:badNonce"* ]]; then
2147
      debug "bad nonce"
2148
      nonce=$(echo "$responseHeaders" | grep -i "^replay-nonce:" | awk '{print $2}' | tr -d '\r\n ')
2149
      debug "trying new nonce $nonce"
2150
    else
2151
      nonceproblem="false"
2152
    fi
2153
  done
2154
}
2155

    
2156
sign_string() { # sign a string with a given key and algorithm and return urlbase64
2157
                # sets the result in variable signed64
2158
  str=$1
2159
  key=$2
2160
  signalg=$3
2161

    
2162
  if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key
2163
    signed64="$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" | urlbase64)"
2164
  elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key.
2165
    # ECDSA signature width
2166
    # e.g. 521 bits requires 66 bytes to express, a signature consists of 2 integers so 132 bytes
2167
    # https://crypto.stackexchange.com/questions/12299/ecc-key-size-and-signature-size/
2168
    if [ "$signalg" = "sha256" ]; then
2169
      w=64
2170
    elif [ "$signalg" = "sha384" ]; then
2171
      w=96
2172
    elif [ "$signalg" = "sha512" ]; then
2173
      w=132
2174
    else
2175
      error_exit "Unknown signing algorithm $signalg"
2176
    fi
2177
    asn1parse=$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" | openssl asn1parse -inform DER)
2178
    #shellcheck disable=SC2086
2179
    R=$(echo $asn1parse | awk '{ print $13 }' | cut -c2-)
2180
    debug "R $R"
2181
    #shellcheck disable=SC2086
2182
    S=$(echo $asn1parse | awk '{ print $20 }' | cut -c2-)
2183
    debug "S $S"
2184

    
2185
    # pad R and S to the correct length for the signing algorithm
2186
    signed64=$(printf "%${w}s%${w}s" "${R}" "${S}" | tr ' ' '0' | hex2bin | urlbase64 )
2187
    debug "encoded RS $signed64"
2188
  fi
2189
}
2190

    
2191
signal_exit() { # Handle trapped signals
2192
  case $1 in
2193
    INT)
2194
      error_exit "Program interrupted by user" ;;
2195
    TERM)
2196
      echo -e "\n$PROGNAME: Program terminated" >&2
2197
      graceful_exit ;;
2198
    *)
2199
      error_exit "$PROGNAME: Terminating on unknown signal" ;;
2200
  esac
2201
}
2202

    
2203
urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and '/' replaced with '_'
2204
  openssl base64 -e | tr -d '\n\r' | os_esed -e 's:=*$::g' -e 'y:+/:-_:'
2205
}
2206

    
2207
# base64url decode
2208
# From: https://gist.github.com/alvis/89007e96f7958f2686036d4276d28e47
2209
urlbase64_decode() {
2210
  INPUT=$1 # $(if [ -z "$1" ]; then echo -n $(cat -); else echo -n "$1"; fi)
2211
  MOD=$(($(echo -n "$INPUT" | wc -c) % 4))
2212
  PADDING=$(if [ $MOD -eq 2 ]; then echo -n '=='; elif [ $MOD -eq 3 ]; then echo -n '=' ; fi)
2213
  echo -n "$INPUT$PADDING" |
2214
    sed s/-/+/g |
2215
    sed s/_/\\//g |
2216
    openssl base64 -d -A
2217
}
2218

    
2219
usage() { # echos out the program usage
2220
  echo "Usage: $PROGNAME [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet]"\
2221
       "[-Q|--mute] [-u|--upgrade] [-k|--keep #] [-U|--nocheck] [-r|--revoke cert key] [-w working_dir] domain"
2222
}
2223

    
2224
write_domain_template() { # write out a template file for a domain.
2225
  if [[ -s "$WORKING_DIR/getssl_default.cfg" ]]; then
2226
  	export DOMAIN="$DOMAIN"
2227
  	export EX_SANS="$EX_SANS"
2228
  	envsubst < "$WORKING_DIR/getssl_default.cfg"  > "$1"
2229
  else
2230
    cat > "$1" <<- _EOF_domain_
2231
		# vim: filetype=sh
2232
		#
2233
		# This file is read second (and per domain if running with the -a option)
2234
		# and overwrites any settings from the first file
2235
		#
2236
		# Uncomment and modify any variables you need
2237
		# see https://github.com/srvrco/getssl/wiki/Config-variables for details
2238
		# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs
2239
		#
2240
		# The staging server is best for testing
2241
		#CA="https://acme-staging-v02.api.letsencrypt.org"
2242
		# This server issues full certificates, however has rate limits
2243
		#CA="https://acme-v02.api.letsencrypt.org"
2244

    
2245
		# Private key types - can be rsa, prime256v1, secp384r1 or secp521r1
2246
		#PRIVATE_KEY_ALG="rsa"
2247

    
2248
		# Additional domains - this could be multiple domains / subdomains in a comma separated list
2249
		# Note: this is Additional domains - so should not include the primary domain.
2250
		SANS="${EX_SANS}"
2251

    
2252
		# Acme Challenge Location. The first line for the domain, the following ones for each additional domain.
2253
		# If these start with ssh: then the next variable is assumed to be the hostname and the rest the location.
2254
		# An ssh key will be needed to provide you with access to the remote server.
2255
		# Optionally, you can specify a different userid for ssh/scp to use on the remote server before the @ sign.
2256
		# If left blank, the username on the local server will be used to authenticate against the remote server.
2257
		# If these start with ftp: then the next variables are ftpuserid:ftppassword:servername:ACL_location
2258
		# These should be of the form "/path/to/your/website/folder/.well-known/acme-challenge"
2259
		# where "/path/to/your/website/folder/" is the path, on your web server, to the web root for your domain.
2260
		# You can also user WebDAV over HTTPS as transport mechanism. To do so, start with davs: followed by username,
2261
		# password, host, port (explicitly needed even if using default port 443) and path on the server.
2262
        # Multiple locations can be defined for a file by separating the locations with a semi-colon.
2263
		#ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge'
2264
		#     'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge'
2265
		#     'ssh:sshuserid@server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge'
2266
		#     'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge'
2267
		#     'davs:davsuserid:davspassword:{DOMAIN}:443:/web/.well-known/acme-challenge')
2268

    
2269
		# Specify SSH options, e.g. non standard port in SSH_OPTS
2270
		# (Can also use SCP_OPTS and SFTP_OPTS)
2271
		# SSH_OPTS=-p 12345
2272

    
2273
		# Set USE_SINGLE_ACL="true" to use a single ACL for all checks
2274
		#USE_SINGLE_ACL="false"
2275

    
2276
		# Location for all your certs, these can either be on the server (full path name)
2277
		# or using ssh /sftp as for the ACL
2278
		#DOMAIN_CERT_LOCATION="/etc/ssl/${DOMAIN}.crt" # this is domain cert
2279
		#DOMAIN_KEY_LOCATION="/etc/ssl/${DOMAIN}.key" # this is domain key
2280
		#CA_CERT_LOCATION="/etc/ssl/chain.crt" # this is CA cert
2281
		#DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert
2282
		#DOMAIN_PEM_LOCATION="" # this is the domain key, domain cert and CA cert
2283

    
2284
		# The command needed to reload apache / nginx or whatever you use
2285
		#RELOAD_CMD=""
2286

    
2287
		# Uncomment the following line to prevent non-interactive renewals of certificates
2288
		#PREVENT_NON_INTERACTIVE_RENEWAL="true"
2289

    
2290
		# Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp,
2291
		# smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which
2292
		# will be checked for certificate expiry and also will be checked after
2293
		# an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true
2294
		#SERVER_TYPE="https"
2295
		#CHECK_REMOTE="true"
2296
		#CHECK_REMOTE_WAIT="2" # wait 2 seconds before checking the remote server
2297
		_EOF_domain_
2298
  fi
2299
}
2300

    
2301
write_getssl_template() { # write out the main template file
2302
  cat > "$1" <<- _EOF_getssl_
2303
	# vim: filetype=sh
2304
	#
2305
	# This file is read first and is common to all domains
2306
	#
2307
	# Uncomment and modify any variables you need
2308
	# see https://github.com/srvrco/getssl/wiki/Config-variables for details
2309
	#
2310
	# The staging server is best for testing (hence set as default)
2311
	CA="https://acme-staging-v02.api.letsencrypt.org"
2312
	# This server issues full certificates, however has rate limits
2313
	#CA="https://acme-v02.api.letsencrypt.org"
2314

    
2315
	# The agreement that must be signed with the CA, if not defined the default agreement will be used
2316
	#AGREEMENT="$AGREEMENT"
2317

    
2318
	# Set an email address associated with your account - generally set at account level rather than domain.
2319
	#ACCOUNT_EMAIL="me@example.com"
2320
	ACCOUNT_KEY_LENGTH=4096
2321
	ACCOUNT_KEY="$WORKING_DIR/account.key"
2322

    
2323
	# Account key and private key types - can be rsa, prime256v1, secp384r1 or secp521r1
2324
	#ACCOUNT_KEY_TYPE="rsa"
2325
	PRIVATE_KEY_ALG="rsa"
2326
	#REUSE_PRIVATE_KEY="true"
2327

    
2328
	# The command needed to reload apache / nginx or whatever you use
2329
	#RELOAD_CMD=""
2330

    
2331
	# The time period within which you want to allow renewal of a certificate
2332
	#  this prevents hitting some of the rate limits.
2333
	# Creating a file called FORCE_RENEWAL in the domain directory allows one-off overrides
2334
	# of this setting
2335
	RENEW_ALLOW="30"
2336

    
2337
	# Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp,
2338
	# smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which
2339
	# will be checked for certificate expiry and also will be checked after
2340
	# an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true
2341
	SERVER_TYPE="https"
2342
	CHECK_REMOTE="true"
2343

    
2344
	# Use the following 3 variables if you want to validate via DNS
2345
	#VALIDATE_VIA_DNS="true"
2346
	#DNS_ADD_COMMAND=
2347
	#DNS_DEL_COMMAND=
2348
	_EOF_getssl_
2349
}
2350

    
2351
write_openssl_conf() { # write out a minimal openssl conf
2352
  cat > "$1" <<- _EOF_openssl_conf_
2353
	# minimal openssl.cnf file
2354
	distinguished_name  = req_distinguished_name
2355
	[ req_distinguished_name ]
2356
	[v3_req]
2357
	[v3_ca]
2358
	_EOF_openssl_conf_
2359
}
2360

    
2361
# Trap signals
2362
trap "signal_exit TERM" TERM HUP
2363
trap "signal_exit INT"  INT
2364

    
2365
# Parse command-line
2366
while [[ -n ${1+defined} ]]; do
2367
  case $1 in
2368
    -h | --help)
2369
      help_message; graceful_exit ;;
2370
    -d | --debug)
2371
      _USE_DEBUG=1 ;;
2372
    -c | --create)
2373
      _CREATE_CONFIG=1 ;;
2374
    -f | --force)
2375
      _FORCE_RENEW=1 ;;
2376
    --notify-valid)
2377
      # Exit 2 if certificate is valid and doesn't need renewing
2378
      _NOTIFY_VALID=2 ;;
2379
    -a | --all)
2380
      _CHECK_ALL=1 ;;
2381
    -k | --keep)
2382
      shift; _KEEP_VERSIONS="$1";;
2383
    -q | --quiet)
2384
      _QUIET=1 ;;
2385
    -Q | --mute)
2386
      _QUIET=1
2387
      _MUTE=1 ;;
2388
    -r | --revoke)
2389
      _REVOKE=1
2390
      shift
2391
      REVOKE_CERT="$1"
2392
      shift
2393
      REVOKE_KEY="$1"
2394
      shift
2395
      CA="$1"
2396
      REVOKE_CA="$1"
2397
      REVOKE_REASON=0 ;;
2398
    -u | --upgrade)
2399
      _UPGRADE=1 ;;
2400
    -U | --nocheck)
2401
      _UPGRADE_CHECK=0 ;;
2402
    -i | --install)
2403
      _CERT_INSTALL=1 ;;
2404
    --check-config)
2405
      _ONLY_CHECK_CONFIG=1 ;;
2406
    -w)
2407
      shift; WORKING_DIR="$1" ;;
2408
    --source)
2409
      return ;;
2410
    -*)
2411
      usage
2412
      error_exit "Unknown option $1" ;;
2413
    *)
2414
      if [[ -n $DOMAIN ]]; then
2415
        error_exit "invalid command line $DOMAIN - it appears to contain more than one domain"
2416
      fi
2417
      DOMAIN="$1"
2418
      if [[ -z $DOMAIN ]]; then
2419
        error_exit "invalid command line - it appears to contain a null variable"
2420
      fi ;;
2421
  esac
2422
  shift
2423
done
2424

    
2425
# Main logic
2426
############
2427

    
2428
# Get the current OS, so the correct functions can be used for that OS. (sets the variable os)
2429
get_os
2430

    
2431
# check if "recent" version of bash.
2432
#if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -lt 42 ]]; then
2433
#  info "this script is designed for bash v4.2 or later - earlier version may give errors"
2434
#fi
2435

    
2436
#check if required applications are included
2437

    
2438
requires which
2439
requires openssl
2440
requires curl
2441
requires dig nslookup drill host DNS_CHECK_FUNC
2442
requires dirname
2443
requires awk
2444
requires tr
2445
requires date
2446
requires grep
2447
requires sed
2448
requires sort
2449
requires mktemp
2450

    
2451
# Check if upgrades are available (unless they have specified -U to ignore Upgrade checks)
2452
if [[ $_UPGRADE_CHECK -eq 1 ]]; then
2453
  check_getssl_upgrade
2454
fi
2455

    
2456
# Revoke a certificate if requested
2457
if [[ $_REVOKE -eq 1 ]]; then
2458
  if [[ -z $REVOKE_CA ]]; then
2459
    CA=$DEFAULT_REVOKE_CA
2460
  elif [[ "$REVOKE_CA" == "-d" ]]; then
2461
    _USE_DEBUG=1
2462
    CA=$DEFAULT_REVOKE_CA
2463
  else
2464
    CA=$REVOKE_CA
2465
  fi
2466

    
2467
  obtain_ca_resource_locations
2468
  revoke_certificate
2469
  graceful_exit
2470
fi
2471

    
2472
# get latest agreement from CA (as default)
2473
AGREEMENT=$(curl --user-agent "$CURL_USERAGENT" -I "${CA}/terms" 2>/dev/null | awk 'tolower($1) ~ "location:" {print $2}'|tr -d '\r')
2474

    
2475
# if nothing in command line, print help and exit.
2476
if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]]; then
2477
  help_message
2478
  graceful_exit
2479
fi
2480

    
2481
# Test working directory candidates if unset. Last candidate defaults (~/getssl/)
2482
if [[ -z "${WORKING_DIR}" ]]
2483
then
2484
  for WORKING_DIR in "${WORKING_DIR_CANDIDATES[@]}"
2485
  do
2486
    debug "Testing working dir location '${WORKING_DIR}'"
2487
    if [[ -s "$WORKING_DIR/getssl.cfg" ]]
2488
    then
2489
      break
2490
    fi
2491
  done
2492
fi
2493

    
2494
# if the "working directory" doesn't exist, then create it.
2495
if [[ ! -d "$WORKING_DIR" ]]; then
2496
  debug "Making working directory - $WORKING_DIR"
2497
  mkdir -p "$WORKING_DIR"
2498
fi
2499

    
2500
# read any variables from config in working directory
2501
if [[ -s "$WORKING_DIR/getssl.cfg" ]]; then
2502
  debug "reading config from $WORKING_DIR/getssl.cfg"
2503
  # shellcheck source=/dev/null
2504
  . "$WORKING_DIR/getssl.cfg"
2505
fi
2506

    
2507
# Define defaults for variables not set in the main config.
2508
ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account.key}"
2509
DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}"
2510
DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN"
2511
CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt"
2512
FULL_CHAIN="$DOMAIN_DIR/fullchain.crt"
2513
CA_CERT="$DOMAIN_DIR/chain.crt"
2514
TEMP_DIR="$DOMAIN_DIR/tmp"
2515
if [[ "$os" == "mingw" ]]; then
2516
  CSR_SUBJECT="//"
2517
fi
2518

    
2519
# Set the OPENSSL_CONF environment variable so openssl knows which config to use
2520
export OPENSSL_CONF=$SSLCONF
2521

    
2522
# if "-a" option then check other parameters and create run for each domain.
2523
if [[ ${_CHECK_ALL} -eq 1 ]]; then
2524
  info "Check all certificates"
2525

    
2526
  if [[ ${_CREATE_CONFIG} -eq 1 ]]; then
2527
    error_exit "cannot combine -c|--create with -a|--all"
2528
  fi
2529

    
2530
  if [[ ${_FORCE_RENEW} -eq 1 ]]; then
2531
    error_exit "cannot combine -f|--force with -a|--all because of rate limits"
2532
  fi
2533

    
2534
  if [[ ! -d "$DOMAIN_STORAGE" ]]; then
2535
    error_exit "DOMAIN_STORAGE not found  - $DOMAIN_STORAGE"
2536
  fi
2537

    
2538
  for dir in "${DOMAIN_STORAGE}"/*; do
2539
    if [[ -d "$dir" ]]; then
2540
      debug "Checking $dir"
2541
      cmd="$0 -U" # No update checks when calling recursively
2542
      if [[ ${_USE_DEBUG} -eq 1 ]]; then
2543
        cmd="$cmd -d"
2544
      fi
2545
      if [[ ${_QUIET} -eq 1 ]]; then
2546
        cmd="$cmd -q"
2547
      fi
2548
      # check if $dir is a directory with a getssl.cfg in it
2549
      if [[ -f "$dir/getssl.cfg" ]]; then
2550
        cmd="$cmd -w $WORKING_DIR $(basename "$dir")"
2551
        debug "CMD: $cmd"
2552
        eval "$cmd"
2553
      fi
2554
    fi
2555
  done
2556

    
2557
  graceful_exit
2558
fi
2559
# end of "-a" option (looping through all domains)
2560

    
2561
# if "-c|--create" option used, then create config files.
2562
if [[ ${_CREATE_CONFIG} -eq 1 ]]; then
2563
  # If main config file does not exists then create it.
2564
  if [[ ! -s "$WORKING_DIR/getssl.cfg" ]]; then
2565
    info "creating main config file $WORKING_DIR/getssl.cfg"
2566
    if [[ ! -s "$SSLCONF" ]]; then
2567
      SSLCONF="$WORKING_DIR/openssl.cnf"
2568
      write_openssl_conf "$SSLCONF"
2569
    fi
2570
    write_getssl_template "$WORKING_DIR/getssl.cfg"
2571
  fi
2572
  # If domain and domain config don't exist then create them.
2573
  if [[ ! -d "$DOMAIN_DIR" ]]; then
2574
    info "Making domain directory - $DOMAIN_DIR"
2575
    mkdir -p "$DOMAIN_DIR"
2576
  fi
2577
  if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then
2578
    info "domain config already exists $DOMAIN_DIR/getssl.cfg"
2579
  else
2580
    info "creating domain config file in $DOMAIN_DIR/getssl.cfg"
2581
    # if domain has an existing cert, copy from domain and use to create defaults.
2582
    EX_CERT=$(echo \
2583
      | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:443" 2>/dev/null \
2584
      | openssl x509 2>/dev/null)
2585
    EX_SANS="www.${DOMAIN}"
2586
    if [[ -n "${EX_CERT}" ]]; then
2587
      EX_SANS=$(echo "$EX_CERT" \
2588
        | openssl x509 -noout -text 2>/dev/null| grep "Subject Alternative Name" -A2 \
2589
        | grep -Eo "DNS:[a-zA-Z 0-9.-]*" | sed "s@DNS:$DOMAIN@@g" | grep -v '^$' | cut -c 5-)
2590
      EX_SANS=${EX_SANS//$'\n'/','}
2591
    fi
2592
    write_domain_template "$DOMAIN_DIR/getssl.cfg"
2593
  fi
2594
  TEMP_DIR="$DOMAIN_DIR/tmp"
2595
  # end of "-c|--create" option, so exit
2596
  graceful_exit
2597
fi
2598
# end of "-c|--create" option to create config file.
2599

    
2600
# if domain directory doesn't exist, then create it.
2601
if [[ ! -d "$DOMAIN_DIR" ]]; then
2602
  debug "Making working directory - $DOMAIN_DIR"
2603
  mkdir -p "$DOMAIN_DIR"
2604
fi
2605

    
2606
# define a temporary directory, and if it doesn't exist, create it.
2607
TEMP_DIR="$DOMAIN_DIR/tmp"
2608
if [[ ! -d "${TEMP_DIR}" ]]; then
2609
  debug "Making temp directory - ${TEMP_DIR}"
2610
  mkdir -p "${TEMP_DIR}"
2611
fi
2612

    
2613
# read any variables from config in domain directory
2614
if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then
2615
  debug "reading config from $DOMAIN_DIR/getssl.cfg"
2616
  # shellcheck source=/dev/null
2617
  . "$DOMAIN_DIR/getssl.cfg"
2618
fi
2619

    
2620
# Ensure SANS is comma separated by replacing any number of commas or spaces with a single comma
2621
# shellcheck disable=SC2001
2622
SANS=$(echo "$SANS" | sed 's/[, ]\+/,/g')
2623

    
2624
# from SERVER_TYPE set REMOTE_PORT and REMOTE_EXTRA
2625
set_server_type
2626

    
2627
# check what dns utils are installed
2628
find_dns_utils
2629

    
2630
# auto upgrade clients to v2
2631
auto_upgrade_v2
2632

    
2633
# check config for typical errors.
2634
check_config
2635

    
2636
# exit if just checking config (used for testing)
2637
if [ "${_ONLY_CHECK_CONFIG}" -eq 1 ]; then
2638
  info "Configuration check successful"
2639
  graceful_exit
2640
fi
2641

    
2642
# if -i|--install install certs, reload and exit
2643
if [ "0${_CERT_INSTALL}" -eq 1 ]; then
2644
  cert_install
2645
  reload_service
2646
  graceful_exit
2647
fi
2648

    
2649
if [[ -e "$DOMAIN_DIR/FORCE_RENEWAL" ]]; then
2650
  rm -f "$DOMAIN_DIR/FORCE_RENEWAL" || error_exit "problem deleting file $DOMAIN_DIR/FORCE_RENEWAL"
2651
  _FORCE_RENEW=1
2652
  info "${DOMAIN}: forcing renewal (due to FORCE_RENEWAL file)"
2653
fi
2654

    
2655
obtain_ca_resource_locations
2656

    
2657
# Check if awk supports json_awk (required for ACMEv2)
2658
if [[ $API -eq 2 ]]; then
2659
    json_awk_test=$(json_awk '{ "test": "1" }' 2>/dev/null)
2660
    if [[ "${json_awk_test}" == "" ]]; then
2661
        error_exit "Your version of awk does not work with json_awk (see http://github.com/step-/JSON.awk/issues/6), please install a newer version of mawk or gawk"
2662
    fi
2663
fi
2664

    
2665
# if check_remote is true then connect and obtain the current certificate (if not forcing renewal)
2666
if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then
2667
  debug "getting certificate for $DOMAIN from remote server"
2668
  if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
2669
    # shellcheck disable=SC2086
2670
    # check if openssl supports RSA-PSS
2671
    if [[ $(echo | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} -sigalgs RSA-PSS+SHA256 2>/dev/null) ]]; then
2672
        CIPHER="-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA512"
2673
    else
2674
        CIPHER="-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512"
2675
    fi
2676
  else
2677
    CIPHER=""
2678
  fi
2679
  # shellcheck disable=SC2086
2680
  EX_CERT=$(echo \
2681
    | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${CIPHER} 2>/dev/null \
2682
    | openssl x509 2>/dev/null)
2683
  if [[ -n "$EX_CERT" ]]; then # if obtained a cert
2684
    if [[ -s "$CERT_FILE" ]]; then # if local exists
2685
      CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null)
2686
    else # since local doesn't exist leave empty so that the domain validation will happen
2687
      CERT_LOCAL=""
2688
    fi
2689
    CERT_REMOTE=$(echo "$EX_CERT" | openssl x509 -noout -fingerprint 2>/dev/null)
2690
    if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then
2691
      debug "certificate on server is same as the local cert"
2692
    else
2693
      # check if the certificate is for the right domain
2694
      EX_CERT_DOMAIN=$(echo "$EX_CERT" | openssl x509 -text \
2695
        | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \
2696
        | sort -u | grep "^$DOMAIN\$")
2697
      if [[ "$EX_CERT_DOMAIN" == "$DOMAIN" ]]; then
2698
        # check renew-date on ex_cert and compare to local ( if local exists)
2699
        enddate_ex=$(echo "$EX_CERT" | openssl x509 -noout -enddate 2>/dev/null| cut -d= -f 2-)
2700
        enddate_ex_s=$(date_epoc "$enddate_ex")
2701
        debug "external cert has enddate $enddate_ex ( $enddate_ex_s ) "
2702
        if [[ -s "$CERT_FILE" ]]; then # if local exists
2703
          enddate_lc=$(openssl x509 -noout -enddate < "$CERT_FILE" 2>/dev/null| cut -d= -f 2-)
2704
          enddate_lc_s=$(date_epoc "$enddate_lc")
2705
          debug "local cert has enddate $enddate_lc ( $enddate_lc_s ) "
2706
        else
2707
          enddate_lc_s=0
2708
          debug "local cert doesn't exist"
2709
        fi
2710
        if [[ "$enddate_ex_s" -eq "$enddate_lc_s" ]]; then
2711
          debug "certificates expire at the same time"
2712
        elif [[ "$enddate_ex_s" -gt "$enddate_lc_s" ]]; then
2713
          # remote has longer to expiry date than local copy.
2714
          debug "remote cert has longer to run than local cert - ignoring"
2715
        else
2716
          info "${DOMAIN}: remote cert expires sooner than local, attempting to upload from local"
2717
          copy_file_to_location "domain certificate" \
2718
                                "$CERT_FILE" \
2719
                                "$DOMAIN_CERT_LOCATION"
2720
          copy_file_to_location "private key" \
2721
                                "$DOMAIN_DIR/${DOMAIN}.key" \
2722
                                "$DOMAIN_KEY_LOCATION"
2723
          copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION"
2724
          cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem"
2725
          copy_file_to_location "full pem" \
2726
                                "$TEMP_DIR/${DOMAIN}_chain.pem" \
2727
                                "$DOMAIN_CHAIN_LOCATION"
2728
          cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem"
2729
          copy_file_to_location "private key and domain cert pem" \
2730
                                "$TEMP_DIR/${DOMAIN}_K_C.pem"  \
2731
                                "$DOMAIN_KEY_CERT_LOCATION"
2732
          cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem"
2733
          copy_file_to_location "full pem" \
2734
                                "$TEMP_DIR/${DOMAIN}.pem"  \
2735
                                "$DOMAIN_PEM_LOCATION"
2736
          reload_service
2737
        fi
2738
      else
2739
        info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate"
2740
      fi
2741
    fi
2742
  else
2743
    info "${DOMAIN}: no certificate obtained from host"
2744
  fi
2745
  # end of .... if obtained a cert
2746
fi
2747
# end of .... check_remote is true then connect and obtain the current certificate
2748

    
2749
# if there is an existing certificate file, check details.
2750
if [[ -s "$CERT_FILE" ]]; then
2751
  debug "certificate $CERT_FILE exists"
2752
  enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-)
2753
  debug "local cert is valid until $enddate"
2754
  if [[ "$enddate" != "-" ]]; then
2755
    enddate_s=$(date_epoc "$enddate")
2756
    if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW -ne 1 ]]; then
2757
      issuer=$(openssl x509 -in "$CERT_FILE" -noout -issuer 2>/dev/null)
2758
      if [[ "$issuer" == *"Fake LE Intermediate"* ]] && [[ "$CA" == "https://acme-v02.api.letsencrypt.org" ]]; then
2759
        debug "upgrading from fake cert to real"
2760
      else
2761
        info "${DOMAIN}: certificate is valid for more than $RENEW_ALLOW days (until $enddate)"
2762
        # everything is OK, so exit, if requested with the --notify-valid, exit with code 2
2763
        graceful_exit $_NOTIFY_VALID
2764
      fi
2765
    else
2766
      debug "${DOMAIN}: certificate needs renewal"
2767
    fi
2768
  fi
2769
fi
2770
# end of .... if there is an existing certificate file, check details.
2771

    
2772
if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then
2773
  errmsg="$DOMAIN due for renewal,"
2774
  errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config"
2775
  error_exit "$errmsg"
2776
fi
2777

    
2778
# create account key if it doesn't exist.
2779
if [[ -s "$ACCOUNT_KEY" ]]; then
2780
  debug "Account key exists at $ACCOUNT_KEY skipping generation"
2781
else
2782
  info "creating account key $ACCOUNT_KEY"
2783
  create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH"
2784
fi
2785

    
2786
# if not reusing private key, then remove the old keys
2787
if [[ "$REUSE_PRIVATE_KEY" != "true" ]]; then
2788
  if [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; then
2789
   rm -f "$DOMAIN_DIR/${DOMAIN}.key"
2790
  fi
2791
  if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then
2792
   rm -f "$DOMAIN_DIR/${DOMAIN}.ec.key"
2793
  fi
2794
fi
2795
# create new domain keys if they don't already exist
2796
if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then
2797
  create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH"
2798
else
2799
  create_key "rsa" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH"
2800
  create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.ec.key" "$DOMAIN_KEY_LENGTH"
2801
fi
2802
# End of creating domain keys.
2803

    
2804
#create SAN
2805
if [[ -z "$SANS" ]]; then
2806
  SANLIST="subjectAltName=DNS:${DOMAIN}"
2807
elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
2808
  SANLIST="subjectAltName=DNS:${SANS//[, ]/,DNS:}"
2809
else
2810
  SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//[, ]/,DNS:}"
2811
fi
2812
debug "created SAN list = $SANLIST"
2813

    
2814
#create CSR's
2815
if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then
2816
  create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key"
2817
else
2818
  create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key"
2819
  create_csr "$DOMAIN_DIR/${DOMAIN}.ec.csr" "$DOMAIN_DIR/${DOMAIN}.ec.key"
2820
fi
2821

    
2822
# use account key to register with CA
2823
# currently the code registers every time, and gets an "already registered" back if it has been.
2824
get_signing_params "$ACCOUNT_KEY"
2825

    
2826
info "Registering account"
2827
# send the request to the ACME server.
2828
if [[ $API -eq 1 ]]; then
2829
  if [[ "$ACCOUNT_EMAIL" ]] ; then
2830
	regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}'
2831
  else
2832
	regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}'
2833
  fi
2834
  send_signed_request "$URL_new_reg"  "$regjson"
2835
elif [[ $API -eq 2 ]]; then
2836
  if [[ "$ACCOUNT_EMAIL" ]] ; then
2837
	regjson='{"termsOfServiceAgreed": true, "contact": ["mailto: '$ACCOUNT_EMAIL'"]}'
2838
  else
2839
	regjson='{"termsOfServiceAgreed": true}'
2840
  fi
2841
  send_signed_request "$URL_newAccount"  "$regjson"
2842
else
2843
	debug "cant determine account API"
2844
	graceful_exit
2845
fi
2846

    
2847
if [[ "$code" == "" ]] || [[ "$code" == '201' ]] ; then
2848
  info "Registered"
2849
  KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ')
2850
  debug "KID=_$KID}_"
2851
  echo "$response" > "$TEMP_DIR/account.json"
2852
elif [[ "$code" == '409' ]] ; then
2853
  KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ')
2854
  debug responseHeaders "$responseHeaders"
2855
  debug "Already registered KID=$KID"
2856
elif [[ "$code" == '200' ]] ; then
2857
  KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ')
2858
  debug responseHeaders "$responseHeaders"
2859
  debug "Already registered account, KID=${KID}"
2860
else
2861
  error_exit "Error registering account ...$responseHeaders ... $(json_get "$response" detail)"
2862
fi
2863
# end of registering account with CA
2864

    
2865
# verify each domain
2866
info "Verify each domain"
2867

    
2868
# loop through domains for cert ( from SANS list)
2869
if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then
2870
  alldomains=${SANS//[, ]/ }
2871
else
2872
  alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")
2873
fi
2874

    
2875
if [[ $API -eq 2 ]]; then
2876
  create_order
2877
fi
2878

    
2879
fulfill_challenges
2880

    
2881
# Verification has been completed for all SANS, so request certificate.
2882
info "Verification completed, obtaining certificate."
2883

    
2884
#obtain the certificate.
2885
get_certificate "$DOMAIN_DIR/${DOMAIN}.csr" \
2886
                "$CERT_FILE" \
2887
                "$CA_CERT" \
2888
                "$FULL_CHAIN"
2889
if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
2890
  info "Creating order for EC certificate"
2891
  if [[ $API -eq 2 ]]; then
2892
    create_order
2893
    fulfill_challenges
2894
  fi
2895
  info "obtaining EC certificate."
2896
  get_certificate "$DOMAIN_DIR/${DOMAIN}.ec.csr" \
2897
                  "${CERT_FILE%.*}.ec.crt" \
2898
                  "${CA_CERT%.*}.ec.crt" \
2899
                  "${FULL_CHAIN%.*}.ec.crt"
2900
fi
2901

    
2902
# create Archive of new certs and keys.
2903
cert_archive
2904

    
2905
debug "Certificates obtained and archived locally, will now copy to specified locations"
2906

    
2907
# copy certs to the correct location (creating concatenated files as required)
2908
cert_install
2909

    
2910
# Run reload command to restart apache / nginx or whatever system
2911
reload_service
2912

    
2913
# deactivate authorizations
2914
if [[ "$DEACTIVATE_AUTH" == "true" ]]; then
2915
  debug "in deactivate list is $deactivate_url_list"
2916
  for deactivate_url in $deactivate_url_list; do
2917
    send_signed_request "$deactivate_url" ""
2918
    d=$(json_get "$response" "hostname")
2919
    info "deactivating domain $d"
2920
    debug "deactivating  $deactivate_url"
2921
    send_signed_request "$deactivate_url" "{\"resource\": \"authz\", \"status\": \"deactivated\"}"
2922
    # check response
2923
    if [[ "$code" == "200" ]]; then
2924
      debug "Authorization deactivated"
2925
    else
2926
      error_exit "$domain: Deactivation error: $code"
2927
    fi
2928
  done
2929
fi
2930
# end of deactivating authorizations
2931

    
2932
# Check if the certificate is installed correctly
2933
if [[ ${CHECK_REMOTE} == "true" ]]; then
2934
  sleep "$CHECK_REMOTE_WAIT"
2935
  if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then
2936
    # shellcheck disable=SC2086
2937
    # check if openssl supports RSA-PSS
2938
    if [[ $(echo | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} -sigalgs RSA-PSS+SHA256 2>/dev/null) ]]; then
2939
        PARAMS=("-sigalgs RSA-PSS+SHA256:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512" "-sigalgs ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512")
2940
    else
2941
        PARAMS=("-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512" "-sigalgs ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512")
2942
    fi
2943

    
2944
    CERTS=("$CERT_FILE" "${CERT_FILE%.*}.ec.crt")
2945
    TYPES=("rsa" "$PRIVATE_KEY_ALG")
2946
  else
2947
    PARAMS=("")
2948
    CERTS=("$CERT_FILE")
2949
    TYPES=("$PRIVATE_KEY_ALG")
2950
  fi
2951

    
2952
  for ((i=0; i<${#PARAMS[@]};++i)); do
2953
    debug "Checking ${CERTS[i]}"
2954
    # shellcheck disable=SC2086
2955
    CERT_REMOTE=$(echo \
2956
        | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${PARAMS[i]} 2>/dev/null \
2957
        | openssl x509 -noout -fingerprint 2>/dev/null)
2958
    CERT_LOCAL=$(openssl x509 -noout -fingerprint < "${CERTS[i]}" 2>/dev/null)
2959
    debug CERT_LOCAL="${CERT_LOCAL}"
2960
    debug CERT_REMOTE="${CERT_REMOTE}"
2961
    if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then
2962
        info "${DOMAIN} - ${TYPES[i]} certificate installed OK on server"
2963
    elif [[ "$CERT_REMOTE" == "" ]]; then
2964
        info "${CERTS[i]} not returned by server"
2965
        error_exit "${DOMAIN} - ${TYPES[i]} certificate obtained but not installed on server"
2966
    else
2967
        info "${CERTS[i]} didn't match server"
2968
        error_exit "${DOMAIN} - ${TYPES[i]} certificate obtained but certificate on server is different from the new certificate"
2969
    fi
2970
  done
2971
fi
2972
# end of Check if the certificate is installed correctly
2973

    
2974
# To have reached here, a certificate should have been successfully obtained.
2975
# Use echo rather than info so that 'quiet' is ignored.
2976
echo "certificate obtained for ${DOMAIN}"
2977

    
2978
# gracefully exit ( tidying up temporary files etc).
2979
graceful_exit
(14-14/33)