diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 116d2a5..135ea05 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -12,6 +12,7 @@ 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 '....' @@ -21,8 +22,9 @@ Steps to reproduce the 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)] + +- 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/workflows/run-all-tests.yml b/.github/workflows/run-all-tests.yml new file mode 100644 index 0000000..08fb41b --- /dev/null +++ b/.github/workflows/run-all-tests.yml @@ -0,0 +1,82 @@ +name: Run all tests +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-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-centos7-staging: + 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-staging + 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 + test-ubuntu-staging: + needs: test-centos7-staging + 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-staging 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/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 8549bda..3882b50 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,87 @@ +![Run all tests](https://github.com/srvrco/getssl/workflows/Run%20all%20tests/badge.svg) ![shellcheck](https://github.com/srvrco/getssl/workflows/shellcheck/badge.svg) # getssl -Obtain SSL certificates from the letsencrypt.org ACME server. Suitable for automating the process on remote servers. + +Obtain SSL certificates from the letsencrypt.org ACME server. Suitable +for automating the process on remote servers. + +## Upgrading from ACME v01 to ACME v02 + +Find the following line in your `getssl.cfg` file: + +```CA="https://acme-v01.api.letsencrypt.org"``` + +and change it to: + +```CA="https://acme-v02.api.letsencrypt.org"``` ## Features -* **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. + +* **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 ## 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 getssl ver. 2.02 Obtain SSL certificates from the letsencrypt.org ACME server @@ -63,27 +106,36 @@ Options: 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 Verifying yourdomain.com @@ -98,35 +150,46 @@ 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 I use the following cron -``` + +```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: -``` +```getssl # 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" @@ -143,17 +206,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: -``` +```getssl # 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" @@ -195,24 +260,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 -``` +```getssl # 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" @@ -231,6 +304,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 @@ -252,23 +326,32 @@ 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). ## 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/dns_add_challtestsrv b/dns_scripts/dns_add_challtestsrv old mode 100644 new mode 100755 index 601bcfc..98444b5 --- a/dns_scripts/dns_add_challtestsrv +++ b/dns_scripts/dns_add_challtestsrv @@ -4,4 +4,4 @@ fulldomain="${1}" token="${2}" -curl -X POST -d "{\"host\":\"_acme-challenge.${fulldomain}.\", \"value\": \"${token}\"}" http://10.30.50.3:8055/set-txt +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_duckdns b/dns_scripts/dns_add_duckdns new file mode 100755 index 0000000..ef40efe --- /dev/null +++ b/dns_scripts/dns_add_duckdns @@ -0,0 +1,19 @@ +#!/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" + +response=$(curl --silent "https://www.duckdns.org/update?domains=${domain}&token=${token}&txt=${txtvalue}") +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_nsupdate b/dns_scripts/dns_add_nsupdate index 13b0fc9..98f5e7f 100755 --- a/dns_scripts/dns_add_nsupdate +++ b/dns_scripts/dns_add_nsupdate @@ -14,26 +14,28 @@ 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 +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 index a972dfa..f2011bf 100755 --- a/dns_scripts/dns_route53.py +++ b/dns_scripts/dns_route53.py @@ -31,7 +31,7 @@ for zone in response['HostedZones']: if not zone['Config']['PrivateZone']: zone_list[zone['Name']] = zone['Id'] -for key in sorted(zone_list.iterkeys(), key=len, reverse=True): +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] diff --git a/docker-compose.yml b/docker-compose.yml index f4b3567..ec5c24a 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,12 +3,10 @@ services: pebble: image: letsencrypt/pebble:latest # TODO enable -strict - command: pebble -config /test/config/pebble-config.json -dnsserver 10.30.50.3:8053 + 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" - # don't reuse authorizations (breaks testing force renew) - PEBBLE_AUTHZREUSE: 0 ports: - 14000:14000 # HTTPS ACME API - 15000:15000 # HTTPS Management API @@ -17,22 +15,13 @@ services: ipv4_address: 10.30.50.2 challtestsrv: image: letsencrypt/pebble-challtestsrv:latest - command: pebble-challtestsrv -defaultIPv6 "" -defaultIPv4 10.30.50.3 + 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 - getssl: - build: - context: . - dockerfile: test/Dockerfile-ubuntu - container_name: getssl - volumes: - - .:/getssl - networks: - acmenet: - ipv4_address: 10.30.50.4 + networks: acmenet: diff --git a/getssl b/getssl index 349e60e..a3f5eb9 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) @@ -196,10 +198,41 @@ # 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 # ---------------------------------------------------------------------------------------- PROGNAME=${0##*/} -VERSION="2.15" +PROGDIR="$(cd "$(dirname "$0")" || exit; pwd -P;)" +VERSION="2.26" # defaults ACCOUNT_KEY_LENGTH=4096 @@ -215,7 +248,7 @@ CSR_SUBJECT="/" CURL_USERAGENT="${PROGNAME}/${VERSION}" DEACTIVATE_AUTH="false" DEFAULT_REVOKE_CA="https://acme-v02.api.letsencrypt.org" -DNS_EXTRA_WAIT="" +DNS_EXTRA_WAIT=60 DNS_WAIT=10 DOMAIN_KEY_LENGTH=4096 DUAL_RSA_ECDSA="false" @@ -237,7 +270,7 @@ 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") _CHECK_ALL=0 _CREATE_CONFIG=0 _FORCE_RENEW=0 @@ -280,16 +313,90 @@ cert_archive() { # Archive certificate file by copying files to dated archive d 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" + info "sending request to ACME server saying we're ready for challenge" # check response from our request to perform challenge if [[ $API -eq 1 ]]; then send_signed_request "$uri" "{\"resource\": \"challenge\", \"keyAuthorization\": \"$keyauthorization\"}" + if [[ -n "$code" ]] && [[ ! "$code" == '202' ]] ; then error_exit "$domain:Challenge error: $code" fi @@ -303,7 +410,7 @@ check_challenge_completion() { # checks with the ACME server if our challenge is # loop "forever" to keep checking for a response from the ACME server. while true ; do - debug "checking if challenge is complete" + info "checking if challenge is complete" if [[ $API -eq 1 ]]; then if ! get_cr "$uri" ; then error_exit "$domain:Verify error:$code" @@ -322,7 +429,7 @@ 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") error_exit "$domain:Verify error:$err_detail" fi @@ -330,7 +437,8 @@ check_challenge_completion() { # checks with the ACME server if our challenge is if [[ "$status" == "pending" ]] ; 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 @@ -414,30 +522,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}" |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 + found_ip=false + if [[ -n "$HAS_DIG_OR_DRILL" ]]; then + debug "DNS lookup using $HAS_DIG_OR_DRILL ${d}" + if [[ "$($HAS_DIG_OR_DRILL -t SOA "${d}" |grep -c -i "^${d}")" -ge 1 ]]; then + found_ip=true + elif [[ "$($HAS_DIG_OR_DRILL -t A "${d}"|grep -c -i "^${d}")" -ge 1 ]]; then + found_ip=true + elif [[ "$($HAS_DIG_OR_DRILL -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 "${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 -query=AAAA "${d}"|grep -c -i "^${d}.*has AAAA address")" -ge 1 ]]; then + debug "found IPv6 record for ${d}" + found_ip=true + elif [[ "$(nslookup "${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 @@ -522,7 +643,11 @@ clean_up() { # Perform pre-exit housekeeping shopt -u nullglob fi if [[ -n "$DOMAIN_DIR" ]]; then - rm -rf "${TEMP_DIR:?}" + 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 [[ -n "$TEMP_UPGRADE_FILE" ]] && [[ -f "$TEMP_UPGRADE_FILE" ]]; then rm -f "$TEMP_UPGRADE_FILE" @@ -533,13 +658,17 @@ 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 @@ -573,7 +702,7 @@ copy_file_to_location() { # copies a file, using scp, sftp or ftp if required. user $ftpuser $ftppass cd $ftpdirn lcd $fromdir - put $fromfile + put ./$fromfile _EOF elif [[ "${to:0:5}" == "sftp:" ]] ; then debug "using sftp to copy the file from $from" @@ -585,13 +714,28 @@ 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") + 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}" else if ! mkdir -p "$(dirname "$to")" ; then error_exit "cannot create ACL directory $(basename "$to")" @@ -687,13 +831,62 @@ create_key() { # create a domain key (if it doesn't already exist) 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") + + 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") + # find array position (This is O(n2) but that doubt we'll see performance issues) + dn=0 + for d in $alldomains; do + # Convert domain to lowercase as response from server will be in lowercase + d=$(echo "$d" | tr "[:upper:]" "[:lower:]") + if [ "$d" == "$authdomain" ]; then + debug "Saving authorization response for $authdomain for domain alldomains[$dn]" + 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 @@ -729,304 +922,631 @@ 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 CNAME "$gad_d"| grep "^$gad_d") - else - res=$($DNS_CHECK_FUNC CNAME "$gad_d" "@$gad_s"| grep "^$gad_d") - fi - if [[ -n "$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 NS "$gad_d"| grep "^$gad_d") - else - res=$($DNS_CHECK_FUNC NS "$gad_d" "@$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)" ]]; then + debug "HAS DIG_OR_DRILL=dig" + HAS_DIG_OR_DRILL="dig" 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") +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 - res=$(nslookup -debug=1 -type=soa -type=ns "$gad_d" "${gad_s}") + DOMAIN_ACL="${ACL[$dn]}" 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 + # 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" - 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" + # 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 else - primary_ns=$(echo "$all_auth_dns_servers" | awk '{print $1}') + response=${AuthLinkResponse[$dn]} + responseHeaders=${AuthLinkResponseHeader[$dn]} + response_status=$(json_get "$response" status) 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" - 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') - debug "certdata location = $CertData" - 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" + 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" + # 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 - # 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 + keyauthorization="$token.$thumbprint" + debug keyauthorization "$keyauthorization" - # 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 - send_signed_request "$FinalizeLink" "{\"csr\": \"$der\"}" "needbase64" - debug "order link was $OrderLink" - send_signed_request "$OrderLink" "" - CertData=$(json_get "$response" "certificate") - debug "CertData is at $CertData" - send_signed_request "$CertData" "" "" "$FULL_CHAIN" - info "Full certificate saved in $FULL_CHAIN" - awk -v CERT_FILE="$CERT_FILE" -v CA_CERT="$CA_CERT" 'BEGIN {outfile=CERT_FILE} split_after==1 {outfile=CA_CERT;split_after=0} /-----END CERTIFICATE-----/ {split_after=1} {print > outfile}' "$FULL_CHAIN" - info "Certificate saved in $CERT_FILE" - fi -} + #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" -get_cr() { # get curl response - url="$1" - debug url "$url" - response=$(curl --user-agent "$CURL_USERAGENT" --silent "$url") - ret=$? - debug response "$response" - code=$(json_get "$response" status) - debug code "$code" - debug "get_cr return code $ret" - return $ret -} + # shellcheck disable=SC2018,SC2019 + lower_d=$(echo "$d" | tr A-Z a-z) + debug "adding dns via command: $DNS_ADD_COMMAND $lower_d $auth_key" + if ! eval "$DNS_ADD_COMMAND" "$lower_d" "$auth_key" ; then + error_exit "DNS_ADD_COMMAND failed for domain $d" + fi -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" -} + # 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" -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) + # make a directory to hold pending dns-challenges + if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then + mkdir "$TEMP_DIR/dns_verify" + fi - 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 + # 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 + 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" + # 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 + # shellcheck 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 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 - 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" + # increment domain-counter + ((dn++)) fi - thumbprint="$(printf "%s" "$jwk" | openssl dgst -sha256 -binary | urlbase64)" - debug "jwk alg = $jwkalg" - debug "jwk = $jwk" - debug "thumbprint $thumbprint" -} +done # end of ... loop through domains for cert ( from SANS list) -graceful_exit() { # normal exit function. - clean_up - exit -} +# 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" -help_message() { # print out the help message - cat <<- _EOF_ - $PROGNAME ver. $VERSION - Obtain SSL certificates from the letsencrypt.org ACME server + # 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 + debug "$DNS_CHECK_FUNC" TXT "_acme-challenge.${d}" "@${ns}" + check_result=$($DNS_CHECK_FUNC TXT "_acme-challenge.${d}" "@${ns}" \ + | grep 'IN\WTXT'|awk -F'"' '{ print $2}') + elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then + check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${d}" "${ns}" \ + | grep 'descriptive text'|awk -F'"' '{ print $2}') + else + check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ + | grep 'text ='|awk -F'"' '{ print $2}') + fi + debug "expecting $auth_key" + debug "${ns} gave ... $check_result" - $(usage) + 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 - Options: - -a, --all Check all certificates - -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 - -q, --quiet Quiet mode (only outputs on error, success of new cert, or getssl was upgraded) - -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 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" + 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 - _EOF_ -} + # 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" -hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char -# printf -- "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" - echo -e -n "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" -} + check_challenge_completion "$uri" "$d" "$keyauthorization" -info() { # write out info as long as the quiet flag has not been set. - if [[ ${_QUIET} -eq 0 ]]; then - echo "$@" - fi + debug "remove DNS entry" + # shellcheck disable=SC2018,SC2019 + lower_d=$(echo "$d" | tr A-Z a-z) + eval "$DNS_DEL_COMMAND" "$lower_d" "$auth_key" + # remove $dnsfile after each loop. + rm -f "$dnsfile" + fi + done +fi +# end of ... perform validation if via DNS challenge +#end of varify each domain. } -json_awk() { # AWK json converter used for API2 - needs tidying up ;) -# shellcheck disable=SC2086 -echo $1 | awk ' -{ - tokenize($0) # while(get_token()) {print TOKEN} - if (0 == parse()) { - apply(JPATHS, NJPATHS) - } -} +get_auth_dns() { # get the authoritative dns server for a domain (sets primary_ns ) + orig_gad_d="$1" # domain name + gad_s="$PUBLIC_DNS_SERVER" # start with PUBLIC_DNS_SERVER -function apply (ary,size,i) { - for (i=1; i/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 -function get_token() { - TOKEN = TOKENS[++ITOKENS] # for internal tokenize() - return ITOKENS < NTOKENS -} + if [[ -n "$HAS_DIG_OR_DRILL" ]]; then + gad_d="$orig_gad_d" + debug Using "$HAS_DIG_OR_DRILL SOA +trace +nocomments $gad_d @$gad_s" to find primary nameserver + # Use SOA +trace to find the name server + if [[ -z "$gad_s" ]]; then + res=$($HAS_DIG_OR_DRILL SOA +trace +nocomments "$gad_d" 2>/dev/null | grep "IN\WNS\W" | tail -1) + else + res=$($HAS_DIG_OR_DRILL SOA +trace +nocomments "$gad_d" "@$gad_s" 2>/dev/null | grep "IN\WNS\W" | tail -1) + fi -function parse_array(a1,idx,ary,ret) { - idx=0 - ary="" - get_token() - if (TOKEN != "]") { + # fallback to existing code + if [[ -z "$res" ]]; then + debug Checking for CNAME using "$HAS_DIG_OR_DRILL CNAME $gad_d @$gad_s" + if [[ -z "$gad_s" ]]; then #checking for CNAMEs (need grep as dig 9.11 sometimes returns everything not just CNAME entries) + res=$($HAS_DIG_OR_DRILL CNAME "$gad_d"| grep "^$gad_d" | grep CNAME) + else + res=$($HAS_DIG_OR_DRILL CNAME "$gad_d" "@$gad_s"| grep "^$gad_d" | grep CNAME) + fi + if [[ -n "$res" ]]; then # domain is a CNAME so get main domain + gad_d=$(echo "$res"| awk '{print $5}' |sed 's/\.$//g') + debug Domain is a CNAME, actual domain is "$gad_d" + fi + # If gad_d is an A record then this returns the SOA for the root domain, e.g. without the www + # dig NS ubuntu.getssl.text + # > getssl.test. IN SOA ns1.duckdns.org + # If gad_d is a CNAME record then this returns the NS for the domain pointed to by $gad_d + # dig NS www.getssl.text + # > www.getssl.test. IN CNAME getssl.test + # > getssl.test. IN NS ns1.duckdns.org + debug Using "$HAS_DIG_OR_DRILL NS $gad_d @$gad_s" to find primary nameserver + if [[ -z "$gad_s" ]]; then + res=$($HAS_DIG_OR_DRILL NS "$gad_d"| grep -E "IN\W(NS|SOA)\W" | tail -1) + else + res=$($HAS_DIG_OR_DRILL NS "$gad_d" "@$gad_s"| grep -E "IN\W(NS|SOA)\W" | tail -1) + fi + fi + if [[ -n "$res" ]]; then + all_auth_dns_servers=$(echo "$res" | awk '$4 ~ "NS" {print $5}' | 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 + return + fi + fi + + 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 -t NS "$gad_d"| grep "name server") + else + res=$(host -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 + return + fi + fi + + if [[ "$HAS_NSLOOKUP" == "true" ]]; then + gad_d="$orig_gad_d" + debug Using "nslookup -debug -type=soa -type=ns $gad_d $gad_s" to find primary name server + res=$(nslookup -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 -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 -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" + info "Full certificate saved in $gc_fullchain" + 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" + 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" + 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. + clean_up + exit +} + +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 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 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 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" + + _EOF_ +} + +hex2bin() { # Remove spaces, add leading zero, escape as hex string ensuring no trailing new line char +# printf -- "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" + echo -e -n "$(cat | os_esed -e 's/[[:space:]]//g' -e 's/^(.(.{2})*)$/0\1/' -e 's/(.{2})/\\x\1/g')" +} + +info() { # write out info as long as the quiet flag has not been set. + if [[ ${_QUIET} -eq 0 ]]; then + echo "$@" + fi +} + +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/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" + 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 } @@ -1561,6 +2053,18 @@ 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" @@ -1582,6 +2086,7 @@ write_domain_template() { # write out a template file for a domain. # This server issues full certificates, however has rate limits #CA="https://acme-v01.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 @@ -1596,12 +2101,19 @@ write_domain_template() { # write out a template file for a domain. # 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. + # 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. #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') + # 'ftp:ftpuserid:ftppassword:${DOMAIN}:/web/.well-known/acme-challenge' + # 'davs:davsuserid:davspassword:{DOMAIN}:443:/web/.well-known/acme-challenge') - #Set USE_SINGLE_ACL="true" to use a single ACL for all checks + # 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" # Location for all your certs, these can either be on the server (full path name) @@ -1615,6 +2127,9 @@ write_domain_template() { # write out a template file for a domain. # The command needed to reload apache / nginx or whatever you use #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 @@ -1628,27 +2143,38 @@ write_domain_template() { # write out a template file for a domain. 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-v02.api.letsencrypt.org/directory" + 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="$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 #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, @@ -1707,743 +2233,511 @@ while [[ -n ${1+defined} ]]; do REVOKE_KEY="$1" shift REVOKE_CA="$1" ;; - -u | --upgrade) - _UPGRADE=1 ;; - -U | --nocheck) - _UPGRADE_CHECK=0 ;; - -w) - shift; WORKING_DIR="$1" ;; - -*) - usage - error_exit "Unknown option $1" ;; - *) - if [[ -n $DOMAIN ]]; then - error_exit "invalid command line $DOMAIN - it appears to contain more than one domain" - fi - DOMAIN="$1" - if [[ -z $DOMAIN ]]; then - error_exit "invalid command line - it appears to contain a null variable" - fi ;; - esac - shift -done - -# Main logic -############ - -# Get the current OS, so the correct functions can be used for that OS. (sets the variable os) -get_os - -# check if "recent" version of bash. -#if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -lt 42 ]]; then -# info "this script is designed for bash v4.2 or later - earlier version may give errors" -#fi - -#check if required applications are included - -requires which -requires openssl -requires curl -requires nslookup drill dig host DNS_CHECK_FUNC -requires awk -requires tr -requires date -requires grep -requires sed -requires sort -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 -fi - -# Revoke a certificate if requested -if [[ $_REVOKE -eq 1 ]]; then - if [[ -z $REVOKE_CA ]]; then - CA=$DEFAULT_REVOKE_CA - elif [[ "$REVOKE_CA" == "-d" ]]; then - _USE_DEBUG=1 - CA=$DEFAULT_REVOKE_CA - else - CA=$REVOKE_CA - fi - URL_revoke=$(curl --user-agent "$CURL_USERAGENT" "${CA}/directory" 2>/dev/null | grep "revoke-cert" | awk -F'"' '{print $4}') - revoke_certificate - graceful_exit -fi - -# get latest agreement from CA (as default) -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 - help_message - graceful_exit -fi - -# if the "working directory" doesn't exist, then create it. -if [[ ! -d "$WORKING_DIR" ]]; then - debug "Making working directory - $WORKING_DIR" - mkdir -p "$WORKING_DIR" -fi - -# read any variables from config in working directory -if [[ -s "$WORKING_DIR/getssl.cfg" ]]; then - debug "reading config from $WORKING_DIR/getssl.cfg" - # shellcheck source=/dev/null - . "$WORKING_DIR/getssl.cfg" -fi - -# Define defaults for variables not set in the main config. -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 - CSR_SUBJECT="//" -fi - -# Set the OPENSSL_CONF environment variable so openssl knows which config to use -export OPENSSL_CONF=$SSLCONF - -# if "-a" option then check other parameters and create run for each domain. -if [[ ${_CHECK_ALL} -eq 1 ]]; then - info "Check all certificates" - - if [[ ${_CREATE_CONFIG} -eq 1 ]]; then - error_exit "cannot combine -c|--create with -a|--all" - fi - - if [[ ${_FORCE_RENEW} -eq 1 ]]; then - error_exit "cannot combine -f|--force with -a|--all because of rate limits" - fi - - if [[ ! -d "$DOMAIN_STORAGE" ]]; then - error_exit "DOMAIN_STORAGE not found - $DOMAIN_STORAGE" - fi - - for dir in "${DOMAIN_STORAGE}"/*; do - if [[ -d "$dir" ]]; then - debug "Checking $dir" - cmd="$0 -U" # No update checks when calling recursively - if [[ ${_USE_DEBUG} -eq 1 ]]; then - cmd="$cmd -d" - fi - 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")" - debug "CMD: $cmd" - eval "$cmd" - fi - fi - done - - graceful_exit -fi -# end of "-a" option (looping through all domains) - -# if "-c|--create" option used, then create config files. -if [[ ${_CREATE_CONFIG} -eq 1 ]]; then - # If main config file does not exists then create it. - if [[ ! -s "$WORKING_DIR/getssl.cfg" ]]; then - info "creating main config file $WORKING_DIR/getssl.cfg" - if [[ ! -s "$SSLCONF" ]]; then - SSLCONF="$WORKING_DIR/openssl.cnf" - write_openssl_conf "$SSLCONF" - fi - write_getssl_template "$WORKING_DIR/getssl.cfg" - fi - # If domain and domain config don't exist then create them. - if [[ ! -d "$DOMAIN_DIR" ]]; then - info "Making domain directory - $DOMAIN_DIR" - mkdir -p "$DOMAIN_DIR" - fi - if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then - info "domain config already exists $DOMAIN_DIR/getssl.cfg" - else - 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 - 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-) - EX_SANS=${EX_SANS//$'\n'/','} - fi - write_domain_template "$DOMAIN_DIR/getssl.cfg" - fi - TEMP_DIR="$DOMAIN_DIR/tmp" - # end of "-c|--create" option, so exit - graceful_exit -fi -# end of "-c|--create" option to create config file. - -# if domain directory doesn't exist, then create it. -if [[ ! -d "$DOMAIN_DIR" ]]; then - debug "Making working directory - $DOMAIN_DIR" - mkdir -p "$DOMAIN_DIR" -fi - -# define a temporary directory, and if it doesn't exist, create it. -TEMP_DIR="$DOMAIN_DIR/tmp" -if [[ ! -d "${TEMP_DIR}" ]]; then - debug "Making temp directory - ${TEMP_DIR}" - mkdir -p "${TEMP_DIR}" -fi - -# read any variables from config in domain directory -if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then - debug "reading config from $DOMAIN_DIR/getssl.cfg" - # shellcheck source=/dev/null - . "$DOMAIN_DIR/getssl.cfg" -fi - -# from SERVER_TYPE set REMOTE_PORT and REMOTE_EXTRA -set_server_type - -# check config for typical errors. -check_config - -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 --user-agent "$CURL_USERAGENT" "${CA}" 2>/dev/null) -debug "ca_all_loc from ${CA} 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}') -if [[ -z "$URL_new_reg" ]] && [[ -z "$URL_newAccount" ]]; then - ca_all_loc=$(curl --user-agent "$CURL_USERAGENT" "${CA}/directory" 2>/dev/null) - debug "ca_all_loc from ${CA}/directory 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}') -fi - -if [[ -n "$URL_new_reg" ]]; then - API=1 -elif [[ -n "$URL_newAccount" ]]; then - API=2 -else - info "unknown API version" - graceful_exit -fi -debug "Using API v$API" - -# 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 - debug "getting certificate for $DOMAIN from remote server" - # shellcheck disable=SC2086 - EX_CERT=$(echo \ - | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 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 - CERT_LOCAL="" - fi - CERT_REMOTE=$(echo "$EX_CERT" | openssl x509 -noout -fingerprint 2>/dev/null) - if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then - debug "certificate on server is same as the local cert" - 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; }' \ - | sort -u | grep "^$DOMAIN\$") - if [[ "$EX_CERT_DOMAIN" == "$DOMAIN" ]]; then - # check renew-date on ex_cert and compare to local ( if local exists) - enddate_ex=$(echo "$EX_CERT" | openssl x509 -noout -enddate 2>/dev/null| cut -d= -f 2-) - enddate_ex_s=$(date_epoc "$enddate_ex") - debug "external cert has enddate $enddate_ex ( $enddate_ex_s ) " - if [[ -s "$CERT_FILE" ]]; then # if local exists - enddate_lc=$(openssl x509 -noout -enddate < "$CERT_FILE" 2>/dev/null| cut -d= -f 2-) - enddate_lc_s=$(date_epoc "$enddate_lc") - debug "local cert has enddate $enddate_lc ( $enddate_lc_s ) " - else - enddate_lc_s=0 - debug "local cert doesn't exist" - fi - if [[ "$enddate_ex_s" -eq "$enddate_lc_s" ]]; then - debug "certificates expire at the same time" - elif [[ "$enddate_ex_s" -gt "$enddate_lc_s" ]]; then - # remote has longer to expiry date than local copy. - debug "remote cert has longer to run than local cert - ignoring" - else - info "${DOMAIN}: remote cert expires sooner than local, attempting to upload from local" - 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" - cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem" - copy_file_to_location "full pem" \ - "$TEMP_DIR/${DOMAIN}_chain.pem" \ - "$DOMAIN_CHAIN_LOCATION" - 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" \ - "$DOMAIN_KEY_CERT_LOCATION" - cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" - copy_file_to_location "full pem" \ - "$TEMP_DIR/${DOMAIN}.pem" \ - "$DOMAIN_PEM_LOCATION" - reload_service - fi - else - info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate" + -u | --upgrade) + _UPGRADE=1 ;; + -U | --nocheck) + _UPGRADE_CHECK=0 ;; + -i | --install) + _CERT_INSTALL=1 ;; + -w) + shift; WORKING_DIR="$1" ;; + -*) + usage + error_exit "Unknown option $1" ;; + *) + if [[ -n $DOMAIN ]]; then + error_exit "invalid command line $DOMAIN - it appears to contain more than one domain" fi - fi - else - info "${DOMAIN}: no certificate obtained from host" - fi - # end of .... if obtained a cert -fi -# end of .... check_remote is true then connect and obtain the current certificate + DOMAIN="$1" + if [[ -z $DOMAIN ]]; then + error_exit "invalid command line - it appears to contain a null variable" + fi ;; + esac + shift +done -# if there is an existing certificate file, check details. -if [[ -s "$CERT_FILE" ]]; then - debug "certificate $CERT_FILE exists" - enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-) - debug "local cert is valid until $enddate" - if [[ "$enddate" != "-" ]]; 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-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 - fi - else - debug "${DOMAIN}: certificate needs renewal" - fi - fi -fi -# end of .... if there is an existing certificate file, check details. +# Main logic +############ -if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then - errmsg="$DOMAIN due for renewal," - errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config" - error_exit "$errmsg" -fi +# Get the current OS, so the correct functions can be used for that OS. (sets the variable os) +get_os -# create account key if it doesn't exist. -if [[ -s "$ACCOUNT_KEY" ]]; then - debug "Account key exists at $ACCOUNT_KEY skipping generation" -else - info "creating account key $ACCOUNT_KEY" - create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" +# check if "recent" version of bash. +#if [[ "${BASH_VERSINFO[0]}${BASH_VERSINFO[1]}" -lt 42 ]]; then +# info "this script is designed for bash v4.2 or later - earlier version may give errors" +#fi + +#check if required applications are included + +requires which +requires openssl +requires curl +requires dig nslookup drill host DNS_CHECK_FUNC +requires dirname +requires awk +requires tr +requires date +requires grep +requires sed +requires sort +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 fi -# 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" - fi - if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then - rm -f "$DOMAIN_DIR/${DOMAIN}.ecs.key" +# Revoke a certificate if requested +if [[ $_REVOKE -eq 1 ]]; then + if [[ -z $REVOKE_CA ]]; then + CA=$DEFAULT_REVOKE_CA + elif [[ "$REVOKE_CA" == "-d" ]]; then + _USE_DEBUG=1 + CA=$DEFAULT_REVOKE_CA + else + CA=$REVOKE_CA fi + URL_revoke=$(curl --user-agent "$CURL_USERAGENT" "${CA}/directory" 2>/dev/null | grep "revoke-cert" | awk -F'"' '{print $4}') + revoke_certificate + graceful_exit fi -# create new domain keys if they don't already exist -if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then - create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" -else - create_key "rsa" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" - create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.ec.key" "$DOMAIN_KEY_LENGTH" -fi -# End of creating domain keys. -#create SAN -if [[ -z "$SANS" ]]; then - SANLIST="subjectAltName=DNS:${DOMAIN}" -elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - SANLIST="subjectAltName=DNS:${SANS//,/,DNS:}" -else - SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//,/,DNS:}" +# get latest agreement from CA (as default) +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 + help_message + graceful_exit fi -debug "created SAN list = $SANLIST" -#create CSR's -if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then - create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" -else - create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" - create_csr "$DOMAIN_DIR/${DOMAIN}.ec.csr" "$DOMAIN_DIR/${DOMAIN}.ec.key" +# 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 -# use account key to register with CA -# currently the code registers every time, and gets an "already registered" back if it has been. -get_signing_params "$ACCOUNT_KEY" +# if the "working directory" doesn't exist, then create it. +if [[ ! -d "$WORKING_DIR" ]]; then + debug "Making working directory - $WORKING_DIR" + mkdir -p "$WORKING_DIR" +fi -info "Registering account" -# send the request to the ACME server. -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 +# read any variables from config in working directory +if [[ -s "$WORKING_DIR/getssl.cfg" ]]; then + debug "reading config from $WORKING_DIR/getssl.cfg" + # shellcheck source=/dev/null + . "$WORKING_DIR/getssl.cfg" 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 - 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 ...$responseHeaders ... $(json_get "$response" detail)" +# Define defaults for variables not set in the main config. +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 + CSR_SUBJECT="//" fi -# end of registering account with CA -# verify each domain -info "Verify each domain" +# Set the OPENSSL_CONF environment variable so openssl knows which config to use +export OPENSSL_CONF=$SSLCONF -# loop through domains for cert ( from SANS list) -if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then - alldomains=${SANS//,/ } -else - alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") -fi +# if "-a" option then check other parameters and create run for each domain. +if [[ ${_CHECK_ALL} -eq 1 ]]; then + info "Check all certificates" -if [[ $API -eq 2 ]]; then - 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 "finalise link $FinalizeLink" - 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 -fi + if [[ ${_CREATE_CONFIG} -eq 1 ]]; then + error_exit "cannot combine -c|--create with -a|--all" + 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]}" + if [[ ${_FORCE_RENEW} -eq 1 ]]; then + error_exit "cannot combine -f|--force with -a|--all because of rate limits" + fi + + if [[ ! -d "$DOMAIN_STORAGE" ]]; then + error_exit "DOMAIN_STORAGE not found - $DOMAIN_STORAGE" fi - # 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" + for dir in "${DOMAIN_STORAGE}"/*; do + if [[ -d "$dir" ]]; then + debug "Checking $dir" + cmd="$0 -U" # No update checks when calling recursively + if [[ ${_USE_DEBUG} -eq 1 ]]; then + cmd="$cmd -d" + fi + if [[ ${_QUIET} -eq 1 ]]; then + cmd="$cmd -q" + fi + # 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 + fi + done + + graceful_exit +fi +# end of "-a" option (looping through all domains) - # 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" +# if "-c|--create" option used, then create config files. +if [[ ${_CREATE_CONFIG} -eq 1 ]]; then + # If main config file does not exists then create it. + if [[ ! -s "$WORKING_DIR/getssl.cfg" ]]; then + info "creating main config file $WORKING_DIR/getssl.cfg" + if [[ ! -s "$SSLCONF" ]]; then + SSLCONF="$WORKING_DIR/openssl.cnf" + write_openssl_conf "$SSLCONF" fi + write_getssl_template "$WORKING_DIR/getssl.cfg" + fi + # If domain and domain config don't exist then create them. + if [[ ! -d "$DOMAIN_DIR" ]]; then + info "Making domain directory - $DOMAIN_DIR" + mkdir -p "$DOMAIN_DIR" + fi + if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then + info "domain config already exists $DOMAIN_DIR/getssl.cfg" else - response_status="" + 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 + 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-) + EX_SANS=${EX_SANS//$'\n'/','} + fi + write_domain_template "$DOMAIN_DIR/getssl.cfg" fi + TEMP_DIR="$DOMAIN_DIR/tmp" + # end of "-c|--create" option, so exit + graceful_exit +fi +# end of "-c|--create" option to create config file. - 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 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" - else # APIv2 - send_signed_request "${AuthLink[$dn]}" "" - debug "authlink response = $response" - # get the token from the http-01 component - token=$(json_get "$response" "challenges" "type" "dns-01" "token") - debug token "$token" - # get the uri from the http component - uri=$(json_get "$response" "challenges" "type" "dns-01" "url") - debug uri "$uri" - fi +# if domain directory doesn't exist, then create it. +if [[ ! -d "$DOMAIN_DIR" ]]; then + debug "Making working directory - $DOMAIN_DIR" + mkdir -p "$DOMAIN_DIR" +fi - keyauthorization="$token.$thumbprint" - debug keyauthorization "$keyauthorization" +# define a temporary directory, and if it doesn't exist, create it. +TEMP_DIR="$DOMAIN_DIR/tmp" +if [[ ! -d "${TEMP_DIR}" ]]; then + debug "Making temp directory - ${TEMP_DIR}" + mkdir -p "${TEMP_DIR}" +fi - #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" +# read any variables from config in domain directory +if [[ -s "$DOMAIN_DIR/getssl.cfg" ]]; then + debug "reading config from $DOMAIN_DIR/getssl.cfg" + # shellcheck source=/dev/null + . "$DOMAIN_DIR/getssl.cfg" +fi - 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 +# from SERVER_TYPE set REMOTE_PORT and REMOTE_EXTRA +set_server_type - # 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" +# check what dns utils are installed +find_dns_utils - # make a directory to hold pending dns-challenges - if [[ ! -d "$TEMP_DIR/dns_verify" ]]; then - mkdir "$TEMP_DIR/dns_verify" - fi +# check config for typical errors. +check_config - # 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_ +# if -i|--install install certs, reload and exit +if [ "0${_CERT_INSTALL}" -eq 1 ]; then + cert_install + reload_service + graceful_exit +fi - 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") - debug token "$token" - # get the uri from the http component - uri=$(json_get "$response" "uri" "http-01") - debug uri "$uri" - else # APIv2 - send_signed_request "${AuthLink[$dn]}" "" - debug "authlink response = $response" - # get the token from the http-01 component - token=$(json_get "$response" "challenges" "type" "http-01" "token") - debug token "$token" - # get the uri from the http component - uri=$(json_get "$response" "challenges" "type" "http-01" "url" | head -n1) - debug uri "$uri" - 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 - #create signed authorization key from token. - keyauthorization="$token.$thumbprint" - debug keyauthorization "$keyauthorization" +# Obtain CA resource locations +ca_all_loc=$(curl --user-agent "$CURL_USERAGENT" "${CA}" 2>/dev/null) +debug "ca_all_loc from ${CA} 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}') +if [[ -z "$URL_new_reg" ]] && [[ -z "$URL_newAccount" ]]; then + ca_all_loc=$(curl --user-agent "$CURL_USERAGENT" "${CA}/directory" 2>/dev/null) + debug "ca_all_loc from ${CA}/directory 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}') +fi - # save variable into temporary file - echo -n "$keyauthorization" > "$TEMP_DIR/$token" - chmod 644 "$TEMP_DIR/$token" +if [[ -n "$URL_new_reg" ]]; then + API=1 +elif [[ -n "$URL_newAccount" ]]; then + API=2 +else + info "unknown API version" + graceful_exit +fi +debug "Using API v$API" - # 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" +# 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 - wellknown_url="${CHALLENGE_CHECK_TYPE}://${d}/.well-known/acme-challenge/$token" - debug wellknown_url "$wellknown_url" +# 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 + debug "getting certificate for $DOMAIN from remote server" + # shellcheck disable=SC2086 + EX_CERT=$(echo \ + | openssl s_client -servername "${DOMAIN}" -connect "${DOMAIN}:${REMOTE_PORT}" ${REMOTE_EXTRA} 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 + CERT_LOCAL="" + fi + CERT_REMOTE=$(echo "$EX_CERT" | openssl x509 -noout -fingerprint 2>/dev/null) + if [[ "$CERT_LOCAL" == "$CERT_REMOTE" ]]; then + debug "certificate on server is same as the local cert" + 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; }' \ + | sort -u | grep "^$DOMAIN\$") + if [[ "$EX_CERT_DOMAIN" == "$DOMAIN" ]]; then + # check renew-date on ex_cert and compare to local ( if local exists) + enddate_ex=$(echo "$EX_CERT" | openssl x509 -noout -enddate 2>/dev/null| cut -d= -f 2-) + enddate_ex_s=$(date_epoc "$enddate_ex") + debug "external cert has enddate $enddate_ex ( $enddate_ex_s ) " + if [[ -s "$CERT_FILE" ]]; then # if local exists + enddate_lc=$(openssl x509 -noout -enddate < "$CERT_FILE" 2>/dev/null| cut -d= -f 2-) + enddate_lc_s=$(date_epoc "$enddate_lc") + debug "local cert has enddate $enddate_lc ( $enddate_lc_s ) " + else + enddate_lc_s=0 + debug "local cert doesn't exist" + fi + if [[ "$enddate_ex_s" -eq "$enddate_lc_s" ]]; then + debug "certificates expire at the same time" + elif [[ "$enddate_ex_s" -gt "$enddate_lc_s" ]]; then + # remote has longer to expiry date than local copy. + debug "remote cert has longer to run than local cert - ignoring" + else + info "${DOMAIN}: remote cert expires sooner than local, attempting to upload from local" + 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" + cat "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}_chain.pem" + copy_file_to_location "full pem" \ + "$TEMP_DIR/${DOMAIN}_chain.pem" \ + "$DOMAIN_CHAIN_LOCATION" + 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" \ + "$DOMAIN_KEY_CERT_LOCATION" + cat "$DOMAIN_DIR/${DOMAIN}.key" "$CERT_FILE" "$CA_CERT" > "$TEMP_DIR/${DOMAIN}.pem" + copy_file_to_location "full pem" \ + "$TEMP_DIR/${DOMAIN}.pem" \ + "$DOMAIN_PEM_LOCATION" + reload_service + fi + else + info "${DOMAIN}: Certificate on remote domain does not match, ignoring remote certificate" + fi + fi + else + info "${DOMAIN}: no certificate obtained from host" + fi + # end of .... if obtained a cert +fi +# end of .... check_remote is true then connect and obtain the current certificate - if [[ "$SKIP_HTTP_TOKEN_CHECK" == "true" ]]; then - info "SKIP_HTTP_TOKEN_CHECK=true so not checking that token is working correctly" +# if there is an existing certificate file, check details. +if [[ -s "$CERT_FILE" ]]; then + debug "certificate $CERT_FILE exists" + enddate=$(openssl x509 -in "$CERT_FILE" -noout -enddate 2>/dev/null| cut -d= -f 2-) + debug "local cert is valid until $enddate" + if [[ "$enddate" != "-" ]]; 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-v02.api.letsencrypt.org" ]]; then + debug "upgrading from fake cert to real" 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 + info "${DOMAIN}: certificate is valid for more than $RENEW_ALLOW days (until $enddate)" + # everything is OK, so exit. + graceful_exit fi + else + debug "${DOMAIN}: certificate needs renewal" + fi + fi +fi +# end of .... if there is an existing certificate file, check details. - check_challenge_completion "$uri" "$d" "$keyauthorization" +if [[ ! -t 0 ]] && [[ "$PREVENT_NON_INTERACTIVE_RENEWAL" = "true" ]]; then + errmsg="$DOMAIN due for renewal," + errmsg="${errmsg} but not completed due to PREVENT_NON_INTERACTIVE_RENEWAL=true in config" + error_exit "$errmsg" +fi - 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 - # shellcheck 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 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++)) +# create account key if it doesn't exist. +if [[ -s "$ACCOUNT_KEY" ]]; then + debug "Account key exists at $ACCOUNT_KEY skipping generation" +else + info "creating account key $ACCOUNT_KEY" + create_key "$ACCOUNT_KEY_TYPE" "$ACCOUNT_KEY" "$ACCOUNT_KEY_LENGTH" +fi + +# 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" fi -done # end of ... loop through domains for cert ( from SANS list) + if [[ -s "$DOMAIN_DIR/${DOMAIN}.ec.key" ]]; then + rm -f "$DOMAIN_DIR/${DOMAIN}.ec.key" + fi +fi +# create new domain keys if they don't already exist +if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then + create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" +else + create_key "rsa" "$DOMAIN_DIR/${DOMAIN}.key" "$DOMAIN_KEY_LENGTH" + create_key "${PRIVATE_KEY_ALG}" "$DOMAIN_DIR/${DOMAIN}.ec.key" "$DOMAIN_KEY_LENGTH" +fi +# End of creating domain keys. -# 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" +#create SAN +if [[ -z "$SANS" ]]; then + SANLIST="subjectAltName=DNS:${DOMAIN}" +elif [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then + SANLIST="subjectAltName=DNS:${SANS//,/,DNS:}" +else + SANLIST="subjectAltName=DNS:${DOMAIN},DNS:${SANS//,/,DNS:}" +fi +debug "created SAN list = $SANLIST" - # 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 TXT "_acme-challenge.${d}" "@${ns}" \ - | grep '300 IN TXT'|awk -F'"' '{ print $2}') - elif [[ "$DNS_CHECK_FUNC" == "host" ]]; then - check_result=$($DNS_CHECK_FUNC -t TXT "_acme-challenge.${d}" "${ns}" \ - | grep 'descriptive text'|awk -F'"' '{ print $2}') - else - check_result=$(nslookup -type=txt "_acme-challenge.${d}" "${ns}" \ - | grep 'text ='|awk -F'"' '{ print $2}') - fi - debug "expecting $auth_key" - debug "${ns} gave ... $check_result" +#create CSR's +if [[ "$DUAL_RSA_ECDSA" == "false" ]]; then + create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" +else + create_csr "$DOMAIN_DIR/${DOMAIN}.csr" "$DOMAIN_DIR/${DOMAIN}.key" + create_csr "$DOMAIN_DIR/${DOMAIN}.ec.csr" "$DOMAIN_DIR/${DOMAIN}.ec.key" +fi - 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 +# use account key to register with CA +# currently the code registers every time, and gets an "already registered" back if it has been. +get_signing_params "$ACCOUNT_KEY" - 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" +info "Registering account" +# send the request to the ACME server. +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 - # 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" +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 + 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 ...$responseHeaders ... $(json_get "$response" detail)" +fi +# end of registering account with CA - check_challenge_completion "$uri" "$d" "$keyauthorization" +# verify each domain +info "Verify each domain" - debug "remove DNS entry" - eval "$DNS_DEL_COMMAND" "$d" "$auth_key" - # remove $dnsfile after each loop. - rm -f "$dnsfile" - fi - done +# loop through domains for cert ( from SANS list) +if [[ "$IGNORE_DIRECTORY_DOMAIN" == "true" ]]; then + alldomains=${SANS//,/ } +else + alldomains=$(echo "$DOMAIN,$SANS" | sed "s/,/ /g") fi -# end of ... perform validation if via DNS challenge -#end of varify each domain. -# Verification has been completed for all SANS, so request certificate. +if [[ $API -eq 2 ]]; then + create_order +fi + +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%.*}.ec.crt" \ - "${CA_CERT%.*}.ec.crt" + "${CA_CERT%.*}.ec.crt" \ + "${FULL_CHAIN%.*}.ec.crt" fi # create Archive of new certs and keys. @@ -2452,73 +2746,8 @@ 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) -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.crt" - fi - if [[ -n "$DOMAIN_KEY_LOCATION" ]]; then - copy_file_to_location "ec private key" \ - "$DOMAIN_DIR/${DOMAIN}.ec.key" \ - "${DOMAIN_KEY_LOCATION%.*}.ec.key" - fi - if [[ -n "$CA_CERT_LOCATION" ]]; then - copy_file_to_location "ec CA certificate" \ - "${CA_CERT%.*}.ec.crt" \ - "${CA_CERT_LOCATION%.*}.ec.crt" - fi -fi +cert_install -# 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" # Run reload command to restart apache / nginx or whatever system reload_service diff --git a/test/1-simple-http01-dig.bats b/test/1-simple-http01-dig.bats new file mode 100644 index 0000000..be01f27 --- /dev/null +++ b/test/1-simple-http01-dig.bats @@ -0,0 +1,40 @@ +#! /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/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 HTTP-01 verification (dig)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors +} diff --git a/test/1-simple-http01-nslookup.bats b/test/1-simple-http01-nslookup.bats new file mode 100644 index 0000000..78d175a --- /dev/null +++ b/test/1-simple-http01-nslookup.bats @@ -0,0 +1,40 @@ +#! /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/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 HTTP-01 verification (nslookup)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors +} diff --git a/test/1-simple-http01.bats b/test/1-simple-http01.bats new file mode 100644 index 0000000..a57010b --- /dev/null +++ b/test/1-simple-http01.bats @@ -0,0 +1,35 @@ +#! /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 certificate using HTTP-01 verification (any dns tool)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01.cfg" + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors +} + + +@test "Force renewal of certificate using HTTP-01 (any dns tool)" { + 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/10-mixed-case-staging.bats b/test/10-mixed-case-staging.bats new file mode 100644 index 0000000..ea622f7 --- /dev/null +++ b/test/10-mixed-case-staging.bats @@ -0,0 +1,22 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +@test "Check can create certificate if domain is not lowercase using staging server and DuckDNS" { + if [ -z "$STAGING" ]; then + skip "Running internal tests, skipping external test" + fi + + CONFIG_FILE="getssl-staging-dns01.cfg" + GETSSL_CMD_HOST=$(echo $GETSSL_HOST | tr a-z A-Z) + + setup_environment + init_getssl + create_certificate + + assert_success + check_output_for_errors +} diff --git a/test/10-mixed-case.bats b/test/10-mixed-case.bats new file mode 100644 index 0000000..b1d8f07 --- /dev/null +++ b/test/10-mixed-case.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() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt +} + +@test "Check that HTTP-01 verification works if the domain is not lowercase" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + CONFIG_FILE="getssl-http01.cfg" + GETSSL_CMD_HOST=$(echo $GETSSL_HOST | tr a-z A-Z) + + setup_environment + init_getssl + create_certificate + + assert_success + check_output_for_errors +} + +@test "Check that DNS-01 verification works if the domain is not lowercase" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-dns01.cfg" + GETSSL_CMD_HOST=$(echo $GETSSL_HOST | tr a-z A-Z) + setup_environment + + init_getssl + create_certificate + assert_success + check_output_for_errors +} diff --git a/test/11-test--install.bats b/test/11-test--install.bats new file mode 100644 index 0000000..e034326 --- /dev/null +++ b/test/11-test--install.bats @@ -0,0 +1,63 @@ +#! /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 that config files in /etc/getssl works" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + CONFIG_FILE="getssl-http01.cfg" + setup_environment + + # Fail if not running in docker and /etc/getssl already exists + refute [ -d /etc/getssl ] + + # Create /etc/getssl/$DOMAIN + mkdir -p /etc/getssl/${GETSSL_CMD_HOST} + + # Copy the config file to /etc/getssl + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "/etc/getssl/${GETSSL_CMD_HOST}/getssl.cfg" + cp "${CODE_DIR}/test/test-config/getssl-etc-template.cfg" "/etc/getssl/getssl.cfg" + + # Run getssl + run ${CODE_DIR}/getssl "$GETSSL_CMD_HOST" + + assert_success + check_output_for_errors + assert_line 'Verification completed, obtaining certificate.' + assert_line 'Requesting certificate' + refute [ -d '$HOME/.getssl' ] +} + + +@test "Check that --install doesn't call the ACME server" { + # NOTE that this test depends on the previous test! + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + CONFIG_FILE="getssl-http01.cfg" + + # Run getssl + run ${CODE_DIR}/getssl --install "$GETSSL_CMD_HOST" + + assert_success + check_output_for_errors + refute_line 'Verification completed, obtaining certificate.' + refute_line 'Requesting certificate' + assert_line --partial 'copying domain certificate to' + assert_line --partial 'copying private key to' + assert_line --partial 'copying CA certificate to' + + # Cleanup previous test + rm -rf /etc/getssl +} diff --git a/test/11-test-no-domain-storage.bats b/test/11-test-no-domain-storage.bats new file mode 100644 index 0000000..3be0be5 --- /dev/null +++ b/test/11-test-no-domain-storage.bats @@ -0,0 +1,20 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +@test "Check that if domain storage isn't set getssl doesn't try to delete /tmp" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + CONFIG_FILE="getssl-http01-no-domain-storage.cfg" + setup_environment + mkdir ${INSTALL_DIR}/.getssl + cp "${CODE_DIR}/test/test-config/${CONFIG_FILE}" "${INSTALL_DIR}/.getssl/getssl.cfg" + run ${CODE_DIR}/getssl -a + assert_success + check_output_for_errors + assert_line 'Not going to delete TEMP_DIR ///tmp as it appears to be /tmp' +} diff --git a/test/2-simple-dns01-dig.bats b/test/2-simple-dns01-dig.bats new file mode 100644 index 0000000..0a54684 --- /dev/null +++ b/test/2-simple-dns01-dig.bats @@ -0,0 +1,53 @@ +#! /usr/bin/env bats + +load '/bats-support/load.bash' +load '/bats-assert/load.bash' +load '/getssl/test/test_helper.bash' + + +setup() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + 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)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + 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)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + 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..dc6f2f5 --- /dev/null +++ b/test/2-simple-dns01-nslookup.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() { + export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt + 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)" { + if [ -n "$STAGING" ]; then + skip "Using staging server, skipping internal test" + fi + + 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/3-dual-rsa-ecdsa.bats b/test/3-dual-rsa-ecdsa.bats new file mode 100644 index 0000000..486fa13 --- /dev/null +++ b/test/3-dual-rsa-ecdsa.bats @@ -0,0 +1,65 @@ +#! /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 + CONFIG_FILE="getssl-http01-dual-rsa-ecdsa.cfg" + 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 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 + CONFIG_FILE="getssl-dns01-dual-rsa-ecdsa.cfg" + 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/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..394e8d6 --- /dev/null +++ b/test/6-dual-rsa-ecdsa-copy-2-locations.bats @@ -0,0 +1,48 @@ +#! /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 + CONFIG_FILE="getssl-http01-dual-rsa-ecdsa-2-locations.cfg" + setup_environment + mkdir -p /root/a.${GETSSL_HOST} + + init_getssl + create_certificate + assert_success + check_output_for_errors + + # 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" ] +} diff --git a/test/7-staging-dns01-dig.bats b/test/7-staging-dns01-dig.bats new file mode 100644 index 0000000..7e21124 --- /dev/null +++ b/test/7-staging-dns01-dig.bats @@ -0,0 +1,30 @@ +#! /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, dig and DuckDNS" { + if [ -z "$STAGING" ]; then + skip "Running internal tests, skipping external test" + fi + CONFIG_FILE="getssl-staging-dns01.cfg" + + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors +} + +@test "Force renewal of certificate using staging server, dig and DuckDNS" { + if [ -z "$STAGING" ]; then + skip "Running internal tests, skipping external test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors + cleanup_environment +} diff --git a/test/7-staging-dns01-nslookup.bats b/test/7-staging-dns01-nslookup.bats new file mode 100644 index 0000000..bd8d9da --- /dev/null +++ b/test/7-staging-dns01-nslookup.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 + if [ -f /usr/bin/dig ]; then + mv /usr/bin/dig /usr/bin/dig.getssl.bak + fi +} + + +teardown() { + if [ -f /usr/bin/dig.getssl.bak ]; then + mv /usr/bin/dig.getssl.bak /usr/bin/dig + fi +} + + +@test "Create new certificate using staging server, nslookup and DuckDNS" { + if [ -z "$STAGING" ]; then + skip "Running internal tests, skipping external test" + fi + CONFIG_FILE="getssl-staging-dns01.cfg" + + setup_environment + init_getssl + create_certificate + assert_success + check_output_for_errors "debug" +} + + +@test "Force renewal of certificate using staging server, nslookup and DuckDNS" { + if [ -z "$STAGING" ]; then + skip "Running internal tests, skipping external test" + fi + run ${CODE_DIR}/getssl -f $GETSSL_HOST + assert_success + check_output_for_errors "debug" + cleanup_environment +} diff --git a/test/8-staging-ecdsa.bats b/test/8-staging-ecdsa.bats new file mode 100644 index 0000000..127e989 --- /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 internal tests, skipping external test" + fi + CONFIG_FILE="getssl-staging-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 internal tests, skipping external test" + fi + CONFIG_FILE="getssl-staging-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..c1de91c --- /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-multiple-domains-dns01.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-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..caad22a --- /dev/null +++ b/test/Dockerfile-alpine @@ -0,0 +1,22 @@ +FROM alpine:latest + +# Note this image uses busybox awk instead of gawk + +RUN apk --no-cache add supervisor openssl git curl bind-tools wget nginx bash + +WORKDIR /root + +# Create nginx directories in standard places +RUN mkdir /run/nginx +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core +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..61c8b6b --- /dev/null +++ b/test/Dockerfile-centos6 @@ -0,0 +1,24 @@ +FROM centos:centos6 + +# 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 dnsutils wget nginx + +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 + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core +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-centos7 b/test/Dockerfile-centos7 new file mode 100644 index 0000000..02fbcb7 --- /dev/null +++ b/test/Dockerfile-centos7 @@ -0,0 +1,20 @@ +FROM centos:centos7 + +# 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 ldns wget which nginx + +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 +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 diff --git a/test/Dockerfile-centos7-staging b/test/Dockerfile-centos7-staging new file mode 100644 index 0000000..899bf9b --- /dev/null +++ b/test/Dockerfile-centos7-staging @@ -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 wget which nginx + +ENV staging "true" +ENV DUCKDNS_TOKEN 1d616aa9-b8e4-4bb4-b312-3289de82badb + +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 +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-debian b/test/Dockerfile-debian new file mode 100644 index 0000000..b5da5dd --- /dev/null +++ b/test/Dockerfile-debian @@ -0,0 +1,20 @@ +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 wget nginx-light + +WORKDIR /root +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private + +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core +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-rhel6 b/test/Dockerfile-rhel6 deleted file mode 100644 index 019da84..0000000 --- a/test/Dockerfile-rhel6 +++ /dev/null @@ -1,22 +0,0 @@ -FROM roboxes/rhel6 -# FROM centos:centos6 -# bionic = latest 18 version - -# Update and install required software -RUN yum -y update -RUN yum -y install epel-release -RUN yum -y install git curl dnsutils wget # nginx-light - -WORKDIR /root -#RUN mkdir /etc/nginx/pki -#RUN mkdir /etc/nginx/pki/private -#COPY ./test/test-config/nginx-ubuntu-sites-enabled-default /etc/nginx/sites-enabled/default - -# BATS (Bash Automated Testings) -# RUN git clone https://github.com/bats-core/bats-core.git -# RUN bats-core/install.sh /usr/local - -EXPOSE 80 443 - -# Run eternal loop - for testing -CMD ["/bin/bash", "-c", "while :; do sleep 10; done"] diff --git a/test/Dockerfile-ubuntu b/test/Dockerfile-ubuntu index b0f09f8..66d7a35 100644 --- a/test/Dockerfile-ubuntu +++ b/test/Dockerfile-ubuntu @@ -1,23 +1,26 @@ -FROM ubuntu:xenial -# bionic = latest 18 version +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 -# TODO work out why default version of awk fails -RUN apt-get install -y git curl dnsutils wget gawk nginx-light # linux-libc-dev make gcc binutils +RUN apt-get update --fix-missing +RUN apt-get install -y git curl dnsutils wget nginx-light RUN apt-get install -y vim dos2unix # for debugging # TODO test with drill, dig, host WORKDIR /root -RUN mkdir /etc/nginx/pki -RUN mkdir /etc/nginx/pki/private -COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default -# BATS (Bash Automated Testings) -# RUN git clone https://github.com/bats-core/bats-core.git -# RUN bats-core/install.sh /usr/local +# Prevent "Can't load /root/.rnd into RNG" error from openssl +RUN touch /root/.rnd -EXPOSE 80 443 +# BATS (Bash Automated Testings) +RUN git clone https://github.com/bats-core/bats-core.git /bats-core +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 ["/bin/bash", "-c", "while :; do sleep 10; done"] +CMD tail -f /dev/null diff --git a/test/Dockerfile-ubuntu-staging b/test/Dockerfile-ubuntu-staging new file mode 100644 index 0000000..58762d0 --- /dev/null +++ b/test/Dockerfile-ubuntu-staging @@ -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 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 wget nginx-light +RUN apt-get install -y vim dos2unix # for debugging +# TODO test with drill, dig, host + +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 +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..030d03a --- /dev/null +++ b/test/Dockerfile-ubuntu16 @@ -0,0 +1,25 @@ +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 wget nginx-light + +WORKDIR /root +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default + +# 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 +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-ubuntu18 b/test/Dockerfile-ubuntu18 new file mode 100644 index 0000000..1d68cd3 --- /dev/null +++ b/test/Dockerfile-ubuntu18 @@ -0,0 +1,27 @@ +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 wget gawk nginx-light + +WORKDIR /root +RUN mkdir /etc/nginx/pki +RUN mkdir /etc/nginx/pki/private +COPY ./test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default + +# 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 +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/README-Testing.md b/test/README-Testing.md new file mode 100644 index 0000000..3cd4b2c --- /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 nn 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/README.md b/test/README.md deleted file mode 100644 index 9d0aedd..0000000 --- a/test/README.md +++ /dev/null @@ -1,20 +0,0 @@ -# Testing - -This directory contains a simple test script which tests creating certificates with Pebble (testing version of the LetsEncrypt server) - -Start up pebble, the challdnstest server for DNS challenges -`docker-compose -f "docker-compose.yml" up -d --build` - -Run the tests -`docker exec -it getssl /getssl/test/run-test.sh` - -Debug (need to set CURL_CA_BUNDLE as pebble uses a local certificate, otherwise you get a "unknown API version" error) -`docker exec -it getssl /bin/bash` -`export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt` -`/getssl/getssl -d getssl` - -# TODO -1. Move to BATS (bash automated testing) instead of run-test.sh -2. Test RHEL6, Debian as well -3. Test SSH, SFTP -4. Test wildcards 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/restart-nginx b/test/restart-nginx new file mode 100755 index 0000000..e62433d --- /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" == "centos7" ]; 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..43c4e40 --- /dev/null +++ b/test/run-test.cmd @@ -0,0 +1,54 @@ +@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:staging=%==x%OS% GOTO staging +set ALIAS=%OS%.getssl.test +set STAGING= +GOTO Run + +:NoOS +set OS=ubuntu +GOTO CheckCommand + +:NoCmd +REM set COMMAND=/getssl/test/run-bats.sh +set COMMAND=bats /getssl/test +GOTO CheckAlias + +:staging +set ALIAS=%OS:-staging=%-getssl.duckdns.org +set STAGING=--env STAGING=true + +:Run +for %%I in (.) do set CurrDirName=%%~nxI + +docker build --rm -f "test\Dockerfile-%OS%" -t getssl-%OS% . +@echo on +docker run -it ^ + --env GETSSL_HOST=%ALIAS% %STAGING% ^ + --env GETSSL_OS=%OS:-staging=% ^ + -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 ^ + --name getssl-%OS% ^ + getssl-%OS% ^ + %COMMAND% diff --git a/test/run-test.sh b/test/run-test.sh old mode 100644 new mode 100755 index 8051922..d99d5a3 --- a/test/run-test.sh +++ b/test/run-test.sh @@ -1,43 +1,47 @@ -#! /bin/bash +#! /usr/bin/env bash -set -e - -# Test setup -if [[ -d /root/.getssl ]]; then - rm -r /root/.getssl +if [ $# -eq 0 ]; then + echo "Usage: $(basename "$0") []" + echo "e.g. $(basename "$0") alpine bats /getssl/test" + exit 1 fi +OS=$1 -wget --no-clobber https://raw.githubusercontent.com/letsencrypt/pebble/master/test/certs/pebble.minica.pem -# cat /etc/pki/tls/certs/ca-bundle.crt /root/pebble.minica.pem > /root/pebble-ca-bundle.crt -cat /etc/ssl/certs/ca-certificates.crt /root/pebble.minica.pem > /root/pebble-ca-bundle.crt -export CURL_CA_BUNDLE=/root/pebble-ca-bundle.crt - -curl -X POST -d '{"host":"getssl", "addresses":["10.30.50.4"]}' http://10.30.50.3:8055/add-a - -# Test #1 - http-01 verification -echo Test \#1 - http-01 verification - -cp /getssl/test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default -service nginx restart -/getssl/getssl -c getssl -cp /getssl/test/test-config/getssl-http01.cfg /root/.getssl/getssl/getssl.cfg -/getssl/getssl -f getssl - -# Test #2 - http-01 forced renewal -echo Test \#2 - http-01 forced renewal -/getssl/getssl getssl -f - -# Test cleanup -rm -r /root/.getssl +if [ $# -gt 1 ]; then + shift + COMMAND=$* +else + COMMAND="bats /getssl/test" +fi -# Test #3 - dns-01 verification -echo Test \#3 - dns-01 verification -cp /getssl/test/test-config/nginx-ubuntu-no-ssl /etc/nginx/sites-enabled/default -service nginx restart -/getssl/getssl -c getssl -cp /getssl/test/test-config/getssl-dns01.cfg /root/.getssl/getssl/getssl.cfg -/getssl/getssl getssl +if [[ "$OS" == *"staging"* ]]; then + ALIAS="${OS%-staging}-getssl.duckdns.org" + STAGING="--env STAGING=true" +else + ALIAS="$OS.getssl.test" + STAGING="" +fi -# Test #4 - dns-01 forced renewal -echo Test \#4 - dns-01 forced renewal -/getssl/getssl getssl -f +docker build --rm -f "test/Dockerfile-$OS" -t "getssl-$OS" . +# shellcheck disable=SC2086 +docker run \ + --env GETSSL_HOST=$ALIAS $STAGING \ + --env GETSSL_OS=${OS%-staging} \ + -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" \ + --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..9759570 --- /dev/null +++ b/test/test-config/alpine-supervisord.conf @@ -0,0 +1,14 @@ +[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 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..8f29088 --- /dev/null +++ b/test/test-config/getssl-dns01-dual-rsa-ecdsa.cfg @@ -0,0 +1,39 @@ +# 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" +PUBLIC_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT="" + +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. The first line for the domain, the following ones for each additional domain. +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-dns01.cfg b/test/test-config/getssl-dns01.cfg index 49c58b5..0b816b1 100644 --- a/test/test-config/getssl-dns01.cfg +++ b/test/test-config/getssl-dns01.cfg @@ -2,35 +2,19 @@ # 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" 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 - -#PRIVATE_KEY_ALG="rsa" +PUBLIC_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT="" # 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="" # 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/html/.well-known/acme-challenge') -# 'ssh:server5:/var/www/getssltest.hopto.org/web/.well-known/acme-challenge' -# 'ssh:sshuserid@server5:/var/www/getssltest.hopto.org/web/.well-known/acme-challenge' -# 'ftp:ftpuserid:ftppassword:getssltest.hopto.org:/web/.well-known/acme-challenge') #Set USE_SINGLE_ACL="true" to use a single ACL for all checks USE_SINGLE_ACL="false" @@ -44,11 +28,8 @@ 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 /etc/nginx/sites-enabled/default && service nginx restart" +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" -# 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" +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" 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-dual-rsa-ecdsa-2-locations.cfg b/test/test-config/getssl-http01-dual-rsa-ecdsa-2-locations.cfg new file mode 100644 index 0000000..80533ce --- /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-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.cfg b/test/test-config/getssl-http01-dual-rsa-ecdsa.cfg new file mode 100644 index 0000000..f6cfcb7 --- /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-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-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-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.cfg b/test/test-config/getssl-http01.cfg index f3dc5ad..f7d75ea 100644 --- a/test/test-config/getssl-http01.cfg +++ b/test/test-config/getssl-http01.cfg @@ -2,34 +2,13 @@ # 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" 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" - -#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="" -# 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. +# Acme Challenge Location. ACL=('/var/www/html/.well-known/acme-challenge') -# 'ssh:server5:/var/www/getssltest.hopto.org/web/.well-known/acme-challenge' -# 'ssh:sshuserid@server5:/var/www/getssltest.hopto.org/web/.well-known/acme-challenge' -# 'ftp:ftpuserid:ftppassword:getssltest.hopto.org:/web/.well-known/acme-challenge') #Set USE_SINGLE_ACL="true" to use a single ACL for all checks USE_SINGLE_ACL="false" @@ -43,11 +22,8 @@ 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 /etc/nginx/sites-enabled/default && service nginx restart" +RELOAD_CMD="cp /getssl/test/test-config/nginx-ubuntu-ssl ${NGINX_CONFIG} && /getssl/test/restart-nginx" -# 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" +# Define the server type and confirm correct certificate is installed +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/getssl-ignore-directory-domain.cfg b/test/test-config/getssl-ignore-directory-domain.cfg new file mode 100644 index 0000000..9777891 --- /dev/null +++ b/test/test-config/getssl-ignore-directory-domain.cfg @@ -0,0 +1,39 @@ +# 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" +PUBLIC_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT="" + +# 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" + +# Acme Challenge Location. The first line for the domain, the following ones for each additional domain. +ACL=( + '/var/www/html/.well-known/acme-challenge' + '/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-multiple-domains-dns01.cfg b/test/test-config/getssl-multiple-domains-dns01.cfg new file mode 100644 index 0000000..8754677 --- /dev/null +++ b/test/test-config/getssl-multiple-domains-dns01.cfg @@ -0,0 +1,38 @@ +# 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" +PUBLIC_DNS_SERVER=10.30.50.3 +DNS_EXTRA_WAIT="" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="getssl.test" + +# Acme Challenge Location. The first line for the domain, the following ones for each additional domain. +ACL=( + '/var/www/html/.well-known/acme-challenge' + '/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-staging-dns01.cfg b/test/test-config/getssl-staging-dns01.cfg new file mode 100644 index 0000000..8c12ee1 --- /dev/null +++ b/test/test-config/getssl-staging-dns01.cfg @@ -0,0 +1,37 @@ +# Test that the script works with external dns provider and staging server +# +CA="https://acme-staging-v02.api.letsencrypt.org/directory" + +VALIDATE_VIA_DNS=true +DNS_ADD_COMMAND="/getssl/dns_scripts/dns_add_duckdns" +DNS_DEL_COMMAND="/getssl/dns_scripts/dns_del_duckdns" +PUBLIC_DNS_SERVER=ns2.duckdns.org +CHECK_ALL_AUTH_DNS=true +DNS_EXTRA_WAIT=60 + +ACCOUNT_KEY_TYPE="rsa" +PRIVATE_KEY_ALG="rsa" + +# Additional domains - this could be multiple domains / subdomains in a comma separated list +SANS="" + +# Acme Challenge Location. The first line for the domain, the following ones for each additional domain. +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 (using a custom port) +SERVER_TYPE="https" +CHECK_REMOTE="true" diff --git a/test/test-config/nginx-centos7.conf b/test/test-config/nginx-centos7.conf new file mode 100644 index 0000000..2327039 --- /dev/null +++ b/test/test-config/nginx-centos7.conf @@ -0,0 +1,37 @@ +# For more information on configuration, see: +# * Official English Documentation: http://nginx.org/en/docs/ +# * Official Russian Documentation: http://nginx.org/ru/docs/ + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log; +pid /run/nginx.pid; + +# Load dynamic modules. See /usr/share/doc/nginx/README.dynamic. +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; + + # Load modular configuration files from the /etc/nginx/conf.d directory. + # See http://nginx.org/en/docs/ngx_core_module.html#include + # for more information. + include /etc/nginx/conf.d/*.conf; +} diff --git a/test/test-config/nginx-ubuntu-no-ssl b/test/test-config/nginx-ubuntu-no-ssl index c78d646..e7b046e 100644 --- a/test/test-config/nginx-ubuntu-no-ssl +++ b/test/test-config/nginx-ubuntu-no-ssl @@ -1,16 +1,3 @@ -## -# You should look at the following URL's in order to grasp a solid understanding -# of Nginx configuration files in order to fully unleash the power of Nginx. -# http://wiki.nginx.org/Pitfalls -# http://wiki.nginx.org/QuickStart -# http://wiki.nginx.org/Configuration -# -# Generally, you will want to move this file somewhere, and start with a clean -# file but keep this around for reference. Or just disable in sites-enabled. -# -# Please see /usr/share/doc/nginx-doc/examples/ for more detailed examples. -## - # Default server configuration # server { @@ -26,18 +13,6 @@ server { listen 5001 default_server; listen [::]:5001 default_server; - # - # Note: You should disable gzip for SSL traffic. - # See: https://bugs.debian.org/773332 - # - # Read up on ssl_ciphers to ensure a secure configuration. - # See: https://bugs.debian.org/765782 - # - # Self signed certs generated by the ssl-cert package - # Don't use them in a production server! - # - # include snippets/snakeoil.conf; - root /var/www/html; # Add index.php to the list if you are using PHP @@ -52,42 +27,4 @@ server { # as directory, then fall back to displaying a 404. try_files $uri $uri/ =404; } - - # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 - # - #location ~ \.php$ { - # include snippets/fastcgi-php.conf; - # - # # With php7.0-cgi alone: - # fastcgi_pass 127.0.0.1:9000; - # # With php7.0-fpm: - # fastcgi_pass unix:/run/php/php7.0-fpm.sock; - #} - - # deny access to .htaccess files, if Apache's document root - # concurs with nginx's one - # - #location ~ /\.ht { - # deny all; - #} } - - -# Virtual Host configuration for example.com -# -# You can move that to a different file under sites-available/ and symlink that -# to sites-enabled/ to enable it. -# -#server { -# listen 80; -# listen [::]:80; -# -# server_name example.com; -# -# root /var/www/example.com; -# index index.html; -# -# location / { -# try_files $uri $uri/ =404; -# } -#} diff --git a/test/test_helper.bash b/test/test_helper.bash new file mode 100644 index 0000000..1e65189 --- /dev/null +++ b/test/test_helper.bash @@ -0,0 +1,99 @@ +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" ] +} + +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() { + curl --silent -X POST -d '{"host":"'"$GETSSL_HOST"'"}' http://10.30.50.3:8055/clear-a +} + +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 + + curl --silent -X POST -d '{"host":"'"$GETSSL_HOST"'", "addresses":["'"$GETSSL_IP"'"]}' http://10.30.50.3:8055/add-a + cp ${CODE_DIR}/test/test-config/nginx-ubuntu-no-ssl "${NGINX_CONFIG}" + /getssl/test/restart-nginx +} + +# start nginx 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- + fi +elif [ "$GETSSL_OS" == "centos7" ]; then + if [ -z "$(pgrep nginx)" ]; then + nginx >&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 [ ! -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