diff --git a/.editorconfig b/.editorconfig index 91d16ec..5abccad 100644 --- a/.editorconfig +++ b/.editorconfig @@ -7,3 +7,6 @@ end_of_line = lf insert_final_newline = true indent_style = space indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitattributes b/.gitattributes index 21cd744..7653b18 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,3 +1,11 @@ # Files not to include in .zip/.tar.gz archives # .git* export-ignore + +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto + +# Make all text files lf formatted +* text eol=lf + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..135ea05 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,30 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Operating system (please complete the following information):** + +- OS: [e.g. Debian 9, Ubuntu 18.04, freeBSD ] +- Bash Version [e.g. GNU bash, version 4.4.12(1)-release (x86_64-pc-linux-gnu)] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..425aacb --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/run-tests-pebble.yml b/.github/workflows/run-tests-pebble.yml new file mode 100644 index 0000000..5dc53a4 --- /dev/null +++ b/.github/workflows/run-tests-pebble.yml @@ -0,0 +1,97 @@ +name: Run all tests on pebble +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + test-alpine: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Alpine + run: test/run-test.sh alpine + test-bash-4-0: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Alpine using Bash 4.0 + run: test/run-test.sh bash4-0 + test-bash-4-2: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Alpine using Bash 4.2 + run: test/run-test.sh bash4-2 + test-bash-5-0: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Alpine using Bash 5 + run: test/run-test.sh bash5-0 + test-centos6: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on CentOS6 + run: test/run-test.sh centos6 + test-centos7: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on CentOS7 + run: test/run-test.sh centos7 + test-centos8: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on CentOS8 + run: test/run-test.sh centos8 + test-debian: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Debian + run: test/run-test.sh debian + test-ubuntu: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Ubuntu + run: test/run-test.sh ubuntu + test-ubuntu16: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Ubuntu16 + run: test/run-test.sh ubuntu16 + test-ubuntu18: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Ubuntu18 + run: test/run-test.sh ubuntu18 diff --git a/.github/workflows/run-tests-staging-duckdns.yml b/.github/workflows/run-tests-staging-duckdns.yml new file mode 100644 index 0000000..54da950 --- /dev/null +++ b/.github/workflows/run-tests-staging-duckdns.yml @@ -0,0 +1,25 @@ +name: Run all tests using DuckDNS +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + test-centos7-duckdns: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on CentOS7 against Staging using DuckDNS + run: test/run-test.sh centos7-duckdns + test-ubuntu-duckdns: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Ubuntu against Staging using DuckDNS + run: test/run-test.sh ubuntu-duckdns diff --git a/.github/workflows/run-tests-staging-dynu.yml b/.github/workflows/run-tests-staging-dynu.yml new file mode 100644 index 0000000..c523278 --- /dev/null +++ b/.github/workflows/run-tests-staging-dynu.yml @@ -0,0 +1,25 @@ +name: Run all tests using Dynu +on: + push: + branches: + - master + pull_request: + branches: + - master +jobs: + test-centos7-dynu: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on CentOS7 against Staging using Dynu + run: test/run-test.sh centos7-dynu + test-ubuntu-dynu: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Build the docker-compose stack + run: docker-compose up -d --build + - name: Run test suite on Ubuntu against Staging using Dynu + run: test/run-test.sh ubuntu-dynu diff --git a/.github/workflows/shellcheck.yml b/.github/workflows/shellcheck.yml new file mode 100644 index 0000000..37b9cad --- /dev/null +++ b/.github/workflows/shellcheck.yml @@ -0,0 +1,17 @@ +name: shellcheck + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Lint check + uses: azohra/shell-linter@v0.3.0 + with: + path: "getssl" diff --git a/.github/workflows/stale2.yml b/.github/workflows/stale2.yml new file mode 100644 index 0000000..ce6f9c1 --- /dev/null +++ b/.github/workflows/stale2.yml @@ -0,0 +1,31 @@ +on: + schedule: + - cron: "0 0 * * *" + +name: Run Stale Bot on Issue Comments + +jobs: + build: + name: stale + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: stale + uses: gatsbyjs/stale@master + with: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + DRY_RUN: true + DAYS_BEFORE_STALE: 60 + DAYS_BEFORE_CLOSE: 30 + STALE_ISSUE_LABEL: 'stale' + STALE_PR_LABEL: 'stale' + OPERATIONS_PER_RUN: 30 + STALE_ISSUE_MESSAGE: 'This issue will be closed as no updates for 60 days' + CLOSE_MESSAGE: 'Closing stale issue after 90 days of inactivity' + EXEMPT_ISSUE_LABELS: | + bug + documentation + enhancement + feature + help wanted + rfc diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 867a6d2..0000000 --- a/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -language: bash - -# Use container-based infrastructure for quicker build start-up -sudo: false - -addons: - apt: - sources: - - debian-sid # Grab shellcheck from the Debian repo (o_O) - packages: - - shellcheck - -script: - - bash -c 'shopt -s globstar; shellcheck getssl' - -matrix: - fast_finish: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 482a7aa..3c8242c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,24 +1,31 @@ # How to contribute -If you are happy writing in bash, please create a PR for any changes you'd like to see included (or bug fixes). +If you are happy writing in bash, please create a PR for any changes +you'd like to see included (or bug fixes). -If you aren't happy writing in bash, please open an issue with as much detail as possible about the issue or what you'd like to see added / improved. +If you aren't happy writing in bash, please open an issue with as much +detail as possible about the issue or what you'd like to see added / +improved. ## Submitting changes -Please update the 'revision history' and version number at the top of the code (without this I can't easily do a merge) +Please update the 'revision history' and version number at the top of +the code (without this I can't easily do a merge) -Please update just one issue per PR. If there are multiple issues, please provide separate PR's one per issue. +Please update just one issue per PR. If there are multiple issues, +please provide separate PR's one per issue. ## Coding conventions -Please see the guidelines at https://github.com/srvrco/getssl/wiki/Bash-Style-guide +Please see the guidelines at ## Testing -Please test with [shellcheck](https://github.com/koalaman/shellcheck), although this will also be tested on github ( via travis) on all PRs. +Please test with [shellcheck](https://github.com/koalaman/shellcheck), +although this will also be tested on github (via travis) on all PRs. -Please remember that the system is used across a wide range of platforms, so if you have access to multiple operating systems, please test on all. +Please remember that the system is used across a wide range of +platforms, so if you have access to multiple operating systems, please +test on all. - -Thanks :) +Thanks :) diff --git a/README.md b/README.md index 879d8f3..7ff8b2e 100644 --- a/README.md +++ b/README.md @@ -1,93 +1,155 @@ -# getssl -Obtain SSL certificates from the letsencrypt.org ACME server. Suitable for automating the process on remote servers. +# getssl + +![Run all tests](https://github.com/srvrco/getssl/workflows/Run%20all%20tests/badge.svg) ![shellcheck](https://github.com/srvrco/getssl/workflows/shellcheck/badge.svg) + +Obtain SSL certificates from the letsencrypt.org ACME server. Suitable +for automating the process on remote servers. + +## Table of Contents +- [Features](#features) +- [Installation](#installation) +- [Overview](#overview) +- [Getting started](#getting-started) +- [Detailed guide to getting started with more examples](#detailed-guide-to-getting-started-with-more-examples) +- [Wildcard certificates](#wildcard-certificates) +- [Automating updates](#automating-updates) +- [Structure](#structure) +- [Server-Types](#server-types) +- [Revoke a certificate](#revoke-a-certificate) +- [Elliptic curve keys](#elliptic-curve-keys) +- [Preferred Chain](#preferred-chain) +- [Include Root certificate in full chain](#include-root-certificate-in-full-chain) +- [Issues / problems / help](#issues--problems--help) ## Features -* **Bash** - It runs on virtually all unix machines, including BSD, most Linux distributions, MAC OSX. -* **Get certificates for remote servers** - The tokens used to provide validation of domain ownership, and the certificates themselves can be automatically copied to remote servers (via ssh, sftp or ftp for tokens). The script doesn't need to run on the server itself. This can be useful if you don't have access to run such scripts on the server itself, e.g. if it's a shared server. -* **Runs as a daily cron** - so certificates will be automatically renewed when required. + +* **Bash** - It runs on virtually all unix machines, including BSD, most + Linux distributions, macOS. +* **Get certificates for remote servers** - The tokens used to provide + validation of domain ownership, and the certificates themselves can be + automatically copied to remote servers (via ssh, sftp or ftp for + tokens). The script doesn't need to run on the server itself. This can + be useful if you don't have access to run such scripts on the server + itself, e.g. if it's a shared server. +* **Runs as a daily cron** - so certificates will be automatically + renewed when required. * **Automatic certificate renewals** -* **Checks certificates are correctly loaded**. After installation of a new certificate it will test the port specified ( see [Server-Types](#server-types) for options ) that the certificate is actually being used correctly. -* **Automatically updates** - The script can automatically update itself with bug fixes etc if required. -* **Extensively configurable** - With a simple configuration file for each certificate it is possible to configure it exactly for your needs, whether a simple single domain or multiple domains across multiple servers on the same certificate. +* **Checks certificates are correctly loaded** - After installation of a + new certificate it will test the port specified ( see + [Server-Types](#server-types) for options ) that the certificate is + actually being used correctly. +* **Automatically updates** - The script can automatically update itself + with bug fixes etc if required. +* **Extensively configurable** - With a simple configuration file for + each certificate it is possible to configure it exactly for your + needs, whether a simple single domain or multiple domains across + multiple servers on the same certificate. * **Supports http and dns challenges** - Full ACME implementation * **Simple and easy to use** -* **Detailed debug info** - Whilst it shouldn't be needed, detailed debug information is available. -* **Reload services** - After a new certificate is obtained then the relevant services (e.g. apache/nginx/postfix) can be reloaded. +* **Detailed debug info** - Whilst it shouldn't be needed, detailed + debug information is available. +* **Reload services** - After a new certificate is obtained then the + relevant services (e.g. apache/nginx/postfix) can be reloaded. +* **ACME v1 and V2** - Supports both ACME versions 1 and 2 (note ACMEv1 is deprecated and clients will automatically use v2) ## Installation -Since the script is only one file, you can use the following command for a quick installation of GetSSL only: -``` + +Since the script is only one file, you can use the following command for +a quick installation of GetSSL only: + +```sh curl --silent https://raw.githubusercontent.com/srvrco/getssl/master/getssl > getssl ; chmod 700 getssl ``` -This will copy the getssl Bash script to the current location and change the permissions to make it executable for you. -For a more comprehensive installation (e.g. install also helper scripts) use the provided Makefile with each release tarball. Use the `install` target. +This will copy the getssl Bash script to the current location and change +the permissions to make it executable for you. + +For a more comprehensive installation (e.g. install also helper scripts) +use the provided Makefile with each release tarball. Use the `install` +target. You'll find the latest version in the git repository: -``` +```sh git clone https://github.com/srvrco/getssl.git ``` -For Arch Linux there are packages in the AUR, see [here](https://aur.archlinux.org/packages/getssl/) and [there](https://aur.archlinux.org/packages/getssl-git/). +For Arch Linux there are packages in the AUR, see +[here](https://aur.archlinux.org/packages/getssl/) and +[there](https://aur.archlinux.org/packages/getssl-git/). -If you use puppet, there is a [GetSSL Puppet module](https://github.com/dthielking/puppet_getssl) by dthielking +If you use puppet, there is a [GetSSL Puppet +module](https://github.com/dthielking/puppet_getssl) by dthielking ## Overview -GetSSL was written in standard bash ( so it can be run on a server, a desktop computer, or even a virtualbox) and add the checks, and certificates to a remote server ( providing you have a ssh with key, sftp or ftp access to the remote server). +GetSSL was written in standard bash ( so it can be run on a server, a +desktop computer, or even a virtualbox) and add the checks, and +certificates to a remote server ( providing you have a ssh with key, +sftp or ftp access to the remote server). -``` -getssl ver. 2.02 +```getssl -h +getssl ver. 2.35 Obtain SSL certificates from the letsencrypt.org ACME server -Usage: getssl [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet] [-Q|--mute] [-u|--upgrade] [-k|--keep #] [-U|--nocheck] [-r|--revoke cert key] [-w working_dir] domain +Usage: getssl [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet] [-Q|--mute] [-u|--upgrade] [-k|--keep #] [-U|--nocheck] [-r|--revoke cert key] [-w working_dir] [--preferred-chain chain] domain Options: -a, --all Check all certificates - -d, --debug Outputs debug information + -d, --debug Output debug information -c, --create Create default config files -f, --force Force renewal of cert (overrides expiry checks) -h, --help Display this help message and exit + -i, --install Install certificates and reload service -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) - -Q, --mute Like -q, but mutes notification about successful upgrade + -Q, --mute Like -q, but also mute notification about successful upgrade -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) - -u, --upgrade Upgrade getssl if a more recent version is available - -k, --keep "#" Maximum amount of old getssl versions to keep when upgrading + -u, --upgrade Upgrade getssl if a more recent version is available - can be used with or without domain(s) + -k, --keep "#" Maximum number of old getssl versions to keep when upgrading -U, --nocheck Do not check if a more recent version is available -w working_dir "Working directory" + --preferred-chain "chain" Use an alternate chain for the certificate ``` ## Getting started Once you have obtained the script (see Installation above), the next step is to use -```./getssl -c yourdomain.com``` +```sh +./getssl -c yourdomain.com +``` -where yourdomain.com is the primary domain name that you want to create a certificate for. This will create the following folders and files. +where yourdomain.com is the primary domain name that you want to create +a certificate for. This will create the following folders and files. -``` +```sh ~/.getssl ~/.getssl/getssl.cfg ~/.getssl/yourdomain.com ~/.getssl/yourdomain.com/getssl.cfg ``` -You can then edit ~/.getssl/getssl.cfg to set the values you want as the default for the majority of your certificates. +You can then edit `~/.getssl/getssl.cfg` to set the values you want as the +default for the majority of your certificates. -Then edit ~/.getssl/yourdomain.com/getssl.cfg to have the values you want for this specific domain (make sure to uncomment and specify correct `ACL` option, since it is required). +Then edit `~/.getssl/yourdomain.com/getssl.cfg` to have the values you +want for this specific domain (make sure to uncomment and specify +correct `ACL` option, since it is required). -You can then just run; +You can then just run: -```getssl yourdomain.com ``` - -and it should run, providing output like; +```sh +getssl yourdomain.com ``` + +and it should run, providing output like: + +```sh Registering account Verify each domain -Verifing yourdomain.com +Verifying yourdomain.com Verified yourdomain.com -Verifing www.yourdomain.com +Verifying www.yourdomain.com Verified www.yourdomain.com Verification completed, obtaining certificate. Certificate saved in /home/user/.getssl/yourdomain.com/yourdomain.com.crt @@ -97,35 +159,76 @@ copying private key to ssh:server5:/home/yourdomain/ssl/domain.key copying CA certificate to ssh:server5:/home/yourdomain/ssl/chain.crt reloading SSL services ``` -**This will (by default) use the staging server, so should give you a certificate that isn't trusted ( Fake Let's Encrypt).** + +**This will (by default) use the staging server, so should give you a +certificate that isn't trusted ( Fake Let's Encrypt).** Change the server in your config file to get a fully valid certificate. -**Note:** Verification is done via port 80 (http), port 443 (https) or dns. The certificate can be used (and checked with getssl) on alternate ports. +**Note:** Verification is done via port 80 (http), port 443 (https) or +dns. The certificate can be used (and checked with getssl) on alternate +ports. -## Automating updates +## Detailed guide to getting started with more examples + +[Guide to getting a certificate for example.com and www.example.com](https://github.com/srvrco/getssl/wiki/Guide-to-getting-a-certificate-for-example.com-and-www.example.com) + +## Wildcard certificates + +`getssl` supports creating wildcard certificates, i.e. _*.example.com_ which allows a single certificate to be used for any domain under *example.com*, e.g. *www.example.com*, *mail.example.com*. These must be validated using the dns-01 method. + +A *partial* example `getssl.cfg` file is: -I use the following cron +```sh +VALIDATE_VIA_DNS=true +export CPANEL_USERNAME='' +export CPANEL_URL='https://www.cpanel.host:2083' +export CPANEL_APITOKEN='1ABC2DEF3GHI4JKL5MNO6PQR7STU8VWX9YZA' +DNS_ADD_COMMAND=/home/root/getssl/dns_scripts/dns_add_cpanel +DNS_DEL_COMMAND=/home/root/getssl/dns_scripts/dns_del_cpanel ``` + +Create the wildcard certificate (need to use quotes to prevent globbing): + +```sh +getssl "*.example.domain" +``` + +You can renew the certificate using `getssl -a` to renew all configured certificates. + +You can also specify additional domains in the `SANS` line, e.g. `SANS="www.test.example.com"`. +This cannot contain any of the domains which would be covered by the wildcard certificate. + +## Automating updates + +I use the following **cron** job + +```cron 23 5 * * * /root/scripts/getssl -u -a -q ``` -The cron will automatically update getssl and renew any certificates, only giving output if there are issues / errors. + +The cron will automatically update getssl and renew any certificates, +only giving output if there are issues / errors. * The -u flag updates getssl if there is a more recent version available. * The -a flag automatically renews any certificates that are due for renewal. -* The -q flag is "quiet" so that it only outputs and emails me if there was an error / issue. +* The -q flag is "quiet" so that it only outputs and emails me if there + was an error / issue. ## Structure -The design aim was to provide flexibility in running the code. The default working directory is ~/.getssl ( which can be modified via the command line) +The design aim was to provide flexibility in running the code. The +default working directory is `~/.getssl` (which can be modified via the +command line). -Within the **working directory** is a config file, getssl.cfg which is a simple bash file containing variables, an example of which is +Within the **working directory** is a config file `getssl.cfg` which is a +simple bash file containing variables, an example of which is: -``` +```sh # Uncomment and modify any variables you need # The staging server is best for testing (hence set as default) -CA="https://acme-staging.api.letsencrypt.org" +CA="https://acme-staging-v02.api.letsencrypt.org" # This server issues full certificates, however has rate limits -#CA="https://acme-v01.api.letsencrypt.org" +#CA="https://acme-v02.api.letsencrypt.org" AGREEMENT="https://letsencrypt.org/documents/LE-SA-v1.0.1-July-27-2015.pdf" @@ -142,17 +245,19 @@ RENEW_ALLOW="30" SSLCONF="/usr/lib/ssl/openssl.cnf" ``` -then, within the **working directory** there will be a folder for each certificate (based on its domain name). Within that folder will be a config file (again called getssl.cfg). An example of which is; +then, within the **working directory** there will be a folder for each +certificate (based on its domain name). Within that folder will be a +config file (again called `getssl.cfg`). An example of which is: -``` +```sh # Uncomment and modify any variables you need # see https://github.com/srvrco/getssl/wiki/Config-variables for details # see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs # # The staging server is best for testing -#CA="https://acme-staging.api.letsencrypt.org" +#CA="https://acme-staging-v02.api.letsencrypt.org" # This server issues full certificates, however has rate limits -#CA="https://acme-v01.api.letsencrypt.org" +#CA="https://acme-v02.api.letsencrypt.org" #AGREEMENT="https://letsencrypt.org/documents/LE-SA-v1.1.1-August-1-2016.pdf" @@ -183,7 +288,9 @@ DOMAIN_KEY_LOCATION="ssh:server5:/etc/ssl/domain.key" #DOMAIN_PEM_LOCATION="" this is the domain_key. domain cert and CA cert -# The command needed to reload apache / nginx or whatever you use +# The command needed to reload apache / nginx or whatever you use. +# Several (ssh) commands may be given using a bash array: +# RELOAD_CMD=('ssh:sshuserid@server5:systemctl reload httpd' 'logger getssl for server5 efficient.') RELOAD_CMD="service apache2 reload" # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, @@ -194,24 +301,32 @@ RELOAD_CMD="service apache2 reload" #CHECK_REMOTE="true" ``` -If a location for a file starts with ssh: it is assumed the next part of the file is the hostname, followed by a colon, and then the path. -Files will be securely copied using scp, and it assumes that you have a key on the server ( for passwordless access). You can set the user, port etc for the server in your .ssh/config file +If a location for a file starts with `ssh:` it is assumed the next part +of the file is the hostname, followed by a colon, and then the path. +Files will be securely copied using scp, and it assumes that you have a +key on the server (for passwordless access). You can set the user, +port etc for the server in your `.ssh/config` file. -If an ACL starts with ftp: or sftp: it as assumed that the line is in the format "ftp:UserID:Password:servername:/path/to/acme-challenge". sftp requires sshpass. -Note: FTP can be used for copying tokens only and can **not** be used for uploading private key or certificates as it's not a secure method of transfer. +If an ACL starts with `ftp:` or `sftp:` it as assumed that the line is +in the format "ftp:UserID:Password:servername:/path/to/acme-challenge". +sftp requires sshpass. +Note: FTP can be used for copying tokens only +and can **not** be used for uploading private key or certificates as +it's not a secure method of transfer. ssh can also be used for the reload command if using on remote servers. Multiple locations can be defined for a file by separating the locations with a semi-colon. +A typical config file for `example.com` and `www.example.com` on the +same server would be: -A typical config file for example.com and www.example.com on the same server would be -``` +```sh # uncomment and modify any variables you need # The staging server is best for testing -CA="https://acme-staging.api.letsencrypt.org" +CA="https://acme-staging-v02.api.letsencrypt.org" # This server issues full certificates, however has rate limits -#CA="https://acme-v01.api.letsencrypt.org" +#CA="https://acme-v02.api.letsencrypt.org" # additional domains - this could be multiple domains / subdomains in a comma separated list SANS="www.example.com" @@ -230,6 +345,7 @@ RELOAD_CMD="service apache2 reload" ``` ## Server-Types + OpenSSL has built-in support for getting the certificate from a number of SSL services these are available in getssl to check if the certificate is installed correctly @@ -251,23 +367,60 @@ these are available in getssl to check if the certificate is installed correctly | ldaps | 636 | | | port number | | | - ## Revoke a certificate In general revoking a certificate is not required. Usage: `getssl -r path/to/cert path/to/key [CA_server]` -You need to specify both the certificate you want to revoke, and the account or private domain key which was used to sign / obtain the original certificate. The CA_server is an optional parameter and defaults to Let's Encrypt ( "https://acme-v01.api.letsencrypt.org" ) as that is currently the only Certificate Authority using the ACME protocol. - +You need to specify both the certificate you want to revoke, and the +account or private domain key which was used to sign / obtain the +original certificate. The `CA_server` is an optional parameter and +defaults to Let's Encrypt ("") as +that is currently the only Certificate Authority using the ACME +protocol. ## Elliptic curve keys -You can use Elliptic curve keys for both the account key and the domain key (different of course, don't use the same key for both). prime256v1 (NIST P-256) and secp384r1 (NIST P-384) are both fully supported. secp521r1 (NIST P-521) is included in the code, but not currently supported by Let's Encrypt). +You can use Elliptic curve keys for both the account key and the domain +key (different of course, don't use the same key for both). prime256v1 +(NIST P-256) and secp384r1 (NIST P-384) are both fully supported. +secp521r1 (NIST P-521) is included in the code, but not currently +supported by Let's Encrypt). + +## Preferred Chain + +If a CA offers multiple chains then it is possible to select which chain +is used by using the `PREFERRED_CHAIN` variable in `getssl.cfg` or specifying + `--preferred-chain` in the call to `getssl` + +This uses wildcard matching so requesting "X1" returns the first certificate +returned by the CA which contains the text "X1", Note you may need to escape +any characters which special characters, e.g. +` PREFERRED_CHAIN="\(STAGING\) Doctored Durian Root CA X3"` + +* Staging options are: "(STAGING) Doctored Durian Root CA X3" and "(STAGING) Pretend Pear X1" +* Production options are: "ISRG Root X1" and "ISRG Root X2" + +## Include Root certificate in full chain + +Some servers, including those that use Java keystores, will not accept a server certificate if it cannot valid the full chain of signers. + +Specifically, Nutanix Prism (Element and Central) will not accept the `fullchain.crt` until the root CA's certificate has been appended to it manually. + +If your application requires the full chain, i.e. including the +root certificate of the CA, then this can be included in the `fullchain.crt` file by +adding the following line to `getssl.cfg` + +```sh +FULL_CHAIN_INCLUDE_ROOT="true" +``` ## Issues / problems / help -If you have any issues, please log them at https://github.com/srvrco/getssl/issues + +If you have any issues, please log them at There are additional help pages on the [wiki](https://github.com/srvrco/getssl/wiki) -If you have any suggestions for improvements then pull requests are welcomed, or raise an issue. +If you have any suggestions for improvements then pull requests are +welcomed, or raise an issue. diff --git a/dns_scripts/Cloudflare-README.md b/dns_scripts/Cloudflare-README.md new file mode 100644 index 0000000..f831cfb --- /dev/null +++ b/dns_scripts/Cloudflare-README.md @@ -0,0 +1,52 @@ +## Using Cloudflare DNS for LetsEncrypt domain validation + +### Enabling the scripts + +Set the following options in `getssl.cfg` (either global or domain-specific): + +``` +VALIDATE_VIA_DNS="true" +DNS_ADD_COMMAND="/usr/share/getssl/dns_scripts/dns_add_cloudflare" +DNS_DEL_COMMAND="/usr/share/getssl/dns_scripts/dns_del_cloudflare" +``` + +### Authentication + +There are 2 methods of authenticating with Cloudflare: + +1. API Keys - Account level, all-purpose tokens +2. API Tokens - Scoped and permissioned access to resources + +Both are configured from your profile in the [Cloudflare dashboard][1] + +[1]: https://dash.cloudflare.com/profile/api-tokens + +#### API Keys + +The **Zone ID** for the domain will be searched for programmatically. + +Set the following options in `getssl.cfg`: + +``` +export CF_EMAIL="..." # Cloudflare account email address +export CF_KEY="..." # Global API Key +``` + +#### API Tokens + +Cloudflare provides a template for creating an API Token with access to edit +zone records. Tokens must be created with at least '**DNS:Edit** permissions +for the domain to add/delete records. + +The API requires higher privileges to be able to list zones, therefore this +method also requires the **Zone ID** from the Overview tab in the Cloudflare +Dashboard. + +Set the following options in the domain-specific `getssl.cfg` + +``` +export CF_API_TOKEN="..." +export CF_ZONE_ID="..." +``` + +__Note__: API Keys will be used instead if also configured diff --git a/dns_scripts/DNS_ROUTE53.md b/dns_scripts/DNS_ROUTE53.md new file mode 100644 index 0000000..30ee075 --- /dev/null +++ b/dns_scripts/DNS_ROUTE53.md @@ -0,0 +1,10 @@ +# Do DNS-01 verification using Route53 + +I was not about to implement this in BASH, sorry guys. I'd like you to have it, however. + +It's pretty simple to use. + +1. pip install boto3 dnspython +2. ln -s dns_route53.py dns_add_route53 +3. ln -s dns_route53.py dns_del_route53 +4. Use it just like the other scripts diff --git a/dns_scripts/GoDaddy-README.txt b/dns_scripts/GoDaddy-README.txt new file mode 100644 index 0000000..d58ba73 --- /dev/null +++ b/dns_scripts/GoDaddy-README.txt @@ -0,0 +1,60 @@ +Using GoDaddy DNS for LetsEncrypt domain validation. + +Quick guide to setting up getssl for domain validation of +GoDaddy DNS domains. + +There are two prerequisites to using getssl with GoDaddy DNS: + +1) Obtain an API access key from developer.godaddy.com + At first sign-up, you will be required to take a "test" key. + This is NOT what you need. Accept it, then get a "Production" + key. At this writing, there is no charge - but you must have + a GoDaddy customer account. + + You must get the API key for the account which owns the domain + that you want to get certificates for. If the domains that you + manage are owned by more than one account, get a key for each. + + The access key consists of a "Key" and a "Secret". You need + both. + +2) Obtain JSON.sh - https://github.com/dominictarr/JSON.sh + +With those in hand, the installation procedure is: + +1) Put JSON.sh in the getssl DNS scripts directory + Default: /usr/share/getssl/dns_scripts + +2) Open your config file (the global file in ~/.getssl/getssl.cfg + or the per-account file in ~/.getssl/example.net/getssl.cfg + +3) Set the following options: + VALIDATE_VIA_DNS="true" + DNS_ADD_COMMAND="/usr/share/getssl/dns_scripts/dns_add_godaddy" + DNS_DEL_COMMAND="/usr/share/getssl/dns_scripts/dns_del_godaddy" + # The API key for your account/this domain + export GODADDY_KEY="..." GODADDY_SECRET="..." + + 4) Set any other options that you wish (per the standard + directions.) Use the test CA to make sure that + everything is setup correctly. + +That's it. getssl example.net will now validate with DNS. + +To trace record additions and removals, run getssl as +GODADDY_TRACE=Y getssl example.net + +There are additional options, which are documented in the +*godaddy" files and dns_godaddy -h. + +Copyright (2017) Timothe Litt litt at acm _dot org + +This sofware may be freely used providing this notice is included with +all copies. The name of the author may not be used to endorse +any other product or derivative work. No warranty is provided +and the user assumes all responsibility for use of this software. + +Report any issues to https://github.com/tlhackque/getssl/issues. + +Enjoy. + diff --git a/dns_scripts/dns_add_challtestsrv b/dns_scripts/dns_add_challtestsrv new file mode 100755 index 0000000..98444b5 --- /dev/null +++ b/dns_scripts/dns_add_challtestsrv @@ -0,0 +1,7 @@ +#!/usr/bin/env bash +# Simple script to update the challtestserv mock DNS server when testing DNS responses + +fulldomain="${1}" +token="${2}" + +curl --silent -X POST -d "{\"host\":\"_acme-challenge.${fulldomain}.\", \"value\": \"${token}\"}" http://10.30.50.3:8055/set-txt diff --git a/dns_scripts/dns_add_clouddns b/dns_scripts/dns_add_clouddns new file mode 100755 index 0000000..f20d1ab --- /dev/null +++ b/dns_scripts/dns_add_clouddns @@ -0,0 +1,103 @@ +#!/usr/bin/env bash +# Need to add your email address and API key to clouddns below or set as env variables +email=${CLOUDDNS_EMAIL:-''} +password=${CLOUDDNS_PASSWORD:-''} +client=${CLOUDDNS_CLIENT:-''} + +# This script adds a token to clouddns DNS for the ACME challenge +# usage dns_add_clouddns "domain name" "token" +# return codes are; +# 0 - success +# 1 - error in input +# 2 - error within internal processing +# 3 - error in result ( domain not found in clouddns etc) + +fulldomain="${1}" +token="${2}" +API='https://admin.vshosting.cloud/clouddns' +LOGIN_API='https://admin.vshosting.cloud/api/public/auth/login' + +# Check initial parameters +if [[ -z "$fulldomain" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi +if [[ -z "$token" ]]; then + echo "DNS script requires challenge token as second parameter" + exit 1 +fi +if [[ -z "$email" ]]; then + echo "CLOUDDNS_EMAIL (email) parameter not set" + exit 1 +fi +if [[ -z "$password" ]]; then + echo "CLOUDDNS_PASSWORD (password) parameter not set" + exit 1 +fi +if [[ -z "$client" ]]; then + echo "CLOUDDNS_CLIENT (id) parameter not set" + exit 1 +fi + +# Login to clouddns to get accessToken +resp=$(curl --silent -X POST -H 'Content-Type: application/json' "$LOGIN_API" \ + --data "{\"email\": \"$email\", \"password\": \"$password\"}") +re='"accessToken":"([^,]*)",' # Match access token +if [[ "${resp// }" =~ $re ]]; then + access_token="${BASH_REMATCH[1]}" +fi +if [[ -z "$access_token" ]]; then + echo 'Could not get access token; check your credentials' + exit 3 +fi +curl_params=( -H "Authorization: Bearer $access_token" -H 'Content-Type: application/json' ) + +# Get main domain +resp=$(curl --silent "${curl_params[@]}" -X POST "$API/domain/search" \ + --data "{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$client\"}]}") +domain_slice="$fulldomain" +while [[ -z "$domain_root" ]]; do + if [[ "${resp// }" =~ domainName\":\"$domain_slice ]]; then + domain_root="$domain_slice" + _debug domain_root "$domain_root" + fi + domain_slice="${domain_slice#[^\.]*.}" +done + +# Get domain id +resp=$(curl --silent "${curl_params[@]}" -X POST "$API/domain/search" \ + --data "{\"search\": [{\"name\": \"clientId\", \"operator\": \"eq\", \"value\": \"$client\"}, {\"name\": \"domainName\", \"operator\": \"eq\", \"value\": \"$domain_root.\"}]}") +re='domainType":"[^"]*","id":"([^,]*)",' # Match domain id +if [[ "${resp//[$'\t\r\n ']}" =~ $re ]]; then + domain_id="${BASH_REMATCH[1]}" +fi + +if [[ -z "$domain_id" ]]; then + echo 'Domain name not found on your CloudDNS account' + exit 3 +fi + +# Add challenge record +txt_record="_acme-challenge.$fulldomain." +resp=$(curl --silent "${curl_params[@]}" -X POST "$API/record-txt" \ + --data "{\"type\":\"TXT\",\"name\":\"$txt_record\",\"value\":\"$token\",\"domainId\":\"$domain_id\"}") + +# If adding record failed (error:) then print error message +if [[ "${resp// }" == *'"error"'* ]]; then + if [[ "${resp// }" == *'"code":4136'* ]]; then + echo "DNS challenge token already exists" + exit + fi + re='"message":"([^"]+)"' + if [[ "$resp" =~ $re ]]; then + echo "Error: DNS challenge not added: ${BASH_REMATCH[1]}" + exit 3 + else + echo "Error: DNS challenge not added: unknown error - ${resp}" + exit 3 + fi +fi + +# Publish challenge record +resp=$(curl --silent "${curl_params[@]}" -X PUT "$API/domain/$domain_id/publish" \ + --data "{\"soaTtl\":300}") diff --git a/dns_scripts/dns_add_cloudflare b/dns_scripts/dns_add_cloudflare index ea323c1..b1a6a16 100755 --- a/dns_scripts/dns_add_cloudflare +++ b/dns_scripts/dns_add_cloudflare @@ -1,9 +1,11 @@ #!/usr/bin/env bash -# need to add your email address and API key to cloudflare below or set as env variables +# either configure here or export environment variables in getssl.cfg email=${CF_EMAIL:-''} key=${CF_KEY:-''} +api_token=${CF_API_TOKEN:-''} +zone_id=${CF_ZONE_ID:-''} -# This script adds a token to cloudflare DNS for the ACME challenge +# This script adds a TXT record to cloudflare DNS for the ACME challenge # usage dns_add_cloudflare "domain name" "token" # return codes are; # 0 - success @@ -14,7 +16,11 @@ key=${CF_KEY:-''} fulldomain="${1}" token="${2}" API='https://api.cloudflare.com/client/v4/zones' -curl_params=( -H "X-Auth-Email: $email" -H "X-Auth-Key: $key" -H 'Content-Type: application/json' ) +if [[ -z "$api_token" ]]; then + curl_params=( -H "X-Auth-Email: $email" -H "X-Auth-Key: $key" -H 'Content-Type: application/json' ) +else + curl_params=( -H "Authorization: Bearer $api_token" -H 'Content-Type: application/json' ) +fi # check initial parameters @@ -28,84 +34,88 @@ if [[ -z "$token" ]]; then exit 1 fi -if [[ -z "$email" ]]; then - echo "CF_EMAIL (email) parameter not set" - exit 1 -fi +if [[ -z "$api_token" ]]; then + if [[ -z "$email" ]]; then + echo "CF_EMAIL (email) parameter not set" + exit 1 + fi -if [[ -z "$key" ]]; then - echo "CF_KEY (key) parameter not set" - exit 1 + if [[ -z "$key" ]]; then + echo "CF_KEY (key) parameter not set" + exit 1 + fi fi -# get a list of all domain names from cloudflare -# If you have a lot, you may need add "&page=1&per_page=1000" and/or "&status=active" -resp=$(curl --silent "${curl_params[@]}" -X GET "$API") -re='"result":\[(([^][]*\[[^][]*])*[^][]*)]' # find result section -if [[ "${resp// }" =~ $re ]]; then - resp="${BASH_REMATCH[1]}" -fi - -# iterate through all sections to obtain a list of domains -while [[ "$resp" ]]; do - re='[^}{]*\{(([^}{]*\{[^}{]*})*[^}{]*)}(.*)' - if [[ "$resp" =~ $re ]]; then - first="${BASH_REMATCH[1]}" - resp="${BASH_REMATCH[3]}" +if [[ -z "$zone_id" ]]; then + # get a list of all domain names from cloudflare + # If you have a lot, you may need add "&page=1&per_page=1000" and/or "&status=active" + resp=$(curl --silent "${curl_params[@]}" -X GET "$API") + re='"result":\[(([^][]*\[[^][]*])*[^][]*)]' # find result section + if [[ "${resp// }" =~ $re ]]; then + resp="${BASH_REMATCH[1]}" fi - # remove subsections - leave only domain level - while [[ "$first" =~ (.*)[\[\{][^]\{\}[]*[\]\}](.*) ]]; do - first="${BASH_REMATCH[1]}${BASH_REMATCH[2]}" - done - re='"name":"([^"]*)"' - if [[ "$first" =~ $re ]]; then - domains=( "${domains[@]}" "${BASH_REMATCH[1]}" ) - else - echo "Error getting domain name" - exit 2 - fi - re='"id":"([^"]*)"' - if [[ "$first" =~ $re ]]; then - ids=( "${ids[@]}" "${BASH_REMATCH[1]}" ) - else - echo "Error getting domain id" - exit 2 - fi -done -# split required domain name into an array -dnarray=(${fulldomain//./ }) -# get number of parts in required domain name -NumParts=${#dnarray[@]} -# build a test domain name, starting with the largest, and reduce it -# until a match is found, set domain = first ( longest) match. -domain="" -i=1 -while [ $i -lt "$NumParts" ]; do - testdomain="${dnarray[i-1]}" - for ((j=i; j ] [-d|--delete ] [-s|--search ] [-h|--help] [-t|--type] "\ + "[-q|--quiet] [-c|--check] [-S|--status] [-l|--lock #] [-T|--ttl] [-u|--update] [-w|--weight] [-L|--Line]" +} + +help_message() { # print out the help message + cat <<- _EOF_ + $PROGNAME Version. $VERSION + $(usage) + + Options: + -a, --add Add Domain Record 域名 ip (默认类型TXT) + -d, --delete Delete Domain Record 域名 (默认类型TXT) + -s, --search Search Domain Record 域名 + -t, --type Record Type 类型(A、MX、CNAME、TXT、REDIRECT_URL、FORWORD_URL、NS、AAAA、SRV) + _EOF_ +} + +_arg_check(){ + [ -z "$1" ] || _arg_count=$1 + shift + [ ${#} -lt $_arg_count ] && help_message && exit 1 || (echo $2 | grep "^-") && help_message && exit 1 + #If the number of arguments <$_ARG_COUNT print help and exit, and if the second argument begins with “-” print help and exit + return 0 +} + +#[ ${#} -lt 2 ] && help_message && exit 1 #Same as below +#[ -z "$2" ] && help_message && exit 1 #Same as below +_arg_check 2 $@ + +_debug (){ + if [ "$__debug" -eq 1 ]; then + echo -e "\033[1;31m # debug: $(date "+%m %d %T") | Func: ${FUNCNAME[@]} | Line:${BASH_LINENO[@]} \033[0m" "\n $@ " #"Current FUNCNAME ${FUNCNAME} #$LINENO " #"$(($RANDOM%10))" + fi + return 0 +} + +_requires() { + _cmds='' # Check if the commands exists + if [[ "$#" -gt 0 ]]; then + for i in "$@"; do + if eval type type >/dev/null 2>&1; then + eval type "$i" >/dev/null 2>&1 + elif command >/dev/null 2>&1; then + command -v "$i" >/dev/null 2>&1 + else + which "$i" >/dev/null 2>&1 + fi + #[ "$?" -eq 0 ] && _debug "checking for $i exists = ok" || _cmds=$_cmds"$i: " + #shellcheck disable=SC2181 + if [ "$?" -eq 0 ]; then + #_debug "checking for $i exists = ok" + continue + else + _cmds=$_cmds"$i: " + fi + done + else + echo "Usage: _requires [command] " + return 1 + fi + [ -n "$_cmds" ] && { echo -e "\033[1;31m $_cmds command not found \033[0m" && return 1 ;} || return 0 +} + +_requires openssl + +#shellcheck disable=SC2120 +_hex_dump() { #ascii hex + local _str='' + [ $# -gt 0 ] && _str=$@ || read _str + local _str_len=${#_str} + local i=1 + while [ "$i" -le "$_str_len" ]; do + local _str_c="$(printf "%s" "$_str" | cut -c "$i")" + printf " %02x" "'$_str_c" + i=$(($i + 1)) + done + #printf "%s" " 0a" +} + +_urlencode() { + local length="${#1}" + local i='' + for i in $(awk "BEGIN { for ( i=0; i<$length; i++ ) print i }") + do + #local _strc="$(printf "%s" "$1" | cut -c "$i")" #i=1; i<=$length; i++ + local _strc="${1:$i:1}" + case $_strc in [a-zA-Z0-9.~_-]) printf "%s" "$_strc" ;; *) printf "%%%02X" "'$_strc" ;; + esac + done +} + +_signature(){ + signature='' + _hexkey=$(printf "%s" "$AccessKeySecret&" | _hex_dump |sed 's/ //g') + #signature=$(printf "%s" "GET&%2F&$(_urlencode "$query")" | openssl dgst -sha1 -hmac $(printf "%s" "$AccessKeySecret&" | _hex_dump |sed 's/ //g'| xxd -r -p ) -binary | openssl base64 -e) + signature=$(printf "%s" "GET&%2F&$(_urlencode "$query")" | openssl dgst -sha1 -mac HMAC -macopt "hexkey:$_hexkey" -binary | openssl base64 -e) + signature=$(_urlencode "$signature") +} + +_query() { + [ -n "$__type" ] && { [[ "$_Action" = "AddDomainRecord" ]] && _Type="$__type" || { [ "$_Action" = "DescribeDomainRecords" ] && _TypeKeyWord="$__type"; } ; } + query='' + [ -n $AccessKeyId ] && query=$query'AccessKeyId='$AccessKeyId + query=$query'&Action='"$1" + [ -z $_DomainNames ] || query=$query'&DomainName='$_DomainNames + query=$query'&Format=json' + [ -z $_RR ] || query=$query'&RR='$_RR + [ -z $_RRKeyWord ] || query=$query'&RRKeyWord='$_RRKeyWord + [ -z $_RecordId ] || query=$query'&RecordId='$_RecordId + query=$query'&SignatureMethod=HMAC-SHA1' + query=$query"&SignatureNonce=$(date +"%s%N")" + query=$query'&SignatureVersion=1.0' + query=$query'&Timestamp='$_timestamp + [ -z $_Type ] || query=$query'&Type='$_Type + [ -z $_TypeKeyWord ] || query=$query'&TypeKeyWord='$_TypeKeyWord + [ -z $_Value ] || query=$query'&Value='$_Value + [ -z $_ValueKeyWord ] || query=$query'&ValueKeyWord='$_ValueKeyWord + query=$query'&Version=2015-01-09' + #_debug "$query" + _signature + return 0 +} + +_Get_RecordIds(){ + _Action="DescribeDomainRecords" + _query $_Action $_DomainNames + url="${Ali_API}?${query}&Signature=${signature}" + _debug $url + _RecordIds=$(curl -k -s $url | grep -Po 'RecordId[": "]+\K[^"]+') && __delete="1" #RecordId requisite + _debug $_RecordIds + return 0 +} + +__type='TXT' +_DomainNames=$(printf "%s" $1| awk -F"." '{if(NF>=2){print $(NF-1)"."$NF}}') #awk -F\. '{print $(NF-1) FS $NF}') #requisite +_RRKeyWord="_acme-challenge" + +_Get_RecordIds + +_RRKeyWord='' +_TypeKeyWord='' +_ValueKeyWord='' + +if [ "$__delete" = "1" ];then + _Action="DeleteDomainRecord" #Action requisite + _DomainNames='' + for _RecordId in ${_RecordIds[@]} #Delete multiple txt domain record + do + _debug "_RecordId" $_RecordId + _query $_Action $_RecordId + url="${Ali_API}?${query}&Signature=${signature}" + _debug $url + curl -k -s $url && ( echo -e "\n\033[1;32m Aliyun DNS record _acme-challenge.$1 has been deleted \033[0m") + done +else + _Action="AddDomainRecord" #requisite + _RR=$(printf "_acme-challenge.%s" $1| awk -F'.' '{if(NF>2){gsub("."$(NF-1)"."$NF,"");print}}') #requisite + _Value=$2 #requisite + _query $_Action $_DomainNames + url="${Ali_API}?${query}&Signature=${signature}" + _debug $url + curl -k -s $url && (echo -e "\n\033[1;32m Start Checking aliyun DNS record _acme-challenge.$1 \033[0m") + exit 0 +fi diff --git a/dns_scripts/dns_add_duckdns b/dns_scripts/dns_add_duckdns new file mode 100755 index 0000000..9d1776a --- /dev/null +++ b/dns_scripts/dns_add_duckdns @@ -0,0 +1,28 @@ +#!/bin/bash + +# need to add your Token for duckdns below +token=${DUCKDNS_TOKEN:-} + +if [ -z "$token" ]; then + echo "DUCKDNS_TOKEN not set" + exit 1 +fi + +domain="$1" +txtvalue="$2" +i=1 + +response=$(curl --retry 5 --silent "https://www.duckdns.org/update?domains=${domain}&token=${token}&txt=${txtvalue}") + +while [[ "${response}" == *"502 Bad Gateway"* ]] && [ $i -le 5 ]; do + echo "Retrying Bad Gateway response (attempt $i of 5)" + sleep 5 + i=$((i+1)) + response=$(curl --retry 5 --silent "https://www.duckdns.org/update?domains=${domain}&token=${token}&txt=${txtvalue}") +done + +if [ "$response" != "OK" ]; then + echo "Failed to update TXT record for ${domain} at duckdns.org (is the TOKEN valid?)" + echo "Response: $response" + exit 1 +fi diff --git a/dns_scripts/dns_add_dynu b/dns_scripts/dns_add_dynu new file mode 100755 index 0000000..e20470d --- /dev/null +++ b/dns_scripts/dns_add_dynu @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# Need to add your API key below or set as env variable +apikey=${DYNU_API_KEY:-''} + +# This script adds a token to dynu.com DNS for the ACME challenge +# usage dns_add_dynu "domain name" "token" +# return codes are; +# 0 - success +# 1 - error in input +# 2 - error within internal processing +# 3 - error in result ( domain not found in dynu.com etc) + +fulldomain="${1}" +token="${2}" + +API='https://api.dynu.com/v2/dns' + +# Check initial parameters +if [[ -z "$fulldomain" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi +if [[ -z "$token" ]]; then + echo "DNS script requires challenge token as second parameter" + exit 1 +fi + +curl_params=( -H "accept: application/json" -H "API-Key: $apikey" -H 'Content-Type: application/json' ) + +# Get domain id +# curl -X GET https://api.dynu.com/v2/dns/getroot/ubuntu-getssl.freeddns.org +resp=$(curl --silent "${curl_params[@]}" -X GET "$API/getroot/${fulldomain}") + +# Match domain id +re="\"id\":([^,]*),\"domainName\":\"${fulldomain}\"" +if [[ "$resp" =~ $re ]]; then + domain_id="${BASH_REMATCH[1]}" +fi + +if [[ -z "$domain_id" ]]; then + echo 'Domain name not found on your Dynu account' + exit 3 +fi + +# Check for existing _acme-challenge TXT record +# curl -X GET "https://api.dynu.com/v2/dns/record/_acme-challenge.ubuntu-getssl.freeddns.org?recordType=TXT" +resp=$(curl --silent "${curl_params[@]}" -X GET "${API}/record/_acme-challenge.${fulldomain}?recordType=TXT") +re="\"id\":([^,]*)" +if [[ "$resp" =~ $re ]]; then + record_id="${BASH_REMATCH[1]}" +fi + +if [[ -z "$record_id" ]]; then + # Add new TXT challenge record + resp=$(curl --silent \ + "${curl_params[@]}" \ + -X POST "${API}/${domain_id}/record" \ + --data "{\"nodeName\":\"_acme-challenge\",\"recordType\":\"TXT\",\"state\":\"true\",\"textData\":\"$token\"}") +else + # Update existing record + # curl -X POST https://api.dynu.com/v2/dns/9329328/record/7082063 -d "{\"nodeName\":\"_acme-challenge\",\"recordType\":\"TXT\",\"state\":\"true\",\"textData\":\"Test2\"}" + resp=$(curl --silent \ + "${curl_params[@]}" \ + -X POST "${API}/${domain_id}/record/${record_id}" \ + --data "{\"nodeName\":\"_acme-challenge\",\"recordType\":\"TXT\",\"state\":\"true\",\"textData\":\"$token\"}") +fi + +# If adding record failed (exception:) then print error message +if [[ "$resp" != *"\"statusCode\":200"* ]]; then + echo "Error: DNS challenge not added: unknown error - ${resp}" + exit 3 +fi diff --git a/dns_scripts/dns_add_joker b/dns_scripts/dns_add_joker new file mode 100755 index 0000000..c100886 --- /dev/null +++ b/dns_scripts/dns_add_joker @@ -0,0 +1,44 @@ +#!/bin/bash + +FULLDOMAIN=$1 +TOKEN=$2 +TMPFILE=$(mktemp /tmp/dns_add_joker.XXXXXXX) + +USERNAME="youruser" +PASSWORD="yourpassword" + +# Verify that required parameters are set +if [[ -z "${FULLDOMAIN}" ]]; then + echo "DNS script requires full domain name as first parameter" + exit 1 +fi + +if [[ -z "${TOKEN}" ]]; then + echo "DNS script requires challenge token as second parameter" + exit 1 +fi + +DOMAIN_ROOT=$(echo "${FULLDOMAIN}" | awk -F\. '{print $(NF-1) FS $NF}') + +SID=$(curl --silent -X POST https://dmapi.joker.com/request/login \ + -H "Accept: application/json" -H "User-Agent: getssl/0.1" \ + -H "application/x-www-form-urlencoded" -d "username=${USERNAME}&password=${PASSWORD}" \ + -i -k 2>/dev/null | grep Auth-Sid | awk '{ print $2 }') + +## put zone data in tempfile +curl --silent -X POST https://dmapi.joker.com/request/dns-zone-get \ + -H "Accept: application/json" -H "User-Agent: getssl/0.1" \ + -H "application/x-www-form-urlencoded" -d "domain=${DOMAIN_ROOT}&auth-sid=${SID}" | \ + tail -n +7 >"${TMPFILE}" + +## add txt record +printf "_acme-challenge.%s. TXT 0 \"%s \" 300\n\n" "${FULLDOMAIN}" "${TOKEN}" >>"${TMPFILE}" + +## generate encoded url data +URLDATA=$(cat "${TMPFILE}" | sed 's/ /%20/g' | sed 's/"/%22/g' | sed ':a;N;$!ba;s/\n/%0A/g') + +## write new zonefile to joker +curl --silent --output /dev/null "https://dmapi.joker.com/request/dns-zone-put?domain=${DOMAIN_ROOT}&zone=${URLDATA}&auth-sid=${SID}" 2>&1 + +## remove tempfile +rm -f "${TMPFILE}" diff --git a/dns_scripts/dns_add_nsupdate b/dns_scripts/dns_add_nsupdate index 55178db..2680d81 100755 --- a/dns_scripts/dns_add_nsupdate +++ b/dns_scripts/dns_add_nsupdate @@ -14,26 +14,29 @@ token="$2" if [ -n "${DNS_NSUPDATE_KEYFILE}" ]; then - if [ -n "${DNS_NSUPDATE_KEY_HOOK}" ] && ! ${DNS_NSUPDATE_KEY_HOOK} 'add' 'open' ${fulldomain} ; then - exit $(( $? + 128 )) - fi + if [ -n "${DNS_NSUPDATE_KEY_HOOK}" ] && ! ${DNS_NSUPDATE_KEY_HOOK} 'add' 'open' "${fulldomain}" ; then + exit $(( $? + 128 )) + fi - options="-k ${DNS_NSUPDATE_KEYFILE}" + options="-k ${DNS_NSUPDATE_KEYFILE}" fi -# Note that blank line is a "send" command to nsupdate +cmd= +if [ -n "${DNS_SERVER}" ]; then + cmd+="server ${DNS_SERVER}\n" +fi -nsupdate ${options} -v </dev/null | grep Auth-Sid | awk '{ print $2 }') + +## put zone data in tempfile +curl --silent -X POST https://dmapi.joker.com/request/dns-zone-get \ + -H "Accept: application/json" -H "User-Agent: getssl/0.1" \ + -H "application/x-www-form-urlencoded" -d "domain=${DOMAIN_ROOT}&auth-sid=${SID}" | \ + tail -n +7 >"${TMPFILE}" + +## remove txt record +sed -i "/_acme-challenge.${FULLDOMAIN}.*${TOKEN}.*/d" "${TMPFILE}" + +## generate encoded url data +URLDATA=$(cat "${TMPFILE}" | sed 's/ /%20/g' | sed 's/"/%22/g' | sed ':a;N;$!ba;s/\n/%0A/g') + +## write new zonefile to joker +curl --silent --output /dev/null "https://dmapi.joker.com/request/dns-zone-put?domain=${DOMAIN_ROOT}&zone=${URLDATA}&auth-sid=${SID}" 2>&1 + +## remove tempfile +rm -f "${TMPFILE}" diff --git a/dns_scripts/dns_del_nsupdate b/dns_scripts/dns_del_nsupdate index b3a553e..fc5a254 100755 --- a/dns_scripts/dns_del_nsupdate +++ b/dns_scripts/dns_del_nsupdate @@ -14,26 +14,29 @@ token="$2" # 'open" / 'close' if [ -n "${DNS_NSUPDATE_KEYFILE}" ]; then - if [ -n "${DNS_NSUPDATE_KEY_HOOK}" ] && ! ${DNS_NSUPDATE_KEY_HOOK} 'del' 'open' ${fulldomain} ; then - exit $(( $? + 128 )) - fi + if [ -n "${DNS_NSUPDATE_KEY_HOOK}" ] && ! "${DNS_NSUPDATE_KEY_HOOK}" 'del' 'open' "${fulldomain}" ; then + exit $(( $? + 128 )) + fi - options="-k ${DNS_NSUPDATE_KEYFILE}" + options="-k ${DNS_NSUPDATE_KEYFILE}" fi -# Note that blank line is a "send" command to nsupdate +cmd= +if [ -n "${DNS_SERVER}" ]; then + cmd+="server ${DNS_SERVER}\n" +fi -nsupdate ${options} -v <$(dirname "$(readlink -f "$0")")/freednscookie.dat + + # We may have to cycle through the domain name to find the + # TLD that we own... + i=1 + wmax="$(echo "$fulldomain" | tr '.' ' ' | wc -w)" + while [ "$i" -lt "$wmax" ]; do + # split our full domain name into two parts... + sub_domain="$(echo "$fulldomain" | cut -d. -f -"$i")" + i="$(_math "$i" + 1)" + top_domain="$(echo "$fulldomain" | cut -d. -f "$i"-100)" + #echo "Debug: sub_domain: $sub_domain" + #echo "Debug: top_domain: $top_domain" + + DNSdomainid="$(_freedns_domain_id "$top_domain")" + if [ "$?" = "0" ]; then + echo "Info:Domain $top_domain found at FreeDNS, domain_id $DNSdomainid" + break + else + echo "Info:Domain $top_domain not found at FreeDNS, try with next level of TLD" + fi + done + + if [ -z "$DNSdomainid" ]; then + # If domain ID is empty then something went wrong (top level + # domain not found at FreeDNS). + echo "ERROR: Domain $top_domain not found at FreeDNS" + return 1 + fi + + # Add in new TXT record with the value provided + #echo "Debug: Adding TXT record for $fulldomain, $txtvalue" + _freedns_add_txt_record "$FREEDNS_COOKIE" "$DNSdomainid" "$sub_domain" "$txtvalue" + return $? +} + +#Usage: fulldomain txtvalue +#Remove the txt record after validation. +dns_freedns_rm() { + fulldomain="_acme-challenge.$1" + txtvalue="$2" + + echo "Info:Delete TXT record using FreeDNS" + #echo "Debug: fulldomain: $fulldomain" + #echo "Debug: txtvalue: $txtvalue" + + # Need to read cookie from conf file again in case new value set + # during login to FreeDNS when TXT record was created. + FREEDNS_COOKIE="$(cat $(dirname "$(readlink -f "$0")")/freednscookie.dat)" + #echo "Debug: FreeDNS login cookies: $FREEDNS_COOKIE" + + TXTdataid="$(_freedns_data_id "$fulldomain" "TXT")" + if [ "$?" != "0" ]; then + echo "Info:Cannot delete TXT record for $fulldomain, record does not exist at FreeDNS" + return 1 + fi + #echo "Debug: Data ID's found, $TXTdataid" + + # now we have one (or more) TXT record data ID's. Load the page + # for that record and search for the record txt value. If match + # then we can delete it. + lines="$(echo "$TXTdataid" | wc -l)" + #echo "Debug: Found $lines TXT data records for $fulldomain" + i=0 + while [ "$i" -lt "$lines" ]; do + i="$(_math "$i" + 1)" + dataid="$(echo "$TXTdataid" | sed -n "${i}p")" + #echo "Debug: $dataid" + + htmlpage="$(_freedns_retrieve_data_page "$FREEDNS_COOKIE" "$dataid")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + echo "ERROR: Has your FreeDNS username and password changed? If so..." + echo "ERROR: Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + echo "$htmlpage" | grep "value=\""$txtvalue"\"" >/dev/null + if [ "$?" = "0" ]; then + # Found a match... delete the record and return + echo "Info:Deleting TXT record for $fulldomain, $txtvalue" + _freedns_delete_txt_record "$FREEDNS_COOKIE" "$dataid" + return $? + fi + done + + # If we get this far we did not find a match + # Not necessarily an error, but log anyway. + echo "Info:Cannot delete TXT record for $fulldomain, $txtvalue. Does not exist at FreeDNS" + return 0 +} + +#################### Private functions below ################################## + +# usage: _freedns_login username password +# print string "cookie=value" etc. +# returns 0 success +_freedns_login() { + export _H1="Accept-Language:en-US" + username="$1" + password="$2" + url="https://freedns.afraid.org/zc.php?step=2" + + #echo "Debug: Login to FreeDNS as user $username" + data="username=$(printf '%s' "$username" | _url_encode)&password=$(printf '%s' "$password" | _url_encode)&submit=Login&action=auth" + #echo "$data" + + if [ -z "$HTTP_HEADER" ] || ! touch "$HTTP_HEADER"; then + HTTP_HEADER="$(_mktemp)" + fi + htmlpage="$(curl -L --silent --dump-header $HTTP_HEADER -X POST -H "$_H1" -H "$_H2" --data "$data" "$url")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS login failed for user $username bad RC from _post" + return 1 + fi + + cookies="$(grep -i '^Set-Cookie.*dns_cookie.*$' "$HTTP_HEADER" | _head_n 1 | tr -d "\r\n" | cut -d " " -f 2)" + + # if cookies is not empty then logon successful + if [ -z "$cookies" ]; then + #echo "Debug3: htmlpage: $htmlpage" + echo "ERROR: FreeDNS login failed for user $username. Check $HTTP_HEADER file" + return 1 + fi + + printf "%s" "$cookies" + return 0 +} + +# usage _freedns_retrieve_subdomain_page login_cookies +# echo page retrieved (html) +# returns 0 success +_freedns_retrieve_subdomain_page() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + url="https://freedns.afraid.org/subdomain/" + + #echo "Debug: Retrieve subdomain page from FreeDNS" + + htmlpage="$(curl -L --silent -H "$_H1" -H "$_H2" "$url")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS retrieve subdomains failed bad RC from _get" + return 1 + elif [ -z "$htmlpage" ]; then + echo "ERROR: FreeDNS returned empty subdomain page" + return 1 + fi + + #echo "Debug3: htmlpage: $htmlpage" + + printf "%s" "$htmlpage" + return 0 +} + +# usage _freedns_retrieve_data_page login_cookies data_id +# echo page retrieved (html) +# returns 0 success +_freedns_retrieve_data_page() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + data_id="$2" + url="https://freedns.afraid.org/subdomain/edit.php?data_id=$2" + + #echo "Debug: Retrieve data page for ID $data_id from FreeDNS" + + htmlpage="$(curl -L --silent -H "$_H1" -H "$_H2" "$url")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS retrieve data page failed bad RC from _get" + return 1 + elif [ -z "$htmlpage" ]; then + echo "ERROR: FreeDNS returned empty data page" + return 1 + fi + + #echo "Debug3: htmlpage: $htmlpage" + + printf "%s" "$htmlpage" + return 0 +} + +# usage _freedns_add_txt_record login_cookies domain_id subdomain value +# returns 0 success +_freedns_add_txt_record() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + domain_id="$2" + subdomain="$3" + value="$(printf '%s' "$4" | _url_encode)" + url="https://freedns.afraid.org/subdomain/save.php?step=2" + + if [ -z "$HTTP_HEADER" ] || ! touch "$HTTP_HEADER"; then + HTTP_HEADER="$(_mktemp)" + fi + htmlpage="$(curl -L --silent --dump-header $HTTP_HEADER -X POST -H "$_H1" -H "$_H2" --data "type=TXT&domain_id=$domain_id&subdomain=$subdomain&address=%22$value%22&send=Save%21" "$url")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS failed to add TXT record for $subdomain bad RC from _post" + return 1 + elif ! grep "200 OK" "$HTTP_HEADER" >/dev/null; then + #echo "Debug3: htmlpage: $(cat $HTTP_HEADER)" + echo "ERROR: FreeDNS failed to add TXT record for $subdomain. Check $HTTP_HEADER file" + return 1 + elif _contains "$htmlpage" "security code was incorrect"; then + #echo "Debug3: htmlpage: $htmlpage" + echo "ERROR: FreeDNS failed to add TXT record for $subdomain as FreeDNS requested security code" + echo "ERROR: Note that you cannot use automatic DNS validation for FreeDNS public domains" + return 1 + fi + + #echo "Debug3: htmlpage: $htmlpage" + echo "Info:Added acme challenge TXT record for $fulldomain at FreeDNS" + return 0 +} + +# usage _freedns_delete_txt_record login_cookies data_id +# returns 0 success +_freedns_delete_txt_record() { + export _H1="Cookie:$1" + export _H2="Accept-Language:en-US" + data_id="$2" + url="https://freedns.afraid.org/subdomain/delete2.php" + + htmlheader="$(curl -L --silent -I -H "$_H1" -H "$_H2" "$url?data_id%5B%5D=$data_id&submit=delete+selected")" + + if [ "$?" != "0" ]; then + echo "ERROR: FreeDNS failed to delete TXT record for $data_id bad RC from _get" + return 1 + elif ! _contains "$htmlheader" "200 OK"; then + #echo "Debug2: htmlheader: $htmlheader" + echo "ERROR: FreeDNS failed to delete TXT record $data_id" + return 1 + fi + + echo "Info:Deleted acme challenge TXT record for $fulldomain at FreeDNS" + return 0 +} + +# usage _freedns_domain_id domain_name +# echo the domain_id if found +# return 0 success +_freedns_domain_id() { + # Start by escaping the dots in the domain name + search_domain="$(echo "$1" | sed 's/\./\\./g')" + + # Sometimes FreeDNS does not return the subdomain page but rather + # returns a page regarding becoming a premium member. This usually + # happens after a period of inactivity. Immediately trying again + # returns the correct subdomain page. So, we will try twice to + # load the page and obtain our domain ID + attempts=2 + while [ "$attempts" -gt "0" ]; do + attempts="$(_math "$attempts" - 1)" + + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + echo "ERROR: Has your FreeDNS username and password changed? If so..." + echo "ERROR: Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + domain_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's//@/g' | tr '@' '\n' \ + | grep "$search_domain\|$search_domain(.*)" \ + | sed -n 's/.*\(edit\.php?edit_domain_id=[0-9a-zA-Z]*\).*/\1/p' \ + | cut -d = -f 2)" + # The above beauty extracts domain ID from the html page... + # strip out all blank space and new lines. Then insert newlines + # before each table row + # search for the domain within each row (which may or may not have + # a text string in brackets (.*) after it. + # And finally extract the domain ID. + if [ -n "$domain_id" ]; then + printf "%s" "$domain_id" + return 0 + fi + #echo "Debug:Domain $search_domain not found. Retry loading subdomain page ($attempts attempts remaining)" + done + #echo "Debug:Domain $search_domain not found after retry" + return 1 +} + +# usage _freedns_data_id domain_name record_type +# echo the data_id(s) if found +# return 0 success +_freedns_data_id() { + # Start by escaping the dots in the domain name + search_domain="$(echo "$1" | sed 's/\./\\./g')" + record_type="$2" + + # Sometimes FreeDNS does not return the subdomain page but rather + # returns a page regarding becoming a premium member. This usually + # happens after a period of inactivity. Immediately trying again + # returns the correct subdomain page. So, we will try twice to + # load the page and obtain our domain ID + attempts=2 + while [ "$attempts" -gt "0" ]; do + attempts="$(_math "$attempts" - 1)" + + htmlpage="$(_freedns_retrieve_subdomain_page "$FREEDNS_COOKIE")" + if [ "$?" != "0" ]; then + if [ "$using_cached_cookies" = "true" ]; then + echo "ERROR: Has your FreeDNS username and password changed? If so..." + echo "ERROR: Please export as FREEDNS_User / FREEDNS_Password and try again." + fi + return 1 + fi + + data_id="$(echo "$htmlpage" | tr -d " \t\r\n\v\f" | sed 's//@/g' | tr '@' '\n' \ + | grep "$record_type" \ + | grep "$search_domain" \ + | sed -n 's/.*\(edit\.php?data_id=[0-9a-zA-Z]*\).*/\1/p' \ + | cut -d = -f 2)" + # The above beauty extracts data ID from the html page... + # strip out all blank space and new lines. Then insert newlines + # before each table row + # search for the record type withing each row (e.g. TXT) + # search for the domain within each row (which is within a + # anchor. And finally extract the domain ID. + if [ -n "$data_id" ]; then + printf "%s" "$data_id" + return 0 + fi + #echo "Debug:Domain $search_domain not found. Retry loading subdomain page ($attempts attempts remaining)" + done + #echo "Debug:Domain $search_domain not found after retry" + return 1 +} + +#### BEGIN things shamefully ripped from https://github.com/Neilpang/acme.sh/blob/master/acme.sh + +#_ascii_hex str +#this can only process ascii chars, should only be used when od command is missing as a backup way. +_ascii_hex() { + _debug2 "Using _ascii_hex" + _str="$1" + _str_len=${#_str} + _h_i=1 + while [ "$_h_i" -le "$_str_len" ]; do + _str_c="$(printf "%s" "$_str" | cut -c "$_h_i")" + printf " %02x" "'$_str_c" + _h_i="$(_math "$_h_i" + 1)" + done +} + +#stdin output hexstr splited by one space +#input:"abc" +#output: " 61 62 63" +_hex_dump() { + if _exists od; then + od -A n -v -t x1 | tr -s " " | sed 's/ $//' | tr -d "\r\t\n" + elif _exists hexdump; then + hexdump -v -e '/1 ""' -e '/1 " %02x" ""' + elif _exists xxd; then + xxd -ps -c 20 -i | sed "s/ 0x/ /g" | tr -d ",\n" | tr -s " " + else + str=$(cat) + _ascii_hex "$str" + fi +} + +#url encode, no-preserved chars +#A B C D E F G H I J K L M N O P Q R S T U V W X Y Z +#41 42 43 44 45 46 47 48 49 4a 4b 4c 4d 4e 4f 50 51 52 53 54 55 56 57 58 59 5a + +#a b c d e f g h i j k l m n o p q r s t u v w x y z +#61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a + +#0 1 2 3 4 5 6 7 8 9 - _ . ~ +#30 31 32 33 34 35 36 37 38 39 2d 5f 2e 7e + +#stdin stdout +_url_encode() { + _hex_str=$(_hex_dump) + for _hex_code in $_hex_str; do + #upper case + case "${_hex_code}" in + "41") + printf "%s" "A" + ;; + "42") + printf "%s" "B" + ;; + "43") + printf "%s" "C" + ;; + "44") + printf "%s" "D" + ;; + "45") + printf "%s" "E" + ;; + "46") + printf "%s" "F" + ;; + "47") + printf "%s" "G" + ;; + "48") + printf "%s" "H" + ;; + "49") + printf "%s" "I" + ;; + "4a") + printf "%s" "J" + ;; + "4b") + printf "%s" "K" + ;; + "4c") + printf "%s" "L" + ;; + "4d") + printf "%s" "M" + ;; + "4e") + printf "%s" "N" + ;; + "4f") + printf "%s" "O" + ;; + "50") + printf "%s" "P" + ;; + "51") + printf "%s" "Q" + ;; + "52") + printf "%s" "R" + ;; + "53") + printf "%s" "S" + ;; + "54") + printf "%s" "T" + ;; + "55") + printf "%s" "U" + ;; + "56") + printf "%s" "V" + ;; + "57") + printf "%s" "W" + ;; + "58") + printf "%s" "X" + ;; + "59") + printf "%s" "Y" + ;; + "5a") + printf "%s" "Z" + ;; + + #lower case + "61") + printf "%s" "a" + ;; + "62") + printf "%s" "b" + ;; + "63") + printf "%s" "c" + ;; + "64") + printf "%s" "d" + ;; + "65") + printf "%s" "e" + ;; + "66") + printf "%s" "f" + ;; + "67") + printf "%s" "g" + ;; + "68") + printf "%s" "h" + ;; + "69") + printf "%s" "i" + ;; + "6a") + printf "%s" "j" + ;; + "6b") + printf "%s" "k" + ;; + "6c") + printf "%s" "l" + ;; + "6d") + printf "%s" "m" + ;; + "6e") + printf "%s" "n" + ;; + "6f") + printf "%s" "o" + ;; + "70") + printf "%s" "p" + ;; + "71") + printf "%s" "q" + ;; + "72") + printf "%s" "r" + ;; + "73") + printf "%s" "s" + ;; + "74") + printf "%s" "t" + ;; + "75") + printf "%s" "u" + ;; + "76") + printf "%s" "v" + ;; + "77") + printf "%s" "w" + ;; + "78") + printf "%s" "x" + ;; + "79") + printf "%s" "y" + ;; + "7a") + printf "%s" "z" + ;; + #numbers + "30") + printf "%s" "0" + ;; + "31") + printf "%s" "1" + ;; + "32") + printf "%s" "2" + ;; + "33") + printf "%s" "3" + ;; + "34") + printf "%s" "4" + ;; + "35") + printf "%s" "5" + ;; + "36") + printf "%s" "6" + ;; + "37") + printf "%s" "7" + ;; + "38") + printf "%s" "8" + ;; + "39") + printf "%s" "9" + ;; + "2d") + printf "%s" "-" + ;; + "5f") + printf "%s" "_" + ;; + "2e") + printf "%s" "." + ;; + "7e") + printf "%s" "~" + ;; + #other hex + *) + printf '%%%s' "$_hex_code" + ;; + esac + done +} + +_exists() { + cmd="$1" + if [ -z "$cmd" ]; then + _usage "Usage: _exists cmd" + return 1 + fi + + if eval type type >/dev/null 2>&1; then + eval type "$cmd" >/dev/null 2>&1 + elif command >/dev/null 2>&1; then + command -v "$cmd" >/dev/null 2>&1 + else + which "$cmd" >/dev/null 2>&1 + fi + ret="$?" + #echo "Debug3: $cmd exists=$ret" + return $ret +} + +_head_n() { + head -n "$1" +} + +_mktemp() { + if _exists mktemp; then + if mktemp 2>/dev/null; then + return 0 + elif _contains "$(mktemp 2>&1)" "-t prefix" && mktemp -t "$PROJECT_NAME" 2>/dev/null; then + #for Mac osx + return 0 + fi + fi + if [ -d "/tmp" ]; then + echo "/tmp/${PROJECT_NAME}wefADf24sf.$(_time).tmp" + return 0 + elif [ "$LE_TEMP_DIR" ] && mkdir -p "$LE_TEMP_DIR"; then + echo "/$LE_TEMP_DIR/wefADf24sf.$(_time).tmp" + return 0 + fi + _err "Can not create temp file." +} + +#a + b +_math() { + _m_opts="$@" + printf "%s" "$(($_m_opts))" +} + +_contains() { + _str="$1" + _sub="$2" + echo "$_str" | grep -- "$_sub" >/dev/null 2>&1 +} + +##Now actually do something with that function +case "$1" in + + add) + dns_freedns_add $2 $3 + ;; + rm) + dns_freedns_rm $2 $3 + ;; +esac diff --git a/dns_scripts/dns_route53.py b/dns_scripts/dns_route53.py new file mode 100755 index 0000000..6b88b37 --- /dev/null +++ b/dns_scripts/dns_route53.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +import boto3, sys, time +from os.path import basename +import dns.resolver + +client = boto3.client('route53') + +name = sys.argv[0] +fqdn = sys.argv[1] +challenge = sys.argv[2] + +bname = basename(name) +if bname == 'dns_add_route53': + action = 'UPSERT' +elif bname == 'dns_del_route53': + action = 'DELETE' +else: + print("No such action: {a}".format(a=bname)) + sys.exit(1) + +try: + response = client.list_hosted_zones() +except Exception as e: + print("Oops: {e!r}".format(e=e)) + sys.exit(1) + +zone_id = "" +zone_list = dict() +for zone in response['HostedZones']: + if not zone['Config']['PrivateZone']: + zone_list[zone['Name']] = zone['Id'] + +for key in sorted(zone_list.keys(), key=len, reverse=True): + if ".{z}".format(z=key) in ".{z}.".format(z=fqdn): + zone_id = zone_list[key] + +if zone_id == "": + print("We didn't find the zone") + sys.exit(1) + +challenge_fqdn = "_acme-challenge.{f}".format(f=fqdn) +try: + response = client.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + 'Comment': 'getssl/Letsencrypt verification', + 'Changes': [ + { + 'Action': action, + 'ResourceRecordSet': { + 'Name': challenge_fqdn, + 'Type': 'TXT', + 'TTL': 300, + 'ResourceRecords': [{'Value': "\"{c}\"".format(c=challenge)}] + } + }, + ] + } + ) +except Exception as e: + print("Oops: {e!r}".format(e=e)) + sys.exit(1) + +waiting = 0 +if action == 'UPSERT': + # Wait until we see the record before returning. The ACME server's timeout is too short. + # But only if we're adding the record. Don't care how long it takes to delete. + while (True): + try: + my_resolver = dns.resolver.Resolver(configure=False) + my_resolver.nameservers = ['8.8.8.8', '8.8.4.4'] + results = my_resolver.resolve(challenge_fqdn, 'TXT') + data = str(results.response.answer[0][0]).strip('\"') + if data == challenge: + print("found {f} entry".format(f=challenge_fqdn)) + else: + print("found {f} entry but it has bad data: {d}".format(f=challenge_fqdn, + d=data)) + break + + except dns.resolver.NXDOMAIN: + waiting += 10 + print("Didn't find {f} entry yet, sleeping... ({w}s)".format(f=challenge_fqdn, + w=waiting)) + time.sleep(10) + pass diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..f2b1489 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,33 @@ +version: '3' +services: + pebble: + image: letsencrypt/pebble:latest + # TODO enable -strict + command: pebble -config /test/config/pebble-config.json -dnsserver 10.30.50.3:53 + environment: + # with Go 1.13.x which defaults TLS 1.3 to on + GODEBUG: "tls13=1" + PEBBLE_ALTERNATE_ROOTS: 2 + ports: + - 14000:14000 # HTTPS ACME API + - 15000:15000 # HTTPS Management API + networks: + acmenet: + ipv4_address: 10.30.50.2 + challtestsrv: + image: letsencrypt/pebble-challtestsrv:latest + command: pebble-challtestsrv -defaultIPv6 "" -defaultIPv4 10.30.50.3 -dns01 ":53" + ports: + - 8055:8055 # HTTP Management API + networks: + acmenet: + ipv4_address: 10.30.50.3 + + +networks: + acmenet: + driver: bridge + ipam: + driver: default + config: + - subnet: 10.30.50.0/24 diff --git a/getssl b/getssl index 1453870..4f5fa34 100755 --- a/getssl +++ b/getssl @@ -15,6 +15,8 @@ # 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) @@ -50,7 +52,7 @@ # 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 AMCE changes. (0.34) +# 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) @@ -114,20 +116,20 @@ # 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 OSX (1.50) +# 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 alllow multiple tokens in DNS challenge (1.57) -# 2016-10-14 added CHECK_ALL_AUTH_DNS option to check all DNS servres, not just one primary server (1.58) +# 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 permsissions to 700 for getssl script during upgrade (1.63) +# 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) @@ -173,7 +175,7 @@ # 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 amount of old versions to keep (2.01) +# 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) @@ -184,39 +186,114 @@ # 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) # ---------------------------------------------------------------------------------------- +case :$SHELLOPTS: in + *:posix:*) echo -e "${0##*/}: Running with POSIX mode enabled is not supported" >&2; exit 1;; +esac + PROGNAME=${0##*/} -VERSION="2.10" +PROGDIR="$(cd "$(dirname "$0")" || exit; pwd -P;)" +VERSION="2.36" # defaults ACCOUNT_KEY_LENGTH=4096 ACCOUNT_KEY_TYPE="rsa" -export AUTH_DNS_SERVER="" -CA="https://acme-staging.api.letsencrypt.org" +CA="https://acme-staging-v02.api.letsencrypt.org/directory" CA_CERT_LOCATION="" CHALLENGE_CHECK_TYPE="http" -CHECK_ALL_AUTH_DNS="false" -CHECK_CERT_TIMEOUT="4" CHECK_REMOTE="true" CHECK_REMOTE_WAIT=0 CODE_LOCATION="https://raw.githubusercontent.com/srvrco/getssl/master/getssl" CSR_SUBJECT="/" +CURL_USERAGENT="${PROGNAME}/${VERSION}" DEACTIVATE_AUTH="false" -DEFAULT_REVOKE_CA="https://acme-v01.api.letsencrypt.org" -DNS_CHECK_FUNC="" -DNS_CHECK_OPTIONS="" -DNS_EXTRA_WAIT="" -DNS_WAIT=10 +DEFAULT_REVOKE_CA="https://acme-v02.api.letsencrypt.org" DOMAIN_KEY_LENGTH=4096 DUAL_RSA_ECDSA="false" +FTP_OPTIONS="" +FULL_CHAIN_INCLUDE_ROOT="false" GETSSL_IGNORE_CP_PRESERVE="false" HTTP_TOKEN_CHECK_WAIT=0 IGNORE_DIRECTORY_DOMAIN="false" ORIG_UMASK=$(umask) +PREFERRED_CHAIN="" # Set this to use an alternative root certificate PREVIOUSLY_VALIDATED="true" PRIVATE_KEY_ALG="rsa" -export PUBLIC_DNS_SERVER="" RELOAD_CMD="" RENEW_ALLOW="30" REUSE_PRIVATE_KEY="true" @@ -227,29 +304,62 @@ OCSP_MUST_STAPLE="false" TEMP_UPGRADE_FILE="" TOKEN_USER_ID="" USE_SINGLE_ACL="false" -VALIDATE_VIA_DNS="" -WORKING_DIR=~/.getssl +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=10 # 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 _CHECK_ALL=0 _CREATE_CONFIG=0 _FORCE_RENEW=0 _KEEP_VERSIONS="" _MUTE=0 +_NOTIFY_VALID=0 _QUIET=0 _RECREATE_CSR=0 _REVOKE=0 +_RUNNING_TEST=0 +_TEST_SKIP_CNAME_CALL=0 +_TEST_SKIP_SOA_CALL=0 _UPGRADE=0 _UPGRADE_CHECK=1 _USE_DEBUG=0 +_ONLY_CHECK_CONFIG=0 config_errors="false" -LANG=C +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 achive copy of current new certs" + 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 @@ -259,35 +369,121 @@ cert_archive() { # Archive certificate file by copying files to dated archive d 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::-4}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/${DOMAIN}.ec.crt" + 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::-4}.ec.crt" "${DOMAIN_DIR}/archive/${date_time}/chain.ec.crt" - cat "${CERT_FILE::-4}.ec.crt" "${CA_CERT::-4}.ec.crt" > "${DOMAIN_DIR}/archive/${date_time}/fullchain.ec.crt" + 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" } +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 "$CERT_FILE" "$CA_CERT" > "$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 - debug "sending request to ACME server saying we're ready for challenge" - send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" + info "sending request to ACME server saying we're ready for challenge" # check response from our request to perform challenge - if [[ ! -z "$code" ]] && [[ ! "$code" == '202' ]] ; then - error_exit "$domain:Challenge error: $code" + 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 - debug "checking" - if ! get_cr "$uri" ; then - error_exit "$domain:Verify error:$code" + 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) @@ -300,15 +496,18 @@ check_challenge_completion() { # checks with the ACME server if our challenge is # if ACME response is that their check gave an invalid response, error exit if [[ "$status" == "invalid" ]] ; then - err_detail=$(json_get "$response" detail) + err_detail=$(echo "$response" | grep "detail") + # TODO need to check for "DNS problem: SERVFAIL looking up CAA ..." and retry error_exit "$domain:Verify error:$err_detail" fi - # if ACME response is pending ( they haven't completed checks yet) then wait and try again. - if [[ "$status" == "pending" ]] ; then + # 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" ]]; then info "Pending" else - error_exit "$domain:Verify error:$response" + err_detail=$(echo "$response" | grep "detail") + error_exit "$domain:Verify error:$status:$err_detail" fi debug "sleep 5 secs before testing verify again" sleep 5 @@ -321,6 +520,82 @@ check_challenge_completion() { # checks with the ACME server if our challenge is fi } +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" + ntries=0 + check_dns="fail" + while [[ "$check_dns" == "fail" ]]; do + if [[ "$os" == "cygwin" ]]; 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 + debug "$DNS_CHECK_FUNC" TXT "${rr}" "@${ns}" + check_result=$($DNS_CHECK_FUNC TXT "${rr}" "@${ns}" \ + | grep -i "^${rr}" \ + | grep 'IN\WTXT'|awk -F'"' '{ print $2}') + debug "check_result=$check_result" + if [[ -z "$check_result" ]]; then + debug "$DNS_CHECK_FUNC" ANY "${rr}" "@${ns}" + check_result=$($DNS_CHECK_FUNC 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 + check_result=$($DNS_CHECK_FUNC -t TXT "${rr}" "${ns}" \ + | grep 'descriptive text'|awk -F'"' '{ print $2}') + else + check_result=$(nslookup -type=txt "${rr}" "${ns}" \ + | grep 'text ='|awk -F'"' '{ print $2}') + if [[ -z "$check_result" ]]; then + check_result=$(nslookup -type=any "${rr}" "${ns}" \ + | grep 'text ='|awk -F'"' '{ print $2}') + 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 + test_output "Deleting DNS RR via command: ${DNS_DEL_COMMAND}" + del_dns_rr "${d}" "${auth_key}" + test_output "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" @@ -348,13 +623,13 @@ check_config() { # check the config files for all obvious errors config_errors=true fi - # get all domains + # get all domains into an array if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=${SANS//,/ } + read -r -a alldomains <<< "${SANS//[, ]/ }" else - alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") + read -r -a alldomains <<< "$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")" fi - if [[ -z "$alldomains" ]]; then + if [[ -z "${alldomains[*]}" ]]; then info "${DOMAIN}: no domains specified" config_errors=true fi @@ -371,12 +646,15 @@ check_config() { # check the config files for all obvious errors fi dn=0 - tmplist=$(mktemp) - for d in $alldomains; do # loop over domains (dn is domain number) + 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 @@ -392,30 +670,43 @@ check_config() { # check the config files for all obvious errors info "${DOMAIN}: ACL location not specified for domain $d in $DOMAIN_DIR/getssl.cfg" config_errors=true fi - # check domain exist - if [[ "$DNS_CHECK_FUNC" =~ ^drill ]] || [[ "$DNS_CHECK_FUNC" =~ ^dig ]]; then - if [[ "$($DNS_CHECK_FUNC "${d}" SOA|grep -c "^${d}")" -ge 1 ]]; then - debug "found IP for ${d}" - else - info "${DOMAIN}: DNS lookup failed for ${d}" - config_errors=true + + # 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 + debug "DNS lookup using $HAS_DIG_OR_DRILL ${d}" + if [[ "$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -t SOA "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then + found_ip=true + elif [[ "$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -t A "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then + found_ip=true + elif [[ "$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -t AAAA "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then + found_ip=true fi - elif [[ "$DNS_CHECK_FUNC" =~ ^host ]]; then - if [[ "$($DNS_CHECK_FUNC "${d}" |grep -c "^${d}")" -ge 1 ]]; then - debug "found IP for ${d}" - else - info "${DOMAIN}: DNS lookup failed for ${d}" - config_errors=true + fi + + if [[ "$HAS_HOST" == "true" ]]; then + debug "DNS lookup using host ${d}" + if [[ "$(host $DNS_CHECK_OPTIONS "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then + found_ip=true fi - elif [[ "$(nslookup -query=AAAA "${d}"|grep -c "^${d}.*has AAAA address")" -ge 1 ]]; then - debug "found IPv6 record for ${d}" - elif [[ "$(nslookup "${d}"| grep -c ^Name)" -ge 1 ]]; then - debug "found IPv4 record for ${d}" - else + fi + + if [[ "$HAS_NSLOOKUP" == "true" ]]; then + debug "DNS lookup using nslookup -query AAAA ${d}" + 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 http-01 challenge + fi # end using dns-01 challenge ((dn++)) done @@ -429,12 +720,14 @@ check_config() { # check the config files for all obvious errors } check_getssl_upgrade() { # check if a more recent version of code is available available - TEMP_UPGRADE_FILE="$(mktemp)" - curl --silent "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE" + TEMP_UPGRADE_FILE="$(mktemp 2>/dev/null || mktemp -t getssl.XXXXXX)" + if [ "$TEMP_UPGRADE_FILE" == "" ]; then + error_exit "mktemp failed" + fi + curl --user-agent "$CURL_USERAGENT" --silent "$CODE_LOCATION" --output "$TEMP_UPGRADE_FILE" errcode=$? if [[ $errcode -eq 60 ]]; then - longmsg=$'Can not authenticate SSL peer. Your ca_bundle.crt and/or curl may need\nupdating. ca_bundle.crt can be updates with mk-ca-bundle. Curl should\nsupport SNI (multiple SSL domains on a single IP)' - error_exit "$longmsg" + 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 : $errcode" fi @@ -446,41 +739,48 @@ check_getssl_upgrade() { # check if a more recent version of code is available a # use a default of 0 for cases where the latest code has not been obtained. if [[ "${latestvdec:-0}" -gt "$localvdec" ]]; then if [[ ${_UPGRADE} -eq 1 ]]; then - install "$0" "${0}.v${VERSION}" - install -m 700 "$TEMP_UPGRADE_FILE" "$0" + 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 if [[ ${_MUTE} -eq 0 ]]; then echo "Updated getssl from v${VERSION} to v${latestversion}" - echo "these update notification can be turned off using the -Q option" + echo "These update notifications can be turned off using the -Q option" echo "" echo "Updates are;" - awk "/\(${VERSION}\)$/ {s=1} s; /\(${latestversion}\)$/ {s=0}" "$TEMP_UPGRADE_FILE" | awk '{if(NR>1)print}' + awk "/\(${VERSION}\)$/ {s=1} s; /\(${latestversion}\)$/ || /^# ----/ {s=0}" "$TEMP_UPGRADE_FILE" | awk '{if(NR>1)print}' echo "" fi if [[ -n "$_KEEP_VERSIONS" ]] && [[ "$_KEEP_VERSIONS" =~ ^[0-9]+$ ]]; then # Obtain all locally stored old versions in getssl_versions declare -a getssl_versions shopt -s nullglob - for getssl_version in $0.v*; do + for getssl_version in "$0".v*; do getssl_versions[${#getssl_versions[@]}]="$getssl_version" done shopt -u nullglob # Explicitly sort the getssl_versions array to make sure shopt -s -o noglob + # shellcheck disable=SC2207 IFS=$'\n' getssl_versions=($(sort <<< "${getssl_versions[*]}")) shopt -u -o noglob - # Remove entries until given amount of old versions to keep is reached + # Remove entries until given number of old versions to keep is reached while [[ ${#getssl_versions[@]} -gt $_KEEP_VERSIONS ]]; do debug "removing old version ${getssl_versions[0]}" rm "${getssl_versions[0]}" getssl_versions=("${getssl_versions[@]:1}") done fi - eval "$ORIGCMD" + if ! eval "$ORIGCMD"; then + error_exit "Running upgraded getssl failed" + fi graceful_exit else info "" info "A more recent version (v${latestversion}) of getssl is available, please update" - info "the easiest way is to use the -u or --upgrade flag" + info "The easiest way is to use the -u or --upgrade flag" info "" fi fi @@ -491,18 +791,22 @@ clean_up() { # Perform pre-exit housekeeping if [[ $VALIDATE_VIA_DNS == "true" ]]; then # Tidy up DNS entries if things failed part way though. shopt -s nullglob - for dnsfile in $TEMP_DIR/dns_verify/*; do + for dnsfile in "$TEMP_DIR"/dns_verify/*; do # shellcheck source=/dev/null . "$dnsfile" debug "attempting to clean up DNS entry for $d" - eval "$DNS_DEL_COMMAND" "$d" "$auth_key" + del_dns_rr "${d}" "${auth_key}" done shopt -u nullglob fi - if [[ ! -z "$DOMAIN_DIR" ]]; then - rm -rf "${TEMP_DIR:?}" + if [[ -n "$DOMAIN_DIR" ]]; then + if [ "${TEMP_DIR}" -ef "/tmp" ]; then + info "Not going to delete TEMP_DIR ${TEMP_DIR} as it appears to be /tmp" + else + rm -rf "${TEMP_DIR:?}" + fi fi - if [[ ! -z "$TEMP_UPGRADE_FILE" ]] && [[ -f "$TEMP_UPGRADE_FILE" ]]; then + if [[ -n "$TEMP_UPGRADE_FILE" ]] && [[ -f "$TEMP_UPGRADE_FILE" ]]; then rm -f "$TEMP_UPGRADE_FILE" fi } @@ -511,28 +815,36 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. cert=$1 # descriptive name, just used for display from=$2 # current file location to=$3 # location to move file to. + suffix=$4 # (optional) optional suffix for DUAL_RSA_ECDSA, i.e. save to private.key becomes save to private.ec.key IFS=\; read -r -a copy_locations <<<"$3" for to in "${copy_locations[@]}"; do + if [[ -n "$suffix" ]]; then + to="${to%.*}.${suffix}.${to##*.}" + fi info "copying $cert to $to" - debug "copying from $from to $to" if [[ "${to:0:4}" == "ssh:" ]] ; then - debug "using scp scp -q $from ${to:4}" - if ! scp -q "$from" "${to:4}" >/dev/null 2>&1 ; then + debug "using scp -q $SCP_OPTS $from ${to:4}" + # shellcheck disable=SC2086 + if ! scp -q $SCP_OPTS "$from" "${to:4}" >/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" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then + 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 - ssh "$servername" "chown $TOKEN_USER_ID $tofile" + # 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 sercure method for copying certificates or keys" + 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}') @@ -545,12 +857,18 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. fromfile=$(basename "$from") debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile" debug "from dir=$fromdir file=$fromfile" - ftp -n <<- _EOF + 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 - user $ftpuser $ftppass + user $ftpuser $ftppass + $FTP_OPTIONS cd $ftpdirn lcd $fromdir - put $fromfile + put ./$fromfile _EOF elif [[ "${to:0:5}" == "sftp:" ]] ; then debug "using sftp to copy the file from $from" @@ -562,13 +880,42 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. ftpfile=$(basename "$ftplocn") fromdir=$(dirname "$from") fromfile=$(basename "$from") - debug "sftp user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile" + debug "sftp $SFTP_OPTS user=$ftpuser - pass=$ftppass - host=$ftphost dir=$ftpdirn file=$ftpfile" debug "from dir=$fromdir file=$fromfile" - sshpass -p "$ftppass" sftp "$ftpuser@$ftphost" <<- _EOF + # shellcheck disable=SC2086 + sshpass -p "$ftppass" sftp $SFTP_OPTS "$ftpuser@$ftphost" <<- _EOF cd $ftpdirn lcd $fromdir - put $fromfile + 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" + curl -u "${davsuser}:${davspass}" -T "${fromdir}/${fromfile}" "https://${davshost}:${davsport}${davsdirn}${davsfile}" + elif [[ "${to:0:6}" == "ftpes:" ]] ; then + 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 dir=$ftpdirn file=$ftpfile" + debug "from dir=$fromdir file=$fromfile" + curl --insecure --ftp-ssl -u "${ftpuser}:${ftppass}" -T "${fromdir}/${fromfile}" "ftp://${ftphost}${ftpdirn}/" else if ! mkdir -p "$(dirname "$to")" ; then error_exit "cannot create ACL directory $(basename "$to")" @@ -582,7 +929,7 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. error_exit "cannot copy $from to $to" fi fi - if [[ "$cert" == "challenge token" ]] && [[ ! -z "$TOKEN_USER_ID" ]]; then + if [[ "$cert" == "challenge token" ]] && [[ -n "$TOKEN_USER_ID" ]]; then chown "$TOKEN_USER_ID" "$to" fi fi @@ -598,24 +945,26 @@ create_csr() { # create a csr using a given key (if it doesn't already exist) debug "domain csr exists at - $csr_file" # check all domains in config are in csr if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u) + read -d '\n' -r -a alldomains <<< "$(echo "$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u)" else - alldomains=$(echo "$DOMAIN,$SANS" | sed -e 's/ //g; s/,$//; y/,/\n/' | sort -u) + 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 + 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}$")" + ".... $(echo "${domains_in_csr}"| grep "^${d}$")" _RECREATE_CSR=1 fi done # check all domains in csr are in config - if [[ "$alldomains" != "$domains_in_csr" ]]; then + 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 + test_output "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 @@ -624,7 +973,7 @@ create_csr() { # create a csr using a given key (if it doesn't already exist) if [[ ! -s "$csr_file" ]] || [[ "$_RECREATE_CSR" == "1" ]]; then info "creating domain csr - $csr_file" # create a temporary config file, for portability. - tmp_conf=$(mktemp) + tmp_conf=$(mktemp 2>/dev/null || mktemp -t getssl) || error_exit "mktemp failed" cat "$SSLCONF" > "$tmp_conf" printf "[SAN]\n%s" "$SANLIST" >> "$tmp_conf" # add OCSP Must-Staple to the domain csr @@ -658,19 +1007,72 @@ create_key() { # create a domain key (if it doesn't already exist) esac umask "$ORIG_UMASK" # remove csr on generation of new domain key - if [[ -e "${key_loc::-4}.csr" ]]; then - rm -f "${key_loc::-4}.csr" + 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}]" + # request NewOrder currently seems to ignore the dates .... + # dstring="${dstring},\"notBefore\": \"$(date -d "-1 hour" --utc +%FT%TZ)\"" + # dstring="${dstring},\"notAfter\": \"$(date -d "2 days" --utc +%FT%TZ)\"" + request="{\"identifiers\": $dstring}" + 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" && -z "$wildcard" ) || ( "$lower_d" == "*.${authdomain}" && -n "$wildcard" ) ]]; 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 + done + fi +} + date_epoc() { # convert the date into epoch time if [[ "$os" == "bsd" ]]; then date -j -f "%b %d %T %Y %Z" "$1" +%s elif [[ "$os" == "mac" ]]; then date -j -f "%b %d %T %Y %Z" "$1" +%s elif [[ "$os" == "busybox" ]]; then - de_ld=$(echo "$1" | awk '{print $1 $2 $3 $4}') + de_ld=$(echo "$1" | awk '{print $1 " " $2 " " $3 " " $4}') date -D "%b %d %T %Y" -d "$de_ld" +%s else date -d "$1" +%s @@ -681,7 +1083,7 @@ date_epoc() { # convert the date into epoch time date_fmt() { # format date from epoc time to YYYY-MM-DD if [[ "$os" == "bsd" ]]; then #uses older style date function. date -j -f "%s" "$1" +%F - elif [[ "$os" == "mac" ]]; then # MAC OSX uses older BSD style date. + elif [[ "$os" == "mac" ]]; then # macOS uses older BSD style date. date -j -f "%s" "$1" +%F else date -d "@$1" +%F @@ -695,8 +1097,19 @@ date_renew() { # calculates the renewal time in epoch debug() { # write out debug info if the debug flag has been set if [[ ${_USE_DEBUG} -eq 1 ]]; then - echo " " - echo "$@" + # If running tests then output in TAP format (for debugging tests) + if [[ ${_RUNNING_TEST} -eq 1 ]]; then + echo "# $(date "+%b %d %T") ${FUNCNAME[1]}:${BASH_LINENO[1]}" "$@" >&3 + else + echo " " + echo "$@" + fi + fi +} + +test_output() { # write out debug output for testing + if [[ ${_RUNNING_TEST} -eq 1 ]]; then + echo "#" "$@" fi } @@ -706,252 +1119,650 @@ error_exit() { # give error message on error exit exit 1 } -get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns ) - gad_d="$1" # domain name - gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER - - if [[ "$os" == "cygwin" ]]; then - all_auth_dns_servers=$(nslookup -type=soa "${d}" ${PUBLIC_DNS_SERVER} 2>/dev/null \ - | grep "primary name server" \ - | awk '{print $NF}') - if [[ -z "$all_auth_dns_servers" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - fi - primary_ns="$all_auth_dns_servers" - return - fi - if [[ "$DNS_CHECK_FUNC" =~ ^drill ]] || [[ "$DNS_CHECK_FUNC" =~ ^dig ]]; then - if [[ -z "$gad_s" ]]; then #checking for CNAMEs - res=$($DNS_CHECK_FUNC "$gad_d" CNAME| grep "^$gad_d") - else - res=$($DNS_CHECK_FUNC "$gad_d" CNAME "@$gad_s"| grep "^$gad_d") - fi - if [[ ! -z "$res" ]]; then # domain is a CNAME so get main domain - gad_d=$(echo "$res"| awk '{print $5}' |sed 's/\.$//g') - fi - if [[ -z "$gad_s" ]]; then #checking for CNAMEs - res=$($DNS_CHECK_FUNC "$gad_d" NS| grep "^$gad_d") - else - res=$($DNS_CHECK_FUNC "$gad_d" NS "@$gad_s"| grep "^$gad_d") - fi - if [[ -z "$res" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - else - all_auth_dns_servers=$(echo "$res" | awk '$4 ~ "NS" {print $5}' | sed 's/\.$//g'|tr '\n' ' ') - fi - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') +find_dns_utils() { + HAS_NSLOOKUP=false + HAS_DIG_OR_DRILL="" + HAS_HOST=false + if [[ -n "$(command -v nslookup 2>/dev/null)" ]]; then + debug "HAS NSLOOKUP=true" + HAS_NSLOOKUP=true fi - return - fi - if [[ "$DNS_CHECK_FUNC" =~ ^host ]]; then - if [[ -z "$gad_s" ]]; then - res=$($DNS_CHECK_FUNC -t NS "$gad_d"| grep "name server") - else - res=$($DNS_CHECK_FUNC -t NS "$gad_d" "$gad_s"| grep "name server") - fi - if [[ -z "$res" ]]; then - error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" - else - all_auth_dns_servers=$(echo "$res" | awk '{print $4}' | sed 's/\.$//g'|tr '\n' ' ') - fi - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + if [[ -n "$(command -v drill 2>/dev/null)" ]]; then + debug "HAS DIG_OR_DRILL=drill" + HAS_DIG_OR_DRILL="drill" + elif [[ -n "$(command -v dig 2>/dev/null)" ]] && dig >/dev/null 2>&1; then + if [[ $(dig -r >/dev/null 2>&1) ]]; then + # use dig -r so ~/.digrc is not used + HAS_DIG_OR_DRILL="dig -r" + else + HAS_DIG_OR_DRILL="dig" + fi + debug "HAS DIG_OR_DRILL=$HAS_DIG_OR_DRILL" fi - return - fi - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" ${gad_s}) - - if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then - # this is a Non-authoritative server, need to check for an authoritative one. - gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') - if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then - # if domain name doesn't exist, then find auth servers for next level up - gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }') - gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}') + if [[ -n "$(command -v host 2>/dev/null)" ]]; then + debug "HAS HOST=true" + HAS_HOST=true fi - fi - - if [[ -z "$gad_s" ]]; then - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d") - else - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" "${gad_s}") - fi - - if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then - gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') - elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then - gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }') - gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') - fi +} - all_auth_dns_servers=$(nslookup -type=soa -type=ns "$gad_d" "$gad_s" \ - | awk ' $2 ~ "nameserver" {print $4}' \ - | sed 's/\.$//g'| tr '\n' ' ') - if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then - primary_ns="$all_auth_dns_servers" - else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') +find_ftp_command() { + FTP_COMMAND="" + if [[ -n "$(command -v ftp 2>/dev/null)" ]]; then + debug "Has ftp" + FTP_COMMAND="ftp -n" + elif [[ -n "$(command -v lftp 2>/dev/null)" ]]; then + debug "Has lftp" + FTP_COMMAND="lftp" fi } -get_certificate() { # get certificate for csr, if all domains validated. - gc_csr=$1 # the csr file - gc_certfile=$2 # The filename for the certificate - gc_cafile=$3 # The filename for the CA certificate - - der=$(openssl req -in "$gc_csr" -outform DER | urlbase64) - debug "der $der" - send_signed_request "$URL_new_cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64" - - # convert certificate information into correct format and save to file. - CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r') - debug "certdata location = $CertData" - if [[ "$CertData" ]] ; then - echo -----BEGIN CERTIFICATE----- > "$gc_certfile" - curl --silent "$CertData" | openssl base64 -e >> "$gc_certfile" - echo -----END CERTIFICATE----- >> "$gc_certfile" - info "Certificate saved in $gc_certfile" - fi - # If certificate wasn't a valid certificate, error exit. - if [[ -z "$CertData" ]] ; then - response2=$(echo "$response" | fold -w64 |openssl base64 -d) - debug "response was $response" - error_exit "Sign failed: $(echo "$response2" | grep "detail")" - fi +add_dns_rr() { + d=${1} + auth_key=${2} - # get a copy of the CA certificate. - IssuerData=$(grep -i '^Link' "$CURL_HEADER" \ - | cut -d " " -f 2\ - | cut -d ';' -f 1 \ - | sed 's///g') - if [[ "$IssuerData" ]] ; then - echo -----BEGIN CERTIFICATE----- > "$gc_cafile" - curl --silent "$IssuerData" | openssl base64 -e >> "$gc_cafile" - echo -----END CERTIFICATE----- >> "$gc_cafile" - info "The intermediate CA cert is in $gc_cafile" - fi + # shellcheck disable=SC2018,SC2019 + lower_d=$(printf '%s' "${d#\*.}" | tr 'A-Z' 'a-z') + debug "adding DNS RR via command: ${DNS_ADD_COMMAND} ${lower_d} ${auth_key}" + eval "${DNS_ADD_COMMAND}" "${lower_d}" "${auth_key}" } -get_cr() { # get curl response - url="$1" - debug url "$url" - response=$(curl --silent "$url") - ret=$? - debug response "$response" - code=$(json_get "$response" status) - debug code "$code" - debug "get_cr return code $ret" - return $ret +del_dns_rr() { + d=${1} + auth_key=${2} + + # shellcheck disable=SC2018,SC2019 + lower_d=$(printf '%s' "${d#\*.}" | tr 'A-Z' 'a-z') + debug "removing DNS RR via command: ${DNS_DEL_COMMAND} ${lower_d} ${auth_key}" + eval "${DNS_DEL_COMMAND}" "${lower_d}" "${auth_key}" } -get_os() { # function to get the current Operating System - uname_res=$(uname -s) - if [[ $(date -h 2>&1 | grep -ic busybox) -gt 0 ]]; then - os="busybox" - elif [[ ${uname_res} == "Linux" ]]; then - os="linux" - elif [[ ${uname_res} == "FreeBSD" ]]; then - os="bsd" - elif [[ ${uname_res} == "Darwin" ]]; then - os="mac" - elif [[ ${uname_res:0:6} == "CYGWIN" ]]; then - os="cygwin" - elif [[ ${uname_res:0:5} == "MINGW" ]]; then - os="mingw" +fulfill_challenges() { +dn=0 +for d in "${alldomains[@]}"; do + # $d is domain in current loop, which is number $dn for ACL + info "Verifying $d" + if [[ "$USE_SINGLE_ACL" == "true" ]]; then + DOMAIN_ACL="${ACL[0]}" else - os="unknown" + DOMAIN_ACL="${ACL[$dn]}" fi - debug "detected os type = $os" -} -get_signing_params() { # get signing parameters from key - skey=$1 - if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key - pub_exp64=$(openssl rsa -in "${skey}" -noout -text \ - | grep publicExponent \ - | grep -oE "0x[a-f0-9]+" \ - | cut -d'x' -f2 \ - | hex2bin \ - | urlbase64) - pub_mod64=$(openssl rsa -in "${skey}" -noout -modulus \ - | cut -d'=' -f2 \ - | hex2bin \ - | urlbase64) - - jwk='{"e":"'"${pub_exp64}"'","kty":"RSA","n":"'"${pub_mod64}"'"}' - jwkalg="RS256" - signalg="sha256" - elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. - crv="$(openssl ec -in "$skey" -noout -text 2>/dev/null | awk '$2 ~ "CURVE:" {print $3}')" - if [[ -z "$crv" ]]; then - gsp_keytype="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ - | grep "^ASN1 OID:" \ - | awk '{print $3}')" - case "$gsp_keytype" in - prime256v1) crv="P-256" ;; - secp384r1) crv="P-384" ;; - secp521r1) crv="P-521" ;; - *) error_exit "invalid curve algorithm type $gsp_keytype";; - esac + # request a challenge token from ACME server + if [[ $API -eq 1 ]]; then + request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"${d##\*.}\"}}" + send_signed_request "$URL_new_authz" "$request" + debug "completed send_signed_request" + + # check if we got a valid response and token, if not then error exit + if [[ -n "$code" ]] && [[ ! "$code" == '201' ]] ; then + error_exit "new-authz error: $response" fi - case "$crv" in - P-256) jwkalg="ES256" ; signalg="sha256" ;; - P-384) jwkalg="ES384" ; signalg="sha384" ;; - P-521) jwkalg="ES512" ; signalg="sha512" ;; - *) error_exit "invalid curve algorithm type $crv";; - esac - pubtext="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ - | awk '/^pub:/{p=1;next}/^ASN1 OID:/{p=0}p' \ - | tr -d ": \n\r")" - mid=$(( (${#pubtext} -2) / 2 + 2 )) - debug "pubtext = $pubtext" - x64=$(echo "$pubtext" | cut -b 3-$mid | hex2bin | urlbase64) - y64=$(echo "$pubtext" | cut -b $((mid+1))-${#pubtext} | hex2bin | urlbase64) - jwk='{"crv":"'"$crv"'","kty":"EC","x":"'"$x64"'","y":"'"$y64"'"}' - debug "jwk $jwk" else - error_exit "Invalid key file" + response=${AuthLinkResponse[$dn]} + responseHeaders=${AuthLinkResponseHeader[$dn]} + response_status=$(json_get "$response" status) fi - thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)" - debug "jwk alg = $jwkalg" - debug "jwk = $jwk" - debug "thumbprint $thumbprint" -} -graceful_exit() { # normal exit function. - clean_up - exit -} + if [[ $response_status == "valid" ]]; then + info "$d is already validated" + if [[ "$DEACTIVATE_AUTH" == "true" ]]; then + deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")" + deactivate_url_list+=" $deactivate_url " + debug "url added to deactivate list ${deactivate_url}" + debug "deactivate list is now $deactivate_url_list" + fi + # increment domain-counter + ((dn++)) + else + PREVIOUSLY_VALIDATED="false" + if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification + if [[ $API -eq 1 ]]; then + # get the dns component of the ACME response + # get the token and uri from the dns component + token=$(json_get "$response" "token" "dns-01") + uri=$(json_get "$response" "uri" "dns-01") + debug uri "$uri" + else # APIv2 + debug "authlink response = ${response//[$'\t\r\n']}" + # get the token and uri from the dns-01 component + token=$(json_get "$response" "challenges" "type" "dns-01" "token") + uri=$(json_get "$response" "challenges" "type" "dns-01" "url") + debug uri "$uri" + fi -help_message() { # print out the help message - cat <<- _EOF_ - $PROGNAME ver. $VERSION - Obtain SSL certificates from the letsencrypt.org ACME server + keyauthorization="$token.$thumbprint" + debug keyauthorization "$keyauthorization" + + #create signed authorization key from token. + auth_key=$(printf '%s' "$keyauthorization" \ + | openssl dgst -sha256 -binary \ + | openssl base64 -e \ + | tr -d '\n\r' \ + | sed -e 's:=*$::g' -e 'y:+/:-_:') + debug auth_key "$auth_key" + + add_dns_rr "${d}" "${auth_key}" \ + || error_exit "DNS_ADD_COMMAND failed for domain $d" + + # shellcheck disable=SC2018,SC2019 + rr="_acme-challenge.$(printf '%s' "${d#\*.}" | tr 'A-Z' 'a-z')" + + # find a primary / authoritative DNS server for the domain + if [[ -z "$AUTH_DNS_SERVER" ]]; then + # Find authorative dns server for _acme-challenge.{domain} (for CNAMES/acme-dns) + get_auth_dns "${rr}" + if test -n "${cname}"; then + rr=${cname} + fi + + # If no authorative dns server found, try again for {domain} + if [[ -z "$primary_ns" ]]; then + get_auth_dns "$d" + fi + elif [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then + primary_ns="$AUTH_DNS_SERVER $PUBLIC_DNS_SERVER" + else + primary_ns="$AUTH_DNS_SERVER" + fi + debug set primary_ns = "$primary_ns" + + # internal check + check_challenge_completion_dns "${d}" "${rr}" "${primary_ns}" "${auth_key}" + + # let Let's Encrypt check + check_challenge_completion "${uri}" "${d}" "${keyauthorization}" + + del_dns_rr "${d}" "${auth_key}" + else # set up the correct http token for verification + if [[ $API -eq 1 ]]; then + # get the token from the http component + token=$(json_get "$response" "token" "http-01") + # get the uri from the http component + uri=$(json_get "$response" "uri" "http-01") + debug uri "$uri" + else # APIv2 + debug "authlink response = ${response//[$'\t\r\n']}" + # get the token from the http-01 component + token=$(json_get "$response" "challenges" "type" "http-01" "token") + # get the uri from the http component + uri=$(json_get "$response" "challenges" "type" "http-01" "url" | head -n1) + debug uri "$uri" + fi + + #create signed authorization key from token. + keyauthorization="$token.$thumbprint" + + # save variable into temporary file + echo -n "$keyauthorization" > "$TEMP_DIR/$token" + chmod 644 "$TEMP_DIR/$token" + + # copy to token to acme challenge location + umask 0022 + IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" + for t_loc in "${token_locations[@]}"; do + debug "copying file from $TEMP_DIR/$token to ${t_loc}" + copy_file_to_location "challenge token" \ + "$TEMP_DIR/$token" \ + "${t_loc}/$token" + done + umask "$ORIG_UMASK" + + wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}/.well-known/acme-challenge/$token" + debug wellknown_url "$wellknown_url" + + if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then + info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly" + else + sleep "$HTTP_TOKEN_CHECK_WAIT" + # check that we can reach the challenge ourselves, if not, then error + if [[ ! "$(curl --user-agent "$CURL_USERAGENT" -k --silent --location "$wellknown_url")" == "$keyauthorization" ]]; then + error_exit "for some reason could not reach $wellknown_url - please check it manually" + fi + fi + + check_challenge_completion "$uri" "$d" "$keyauthorization" + + debug "remove token from ${DOMAIN_ACL}" + IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" + for t_loc in "${token_locations[@]}"; do + if [[ "${t_loc:0:4}" == "ssh:" ]] ; then + sshhost=$(echo "${t_loc}"| awk -F: '{print $2}') + command="rm -f ${t_loc:(( ${#sshhost} + 5))}/${token:?}" + debug "running following command to remove token" + debug "ssh $SSH_OPTS $sshhost ${command}" + # shellcheck disable=SC2029 disable=SC2086 + ssh $SSH_OPTS "$sshhost" "${command}" 1>/dev/null 2>&1 + rm -f "${TEMP_DIR:?}/${token:?}" + elif [[ "${t_loc:0:4}" == "ftp:" ]] ; then + debug "using ftp to remove token file" + ftpuser=$(echo "${t_loc}"| awk -F: '{print $2}') + ftppass=$(echo "${t_loc}"| awk -F: '{print $3}') + ftphost=$(echo "${t_loc}"| awk -F: '{print $4}') + ftplocn=$(echo "${t_loc}"| awk -F: '{print $5}') + debug "$FTP_COMMAND user=$ftpuser - pass=$ftppass - host=$ftphost location=$ftplocn" + $FTP_COMMAND <<- EOF + open $ftphost + user $ftpuser $ftppass + cd $ftplocn + delete ${token:?} + EOF + else + rm -f "${t_loc:?}/${token:?}" + fi + done + fi + # increment domain-counter + ((dn++)) + fi +done # end of ... loop through domains for cert ( from SANS list) +#end of verify each domain. +} + +get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns ) + orig_gad_d="$1" # domain name + orig_gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER + gad_d="$orig_gad_d" + gad_s="$orig_gad_s" + + if [[ "$os" == "cygwin" ]]; then + # shellcheck disable=SC2086 + all_auth_dns_servers=$(nslookup -type=soa "${d}" ${PUBLIC_DNS_SERVER} 2>/dev/null \ + | grep "primary name server" \ + | awk '{print $NF}') + if [[ -z "$all_auth_dns_servers" ]]; then + error_exit "couldn't find primary DNS server - please set AUTH_DNS_SERVER in config" + fi + primary_ns="$all_auth_dns_servers" + if [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then + primary_ns="$primary_ns $PUBLIC_DNS_SERVER" + fi + + return + fi + + if [[ -n "$HAS_DIG_OR_DRILL" ]]; then + if [[ -n "$gad_s" ]]; then + gad_s="@$gad_s" + fi + + # Check if domain is a CNAME, first + test_output "Using $HAS_DIG_OR_DRILL CNAME" + + # Two options here; either dig CNAME will return the CNAME and the NS or just the CNAME + debug Checking for CNAME using "$HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS CNAME $gad_d $gad_s" + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS CNAME "$gad_d" $gad_s| grep "^$gad_d") + cname=$(echo "$res"| awk '$4 ~ "CNAME" {print $5}' |sed 's/\.$//g') + + if [[ $_TEST_SKIP_CNAME_CALL == 0 ]]; then + debug Checking if CNAME result contains NS records + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS CNAME "$gad_d" $gad_s| grep -E "IN\W(NS|SOA)\W") + else + res= + fi + + if [[ -n "${cname}" ]]; then + # domain is a CNAME: resolve it and continue with that + debug Domain is a CNAME, actual domain is "$cname" + gad_d=${cname} + fi + + # Use SOA +trace to find the name server + if [[ -z "$res" ]] && [[ $_TEST_SKIP_SOA_CALL == 0 ]]; then + if [[ "$HAS_DIG_OR_DRILL" == "drill" ]]; then + debug Using "$HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -T $gad_d $gad_s" to find primary nameserver + test_output "Using $HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS SOA" + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS -T SOA "$gad_d" $gad_s 2>/dev/null | grep "IN\WNS\W") + else + debug Using "$HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS SOA +trace +nocomments $gad_d $gad_s" to find primary nameserver + test_output "Using $HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS SOA" + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS SOA +trace +nocomments "$gad_d" $gad_s 2>/dev/null | grep "IN\WNS\W") + fi + fi + + # Query for NS records + if [[ -z "$res" ]]; then + test_output "Using $HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS NS" + debug Using "$HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS NS $gad_d $gad_s" to find primary nameserver + res=$($HAS_DIG_OR_DRILL $DNS_CHECK_OPTIONS NS "$gad_d" $gad_s | grep -E "IN\W(NS|SOA)\W") + fi + + if [[ -n "$res" ]]; then + # Convert dig output into an array of nameservers + IFS=$'\n' read -r -d '' -a ns_servers < <(echo "$res" | awk '$4 ~ "(NS|SOA)" {print $5}' | sed 's/\.$//g') + + # 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 + # i.e. if we have root, google, duckdns1, duckdns2 then return all the duckdns servers + ns_domain=${ns_servers[${#ns_servers[@]} -1 ]#*.} + all_auth_dns_servers="" + for i in "${ns_servers[@]}"; do + if [[ $i =~ $ns_domain ]]; then + all_auth_dns_servers="$all_auth_dns_servers $i" + fi + done + + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print " " $1}') + fi + + if [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then + primary_ns="$primary_ns $PUBLIC_DNS_SERVER" + fi + + test_output set primary_ns ="$primary_ns" + + return + fi + fi + + # Remove leading '@' if we tried using dig/drill + gad_s="$orig_gad_s" + + if [[ "$HAS_HOST" == "true" ]]; then + gad_d="$orig_gad_d" + debug Using "host -t NS" to find primary name server for "$gad_d" + if [[ -z "$gad_s" ]]; then + res=$(host $DNS_CHECK_OPTIONS -t NS "$gad_d"| grep "name server") + else + # shellcheck disable=SC2086 + res=$(host $DNS_CHECK_OPTIONS -t NS "$gad_d" $gad_s| grep "name server") + fi + if [[ -n "$res" ]]; then + all_auth_dns_servers=$(echo "$res" | awk '{print $4}' | sed 's/\.$//g'|tr '\n' ' ') + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + fi + + if [[ "$CHECK_PUBLIC_DNS_SERVER" == "true" ]]; then + primary_ns="$primary_ns $PUBLIC_DNS_SERVER" + fi + + return + fi + fi + + if [[ "$HAS_NSLOOKUP" == "true" ]]; then + gad_d="$orig_gad_d" + debug Using "nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns $gad_d $gad_s" to find primary name server + # shellcheck disable=SC2086 + res=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" ${gad_s}) + + if [[ "$(echo "$res" | grep -c "Non-authoritative")" -gt 0 ]]; then + # this is a Non-authoritative server, need to check for an authoritative one. + gad_s=$(echo "$res" | awk '$2 ~ "nameserver" {print $4; exit }' |sed 's/\.$//g') + if [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + # if domain name doesn't exist, then find auth servers for next level up + gad_s=$(echo "$res" | awk '$1 ~ "origin" {print $3; exit }') + gad_d=$(echo "$res" | awk '$1 ~ "->" {print $2; exit}') + # handle scenario where awk returns nothing + if [[ -z "$gad_d" ]]; then + gad_d="$orig_gad_d" + fi + fi + + # shellcheck disable=SC2086 + res=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" ${gad_s}) + fi + + if [[ "$(echo "$res" | grep -c "canonical name")" -gt 0 ]]; then + gad_d=$(echo "$res" | awk ' $2 ~ "canonical" {print $5; exit }' |sed 's/\.$//g') + elif [[ "$(echo "$res" | grep -c "an't find")" -gt 0 ]]; then + gad_s=$(echo "$res" | awk ' $1 ~ "origin" {print $3; exit }') + gad_d=$(echo "$res"| awk '$1 ~ "->" {print $2; exit}') + # handle scenario where awk returns nothing + if [[ -z "$gad_d" ]]; then + gad_d="$orig_gad_d" + fi + fi + + # shellcheck disable=SC2086 + # not quoting gad_s fixes the nslookup: couldn't get address for '': not found warning (#332) + all_auth_dns_servers=$(nslookup $DNS_CHECK_OPTIONS -debug -type=soa -type=ns "$gad_d" $gad_s \ + | awk '$1 ~ "nameserver" {print $3}' \ + | sed 's/\.$//g'| tr '\n' ' ') + + if [[ -n "$all_auth_dns_servers" ]]; then + if [[ $CHECK_ALL_AUTH_DNS == "true" ]]; then + primary_ns="$all_auth_dns_servers" + else + primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + fi + + return + fi + fi + + # nslookup on alpine/ubuntu containers doesn't support -debug, print a warning in this case + # This means getssl cannot check that the DNS record has been updated on the primary name server + info "Warning: Couldn't find primary DNS server - please set PUBLIC_DNS_SERVER or AUTH_DNS_SERVER in config" + info "This means getssl cannot check the DNS entry has been updated" +} + +get_certificate() { # get certificate for csr, if all domains validated. + gc_csr=$1 # the csr file + gc_certfile=$2 # The filename for the certificate + gc_cafile=$3 # The filename for the CA certificate + gc_fullchain=$4 # The filename for the fullchain + + der=$(openssl req -in "$gc_csr" -outform DER | urlbase64) + + if [[ $API -eq 1 ]]; then + send_signed_request "$URL_new_cert" "{\"resource\": \"new-cert\", \"csr\": \"$der\"}" "needbase64" + # convert certificate information into correct format and save to file. + CertData=$(awk ' $1 ~ "^Location" {print $2}' "$CURL_HEADER" |tr -d '\r') + if [[ "$CertData" ]] ; then + echo -----BEGIN CERTIFICATE----- > "$gc_certfile" + curl --user-agent "$CURL_USERAGENT" --silent "$CertData" | openssl base64 -e >> "$gc_certfile" + echo -----END CERTIFICATE----- >> "$gc_certfile" + info "Certificate saved in $CERT_FILE" + fi + + # If certificate wasn't a valid certificate, error exit. + if [[ -z "$CertData" ]] ; then + response2=$(echo "$response" | fold -w64 |openssl base64 -d) + debug "response was $response" + error_exit "Sign failed: $(echo "$response2" | grep "detail")" + fi + + # get a copy of the CA certificate. + IssuerData=$(grep -i '^Link' "$CURL_HEADER" \ + | cut -d " " -f 2\ + | cut -d ';' -f 1 \ + | sed 's///g') + if [[ "$IssuerData" ]] ; then + echo -----BEGIN CERTIFICATE----- > "$gc_cafile" + curl --user-agent "$CURL_USERAGENT" --silent "$IssuerData" | openssl base64 -e >> "$gc_cafile" + echo -----END CERTIFICATE----- >> "$gc_cafile" + info "The intermediate CA cert is in $gc_cafile" + fi + else # APIv2 + info "Requesting Finalize Link" + send_signed_request "$FinalizeLink" "{\"csr\": \"$der\"}" "needbase64" + info Requesting Order Link + debug "order link was $OrderLink" + send_signed_request "$OrderLink" "" + # if ACME response is processing (still creating certificates) then wait and try again. + while [[ "$response_status" == "processing" ]]; do + info "ACME server still Processing certificates" + sleep 5 + send_signed_request "$OrderLink" "" + done + info "Requesting certificate" + CertData=$(json_get "$response" "certificate") + send_signed_request "$CertData" "" "" "$gc_fullchain" + IFS=$'\n' read -r -d '' -a alternate_links < <(echo "$responseHeaders" | grep "^Link" | grep "alternate" | awk -F"[<>]" '{print $2}') + debug "Alternate Links are ${alternate_links[*]}" + if [[ -n "$PREFERRED_CHAIN" ]]; then + cert_to_check=$(mktemp 2>/dev/null || mktemp -t getssl.XXXXXX) || error_exit "mktemp failed" + # Check the default certificate to see if that has the required chain + cp "$gc_fullchain" "$cert_to_check" + i=0 + while [[ $i -le ${#alternate_links[@]} ]]; do + cert_issuer=$(openssl crl2pkcs7 -nocrl -certfile "$cert_to_check" | openssl pkcs7 -print_certs -text -noout | grep 'Issuer:' | tail -1 | awk -F"CN=" '{ print $2 }') + debug Certificate issued by "$cert_issuer" + if [[ $cert_issuer = *${PREFERRED_CHAIN}* ]]; then + debug "Found required certificate" + cp "$cert_to_check" "$gc_fullchain" + break + fi + + if [[ $i -lt ${#alternate_links[@]} ]]; then + debug "Fetching next alternate certificate $i ${alternate_links[$i]}" + send_signed_request "${alternate_links[$i]}" "" "" "$cert_to_check" + fi + i=$(( i + 1 )) + done + + # tidy up + rm -f "$cert_to_check" + fi + + 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" + if [[ "$FULL_CHAIN_INCLUDE_ROOT" = "true" ]]; then + # Some of the code below was copied from zakjan/cert-chain-resolver + + # Download the certificate for the issuer using the "CA Issuers" attribute from the AIA x509 extension + issuer_url=$(openssl x509 -inform pem -noout -text -in "$gc_certfile" | awk 'BEGIN {FS="CA Issuers - URI:"} NF==2 {print $2; exit}') + debug Issuer for "$gc_certfile" is "$issuer_url" + + # Keep downloading issuer certficates until we find the root certificate (which doesn't have a "CA Issuers" attribure) + cp "$gc_certfile" "$gc_fullchain" + while [[ -n "$issuer_url" ]]; do + debug Fetching certificate issuer from "$issuer_url" + issuer_cert=$(curl --user-agent "$CURL_USERAGENT" --silent "$issuer_url" | openssl x509 -inform der -outform pem) + debug Fetched issuer certificate "$(echo "$issuer_cert" | openssl x509 -inform pem -noout -text | awk 'BEGIN {FS="Subject: "} NF==2 {print $2; exit}')" + echo "$issuer_cert" >> "$gc_fullchain" + + # get issuer for the certificate that's just been downloaded + issuer_url=$(echo "$issuer_cert" | openssl x509 -inform pem -noout -text | awk 'BEGIN {FS="CA Issuers - URI:"} NF==2 {print $2; exit}') + done + fi + info "Certificate saved in $gc_certfile" + fi +} + +get_cr() { # get curl response + url="$1" + debug url "$url" + response=$(curl --user-agent "$CURL_USERAGENT" --silent "$url") + ret=$? + debug response "${response//[$'\t\r\n']}" + code=$(json_get "$response" status) + debug code "$code" + debug "get_cr return code $ret" + return $ret +} + +get_os() { # function to get the current Operating System + uname_res=$(uname -s) + if [[ $(date -h 2>&1 | grep -ic busybox) -gt 0 ]]; then + os="busybox" + elif [[ ${uname_res} == "Linux" ]]; then + os="linux" + elif [[ ${uname_res} == "FreeBSD" ]]; then + os="bsd" + elif [[ ${uname_res} == "Darwin" ]]; then + os="mac" + elif [[ ${uname_res:0:6} == "CYGWIN" ]]; then + os="cygwin" + elif [[ ${uname_res:0:5} == "MINGW" ]]; then + os="mingw" + else + os="unknown" + fi + debug "detected os type = $os" + if [[ -f /etc/issue ]]; then + debug "Running $(cat /etc/issue)" + fi +} + +get_signing_params() { # get signing parameters from key + skey=$1 + if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key + pub_exp64=$(openssl rsa -in "${skey}" -noout -text \ + | grep publicExponent \ + | grep -oE "0x[a-f0-9]+" \ + | cut -d'x' -f2 \ + | hex2bin \ + | urlbase64) + pub_mod64=$(openssl rsa -in "${skey}" -noout -modulus \ + | cut -d'=' -f2 \ + | hex2bin \ + | urlbase64) + + jwk='{"e":"'"${pub_exp64}"'","kty":"RSA","n":"'"${pub_mod64}"'"}' + jwkalg="RS256" + signalg="sha256" + elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. + crv="$(openssl ec -in "$skey" -noout -text 2>/dev/null | awk '$2 ~ "CURVE:" {print $3}')" + if [[ -z "$crv" ]]; then + gsp_keytype="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ + | grep "^ASN1 OID:" \ + | awk '{print $3}')" + case "$gsp_keytype" in + prime256v1) crv="P-256" ;; + secp384r1) crv="P-384" ;; + secp521r1) crv="P-521" ;; + *) error_exit "invalid curve algorithm type $gsp_keytype";; + esac + fi + case "$crv" in + P-256) jwkalg="ES256" ; signalg="sha256" ;; + P-384) jwkalg="ES384" ; signalg="sha384" ;; + P-521) jwkalg="ES512" ; signalg="sha512" ;; + *) error_exit "invalid curve algorithm type $crv";; + esac + pubtext="$(openssl ec -in "$skey" -noout -text 2>/dev/null \ + | awk '/^pub:/{p=1;next}/^ASN1 OID:/{p=0}p' \ + | tr -d ": \n\r")" + mid=$(( (${#pubtext} -2) / 2 + 2 )) + x64=$(echo "$pubtext" | cut -b 3-$mid | hex2bin | urlbase64) + y64=$(echo "$pubtext" | cut -b $((mid+1))-${#pubtext} | hex2bin | urlbase64) + jwk='{"crv":"'"$crv"'","kty":"EC","x":"'"$x64"'","y":"'"$y64"'"}' + else + error_exit "Invalid key file" + fi + thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)" + debug "jwk alg = $jwkalg" +} + +graceful_exit() { # normal exit function. + exit_code=$1 + clean_up + # shellcheck disable=SC2086 + exit $exit_code +} + +help_message() { # print out the help message + cat <<- _EOF_ + $PROGNAME ver. $VERSION + Obtain SSL certificates from the letsencrypt.org ACME server $(usage) Options: -a, --all Check all certificates - -d, --debug Outputs debug information + -d, --debug Output debug information -c, --create Create default config files -f, --force Force renewal of cert (overrides expiry checks) -h, --help Display this help message and exit + -i, --install Install certificates and reload service -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) - -Q, --mute Like -q, but mutes notification about successful upgrade + -Q, --mute Like -q, but also mute notification about successful upgrade -r, --revoke "cert" "key" [CA_server] Revoke a certificate (the cert and key are required) - -u, --upgrade Upgrade getssl if a more recent version is available - -k, --keep "#" Maximum amount of old getssl versions to keep when upgrading + -u, --upgrade Upgrade getssl if a more recent version is available - can be used with or without domain(s) + -k, --keep "#" Maximum number of old getssl versions to keep when upgrading -U, --nocheck Do not check if a more recent version is available + -v --version Display current version of $PROGNAME -w working_dir "Working directory" + --preferred-chain "chain" Use an alternate chain for the certificate _EOF_ } @@ -963,33 +1774,249 @@ hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no info() { # write out info as long as the quiet flag has not been set. if [[ ${_QUIET} -eq 0 ]]; then + # If running tests then output in TAP format as well (for debugging tests) + if [[ ${_RUNNING_TEST} -eq 1 ]]; then + echo "# $(date "+%b %d %T") ${FUNCNAME[1]}:${BASH_LINENO[1]}" "$@" >&3 + fi + echo "$@" fi } -json_get() { # get the value corresponding to $2 in the JSON passed as $1. - # remove newlines, so it's a single chunk of JSON - json_data=$( echo "$1" | tr '\n' ' ') - # if $3 is defined, this is the section which the item is in. - if [[ ! -z "$3" ]]; then - jg_section=$(echo "$json_data" | awk -F"[}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${3}"'\"/){print $i}}}') - if [[ "$2" == "uri" ]]; then - jg_subsect=$(echo "$jg_section" | awk -F"[,]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i)}}}') - jg_result=$(echo "$jg_subsect" | awk -F'"' '{print $4}') +json_awk() { # AWK json converter used for API2 - needs tidying up ;) +# shellcheck disable=SC2086 +echo "$1" | tr -d '\n' | awk ' +{ + tokenize($0) # while(get_token()) {print TOKEN} + if (0 == parse()) { + apply(JPATHS, NJPATHS) + } +} + +function apply (ary,size,i) { + for (i=1; i NTOKENS) to = NTOKENS + for (i = from; i < ITOKENS; i++) + context = context sprintf("%s ", TOKENS[i]) + context = context "<<" got ">> " + for (i = ITOKENS + 1; i <= to; i++) + context = context sprintf("%s ", TOKENS[i]) + scream("json_awk expected <" expected "> but got <" got "> at input token " ITOKENS "\n" context) +} + +function reset() { + TOKEN=""; delete TOKENS; NTOKENS=ITOKENS=0 + delete JPATHS; NJPATHS=0 + VALUE="" +} + +function scream(msg) { + FAILS[FILENAME] = FAILS[FILENAME] (FAILS[FILENAME]!="" ? "\n" : "") msg + msg = FILENAME ": " msg + print msg >"/dev/stderr" +} + +function tokenize(a1,pq,pb,ESCAPE,CHAR,STRING,NUMBER,KEYWORD,SPACE) { + SPACE="[ \t\n]+" + 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) + gsub("\n" SPACE, "\n", a1) + sub(/^\n/, "", a1) + ITOKENS=0 # get_token() helper + return NTOKENS = split(a1, TOKENS, /\n/) +}' +} + +json_get() { # get values from json + if [[ -z "$1" ]] || [[ "$1" == "null" ]]; then + echo "json was blank" + return + fi + if [[ $API = 1 ]]; then + # remove newlines, so it's a single chunk of JSON + json_data=$( echo "$1" | tr '\n' ' ') + # if $3 is defined, this is the section which the item is in. + if [[ -n "$3" ]]; then + jg_section=$(echo "$json_data" | awk -F"[}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${3}"'\"/){print $i}}}') + if [[ "$2" == "uri" ]]; then + jg_subsect=$(echo "$jg_section" | awk -F"[,]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i)}}}') + jg_result=$(echo "$jg_subsect" | awk -F'"' '{print $4}') + else + jg_result=$(echo "$jg_section" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + fi else - jg_result=$(echo "$jg_section" | awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + jg_result=$(echo "$json_data" |awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + fi + # check number of quotes + jg_q=${jg_result//[^\"]/} + # if 2 quotes, assume it's a quoted variable and just return the data within the quotes. + if [[ ${#jg_q} -eq 2 ]]; then + echo "$jg_result" | awk -F'"' '{print $2}' + else + echo "$jg_result" fi else - jg_result=$(echo "$json_data" |awk -F"[,:}]" '{for(i=1;i<=NF;i++){if($i~/\"'"${2}"'\"/){print $(i+1)}}}') + if [[ -n "$6" ]]; then + full=$(json_awk "$1") + section=$(echo "$full" | grep "\"$2\"" | grep "\"$3\"" | grep "\"$4\"" | awk -F"," '{print $2}') + echo "$full" | grep "^..${5}\",$section\]" | awk '{print $2}' | tr -d '"' + elif [[ -n "$5" ]]; then + full=$(json_awk "$1") + section=$(echo "$full" | grep "\"$2\"" | grep "\"$3\"" | grep "\"$4\"" | awk -F"," '{print $2}') + echo "$full" | grep "^..${2}\",$section" | grep "$5" | awk '{print $2}' | tr -d '"' + elif [[ -n "$3" ]]; then + json_awk "$1" | grep "^..${2}...${3}" | awk '{print $2}' | tr -d '"' + elif [[ -n "$2" ]]; then + json_awk "$1" | grep "^..${2}" | awk '{print $2}' | tr -d '"' + else + json_awk "$1" + fi fi - # check number of quotes - jg_q=${jg_result//[^\"]/} - # if 2 quotes, assume it's a quoted variable and just return the data within the quotes. - if [[ ${#jg_q} -eq 2 ]]; then - echo "$jg_result" | awk -F'"' '{print $2}' +} + +obtain_ca_resource_locations() +{ + for suffix in "" "/directory" "/dir"; + do + # Obtain CA resource locations + ca_all_loc=$(curl --user-agent "$CURL_USERAGENT" "${CA}${suffix}" 2>/dev/null) + debug "ca_all_loc from ${CA}${suffix} gives $ca_all_loc" + # APIv1 + URL_new_reg=$(echo "$ca_all_loc" | grep "new-reg" | awk -F'"' '{print $4}') + URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}') + URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}') + #API v2 + URL_newAccount=$(echo "$ca_all_loc" | grep "newAccount" | awk -F'"' '{print $4}') + URL_newNonce=$(echo "$ca_all_loc" | grep "newNonce" | awk -F'"' '{print $4}') + URL_newOrder=$(echo "$ca_all_loc" | grep "newOrder" | awk -F'"' '{print $4}') + URL_revoke=$(echo "$ca_all_loc" | grep "revokeCert" | awk -F'"' '{print $4}') + + if [[ -n "$URL_new_reg" ]] || [[ -n "$URL_newAccount" ]]; then + break + fi + done + + if [[ -n "$URL_new_reg" ]]; then + API=1 + elif [[ -n "$URL_newAccount" ]]; then + API=2 else - echo "$jg_result" + error_exit "unknown API version" fi + debug "Using API v$API" } os_esed() { # Use different sed version for different os types (extended regex) @@ -1005,7 +2032,7 @@ os_esed() { # Use different sed version for different os types (extended regex) purge_archive() { # purge archive of old, invalid, certificates arcdir="$1/archive" debug "purging archives in ${arcdir}/" - for padir in $arcdir/????_??_??_??_??; do + for padir in "$arcdir"/????_??_??_??_??; do # check each directory if [[ -d "$padir" ]]; then tstamp=$(basename "$padir"| awk -F"_" '{print $1"-"$2"-"$3" "$4":"$5}') @@ -1028,23 +2055,27 @@ purge_archive() { # purge archive of old, invalid, certificates } reload_service() { # Runs a command to reload services ( via ssh if needed) - if [[ ! -z "$RELOAD_CMD" ]]; then + if [[ -n "$RELOAD_CMD" ]]; then info "reloading SSL services" - if [[ "${RELOAD_CMD:0:4}" == "ssh:" ]] ; then - sshhost=$(echo "$RELOAD_CMD"| awk -F: '{print $2}') - command=${RELOAD_CMD:(( ${#sshhost} + 5))} - debug "running following command to reload cert" - debug "ssh $sshhost ${command}" - # shellcheck disable=SC2029 - ssh "$sshhost" "${command}" 1>/dev/null 2>&1 - # allow 2 seconds for services to restart - sleep 2 - else - debug "running reload command $RELOAD_CMD" - if ! eval "$RELOAD_CMD" ; then - error_exit "error running $RELOAD_CMD" + for ARELOAD_CMD in "${RELOAD_CMD[@]}" + do + if [[ "${ARELOAD_CMD:0:4}" == "ssh:" ]] ; then + sshhost=$(echo "$ARELOAD_CMD"| awk -F: '{print $2}') + command=${ARELOAD_CMD:(( ${#sshhost} + 5))} + debug "running following command to reload cert:" + debug "ssh $SSH_OPTS $sshhost ${command}" + # shellcheck disable=SC2029 + # shellcheck disable=SC2086 + ssh $SSH_OPTS "$sshhost" "${command}" 1>/dev/null 2>&1 + # allow 2 seconds for services to restart + sleep 2 + else + debug "running reload command: $ARELOAD_CMD" + if ! eval "$ARELOAD_CMD" ; then + error_exit "error running: $ARELOAD_CMD" + fi fi - fi + done fi } @@ -1054,10 +2085,10 @@ revoke_certificate() { # revoke a certificate ACCOUNT_KEY="$REVOKE_KEY" # need to set the revoke key as "account_key" since it's used in send_signed_request. get_signing_params "$REVOKE_KEY" - TEMP_DIR=$(mktemp -d) - debug "revoking from $CA" - rcertdata=$(openssl x509 -in "$REVOKE_CERT" -inform PEM -outform DER | urlbase64) - send_signed_request "$URL_revoke" "{\"resource\": \"revoke-cert\", \"certificate\": \"$rcertdata\"}" + TEMP_DIR=$(mktemp -d 2>/dev/null || mktemp -d -t getssl) || error_exit "mktemp failed" + debug "revoking from $URL_revoke" + rcertdata=$(sed '1d;$d' "$REVOKE_CERT" | tr -d "\r\n" | tr '/+' '_-' | tr -d '= ') + send_signed_request "$URL_revoke" "{\"certificate\": \"$rcertdata\",\"reason\": $REVOKE_REASON}" if [[ $code -eq "200" ]]; then info "certificate revoked" else @@ -1066,21 +2097,23 @@ revoke_certificate() { # revoke a certificate } requires() { # check if required function is available + args=("${@}") + lastarg=${args[${#args[@]}-1]} if [[ "$#" -gt 1 ]]; then # if more than 1 value, check list for i in "$@"; do - if [[ "$i" == "${!#}" ]]; then # if on last variable then exit as not found + if [[ "$i" == "$lastarg" ]]; then # if on last variable then exit as not found error_exit "this script requires one of: ${*:1:$(($#-1))}" fi - res=$(which "$i" 2>/dev/null) + res=$(command -v "$i" 2>/dev/null) debug "checking for $i ... $res" - if [[ ! -z "$res" ]]; then # if function found, then set variable to function and return - debug "function $i found at $res - setting ${!#} to $i" - eval "${!#}=\$i" + if [[ -n "$res" ]]; then # if function found, then set variable to function and return + debug "function $i found at $res - setting ${lastarg} to $i" + eval "${lastarg}=\$i" return fi done else # only one value, so check it. - result=$(which "$1" 2>/dev/null) + result=$(command -v "$1" 2>/dev/null) debug "checking for required $1 ... $result" if [[ -z "$result" ]]; then error_exit "This script requires $1 installed" @@ -1133,71 +2166,139 @@ send_signed_request() { # Sends a request to the ACME server, signed with your p url=$1 payload=$2 needbase64=$3 + outfile=$4 # save response into this file (certificate data) debug url "$url" - debug payload "$payload" CURL_HEADER="$TEMP_DIR/curl.header" dp="$TEMP_DIR/curl.dump" - CURL="curl --http1.1 --silent --dump-header $CURL_HEADER " + + CURL="curl " + # shellcheck disable=SC2072 + if [[ "$($CURL -V | head -1 | cut -d' ' -f2 )" > "7.33" ]]; then + CURL="$CURL --http1.1 " + fi + + CURL="$CURL --user-agent $CURL_USERAGENT --silent --dump-header $CURL_HEADER " + if [[ ${_USE_DEBUG} -eq 1 ]]; then CURL="$CURL --trace-ascii $dp " fi - # convert payload to url base 64 - payload64="$(printf '%s' "${payload}" | urlbase64)" - debug payload64 "$payload64" + # convert payload to url base 64 + payload64="$(printf '%s' "${payload}" | urlbase64)" + + # get nonce from ACME server + if [[ $API -eq 1 ]]; then + nonceurl="$CA/directory" + nonce=$($CURL -I "$nonceurl" | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ') + else # APIv2 + nonce=$($CURL -I "$URL_newNonce" | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ') + fi + + nonceproblem="true" + while [[ "$nonceproblem" == "true" ]]; do + + # Build header with just our public key and algorithm information + header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}' + + # Build another header which also contains the previously received nonce and encode it as urlbase64 + if [[ $API -eq 1 ]]; then + protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' + protected64="$(printf '%s' "${protected}" | urlbase64)" + else # APIv2 + if [[ -z "$KID" ]]; then + debug "KID is blank, so using jwk" + protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' + protected64="$(printf '%s' "${protected}" | urlbase64)" + else + debug "using KID=${KID}" + protected="{\"alg\": \"$jwkalg\", \"kid\": \"$KID\",\"nonce\": \"${nonce}\", \"url\": \"${url}\"}" + protected64="$(printf '%s' "${protected}" | urlbase64)" + fi + fi + + # Sign header with nonce and our payload with our private key and encode signature as urlbase64 + sign_string "$(printf '%s' "${protected64}.${payload64}")" "${ACCOUNT_KEY}" "$signalg" + + # Send header + extended header + payload + signature to the acme-server + debug "payload = $payload" + if [[ $API -eq 1 ]]; then + body="{\"header\": ${header}," + body="${body}\"protected\": \"${protected64}\"," + body="${body}\"payload\": \"${payload64}\"," + body="${body}\"signature\": \"${signed64}\"}" + else + body="{" + body="${body}\"protected\": \"${protected64}\"," + body="${body}\"payload\": \"${payload64}\"," + body="${body}\"signature\": \"${signed64}\"}" + fi + + code="500" + loop_limit=5 + while [[ "$code" -eq 500 ]]; do + if [[ "$outfile" ]] ; then + $CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" > "$outfile" + errcode=$? + response=$(cat "$outfile") + elif [[ "$needbase64" ]] ; then + response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url" | urlbase64) + errcode=$? + else + response=$($CURL -X POST -H "Content-Type: application/jose+json" --data "$body" "$url") + errcode=$? + fi + + if [[ $errcode -gt 0 || ( "$response" == "" && $url != *"revoke"* ) ]]; then + error_exit "ERROR curl \"$url\" failed with $errcode and returned $response" + fi - # get nonce from ACME server - nonceurl="$CA/directory" - nonce=$($CURL -I $nonceurl | grep "^Replay-Nonce:" | awk '{print $2}' | tr -d '\r\n ') - - debug nonce "$nonce" - - # Build header with just our public key and algorithm information - header='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"'}' - - # Build another header which also contains the previously received nonce and encode it as urlbase64 - protected='{"alg": "'"$jwkalg"'", "jwk": '"$jwk"', "nonce": "'"${nonce}"'", "url": "'"${url}"'"}' - protected64="$(printf '%s' "${protected}" | urlbase64)" - debug protected "$protected" - - # Sign header with nonce and our payload with our private key and encode signature as urlbase64 - sign_string "$(printf '%s' "${protected64}.${payload64}")" "${ACCOUNT_KEY}" "$signalg" - - # Send header + extended header + payload + signature to the acme-server - body="{\"header\": ${header}," - body="${body}\"protected\": \"${protected64}\"," - body="${body}\"payload\": \"${payload64}\"," - body="${body}\"signature\": \"${signed64}\"}" - debug "header, payload and signature = $body" - - code="500" - loop_limit=5 - while [[ "$code" -eq 500 ]]; do - if [[ "$needbase64" ]] ; then - response=$($CURL -X POST --data "$body" "$url" | urlbase64) - else - response=$($CURL -X POST --data "$body" "$url") - fi - - touch "$CURL_HEADER" - responseHeaders=$(cat "$CURL_HEADER") - debug responseHeaders "$responseHeaders" - debug response "$response" - code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1) - debug code "$code" - response_status=$(json_get "$response" status \ - | head -1| awk -F'"' '{print $2}') - debug "response status = $response_status" - - if [[ "$code" -eq 500 ]]; then - info "error on acme server - trying again ...." - sleep 2 - loop_limit=$((loop_limit - 1)) - if [[ $loop_limit -lt 1 ]]; then - error_exit "500 error from ACME server: $response" + responseHeaders=$(cat "$CURL_HEADER") + if [[ "$needbase64" && ${response##*()} != "{"* ]]; then + # response is in base64 too, decode + response=$(urlbase64_decode "$response") + fi + + debug responseHeaders "$responseHeaders" + debug response "${response//[$'\t\r\n']}" + code=$(awk ' $1 ~ "^HTTP" {print $2}' "$CURL_HEADER" | tail -1) + debug code "$code" + if [[ "$code" == 4* && $response != *"error:badNonce"* && "$code" != 409 ]]; then + detail=$(echo "$response" | grep "detail") + error_exit "ACME server returned error: ${code}: ${detail}" fi + + if [[ $API -eq 1 ]]; then + response_status=$(json_get "$response" status \ + | head -1| awk -F'"' '{print $2}') + else # APIv2 + if [[ "$outfile" && "$response" ]]; then + debug "response written to $outfile" + elif [[ ${response##*()} == "{"* ]]; then + response_status=$(json_get "$response" status) + else + debug "response not in json format" + debug "$response" + fi + fi + debug "response status = $response_status" + if [[ "$code" -eq 500 ]]; then + info "error on acme server - trying again ...." + debug "loop_limit = $loop_limit" + sleep 5 + loop_limit=$((loop_limit - 1)) + if [[ $loop_limit -lt 1 ]]; then + error_exit "500 error from ACME server: $response" + fi + fi + done + if [[ $response == *"error:badNonce"* ]]; then + debug "bad nonce" + nonce=$(echo "$responseHeaders" | grep -i "^replay-nonce:" | awk '{print $2}' | tr -d '\r\n ') + debug "trying new nonce $nonce" + else + nonceproblem="false" fi done } @@ -1211,49 +2312,28 @@ sign_string() { # sign a string with a given key and algorithm and return urlbas if openssl rsa -in "${skey}" -noout 2>/dev/null ; then # RSA key signed64="$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" | urlbase64)" elif openssl ec -in "${skey}" -noout 2>/dev/null ; then # Elliptic curve key. - signed=$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" -hex | awk '{print $2}') - debug "EC signature $signed" - if [[ "${signed:4:4}" == "0220" ]]; then #sha256 - R=$(echo "$signed" | cut -c 9-72) - part2=$(echo "$signed" | cut -c 73-) - elif [[ "${signed:4:4}" == "0221" ]]; then #sha256 - R=$(echo "$signed" | cut -c 11-74) - part2=$(echo "$signed" | cut -c 75-) - elif [[ "${signed:4:4}" == "0230" ]]; then #sha384 - R=$(echo "$signed" | cut -c 9-104) - part2=$(echo "$signed" | cut -c 105-) - elif [[ "${signed:4:4}" == "0231" ]]; then #sha384 - R=$(echo "$signed" | cut -c 11-106) - part2=$(echo "$signed" | cut -c 107-) - elif [[ "${signed:6:4}" == "0241" ]]; then #sha512 - R=$(echo "$signed" | cut -c 11-140) - part2=$(echo "$signed" | cut -c 141-) - elif [[ "${signed:6:4}" == "0242" ]]; then #sha512 - R=$(echo "$signed" | cut -c 11-142) - part2=$(echo "$signed" | cut -c 143-) + # ECDSA signature width + # e.g. 521 bits requires 66 bytes to express, a signature consists of 2 integers so 132 bytes + # https://crypto.stackexchange.com/questions/12299/ecc-key-size-and-signature-size/ + if [ "$signalg" = "sha256" ]; then + w=64 + elif [ "$signalg" = "sha384" ]; then + w=96 + elif [ "$signalg" = "sha512" ]; then + w=132 else - error_exit "error in EC signing couldn't get R from $signed ($signalg using $key)" + error_exit "Unknown signing algorithm $signalg" fi + asn1parse=$(printf '%s' "${str}" | openssl dgst -"$signalg" -sign "$key" | openssl asn1parse -inform DER) + #shellcheck disable=SC2086 + R=$(echo $asn1parse | awk '{ print $13 }' | cut -c2-) debug "R $R" - - if [[ "${part2:0:4}" == "0220" ]]; then #sha256 - S=$(echo "$part2" | cut -c 5-68) - elif [[ "${part2:0:4}" == "0221" ]]; then #sha256 - S=$(echo "$part2" | cut -c 7-70) - elif [[ "${part2:0:4}" == "0230" ]]; then #sha384 - S=$(echo "$part2" | cut -c 5-100) - elif [[ "${part2:0:4}" == "0231" ]]; then #sha384 - S=$(echo "$part2" | cut -c 7-102) - elif [[ "${part2:0:4}" == "0241" ]]; then #sha512 - S=$(echo "$part2" | cut -c 5-136) - elif [[ "${part2:0:4}" == "0242" ]]; then #sha512 - S=$(echo "$part2" | cut -c 5-136) - else - error_exit "error in EC signing couldn't get S from $signed" - fi - + #shellcheck disable=SC2086 + S=$(echo $asn1parse | awk '{ print $20 }' | cut -c2-) debug "S $S" - signed64=$(printf '%s' "${R}${S}" | hex2bin | urlbase64 ) + + # pad R and S to the correct length for the signing algorithm + signed64=$(printf "%${w}s%${w}s" "${R}" "${S}" | tr ' ' '0' | hex2bin | urlbase64 ) debug "encoded RS $signed64" fi } @@ -1274,102 +2354,158 @@ urlbase64() { # urlbase64: base64 encoded string with '+' replaced with '-' and openssl base64 -e | tr -d '\n\r' | os_esed -e 's:=*$::g' -e 'y:+/:-_:' } +# base64url decode +# From: https://gist.github.com/alvis/89007e96f7958f2686036d4276d28e47 +urlbase64_decode() { + INPUT=$1 # $(if [ -z "$1" ]; then echo -n $(cat -); else echo -n "$1"; fi) + MOD=$(($(echo -n "$INPUT" | wc -c) % 4)) + PADDING=$(if [ $MOD -eq 2 ]; then echo -n '=='; elif [ $MOD -eq 3 ]; then echo -n '=' ; fi) + echo -n "$INPUT$PADDING" | + sed s/-/+/g | + sed s/_/\\//g | + openssl base64 -d -A +} + usage() { # echos out the program usage echo "Usage: $PROGNAME [-h|--help] [-d|--debug] [-c|--create] [-f|--force] [-a|--all] [-q|--quiet]"\ - "[-Q|--mute] [-u|--upgrade] [-k|--keep #] [-U|--nocheck] [-r|--revoke cert key] [-w working_dir] domain" + "[-Q|--mute] [-u|--upgrade] [-k|--keep #] [-U|--nocheck] [-r|--revoke cert key] [-w working_dir]"\ + "[--preferred-chain chain] domain" } write_domain_template() { # write out a template file for a domain. - cat > "$1" <<- _EOF_domain_ - # Uncomment and modify any variables you need - # see https://github.com/srvrco/getssl/wiki/Config-variables for details - # see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs - # - # The staging server is best for testing - #CA="https://acme-staging.api.letsencrypt.org" - # This server issues full certificates, however has rate limits - #CA="https://acme-v01.api.letsencrypt.org" - - #PRIVATE_KEY_ALG="rsa" - - # Additional domains - this could be multiple domains / subdomains in a comma separated list - # Note: this is Additional domains - so should not include the primary domain. - SANS="${EX_SANS}" - - # Acme Challenge Location. The first line for the domain, the following ones for each additional domain. - # If these start with ssh: then the next variable is assumed to be the hostname and the rest the location. - # An ssh key will be needed to provide you with access to the remote server. - # Optionally, you can specify a different userid for ssh/scp to use on the remote server before the @ sign. - # If left blank, the username on the local server will be used to authenticate against the remote server. - # If these start with ftp: then the next variables are ftpuserid:ftppassword:servername:ACL_location - # These should be of the form "/path/to/your/website/folder/.well-known/acme-challenge" - # where "/path/to/your/website/folder/" is the path, on your web server, to the web root for your domain. - #ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ssh:sshuserid@server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' - # 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge') - - #Set USE_SINGLE_ACL="true" to use a single ACL for all checks - #USE_SINGLE_ACL="false" - - # Location for all your certs, these can either be on the server (full path name) - # or using ssh /sftp as for the ACL - #DOMAIN_CERT_LOCATION="/etc/ssl/${DOMAIN}.crt" - #DOMAIN_KEY_LOCATION="/etc/ssl/${DOMAIN}.key" - #CA_CERT_LOCATION="/etc/ssl/chain.crt" - #DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert - #DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert - - # The command needed to reload apache / nginx or whatever you use - #RELOAD_CMD="" - - # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, - # smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which - # will be checked for certificate expiry and also will be checked after - # an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true - #SERVER_TYPE="https" - #CHECK_REMOTE="true" - - # Unusual configurations (especially split views) may require these. - # If these (or any variable) apply to all your domains, put them in - # the per-domain getssl.cfg. - # - # If you must use an external DNS Server (e.g. due to split views) - # Specify it here. Otherwise, the default is to find the zone master. - # The default will usually work. - # PUBLIC_DNS_SERVER="8.8.8.8" - - # If getssl is unable to determine the authoritative nameserver for a domain - # it will as you to enter AUTH_DNS_SERVER. This is the primary server that - # getssl will use to check for the acme tokens. It must be visible externally - # as well as internally. It need not be "authoritiative" in the RFC1035 sense. - # AUTH_DNS_SERVER="8.8.8.8" - _EOF_domain_ + if [[ -s "$WORKING_DIR/getssl_default.cfg" ]]; then + export DOMAIN="$DOMAIN" + export EX_SANS="$EX_SANS" + envsubst < "$WORKING_DIR/getssl_default.cfg" > "$1" + else + cat > "$1" <<- _EOF_domain_ + # vim: filetype=sh + # + # This file is read second (and per domain if running with the -a option) + # and overwrites any settings from the first file + # + # Uncomment and modify any variables you need + # see https://github.com/srvrco/getssl/wiki/Config-variables for details + # see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + # + # The staging server is best for testing + #CA="https://acme-staging-v02.api.letsencrypt.org" + # This server issues full certificates, however has rate limits + #CA="https://acme-v02.api.letsencrypt.org" + + # Private key types - can be rsa, prime256v1, secp384r1 or secp521r1 + #PRIVATE_KEY_ALG="rsa" + + # Additional domains - this could be multiple domains / subdomains in a comma separated list + # Note: this is Additional domains - so should not include the primary domain. + SANS="${EX_SANS}" + + # Acme Challenge Location. The first line for the domain, the following ones for each additional domain. + # If these start with ssh: then the next variable is assumed to be the hostname and the rest the location. + # An ssh key will be needed to provide you with access to the remote server. + # Optionally, you can specify a different userid for ssh/scp to use on the remote server before the @ sign. + # If left blank, the username on the local server will be used to authenticate against the remote server. + # If these start with ftp:/ftpes: then the next variables are ftpuserid:ftppassword:servername:ACL_location + # These should be of the form "/path/to/your/website/folder/.well-known/acme-challenge" + # where "/path/to/your/website/folder/" is the path, on your web server, to the web root for your domain. + # You can also user WebDAV over HTTPS as transport mechanism. To do so, start with davs: followed by username, + # password, host, port (explicitly needed even if using default port 443) and path on the server. + # Multiple locations can be defined for a file by separating the locations with a semi-colon. + #ACL=('/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ssh:server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ssh:sshuserid@server5:/var/www/${DOMAIN}/web/.well-known/acme-challenge' + # 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge' + # 'davs:davsuserid:davspassword:{DOMAIN}:443:/web/.well-known/acme-challenge' + # 'ftpes:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge') + + # Specify SSH options, e.g. non standard port in SSH_OPTS + # (Can also use SCP_OPTS and SFTP_OPTS) + # SSH_OPTS=-p 12345 + + # Set USE_SINGLE_ACL="true" to use a single ACL for all checks + #USE_SINGLE_ACL="false" + + # Preferred Chain - use an different certificate root from the default + # This uses wildcard matching so requesting "X1" returns the correct certificate - may need to escape characters + # Staging options are: "(STAGING) Doctored Durian Root CA X3" and "(STAGING) Pretend Pear X1" + # Production options are: "ISRG Root X1" and "ISRG Root X2" + #PREFERRED_CHAIN="\(STAGING\) Pretend Pear X1" + + # Uncomment this if you need the full chain file to include the root certificate (Java keystores, Nutanix Prism) + #FULL_CHAIN_INCLUDE_ROOT="true" + + # Location for all your certs, these can either be on the server (full path name) + # or using ssh /sftp as for the ACL + #DOMAIN_CERT_LOCATION="/etc/ssl/${DOMAIN}.crt" # this is domain cert + #DOMAIN_KEY_LOCATION="/etc/ssl/${DOMAIN}.key" # this is domain key + #CA_CERT_LOCATION="/etc/ssl/chain.crt" # this is CA cert + #DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert + #DOMAIN_PEM_LOCATION="" # this is the domain key, domain cert and CA cert + + # The command needed to reload apache / nginx or whatever you use. + # Several (ssh) commands may be given using a bash array: + # RELOAD_CMD=('ssh:sshuserid@server5:systemctl reload httpd' 'logger getssl for server5 efficient.') + #RELOAD_CMD="" + + # Uncomment the following line to prevent non-interactive renewals of certificates + #PREVENT_NON_INTERACTIVE_RENEWAL="true" + + # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, + # smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which + # will be checked for certificate expiry and also will be checked after + # an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true + #SERVER_TYPE="https" + #CHECK_REMOTE="true" + #CHECK_REMOTE_WAIT="2" # wait 2 seconds before checking the remote server + _EOF_domain_ + fi } write_getssl_template() { # write out the main template file cat > "$1" <<- _EOF_getssl_ + # vim: filetype=sh + # + # This file is read first and is common to all domains + # # Uncomment and modify any variables you need # see https://github.com/srvrco/getssl/wiki/Config-variables for details # # The staging server is best for testing (hence set as default) - CA="https://acme-staging.api.letsencrypt.org" + CA="https://acme-staging-v02.api.letsencrypt.org" # This server issues full certificates, however has rate limits - #CA="https://acme-v01.api.letsencrypt.org" + #CA="https://acme-v02.api.letsencrypt.org" + # The agreement that must be signed with the CA, if not defined the default agreement will be used #AGREEMENT="$AGREEMENT" # Set an email address associated with your account - generally set at account level rather than domain. #ACCOUNT_EMAIL="me@example.com" ACCOUNT_KEY_LENGTH=4096 ACCOUNT_KEY="$WORKING_DIR/account.key" + + # Account key and private key types - can be rsa, prime256v1, secp384r1 or secp521r1 + #ACCOUNT_KEY_TYPE="rsa" PRIVATE_KEY_ALG="rsa" #REUSE_PRIVATE_KEY="true" - # The command needed to reload apache / nginx or whatever you use + # Preferred Chain - use an different certificate root from the default + # This uses wildcard matching so requesting "X1" returns the correct certificate - may need to escape characters + # Staging options are: "(STAGING) Doctored Durian Root CA X3" and "(STAGING) Pretend Pear X1" + # Production options are: "ISRG Root X1" and "ISRG Root X2" + #PREFERRED_CHAIN="\(STAGING\) Pretend Pear X1" + + # Uncomment this if you need the full chain file to include the root certificate (Java keystores, Nutanix Prism) + #FULL_CHAIN_INCLUDE_ROOT="true" + + # The command needed to reload apache / nginx or whatever you use. + # Several (ssh) commands may be given using a bash array: + # RELOAD_CMD=('ssh:sshuserid@server5:systemctl reload httpd' 'logger getssl for server5 efficient.') #RELOAD_CMD="" + # The time period within which you want to allow renewal of a certificate # this prevents hitting some of the rate limits. + # Creating a file called FORCE_RENEWAL in the domain directory allows one-off overrides + # of this setting RENEW_ALLOW="30" # Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, @@ -1418,40 +2554,55 @@ while [[ -n ${1+defined} ]]; do case $1 in -h | --help) help_message; graceful_exit ;; + -v | --version) + echo "$PROGNAME V$VERSION"; graceful_exit ;; -d | --debug) - _USE_DEBUG=1 ;; + _USE_DEBUG=1 ;; -c | --create) - _CREATE_CONFIG=1 ;; + _CREATE_CONFIG=1 ;; -f | --force) - _FORCE_RENEW=1 ;; + _FORCE_RENEW=1 ;; + --notify-valid) + # Exit 2 if certificate is valid and doesn't need renewing + _NOTIFY_VALID=2 ;; -a | --all) - _CHECK_ALL=1 ;; + _CHECK_ALL=1 ;; -k | --keep) - shift; _KEEP_VERSIONS="$1";; + shift; _KEEP_VERSIONS="$1";; -q | --quiet) - _QUIET=1 ;; + _QUIET=1 ;; -Q | --mute) - _QUIET=1 - _MUTE=1 ;; + _QUIET=1 + _MUTE=1 ;; -r | --revoke) - _REVOKE=1 - shift - REVOKE_CERT="$1" - shift - REVOKE_KEY="$1" - shift - REVOKE_CA="$1" ;; + _REVOKE=1 + shift + REVOKE_CERT="$1" + shift + REVOKE_KEY="$1" + shift + CA="$1" + REVOKE_CA="$1" + REVOKE_REASON=0 ;; -u | --upgrade) - _UPGRADE=1 ;; + _UPGRADE=1 ;; -U | --nocheck) _UPGRADE_CHECK=0 ;; + -i | --install) + _CERT_INSTALL=1 ;; + --check-config) + _ONLY_CHECK_CONFIG=1 ;; -w) shift; WORKING_DIR="$1" ;; - -* | --*) + -preferred-chain) + shift; PREFERRED_CHAIN="$1" ;; + --source) + return ;; + -*) usage error_exit "Unknown option $1" ;; *) - if [[ ! -z $DOMAIN ]]; then + if [[ -n $DOMAIN ]]; then error_exit "invalid command line $DOMAIN - it appears to contain more than one domain" fi DOMAIN="$1" @@ -1478,6 +2629,8 @@ get_os requires which requires openssl requires curl +requires dig nslookup drill host DNS_CHECK_FUNC +requires dirname requires awk requires tr requires date @@ -1489,6 +2642,11 @@ requires mktemp # Check if upgrades are available (unless they have specified -U to ignore Upgrade checks) if [[ $_UPGRADE_CHECK -eq 1 ]]; then check_getssl_upgrade + # if nothing in command line and no revocation and not only config check, + # then exit after upgrade + if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]] && [[ ${_REVOKE} -ne 1 ]] && [ "${_ONLY_CHECK_CONFIG}" -ne 1 ]; then + graceful_exit + fi fi # Revoke a certificate if requested @@ -1501,13 +2659,14 @@ if [[ $_REVOKE -eq 1 ]]; then else CA=$REVOKE_CA fi - URL_revoke=$(curl "${CA}/directory" 2>/dev/null | grep "revoke-cert" | awk -F'"' '{print $4}') + + obtain_ca_resource_locations revoke_certificate graceful_exit fi # get latest agreement from CA (as default) -AGREEMENT=$(curl -I "${CA}/terms" 2>/dev/null | awk '$1 ~ "Location:" {print $2}'|tr -d '\r') +AGREEMENT=$(curl --user-agent "$CURL_USERAGENT" -I "${CA}/terms" 2>/dev/null | awk 'tolower($1) ~ "location:" {print $2}'|tr -d '\r') # if nothing in command line, print help and exit. if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]]; then @@ -1515,6 +2674,19 @@ if [[ -z "$DOMAIN" ]] && [[ ${_CHECK_ALL} -ne 1 ]]; then graceful_exit fi +# Test working directory candidates if unset. Last candidate defaults (~/getssl/) +if [[ -z "${WORKING_DIR}" ]] +then + for WORKING_DIR in "${WORKING_DIR_CANDIDATES[@]}" + do + debug "Testing working dir location '${WORKING_DIR}'" + if [[ -s "$WORKING_DIR/getssl.cfg" ]] + then + break + fi + done +fi + # if the "working directory" doesn't exist, then create it. if [[ ! -d "$WORKING_DIR" ]]; then debug "Making working directory - $WORKING_DIR" @@ -1539,6 +2711,7 @@ ACCOUNT_KEY="${ACCOUNT_KEY:=$WORKING_DIR/account.key}" DOMAIN_STORAGE="${DOMAIN_STORAGE:=$WORKING_DIR}" DOMAIN_DIR="$DOMAIN_STORAGE/$DOMAIN" CERT_FILE="$DOMAIN_DIR/${DOMAIN}.crt" +FULL_CHAIN="$DOMAIN_DIR/fullchain.crt" CA_CERT="$DOMAIN_DIR/chain.crt" TEMP_DIR="$DOMAIN_DIR/tmp" if [[ "$os" == "mingw" ]]; then @@ -1564,7 +2737,7 @@ if [[ ${_CHECK_ALL} -eq 1 ]]; then error_exit "DOMAIN_STORAGE not found - $DOMAIN_STORAGE" fi - for dir in ${DOMAIN_STORAGE}/*; do + for dir in "${DOMAIN_STORAGE}"/*; do if [[ -d "$dir" ]]; then debug "Checking $dir" cmd="$0 -U" # No update checks when calling recursively @@ -1574,9 +2747,9 @@ if [[ ${_CHECK_ALL} -eq 1 ]]; then if [[ ${_QUIET} -eq 1 ]]; then cmd="$cmd -q" fi - # check if $dir looks like a domain name (contains a period) - if [[ $(basename "$dir") == *.* ]]; then - cmd="$cmd -w $WORKING_DIR $(basename "$dir")" + # check if $dir is a directory with a getssl.cfg in it + if [[ -f "$dir/getssl.cfg" ]]; then + cmd="$cmd -w $WORKING_DIR \"$(basename "$dir")\"" debug "CMD: $cmd" eval "$cmd" fi @@ -1606,24 +2779,22 @@ if [[ ${_CREATE_CONFIG} -eq 1 ]]; then if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then info "domain config already exists $DOMAIN_DIR/getssl.cfg" else - info "Contacting ${DOMAIN} to inspect current certificate" - EX_CERT=$( { - if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -ge 43 ]]; then - openssl s_client -servername "$DOMAIN" -connect "$DOMAIN":443 /dev/null & PID=$! - sleep ${CHECK_CERT_TIMEOUT} & PIDW=$! - wait -n # Requires bash 4.3+ - kill -9 "$PID" "$PIDW" 2>/dev/null - else - openssl s_client -servername "$DOMAIN" -connect "$DOMAIN":443 /dev/null - fi - } | openssl x509 2>/dev/null) - EX_SANS="www.${DOMAIN}" - if [[ ! -z "${EX_CERT}" ]]; then + info "creating domain config file in $DOMAIN_DIR/getssl.cfg" + # if domain has an existing cert, copy from domain and use to create defaults. + EX_CERT=$(echo \ + | openssl s_client -servername "${DOMAIN##\*.}" -connect "${DOMAIN##\*.}:443" 2>/dev/null \ + | openssl x509 2>/dev/null) + EX_SANS="www.${DOMAIN##\*.}" + if [[ -n "${EX_CERT}" ]]; then + escaped_d=${DOMAIN/\*/\\\*} EX_SANS=$(echo "$EX_CERT" \ | openssl x509 -noout -text 2>/dev/null| grep "Subject Alternative Name" -A2 \ - | grep -Eo "DNS:[a-zA-Z 0-9.-]*" | sed "s@DNS:$DOMAIN@@g" | grep -v '^$' | cut -c 5-) + | grep -Eo "DNS:[a-zA-Z 0-9.\*-]*" | sed "s@DNS:${escaped_d}@@g" | grep -v '^$' | cut -c 5-) EX_SANS=${EX_SANS//$'\n'/','} fi + if [[ -n "${EX_SANS}" ]]; then + info "Adding SANS=$EX_SANS from certificate installed on ${DOMAIN##\*.} to new configuration file" + fi write_domain_template "$DOMAIN_DIR/getssl.cfg" info "created domain config file in $DOMAIN_DIR/getssl.cfg" fi @@ -1653,45 +2824,74 @@ if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then . "$DOMAIN_DIR/getssl.cfg" fi -# In case special options are needed for DNS_CHECK_FUNC, add them -# to the command. E.G. if a TSIG key or bound local IP is required... - -DNS_CHECK_FUNC="${DNS_CHECK_FUNC} ${DNS_CHECK_OPTIONS}" +# Ensure SANS is comma separated by replacing any number of commas or spaces with a single comma +# shellcheck disable=SC2001 +SANS=$(echo "$SANS" | sed 's/[, ]\+/,/g') # from SERVER_TYPE set REMOTE_PORT and REMOTE_EXTRA set_server_type +# check what dns utils are installed +find_dns_utils + +# Find what ftp client is installed +find_ftp_command + +# auto upgrade clients to v2 +auto_upgrade_v2 + # check config for typical errors. check_config +# exit if just checking config (used for testing) +if [ "${_ONLY_CHECK_CONFIG}" -eq 1 ]; then + info "Configuration check successful" + graceful_exit +fi + +# if -i|--install install certs, reload and exit +if [ "0${_CERT_INSTALL}" -eq 1 ]; then + cert_install + reload_service + graceful_exit +fi + if [[ -e "$DOMAIN_DIR/FORCE_RENEWAL" ]]; then rm -f "$DOMAIN_DIR/FORCE_RENEWAL" || error_exit "problem deleting file $DOMAIN_DIR/FORCE_RENEWAL" _FORCE_RENEW=1 info "${DOMAIN}: forcing renewal (due to FORCE_RENEWAL file)" fi -# Obtain CA resource locations -ca_all_loc=$(curl "${CA}/directory" 2>/dev/null) -URL_new_reg=$(echo "$ca_all_loc" | grep "new-reg" | awk -F'"' '{print $4}') -URL_new_authz=$(echo "$ca_all_loc" | grep "new-authz" | awk -F'"' '{print $4}') -URL_new_cert=$(echo "$ca_all_loc" | grep "new-cert" | awk -F'"' '{print $4}') +obtain_ca_resource_locations + +# Check if awk supports json_awk (required for ACMEv2) +if [[ $API -eq 2 ]]; then + json_awk_test=$(json_awk '{ "test": "1" }' 2>/dev/null) + if [[ "${json_awk_test}" == "" ]]; then + 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" + fi +fi # if check_remote is true then connect and obtain the current certificate (if not forcing renewal) if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then - info "Contacting $DOMAIN on port ${REMOTE_PORT} to inspect current certificate" + real_d=${DOMAIN##\*.} + debug "getting certificate for $DOMAIN from remote server ($real_d)" + if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + # shellcheck disable=SC2086 + # check if openssl supports RSA-PSS + if [[ $(echo | openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} -sigalgs RSA-PSS+SHA256 2>/dev/null) ]]; then + CIPHER="-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512:RSA-PSS+SHA256:RSA-PSS+SHA512" + else + CIPHER="-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512" + fi + else + CIPHER="" + fi # shellcheck disable=SC2086 - EX_CERT=$( { - if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -ge 43 ]]; then - echo | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null & PID=$! - sleep ${CHECK_CERT_TIMEOUT} & PIDW=$! - wait -n # Requires bash 4.3+ - kill -9 "$PID" "$PIDW" 2>/dev/null - else - echo | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null - fi - } | openssl x509 2>/dev/null ) - - if [[ ! -z "$EX_CERT" ]]; then # if obtained a cert + EX_CERT=$(echo \ + | openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${CIPHER} 2>/dev/null \ + | openssl x509 2>/dev/null) + if [[ -n "$EX_CERT" ]]; then # if obtained a cert if [[ -s "$CERT_FILE" ]]; then # if local exists CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null) else # since local doesn't exist leave empty so that the domain validation will happen @@ -1703,7 +2903,7 @@ if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then else # check if the certificate is for the right domain EX_CERT_DOMAIN=$(echo "$EX_CERT" | openssl x509 -text \ - | sed -n -e 's/^ *Subject: .* CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ + | sed -n -e 's/^ *Subject: .*CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ | sort -u | grep "^$DOMAIN\$") if [[ "$EX_CERT_DOMAIN" == "$DOMAIN" ]]; then # check renew-date on ex_cert and compare to local ( if local exists) @@ -1751,7 +2951,11 @@ if [[ "${CHECK_REMOTE}" == "true" ]] && [[ $_FORCE_RENEW -eq 0 ]]; then reload_service fi else - info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate" + # Get the domain from the existing certificate for the error message + EX_CERT_DOMAIN=$(echo "$EX_CERT" | openssl x509 -text \ + | sed -n -e 's/^ *Subject: .*CN=\([A-Za-z0-9.-]*\).*$/\1/p; /^ *DNS:.../ { s/ *DNS://g; y/,/\n/; p; }' \ + | sort -u | head -1) + info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate ($EX_CERT_DOMAIN != $real_d)" fi fi else @@ -1770,12 +2974,12 @@ if [[ -s "$CERT_FILE" ]]; then enddate_s=$(date_epoc "$enddate") if [[ $(date_renew) -lt "$enddate_s" ]] && [[ $_FORCE_RENEW -ne 1 ]]; then issuer=$(openssl x509 -in "$CERT_FILE" -noout -issuer 2>/dev/null) - if [[ "$issuer" == *"Fake LE Intermediate"* ]] && [[ "$CA" == "https://acme-v01.api.letsencrypt.org" ]]; then - debug "upgradeing from fake cert to real" + if [[ "$issuer" == *"Fake LE Intermediate"* ]] && [[ "$CA" == "https://acme-v02.api.letsencrypt.org" ]]; then + debug "upgrading from fake cert to real" else info "${DOMAIN}: certificate is valid for more than $RENEW_ALLOW days (until $enddate)" - # everything is OK, so exit. - graceful_exit + # everything is OK, so exit, if requested with the --notify-valid, exit with code 2 + graceful_exit $_NOTIFY_VALID fi else debug "${DOMAIN}: certificate needs renewal" @@ -1798,13 +3002,13 @@ else create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" fi -# if not reusing priavte key, then remove the old keys +# if not reusing private key, then remove the old keys if [[ "$REUSE_PRIVATE_KEY" != "true" ]]; then if [[ -s "$DOMAIN_DIR/${DOMAIN}.key" ]]; then - rm -f "$DOMAIN_DIR/${DOMAIN}.key" + rm -f "$DOMAIN_DIR/${DOMAIN}.key" fi if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then - rm -f "$DOMAIN_DIR/${DOMAIN}.ecs.key" + rm -f "$DOMAIN_DIR/${DOMAIN}.ec.key" fi fi # create new domain keys if they don't already exist @@ -1820,9 +3024,9 @@ fi if [[ -z "$SANS" ]]; then SANLIST="subjectAltName=DNS:${DOMAIN}" elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - SANLIST="subjectAltName=DNS:${SANS//,/,DNS:}" + SANLIST="subjectAltName=DNS:${SANS//[, ]/,DNS:}" else - SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//,/,DNS:}" + SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//[, ]/,DNS:}" fi debug "created SAN list = $SANLIST" @@ -1838,23 +3042,42 @@ fi # currently the code registers every time, and gets an "already registered" back if it has been. get_signing_params "$ACCOUNT_KEY" -if [[ "$ACCOUNT_EMAIL" ]] ; then - regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' -else - regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' -fi - info "Registering account" # send the request to the ACME server. -send_signed_request "$URL_new_reg" "$regjson" +if [[ $API -eq 1 ]]; then + if [[ "$ACCOUNT_EMAIL" ]] ; then + regjson='{"resource": "new-reg", "contact": ["mailto: '$ACCOUNT_EMAIL'"], "agreement": "'$AGREEMENT'"}' + else + regjson='{"resource": "new-reg", "agreement": "'$AGREEMENT'"}' + fi + send_signed_request "$URL_new_reg" "$regjson" +elif [[ $API -eq 2 ]]; then + if [[ "$ACCOUNT_EMAIL" ]] ; then + regjson='{"termsOfServiceAgreed": true, "contact": ["mailto: '$ACCOUNT_EMAIL'"]}' + else + regjson='{"termsOfServiceAgreed": true}' + fi + send_signed_request "$URL_newAccount" "$regjson" +else + debug "cant determine account API" + graceful_exit +fi if [[ "$code" == "" ]] || [[ "$code" == '201' ]] ; then info "Registered" + KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') + debug "KID=_$KID}_" echo "$response" > "$TEMP_DIR/account.json" elif [[ "$code" == '409' ]] ; then - debug "Already registered" + KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') + debug responseHeaders "$responseHeaders" + debug "Already registered KID=$KID" +elif [[ "$code" == '200' ]] ; then + KID=$(echo "$responseHeaders" | grep -i "^location" | awk '{print $2}'| tr -d '\r\n ') + debug responseHeaders "$responseHeaders" + debug "Already registered account, KID=${KID}" else - error_exit "Error registering account ... $(json_get "$response" detail)" + error_exit "Error registering account ...$responseHeaders ... $(json_get "$response" detail)" fi # end of registering account with CA @@ -1863,250 +3086,36 @@ info "Verify each domain" # loop through domains for cert ( from SANS list) if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=${SANS//,/ } + read -r -a alldomains <<< "${SANS//[, ]/ }" else - alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") + read -r -a alldomains <<< "$(echo "$DOMAIN,$SANS" | sed "s/,/ /g")" fi -dn=0 -for d in $alldomains; do - # $d is domain in current loop, which is number $dn for ACL - info "Verifying $d" - if [[ "$USE_SINGLE_ACL" == "true" ]]; then - DOMAIN_ACL="${ACL[0]}" - else - DOMAIN_ACL="${ACL[$dn]}" - fi - - # request a challenge token from ACME server - request="{\"resource\":\"new-authz\",\"identifier\":{\"type\":\"dns\",\"value\":\"$d\"}}" - send_signed_request "$URL_new_authz" "$request" - - debug "completed send_signed_request" - # check if we got a valid response and token, if not then error exit - if [[ ! -z "$code" ]] && [[ ! "$code" == '201' ]] ; then - error_exit "new-authz error: $response" - fi - - if [[ $response_status == "valid" ]]; then - info "$d is already validated" - if [[ "$DEACTIVATE_AUTH" == "true" ]]; then - deactivate_url="$(echo "$responseHeaders" | awk ' $1 ~ "^Location" {print $2}' | tr -d "\r")" - deactivate_url_list+=" $deactivate_url " - debug "url added to deactivate list ${deactivate_url}" - debug "deactivate list is now $deactivate_url_list" - fi - # increment domain-counter - ((dn++)) - else - PREVIOUSLY_VALIDATED="false" - if [[ $VALIDATE_VIA_DNS == "true" ]]; then # set up the correct DNS token for verification - # get the dns component of the ACME response - # get the token from the dns component - token=$(json_get "$response" "token" "dns-01") - debug token "$token" - # get the uri from the dns component - uri=$(json_get "$response" "uri" "dns-01") - debug uri "$uri" - - keyauthorization="$token.$thumbprint" - debug keyauthorization "$keyauthorization" - - #create signed authorization key from token. - auth_key=$(printf '%s' "$keyauthorization" | openssl dgst -sha256 -binary \ - | openssl base64 -e \ - | tr -d '\n\r' \ - | sed -e 's:=*$::g' -e 'y:+/:-_:') - debug auth_key "$auth_key" - - debug "adding dns via command: $DNS_ADD_COMMAND $d $auth_key" - if ! eval "$DNS_ADD_COMMAND" "$d" "$auth_key" ; then - error_exit "DNS_ADD_COMMAND failed for domain $d" - fi - - # find a primary / authoritative DNS server for the domain - if [[ -z "$AUTH_DNS_SERVER" ]]; then - get_auth_dns "$d" - else - primary_ns="$AUTH_DNS_SERVER" - fi - debug primary_ns "$primary_ns" - - # make a directory to hold pending dns-challenges - if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then - mkdir "$TEMP_DIR/dns_verify" - fi - - # generate a file with the current variables for the dns-challenge - cat > "$TEMP_DIR/dns_verify/$d" <<- _EOF_ - token="${token}" - uri="${uri}" - keyauthorization="${keyauthorization}" - d="${d}" - primary_ns="${primary_ns}" - auth_key="${auth_key}" - _EOF_ - - else # set up the correct http token for verification - # get the token from the http component - token=$(json_get "$response" "token" "http-01") - debug token "$token" - # get the uri from the http component - uri=$(json_get "$response" "uri" "http-01") - debug uri "$uri" - - #create signed authorization key from token. - keyauthorization="$token.$thumbprint" - debug keyauthorization "$keyauthorization" - - # save variable into temporary file - echo -n "$keyauthorization" > "$TEMP_DIR/$token" - chmod 644 "$TEMP_DIR/$token" - - # copy to token to acme challenge location - umask 0022 - IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" - for t_loc in "${token_locations[@]}"; do - debug "copying file from $TEMP_DIR/$token to ${t_loc}" - copy_file_to_location "challenge token" \ - "$TEMP_DIR/$token" \ - "${t_loc}/$token" - done - umask "$ORIG_UMASK" - - wellknown_url="${CHALLENGE_CHECK_TYPE}://$d/.well-known/acme-challenge/$token" - debug wellknown_url "$wellknown_url" - - if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then - info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly" - else - sleep "$HTTP_TOKEN_CHECK_WAIT" - # check that we can reach the challenge ourselves, if not, then error - if [[ ! "$(curl -k --silent --location "$wellknown_url")" == "$keyauthorization" ]]; then - error_exit "for some reason could not reach $wellknown_url - please check it manually" - fi - fi - - check_challenge_completion "$uri" "$d" "$keyauthorization" - - debug "remove token from ${DOMAIN_ACL}" - IFS=\; read -r -a token_locations <<<"$DOMAIN_ACL" - for t_loc in "${token_locations[@]}"; do - if [[ "${t_loc:0:4}" == "ssh:" ]] ; then - sshhost=$(echo "${t_loc}"| awk -F: '{print $2}') - command="rm -f ${t_loc:(( ${#sshhost} + 5))}/${token:?}" - debug "running following command to remove token" - debug "ssh $sshhost ${command}" - # shellcheck disable=SC2029 - ssh "$sshhost" "${command}" 1>/dev/null 2>&1 - rm -f "${TEMP_DIR:?}/${token:?}" - elif [[ "${t_loc:0:4}" == "ftp:" ]] ; then - debug "using ftp to remove token file" - ftpuser=$(echo "${t_loc}"| awk -F: '{print $2}') - ftppass=$(echo "${t_loc}"| awk -F: '{print $3}') - ftphost=$(echo "${t_loc}"| awk -F: '{print $4}') - ftplocn=$(echo "${t_loc}"| awk -F: '{print $5}') - debug "ftp user=$ftpuser - pass=$ftppass - host=$ftphost location=$ftplocn" - ftp -n <<- EOF - open $ftphost - user $ftpuser $ftppass - cd $ftplocn - delete ${token:?} - EOF - else - rm -f "${t_loc:?}/${token:?}" - fi - done - fi - # increment domain-counter - ((dn++)) - fi -done # end of ... loop through domains for cert ( from SANS list) - -# perform validation if via DNS challenge -if [[ $VALIDATE_VIA_DNS == "true" ]]; then - # loop through dns-variable files to check if dns has been changed - for dnsfile in $TEMP_DIR/dns_verify/*; do - if [[ -e "$dnsfile" ]]; then - debug "loading DNSfile: $dnsfile" - # shellcheck source=/dev/null - . "$dnsfile" - - # check for token at public dns server, waiting for a valid response. - for ns in $primary_ns; do - debug "checking dns at $ns" - ntries=0 - check_dns="fail" - while [[ "$check_dns" == "fail" ]]; do - if [[ "$os" == "cygwin" ]]; then - check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ - | grep ^_acme -A2\ - | grep '"'|awk -F'"' '{ print $2}') - elif [[ "$DNS_CHECK_FUNC" =~ ^drill ]] || [[ "$DNS_CHECK_FUNC" =~ ^dig ]]; then - check_result=$($DNS_CHECK_FUNC "_acme-challenge.${d}" TXT "@${ns}" \ - | grep ^_acme|awk -F'"' '{ print $2}') - elif [[ "$DNS_CHECK_FUNC" =~ ^host ]]; then - check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${d}" "${ns}" \ - | grep ^_acme|awk -F'"' '{ print $2}') - else - check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ - | grep ^_acme|awk -F'"' '{ print $2}') - fi - debug "expecting $auth_key" - debug "${ns} gave ... $check_result" - - if [[ "$check_result" == *"$auth_key"* ]]; then - check_dns="success" - else - if [[ $ntries -lt 100 ]]; then - ntries=$(( ntries + 1 )) - info "checking DNS at ${ns} for ${d}. Attempt $ntries/100 gave wrong result, "\ - "waiting $DNS_WAIT secs before checking again" - sleep $DNS_WAIT - else - debug "dns check failed - removing existing value" - error_exit "checking _acme-challenge.${d} gave $check_result not $auth_key" - fi - fi - done - done - fi - 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 - - # loop through dns-variable files to let the ACME server check the challenges - for dnsfile in $TEMP_DIR/dns_verify/*; do - if [[ -e "$dnsfile" ]]; then - debug "loading DNSfile: $dnsfile" - # shellcheck source=/dev/null - . "$dnsfile" - - check_challenge_completion "$uri" "$d" "$keyauthorization" - debug "remove DNS entry" - eval "$DNS_DEL_COMMAND" "$d" "$auth_key" - # remove $dnsfile after each loop. - rm -f "$dnsfile" - fi - done +if [[ $API -eq 2 ]]; then + create_order fi -# end of ... perform validation if via DNS challenge -#end of varify each domain. -# Verification has been completed for all SANS, so request certificate. +fulfill_challenges + +# Verification has been completed for all SANS, so request certificate. info "Verification completed, obtaining certificate." #obtain the certificate. get_certificate "$DOMAIN_DIR/${DOMAIN}.csr" \ "$CERT_FILE" \ - "$CA_CERT" + "$CA_CERT" \ + "$FULL_CHAIN" if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + info "Creating order for EC certificate" + if [[ $API -eq 2 ]]; then + create_order + fulfill_challenges + fi + info "obtaining EC certificate." get_certificate "$DOMAIN_DIR/${DOMAIN}.ec.csr" \ - "${CERT_FILE::-4}.ec.crt" \ - "${CA_CERT::-4}.ec.crt" + "${CERT_FILE%.*}.ec.crt" \ + "${CA_CERT%.*}.ec.crt" \ + "${FULL_CHAIN%.*}.ec.crt" fi # create Archive of new certs and keys. @@ -2115,75 +3124,7 @@ cert_archive debug "Certificates obtained and archived locally, will now copy to specified locations" # copy certs to the correct location (creating concatenated files as required) - -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 [[ ! -z "$DOMAIN_CERT_LOCATION" ]]; then - copy_file_to_location "ec domain certificate" \ - "${CERT_FILE::-4}.ec.crt" \ - "${DOMAIN_CERT_LOCATION::-4}.ec.crt" - fi - if [[ ! -z "$DOMAIN_KEY_LOCATION" ]]; then - copy_file_to_location "ec private key" \ - "$DOMAIN_DIR/${DOMAIN}.ec.key" \ - "${DOMAIN_KEY_LOCATION::-4}.ec.key" - fi - if [[ ! -z "$CA_CERT_LOCATION" ]]; then - copy_file_to_location "ec CA certificate" \ - "${CA_CERT::-4}.ec.crt" \ - "${CA_CERT_LOCATION::-4}.ec.crt" - fi -fi - -# if DOMAIN_CHAIN_LOCATION is not blank, then create and copy file. -if [[ ! -z "$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 "$CERT_FILE" "$CA_CERT" > "$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::-4}.ec.crt" "${CA_CERT::-4}.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 [[ ! -z "$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 - umask 077 - 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::-4}.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 - umask "$ORIG_UMASK" -fi -# if DOMAIN_PEM_LOCATION is not blank, then create and copy file. -if [[ ! -z "$DOMAIN_PEM_LOCATION" ]]; then - if [[ "$(dirname "$DOMAIN_PEM_LOCATION")" == "." ]]; then - to_location="${DOMAIN_DIR}/${DOMAIN_PEM_LOCATION}" - else - to_location="${DOMAIN_PEM_LOCATION}" - fi - umask 077 - 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::-4}.ec.crt" "${CA_CERT::-4}.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 - umask "$ORIG_UMASK" -fi -# end of copying certs. +cert_install # Run reload command to restart apache / nginx or whatever system reload_service @@ -2192,8 +3133,8 @@ reload_service if [[ "$DEACTIVATE_AUTH" == "true" ]]; then debug "in deactivate list is $deactivate_url_list" for deactivate_url in $deactivate_url_list; do - resp=$(curl "$deactivate_url" 2>/dev/null) - d=$(json_get "$resp" "hostname") + send_signed_request "$deactivate_url" "" + d=$(json_get "$response" "hostname") info "deactivating domain $d" debug "deactivating $deactivate_url" send_signed_request "$deactivate_url" "{\"resource\": \"authz\", \"status\": \"deactivated\"}" @@ -2209,17 +3150,46 @@ fi # Check if the certificate is installed correctly if [[ ${CHECK_REMOTE} == "true" ]]; then + real_d=${DOMAIN##\*.} sleep "$CHECK_REMOTE_WAIT" - # shellcheck disable=SC2086 - CERT_REMOTE=$(echo \ - | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 2>/dev/null \ - | openssl x509 -noout -fingerprint 2>/dev/null) - CERT_LOCAL=$(openssl x509 -noout -fingerprint < "$CERT_FILE" 2>/dev/null) - if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then - info "${DOMAIN} - certificate installed OK on server" + if [[ "$DUAL_RSA_ECDSA" == "true" ]]; then + # shellcheck disable=SC2086 + # check if openssl supports RSA-PSS + if [[ $(echo | openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} -sigalgs RSA-PSS+SHA256 2>/dev/null) ]]; then + PARAMS=("-sigalgs RSA-PSS+SHA256:RSA-PSS+SHA512:RSA+SHA256:RSA+SHA384:RSA+SHA512" "-sigalgs ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512") + else + PARAMS=("-sigalgs RSA+SHA256:RSA+SHA384:RSA+SHA512" "-sigalgs ECDSA+SHA256:ECDSA+SHA384:ECDSA+SHA512") + fi + + CERTS=("$CERT_FILE" "${CERT_FILE%.*}.ec.crt") + TYPES=("rsa" "$PRIVATE_KEY_ALG") else - error_exit "${DOMAIN} - certificate obtained but certificate on server is different from the new certificate" - fi + PARAMS=("") + CERTS=("$CERT_FILE") + TYPES=("$PRIVATE_KEY_ALG") + fi + + for ((i=0; i<${#PARAMS[@]};++i)); do + debug "Checking ${CERTS[i]}" + # shellcheck disable=SC2086 + debug openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${PARAMS[i]} + # shellcheck disable=SC2086 + CERT_REMOTE=$(echo \ + | openssl s_client -servername "${real_d}" -connect "${real_d}:${REMOTE_PORT}" ${REMOTE_EXTRA} ${PARAMS[i]} 2>/dev/null \ + | openssl x509 -noout -fingerprint 2>/dev/null) + CERT_LOCAL=$(openssl x509 -noout -fingerprint < "${CERTS[i]}" 2>/dev/null) + debug CERT_LOCAL="${CERT_LOCAL}" + debug CERT_REMOTE="${CERT_REMOTE}" + if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then + info "${real_d} - ${TYPES[i]} certificate installed OK on server" + elif [[ "$CERT_REMOTE" == "" ]]; then + info "${CERTS[i]} not returned by server" + error_exit "${real_d} - ${TYPES[i]} certificate obtained but not installed on server" + else + info "${CERTS[i]} didn't match server" + error_exit "${real_d} - ${TYPES[i]} certificate obtained but certificate on server is different from the new certificate" + fi + done fi # end of Check if the certificate is installed correctly diff --git a/other_scripts/cpanel_cert_upload b/other_scripts/cpanel_cert_upload index a76181b..757504f 100755 --- a/other_scripts/cpanel_cert_upload +++ b/other_scripts/cpanel_cert_upload @@ -14,12 +14,12 @@ rawurlencode() { local pos c o for (( pos=0 ; pos ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + + create_certificate + assert_success + check_output_for_errors +} + + +@test "Check we can revoke a certificate (no suffix)" { + if [ -n "$STAGING" ]; then + CONFIG_FILE="getssl-dns01.cfg" + else + CONFIG_FILE="getssl-http01.cfg" + fi + echo 'CA="https://acme-staging-v02.api.letsencrypt.org"' > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + + . "${CODE_DIR}/test/test-config/${CONFIG_FILE}" + CERT=${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt + KEY=${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.key + + run ${CODE_DIR}/getssl -d --revoke $CERT $KEY $CA + assert_success + check_output_for_errors "debug" +} diff --git a/test/16-test-bad-acl.bats b/test/16-test-bad-acl.bats new file mode 100644 index 0000000..d953951 --- /dev/null +++ b/test/16-test-bad-acl.bats @@ -0,0 +1,23 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +} + + +@test "Test behaviour if ACL= line has a space" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01-bad-acl.cfg" + setup_environment + init_getssl + create_certificate + assert_failure +} diff --git a/test/17-test-spaces-in-sans-dns01.bats b/test/17-test-spaces-in-sans-dns01.bats new file mode 100644 index 0000000..9d425af --- /dev/null +++ b/test/17-test-spaces-in-sans-dns01.bats @@ -0,0 +1,87 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Test behaviour if SANS line is space separated instead of comma separated (dns01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-dns01-spaces-sans.cfg" + setup_environment + + # Add hosts to DNS (also need to be added as aliases in docker-compose.yml) + for prefix in a b c; do + curl --silent -X POST -d '{"host":"'$prefix.$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + done + + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Test renewal if SANS line is space separated instead of comma separated (dns01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors + cleanup_environment +} + + +@test "Test behaviour if SANS line is space separated and IGNORE_DIRECTORY_DOMAIN (dns01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-dns01-spaces-sans-and-ignore-dir-domain.cfg" + setup_environment + + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Test renewal if SANS line is space separated and IGNORE_DIRECTORY_DOMAIN (dns01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors + cleanup_environment +} + + +@test "Test behaviour if SANS line is comma and space separated (dns01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-dns01-spaces-and-commas-sans.cfg" + setup_environment + + init_getssl + create_certificate + assert_success + check_output_for_errors + cleanup_environment + + for prefix in a b c; do + curl --silent -X POST -d '{"host":"'$prefix.$GETSSL_HOST'"}' http://10.30.50.3:8055/clear-a + done +} diff --git a/test/17-test-spaces-in-sans-http01.bats b/test/17-test-spaces-in-sans-http01.bats new file mode 100644 index 0000000..1730e99 --- /dev/null +++ b/test/17-test-spaces-in-sans-http01.bats @@ -0,0 +1,87 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Test behaviour if SANS line is space separated instead of comma separated (http01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01-spaces-sans.cfg" + setup_environment + + # Add hosts to DNS (also need to be added as aliases in docker-compose.yml) + for prefix in a b c; do + curl --silent -X POST -d '{"host":"'$prefix.$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + done + + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Test renewal if SANS line is space separated instead of comma separated (http01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors + cleanup_environment +} + + +@test "Test behaviour if SANS line is space separated and IGNORE_DIRECTORY_DOMAIN (http01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01-spaces-sans-and-ignore-dir-domain.cfg" + setup_environment + + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Test renewal if SANS line is space separated and IGNORE_DIRECTORY_DOMAIN (http01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors + cleanup_environment +} + + +@test "Test behaviour if SANS line is comma and space separated (http01)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01-spaces-and-commas-sans.cfg" + setup_environment + + init_getssl + create_certificate + assert_success + check_output_for_errors + cleanup_environment + + for prefix in a b c; do + curl --silent -X POST -d '{"host":"'$prefix.$GETSSL_HOST'"}' http://10.30.50.3:8055/clear-a + done +} diff --git a/test/18-retry-dns-add.bats b/test/18-retry-dns-add.bats new file mode 100644 index 0000000..25318de --- /dev/null +++ b/test/18-retry-dns-add.bats @@ -0,0 +1,38 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +} + + +@test "Check retry add dns command if dns isn't updated" { + if [ -n "$STAGING" ]; then + skip "Running internal tests, skipping external test" + fi + + CONFIG_FILE="getssl-dns01.cfg" + + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +DNS_ADD_COMMAND="/getssl/test/dns_add_fail" + +# Speed up the test by reducing the number or retries and the wait between retries. +DNS_WAIT=2 +DNS_WAIT_COUNT=11 +DNS_EXTRA_WAIT=0 +CHECK_ALL_AUTH_DNS="false" +CHECK_PUBLIC_DNS_SERVER="false" +DNS_WAIT_RETRY_ADD="true" +_RUNNING_TEST=1 +EOF + create_certificate + assert_failure + assert_line --partial "Retrying adding DNS via command" +} diff --git a/test/19-test-add-to-sans.bats b/test/19-test-add-to-sans.bats new file mode 100644 index 0000000..da9deb7 --- /dev/null +++ b/test/19-test-add-to-sans.bats @@ -0,0 +1,64 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + curl --silent -X POST -d '{"host":"a.'$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + curl --silent -X POST -d '{"host":"b.'$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + fi +} + +teardown() { + if [ -z "$STAGING" ]; then + curl --silent -X POST -d '{"host":"a.'$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/clear-a + curl --silent -X POST -d '{"host":"b.'$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/clear-a + fi +} + + +@test "Create certificate to check can add to SANS" { + skip "FIXME: Certificate is not recreated when SANS is updated" + if [ -n "$STAGING" ]; then + skip "Not trying on staging server yet" + CONFIG_FILE="getssl-dns01.cfg" + else + CONFIG_FILE="getssl-dns01-add-to-sans-1.cfg" + fi + . "${CODE_DIR}/test/test-config/${CONFIG_FILE}" + setup_environment + + + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Check we can add a new domain to SANS" { + skip "FIXME: Certificate is not recreated when SANS is updated" + if [ -n "$STAGING" ]; then + skip "Not trying on staging server yet" + CONFIG_FILE="getssl-dns01.cfg" + else + CONFIG_FILE="getssl-dns01-add-to-sans-2.cfg" + fi + # . "${CODE_DIR}/test/test-config/${CONFIG_FILE}" + # CERT=${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt + # KEY=${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.key + # cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl.cfg" + + create_certificate + assert_success + check_output_for_errors + + # As the SANS list changed, a new certificate is needed + assert_line --partial "certificate installed OK on server" + refute_line --partial 'certificate is valid for more than' +} diff --git a/test/2-simple-dns01-dig.bats b/test/2-simple-dns01-dig.bats new file mode 100644 index 0000000..f8f0cb4 --- /dev/null +++ b/test/2-simple-dns01-dig.bats @@ -0,0 +1,49 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi + if [ -f /usr/bin/host ]; then + mv /usr/bin/host /usr/bin/host.getssl.bak + fi + if [ -f /usr/bin/nslookup ]; then + mv /usr/bin/nslookup /usr/bin/nslookup.getssl.bak + fi +} + + +teardown() { + if [ -f /usr/bin/host.getssl.bak ]; then + mv /usr/bin/host.getssl.bak /usr/bin/host + fi + if [ -f /usr/bin/nslookup.getssl.bak ]; then + mv /usr/bin/nslookup.getssl.bak /usr/bin/nslookup + fi +} + + +@test "Create new certificate using DNS-01 verification (dig)" { + CONFIG_FILE="getssl-dns01.cfg" + + setup_environment + init_getssl + create_certificate -d + assert_success + assert_output --partial "dig" + check_output_for_errors "debug" +} + + +@test "Force renewal of certificate using DNS-01 (dig)" { + run ${CODE_DIR}/getssl -d -f $GETSSL_HOST + assert_success + assert_output --partial "dig" + check_output_for_errors "debug" + cleanup_environment +} diff --git a/test/2-simple-dns01-nslookup.bats b/test/2-simple-dns01-nslookup.bats new file mode 100644 index 0000000..f97b5dc --- /dev/null +++ b/test/2-simple-dns01-nslookup.bats @@ -0,0 +1,41 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi + if [ -f /usr/bin/dig ]; then + mv /usr/bin/dig /usr/bin/dig.getssl.bak + fi + if [ -f /usr/bin/host ]; then + mv /usr/bin/host /usr/bin/host.getssl.bak + fi +} + + +teardown() { + if [ -f /usr/bin/dig.getssl.bak ]; then + mv /usr/bin/dig.getssl.bak /usr/bin/dig + fi + if [ -f /usr/bin/host.getssl.bak ]; then + mv /usr/bin/host.getssl.bak /usr/bin/host + fi +} + + +@test "Create new certificate using DNS-01 verification (nslookup)" { + CONFIG_FILE="getssl-dns01.cfg" + + setup_environment + init_getssl + create_certificate -d + assert_success + assert_output --partial "nslookup" + check_output_for_errors "debug" +} diff --git a/test/20-wildcard-simple.bats b/test/20-wildcard-simple.bats new file mode 100644 index 0000000..b56a876 --- /dev/null +++ b/test/20-wildcard-simple.bats @@ -0,0 +1,64 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Create wildcard certificate" { + CONFIG_FILE="getssl-dns01.cfg" + + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Check CHECK_REMOTE works for wildcard certificates" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + run ${CODE_DIR}/getssl "*.$GETSSL_HOST" + assert_success + assert_line --partial "certificate is valid for more than" + check_output_for_errors +} + + +@test "Force renewal of wildcard certificate" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + run ${CODE_DIR}/getssl -f "*.$GETSSL_HOST" + assert_success + refute_line --partial "certificate is valid for more than" + check_output_for_errors +} + + +@test "Check renewal of near-expiration wildcard certificate" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + echo "RENEW_ALLOW=2000" >> "${INSTALL_DIR}/.getssl/*.${GETSSL_HOST}/getssl.cfg" + + run ${CODE_DIR}/getssl "*.$GETSSL_HOST" + assert_success + refute_line --partial "certificate is valid for more than" + check_output_for_errors + cleanup_environment +} diff --git a/test/21-wildcard-dual-rsa.bats b/test/21-wildcard-dual-rsa.bats new file mode 100644 index 0000000..550bbe0 --- /dev/null +++ b/test/21-wildcard-dual-rsa.bats @@ -0,0 +1,74 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Create secp384r1 wildcard certificate" { + CONFIG_FILE="getssl-dns01.cfg" + + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +ACCOUNT_KEY_TYPE="secp384r1" +PRIVATE_KEY_ALG="secp384r1" +EOF + + create_certificate + assert_success + check_output_for_errors + run openssl x509 -noout -text -in "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt" + assert_line --partial "Public Key Algorithm: id-ecPublicKey" + cleanup_environment +} + + +@test "Create dual certificates using DNS-01 verification" { + CONFIG_FILE="getssl-dns01.cfg" + + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" +EOF + + check_nginx + if [ "$OLD_NGINX" = "false" ]; then + echo 'RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-dual-certs ${NGINX_CONFIG} && /getssl/test/restart-nginx"' >> ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + else + echo 'CHECK_REMOTE="false"' >> ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + fi + + create_certificate + assert_success + check_output_for_errors + check_certificates + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/chain.ec.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.ec.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.ec.crt" ] + + run openssl x509 -noout -text -in "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt" + assert_line --partial "Public Key Algorithm: rsaEncryption" + + run openssl x509 -noout -text -in "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.ec.crt" + assert_line --partial "Public Key Algorithm: id-ecPublicKey" + + cleanup_environment +} diff --git a/test/22-wildcard-dual-rsa-ecdsa-copy-2-locations.bats b/test/22-wildcard-dual-rsa-ecdsa-copy-2-locations.bats new file mode 100644 index 0000000..b005af5 --- /dev/null +++ b/test/22-wildcard-dual-rsa-ecdsa-copy-2-locations.bats @@ -0,0 +1,61 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# These are run for every test, not once per file +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Create dual certificates (one wildcard) and copy RSA and ECDSA chain and key to two locations" { + CONFIG_FILE="getssl-dns01.cfg" + + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + + setup_environment + init_getssl + + cat <<- 'EOF' > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key;/root/a.${GETSSL_HOST}/server.key" +DOMAIN_CHAIN_LOCATION="/etc/nginx/pki/domain-chain.crt;/root/a.${GETSSL_HOST}/domain-chain.crt" # this is the domain cert and CA cert +EOF + + check_nginx + if [ "$OLD_NGINX" = "false" ]; then + echo 'RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-dual-certs ${NGINX_CONFIG} && /getssl/test/restart-nginx"' >> ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + else + echo 'CHECK_REMOTE="false"' >> ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + fi + + create_certificate + assert_success + check_output_for_errors + + if [ "$OLD_NGINX" = "false" ]; then + assert_line --partial "rsa certificate installed OK on server" + assert_line --partial "prime256v1 certificate installed OK on server" + fi + + # Check that the RSA chain and key have been copied to both locations + assert [ -e "/etc/nginx/pki/domain-chain.crt" ] + assert [ -e "/root/a.${GETSSL_HOST}/domain-chain.crt" ] + assert [ -e "/etc/nginx/pki/private/server.key" ] + assert [ -e "/root/a.${GETSSL_HOST}/server.key" ] + + # Check that the ECDSA chain and key have been copied to both locations + assert [ -e "/etc/nginx/pki/domain-chain.ec.crt" ] + assert [ -e "/root/a.${GETSSL_HOST}/domain-chain.ec.crt" ] + assert [ -e "/etc/nginx/pki/private/server.ec.key" ] + assert [ -e "/root/a.${GETSSL_HOST}/server.ec.key" ] + + cleanup_environment +} diff --git a/test/23-wildcard-check-globbing.bats b/test/23-wildcard-check-globbing.bats new file mode 100644 index 0000000..1c812b1 --- /dev/null +++ b/test/23-wildcard-check-globbing.bats @@ -0,0 +1,46 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Check for globbing for wildcard domains" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + else + CONFIG_FILE="getssl-dns01.cfg" + fi + + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + setup_environment + + init_getssl + + # Create a directory in /root which looks like a domain so that if glob expansion is performed a certificate for the wrong domain will be created + mkdir -p "${INSTALL_DIR}/a.${GETSSL_HOST}" + + create_certificate + assert_success + check_output_for_errors +} + + +@test "Force renewal of wildcard certificate" { + if [ -n "$STAGING" ]; then + skip "Not trying on staging server yet" + fi + + run ${CODE_DIR}/getssl -f "*.$GETSSL_HOST" + assert_success + refute_line --partial "certificate is valid for more than" + check_output_for_errors +} diff --git a/test/24-wildcard-sans.bats b/test/24-wildcard-sans.bats new file mode 100644 index 0000000..a9ca2a5 --- /dev/null +++ b/test/24-wildcard-sans.bats @@ -0,0 +1,64 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + curl --silent -X POST -d '{"host":"wild-'$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + fi +} + + +teardown() { + if [ -z "$STAGING" ]; then + curl --silent -X POST -d '{"host":"wild-'$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/clear-a + fi +} + + +@test "Check can create certificate for wildcard domain as arg and non-wildcard in SANS" { + CONFIG_FILE="getssl-dns01.cfg" + + # Staging server generates an error if try to create a certificate for *.domain and a.domain + # so create for *.wild-domain and a.domain instead + GETSSL_CMD_HOST="*.wild-${GETSSL_HOST}" + setup_environment + init_getssl + + echo 'SANS="${GETSSL_HOST}"' > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + if [ -n "$STAGING" ]; then + echo 'CHECK_REMOTE="false"' >> ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + fi + + create_certificate + assert_success + check_output_for_errors + run openssl x509 -noout -text -in "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt" + # verify certificate is for wildcard domain with non-wildcard domain in the Subject Alternative Name list + assert_output --regexp "Subject: CN[ ]?=[ ]?\*.wild-${GETSSL_HOST}" + assert_output --partial "DNS:${GETSSL_HOST}" +} + + +@test "Check can create certificate for non-wildcard domain as arg and wildcard in SANS" { + CONFIG_FILE="getssl-dns01.cfg" + + GETSSL_CMD_HOST="${GETSSL_HOST}" + setup_environment + init_getssl + + echo 'SANS="*.wild-${GETSSL_HOST}"' > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + + create_certificate + assert_success + check_output_for_errors + run openssl x509 -noout -text -in "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt" + # verify certificate is for non-wildcard domain with wildcard domain in the Subject Alternative Name list + assert_output --regexp "Subject: CN[ ]?=[ ]?${GETSSL_HOST}" + assert_output --partial "DNS:*.wild-${GETSSL_HOST}" +} diff --git a/test/25-wildcard-all.bats b/test/25-wildcard-all.bats new file mode 100644 index 0000000..8e428ab --- /dev/null +++ b/test/25-wildcard-all.bats @@ -0,0 +1,42 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Check can create certificate for wildcard domain using --all" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + else + CONFIG_FILE="getssl-dns01.cfg" + fi + + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + setup_environment + # Create .getssl directory and .getssl/*.{host} directory + init_getssl + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/*.${GETSSL_HOST}/getssl.cfg" + + # create another domain in the .getssl directory + run ${CODE_DIR}/getssl -c "a.${GETSSL_HOST}" + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/a.${GETSSL_HOST}/getssl.cfg" + + # Create a directory in /root which looks like a domain so that if glob expansion is performed the wildcard certificate won't be created + mkdir -p "${INSTALL_DIR}/a.${GETSSL_HOST}" + + run ${CODE_DIR}/getssl --all + + assert_success + assert_line --partial "Certificate saved in /root/.getssl/*.${GETSSL_HOST}/*.${GETSSL_HOST}" + assert_line --partial "Certificate saved in /root/.getssl/a.${GETSSL_HOST}/a.${GETSSL_HOST}" + check_output_for_errors +} diff --git a/test/26-wildcard-revoke.bats b/test/26-wildcard-revoke.bats new file mode 100644 index 0000000..37f8f10 --- /dev/null +++ b/test/26-wildcard-revoke.bats @@ -0,0 +1,41 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Create certificate to check wildcard revoke" { + CONFIG_FILE="getssl-dns01.cfg" + + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Check we can revoke a wildcard certificate" { + CONFIG_FILE="getssl-dns01.cfg" + . "${CODE_DIR}/test/test-config/${CONFIG_FILE}" + + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + + CERT=${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt + KEY=${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.key + + run ${CODE_DIR}/getssl -d --revoke $CERT $KEY $CA + assert_line "certificate revoked" + assert_success + check_output_for_errors "debug" +} diff --git a/test/27-wildcard-existing-cert.bats b/test/27-wildcard-existing-cert.bats new file mode 100644 index 0000000..76f2aa7 --- /dev/null +++ b/test/27-wildcard-existing-cert.bats @@ -0,0 +1,45 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Check that new creating a new configuration files uses details from existing certificate" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + else + CONFIG_FILE="getssl-dns01.cfg" + fi + + # Create and install certificate for wildcard + another domain + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + setup_environment + init_getssl + + echo 'SANS="a.${GETSSL_HOST}"' > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg + + create_certificate + assert_success + check_output_for_errors + + # Delete configuration + rm -r ${INSTALL_DIR}/.getssl + + # Create configuration + run ${CODE_DIR}/getssl -c "${GETSSL_CMD_HOST}" + + # Assert that the newly created configuration contains the additional domain in SANS + # if this fails then error in tests will be "grep failed" - this means SANS did not hold the expected value + # eg SANS="a.centos7.getssl.test" + grep -q "SANS=\"a.${GETSSL_HOST}\"" ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl.cfg + assert_success +} diff --git a/test/28-wildcard-error-http01-validation.bats b/test/28-wildcard-error-http01-validation.bats new file mode 100644 index 0000000..dbb64b7 --- /dev/null +++ b/test/28-wildcard-error-http01-validation.bats @@ -0,0 +1,31 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Check that trying to create a wildcard certificate using http-01 validation shows an error message" { + if [ -n "$STAGING" ]; then + skip "Internal test, no need to test on staging server" + else + CONFIG_FILE="getssl-http01.cfg" + fi + + # Try and create a wildcard certificate using http-01 validation + GETSSL_CMD_HOST="*.${GETSSL_HOST}" + setup_environment + init_getssl + + create_certificate + assert_failure + assert_line --partial "cannot use http-01 validation for wildcard domains" +} diff --git a/test/29-check-mktemp-failure.bats b/test/29-check-mktemp-failure.bats new file mode 100644 index 0000000..782dc64 --- /dev/null +++ b/test/29-check-mktemp-failure.bats @@ -0,0 +1,47 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Check that getssl -c fails with an error message if mktemp fails" { + if [ -n "$STAGING" ]; then + skip "Internal test, no need to test on staging server" + else + CONFIG_FILE="getssl-http01.cfg" + fi + + # set TMPDIR to an invalid directory and check for failure + export TMPDIR=/getssl.invalid.directory + setup_environment + run ${CODE_DIR}/getssl -c "$GETSSL_CMD_HOST" + assert_failure + assert_line --partial "mktemp failed" +} + + +@test "Check that getssl fails with an error message if mktemp fails" { + if [ -n "$STAGING" ]; then + skip "Internal test, no need to test on staging server" + else + CONFIG_FILE="getssl-http01.cfg" + fi + + setup_environment + init_getssl + + # set TMPDIR to an invalid directory and check for failure + export TMPDIR=/getssl.invalid.directory + create_certificate + assert_failure + assert_line --partial "mktemp failed" +} diff --git a/test/3-dual-rsa-ecdsa.bats b/test/3-dual-rsa-ecdsa.bats new file mode 100644 index 0000000..87527c2 --- /dev/null +++ b/test/3-dual-rsa-ecdsa.bats @@ -0,0 +1,96 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +} + + +@test "Create dual certificates using HTTP-01 verification" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + check_nginx + if [ "$OLD_NGINX" = "false" ]; then + CONFIG_FILE="getssl-http01-dual-rsa-ecdsa.cfg" + else + CONFIG_FILE="getssl-http01-dual-rsa-ecdsa-old-nginx.cfg" + fi + + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors + check_certificates + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/chain.ec.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.ec.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.ec.crt" ] +} + + +@test "Check renewal test works for dual certificates using HTTP-01" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + check_nginx + run ${CODE_DIR}/getssl -d $GETSSL_HOST + + if [ "$OLD_NGINX" = "false" ]; then + assert_line "certificate on server is same as the local cert" + else + assert_line --partial "certificate is valid for more than 30 days" + fi + assert_success +} + + +@test "Force renewal of dual certificates using HTTP-01" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors +} + + +@test "Create dual certificates using DNS-01 verification" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + check_nginx + if [ "$OLD_NGINX" = "false" ]; then + CONFIG_FILE="getssl-dns01-dual-rsa-ecdsa.cfg" + else + CONFIG_FILE="getssl-dns01-dual-rsa-ecdsa-old-nginx.cfg" + fi + + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors + check_certificates + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/chain.ec.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.ec.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.ec.crt" ] +} + + +@test "Force renewal of dual certificates using DNS-01" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors + cleanup_environment +} diff --git a/test/30-handle-dig-failure.bats b/test/30-handle-dig-failure.bats new file mode 100644 index 0000000..1102745 --- /dev/null +++ b/test/30-handle-dig-failure.bats @@ -0,0 +1,44 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + if [ -f /usr/bin/drill ]; then + mv /usr/bin/drill /usr/bin/drill.getssl.bak + fi + if [ -f /usr/bin/dig ]; then + chmod -x /usr/bin/dig + fi +} + + +teardown() { + if [ -f /usr/bin/drill.getssl.bak ]; then + mv /usr/bin/drill.getssl.bak /usr/bin/drill + fi + if [ -f /usr/bin/dig ]; then + chmod +x /usr/bin/dig + fi +} + + +@test "Test that if dig exists but errors HAS_DIG is not set" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + if [ ! -f /usr/bin/dig ]; then + skip "dig not installed, skipping dig test" + fi + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + create_certificate -d + assert_success + refute_line --partial "HAS DIG_OR_DRILL=dig" + check_output_for_errors "debug" +} diff --git a/test/31-test-posix-error.bats b/test/31-test-posix-error.bats new file mode 100644 index 0000000..e9950fd --- /dev/null +++ b/test/31-test-posix-error.bats @@ -0,0 +1,25 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +} + + +@test "Test that running in POSIX mode shows an error" { + # v2.31 uses read to create an array in the get_auth_dns function which causes a parse error in posix mode + # Could be re-written to not use this functionality if it causes for required. + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + run bash --posix "${CODE_DIR}/getssl" + assert_failure + assert_line "getssl: Running with POSIX mode enabled is not supported" + check_output_for_errors +} diff --git a/test/32-test-upgrade.bats b/test/32-test-upgrade.bats new file mode 100644 index 0000000..f8fc68a --- /dev/null +++ b/test/32-test-upgrade.bats @@ -0,0 +1,111 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + + # Turn off warning about detached head + git config --global advice.detachedHead false + run git clone https://github.com/srvrco/getssl.git "$INSTALL_DIR/upgrade-getssl" + + # Don't do version arithmetics any longer, look what was the previous version by getting the last + # line (starting with v) and the one before that from the list of tags. + cd "$INSTALL_DIR/upgrade-getssl" + + # This sets CURRENT_TAG and PREVIOUS_TAG bash variables + eval $(git tag -l | awk 'BEGIN {cur="?.??"};/^v/{prv=cur;cur=substr($1,2)};END{ printf("CURRENT_TAG=\"%s\";PREVIOUS_TAG=\"%s\"\n",cur,prv)}') + + # The version in the file, which we will overwrite + FILE_VERSION=$(awk -F'"' '/^VERSION=/{print $2}' "$CODE_DIR/getssl") + # If FILE_VERSION > CURRENT_TAG then either we are testing a push to master or the last version wasn't released +} + + +teardown() { + rm -r "$INSTALL_DIR/upgrade-getssl" +} + + +@test "Test that we are told that a newer version is available" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + cd "$INSTALL_DIR/upgrade-getssl" + git checkout tags/v${PREVIOUS_TAG} + + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl.cfg" + + # Overwrite checked out getssl-script with copy of new one, but write the previous version into the copy + # Note that this way we mock downgrading getssl and are testing the upgrading of the version in development + cp "$CODE_DIR/getssl" "$INSTALL_DIR/upgrade-getssl/" + sed -i -e "s/VERSION=\"${FILE_VERSION}\"/VERSION=\"${PREVIOUS_TAG}\"/" "$INSTALL_DIR/upgrade-getssl/getssl" + + run "$INSTALL_DIR/upgrade-getssl/getssl" --check-config ${GETSSL_CMD_HOST} + assert_success + + # Check for current tag or file version otherwise push to master fails on a new version (or if the tag hasn't been updated) + assert_line --regexp "A more recent version \(v(${CURRENT_TAG}|${FILE_VERSION})\) of getssl is available, please update" + check_output_for_errors +} + + +@test "Test that we can upgrade to the newer version" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + cd "$INSTALL_DIR/upgrade-getssl" + git checkout tags/v${CURRENT_TAG} + + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl.cfg" + + # Overwrite checked out getssl-script with copy of new one, but write the previous version into the copy + # Note that this way we mock downgrading getssl and are testing the upgrading of the version in development + cp "$CODE_DIR/getssl" "$INSTALL_DIR/upgrade-getssl/" + sed -i -e "s/VERSION=\"${FILE_VERSION}\"/VERSION=\"${PREVIOUS_TAG}\"/" "$INSTALL_DIR/upgrade-getssl/getssl" + + run "$INSTALL_DIR/upgrade-getssl/getssl" --check-config --upgrade ${GETSSL_CMD_HOST} + assert_success + + # Check for current tag or file version otherwise push to master fails on a new version (or if the tag hasn't been updated) + assert_line --regexp "Updated getssl from v${PREVIOUS_TAG} to v(${CURRENT_TAG}|${FILE_VERSION})" +} + + +@test "Test that we can upgrade to the newer version when invoking as \"bash ./getssl\"" { + # Note that `bash getssl` will fail if the CWD isn't in the PATH and an upgrade occurs + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + cd "$INSTALL_DIR/upgrade-getssl" + git checkout tags/v${PREVIOUS_TAG} + + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl.cfg" + + # Overwrite checked out getssl-script with copy of new one, but write the previous version into the copy + # Note that this way we mock downgrading getssl and are testing the upgrading of the version in development + cp "$CODE_DIR/getssl" "$INSTALL_DIR/upgrade-getssl/" + sed -i -e "s/VERSION=\"${FILE_VERSION}\"/VERSION=\"${PREVIOUS_TAG}\"/" "$INSTALL_DIR/upgrade-getssl/getssl" + + run bash ./getssl --check-config --upgrade ${GETSSL_CMD_HOST} + assert_success + + # Check for current tag or file version otherwise push to master fails on a new version (or if the tag hasn't been updated) + assert_line --regexp "Updated getssl from v${PREVIOUS_TAG} to v(${CURRENT_TAG}|${FILE_VERSION})" +} diff --git a/test/33-ftp.bats b/test/33-ftp.bats new file mode 100644 index 0000000..9c2e387 --- /dev/null +++ b/test/33-ftp.bats @@ -0,0 +1,71 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + if [ -n "${VSFTPD_CONF}" ]; then + cp $VSFTPD_CONF ${VSFTPD_CONF}.getssl + + # enable passive and disable active mode + # https://www.pixelstech.net/article/1364817664-FTP-active-mode-and-passive-mode + cat <<- _FTP >> $VSFTPD_CONF +pasv_enable=NO +_FTP + + ${CODE_DIR}/test/restart-ftpd + fi +} + + +teardown() { + if [ -n "${VSFTPD_CONF}" ]; then + cp ${VSFTPD_CONF}.getssl $VSFTPD_CONF + ${CODE_DIR}/test/restart-ftpd + fi +} + + +@test "Use FTP to create challenge file" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + if [[ ! -d /var/www/html/.well-known/acme-challenge ]]; then + mkdir -p /var/www/html/.well-known/acme-challenge + fi + + # Always change ownership and permissions in case previous tests created the directories as root + chgrp -R www-data /var/www/html/.well-known + chmod -R g+w /var/www/html/.well-known + + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +ACL="ftp:ftpuser:ftpuser:${GETSSL_CMD_HOST}:/var/www/html/.well-known/acme-challenge" +EOF + + if [[ "$GETSSL_OS" = "alpine" ]]; then + cat <<- EOF2 >> ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +FTP_OPTIONS="set ftp:passive-mode off" +EOF2 + elif [[ "$FTP_PASSIVE_DEFAULT" == "true" ]]; then + cat <<- EOF3 >> ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +FTP_OPTIONS="passive" +EOF3 + fi + + create_certificate + assert_success + assert_line --partial "ftp:ftpuser:ftpuser:" + if [[ "$GETSSL_OS" != "alpine" ]] && [[ "$FTP_PASSIVE_DEFAULT" == "true" ]]; then + assert_line --partial "Passive mode off" + fi + check_output_for_errors +} diff --git a/test/34-ftp-passive.bats b/test/34-ftp-passive.bats new file mode 100644 index 0000000..0d16574 --- /dev/null +++ b/test/34-ftp-passive.bats @@ -0,0 +1,71 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + if [ -n "${VSFTPD_CONF}" ]; then + cp $VSFTPD_CONF ${VSFTPD_CONF}.getssl + + # enable passive and disable active mode + # https://www.pixelstech.net/article/1364817664-FTP-active-mode-and-passive-mode + cat <<- _FTP >> $VSFTPD_CONF +pasv_enable=YES +pasv_max_port=10100 +pasv_min_port=10090 +connect_from_port_20=NO +_FTP + + ${CODE_DIR}/test/restart-ftpd + fi +} + + +teardown() { + if [ -n "${VSFTPD_CONF}" ]; then + cp ${VSFTPD_CONF}.getssl $VSFTPD_CONF + ${CODE_DIR}/test/restart-ftpd + fi +} + + +@test "Use Passive FTP to create challenge file" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + if [[ ! -d /var/www/html/.well-known/acme-challenge ]]; then + mkdir -p /var/www/html/.well-known/acme-challenge + fi + + # Always change ownership and permissions in case previous tests created the directories as root + chgrp -R www-data /var/www/html/.well-known + chmod -R g+w /var/www/html/.well-known + + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +ACL="ftp:ftpuser:ftpuser:${GETSSL_CMD_HOST}:/var/www/html/.well-known/acme-challenge" +EOF + if [[ "$FTP_PASSIVE_DEFAULT" == "false" ]]; then + cat <<- EOF3 >> ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +FTP_OPTIONS="passive" +EOF3 + fi + + create_certificate + assert_success + assert_line --partial "ftp:ftpuser:ftpuser:" + if [[ "$FTP_PASSIVE_DEFAULT" == "false" ]]; then + assert_line --partial "Passive mode on" + else + refute_line --partial "Passive mode off" + fi + check_output_for_errors +} diff --git a/test/35-preferred-chain.bats b/test/35-preferred-chain.bats new file mode 100644 index 0000000..9c3fc6b --- /dev/null +++ b/test/35-preferred-chain.bats @@ -0,0 +1,111 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Use PREFERRED_CHAIN to select an alternate root" { + if [ -n "$STAGING" ]; then + PREFERRED_CHAIN="\(STAGING\) Pretend Pear X1" + CHECK_CHAIN="(STAGING) Pretend Pear X1" + else + PREFERRED_CHAIN=$(curl --silent https://pebble:15000/roots/2 | openssl x509 -text -noout | grep "Issuer:" | awk -F"CN *= *" '{ print $2 }') + PREFERRED_CHAIN="${PREFERRED_CHAIN# }" # remove leading whitespace + CHECK_CHAIN=$PREFERRED_CHAIN + fi + + CONFIG_FILE="getssl-dns01.cfg" + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +PREFERRED_CHAIN="${PREFERRED_CHAIN}" +EOF + + create_certificate + assert_success + check_output_for_errors + + issuer=$(openssl crl2pkcs7 -nocrl -certfile "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.crt" | openssl pkcs7 -print_certs -text -noout | grep Issuer: | tail -1 | awk -F"CN=" '{ print $2 }') + # verify certificate is issued by preferred chain root + if [[ "${CHECK_CHAIN}" != "$issuer" ]]; then + echo "# PREFERRED_CHAIN=$PREFERRED_CHAIN" + echo "# issuer=$issuer" + fi + + [ "${CHECK_CHAIN}" = "$issuer" ] +} + + +@test "Use PREFERRED_CHAIN to select the default root" { + if [ -n "$STAGING" ]; then + PREFERRED_CHAIN="\(STAGING\) Doctored Durian Root CA X3" + CHECK_CHAIN="(STAGING) Doctored Durian Root CA X3" + else + PREFERRED_CHAIN=$(curl --silent https://pebble:15000/roots/0 | openssl x509 -text -noout | grep Issuer: | awk -F"CN *= *" '{ print $2 }') + PREFERRED_CHAIN="${PREFERRED_CHAIN# }" # remove leading whitespace + CHECK_CHAIN=$PREFERRED_CHAIN + fi + + CONFIG_FILE="getssl-dns01.cfg" + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +PREFERRED_CHAIN="${PREFERRED_CHAIN}" +EOF + + create_certificate + assert_success + check_output_for_errors + + issuer=$(openssl crl2pkcs7 -nocrl -certfile "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.crt" | openssl pkcs7 -print_certs -text -noout | grep Issuer: | tail -1 | awk -F"CN=" '{ print $2 }') + # verify certificate is issued by preferred chain root + if [[ "${CHECK_CHAIN}" != "$issuer" ]]; then + echo "# PREFERRED_CHAIN=$PREFERRED_CHAIN" + echo "# issuer=$issuer" + fi + [ "${CHECK_CHAIN}" = "$issuer" ] +} + + +@test "Use PREFERRED_CHAIN to select an alternate root by suffix" { + if [ -n "$STAGING" ]; then + FULL_PREFERRED_CHAIN="(STAGING) Pretend Pear X1" + else + FULL_PREFERRED_CHAIN=$(curl --silent https://pebble:15000/roots/2 | openssl x509 -text -noout | grep "Issuer:" | awk -F"CN *= *" '{ print $2 }') + FULL_PREFERRED_CHAIN="${FULL_PREFERRED_CHAIN# }" # remove leading whitespace + fi + + # Take the last word from FULL_PREFERRED_CHAIN as the chain to use + PREFERRED_CHAIN="${FULL_PREFERRED_CHAIN##* }" + CONFIG_FILE="getssl-dns01.cfg" + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +PREFERRED_CHAIN="${PREFERRED_CHAIN}" +EOF + + create_certificate + assert_success + check_output_for_errors + + issuer=$(openssl crl2pkcs7 -nocrl -certfile "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.crt" | openssl pkcs7 -print_certs -text -noout | grep Issuer: | tail -1 | awk -F"CN=" '{ print $2 }') + # verify certificate is issued by preferred chain root + if [[ "${FULL_PREFERRED_CHAIN}" != "$issuer" ]]; then + echo "# PREFERRED_CHAIN=$PREFERRED_CHAIN" + echo "# FULL_PREFERRED_CHAIN=$FULL_PREFERRED_CHAIN" + echo "# issuer=$issuer" + fi + [ "${FULL_PREFERRED_CHAIN}" = "$issuer" ] +} diff --git a/test/36-full-chain-inc-root.bats b/test/36-full-chain-inc-root.bats new file mode 100644 index 0000000..5b29d0b --- /dev/null +++ b/test/36-full-chain-inc-root.bats @@ -0,0 +1,94 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + fi +} + + +@test "Use FULL_CHAIN_INCLUDE_ROOT to include the root certificate in the fullchain" { + CONFIG_FILE="getssl-dns01.cfg" + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +FULL_CHAIN_INCLUDE_ROOT="true" +EOF + + create_certificate + assert_success + check_output_for_errors + + if [ -n "$STAGING" ]; then + PREFERRED_CHAIN="(STAGING) Doctored Durian Root CA X3" + else + # pebble doesn't support CA Issuers so the fullchain.crt will just contain the certificate (code path means it won't contain the intermediate cert in this case) + # This is testing that requesting FULL_CHAIN_INCLUDE_ROOT doesn't fail if there is no CA Issuers in the certificate + PREFERRED_CHAIN=$(openssl crl2pkcs7 -nocrl -certfile "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt" | openssl pkcs7 -print_certs -text -noout | grep Subject: | tail -1 | awk -F"CN=" '{ print $2 }') + fi + + final_issuer=$(openssl crl2pkcs7 -nocrl -certfile "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.crt" | openssl pkcs7 -print_certs -text -noout | grep Subject: | tail -1 | awk -F"CN=" '{ print $2 }') + + # verify certificate includes the chain root + if [[ "${PREFERRED_CHAIN}" != "$final_issuer" ]]; then + echo "# PREFERRED_CHAIN=$PREFERRED_CHAIN" + echo "# final_issuer=$final_issuer" + fi + [ "${PREFERRED_CHAIN}" = "$final_issuer" ] +} + + +@test "Use FULL_CHAIN_INCLUDE_ROOT with dual certificates" { + if [ -n "$STAGING" ]; then + PREFERRED_CHAIN="(STAGING) Doctored Durian Root CA X3" + fi + + CONFIG_FILE="getssl-dns01.cfg" + setup_environment + init_getssl + + cat <<- EOF > ${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl_test_specific.cfg +FULL_CHAIN_INCLUDE_ROOT="true" +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" +CHECK_REMOTE="false" +EOF + + create_certificate + assert_success + check_output_for_errors + check_certificates + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/chain.ec.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.ec.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.ec.crt" ] + + if [ -n "$STAGING" ]; then + PREFERRED_CHAIN="(STAGING) Doctored Durian Root CA X3" + else + # pebble doesn't support CA Issuers so the fullchain.crt will just contain the certificate (code path means it won't contain the intermediate cert in this case) + # This is testing that requesting FULL_CHAIN_INCLUDE_ROOT doesn't fail if there is no CA Issuers in the certificate + PREFERRED_CHAIN=$(openssl crl2pkcs7 -nocrl -certfile "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt" | openssl pkcs7 -print_certs -text -noout | grep Subject: | tail -1 | awk -F"CN=" '{ print $2 }') + fi + + # verify both rsa and ecdsa certificates include the chain root + final_issuer=$(openssl crl2pkcs7 -nocrl -certfile "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.crt" | openssl pkcs7 -print_certs -text -noout | grep Subject: | tail -1 | awk -F"CN=" '{ print $2 }') + if [[ "${PREFERRED_CHAIN}" != "$final_issuer" ]]; then + echo "# PREFERRED_CHAIN=$PREFERRED_CHAIN" + echo "# final_issuer=$final_issuer" + fi + [ "${PREFERRED_CHAIN}" = "$final_issuer" ] + ecdsa_final_issuer=$(openssl crl2pkcs7 -nocrl -certfile "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.ec.crt" | openssl pkcs7 -print_certs -text -noout | grep Subject: | tail -1 | awk -F"CN=" '{ print $2 }') + if [[ "$PREFERRED_CHAIN" != "$ecdsa_final_issuer" ]]; then + echo "# PREFERRED_CHAIN=$PREFERRED_CHAIN" + echo "# ecdsa_final_issuer=$ecdsa_final_issuer" + fi + [ "${PREFERRED_CHAIN}" = "$ecdsa_final_issuer" ] +} diff --git a/test/4-more-than-10-hosts.bats b/test/4-more-than-10-hosts.bats new file mode 100644 index 0000000..f4eb95c --- /dev/null +++ b/test/4-more-than-10-hosts.bats @@ -0,0 +1,45 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +} + + +@test "Create certificates for more than 10 hosts using HTTP-01 verification" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01-10-hosts.cfg" + setup_environment + + # Add 11 hosts to DNS (also need to be added as aliases in docker-compose.yml) + for prefix in a b c d e f g h i j k; do + curl --silent -X POST -d '{"host":"'$prefix.$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + done + + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Force renewal of more than 10 certificates using HTTP-01" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors + # Remove all the dns aliases + cleanup_environment + for prefix in a b c d e f g h i j k; do + curl --silent -X POST -d '{"host":"'$prefix.$GETSSL_HOST'"}' http://10.30.50.3:8055/clear-a + done +} diff --git a/test/5-secp384-http01.bats b/test/5-secp384-http01.bats new file mode 100644 index 0000000..3d05159 --- /dev/null +++ b/test/5-secp384-http01.bats @@ -0,0 +1,57 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +} + + +@test "Create new secp384r1 certificate using HTTP-01 verification" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01-secp384.cfg" + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Force renewal of secp384r1 certificate using HTTP-01" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors +} + + +@test "Create new secp521r1 certificate using HTTP-01 verification" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01-secp521.cfg" + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Force renewal of secp521r1 certificate using HTTP-01" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors +} diff --git a/test/6-dual-rsa-ecdsa-copy-2-locations.bats b/test/6-dual-rsa-ecdsa-copy-2-locations.bats new file mode 100644 index 0000000..6f75f25 --- /dev/null +++ b/test/6-dual-rsa-ecdsa-copy-2-locations.bats @@ -0,0 +1,81 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# These are run for every test, not once per file +setup() { + if [ -z "$STAGING" ]; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + curl --silent -X POST -d '{"host":"'a.$GETSSL_HOST'", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + fi +} + + +teardown() { + if [ -z "$STAGING" ]; then + curl --silent -X POST -d '{"host":"'a.$GETSSL_HOST'"}' http://10.30.50.3:8055/clear-a + fi +} + + +@test "Create dual certificates and copy RSA and ECDSA chain and key to two locations" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + check_nginx + if [ "$OLD_NGINX" = "false" ]; then + CONFIG_FILE="getssl-http01-dual-rsa-ecdsa-2-locations.cfg" + else + CONFIG_FILE="getssl-http01-dual-rsa-ecdsa-2-locations-old-nginx.cfg" + fi + + setup_environment + mkdir -p /root/a.${GETSSL_HOST} + + init_getssl + create_certificate + assert_success + check_output_for_errors + if [ "$OLD_NGINX" = "false" ]; then + assert_line --partial "rsa certificate installed OK on server" + assert_line --partial "prime256v1 certificate installed OK on server" + fi + + # Check that the RSA chain and key have been copied to both locations + assert [ -e "/etc/nginx/pki/domain-chain.crt" ] + assert [ -e "/root/a.${GETSSL_HOST}/domain-chain.crt" ] + assert [ -e "/etc/nginx/pki/private/server.key" ] + assert [ -e "/root/a.${GETSSL_HOST}/server.key" ] + + # Check that the ECDSA chain and key have been copied to both locations + assert [ -e "/etc/nginx/pki/domain-chain.ec.crt" ] + assert [ -e "/root/a.${GETSSL_HOST}/domain-chain.ec.crt" ] + assert [ -e "/etc/nginx/pki/private/server.ec.key" ] + assert [ -e "/root/a.${GETSSL_HOST}/server.ec.key" ] +} + + +@test "Create dual certificates and copy to two locations but not returned by server" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + check_nginx + if [ "$OLD_NGINX" = "false" ]; then + CONFIG_FILE="getssl-http01-dual-rsa-ecdsa-2-locations-wrong-nginx.cfg" + else + skip "Skipping as old nginx servers cannot return both certificates" + fi + + setup_environment + mkdir -p /root/a.${GETSSL_HOST} + + init_getssl + create_certificate + assert_failure + assert_line --partial "prime256v1 certificate obtained but not installed on server" +} diff --git a/test/8-staging-ecdsa.bats b/test/8-staging-ecdsa.bats new file mode 100644 index 0000000..6aebd60 --- /dev/null +++ b/test/8-staging-ecdsa.bats @@ -0,0 +1,62 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + + + +@test "Create new certificate using staging server and prime256v1" { + if [ -z "$STAGING" ]; then + skip "Running external tests, skipping internal testing" + fi + CONFIG_FILE="getssl-dns01.cfg" + + setup_environment + init_getssl + sed -e 's/rsa/prime256v1/g' < "${CODE_DIR}/test/test-config/${CONFIG_FILE}" > "${INSTALL_DIR}/.getssl/${GETSSL_HOST}/getssl.cfg" + run ${CODE_DIR}/getssl -d "$GETSSL_HOST" + assert_success + check_output_for_errors "debug" +} + + +@test "Force renewal of certificate using staging server and prime256v1" { + if [ -z "$STAGING" ]; then + skip "Running internal tests, skipping external test" + fi + run ${CODE_DIR}/getssl -d -f $GETSSL_HOST + assert_success + check_output_for_errors "debug" + cleanup_environment +} + + +@test "Create new certificate using staging server and secp384r1" { + if [ -z "$STAGING" ]; then + skip "Running external tests, skipping internal testing" + fi + CONFIG_FILE="getssl-dns01.cfg" + + setup_environment + init_getssl + sed -e 's/rsa/secp384r1/g' < "${CODE_DIR}/test/test-config/${CONFIG_FILE}" > "${INSTALL_DIR}/.getssl/${GETSSL_HOST}/getssl.cfg" + run ${CODE_DIR}/getssl -d "$GETSSL_HOST" + assert_success + check_output_for_errors "debug" +} + + +@test "Force renewal of certificate using staging server and secp384r1" { + if [ -z "$STAGING" ]; then + skip "Running internal tests, skipping external test" + fi + run ${CODE_DIR}/getssl -d -f $GETSSL_HOST + assert_success + check_output_for_errors "debug" + cleanup_environment +} + + +# Note letsencrypt doesn't support ECDSA curve P-521 as it's being deprecated diff --git a/test/9-multiple-domains-dns01.bats b/test/9-multiple-domains-dns01.bats new file mode 100644 index 0000000..65eab09 --- /dev/null +++ b/test/9-multiple-domains-dns01.bats @@ -0,0 +1,60 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +} + + +@test "Create certificates for multi-level domains using DNS-01 verification" { + # This tests we can create a certificate for .getssl.test and getssl.test (in SANS) + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-dns01-multiple-domains.cfg" + setup_environment + + # Add top level domain from SANS to DNS + curl --silent -X POST -d '{"host":"getssl.test", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Force renewal of multi-level domains using DNS-01" { + # This tests we can renew a certificate for .getssl.test and getssl.test (in SANS) + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors + # Remove all the dns aliases + cleanup_environment + curl --silent -X POST -d '{"host":"getssl.tst"}' http://10.30.50.3:8055/clear-a +} + +@test "Test IGNORE_DIRECTORY_DOMAIN using DNS-01 verification" { + # This tests we can create a certificate for getssl.test and .getssl.test (*both* in SANS) + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-dns01-ignore-directory-domain.cfg" + setup_environment + + # Add top level domain from SANS to DNS + curl --silent -X POST -d '{"host":"getssl.test", "addresses":["'$GETSSL_IP'"]}' http://10.30.50.3:8055/add-a + + init_getssl + create_certificate + assert_success + check_output_for_errors +} diff --git a/test/9-test--all.bats b/test/9-test--all.bats new file mode 100644 index 0000000..cb043a5 --- /dev/null +++ b/test/9-test--all.bats @@ -0,0 +1,32 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + export PATH=$PATH:/getssl +} + + +@test "Create new certificate using --all" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + # Setup + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/${GETSSL_HOST}/getssl.cfg" + + # Run test + run ${CODE_DIR}/getssl --all + + # Check success conditions + assert_success + check_output_for_errors +} diff --git a/test/Dockerfile-alpine b/test/Dockerfile-alpine new file mode 100644 index 0000000..2c28f48 --- /dev/null +++ b/test/Dockerfile-alpine @@ -0,0 +1,33 @@ +FROM alpine:latest + +# Note this image uses busybox awk instead of gawk + +RUN apk --no-cache add supervisor openssl git curl bind-tools drill wget nginx bash lftp vsftpd openssh-server + +WORKDIR /root + +# Create nginx directories in standard places +RUN mkdir /run/nginx +RUN mkdir -p /etc/nginx/pki/private + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=true +COPY ./test/test-config/vsftpd.conf /etc/vsftpd.conf +RUN echo "seccomp_sandbox=NO" >> /etc/vsftpd.conf +RUN adduser -D ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser ftpuser www-data +RUN adduser root www-data +RUN chown -R ftpuser.www-data /var/www +RUN chmod g+w -R /var/www + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Use supervisord to run nginx in the background +COPY ./test/test-config/alpine-supervisord.conf /etc/supervisord.conf +CMD [ "tail", "-f", "/dev/null" ] diff --git a/test/Dockerfile-bash4-0 b/test/Dockerfile-bash4-0 new file mode 100644 index 0000000..9b1f790 --- /dev/null +++ b/test/Dockerfile-bash4-0 @@ -0,0 +1,34 @@ +FROM bash:4.0 + +# https://hub.docker.com/_/bash + +RUN apk --no-cache add supervisor openssl git curl bind-tools drill wget nginx lftp vsftpd openssh-server + +WORKDIR /root + +# Create nginx directories in standard places +RUN mkdir /run/nginx +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=true +COPY ./test/test-config/vsftpd.conf /etc/vsftpd.conf +RUN echo "seccomp_sandbox=NO" >> /etc/vsftpd.conf +RUN adduser -D ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser ftpuser www-data +RUN adduser root www-data +RUN chown -R ftpuser.www-data /var/www +RUN chmod g+w -R /var/www + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone https://github.com/bats-core/bats-support /bats-support +RUN git clone https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Use supervisord to run nginx in the background +COPY ./test/test-config/alpine-supervisord.conf /etc/supervisord.conf +CMD tail -f /dev/null diff --git a/test/Dockerfile-bash4-2 b/test/Dockerfile-bash4-2 new file mode 100644 index 0000000..d9386f6 --- /dev/null +++ b/test/Dockerfile-bash4-2 @@ -0,0 +1,34 @@ +FROM bash:4.2 + +# https://hub.docker.com/_/bash + +RUN apk --no-cache add supervisor openssl git curl bind-tools drill wget nginx lftp vsftpd openssh-server + +WORKDIR /root + +# Create nginx directories in standard places +RUN mkdir /run/nginx +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=true +COPY ./test/test-config/vsftpd.conf /etc/vsftpd.conf +RUN echo "seccomp_sandbox=NO" >> /etc/vsftpd.conf +RUN adduser -D ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser ftpuser www-data +RUN adduser root www-data +RUN chown -R ftpuser.www-data /var/www +RUN chmod g+w -R /var/www + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone https://github.com/bats-core/bats-support /bats-support +RUN git clone https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Use supervisord to run nginx in the background +COPY ./test/test-config/alpine-supervisord.conf /etc/supervisord.conf +CMD tail -f /dev/null diff --git a/test/Dockerfile-bash5-0 b/test/Dockerfile-bash5-0 new file mode 100644 index 0000000..2d776eb --- /dev/null +++ b/test/Dockerfile-bash5-0 @@ -0,0 +1,34 @@ +FROM bash:5.0 + +# https://hub.docker.com/_/bash + +RUN apk --no-cache add supervisor openssl git curl bind-tools drill wget nginx lftp vsftpd openssh-server + +WORKDIR /root + +# Create nginx directories in standard places +RUN mkdir /run/nginx +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=true +COPY ./test/test-config/vsftpd.conf /etc/vsftpd.conf +RUN echo "seccomp_sandbox=NO" >> /etc/vsftpd.conf +RUN adduser -D ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser ftpuser www-data +RUN adduser root www-data +RUN chown -R ftpuser.www-data /var/www +RUN chmod g+w -R /var/www + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone https://github.com/bats-core/bats-support /bats-support +RUN git clone https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Use supervisord to run nginx in the background +COPY ./test/test-config/alpine-supervisord.conf /etc/supervisord.conf +CMD tail -f /dev/null diff --git a/test/Dockerfile-centos6 b/test/Dockerfile-centos6 new file mode 100644 index 0000000..e16388f --- /dev/null +++ b/test/Dockerfile-centos6 @@ -0,0 +1,47 @@ +FROM centos:centos6 + +# Note this image uses gawk +# Note if you are running this using WSL2 you need to put the following lines in %userprofile%\.wslconfig +# [wsl2] +# kernelCommandLine = vsyscall=emulate + +# Centos 6 is EOL and is no longer available from the usual mirrors, so switch to https://vault.centos.org +RUN sed -i 's/enabled=1/enabled=0/g' /etc/yum/pluginconf.d/fastestmirror.conf && \ + sed -i 's/^mirrorlist/#mirrorlist/g' /etc/yum.repos.d/*.repo && \ + sed -i 's;^#baseurl=http://mirror;baseurl=https://vault;g' /etc/yum.repos.d/*.repo + +# Update and install required software +RUN yum -y install epel-release +RUN yum -y install git curl dnsutils ldns wget nginx +RUN yum -y install ftp vsftpd +RUN yum -y install openssh-server + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=true +COPY test/test-config/vsftpd.conf /etc/vsftpd/vsftpd.conf +RUN adduser ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser www-data +RUN usermod -G www-data ftpuser +RUN usermod -G www-data root +RUN mkdir -p /var/www/.well-known/acme-challenge +RUN chown -R www-data.www-data /var/www +RUN chmod g+w -R /var/www + +WORKDIR /root +RUN mkdir -p /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/conf.d/default.conf + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core # --branch v1.2.1 +RUN git clone https://github.com/bats-core/bats-support /bats-support +RUN git clone https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local +# Hack to disable BATS pretty formatter which stopped working on centos6 +ENV CI=yes + +EXPOSE 80 443 + +# Run eternal loop - for testing +CMD [ "tail", "-f", "/dev/null" ] diff --git a/test/Dockerfile-centos7 b/test/Dockerfile-centos7 new file mode 100644 index 0000000..794e0bd --- /dev/null +++ b/test/Dockerfile-centos7 @@ -0,0 +1,32 @@ +FROM centos:centos7 + +# Update and install required software +RUN yum -y update +RUN yum -y install epel-release +RUN yum -y install git curl ldns bind-utils wget which nginx +RUN yum -y install ftp vsftpd +RUN yum -y install openssh-server + +WORKDIR /root +RUN mkdir -p /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/conf.d/default.conf +COPY ./test/test-config/nginx-centos7.conf /etc/nginx/nginx.conf + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=true +COPY test/test-config/vsftpd.conf /etc/vsftpd/vsftpd.conf +RUN adduser ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser www-data +RUN usermod -G www-data ftpuser +RUN usermod -G www-data root +RUN mkdir -p /var/www/.well-known/acme-challenge +RUN chown -R www-data.www-data /var/www +RUN chmod g+w -R /var/www + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local diff --git a/test/Dockerfile-centos7-duckdns b/test/Dockerfile-centos7-duckdns new file mode 100644 index 0000000..bf85592 --- /dev/null +++ b/test/Dockerfile-centos7-duckdns @@ -0,0 +1,28 @@ +FROM centos:centos7 + +# Note this image uses gawk + +# Update and install required software +RUN yum -y update +RUN yum -y install epel-release +RUN yum -y install git curl bind-utils ldns wget which nginx + +ENV staging "true" +ENV dynamic_dns "dynu" +ENV DUCKDNS_TOKEN 1d616aa9-b8e4-4bb4-b312-3289de82badb + +WORKDIR /root +RUN mkdir -p /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/conf.d/default.conf +COPY ./test/test-config/nginx-centos7.conf /etc/nginx/nginx.conf + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +EXPOSE 80 443 + +# Run eternal loop - for testing +CMD [ "tail", "-f", "/dev/null" ] diff --git a/test/Dockerfile-centos7-dynu b/test/Dockerfile-centos7-dynu new file mode 100644 index 0000000..01d41a7 --- /dev/null +++ b/test/Dockerfile-centos7-dynu @@ -0,0 +1,29 @@ +FROM centos:centos7 + +# Note this image uses gawk + +# Update and install required software +RUN yum -y update +RUN yum -y install epel-release +RUN yum -y install git curl bind-utils ldns wget which nginx + +ENV staging "true" +ENV dynamic_dns "duckdns" +ENV DYNU_API_KEY 65cXefd35XbYf36546eg5dYcZT6X52Y2 + +WORKDIR /root +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/conf.d/default.conf +COPY ./test/test-config/nginx-centos7.conf /etc/nginx/nginx.conf + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone https://github.com/bats-core/bats-support /bats-support +RUN git clone https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +EXPOSE 80 443 + +# Run eternal loop - for testing +CMD tail -f /dev/null diff --git a/test/Dockerfile-centos8 b/test/Dockerfile-centos8 new file mode 100644 index 0000000..210bd8e --- /dev/null +++ b/test/Dockerfile-centos8 @@ -0,0 +1,34 @@ +FROM centos:centos8 + +# Note this image uses drill, does not have dig or nslookup installed + +# Update and install required software +RUN yum -y update +RUN yum -y install epel-release +RUN yum -y install git curl bind-utils wget which nginx +RUN yum -y install ftp vsftpd +RUN yum -y install openssh-server + +WORKDIR /root +RUN mkdir -p /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/conf.d/default.conf +COPY ./test/test-config/nginx-centos7.conf /etc/nginx/nginx.conf + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=true +COPY test/test-config/vsftpd.conf /etc/vsftpd/vsftpd.conf +RUN adduser ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser www-data +RUN usermod -G www-data ftpuser +RUN usermod -G www-data root +RUN mkdir -p /var/www/.well-known/acme-challenge +RUN chown -R www-data.www-data /var/www +RUN chmod g+w -R /var/www + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local diff --git a/test/Dockerfile-debian b/test/Dockerfile-debian new file mode 100644 index 0000000..04e5232 --- /dev/null +++ b/test/Dockerfile-debian @@ -0,0 +1,32 @@ +FROM debian:latest + +# Note this image uses mawk 1.3 + +# Update and install required software +RUN apt-get update --fix-missing +RUN apt-get install -y git curl dnsutils ldnsutils wget nginx-light +RUN apt-get install -y ftp vsftpd +RUN apt-get install -y openssh-server + +WORKDIR /root +RUN mkdir -p /etc/nginx/pki/private + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=false +COPY test/test-config/vsftpd.conf /etc/vsftpd.conf +RUN adduser ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser ftpuser www-data +RUN adduser root www-data +RUN chown -R www-data.www-data /var/www +RUN chmod g+w -R /var/www + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Run eternal loop - for testing +CMD [ "tail", "-f", "/dev/null" ] diff --git a/test/Dockerfile-ubuntu b/test/Dockerfile-ubuntu new file mode 100644 index 0000000..dd8338d --- /dev/null +++ b/test/Dockerfile-ubuntu @@ -0,0 +1,38 @@ +FROM ubuntu:latest + +# Note this image uses mawk1.3 + +# Set noninteractive otherwise tzdata hangs +ENV DEBIAN_FRONTEND noninteractive + +# Update and install required software +RUN apt-get update --fix-missing +RUN apt-get install -y git curl dnsutils ldnsutils wget nginx-light +RUN apt-get install -y vim dos2unix # for debugging +RUN apt-get install -y ftp vsftpd +RUN apt-get install -y openssh-server + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=false +COPY test/test-config/vsftpd.conf /etc/vsftpd.conf +RUN adduser ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser ftpuser www-data +RUN adduser root www-data +RUN chown -R www-data.www-data /var/www +RUN chmod g+w -R /var/www + +WORKDIR /root + +# Prevent "Can't load /root/.rnd into RNG" error from openssl +RUN touch /root/.rnd + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Run eternal loop - for testing +CMD [ "tail", "-f", "/dev/null" ] diff --git a/test/Dockerfile-ubuntu-duckdns b/test/Dockerfile-ubuntu-duckdns new file mode 100644 index 0000000..a07bb23 --- /dev/null +++ b/test/Dockerfile-ubuntu-duckdns @@ -0,0 +1,30 @@ +FROM ubuntu:latest + +# Note this image uses mawk1.3 + +# Set noninteractive otherwise tzdata hangs +ENV DEBIAN_FRONTEND noninteractive + +# Ensure tests in this image use the staging server +ENV staging "true" +ENV dynamic_dns "duckdns" +ENV DUCKDNS_TOKEN 1d616aa9-b8e4-4bb4-b312-3289de82badb + +# Update and install required software +RUN apt-get update --fix-missing +RUN apt-get install -y git curl dnsutils ldnsutils wget nginx-light +RUN apt-get install -y vim dos2unix # for debugging + +WORKDIR /root + +# Prevent "Can't load /root/.rnd into RNG" error from openssl +RUN touch /root/.rnd + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Run eternal loop - for testing +CMD [ "tail", "-f", "/dev/null" ] diff --git a/test/Dockerfile-ubuntu-dynu b/test/Dockerfile-ubuntu-dynu new file mode 100644 index 0000000..2ea8c31 --- /dev/null +++ b/test/Dockerfile-ubuntu-dynu @@ -0,0 +1,30 @@ +FROM ubuntu:latest + +# Note this image uses mawk1.3 + +# Set noninteractive otherwise tzdata hangs +ENV DEBIAN_FRONTEND noninteractive + +# Ensure tests in this image use the staging server +ENV staging "true" +ENV dynamic_dns "dynu" +ENV DYNU_API_KEY 65cXefd35XbYf36546eg5dYcZT6X52Y2 + +# Update and install required software +RUN apt-get update --fix-missing +RUN apt-get install -y git curl dnsutils ldnsutils wget nginx-light +RUN apt-get install -y vim dos2unix # for debugging + +WORKDIR /root + +# Prevent "Can't load /root/.rnd into RNG" error from openssl +RUN touch /root/.rnd + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone https://github.com/bats-core/bats-support /bats-support +RUN git clone https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Run eternal loop - for testing +CMD tail -f /dev/null diff --git a/test/Dockerfile-ubuntu16 b/test/Dockerfile-ubuntu16 new file mode 100644 index 0000000..00a4589 --- /dev/null +++ b/test/Dockerfile-ubuntu16 @@ -0,0 +1,36 @@ +FROM ubuntu:xenial +# xenial = 16 + +# Note this image uses mawk + +# Update and install required software +RUN apt-get update --fix-missing +RUN apt-get install -y git curl dnsutils ldnsutils wget nginx-light +RUN apt-get install -y ftp vsftpd +RUN apt-get install -y openssh-server + +WORKDIR /root +RUN mkdir -p /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=false +COPY test/test-config/vsftpd.conf /etc/vsftpd.conf +# The default init.d script seems to have an incorrect check that vsftpd has started +COPY test/test-config/vsftpd.initd /etc/init.d/vsftpd +RUN adduser ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser ftpuser www-data +RUN adduser root www-data +RUN chown -R www-data.www-data /var/www +RUN chmod g+w -R /var/www + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +# Run eternal loop - for testing +CMD [ "tail", "-f", "/dev/null" ] diff --git a/test/Dockerfile-ubuntu18 b/test/Dockerfile-ubuntu18 new file mode 100644 index 0000000..91a0429 --- /dev/null +++ b/test/Dockerfile-ubuntu18 @@ -0,0 +1,41 @@ +FROM ubuntu:bionic +# bionic = 18 LTS (long term support) + +# Note this image uses gawk + +# Update and install required software +RUN apt-get update --fix-missing +RUN apt-get install -y git curl dnsutils ldnsutils wget gawk nginx-light +RUN apt-get install -y ftp vsftpd +RUN apt-get install -y openssh-server + +WORKDIR /root +RUN mkdir -p /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default + +# Setup ftp +ENV VSFTPD_CONF=/etc/vsftpd.conf +ENV FTP_PASSIVE_DEFAULT=false +COPY test/test-config/vsftpd.conf /etc/vsftpd.conf +# The default init.d script seems to have an incorrect check that vsftpd has started +COPY test/test-config/vsftpd.initd /etc/init.d/vsftpd +RUN adduser ftpuser +RUN echo 'ftpuser:ftpuser' | chpasswd +RUN adduser ftpuser www-data +RUN adduser root www-data +RUN chown -R www-data.www-data /var/www +RUN chmod g+w -R /var/www + +# Prevent "Can't load /root/.rnd into RNG" error from openssl +RUN touch /root/.rnd + +# BATS (Bash Automated Testings) +RUN git clone --depth 1 https://github.com/bats-core/bats-core.git /bats-core --branch v1.2.1 +RUN git clone --depth 1 https://github.com/bats-core/bats-support /bats-support +RUN git clone --depth 1 https://github.com/bats-core/bats-assert /bats-assert +RUN /bats-core/install.sh /usr/local + +EXPOSE 80 443 + +# Run eternal loop - for testing +CMD [ "tail", "-f", "/dev/null" ] diff --git a/test/README-Testing.md b/test/README-Testing.md new file mode 100644 index 0000000..d713914 --- /dev/null +++ b/test/README-Testing.md @@ -0,0 +1,35 @@ +# Testing + +## Continuous Integration + +For continuous integration testing we have the following: + +`gitactions` script which runs whenever a PR is pushed: + +1. Uses `docker-compose` to start `pebble` (letsencrypt test server) and `challtestsrv` (minimal dns client for pebble) +2. Then runs the `bats` test scripts (all the files with a ".bats" extension) for each OS (alpine, centos6, debian, ubuntu) +3. Runs the `bats` test script against the staging server (using ubuntu docker image and duckdns.org) + +## To run all the tests on a single OS + +1. Start `pebble` and `challtestsrv` using ```docker-compose up -d --build``` +2. Run the test suite ```run-test.sh []``` +3. eg. `run-test.sh ubuntu16` + +## To run a single bats test on a single OS + +1. Start `pebble` and `challtestsrv` using ```docker-compose up -d --build``` +2. ```run-test.sh bats ``` +3. e.g. `run-test.sh ubuntu bats /getssl/test/1-simple-http01.bats` + +## To debug a test + +1. Start `pebble` and `challtestsrv` using ```docker-compose up -d --build``` +2. ```run-test.sh /getssl/test/debug-test.sh ``` +3. e.g. `run-test.sh ubuntu /getssl/test/debug-test.sh -d /getssl/test/test-config/getssl-http01-cfg` + +## TODO + +1. Test wildcards +2. Test SSH, SFTP, SCP +3. Test change of key algorithm (should automatically delete and re-create account.key) diff --git a/test/debug-test.sh b/test/debug-test.sh new file mode 100755 index 0000000..ac94b53 --- /dev/null +++ b/test/debug-test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +# This runs getssl outside of the BATS framework for debugging, etc, against pebble +# Usage: /getssl/test/debug-test.sh getssl-http01.cfg + +DEBUG="" +if [ $# -eq 2 ]; then + DEBUG=$1 + shift +fi + +#shellcheck disable=SC1091 +source /getssl/test/test_helper.bash 3>&1 + +CONFIG_FILE=$1 +if [ ! -e "$CONFIG_FILE" ]; then + CONFIG_FILE=${CODE_DIR}/test/test-config/${CONFIG_FILE} +fi + +setup_environment 3>&1 + +# Only add the pebble CA to the cert bundle if using pebble +if grep -q pebble "${CONFIG_FILE}"; then + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +fi + +"${CODE_DIR}/getssl" -c "$GETSSL_HOST" 3>&1 +cp "${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/${GETSSL_HOST}/getssl.cfg" +# shellcheck disable=SC2086 +"${CODE_DIR}/getssl" ${DEBUG} -f "$GETSSL_HOST" 3>&1 diff --git a/test/dns_add_fail b/test/dns_add_fail new file mode 100755 index 0000000..c39a89a --- /dev/null +++ b/test/dns_add_fail @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# Special test script which will always fail to update dns + +echo "This is a test script to check retry works if DNS isn't updated" +exit 0 diff --git a/test/restart-ftpd b/test/restart-ftpd new file mode 100755 index 0000000..f87d9d8 --- /dev/null +++ b/test/restart-ftpd @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +if [ "$GETSSL_OS" = "alpine" ]; then + killall -HUP vsftpd >&3- +elif [[ "$GETSSL_OS" == "centos"[78] ]]; then + pgrep vsftpd | head -1 | xargs kill -HUP +else + service vsftpd restart >/dev/null >&3- +fi diff --git a/test/restart-nginx b/test/restart-nginx new file mode 100755 index 0000000..4dc8af0 --- /dev/null +++ b/test/restart-nginx @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +if [ "$GETSSL_OS" = "alpine" ]; then + killall -HUP nginx >&3- + sleep 5 +elif [[ "$GETSSL_OS" == "centos"[78] ]]; then + pgrep nginx | head -1 | xargs kill -HUP + sleep 5 +else + service nginx restart >/dev/null >&3- +fi diff --git a/test/run-test.cmd b/test/run-test.cmd new file mode 100644 index 0000000..61b360e --- /dev/null +++ b/test/run-test.cmd @@ -0,0 +1,74 @@ +@echo off +IF %1.==. GOTO NoOS +SET OS=%1 + +:CheckCommand +IF %2.==. GOTO NoCmd +SET COMMAND=%2 %3 + +:CheckAlias +REM check if OS *contains* staging +IF NOT x%OS:duck=%==x%OS% GOTO duckdns +IF NOT x%OS:dynu=%==x%OS% GOTO dynu +IF NOT x%OS:bash=%==x%OS% GOTO bash +SET ALIAS=%OS%.getssl.test +SET STAGING= +SET GETSSL_OS=%OS% +GOTO Run + +:NoOS +SET OS=ubuntu +GOTO CheckCommand + +:NoCmd +REM SET COMMAND=/getssl/test/run-bats.sh +SET COMMAND=bats /getssl/test --timing +GOTO CheckAlias + +:duckdns +SET ALIAS=%OS:-duckdns=%-getssl.duckdns.org +SET STAGING=--env STAGING=true --env dynamic_dns=duckdns +SET GETSSL_OS=%OS:-duckdns=% +GOTO Run + +:dynu +SET ALIAS=%OS:-dynu=%-getssl.freeddns.org +SET STAGING=--env STAGING=true --env dynamic_dns=dynu +SET GETSSL_OS=%OS:-dynu=% +GOTO Run + +:bash +SET ALIAS=%OS%.getssl.test +SET STAGING= +SET GETSSL_OS=alpine + +:Run +FOR %%I in (.) DO SET CurrDirName=%%~nxI + +docker build --pull --rm -f "test\Dockerfile-%OS%" -t getssl-%OS% . +IF %ErrorLevel% EQU 1 GOTO End +@echo on +docker run -it ^ + --env GETSSL_HOST=%ALIAS% %STAGING% ^ + --env GETSSL_OS=%GETSSL_OS% ^ + -v %cd%:/getssl ^ + --rm ^ + --network %CurrDirName%_acmenet ^ + --network-alias %ALIAS% ^ + --network-alias a.%OS%.getssl.test ^ + --network-alias b.%OS%.getssl.test ^ + --network-alias c.%OS%.getssl.test ^ + --network-alias d.%OS%.getssl.test ^ + --network-alias e.%OS%.getssl.test ^ + --network-alias f.%OS%.getssl.test ^ + --network-alias g.%OS%.getssl.test ^ + --network-alias h.%OS%.getssl.test ^ + --network-alias i.%OS%.getssl.test ^ + --network-alias j.%OS%.getssl.test ^ + --network-alias k.%OS%.getssl.test ^ + --network-alias wild-%ALIAS% ^ + --name getssl-%OS% ^ + getssl-%OS% ^ + %COMMAND% + +:End diff --git a/test/run-test.sh b/test/run-test.sh new file mode 100755 index 0000000..ef7e403 --- /dev/null +++ b/test/run-test.sh @@ -0,0 +1,56 @@ +#! /usr/bin/env bash + +if [ $# -eq 0 ]; then + echo "Usage: $(basename "$0") []" + echo "e.g. $(basename "$0") alpine bats /getssl/test" + exit 1 +fi +OS=$1 + +if [ $# -gt 1 ]; then + shift + COMMAND=$* +else + COMMAND="bats /getssl/test --timing" +fi + +ALIAS="$OS.getssl.test" +STAGING="" +GETSSL_OS=$OS + +if [[ "$OS" == *"duckdns"* ]]; then + ALIAS="${OS%-duckdns}-getssl.duckdns.org" + STAGING="--env STAGING=true --env dynamic_dns=duckdns" + GETSSL_OS="${OS%-duckdns}" +elif [[ "$OS" == *"dynu"* ]]; then + ALIAS="${OS%-dynu}-getssl.freeddns.org" + STAGING="--env STAGING=true --env dynamic_dns=dynu" + GETSSL_OS="${OS%-dynu}" +elif [[ "$OS" == "bash"* ]]; then + GETSSL_OS="alpine" +fi + +docker build --rm -f "test/Dockerfile-$OS" -t "getssl-$OS" . +# shellcheck disable=SC2086 +docker run \ + --env GETSSL_HOST=$ALIAS $STAGING \ + --env GETSSL_OS=$GETSSL_OS \ + -v "$(pwd)":/getssl \ + --rm \ + --network ${PWD##*/}_acmenet \ + --network-alias $ALIAS \ + --network-alias "a.$OS.getssl.test" \ + --network-alias "b.$OS.getssl.test" \ + --network-alias "c.$OS.getssl.test" \ + --network-alias "d.$OS.getssl.test" \ + --network-alias "e.$OS.getssl.test" \ + --network-alias "f.$OS.getssl.test" \ + --network-alias "g.$OS.getssl.test" \ + --network-alias "h.$OS.getssl.test" \ + --network-alias "i.$OS.getssl.test" \ + --network-alias "j.$OS.getssl.test" \ + --network-alias "k.$OS.getssl.test" \ + --network-alias "wild-$OS.getssl.test" \ + --name "getssl-$OS" \ + "getssl-$OS" \ + $COMMAND diff --git a/test/test-config/alpine-supervisord.conf b/test/test-config/alpine-supervisord.conf new file mode 100644 index 0000000..bbf671e --- /dev/null +++ b/test/test-config/alpine-supervisord.conf @@ -0,0 +1,23 @@ +[supervisord] +nodaemon=false +logfile=/tmp/supervisord.log +childlogdir=/tmp +pidfile = /tmp/supervisord.pid + +[program:nginx] +command=nginx +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=false +startretries=0 + +[program:vsftpd] +command=vsftpd +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 +autorestart=false +startretries=0 diff --git a/test/test-config/getssl-dns01-add-to-sans-1.cfg b/test/test-config/getssl-dns01-add-to-sans-1.cfg new file mode 100644 index 0000000..ddb514a --- /dev/null +++ b/test/test-config/getssl-dns01-add-to-sans-1.cfg @@ -0,0 +1,37 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 + +# Speed up the test by reducing the number or retries and the wait between retries. +DNS_WAIT=2 +DNS_WAIT_COUNT=11 +DNS_EXTRA_WAIT=0 + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST}" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" +IGNORE_DIRECTORY_DOMAIN="true" + +#_USE_DEBUG=1 +#_RUNNING_TEST=1 diff --git a/test/test-config/getssl-dns01-add-to-sans-2.cfg b/test/test-config/getssl-dns01-add-to-sans-2.cfg new file mode 100644 index 0000000..9196905 --- /dev/null +++ b/test/test-config/getssl-dns01-add-to-sans-2.cfg @@ -0,0 +1,37 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 + +# Speed up the test by reducing the number or retries and the wait between retries. +DNS_WAIT=2 +DNS_WAIT_COUNT=11 +DNS_EXTRA_WAIT=0 + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="b.${GETSSL_HOST}, a.${GETSSL_HOST}" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="false" +IGNORE_DIRECTORY_DOMAIN="true" + +#_USE_DEBUG=1 +#_RUNNING_TEST=1 diff --git a/test/test-config/getssl-dns01-dual-rsa-ecdsa-2-locations.cfg b/test/test-config/getssl-dns01-dual-rsa-ecdsa-2-locations.cfg new file mode 100644 index 0000000..829e73c --- /dev/null +++ b/test/test-config/getssl-dns01-dual-rsa-ecdsa-2-locations.cfg @@ -0,0 +1,37 @@ +# Test that more than one location can be specified for CERT and KEY locations and that the +# files are copied to both locations when both RSA and ECDSA certificates are created +# +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 + +# Speed up the test by reducing the number or retries and the wait between retries. +DNS_WAIT=2 +DNS_WAIT_COUNT=11 +DNS_EXTRA_WAIT=0 + +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" +# SANS="a.${GETSSL_HOST}" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key;/root/a.${GETSSL_HOST}/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="/etc/nginx/pki/domain-chain.crt;/root/a.${GETSSL_HOST}/domain-chain.crt" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-dual-certs ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-dns01-dual-rsa-ecdsa-old-nginx.cfg b/test/test-config/getssl-dns01-dual-rsa-ecdsa-old-nginx.cfg new file mode 100644 index 0000000..15b4108 --- /dev/null +++ b/test/test-config/getssl-dns01-dual-rsa-ecdsa-old-nginx.cfg @@ -0,0 +1,33 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT=0 + +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.ec.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.ec.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="false" diff --git a/test/test-config/getssl-dns01-dual-rsa-ecdsa.cfg b/test/test-config/getssl-dns01-dual-rsa-ecdsa.cfg new file mode 100644 index 0000000..4059dd4 --- /dev/null +++ b/test/test-config/getssl-dns01-dual-rsa-ecdsa.cfg @@ -0,0 +1,33 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT=0 + +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-dual-certs ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-dns01-ignore-directory-domain.cfg b/test/test-config/getssl-dns01-ignore-directory-domain.cfg new file mode 100644 index 0000000..e55fbe0 --- /dev/null +++ b/test/test-config/getssl-dns01-ignore-directory-domain.cfg @@ -0,0 +1,30 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT=0 + +# Ignore directory domain (i.e. the domain passed on the command line), and just use the domains in the SANS list +IGNORE_DIRECTORY_DOMAIN="true" +SANS="getssl.test,$GETSSL_HOST" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-dns01-multiple-domains.cfg b/test/test-config/getssl-dns01-multiple-domains.cfg new file mode 100644 index 0000000..82497ad --- /dev/null +++ b/test/test-config/getssl-dns01-multiple-domains.cfg @@ -0,0 +1,29 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT=0 + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="getssl.test" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-dns01-secp384.cfg b/test/test-config/getssl-dns01-secp384.cfg new file mode 100644 index 0000000..2cc360c --- /dev/null +++ b/test/test-config/getssl-dns01-secp384.cfg @@ -0,0 +1,36 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 + +# Speed up the test by reducing the number or retries and the wait between retries. +DNS_WAIT=2 +DNS_WAIT_COUNT=11 +DNS_EXTRA_WAIT=0 + +ACCOUNT_KEY_TYPE="secp384r1" +PRIVATE_KEY_ALG="secp384r1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-dns01-spaces-and-commas-sans.cfg b/test/test-config/getssl-dns01-spaces-and-commas-sans.cfg new file mode 100644 index 0000000..204d0bf --- /dev/null +++ b/test/test-config/getssl-dns01-spaces-and-commas-sans.cfg @@ -0,0 +1,28 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT=0 + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST}, b.${GETSSL_HOST}, c.${GETSSL_HOST}" + +# Location for all your certs, these can either be on the server (full path name) +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-dns01-spaces-sans-and-ignore-dir-domain.cfg b/test/test-config/getssl-dns01-spaces-sans-and-ignore-dir-domain.cfg new file mode 100644 index 0000000..75e7304 --- /dev/null +++ b/test/test-config/getssl-dns01-spaces-sans-and-ignore-dir-domain.cfg @@ -0,0 +1,29 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT=0 + +# Ignore directory domain (i.e. the domain passed on the command line), and just use the domains in the SANS list +IGNORE_DIRECTORY_DOMAIN="true" +SANS="a.${GETSSL_HOST} b.${GETSSL_HOST} c.${GETSSL_HOST}" + +# Location for all your certs, these can either be on the server (full path name) +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-dns01-spaces-sans.cfg b/test/test-config/getssl-dns01-spaces-sans.cfg new file mode 100644 index 0000000..e954fa0 --- /dev/null +++ b/test/test-config/getssl-dns01-spaces-sans.cfg @@ -0,0 +1,28 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + +CA="https://pebble:14000/dir" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" +AUTH_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT=0 + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST} b.${GETSSL_HOST} c.${GETSSL_HOST}" + +# Location for all your certs, these can either be on the server (full path name) +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-dns01.cfg b/test/test-config/getssl-dns01.cfg new file mode 100644 index 0000000..bd02c50 --- /dev/null +++ b/test/test-config/getssl-dns01.cfg @@ -0,0 +1,59 @@ +# Test that the script works with dns + +VALIDATE_VIA_DNS=true +if [ -z "$STAGING" ]; then + # Settings for challtestserv dns provider running in local docker + CA="https://pebble:14000/dir" + + DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_challtestsrv" + DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_challtestsrv" + AUTH_DNS_SERVER=10.30.50.3 + + # Speed up the test by reducing the number or retries and the wait between retries. + DNS_WAIT=2 + DNS_WAIT_COUNT=11 + DNS_EXTRA_WAIT=0 +else + # Settings for external dns provider and staging server + CA="https://acme-staging-v02.api.letsencrypt.org/directory" + + DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_${dynamic_dns}" + DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_${dynamic_dns}" + PUBLIC_DNS_SERVER="8.8.8.8 resolver1.infoserve.de" + if [[ "${dynamic_dns}" == "dynu" ]]; then + AUTH_DNS_SERVER=ns1.dynu.com + else + AUTH_DNS_SERVER=ns1.duckdns.org + fi + CHECK_ALL_AUTH_DNS="true" + CHECK_PUBLIC_DNS_SERVER="true" + DNS_EXTRA_WAIT=120 + + DNS_WAIT_COUNT=20 + DNS_WAIT=30 + DNS_WAIT_RETRY_ADD="true" +fi +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" + +#_USE_DEBUG=1 +#_RUNNING_TEST=1 + +if [[ -s "$DOMAIN_DIR/getssl_test_specific.cfg" ]]; then + . $DOMAIN_DIR/getssl_test_specific.cfg +fi diff --git a/test/test-config/getssl-etc-template.cfg b/test/test-config/getssl-etc-template.cfg new file mode 100644 index 0000000..6bfc8fd --- /dev/null +++ b/test/test-config/getssl-etc-template.cfg @@ -0,0 +1,45 @@ +# vim: filetype=sh +# +# This file is read first and is common to all domains +# +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# +# The staging server is best for testing (hence set as default) +CA="https://acme-staging-v02.api.letsencrypt.org" +# This server issues full certificates, however has rate limits +#CA="https://acme-v02.api.letsencrypt.org" + +# The agreement that must be signed with the CA, if not defined the default agreement will be used +#AGREEMENT="" + +# Set an email address associated with your account - generally set at account level rather than domain. +#ACCOUNT_EMAIL="me@example.com" +ACCOUNT_KEY_LENGTH=4096 +ACCOUNT_KEY="/etc/getssl/account.key" + +# Account key and private key types - can be rsa, prime256v1, secp384r1 or secp521r1 +#ACCOUNT_KEY_TYPE="rsa" +PRIVATE_KEY_ALG="rsa" +#REUSE_PRIVATE_KEY="true" + +# The command needed to reload apache / nginx or whatever you use +#RELOAD_CMD="" + +# The time period within which you want to allow renewal of a certificate +# this prevents hitting some of the rate limits. +# Creating a file called FORCE_RENEWAL in the domain directory allows one-off overrides +# of this setting +RENEW_ALLOW="30" + +# Define the server type. This can be https, ftp, ftpi, imap, imaps, pop3, pop3s, smtp, +# smtps_deprecated, smtps, smtp_submission, xmpp, xmpps, ldaps or a port number which +# will be checked for certificate expiry and also will be checked after +# an update to confirm correct certificate is running (if CHECK_REMOTE) is set to true +SERVER_TYPE="https" +CHECK_REMOTE="true" + +# Use the following 3 variables if you want to validate via DNS +#VALIDATE_VIA_DNS="true" +#DNS_ADD_COMMAND= +#DNS_DEL_COMMAND= diff --git a/test/test-config/getssl-http01-10-hosts.cfg b/test/test-config/getssl-http01-10-hosts.cfg new file mode 100644 index 0000000..d5c364e --- /dev/null +++ b/test/test-config/getssl-http01-10-hosts.cfg @@ -0,0 +1,28 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + +CA="https://pebble:14000/dir" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST},b.${GETSSL_HOST},c.${GETSSL_HOST},d.${GETSSL_HOST},e.${GETSSL_HOST},f.${GETSSL_HOST},g.${GETSSL_HOST},h.${GETSSL_HOST},i.${GETSSL_HOST},j.${GETSSL_HOST},k.${GETSSL_HOST}" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +# Use a single ACL for all checks +USE_SINGLE_ACL="true" + +# Location for all your certs, these can either be on the server (full path name) +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-bad-acl.cfg b/test/test-config/getssl-http01-bad-acl.cfg new file mode 100644 index 0000000..461d3a3 --- /dev/null +++ b/test/test-config/getssl-http01-bad-acl.cfg @@ -0,0 +1,29 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL= ('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations-old-nginx.cfg b/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations-old-nginx.cfg new file mode 100644 index 0000000..50c3970 --- /dev/null +++ b/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations-old-nginx.cfg @@ -0,0 +1,32 @@ +# Test that more than one location can be specified for CERT and KEY locations and that the +# files are copied to both locations when both RSA and ECDSA certificates are created +# +CA="https://pebble:14000/dir" + +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST}" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="true" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key;/root/a.${GETSSL_HOST}/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="/etc/nginx/pki/domain-chain.crt;/root/a.${GETSSL_HOST}/domain-chain.crt" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="false" diff --git a/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations-wrong-nginx.cfg b/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations-wrong-nginx.cfg new file mode 100644 index 0000000..80533ce --- /dev/null +++ b/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations-wrong-nginx.cfg @@ -0,0 +1,32 @@ +# Test that more than one location can be specified for CERT and KEY locations and that the +# files are copied to both locations when both RSA and ECDSA certificates are created +# +CA="https://pebble:14000/dir" + +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST}" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="true" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key;/root/a.${GETSSL_HOST}/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="/etc/nginx/pki/domain-chain.crt;/root/a.${GETSSL_HOST}/domain-chain.crt" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations.cfg b/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations.cfg new file mode 100644 index 0000000..96e4d4d --- /dev/null +++ b/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations.cfg @@ -0,0 +1,32 @@ +# Test that more than one location can be specified for CERT and KEY locations and that the +# files are copied to both locations when both RSA and ECDSA certificates are created +# +CA="https://pebble:14000/dir" + +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST}" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="true" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key;/root/a.${GETSSL_HOST}/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="/etc/nginx/pki/domain-chain.crt;/root/a.${GETSSL_HOST}/domain-chain.crt" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-dual-certs ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-dual-rsa-ecdsa-old-nginx.cfg b/test/test-config/getssl-http01-dual-rsa-ecdsa-old-nginx.cfg new file mode 100644 index 0000000..9cf155f --- /dev/null +++ b/test/test-config/getssl-http01-dual-rsa-ecdsa-old-nginx.cfg @@ -0,0 +1,33 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="false" diff --git a/test/test-config/getssl-http01-dual-rsa-ecdsa.cfg b/test/test-config/getssl-http01-dual-rsa-ecdsa.cfg new file mode 100644 index 0000000..9e348fc --- /dev/null +++ b/test/test-config/getssl-http01-dual-rsa-ecdsa.cfg @@ -0,0 +1,33 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +DUAL_RSA_ECDSA="true" +ACCOUNT_KEY_TYPE="prime256v1" +PRIVATE_KEY_ALG="prime256v1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-dual-certs ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-no-domain-storage.cfg b/test/test-config/getssl-http01-no-domain-storage.cfg new file mode 100644 index 0000000..efa5318 --- /dev/null +++ b/test/test-config/getssl-http01-no-domain-storage.cfg @@ -0,0 +1,31 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" + +DOMAIN_STORAGE="/" diff --git a/test/test-config/getssl-http01-no-suffix.cfg b/test/test-config/getssl-http01-no-suffix.cfg new file mode 100644 index 0000000..a1295dd --- /dev/null +++ b/test/test-config/getssl-http01-no-suffix.cfg @@ -0,0 +1,30 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" +CA="https://pebble:14000" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-secp384.cfg b/test/test-config/getssl-http01-secp384.cfg new file mode 100644 index 0000000..4fa3e82 --- /dev/null +++ b/test/test-config/getssl-http01-secp384.cfg @@ -0,0 +1,32 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +ACCOUNT_KEY_TYPE="secp384r1" +PRIVATE_KEY_ALG="secp384r1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-secp521.cfg b/test/test-config/getssl-http01-secp521.cfg new file mode 100644 index 0000000..6068fbf --- /dev/null +++ b/test/test-config/getssl-http01-secp521.cfg @@ -0,0 +1,32 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +ACCOUNT_KEY_TYPE="secp521r1" +PRIVATE_KEY_ALG="secp521r1" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-spaces-and-commas-sans.cfg b/test/test-config/getssl-http01-spaces-and-commas-sans.cfg new file mode 100644 index 0000000..c4f02c7 --- /dev/null +++ b/test/test-config/getssl-http01-spaces-and-commas-sans.cfg @@ -0,0 +1,28 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + +CA="https://pebble:14000/dir" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST}, b.${GETSSL_HOST}, c.${GETSSL_HOST}" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +# Use a single ACL for all checks +USE_SINGLE_ACL="true" + +# Location for all your certs, these can either be on the server (full path name) +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-spaces-sans-and-ignore-dir-domain.cfg b/test/test-config/getssl-http01-spaces-sans-and-ignore-dir-domain.cfg new file mode 100644 index 0000000..1b3cdca --- /dev/null +++ b/test/test-config/getssl-http01-spaces-sans-and-ignore-dir-domain.cfg @@ -0,0 +1,29 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + +CA="https://pebble:14000/dir" + +# Ignore directory domain (i.e. the domain passed on the command line), and just use the domains in the SANS list +IGNORE_DIRECTORY_DOMAIN="true" +SANS="a.${GETSSL_HOST} b.${GETSSL_HOST} c.${GETSSL_HOST}" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +# Use a single ACL for all checks +USE_SINGLE_ACL="true" + +# Location for all your certs, these can either be on the server (full path name) +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-spaces-sans.cfg b/test/test-config/getssl-http01-spaces-sans.cfg new file mode 100644 index 0000000..c93b4f7 --- /dev/null +++ b/test/test-config/getssl-http01-spaces-sans.cfg @@ -0,0 +1,28 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs + +CA="https://pebble:14000/dir" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="a.${GETSSL_HOST} b.${GETSSL_HOST} c.${GETSSL_HOST}" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +# Use a single ACL for all checks +USE_SINGLE_ACL="true" + +# Location for all your certs, these can either be on the server (full path name) +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01-two-acl.cfg b/test/test-config/getssl-http01-two-acl.cfg new file mode 100644 index 0000000..dcf28fd --- /dev/null +++ b/test/test-config/getssl-http01-two-acl.cfg @@ -0,0 +1,29 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge;/var/webroot/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-http01.cfg b/test/test-config/getssl-http01.cfg new file mode 100644 index 0000000..9a713bc --- /dev/null +++ b/test/test-config/getssl-http01.cfg @@ -0,0 +1,36 @@ +# Uncomment and modify any variables you need +# see https://github.com/srvrco/getssl/wiki/Config-variables for details +# see https://github.com/srvrco/getssl/wiki/Example-config-files for example configs +# +CA="https://pebble:14000/dir" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') + +#Set USE_SINGLE_ACL="true" to use a single ACL for all checks +USE_SINGLE_ACL="false" + +# Location for all your certs, these can either be on the server (full path name) +# or using ssh /sftp as for the ACL +DOMAIN_CERT_LOCATION="/etc/nginx/pki/server.crt" +DOMAIN_KEY_LOCATION="/etc/nginx/pki/private/server.key" +CA_CERT_LOCATION="/etc/nginx/pki/chain.crt" +DOMAIN_CHAIN_LOCATION="" # this is the domain cert and CA cert +DOMAIN_PEM_LOCATION="" # this is the domain_key, domain cert and CA cert + +# The command needed to reload apache / nginx or whatever you use +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" + +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" + +if [[ -s "$DOMAIN_DIR/getssl_test_specific.cfg" ]]; then + . $DOMAIN_DIR/getssl_test_specific.cfg +fi + +#_USE_DEBUG=1 +#_RUNNING_TEST=1 diff --git a/test/test-config/getssl-upgrade-test-pebble.cfg b/test/test-config/getssl-upgrade-test-pebble.cfg new file mode 100644 index 0000000..f5f8b3b --- /dev/null +++ b/test/test-config/getssl-upgrade-test-pebble.cfg @@ -0,0 +1,8 @@ +# +# Test that auto-upgrade to v2 doesn't change pebble url +# +CA="https://pebble:14000/dir" + + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') diff --git a/test/test-config/getssl-upgrade-test-v1-prod.cfg b/test/test-config/getssl-upgrade-test-v1-prod.cfg new file mode 100644 index 0000000..41f0176 --- /dev/null +++ b/test/test-config/getssl-upgrade-test-v1-prod.cfg @@ -0,0 +1,7 @@ +# +# Test that auto-upgrade to v2 changes v1 prod to v2 prod +# +CA="https://acme-v01.api.letsencrypt.org/directory" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') diff --git a/test/test-config/getssl-upgrade-test-v1-staging.cfg b/test/test-config/getssl-upgrade-test-v1-staging.cfg new file mode 100644 index 0000000..215f246 --- /dev/null +++ b/test/test-config/getssl-upgrade-test-v1-staging.cfg @@ -0,0 +1,7 @@ +# +# Test that auto-upgrade to v2 changes v1 staging to v2 staging +# +CA="https://acme-staging.api.letsencrypt.org/directory" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') diff --git a/test/test-config/getssl-upgrade-test-v2-prod.cfg b/test/test-config/getssl-upgrade-test-v2-prod.cfg new file mode 100644 index 0000000..9b9c009 --- /dev/null +++ b/test/test-config/getssl-upgrade-test-v2-prod.cfg @@ -0,0 +1,7 @@ +# +# Test that auto-upgrade to v2 doesn't change v2 prod url +# +CA="https://acme-v02.api.letsencrypt.org/directory" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') diff --git a/test/test-config/getssl-upgrade-test-v2-staging.cfg b/test/test-config/getssl-upgrade-test-v2-staging.cfg new file mode 100644 index 0000000..98bfd17 --- /dev/null +++ b/test/test-config/getssl-upgrade-test-v2-staging.cfg @@ -0,0 +1,7 @@ +# +# Test that auto-upgrade to v2 doesn't change v2 staging url +# +CA="https://acme-staging-v02.api.letsencrypt.org/directory" + +# Acme Challenge Location. +ACL=('/var/www/html/.well-known/acme-challenge') diff --git a/test/test-config/nginx-centos7.conf b/test/test-config/nginx-centos7.conf new file mode 100644 index 0000000..f487519 --- /dev/null +++ b/test/test-config/nginx-centos7.conf @@ -0,0 +1,29 @@ +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +include /usr/share/nginx/modules/*.conf; + +events { + worker_connections 1024; +} + +http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + include /etc/nginx/conf.d/*.conf; +} diff --git a/test/test-config/nginx-ubuntu-dual-certs b/test/test-config/nginx-ubuntu-dual-certs new file mode 100644 index 0000000..f6a4c80 --- /dev/null +++ b/test/test-config/nginx-ubuntu-dual-certs @@ -0,0 +1,28 @@ +server { + listen 80 default_server; + listen 5002 default_server; + listen [::]:5002 default_server; + + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + listen 5001 ssl default_server; + listen [::]:5001 ssl default_server; + + root /var/www/html; + + index index.html index.htm index.nginx-debian.html; + + ssl_protocols TLSv1 TLSv1.1 TLSv1.2 TLSv1.3; + + server_name _; + ssl_certificate /etc/nginx/pki/server.crt; + ssl_certificate_key /etc/nginx/pki/private/server.key; + + ssl_certificate /etc/nginx/pki/server.ec.crt; + ssl_certificate_key /etc/nginx/pki/private/server.ec.key; + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/test/test-config/nginx-ubuntu-no-ssl b/test/test-config/nginx-ubuntu-no-ssl new file mode 100644 index 0000000..aa32d05 --- /dev/null +++ b/test/test-config/nginx-ubuntu-no-ssl @@ -0,0 +1,21 @@ +server { + listen 80 default_server; + listen 5002 default_server; + listen [::]:5002 default_server; + + listen 443 default_server; + listen [::]:443 default_server; + + listen 5001 default_server; + listen [::]:5001 default_server; + + root /var/www/html; + + index index.html index.htm index.nginx-debian.html; + + server_name _; + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/test/test-config/nginx-ubuntu-ssl b/test/test-config/nginx-ubuntu-ssl new file mode 100644 index 0000000..e87a497 --- /dev/null +++ b/test/test-config/nginx-ubuntu-ssl @@ -0,0 +1,23 @@ +server { + listen 80 default_server; + listen 5002 default_server; + listen [::]:5002 default_server; + + listen 443 ssl default_server; + listen [::]:443 ssl default_server; + + listen 5001 ssl default_server; + listen [::]:5001 ssl default_server; + + root /var/www/html; + + index index.html index.htm index.nginx-debian.html; + + server_name _; + ssl_certificate /etc/nginx/pki/server.crt; + ssl_certificate_key /etc/nginx/pki/private/server.key; + + location / { + try_files $uri $uri/ =404; + } +} diff --git a/test/test-config/vsftpd.conf b/test/test-config/vsftpd.conf new file mode 100644 index 0000000..59ca78a --- /dev/null +++ b/test/test-config/vsftpd.conf @@ -0,0 +1,66 @@ +# Example config file /etc/vsftpd.conf (alpine) /etc/vsftpd/vsftpd.conf +# +# The default compiled in settings are fairly paranoid. This sample file +# loosens things up a bit, to make the ftp daemon more usable. +# Please see vsftpd.conf.5 for all compiled in defaults. +# +# Run standalone? vsftpd can run either from an inetd or as a standalone +# daemon started from an initscript. +listen=YES +# +# This directive enables listening on IPv6 sockets. By default, listening +# on the IPv6 "any" address (::) will accept connections from both IPv6 +# and IPv4 clients. It is not necessary to listen on *both* IPv4 and IPv6 +# sockets. If you want that (perhaps because you want to listen on specific +# addresses) then you must run two copies of vsftpd with two configuration +# files. +#listen_ipv6=NO +# +# Allow anonymous FTP? (Disabled by default). +anonymous_enable=NO +# +# Uncomment this to allow local users to log in. +local_enable=YES +# +# Uncomment this to enable any form of FTP write command. +write_enable=YES +# +# Default umask for local users is 077. You may wish to change this to 022, +# if your users expect that (022 is used by most other ftpd's) +local_umask=022 +# +# Activate directory messages - messages given to remote users when they +# go into a certain directory. +dirmessage_enable=YES +# +# If enabled, vsftpd will display directory listings with the time +# in your local time zone. The default is to display GMT. The +# times returned by the MDTM FTP command are also affected by this +# option. +use_localtime=YES +# +# Activate logging of uploads/downloads. +xferlog_enable=YES +# +# Make sure PORT transfer connections originate from port 20 (ftp-data). +connect_from_port_20=YES +# +# You may change the default value for timing out an idle session. +#idle_session_timeout=600 +# +# You may change the default value for timing out a data connection. +#data_connection_timeout=120 +# +# You may restrict local users to their home directories. See the FAQ for +# the possible risks in this before using chroot_local_user or +# chroot_list_enable below. +chroot_local_user=NO +# +# This string is the name of the PAM service vsftpd will use. +pam_service_name=vsftpd +# +# This option specifies the location of the RSA certificate to use for SSL +# encrypted connections. +rsa_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem +rsa_private_key_file=/etc/ssl/private/ssl-cert-snakeoil.key +ssl_enable=NO diff --git a/test/test-config/vsftpd.initd b/test/test-config/vsftpd.initd new file mode 100755 index 0000000..d5b2b00 --- /dev/null +++ b/test/test-config/vsftpd.initd @@ -0,0 +1,103 @@ +#!/bin/sh + +### BEGIN INIT INFO +# Provides: vsftpd +# Required-Start: $network $remote_fs $syslog +# Required-Stop: $network $remote_fs $syslog +# Default-Start: 2 3 4 5 +# Default-Stop: 0 1 6 +# Short-Description: Very secure FTP server +# Description: Provides a lightweight, efficient FTP server written +# for security. +### END INIT INFO + +set -e + +DAEMON="/usr/sbin/vsftpd" +NAME="vsftpd" +PATH="/sbin:/bin:/usr/sbin:/usr/bin" +LOGFILE="/var/log/vsftpd.log" +CHROOT="/var/run/vsftpd/empty" + +test -x "${DAEMON}" || exit 0 + +. /lib/lsb/init-functions + +if [ ! -e "${LOGFILE}" ] +then + touch "${LOGFILE}" + chmod 640 "${LOGFILE}" + chown root:adm "${LOGFILE}" +fi + +if [ ! -d "${CHROOT}" ] +then + mkdir -p "${CHROOT}" +fi + +case "${1}" in + start) + log_daemon_msg "Starting FTP server" "${NAME}" + + if [ -e /etc/vsftpd.conf ] && ! egrep -iq "^ *listen(_ipv6)? *= *yes" /etc/vsftpd.conf + then + log_warning_msg "vsftpd disabled - listen disabled in config." + exit 0 + fi + + start-stop-daemon --start --background -m --oknodo --pidfile /var/run/vsftpd/vsftpd.pid --exec ${DAEMON} + + n=0 + while [ ${n} -le 5 ] + do + _PID="$(if [ -e /var/run/vsftpd/vsftpd.pid ]; then cat /var/run/vsftpd/vsftpd.pid; fi)" + if ! ps -C vsftpd | grep -qs "${_PID}" + then + break + fi + sleep 1 + n=$(( $n + 1 )) + done + + if ps -C vsftpd | grep -qs "${_PID}" + then + log_warning_msg "vsftpd failed - probably invalid config." + exit 1 + fi + + log_end_msg 0 + ;; + + stop) + log_daemon_msg "Stopping FTP server" "${NAME}" + + start-stop-daemon --stop --pidfile /var/run/vsftpd/vsftpd.pid --oknodo --exec ${DAEMON} + rm -f /var/run/vsftpd/vsftpd.pid + + log_end_msg 0 + ;; + + restart) + ${0} stop + ${0} start + ;; + + reload|force-reload) + log_daemon_msg "Reloading FTP server configuration" + + start-stop-daemon --stop --pidfile /var/run/vsftpd/vsftpd.pid --signal 1 --exec $DAEMON + + log_end_msg "${?}" + ;; + + status) + status_of_proc "${DAEMON}" "FTP server" + ;; + + *) + echo "Usage: ${0} {start|stop|restart|reload|status}" + exit 1 + ;; +esac + +exit 0 diff --git a/test/test_helper.bash b/test/test_helper.bash new file mode 100644 index 0000000..48a8d4b --- /dev/null +++ b/test/test_helper.bash @@ -0,0 +1,124 @@ +INSTALL_DIR=/root +CODE_DIR=/getssl + +check_certificates() +{ + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/chain.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/fullchain.crt" ] + assert [ -e "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/${GETSSL_CMD_HOST}.crt" ] +} + +# Only nginx > 1.11.0 support dual certificates in a single configuration file +# https://unix.stackexchange.com/questions/285924/how-to-compare-a-programs-version-in-a-shell-script +check_nginx() { + requiredver="1.11.0" + currentver=$(nginx -v 2>&1 | awk -F"/" '{print $2}') + if [ "$(printf '%s\n' "$requiredver" "$currentver" | sort -V | head -n1)" = "$requiredver" ]; then + export OLD_NGINX="false" + else + echo "INFO: Running nginx version $currentver which doesn't support dual certificates" >&3 + echo "INFO: not checking that certificate is installed correctly" >&3 + export OLD_NGINX="true" + fi +} + +check_output_for_errors() { + refute_output --regexp '[Ff][Aa][Ii][Ll][Ee][Dd]' + # less strict tests if running with debug output + if [ -n "$1" ]; then + # don't fail for :error:badNonce + refute_output --regexp '[^:][Ee][Rr][Rr][Oo][Rr][^:]' + # don't check for "Warnings:" as there might be a warning message if nslookup doesn't support -debug (alpine/ubuntu) + refute_output --regexp '[Ww][Aa][Rr][Nn][Ii][Nn][Gg][^:]' + else + refute_output --regexp '[Ee][Rr][Rr][Oo][Rr]' + refute_output --regexp '[Ww][Aa][Rr][Nn][Ii][Nn][Gg]' + fi + refute_line --partial 'command not found' +} + +cleanup_environment() { + if [ -z "$STAGING" ]; then + curl --silent -X POST -d '{"host":"'"$GETSSL_HOST"'"}' http://10.30.50.3:8055/clear-a + fi +} + +create_certificate() { + # Create certificate + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/${GETSSL_CMD_HOST}/getssl.cfg" + # shellcheck disable=SC2086 + run ${CODE_DIR}/getssl $1 "$GETSSL_CMD_HOST" +} + +init_getssl() { + # Run initialisation (create account key, etc) + run ${CODE_DIR}/getssl -c "$GETSSL_CMD_HOST" + assert_success + [ -d "$INSTALL_DIR/.getssl" ] +} + +setup_environment() { + # One-off test setup + if [[ -d ${INSTALL_DIR}/.getssl ]]; then + rm -r ${INSTALL_DIR}/.getssl + fi + + if [ -z "$STAGING" ]; then + # Make sure that we have cleared any previous entries, otherwise get random dns failures + curl --silent -X POST -d '{"host":"'"$GETSSL_HOST"'"}' http://10.30.50.3:8055/clear-a + curl --silent -X POST -d '{"host":"'"$GETSSL_HOST"'", "addresses":["'"$GETSSL_IP"'"]}' http://10.30.50.3:8055/add-a + fi + cp ${CODE_DIR}/test/test-config/nginx-ubuntu-no-ssl "${NGINX_CONFIG}" + /getssl/test/restart-nginx +} + +# start nginx and vsftpd in background on alpine via supervisord +# shellcheck disable=SC2153 # Ignore GETSSL_OS looks like typo of GETSSL_IP +if [[ -f /usr/bin/supervisord && -f /etc/supervisord.conf ]]; then + if [[ ! $(pgrep supervisord) ]]; then + /usr/bin/supervisord -c /etc/supervisord.conf >&3- + # Give supervisord time to start + sleep 1 + fi +elif [[ "$GETSSL_OS" == "centos"[78] ]]; then + if [ -z "$(pgrep nginx)" ]; then + nginx >&3- + fi + if [ -z "$(pgrep vsftpd)" ] && [ "$(command -v vsftpd)" ]; then + vsftpd >&3- + fi +fi + +# Find NGINX configuration directory for HTTP-01 testing (need to add SSL to config) +if [[ -f /etc/nginx/conf.d/default.conf ]]; then + export NGINX_CONFIG=/etc/nginx/conf.d/default.conf +elif [[ -f /etc/nginx/sites-enabled/default ]]; then + export NGINX_CONFIG=/etc/nginx/sites-enabled/default +else + echo "Can't find NGINX directory" + exit 1 +fi + +# Find IP address +if [[ -n "$(command -v ip)" ]]; then + GETSSL_IP=$(ip address | awk '/10.30.50/ { print $2 }' | awk -F/ '{ print $1 }') +elif [[ -n "$(command -v hostname)" ]]; then + GETSSL_IP=$(hostname -I | sed -e 's/[[:space:]]*$//') +else + echo "Cannot find IP address" + exit 1 +fi + +export GETSSL_IP + +GETSSL_CMD_HOST=$GETSSL_HOST +export GETSSL_CMD_HOST + +if [ -z "$STAGING" ] && [ ! -f ${INSTALL_DIR}/pebble.minica.pem ]; then + wget --quiet --no-clobber https://raw.githubusercontent.com/letsencrypt/pebble/master/test/certs/pebble.minica.pem 2>&1 + CERT_FILE=/etc/ssl/certs/ca-certificates.crt + if [ ! -f $CERT_FILE ]; then + CERT_FILE=/etc/pki/tls/certs/ca-bundle.crt + fi + cat $CERT_FILE ${INSTALL_DIR}/pebble.minica.pem > ${INSTALL_DIR}/pebble-ca-bundle.crt +fi diff --git a/test/u1-test-get_auth_dns-dig.bats b/test/u1-test-get_auth_dns-dig.bats new file mode 100644 index 0000000..6e64e68 --- /dev/null +++ b/test/u1-test-get_auth_dns-dig.bats @@ -0,0 +1,178 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + for app in drill host nslookup + do + if [ -f /usr/bin/${app} ]; then + mv /usr/bin/${app} /usr/bin/${app}.getssl.bak + fi + done + + . /getssl/getssl --source + find_dns_utils + _RUNNING_TEST=1 + _USE_DEBUG=0 +} + + +teardown() { + for app in drill host nslookup + do + if [ -f /usr/bin/${app}.getssl.bak ]; then + mv /usr/bin/${app}.getssl.bak /usr/bin/${app} + fi + done +} + + +@test "Check get_auth_dns using dig NS" { + # Test that get_auth_dns() handles scenario where NS query returns Authority section + # + # ************** EXAMPLE DIG OUTPUT ************** + # + # ;; ANSWER SECTION: + # ubuntu-getssl.duckdns.org. 60 IN A 54.89.252.137 + # + # ;; AUTHORITY SECTION: + # duckdns.org. 600 IN NS ns2.duckdns.org. + # duckdns.org. 600 IN NS ns3.duckdns.org. + # duckdns.org. 600 IN NS ns1.duckdns.org. + # + # ;; ADDITIONAL SECTION: + # ns2.duckdns.org. 600 IN A 54.191.117.119 + # ns3.duckdns.org. 600 IN A 52.26.169.94 + # ns1.duckdns.org. 600 IN A 54.187.92.222 + + # Disable CNAME check + _TEST_SKIP_CNAME_CALL=1 + + PUBLIC_DNS_SERVER=ns1.duckdns.org + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=false + + run get_auth_dns ubuntu-getssl.duckdns.org + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org' + # Assert that we had to use dig NS + assert_line --partial 'Using dig NS' + + # Check all Authoritive DNS servers are returned if requested + CHECK_ALL_AUTH_DNS=true + run get_auth_dns ubuntu-getssl.duckdns.org + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org' +} + + +@test "Check get_auth_dns using dig SOA" { + # Test that get_auth_dns() handles scenario where SOA query returns Authority section + # + # ************** EXAMPLE DIG OUTPUT ************** + # + # ;; AUTHORITY SECTION: + # duckdns.org. 600 IN SOA ns3.duckdns.org. hostmaster.duckdns.org. 2019170803 6000 120 2419200 600 + + # DuckDNS server returns nothing for SOA, so use public dns instead + PUBLIC_DNS_SERVER=1.0.0.1 + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=false + + run get_auth_dns ubuntu-getssl.duckdns.org + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org' + + # Assert that we had to use dig NS + assert_line --partial 'Using dig SOA' + refute_line --partial 'Using dig NS' + + # Check all Authoritive DNS servers are returned if requested + CHECK_ALL_AUTH_DNS=true + run get_auth_dns ubuntu-getssl.duckdns.org + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org' + + # Check that we also check the public DNS server if requested + CHECK_PUBLIC_DNS_SERVER=true + run get_auth_dns ubuntu-getssl.duckdns.org + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org 1\.0\.0\.1' +} + + +@test "Check get_auth_dns using dig CNAME (public dns)" { + # Test that get_auth_dns() handles scenario where CNAME query returns just a CNAME record + # + # ************** EXAMPLE DIG OUTPUT ************** + # + # ;; ANSWER SECTION: + # www.duckdns.org. 600 IN CNAME DuckDNSAppELB-570522007.us-west-2.elb.amazonaws.com. + + # Disable SOA check + _TEST_SKIP_SOA_CALL=1 + + PUBLIC_DNS_SERVER=1.0.0.1 + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=false + + run get_auth_dns www.duckdns.org + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ns.*\.awsdns.*\.com' + + # Assert that we found a CNAME and use dig NS + assert_line --partial 'Using dig CNAME' + assert_line --partial 'Using dig NS' + + # Check all Authoritive DNS servers are returned if requested + CHECK_ALL_AUTH_DNS=true + run get_auth_dns www.duckdns.org + assert_output --regexp 'set primary_ns = ns.*\.awsdns.*\.com' + + # Check that we also check the public DNS server if requested + CHECK_PUBLIC_DNS_SERVER=true + run get_auth_dns www.duckdns.org + assert_output --regexp 'set primary_ns = ns.*\.awsdns.*\.com 1\.0\.0\.1' +} + + +@test "Check get_auth_dns using dig CNAME (duckdns)" { + # Test that get_auth_dns() handles scenario where CNAME query returns authority section containing NS records + # + # ************** EXAMPLE DIG OUTPUT ************** + # + # ;; ANSWER SECTION: + # www.duckdns.org. 600 IN CNAME DuckDNSAppELB-570522007.us-west-2.elb.amazonaws.com. + # + # ;; AUTHORITY SECTION: + # duckdns.org. 600 IN NS ns1.duckdns.org. + # duckdns.org. 600 IN NS ns2.duckdns.org. + # duckdns.org. 600 IN NS ns3.duckdns.org. + # + # ;; ADDITIONAL SECTION: + # ns1.duckdns.org. 600 IN A 54.187.92.222 + # ns2.duckdns.org. 600 IN A 54.191.117.119 + # ns3.duckdns.org. 600 IN A 52.26.169.94 + + PUBLIC_DNS_SERVER=ns1.duckdns.org + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=false + + run get_auth_dns www.duckdns.org + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org' + + # Assert that we found a CNAME but didn't use dig NS + assert_line --partial 'Using dig CNAME' + refute_line --partial 'Using dig NS' + + # Check all Authoritive DNS servers are returned if requested + CHECK_ALL_AUTH_DNS=true + run get_auth_dns www.duckdns.org + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org' +} diff --git a/test/u2-test-get_auth_dns-drill.bats b/test/u2-test-get_auth_dns-drill.bats new file mode 100644 index 0000000..33b2277 --- /dev/null +++ b/test/u2-test-get_auth_dns-drill.bats @@ -0,0 +1,202 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + for app in dig host nslookup + do + if [ -f /usr/bin/${app} ]; then + mv /usr/bin/${app} /usr/bin/${app}.getssl.bak + fi + done + + . /getssl/getssl --source + find_dns_utils + _RUNNING_TEST=1 + _USE_DEBUG=0 +} + + +teardown() { + for app in dig host nslookup + do + if [ -f /usr/bin/${app}.getssl.bak ]; then + mv /usr/bin/${app}.getssl.bak /usr/bin/${app} + fi + done +} + + +@test "Check get_auth_dns using drill NS" { + if [ ! -f /usr/bin/drill ]; then + # Can't find drill package for centos8 + skip "Drill not installed on this system" + fi + + # Test that get_auth_dns() handles scenario where NS query returns Authority section + # + # ************** EXAMPLE DRILL OUTPUT ************** + # + # ;; ANSWER SECTION: + # ubuntu-getssl.duckdns.org. 60 IN A 54.89.252.137 + # + # ;; AUTHORITY SECTION: + # duckdns.org. 600 IN NS ns2.duckdns.org. + # duckdns.org. 600 IN NS ns3.duckdns.org. + # duckdns.org. 600 IN NS ns1.duckdns.org. + # + # ;; ADDITIONAL SECTION: + # ns2.duckdns.org. 600 IN A 54.191.117.119 + # ns3.duckdns.org. 600 IN A 52.26.169.94 + # ns1.duckdns.org. 600 IN A 54.187.92.222 + + # Disable SOA and CNAME check + _TEST_SKIP_CNAME_CALL=1 + _TEST_SKIP_SOA_CALL=1 + + PUBLIC_DNS_SERVER=ns1.duckdns.org + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=false + + run get_auth_dns ubuntu-getssl.duckdns.org + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org' + # Assert that we had to use drill NS + assert_line --partial 'Using drill NS' + + # Check all Authoritive DNS servers are returned if requested + CHECK_ALL_AUTH_DNS=true + run get_auth_dns ubuntu-getssl.duckdns.org + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org' +} + + +@test "Check get_auth_dns using drill SOA" { + if [ ! -f /usr/bin/drill ]; then + # Can't find drill package for centos8 + skip "Drill not installed on this system" + fi + + # Test that get_auth_dns() handles scenario where SOA query returns Authority section + # + # ************** EXAMPLE DRILL OUTPUT ************** + # + # ;; AUTHORITY SECTION: + # duckdns.org. 600 IN SOA ns3.duckdns.org. hostmaster.duckdns.org. 2019170803 6000 120 2419200 600 + + # DuckDNS server returns nothing for SOA, so use public dns instead + PUBLIC_DNS_SERVER=1.0.0.1 + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=false + + run get_auth_dns ubuntu-getssl.duckdns.org + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org' + + # Assert that we had to use drill NS + assert_line --partial 'Using drill SOA' + refute_line --partial 'Using drill NS' + + # Check all Authoritive DNS servers are returned if requested + CHECK_ALL_AUTH_DNS=true + run get_auth_dns ubuntu-getssl.duckdns.org + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org' + + # Check that we also check the public DNS server if requested + CHECK_PUBLIC_DNS_SERVER=true + run get_auth_dns ubuntu-getssl.duckdns.org + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org 1\.0\.0\.1' +} + + +@test "Check get_auth_dns using drill CNAME (public dns)" { + if [ ! -f /usr/bin/drill ]; then + # Can't find drill package for centos8 + skip "Drill not installed on this system" + fi + + # Test that get_auth_dns() handles scenario where CNAME query returns just a CNAME record + # + # ************** EXAMPLE drill OUTPUT ************** + # + # ;; ANSWER SECTION: + # www.duckdns.org. 600 IN CNAME DuckDNSAppELB-570522007.us-west-2.elb.amazonaws.com. + + # Disable SOA check + _TEST_SKIP_SOA_CALL=1 + + PUBLIC_DNS_SERVER=1.0.0.1 + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=false + + run get_auth_dns www.duckdns.org + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ns.*\.awsdns.*\.com' + + # Assert that we found a CNAME and use drill NS + assert_line --partial 'Using drill CNAME' + assert_line --partial 'Using drill NS' + + # Check all Authoritive DNS servers are returned if requested + CHECK_ALL_AUTH_DNS=true + run get_auth_dns www.duckdns.org + assert_output --regexp 'set primary_ns = ns.*\.awsdns.*\.com' + + # Check that we also check the public DNS server if requested + CHECK_PUBLIC_DNS_SERVER=true + run get_auth_dns www.duckdns.org + assert_output --regexp 'set primary_ns = ns.*\.awsdns.*\.com 1\.0\.0\.1' +} + + +@test "Check get_auth_dns using drill CNAME (duckdns)" { + if [ ! -f /usr/bin/drill ]; then + # Can't find drill package for centos8 + skip "Drill not installed on this system" + fi + + # Test that get_auth_dns() handles scenario where CNAME query returns authority section containing NS records + # + # ************** EXAMPLE drill OUTPUT ************** + # + # ;; ANSWER SECTION: + # www.duckdns.org. 600 IN CNAME DuckDNSAppELB-570522007.us-west-2.elb.amazonaws.com. + # + # ;; AUTHORITY SECTION: + # duckdns.org. 600 IN NS ns1.duckdns.org. + # duckdns.org. 600 IN NS ns2.duckdns.org. + # duckdns.org. 600 IN NS ns3.duckdns.org. + # + # ;; ADDITIONAL SECTION: + # ns1.duckdns.org. 600 IN A 54.187.92.222 + # ns2.duckdns.org. 600 IN A 54.191.117.119 + # ns3.duckdns.org. 600 IN A 52.26.169.94 + + # Disable SOA check + _TEST_SKIP_SOA_CALL=1 + + PUBLIC_DNS_SERVER=ns1.duckdns.org + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=false + + run get_auth_dns www.duckdns.org + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org' + + # Assert that we found a CNAME but didn't use drill NS + assert_line --partial 'Using drill CNAME' + refute_line --partial 'Using drill NS' + + # Check all Authoritive DNS servers are returned if requested + CHECK_ALL_AUTH_DNS=true + run get_auth_dns www.duckdns.org + assert_output --regexp 'set primary_ns = ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org ns[1-4]+\.duckdns\.org' +} diff --git a/test/u3-mktemp-template.bats b/test/u3-mktemp-template.bats new file mode 100644 index 0000000..2cb8040 --- /dev/null +++ b/test/u3-mktemp-template.bats @@ -0,0 +1,11 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +@test "Check mktemp -t getssl.XXXXXX works on all platforms" { + run mktemp -t getssl.XXXXXX + assert_success +} diff --git a/test/u4-create-csr-and-ifs.bats b/test/u4-create-csr-and-ifs.bats new file mode 100644 index 0000000..344a536 --- /dev/null +++ b/test/u4-create-csr-and-ifs.bats @@ -0,0 +1,54 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + . /getssl/getssl --source + find_dns_utils + _RUNNING_TEST=1 + _USE_DEBUG=0 +} + + +@test "Check create_csr works for multiple domains" { + # Create a key + csr_key=$(mktemp -t getssl.key.XXXXXX) || error_exit "mktemp failed" + csr_file=$(mktemp -t getssl.csr.XXXXXX) || error_exit "mktemp failed" + SANS="a.getssl.test,b.getssl.test" + SANLIST="subjectAltName=DNS:${SANS//[, ]/,DNS:}" + create_key "$ACCOUNT_KEY_TYPE" "$csr_key" "$ACCOUNT_KEY_LENGTH" + + # Create an initial csr + run create_csr $csr_file $csr_key + assert_success + + # Check that calling create_csr with the same SANSLIST doesn't re-create the csr + run create_csr $csr_file $csr_key + assert_success + refute_line --partial "does not have the same domains" + + # Check that calling create_csr with a different SANSLIST does re-create the csr + SANS="a.getssl.test,b.getssl.test,c.getssl.test" + SANLIST="subjectAltName=DNS:${SANS//[, ]/,DNS:}" + run create_csr $csr_file $csr_key + assert_success + assert_line --partial "does not contain" + + # Check that calling create_csr with the same SANSLIST, but in a different order does not re-create the csr + SANS="c.getssl.test,a.getssl.test,b.getssl.test" + SANLIST="subjectAltName=DNS:${SANS//[, ]/,DNS:}" + run create_csr $csr_file $csr_key + assert_success + refute_line --partial "does not contain" + + # Check that removing a domain from the SANSLIST causes the csr to be re-created + SANS="c.getssl.test,a.getssl.test" + SANLIST="subjectAltName=DNS:${SANS//[, ]/,DNS:}" + run create_csr $csr_file $csr_key + assert_success + assert_line --partial "does not have the same domains as the config" +} diff --git a/test/u5-test-get_auth_dns-no-root-servers.bats b/test/u5-test-get_auth_dns-no-root-servers.bats new file mode 100644 index 0000000..b88fd41 --- /dev/null +++ b/test/u5-test-get_auth_dns-no-root-servers.bats @@ -0,0 +1,96 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +# This is run for every test +setup() { + cp /etc/resolv.conf /etc/resolv.conf.getssl + cat <<- EOF > /etc/resolv.conf +nameserver 8.8.8.8 +options ndots:0 +EOF + + for app in drill host nslookup + do + if [ -f /usr/bin/${app} ]; then + mv /usr/bin/${app} /usr/bin/${app}.getssl.bak + fi + done + + . /getssl/getssl --source + find_dns_utils + _RUNNING_TEST=1 + _USE_DEBUG=0 +} + + +teardown() { + cat /etc/resolv.conf.getssl > /etc/resolv.conf + for app in drill host nslookup + do + if [ -f /usr/bin/${app}.getssl.bak ]; then + mv /usr/bin/${app}.getssl.bak /usr/bin/${app} + fi + done +} + + +@test "Check get_auth_dns doesn't include root servers (dig NS)" { + # Test that get_auth_dns() handles scenario where NS query returns root servers + # Issue #617 + # + # Log output was: + # Verifying example.com + # checking DNS at h.root-servers.net for example.com. Attempt 1/100 gave wrong result, waiting 10 secs before checking again + # ... (retried until max attempts then failed) + + # Disable SOA and CNAME check + _TEST_SKIP_CNAME_CALL=1 + _TEST_SKIP_SOA_CALL=1 + + PUBLIC_DNS_SERVER= + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=true + + run get_auth_dns example.com + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ' + # Assert that we had to use dig NS + assert_line --partial 'Using dig NS' + + # Check we didn't include any root servers + refute_line --partial 'root-servers.net' +} + + +@test "Check get_auth_dns doesn't include root servers (dig SOA)" { + # Test that get_auth_dns() handles scenario where NS query returns root servers + # Issue #617 + # + # Log output was: + # Verifying example.com + # checking DNS at h.root-servers.net for example.com. Attempt 1/100 gave wrong result, waiting 10 secs before checking again + # ... (retried until max attempts then failed) + + # Disable CNAME check, ensure SOA check is enabled + _TEST_SKIP_CNAME_CALL=1 + _TEST_SKIP_SOA_CALL=0 + + PUBLIC_DNS_SERVER= + CHECK_PUBLIC_DNS_SERVER=false + CHECK_ALL_AUTH_DNS=true + + run get_auth_dns example.com + + # Assert that we've found the primary_ns server + assert_output --regexp 'set primary_ns = ' + # Assert that we had to use dig SOA + assert_line --partial 'Using dig SOA' + + # Check we didn't include any root servers + refute_line --partial 'root-servers.net' +}