#!/usr/bin/env bash # --------------------------------------------------------------------------- # getssl - Obtain SSL certificates from the letsencrypt.org ACME server # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License at for # more details. # For usage, run "getssl -h" or see https://github.com/srvrco/getssl # ACMEv2 process is documented at https://tools.ietf.org/html/rfc8555#section-7.4 # Revision history: # 2016-01-08 Created (v0.1) # 2016-01-11 type correction and upload to github (v0.2) # 2016-01-11 added import of any existing cert on -c option (v0.3) # 2016-01-12 corrected formatting of imported certificate (v0.4) # 2016-01-12 corrected error on removal of token in some instances (v0.5) # 2016-01-18 corrected issue with removing tmp if run as root with the -c option (v0.6) # 2016-01-18 added option to upload a single PEN file ( used by cpanel) (v0.7) # 2016-01-23 added dns challenge option (v0.8) # 2016-01-24 create the ACL directory if it does not exist. (v0.9) - dstosberg # 2016-01-26 correcting a couple of small bugs and allow curl to follow redirects (v0.10) # 2016-01-27 add a very basic openssl.cnf file if it doesn't exist and tidy code slightly (v0.11) # 2016-01-28 Typo corrections, quoted file variables and fix bug on DNS_DEL_COMMAND (v0.12) # 2016-01-28 changed DNS checks to use nslookup and allow hyphen in domain names (v0.13) # 2016-01-29 Fix ssh-reload-command, extra waiting for DNS-challenge, # 2016-01-29 add error_exit and cleanup help message (v0.14) # 2016-01-29 added -a|--all option to renew all configured certificates (v0.15) # 2016-01-29 added option for elliptic curve keys (v0.16) # 2016-01-29 added server-type option to use and check cert validity from website (v0.17) # 2016-01-30 added --quiet option for running in cron (v0.18) # 2016-01-31 removed usage of xxd to make script more compatible across versions (v0.19) # 2016-01-31 removed usage of base64 to make script more compatible across platforms (v0.20) # 2016-01-31 added option to safe a full chain certificate (v0.21) # 2016-02-01 commented code and added option for copying concatenated certs to file (v0.22) # 2016-02-01 re-arrange flow for DNS-challenge, to reduce time taken (v0.23) # 2016-02-04 added options for other server types (ldaps, or any port) and check_remote (v0.24) # 2016-02-04 added short sleep following service restart before checking certs (v0.25) # 2016-02-12 fix challenge token location when directory doesn't exist (v0.26) # 2016-02-17 fix sed -E issue, and reduce length of renew check to 365 days for older systems (v0.27) # 2016-04-05 Ensure DNS cleanup on error exit. (0.28) - pecigonzalo # 2016-04-15 Remove NS Lookup of A record when using dns validation (0.29) - pecigonzalo # 2016-04-17 Improving the wording in a couple of comments and info statements. (0.30) # 2016-05-04 Improve check for if DNS_DEL_COMMAND is blank. (0.31) # 2016-05-06 Setting umask to 077 for security of private keys etc. (0.32) # 2016-05-20 update to reflect changes in staging ACME server json (0.33) # 2016-05-20 tidying up checking of json following ACME changes. (0.34) # 2016-05-21 added AUTH_DNS_SERVER to getssl.cfg as optional definition of authoritative DNS server (0.35) # 2016-05-21 added DNS_WAIT to getssl.cfg as (default = 10 seconds as before) (0.36) # 2016-05-21 added PUBLIC_DNS_SERVER option, for forcing use of an external DNS server (0.37) # 2016-05-28 added FTP method of uploading tokens to remote server (blocked for certs as not secure) (0.38) # 2016-05-28 added FTP method into the default config notes. (0.39) # 2016-05-30 Add sftp with password to copy files (0.40) # 2016-05-30 Add version check to see if there is a more recent version of getssl (0.41) # 2016-05-30 Add [-u|--upgrade] option to automatically upgrade getssl (0.42) # 2016-05-30 Added backup when auto-upgrading (0.43) # 2016-05-30 Improvements to auto-upgrade (0.44) # 2016-05-31 Improved comments - no structural changes # 2016-05-31 After running for nearly 6 months, final testing prior to a 1.00 stable version. (0.90) # 2016-06-01 Reorder functions alphabetically as part of code tidy. (0.91) # 2016-06-03 Version 1.0 of code for release (1.00) # 2016-06-09 bugfix of issue 44, and add success statement (ignoring quiet flag) (1.01) # 2016-06-13 test return status of DNS_ADD_COMMAND and error_exit if a problem (hadleyrich) (1.02) # 2016-06-13 bugfix of issue 45, problem with SERVER_TYPE when it's just a port number (1.03) # 2016-06-13 bugfix issue 47 - DNS_DEL_COMMAND cleanup was run when not required. (1.04) # 2016-06-15 add error checking on RELOAD_CMD (1.05) # 2016-06-20 updated sed and date functions to run on MAC OS X (1.06) # 2016-06-20 added CHALLENGE_CHECK_TYPE variable to allow checks direct on https rather than http (1.07) # 2016-06-21 updated grep functions to run on MAC OS X (1.08) # 2016-06-11 updated to enable running on windows with cygwin (1.09) # 2016-07-02 Corrections to work with older slackware issue #56 (1.10) # 2016-07-02 Updating help info re ACL in config file (1.11) # 2016-07-04 adding DOMAIN_STORAGE as a variable to solve for issue #59 (1.12) # 2016-07-05 updated order to better handle non-standard DOMAIN_STORAGE location (1.13) # 2016-07-06 added additional comments about SANS in example template (1.14) # 2016-07-07 check for duplicate domains in domain / SANS (1.15) # 2016-07-08 modified to be used on older bash for issue #64 (1.16) # 2016-07-11 added -w to -a option and comments in domain template (1.17) # 2016-07-18 remove / regenerate csr when generating new private domain key (1.18) # 2016-07-21 add output of combined private key and domain cert (1.19) # 2016-07-21 updated typo (1.20) # 2016-07-22 corrected issue in nslookup debug option - issue #74 (1.21) # 2016-07-26 add more server-types based on openssl s_client (1.22) # 2016-08-01 updated agreement for letsencrypt (1.23) # 2016-08-02 updated agreement for letsencrypt to update automatically (1.24) # 2016-08-03 improve messages on test of certificate installation (1.25) # 2016-08-04 remove carriage return from agreement - issue #80 (1.26) # 2016-08-04 set permissions for token folders - issue #81 (1.27) # 2016-08-07 allow default chained file creation - issue #85 (1.28) # 2016-08-07 use copy rather than move when archiving certs - issue #86 (1.29) # 2016-08-07 enable use of a single ACL for all checks (if USE_SINGLE_ACL="true" (1.30) # 2016-08-23 check for already validated domains (issue #93) - (1.31) # 2016-08-23 updated already validated domains (1.32) # 2016-08-23 included better force_renew and template for USE_SINGLE_ACL (1.33) # 2016-08-23 enable insecure certificate on https token check #94 (1.34) # 2016-08-23 export OPENSSL_CONF so it's used by all openssl commands (1.35) # 2016-08-25 updated defaults for ACME agreement (1.36) # 2016-09-04 correct issue #101 when some domains already validated (1.37) # 2016-09-12 Checks if which is installed (1.38) # 2016-09-13 Don't check for updates, if -U parameter has been given (1.39) # 2016-09-17 Improved error messages from invalid certs (1.40) # 2016-09-19 remove update check on recursive calls when using -a (1.41) # 2016-09-21 changed shebang for portability (1.42) # 2016-09-21 Included option to Deactivate an Authorization (1.43) # 2016-09-22 retry on 500 error from ACME server (1.44) # 2016-09-22 added additional checks and retry on 500 error from ACME server (1.45) # 2016-09-24 merged in IPv6 support (1.46) # 2016-09-27 added additional debug info issue #119 (1.47) # 2016-09-27 removed IPv6 switch in favour of checking both IPv4 and IPv6 (1.48) # 2016-09-28 Add -Q, or --mute, switch to mute notifications about successfully upgrading getssl (1.49) # 2016-09-30 improved portability to work natively on FreeBSD, Slackware and Mac OS X (1.50) # 2016-09-30 comment out PRIVATE_KEY_ALG from the domain template Issue #125 (1.51) # 2016-10-03 check remote certificate for right domain before saving to local (1.52) # 2016-10-04 allow existing CSR with domain name in subject (1.53) # 2016-10-05 improved the check for CSR with domain in subject (1.54) # 2016-10-06 prints update info on what was included in latest updates (1.55) # 2016-10-06 when using -a flag, ignore folders in working directory which aren't domains (1.56) # 2016-10-12 allow multiple tokens in DNS challenge (1.57) # 2016-10-14 added CHECK_ALL_AUTH_DNS option to check all DNS servers, not just one primary server (1.58) # 2016-10-14 added archive of chain and private key for each cert, and purge old archives (1.59) # 2016-10-17 updated info comment on failed cert due to rate limits. (1.60) # 2016-10-17 fix error messages when using 1.0.1e-fips (1.61) # 2016-10-20 set secure permissions when generating account key (1.62) # 2016-10-20 set permissions to 700 for getssl script during upgrade (1.63) # 2016-10-20 add option to revoke a certificate (1.64) # 2016-10-21 set revocation server default to acme-v01.api.letsencrypt.org (1.65) # 2016-10-21 bug fix for revocation on different servers. (1.66) # 2016-10-22 Tidy up archive code for certificates and reduce permissions for security # 2016-10-22 Add EC signing for secp384r1 and secp521r1 (the latter not yet supported by Let's Encrypt # 2016-10-22 Add option to create a new private key for every cert (REUSE_PRIVATE_KEY="true" by default) # 2016-10-22 Combine EC signing, Private key reuse and archive permissions (1.67) # 2016-10-25 added CHECK_REMOTE_WAIT option ( to pause before final remote check) # 2016-10-25 Added EC account key support ( prime256v1, secp384r1 ) (1.68) # 2016-10-25 Ignore DNS_EXTRA_WAIT if all domains already validated (issue #146) (1.69) # 2016-10-25 Add option for dual ESA / EDSA certs (1.70) # 2016-10-25 bug fix Issue #141 challenge error 400 (1.71) # 2016-10-26 check content of key files, not just recreate if missing. # 2016-10-26 Improvements on portability (1.72) # 2016-10-26 Date formatting for busybox (1.73) # 2016-10-27 bug fix - issue #157 not recognising EC keys on some versions of openssl (1.74) # 2016-10-31 generate EC account keys and tidy code. # 2016-10-31 fix warning message if cert doesn't exist (1.75) # 2016-10-31 remove only specified DNS token #161 (1.76) # 2016-11-03 Reduce long lines, and remove echo from update (1.77) # 2016-11-05 added TOKEN_USER_ID (to set ownership of token files ) # 2016-11-05 updated style to work with latest shellcheck (1.78) # 2016-11-07 style updates # 2016-11-07 bug fix DOMAIN_PEM_LOCATION starting with ./ #167 # 2016-11-08 Fix for openssl 1.1.0 #166 (1.79) # 2016-11-08 Add and comment optional sshuserid for ssh ACL (1.80) # 2016-11-09 Add SKIP_HTTP_TOKEN_CHECK option (Issue #170) (1.81) # 2016-11-13 bug fix DOMAIN_KEY_CERT generation (1.82) # 2016-11-17 add PREVENT_NON_INTERACTIVE_RENEWAL option (1.83) # 2016-12-03 add HTTP_TOKEN_CHECK_WAIT option (1.84) # 2016-12-03 bugfix CSR renewal when no SANS and when using MINGW (1.85) # 2016-12-16 create CSR_SUBJECT variable - Issue #193 # 2016-12-16 added fullchain to archive (1.86) # 2016-12-16 updated DOMAIN_PEM_LOCATION when using DUAL_RSA_ECDSA (1.87) # 2016-12-19 allow user to ignore permission preservation with nfsv3 shares (1.88) # 2016-12-19 bug fix for CA (1.89) # 2016-12-19 included IGNORE_DIRECTORY_DOMAIN option (1.90) # 2016-12-22 allow copying files to multiple locations (1.91) # 2016-12-22 bug fix for copying tokens to multiple locations (1.92) # 2016-12-23 tidy code - place default variables in alphabetical order. # 2016-12-27 update checks to work with openssl in FIPS mode (1.93) # 2016-12-28 fix leftover tmpfiles in upgrade routine (1.94) # 2016-12-28 tidied up upgrade tmpfile handling (1.95) # 2017-01-01 update comments # 2017-01-01 create stable release 2.0 (2.00) # 2017-01-02 Added option to limit number of old versions to keep (2.01) # 2017-01-03 Created check_config function to list all obvious config issues (2.02) # 2017-01-10 force renew if FORCE_RENEWAL file exists (2.03) # 2017-01-12 added drill, dig or host as alternatives to nslookup (2.04) # 2017-01-18 bugfix issue #227 - error deleting csr if doesn't exist # 2017-01-18 issue #228 check private key and account key are different (2.05) # 2017-01-21 issue #231 mingw bugfix and typos in debug messages (2.06) # 2017-01-29 issue #232 use neutral locale for date formatting (2.07) # 2017-01-30 issue #243 compatibility with bash 3.0 (2.08) # 2017-01-30 issue #243 additional compatibility with bash 3.0 (2.09) # 2017-02-18 add OCSP Must-Staple to the domain csr generation (2.10) # 2018-01-04 updating to use the updated letsencrypt APIv2 # 2019-09-30 issue #423 Use HTTP 1.1 as workaround atm (2.11) # 2019-10-02 issue #425 Case insensitive processing of agreement url because of HTTP/2 (2.12) # 2019-10-07 update DNS checks to allow use of CNAMEs (2.13) # 2019-11-18 Rebased master onto APIv2 and added Content-Type: application/jose+json (2.14) # 2019-11-20 #453 and #454 Add User-Agent to all curl requests # 2019-11-22 #456 Fix shellcheck issues # 2019-11-23 #459 Fix missing chain.crt # 2019-12-18 #462 Use POST-as-GET for ACMEv2 endpoints # 2020-01-07 #464 and #486 "json was blank" (change all curl request to use POST-as-GET) # 2020-01-08 Error and exit if rate limited, exit if curl returns nothing # 2020-01-10 Change domain and getssl templates to v2 (2.15) # 2020-01-17 #473 and #477 Don't use POST-as-GET when sending ready for challenge for ACMEv1 (2.16) # 2020-01-22 #475 and #483 Fix grep regex for >9 subdomains in json_get # 2020-01-24 Add support for CloudDNS # 2020-01-24 allow file transfer using WebDAV over HTTPS # 2020-01-26 Use urlbase64_decode() instead of base64 -d # 2020-01-26 Fix "already verified" error for ACMEv2 # 2020-01-29 Check awk new enough to support json_awk # 2020-02-05 Fix epoch_date for busybox # 2020-02-06 Bugfixes for json_awk and nslookup to support old awk versions (2.17) # 2020-02-11 Add SCP_OPTS and SFTP_OPTS # 2020-02-12 Fix for DUAL_RSA_ECDSA not working with ACMEv2 (#334, #474, #502) # 2020-02-12 Fix #424 - Sporadic "error in EC signing couldn't get R from ..." (2.18) # 2020-02-12 Fix "Registration key already in use" (2.19) # 2020-02-13 Fix bug with copying to all locations when creating RSA and ECDSA certs (2.20) # 2020-02-22 Change sign_string to use openssl asn1parse (better fix for #424) # 2020-02-23 Add dig to config check for systems without drill (ubuntu) # 2020-03-11 Use dig +trace to find primary name server and improve dig parsing of CNAME # 2020-03-12 Fix bug with DNS validation and multiple domains (#524) # 2020-03-24 Find primary ns using all dns utils (dig, host, nslookup) # 2020-03-23 Fix staging server URL in domain template (2.21) # 2020-03-30 Fix error message find_dns_utils from over version of "command" # 2020-03-30 Fix problems if domain name isn't in lowercase (2.22) # 2020-04-16 Add alternative working dirs '/etc/getssl/' '${PROGDIR}/conf' '${PROGDIR}/.getssl' # 2020-04-16 Add -i|--install command line option (2.23) # 2020-04-19 Remove dependency on seq, ensure clean_up doesn't try to delete /tmp (2.24) # 2020-04-20 Check for domain using all DNS utilities (2.25) # 2020-04-22 Fix HAS_HOST and HAS_NSLOOKUP checks - wolfaba # 2020-04-22 Fix domain case conversion for different locales - glynge (2.26) # 2020-04-26 Fixed ipv4 confirmation with nslookup - Cyber1000 # 2020-04-29 Fix ftp/sftp problems if challenge starts with a dash # 2020-05-06 Fix missing fullchain.ec.crt when creating dual certificates (2.27) # 2020-05-14 Add --notify-valid option (exit 2 if certificate is valid) # 2020-05-23 Fix --revoke (didn't work with ACMEv02) (2.28) # 2020-06-06 Fix missing URL_revoke definition when no CA directory suffix (#566) # 2020-06-18 Fix CHECK_REMOTE for DUAL_RSA_ECDSA (#570) # 2020-07-14 Support space separated SANS (#574) (2.29) # 2020-08-06 Use -sigalgs instead of -cipher when checking remote for tls1.3 (#570) # 2020-08-31 Fix slow fork bomb when directory containing getssl isn't writeable (#440) # 2020-09-01 Use RSA-PSS when checking remote for DUAL_RSA_ECDSA (#570) # 2020-09-02 Fix issue when SANS is space and comma separated (#579) (2.30) # 2020-10-02 Various fixes to get_auth_dns and changes to support unit tests (#308) # 2020-10-04 Add CHECK_PUBLIC_DNS_SERVER to check the DNS challenge has been updated there # 2020-10-13 Bugfix: strip comments in drill/dig output (mhameed) # 2020-11-18 Wildcard support (#347)(#400)(2.31) # 2020-12-08 Fix mktemp template on alpine (#612) # 2020-12-17 Fix delimiter issues with ${alldomains[]} in create_csr (#614)(vietw) # 2020-12-18 Wrong SANS when domain contains a minus character (atisne) # 2020-12-22 Fixes to get_auth_dns # 2020-12-22 Check that dig doesn't return an error (#611)(2.32) # 2020-12-29 Fix dig SOA lookup (#617)(2.33) # 2021-01-05 Show error if running in POSIX mode (#611) # 2021-01-16 Fix double slash when using root directory with DAVS (ionos) # 2021-01-22 Add FTP_OPTIONS # 2021-01-27 Add the ability to set several reload commands (atisne) # 2021-01-29 Use dig -r (if supported) to ignore.digrc (#630) # 2021-02-07 Allow -u --upgrade without any domain, so that one can only update the script (Benno-K)(2.34) # 2021-02-09 Prevent listing the complete file if version tag missing (#637)(softins) # 2021-02-12 Add PREFERRED_CHAIN # 2021-02-15 ADD ftp explicit SSL with curl for upload the challenge (CoolMischa) # 2021-02-18 Add FULL_CHAIN_INCLUDE_ROOT # 2021-03-25 Fix DNS challenge completion check if CNAMEs on different NS are used (sideeffect42)(2.35) # 2021-05-08 Merge from tlhackque/getssl: GoDaddy, split-view, tempfile permissions fixes, --version(2.36) # 2021-07-07 Request new certificate if SANs have changed (#669)(#673) # 2021-07-12 Do not redirect outputs on remote commands when the debug option is used (atisne) # 2021-07-20 Use +noidnout to enable certificates for IDN domains (#679)(2.37) # 2021-07-22 Only pass +noidnout param to dig/drill(#682)(2.38) # 2021-07-25 Fix copy_file_to_location failures with ssh when suffix applied to file lacking an extension (tlhackque)(#686) # 2021-07-27 Support ftps://, FTPS_OPTIONS, remove default --insecure parameter to ftpes. Report caller(s) of error_exit in debug and test modes (tlhackque)(#687)(2.39) # 2021-07-30 Prefer API V2 when both offered (tlhackque) (#690) (2.40) # 2021-07-30 Run tests with -d to catch intermittent failures, Use fork's repo for upgrade tests. (tlhackque) (#692) (2.41) # 2021-08-26 Improve upgrade check & make upgrade do a full install when possible (tlhackque) (#694) (2.42) # 2021-09-02 Fix version compare - cURL v8 may have single digit minor numbers. (tlhackque) (2.43) # 2021-09-26 Delete key file when key algorithm has changed (makuhama) # 2021-09-30 better error if curl returns 60 (#709) # 2021-10-01 Fix -preferred-chain argument (#712) # 2021-10-01 Show help if no domain specified (#705)(2.44) # 2021-10-08 Extract release tag from release api using awk (fix BSD issues) # 2021-10-11 Fix broken upgrade url (#718)(2.45) # 2021-10-22 Copy fullchain to DOMAIN_CHAIN_LOCATION (amartin-git) # 2021-11-10 Detect Solaris and use gnu tools (#701)(miesi) # 2021-11-12 Support acme-dns and fix CNAME issues (#722)(#308) # 2021-12-14 Enhancements for GoDaddy (support more levels of domain names, no longer require GODADDY_BASE, and actual deletion of resource records) # 2021-12-22 Don't show usage if run with --upgrade (#728) # 2021-12-23 Don't use +idnout if dig shows a warning (#688) # 2022-01-06 Support --account-id (#716)(2.46) # 2022-03-09 Support for ISPConfig API # 2022-05-03 Windows Server and IIS support (2.47) # 2022-05-18 Add FTP_ARGS # 2022-11-01 Add FTP_PORT # 2023-02-04 Create newline to ensure [SAN] section can be parsed (#792)(MRigal) # 2023-02-22 Remove cronie from deb package dependencies (2.48) # 2024-03-18 Refresh the TXT record if a CNAME is found (JoergBruce #828) (2.49) # 2024-03-26 - 2025-07-28 Multiple improvements: DNS script additions for Google Cloud, PowerDNS, Hetzner, INWX, Route 53 bash implementation, profiles support, lowercase replay-nonce headers, RFC 8555 validation and security improvements, test stability improvements, basename dash fix, and processing debug messages (2.50) # 2024-03-26 Test for "true" in wildcard property of authorization responses # 2024-10-16 Add newlines to /directory response (#765)(#859) # 2025-06-18 Support profiles # 2025-07-28 Accept lowercase replay-nonce headers (#884) # ---------------------------------------------------------------------------------------- case :$SHELLOPTS: in *:posix:*) echo -e "${0##*/}: Running with POSIX mode enabled is not supported" >&2; exit 1;; esac PROGNAME=${0##*/} PROGDIR="$(cd "$(dirname "$0")" || exit; pwd -P;)" VERSION="2.50" # defaults ACCOUNT_KEY_LENGTH=4096 ACCOUNT_KEY_TYPE="rsa" ACME_RESPONSE_PENDING_WAIT=5 ARI_ENABLE="true" CA_CERT_LOCATION="" CA="https://acme-staging-v02.api.letsencrypt.org/directory" CHALLENGE_CHECK_TYPE="http" CHECK_REMOTE_WAIT=0 CHECK_REMOTE="true" if [[ -n "${GITHUB_REPOSITORY}" ]] ; then CODE_LOCATION="https://raw.githubusercontent.com/${GITHUB_REPOSITORY}/master/getssl" RELEASE_API="https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/latest" else CODE_LOCATION="https://raw.githubusercontent.com/srvrco/getssl/master/getssl" RELEASE_API="https://api.github.com/repos/srvrco/getssl/releases/latest" fi CSR_SUBJECT="/" CURL_USERAGENT="${PROGNAME}/${VERSION}" DEACTIVATE_AUTH="false" DEFAULT_REVOKE_CA="https://acme-v02.api.letsencrypt.org" DOMAIN_KEY_LENGTH=4096 DUAL_RSA_ECDSA="false" FTP_OPTIONS="" FTPS_OPTIONS="" FTP_ARGS="" FTP_PORT="" FULL_CHAIN_INCLUDE_ROOT="false" GETSSL_IGNORE_CP_PRESERVE="false" HTTP_TOKEN_CHECK_WAIT=0 IGNORE_DIRECTORY_DOMAIN="false" OCSP_MUST_STAPLE="false" ORIG_UMASK=$(umask) PREFERRED_CHAIN="" # Set this to use an alternative root certificate PREVIOUSLY_VALIDATED="true" PRIVATE_KEY_ALG="rsa" PROFILE="" RELOAD_CMD="" RENEW_ALLOW="30" REUSE_PRIVATE_KEY="true" SERVER_TYPE="https" SKIP_HTTP_TOKEN_CHECK="false" SSLCONF="$(openssl version -d 2>/dev/null| cut -d\" -f2)/openssl.cnf" TOKEN_USER_ID="" USE_SINGLE_ACL="false" WORKING_DIR_CANDIDATES=("/etc/getssl" "${PROGDIR}/conf" "${PROGDIR}/.getssl" "${HOME}/.getssl") # Variables used when validating using a DNS entry VALIDATE_VIA_DNS="" # Set this to "true" to enable DNS validation export AUTH_DNS_SERVER="" # Use this DNS server to check the challenge token has been set export DNS_CHECK_OPTIONS="" # Options (such as TSIG file) required by DNS_CHECK_FUNC export PUBLIC_DNS_SERVER="" # Use this DNS server to find the authoritative DNS servers for the domain CHECK_ALL_AUTH_DNS="false" # Check the challenge token has been set on all authoritative DNS servers CHECK_PUBLIC_DNS_SERVER="true" # Check the public DNS server as well as the authoritative DNS servers DNS_ADD_COMMAND="" # Use this command/script to add the challenge token to the DNS entries for the domain DNS_DEL_COMMAND="" # Use this command/script to remove the challenge token from the DNS entries for the domain DNS_WAIT_COUNT=100 # How many times to wait for the DNS record to update DNS_WAIT=5 # How long to wait before checking the DNS record again DNS_EXTRA_WAIT=60 # How long to wait after the DNS entries are visible to us before telling the ACME server to check. DNS_WAIT_RETRY_ADD="false" # Try the dns_add_command again if the DNS record hasn't updated # Private variables _ARI_STARTTIME="" _CHECK_ALL=0 _CREATE_CONFIG=0 _CURL_VERSION="" _FORCE_RENEW=0 _MUTE=0 _NOTIFY_VALID=0 _NOMETER="" _QUIET=0 _RECREATE_CSR=0 _REDIRECT_OUTPUT="1>/dev/null 2>&1" _REPLACES="" _REVOKE=0 _SHOW_ACCOUNT_ID=0 _TEST_SKIP_CNAME_CALL=0 _TEST_SKIP_SOA_CALL=0 _UPGRADE=0 _UPGRADE_CHECK=1 _UPGRADE_TO_TAG="" _USE_DEBUG=0 _ONLY_CHECK_CONFIG=0 config_errors="false" export LANG=C API=1 # store copy of original command in case of upgrading script and re-running ORIGCMD="$0 $*" # Define all functions (in alphabetical order) auto_upgrade_v2() { # Automatically update clients to v2 if [[ "${CA}" == *"acme-v01."* ]] || [[ "${CA}" == *"acme-staging."* ]]; then OLDCA=${CA} # shellcheck disable=SC2001 CA=$(echo "${OLDCA}" | sed "s/v01/v02/g") # shellcheck disable=SC2001 CA=$(echo "${CA}" | sed "s/staging/staging-v02/g") info "Upgraded to v2 (changed ${OLDCA} to ${CA})" fi debug "Using certificate issuer: ${CA}" } cert_archive() { # Archive certificate file by copying files to dated archive dir. debug "creating an archive copy of current new certs" date_time=$(date +%Y_%m_%d_%H_%M) mkdir -p "${DOMAIN_DIR}/archive/${date_time}" umask 077 cp "$CERT_FILE" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.crt" cp "$DOMAIN_DIR/${DOMAIN}.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.csr" cp "$DOMAIN_DIR/${DOMAIN}.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.key" cp "$CA_CERT" "${DOMAIN_DIR}/archive/${date_time}/chain.crt" cat "$CERT_FILE" "$CA_CERT" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.crt" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then cp "${CERT_FILE%.*}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.crt" cp "$DOMAIN_DIR/${DOMAIN}.ec.csr" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.csr" cp "$DOMAIN_DIR/${DOMAIN}.ec.key" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.key" cp "${CA_CERT%.*}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/chain.ec.crt" cat "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.ec.crt" fi umask "$ORIG_UMASK" debug "purging old GetSSL archives" purge_archive "$DOMAIN_DIR" } base64url_decode() { awk '{ if (length($0) % 4 == 3) print $0"="; else if (length($0) % 4 == 2) print $0"=="; else print $0; }' | tr -- '-_' '+/' | base64 -d } cert_install() { # copy certs to the correct location (creating concatenated files as required) umask 077 copy_file_to_location "domain certificate" "$CERT_FILE" "$DOMAIN_CERT_LOCATION" copy_file_to_location "private key" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LOCATION" copy_file_to_location "CA certificate" "$CA_CERT" "$CA_CERT_LOCATION" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then if [[ -n "$DOMAIN_CERT_LOCATION" ]]; then copy_file_to_location "ec domain certificate" \ "${CERT_FILE%.*}.ec.crt" \ "${DOMAIN_CERT_LOCATION}" \ "ec" fi if [[ -n "$DOMAIN_KEY_LOCATION" ]]; then copy_file_to_location "ec private key" \ "$DOMAIN_DIR/${DOMAIN}.ec.key" \ "${DOMAIN_KEY_LOCATION}" \ "ec" fi if [[ -n "$CA_CERT_LOCATION" ]]; then copy_file_to_location "ec CA certificate" \ "${CA_CERT%.*}.ec.crt" \ "${CA_CERT_LOCATION%.*}.crt" \ "ec" fi fi # if DOMAIN_CHAIN_LOCATION is not blank, then create and copy file. if [[ -n "$DOMAIN_CHAIN_LOCATION" ]]; then if [[ "$(dirname "$DOMAIN_CHAIN_LOCATION")" == "." ]]; then to_location="${DOMAIN_DIR}/${DOMAIN_CHAIN_LOCATION}" else to_location="${DOMAIN_CHAIN_LOCATION}" fi cat "$FULL_CHAIN" > "$TEMP_DIR/${DOMAIN}_chain.pem" copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem" "$to_location" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then cat "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}_chain.pem.ec" copy_file_to_location "full chain" "$TEMP_DIR/${DOMAIN}_chain.pem.ec" "${to_location}" "ec" fi fi # if DOMAIN_KEY_CERT_LOCATION is not blank, then create and copy file. if [[ -n "$DOMAIN_KEY_CERT_LOCATION" ]]; then if [[ "$(dirname "$DOMAIN_KEY_CERT_LOCATION")" == "." ]]; then to_location="${DOMAIN_DIR}/${DOMAIN_KEY_CERT_LOCATION}" else to_location="${DOMAIN_KEY_CERT_LOCATION}" fi cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" > "$TEMP_DIR/${DOMAIN}_K_C.pem" copy_file_to_location "private key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem" "$to_location" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" copy_file_to_location "private ec key and domain cert pem" "$TEMP_DIR/${DOMAIN}_K_C.pem.ec" "${to_location}" "ec" fi fi # if DOMAIN_PEM_LOCATION is not blank, then create and copy file. if [[ -n "$DOMAIN_PEM_LOCATION" ]]; then if [[ "$(dirname "$DOMAIN_PEM_LOCATION")" == "." ]]; then to_location="${DOMAIN_DIR}/${DOMAIN_PEM_LOCATION}" else to_location="${DOMAIN_PEM_LOCATION}" fi cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" copy_file_to_location "full key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem" "$to_location" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then cat "$DOMAIN_DIR/${DOMAIN}.ec.key" "${CERT_FILE%.*}.ec.crt" "${CA_CERT%.*}.ec.crt" > "$TEMP_DIR/${DOMAIN}.pem.ec" copy_file_to_location "full ec key, cert and chain pem" "$TEMP_DIR/${DOMAIN}.pem.ec" "${to_location}" "ec" fi fi # end of copying certs. umask "$ORIG_UMASK" } check_challenge_completion() { # checks with the ACME server if our challenge is OK uri=$1 domain=$2 keyauthorization=$3 info "sending request to ACME server saying we're ready for challenge" # check response from our request to perform challenge if [[ $API -eq 1 ]]; then send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" if [[ -n "$code" ]] && [[ ! "$code" == '202' ]] ; then error_exit "$domain:Challenge error: $code" fi else # APIv2 send_signed_request "$uri" "{}" if [[ -n "$code" ]] && [[ ! "$code" == '200' ]] ; then detail=$(echo "$response" | grep "detail" | awk -F\" '{print $4}') error_exit "$domain:Challenge error: $code:Detail: $detail" fi fi # loop "forever" to keep checking for a response from the ACME server. while true ; do info "checking if challenge is complete" if [[ $API -eq 1 ]]; then if ! get_cr "$uri" ; then error_exit "$domain:Verify error:$code" fi else # APIv2 send_signed_request "$uri" "" fi status=$(json_get "$response" status) # If ACME response is valid, then break out of loop if [[ "$status" == "valid" ]] ; then info "Verified $domain" break; fi # if ACME response is "invalid" then abandon the order request - returns error so it can be retried if [[ "$status" == "invalid" ]] ; then err_detail=$(echo "$response" | grep "detail") info "$domain:Verify error:$err_detail" return 1 fi # if ACME response is pending (they haven't completed checks yet) # or valid (completed checks but not created certificate) then wait and try again. if [[ "$status" == "pending" ]] || [[ "$status" == "valid" ]] || [[ "$status" == "processing" ]]; then info "Pending" else err_detail=$(echo "$response" | grep "detail") error_exit "$domain:Verify error:$status:$err_detail" fi debug "sleep 5 secs before testing verify again" sleep "$ACME_RESPONSE_PENDING_WAIT" done return 0 } check_challenge_completion_dns() { # perform validation via DNS challenge d=${1} rr=${2} primary_ns=${3} auth_key=${4} # check for token at public dns server, waiting for a valid response. for ns in $primary_ns; do info "checking DNS at $ns" # add +noidnout if idn-domain so search for domain in results works if [[ "${d}" == xn--* || "${d}" == *".xn--"* ]]; then if [[ "$DNS_CHECK_FUNC" == "nslookup" || "$DNS_CHECK_FUNC" == "host" || ("$DNS_CHECK_FUNC" == "$HAS_DIG_OR_DRILL" && "$DIG_SUPPORTS_NOIDNOUT" == "false") ]]; then info "Info: idn domain but $DNS_CHECK_FUNC doesn't support +noidnout" else debug "adding +noidnout to DNS_CHECK_OPTIONS" DNS_CHECK_OPTIONS="$DNS_CHECK_OPTIONS +noidnout" fi fi ntries=0 check_dns="fail" while [[ "$check_dns" == "fail" ]]; do if [[ "$os" == "cygwin" || "$os" == "mingw64_nt" || "$os" == "msys_nt" ]]; then check_result=$(nslookup -type=txt "${rr}." "${ns}" \ | grep ^_acme -A2\ | grep '"'|awk -F'"' '{ print $2}') elif [[ "$DNS_CHECK_FUNC" == "drill" ]] || [[ "$DNS_CHECK_FUNC" == "dig" ]]; then # shellcheck disable=SC2086 debug "$DNS_CHECK_FUNC" $DNS_CHECK_OPTIONS TXT "${rr}" "@${ns}" # shellcheck disable=SC2086 check_output=$($DNS_CHECK_FUNC $DNS_CHECK_OPTIONS TXT "${rr}" "@${ns}") check_result=$(grep -i "^${rr}"<<<"${check_output}"|grep 'IN\WTXT'|awk -F'"' '{ print $2}') debug "check_result=\"$check_result\"" # Check if rr is a CNAME if [[ -z "$check_result" ]]; then rr_cname=$(grep -i "^${rr}"<<<"${check_output}"|grep 'IN\WCNAME'|awk '{ print $5}') debug "cname check=\"$rr_cname\"" if [[ -n "$rr_cname" ]]; then # shellcheck disable=SC2086 check_output=$($DNS_CHECK_FUNC $DNS_CHECK_OPTIONS TXT "${rr_cname}" "@${ns}") check_result=$(grep -i "^${rr_cname}"<<<"${check_output}"|grep 'IN\WTXT'|awk -F'"' '{ print $2}' | uniq) fi fi if [[ -z "$check_result" ]]; then # shellcheck disable=SC2086 debug "$DNS_CHECK_FUNC" $DNS_CHECK_OPTIONS ANY "${rr}" "@${ns}" # shellcheck disable=SC2086 check_result=$($DNS_CHECK_FUNC $DNS_CHECK_OPTIONS ANY "${rr}" "@${ns}" \ | grep -i "^${rr}" \ | grep 'IN\WTXT'|awk -F'"' '{ print $2}') debug "check_result=\"$check_result\"" fi elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then debug "$DNS_CHECK_FUNC" -t TXT "${rr}" "${ns}" check_result=$($DNS_CHECK_FUNC -t TXT "${rr}" "${ns}" \ | grep 'descriptive text'|awk -F'"' '{ print $2}') debug "check_result=\"$check_result\"" else debug "$DNS_CHECK_FUNC" -type=txt "${rr}" "${ns}" check_result=$(nslookup -type=txt "${rr}" "${ns}" \ | grep 'text ='|awk -F'"' '{ print $2}') debug "check_result=\"$check_result\"" if [[ -z "$check_result" ]]; then debug "$DNS_CHECK_FUNC" -type=any "${rr}" "${ns}" check_result=$(nslookup -type=any "${rr}" "${ns}" \ | grep 'text ='|awk -F'"' '{ print $2}') debug "check_result=\"$check_result\"" fi fi debug "expecting \"$auth_key\"" debug "${ns} gave ... \"$check_result\"" if [[ "$check_result" == *"$auth_key"* ]]; then check_dns="success" else if [[ $ntries -lt $DNS_WAIT_COUNT ]]; then ntries=$(( ntries + 1 )) if [[ $DNS_WAIT_RETRY_ADD == "true" && $(( ntries % 10 )) == 0 ]]; then debug "Deleting DNS via command: ${DNS_DEL_COMMAND}" del_dns_rr "${d}" "${auth_key}" debug "Retrying adding DNS via command: ${DNS_ADD_COMMAND}" add_dns_rr "${d}" "${auth_key}" \ || error_exit "DNS_ADD_COMMAND failed for domain ${d}" fi info "checking DNS at ${ns} for ${rr}. Attempt $ntries/${DNS_WAIT_COUNT} gave wrong result, "\ "waiting $DNS_WAIT secs before checking again" sleep $DNS_WAIT else debug "dns check failed - removing existing value" del_dns_rr "${d}" "${auth_key}" error_exit "checking \"${rr}\" gave \"$check_result\" not \"$auth_key\"" fi fi done done if [[ "$DNS_EXTRA_WAIT" -gt 0 && "$PREVIOUSLY_VALIDATED" != "true" ]]; then info "sleeping $DNS_EXTRA_WAIT seconds before asking the ACME server to check the dns" sleep "$DNS_EXTRA_WAIT" fi } # end of ... perform validation if via DNS challenge check_config() { # check the config files for all obvious errors debug "checking config" # check keys case "$ACCOUNT_KEY_TYPE" in rsa|prime256v1|secp384r1|secp521r1) debug "checked ACCOUNT_KEY_TYPE " ;; *) info "${DOMAIN}: invalid ACCOUNT_KEY_TYPE - $ACCOUNT_KEY_TYPE" config_errors=true ;; esac if [[ "$ACCOUNT_KEY" == "$DOMAIN_DIR/${DOMAIN}.key" ]]; then info "${DOMAIN}: ACCOUNT_KEY and domain key ( $DOMAIN_DIR/${DOMAIN}.key ) must be different" config_errors=true fi case "$PRIVATE_KEY_ALG" in rsa|prime256v1|secp384r1|secp521r1) debug "checked PRIVATE_KEY_ALG " ;; *) info "${DOMAIN}: invalid PRIVATE_KEY_ALG - '$PRIVATE_KEY_ALG'" config_errors=true ;; esac if [[ "$DUAL_RSA_ECDSA" == "true" ]] && [[ "$PRIVATE_KEY_ALG" == "rsa" ]]; then info "${DOMAIN}: PRIVATE_KEY_ALG not set to an EC type and DUAL_RSA_ECDSA=\"true\"" config_errors=true fi # get all domains into an array if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then read -r -a alldomains <<< "${SANS//[, ]/ }" else read -r -a alldomains <<< "$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")" fi if [[ -z "${alldomains[*]}" ]]; then info "${DOMAIN}: no domains specified" config_errors=true fi if [[ $VALIDATE_VIA_DNS == "true" ]]; then # using dns-01 challenge if [[ -z "$DNS_ADD_COMMAND" ]]; then info "${DOMAIN}: DNS_ADD_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")" config_errors=true fi if [[ -z "$DNS_DEL_COMMAND" ]]; then info "${DOMAIN}: DNS_DEL_COMMAND not defined (whilst VALIDATE_VIA_DNS=\"true\")" config_errors=true fi fi dn=0 tmplist=$(mktemp 2>/dev/null || mktemp -t getssl.XXXXXX) || error_exit "mktemp failed" for d in "${alldomains[@]}"; do # loop over domains (dn is domain number) debug "checking domain $d" if [[ "$(grep "^${d}$" "$tmplist")" = "$d" ]]; then info "${DOMAIN}: $d appears to be duplicated in domain, SAN list" config_errors=true elif [[ "$d" != "${d##\*.}" ]] && [[ "$VALIDATE_VIA_DNS" != "true" ]]; then info "${DOMAIN}: cannot use http-01 validation for wildcard domains" config_errors=true else echo "$d" >> "$tmplist" fi if [[ "$USE_SINGLE_ACL" == "true" ]]; then DOMAIN_ACL="${ACL[0]}" else DOMAIN_ACL="${ACL[$dn]}" fi if [[ $VALIDATE_VIA_DNS != "true" ]]; then # using http-01 challenge if [[ -z "${DOMAIN_ACL}" ]]; then info "${DOMAIN}: ACL location not specified for domain $d in $DOMAIN_DIR/getssl.cfg" config_errors=true fi # check domain exists using all DNS utilities. DNS_CHECK_OPTIONS may bind IP address or provide TSIG found_ip=false if [[ -n "$HAS_DIG_OR_DRILL" ]]; then # add +noidnout if idn-domain so search for domain in results works DIG_CHECK_OPTIONS="$DNS_CHECK_OPTIONS" if [[ ("${d}" == xn--* || "${d}" == *".xn--"* ) && "$DIG_SUPPORTS_NOIDNOUT" == "true" ]]; then DIG_CHECK_OPTIONS="$DNS_CHECK_OPTIONS +noidnout" fi debug "DNS lookup using $HAS_DIG_OR_DRILL $DIG_CHECK_OPTIONS ${d}" # shellcheck disable=SC2086 if [[ "$($HAS_DIG_OR_DRILL $DIG_CHECK_OPTIONS -t SOA "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then found_ip=true elif [[ "$($HAS_DIG_OR_DRILL $DIG_CHECK_OPTIONS -t A "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then found_ip=true elif [[ "$($HAS_DIG_OR_DRILL $DIG_CHECK_OPTIONS -t AAAA "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then found_ip=true fi fi if [[ "$HAS_HOST" == "true" ]]; then debug "DNS lookup using host $DNS_CHECK_OPTIONS ${d}" # shellcheck disable=SC2086 if [[ "$(host $DNS_CHECK_OPTIONS "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then found_ip=true fi fi if [[ "$HAS_NSLOOKUP" == "true" ]]; then debug "DNS lookup using nslookup $DNS_CHECK_OPTIONS -query AAAA ${d}" # shellcheck disable=SC2086 if [[ "$(nslookup $DNS_CHECK_OPTIONS -query=AAAA "${d}"|grep -c -i "^${d}.*has AAAA address")" -ge 1 ]]; then debug "found IPv6 record for ${d}" found_ip=true elif [[ "$(nslookup $DNS_CHECK_OPTIONS "${d}"| grep -c ^Name)" -ge 1 ]]; then debug "found IPv4 record for ${d}" found_ip=true fi fi if [[ "$found_ip" == "false" ]]; then info "${DOMAIN}: DNS lookup failed for $d" config_errors=true fi fi # end using dns-01 challenge ((dn++)) done # tidy up rm -f "$tmplist" if [[ "$config_errors" == "true" ]]; then error_exit "${DOMAIN}: exiting due to config errors" fi debug "${DOMAIN}: check_config completed - all OK" } check_getssl_upgrade() { # check if a more recent release is available # Check GitHub for latest stable release, or a specified tag if [[ -n "$_UPGRADE_TO_TAG" ]]; then RELEASE_API="$RELEASE_API/tags/$_UPGRADE_TO_TAG" fi local release_data release_tag release_ver local_ver release_desc NEWCMD debug "Checking for releases at $RELEASE_API" # shellcheck disable=SC2086 release_data="$(curl ${_NOMETER:---silent} --user-agent "$CURL_USERAGENT" -H 'Accept: application/vnd.github.v3+json' "$RELEASE_API")" errcode=$? if [[ $errcode -eq 60 ]]; then error_exit "curl needs updating, your version does not support SNI (multiple SSL domains on a single IP)" elif [[ $errcode -gt 0 ]]; then error_exit "curl error checking releases: $errcode" fi # Replace error in release description with _error (which is ignored by check_output_for_errors() in the tests) sanitised_release_data=${release_data//error/_error} sanitised_release_data=${sanitised_release_data//warning/_warning} debug "${sanitised_release_data//error/_error}" # awk from https://stackoverflow.com/questions/1761341/awk-print-next-record-following-matched-record release_tag=$(awk -F'"' '/tag_name/ {f=NR} f&&NR-1==f' RS=":|," <<<"${release_data}" | sed -e's/"//g') if [[ "${release_tag:0:1}" != 'v' ]] ; then if [[ ${_MUTE} -eq 0 ]]; then info "The current repository has no releases or is improperly tagged; can't check for upgrades: '$release_tag'" fi return 0 fi release_ver="$( tr -d '.v' <<<"${release_tag}")" local_ver="$( tr -d '.' <<<"${VERSION}")" debug "current code is version ${VERSION}" debug "Most recent version is ${release_tag:1}" if [[ -z "$_UPGRADE_TO_TAG" ]] ; then if [[ "$local_ver" -ge "$release_ver" ]] ; then return 0; fi else if [[ "$local_ver" -eq "$release_ver" ]] ; then return 0; fi fi if [[ ${_UPGRADE} -ne 1 ]]; then if [[ ${_MUTE} -eq 0 ]]; then release_desc="$(sed -e'/^"body": *"/!d;s/^"body": *"\([^""]*\).*$/\1/;s/\\r/\r/g;s/\\n/\n/g' <<<"$release_data")" info "" info "A more recent version (${release_tag}) than $VERSION of getssl is available, please update" info "The easiest way is to use the -u or --upgrade flag" info "" info "Release ${release_tag} summary" # Replace error in release description with _error (which is ignored by check_output_for_errors() in the tests) info "${release_desc//error/_error}" info "" fi return 0; fi # Download the latest tag TEMP_UPGRADE_FILE="$(mktemp 2>/dev/null || mktemp -t getssl.XXXXXX)" if [ "$TEMP_UPGRADE_FILE" == "" ]; then error_exit "mktemp failed" fi CODE_LOCATION=$(sed -e"s/master/${release_tag}/" <<<"$CODE_LOCATION") # shellcheck disable=SC2086 debug curl ${_NOMETER:---silent} --user-agent "$CURL_USERAGENT" "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE" # shellcheck disable=SC2086 status=$(curl ${_NOMETER:---silent} -w "%{http_code}" --user-agent "$CURL_USERAGENT" "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE") errcode=$? debug curl errcode=$errcode if [[ $errcode -eq 60 ]]; then error_exit "curl needs updating, your version does not support SNI (multiple SSL domains on a single IP)" elif [[ $errcode -gt 0 ]]; then error_exit "curl error downloading release: $errcode" fi if [[ $status -ne 200 ]]; then error_exit "curl didn't find the updated version of getssl at $CODE_LOCATION" fi if ! install "$0" "${0}.v${VERSION}"; then error_exit "problem renaming old version while updating, check permissions" fi if ! install -m 700 "$TEMP_UPGRADE_FILE" "$0"; then error_exit "problem installing new version while updating, check permissions" fi check=$(bash "$0" -U -v) release_tag_upper=$(echo "$release_tag" | tr "[:lower:]" "[:upper:]") if [[ "$check" != "getssl ${release_tag_upper}" ]]; then info "problem running new version, rolling back to old version" if ! install "${0}.v${VERSION}" "$0"; then error_exit "problem rolling back, you'll need to manually check $0 and $0.${VERSION}" fi error_exit "problem calling new version; output of $TEMP_UPGRADE_FILE -v was \"$check\", expected \"getssl ${release_tag_upper}\"" fi if [[ ${_MUTE} -eq 0 ]]; then echo "Updated getssl from v${VERSION} to ${release_tag}" echo "The old version remains as ${0}.v${VERSION} and should be removed" echo "These update notifications can be turned off using the -Q option" echo "" echo "Updates are:" awk "/\(${VERSION}\)$/ {s=1} s; /\(${release_tag}\)$/ || /^# ----/ {s=0}" "$TEMP_UPGRADE_FILE" | awk '{if(NR>1)print}' echo "" fi # Delete old versions, but not the version just upgraded (which can't be removed since disappearing can confuse bash) declare -a getssl_versions shopt -s nullglob for getssl_version in "$0".v*; do if [[ "$getssl_version" != "${0}.v${VERSION}" ]] ; then getssl_versions[${#getssl_versions[@]}]="$getssl_version" fi done shopt -u nullglob if [[ -n "${getssl_versions[*]}" ]] ; then rm "${getssl_versions[@]}" fi # Inhibit check for upgrades when running the new version NEWCMD="$(sed -e's/ -\(u\|-upgrade\|U\|-nocheck\)//g;s/^\([^ ]* \)/\1--nocheck /' <<<"$ORIGCMD")" clean_up if [[ ${_MUTE} -eq 0 ]]; then info "Installed $release_tag, restarting with $NEWCMD" fi if ! eval "$NEWCMD"; then error_exit "Running upgraded getssl failed" fi graceful_exit } check_version() { # true if version string $1 >= $2 local v1 v2 i n1 n2 n # $1 and $2 can be different lengths, but all parts must be numeric if [[ "$1" == "$2" ]] ; then return 0; fi local IFS='.' # shellcheck disable=SC2206 v1=($1) # shellcheck disable=SC2206 v2=($2) n1="${#v1[@]}" n2="${#v2[@]}" if [[ "$n1" -ge "$n2" ]] ; then n="$n1" ; else n="$n2" ; fi for ((i=0; i/dev/null 2>&1 ; then error_exit "problem copying file to the server using scp. scp $from ${to:4}" fi debug "userid $TOKEN_USER_ID" if [[ "$cert" == "challenge token" ]] && [[ -n "$TOKEN_USER_ID" ]]; then servername=$(echo "$to" | awk -F":" '{print $2}') tofile=$(echo "$to" | awk -F":" '{print $3}') debug "servername $servername" debug "file $tofile" # shellcheck disable=SC2029 # shellcheck disable=SC2086 ssh $SSH_OPTS "$servername" "chown $TOKEN_USER_ID $tofile" fi elif [[ "${to:0:4}" == "ftp:" ]] ; then if [[ "$cert" != "challenge token" ]] ; then error_exit "ftp is not a secure method for copying certificates or keys" fi if [[ -z "$FTP_COMMAND" ]]; then error_exit "No ftp command found" fi debug "using ftp to copy the file from $from" ftpuser=$(echo "$to"| awk -F: '{print $2}') ftppass=$(echo "$to"| awk -F: '{print $3}') ftphost=$(echo "$to"| awk -F: '{print $4}') ftplocn=$(echo "$to"| awk -F: '{print $5}') ftpdirn=$(dirname "$ftplocn") ftpfile=$(basename "$ftplocn") fromdir=$(dirname "$from") fromfile=$(basename "$from") debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost port=$FTP_PORT dir=$ftpdirn file=$ftpfile" debug "from dir=$fromdir file=$fromfile" if [ -n "$FTP_OPTIONS" ]; then # Use eval to expand any variables in FTP_OPTIONS FTP_OPTIONS=$(eval echo "$FTP_OPTIONS") debug "FTP_OPTIONS=$FTP_OPTIONS" fi $FTP_COMMAND <<- _EOF open $ftphost $FTP_PORT user $ftpuser $ftppass $FTP_OPTIONS cd $ftpdirn lcd $fromdir put ./$fromfile _EOF elif [[ "${to:0:5}" == "sftp:" ]] ; then debug "using sftp to copy the file from $from" ftpuser=$(echo "$to"| awk -F: '{print $2}') ftppass=$(echo "$to"| awk -F: '{print $3}') ftphost=$(echo "$to"| awk -F: '{print $4}') ftplocn=$(echo "$to"| awk -F: '{print $5}') ftpdirn=$(dirname "$ftplocn") ftpfile=$(basename "$ftplocn") fromdir=$(dirname "$from") fromfile=$(basename "$from") if [ -n "$FTP_PORT" ]; then SFTP_PORT="-P $FTP_PORT"; else SFTP_PORT=""; fi debug "sftp $SFTP_OPTS user=$ftpuser - pass=$ftppass - host=$ftphost port=$FTP_PORT dir=$ftpdirn file=$ftpfile" debug "from dir=$fromdir file=$fromfile" # shellcheck disable=SC2086 sshpass -p "$ftppass" sftp $SFTP_OPTS $SFTP_PORT "$ftpuser@$ftphost" <<- _EOF cd $ftpdirn lcd $fromdir put ./$fromfile _EOF elif [[ "${to:0:5}" == "davs:" ]] ; then debug "using davs to copy the file from $from" davsuser=$(echo "$to"| awk -F: '{print $2}') davspass=$(echo "$to"| awk -F: '{print $3}') davshost=$(echo "$to"| awk -F: '{print $4}') davsport=$(echo "$to"| awk -F: '{print $5}') davslocn=$(echo "$to"| awk -F: '{print $6}') davsdirn=$(dirname "$davslocn") davsdirn=$(echo "${davsdirn}/" | sed 's,//,/,g') davsfile=$(basename "$davslocn") fromdir=$(dirname "$from") fromfile=$(basename "$from") debug "davs user=$davsuser - pass=$davspass - host=$davshost port=$davsport dir=$davsdirn file=$davsfile" debug "from dir=$fromdir file=$fromfile" # shellcheck disable=SC2086 curl ${_NOMETER} -u "${davsuser}:${davspass}" -T "${fromdir}/${fromfile}" "https://${davshost}:${davsport}${davsdirn}${davsfile}" elif [[ "${to:0:6}" == "ftpes:" ]] || [[ "${to:0:5}" == "ftps:" ]] ; then # FTPES (FTP over explicit TLS/SSL, port 21) and FTPS (FTP over implicit TLS/SSL, port 990). debug "using ${to:0:5} to copy the file from $from" ftpuser=$(echo "$to"| awk -F: '{print $2}') ftppass=$(echo "$to"| awk -F: '{print $3}') ftphost=$(echo "$to"| awk -F: '{print $4}') ftplocn=$(echo "$to"| awk -F: '{print $5}') ftpdirn=$(dirname "$ftplocn") ftpfile=$(basename "$ftplocn") fromdir=$(dirname "$from") fromfile=$(basename "$from") SFTP_PORT=""; if [ -n "$FTP_PORT" ]; then SFTP_PORT=":${FTP_PORT}"; fi debug "${to:0:5} user=$ftpuser - pass=$ftppass - host=$ftphost port=$FTP_PORT dir=$ftpdirn file=$ftpfile" debug "from dir=$fromdir file=$fromfile" if [[ "${to:0:5}" == "ftps:" ]] ; then # if no FTP_PORT is specified, then use default if [ -z "$FTP_PORT" ]; then SFTP_PORT=":990" fi # shellcheck disable=SC2086 debug curl ${_NOMETER} $FTPS_OPTIONS --ftp-ssl --ftp-ssl-reqd -u "${ftpuser}:${ftppass}" -T "${fromdir}/${fromfile}" "ftps://${ftphost}${SFTP_PORT}/${ftpdirn}/" # shellcheck disable=SC2086 curl ${_NOMETER} $FTPS_OPTIONS --ftp-ssl-reqd -u "${ftpuser}:${ftppass}" -T "${fromdir}/${fromfile}" "ftps://${ftphost}${SFTP_PORT}/${ftpdirn}/" else # shellcheck disable=SC2086 debug curl ${_NOMETER} $FTPS_OPTIONS --ftp-ssl --ftp-ssl-reqd -u "${ftpuser}:${ftppass}" -T "${fromdir}/${fromfile}" "ftp://${ftphost}${SFTP_PORT}/${ftpdirn}/" # shellcheck disable=SC2086 curl ${_NOMETER} $FTPS_OPTIONS --ftp-ssl-reqd -u "${ftpuser}:${ftppass}" -T "${fromdir}/${fromfile}" "ftp://${ftphost}${SFTP_PORT}/${ftpdirn}/" fi else if ! mkdir -p "$(dirname "$to")" ; then error_exit "cannot create ACL directory $(basename "$to")" fi if [[ "$GETSSL_IGNORE_CP_PRESERVE" == "true" ]]; then if ! cp "$from" "$to" ; then error_exit "cannot copy $from to $to" fi else if ! cp -p "$from" "$to" ; then error_exit "cannot copy $from to $to" fi fi if [[ "$cert" == "challenge token" ]] && [[ -n "$TOKEN_USER_ID" ]]; then chown "$TOKEN_USER_ID" "$to" fi fi debug "copied $from to $to" done } create_csr() { # create a csr using a given key (if it doesn't already exist) csr_file=$1 csr_key=$2 # check if domain csr exists - if not then create it if [[ -s "$csr_file" ]]; then debug "domain csr exists at - $csr_file" # check all domains in config are in csr if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then read -d '\n' -r -a alldomains <<< "$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u)" else read -d '\n' -r -a alldomains <<< "$(echo "$DOMAIN,$SANS" | sed -e 's/,/ /g; s/ $//; y/ /\n/' | sort -u)" fi domains_in_csr=$(openssl req -text -noout -in "$csr_file" \ | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ | sort -u) for d in "${alldomains[@]}"; do if [[ "$(echo "${domains_in_csr}"| grep "^${d}$")" != "${d}" ]]; then info "existing csr at $csr_file does not contain ${d} - re-create-csr"\ ".... $(echo "${domains_in_csr}"| grep "^${d}$")" _RECREATE_CSR=1 fi done # check all domains in csr are in config if [[ "$(IFS=$'\n'; echo -n "${alldomains[*]}")" != "$domains_in_csr" ]]; then info "existing csr at $csr_file does not have the same domains as the config - re-create-csr" _RECREATE_CSR=1 else debug "Existing csr at $csr_file contains same domains as the config" fi fi # end of ... check if domain csr exists - if not then create it # if CSR does not exist, or flag set to recreate, then create csr if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "1" ]]; then info "creating domain csr - $csr_file" # create a temporary config file, for portability. tmp_conf=$(mktemp 2>/dev/null || mktemp -t getssl) || error_exit "mktemp failed" cat "$SSLCONF" > "$tmp_conf" printf "\n[SAN]\n%s" "$SANLIST" >> "$tmp_conf" # add OCSP Must-Staple to the domain csr # if openssl version >= 1.1.0 one can also use "tlsfeature = status_request" if [[ "$OCSP_MUST_STAPLE" == "true" ]]; then printf "\n1.3.6.1.5.5.7.1.24 = DER:30:03:02:01:05" >> "$tmp_conf" fi openssl req -new -sha256 -key "$csr_key" -subj "$CSR_SUBJECT" -reqexts SAN -config "$tmp_conf" > "$csr_file" rm -f "$tmp_conf" fi } create_key() { # create a domain key (if it doesn't already exist) key_type=$1 # domain key type key_loc=$2 # domain key location key_len=$3 # domain key length - for rsa keys. # check if key exists, if not then create it. if [[ -s "$key_loc" ]]; then debug "domain key exists at $key_loc - skipping generation" # ideally need to check validity of domain key else umask 077 info "creating key - $key_loc" case "$key_type" in rsa) openssl genrsa "$key_len" > "$key_loc";; prime256v1|secp384r1|secp521r1) openssl ecparam -genkey -name "$key_type" > "$key_loc";; *) error_exit "unknown private key algorithm type $key_loc";; esac umask "$ORIG_UMASK" # remove csr on generation of new domain key if [[ -e "${key_loc%.*}.csr" ]]; then rm -f "${key_loc%.*}.csr" fi fi } create_order() { dstring="[" for d in "${alldomains[@]}"; do dstring="${dstring}{\"type\":\"dns\",\"value\":\"$d\"}," done dstring="${dstring::${#dstring}-1}]" replaces="${_REPLACES:+, \"replaces\": \"${_REPLACES}\"}" # Check if the server supports profiles using the URL_profiles variable if [[ -z "$URL_profiles" ]]; then request="{\"identifiers\": $dstring$replaces}" else request="{\"identifiers\": $dstring, \"profile\": \"$PROFILE\"$replaces}" fi send_signed_request "$URL_newOrder" "$request" OrderLink=$(echo "$responseHeaders" | grep -i location | awk '{print $2}'| tr -d '\r\n ') debug "Order link $OrderLink" FinalizeLink=$(json_get "$response" "finalize") debug "Finalize link $FinalizeLink" if [[ $API -eq 1 ]]; then dn=0 for d in "${alldomains[@]}"; do # get authorizations link AuthLink[dn]=$(json_get "$response" "identifiers" "value" "${d##\*.}" "authorizations" "x") debug "authorizations link for $d - ${AuthLink[$dn]}" ((dn++)) done else # Authorization links are unsorted, so fetch the authorization link, find the domain, save response in the correct array position AuthLinks=$(json_get "$response" "authorizations") AuthLinkResponse=() AuthLinkResponseHeader=() for l in $AuthLinks; do debug "Requesting authorizations link for $l" send_signed_request "$l" "" # Get domain from response authdomain=$(json_get "$response" "identifier" "value") wildcard=$(json_get "$response" "wildcard") debug wildcard="$wildcard" # find array position (This is O(n2) but doubt that we'll see performance issues) dn=0 for d in "${alldomains[@]}"; do # Convert domain to lowercase as response from server will be in lowercase lower_d=$(echo "$d" | tr "[:upper:]" "[:lower:]") if [[ ( "$lower_d" == "$authdomain" && "$wildcard" != "true" ) || ( "$lower_d" == "*.${authdomain}" && "$wildcard" == "true" ) ]]; then debug "Saving authorization response for $authdomain for domain alldomains[$dn]" debug "Response = ${response//[$'\t\r\n']}" AuthLinkResponse[dn]=$response AuthLinkResponseHeader[dn]=$responseHeaders fi ((dn++)) done if [[ "$DEACTIVATE_AUTH" == "true" ]]; then deactivate_url_list+=" $l " debug "url added to deactivate list ${l}" debug "deactivate list is now $deactivate_url_list" fi done fi } renew_ari() { # Return "1" if ARI says we should renew # If cert has expired, don't fetch ARI info and just say we should renew if [[ "$1" -lt "$(date +s)" ]]; then debug "Certificate expired, not fetching ARI info" return 0 fi # Construct the certificate identifier which is the base64url encoding # of the authority key identifier and serial number aki=$(openssl x509 -noout -text -in "$CERT_FILE" 2>/dev/null | grep "Authority Key Identifier" -A1 | \ sed 's/^.*keyid://' | \ grep -E '^[A-Fa-f0-9: ]+$' | \ tr -d ':' | \ hex2bin | \ urlbase64) # Extract the hex serial number from openssl output. OpenSSL 3.x puts it # on the same line as "Serial Number:" (as "(0xHEX)"), while older # versions put colon-separated hex on the following line. serial=$(openssl x509 -noout -text -in "$CERT_FILE" 2>/dev/null | \ grep "Serial Number" | \ grep -oiE '0x[0-9a-f]+' | \ cut -dx -f2) if [[ -z "$serial" ]]; then serial=$(openssl x509 -noout -text -in "$CERT_FILE" 2>/dev/null | \ grep "Serial Number" -A1 | \ grep -E '^[A-Fa-f0-9: ]+$' | \ tr -d ': ') fi # If the high bit is set then we need to prepend an extra byte so # the DER encoding is positive if printf "%s\n" "$serial" | grep -q -E '^[89ABCDEFabcdef]'; then serial="00$serial" fi serialb64=$(printf "%s\n" "$serial" | hex2bin | urlbase64) debug "Authority key id is $aki, serial is $serialb64" # shellcheck disable=SC2086 response=$(curl ${_NOMETER} --user-agent "$CURL_USERAGENT" --silent "${URL_renewInfo}/${aki}.${serialb64}") debug "ARI response is $response" _ARI_STARTTIME=$(json_get "$response" suggestedWindow start) if [[ -z "$_ARI_STARTTIME" ]]; then info "Warning: ARI query returned $response" info "Not processing ARI renewal info" return 1 fi debug "Renewal window starts at $_ARI_STARTTIME" if [[ "$(date +%s)" -gt $(date_rfc3339 "$_ARI_STARTTIME") ]]; then debug "Within ARI renewal window, using ARI" _REPLACES="${aki}.${serialb64}" return 0 fi debug "Not within ARI renewal window" return 1 } date_rfc3339() { # convert rfc3339 format date into epoch time # shellcheck disable=SC2018,SC2019 convdate=$(printf "%s\n" "$1" | tr a-z A-Z) if [[ "${convdate: -1:1}" == 'Z' ]]; then convdate="${convdate:0:${#convdate}-1}+0000" else convdate=$(printf "%s\n" "$convdate" | sed -e 's/:\([0-9][0-9]\)$/\1/') fi if [[ "$os" == "bsd" ]]; then date -j -f "%Y-%m-%dT%H:%M:%S%z" "$convdate" +%s elif [[ "$os" == "mac" ]]; then date -j -f "%Y-%m-%dT%H:%M:%S%z" "$convdate" +%s elif [[ "$os" == "busybox" ]]; then # busybox doesn't support timezones in the input format convdate=$(printf "%s\n" "$convdate" | sed 's/[+-][0-9]\{4\}$//') date -D "%Y-%m-%dT%H:%M:%S" -d "$convdate" +%s else # convert date "T