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
|