From 7e8842b4525ec07b4f6c4c0bad77d59b479ea36b Mon Sep 17 00:00:00 2001 From: GitHub Actions Bot Date: Thu, 1 Feb 2024 19:24:13 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20with?= =?UTF-8?q?=204=20commits=20[trident-sync]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example for on-demand tls-alpn-01 Example disclaimer, fallback cert Replace CircleCI with GitHub Actions --- .../core/acme-client/.circleci/.gitignore | 1 - .../core/acme-client/.circleci/config.yml | 145 -------------- .../scripts/tests-install-coredns.sh} | 5 +- .../scripts/tests-install-cts.sh} | 2 +- .../scripts/tests-install-pebble.sh} | 10 +- .../scripts/tests-wait-for-ca.sh} | 4 +- .../acme-client/.github/workflows/tests.yml | 94 +++++++++ packages/core/acme-client/README.md | 2 +- packages/core/acme-client/examples/README.md | 19 ++ packages/core/acme-client/examples/api.js | 1 - packages/core/acme-client/examples/auto.js | 1 - .../core/acme-client/examples/fallback.crt | 19 ++ .../core/acme-client/examples/fallback.key | 28 +++ .../examples/tls-alpn-01/README.md | 44 +++++ .../examples/tls-alpn-01/haproxy.cfg | 23 +++ .../examples/tls-alpn-01/nginx.conf | 19 ++ .../examples/tls-alpn-01/tls-alpn-01.js | 180 ++++++++++++++++++ packages/core/acme-client/package.json | 3 +- .../core/acme-client/scripts/run-tests.sh | 59 ------ .../scripts/test-suite-install-step.sh | 20 -- .../core/acme-client/test/00-pebble.spec.js | 2 +- 21 files changed, 441 insertions(+), 240 deletions(-) delete mode 100644 packages/core/acme-client/.circleci/.gitignore delete mode 100644 packages/core/acme-client/.circleci/config.yml rename packages/core/acme-client/{scripts/test-suite-install-coredns.sh => .github/scripts/tests-install-coredns.sh} (93%) rename packages/core/acme-client/{scripts/test-suite-install-cts.sh => .github/scripts/tests-install-cts.sh} (95%) rename packages/core/acme-client/{scripts/test-suite-install-pebble.sh => .github/scripts/tests-install-pebble.sh} (78%) rename packages/core/acme-client/{scripts/test-suite-wait-for-ca.sh => .github/scripts/tests-wait-for-ca.sh} (79%) create mode 100644 packages/core/acme-client/.github/workflows/tests.yml create mode 100644 packages/core/acme-client/examples/README.md create mode 100644 packages/core/acme-client/examples/fallback.crt create mode 100644 packages/core/acme-client/examples/fallback.key create mode 100644 packages/core/acme-client/examples/tls-alpn-01/README.md create mode 100644 packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg create mode 100644 packages/core/acme-client/examples/tls-alpn-01/nginx.conf create mode 100644 packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js delete mode 100644 packages/core/acme-client/scripts/run-tests.sh delete mode 100644 packages/core/acme-client/scripts/test-suite-install-step.sh diff --git a/packages/core/acme-client/.circleci/.gitignore b/packages/core/acme-client/.circleci/.gitignore deleted file mode 100644 index 0ae14133..00000000 --- a/packages/core/acme-client/.circleci/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.temp.yml diff --git a/packages/core/acme-client/.circleci/config.yml b/packages/core/acme-client/.circleci/config.yml deleted file mode 100644 index 589f75c5..00000000 --- a/packages/core/acme-client/.circleci/config.yml +++ /dev/null @@ -1,145 +0,0 @@ ---- -version: 2.1 - -commands: - pre: - steps: - - run: - name: Setup environment - command: | - echo 'export FORCE_COLOR=1' >> $BASH_ENV - echo 'export NPM_CONFIG_COLOR="always"' >> $BASH_ENV - - - run: node --version - - run: npm --version - - checkout - - enable-eab: - steps: - - run: - name: Enable EAB through environment - command: | - echo 'export ACME_CAP_EAB_ENABLED=1' >> $BASH_ENV - - install-cts: - steps: - - run: - name: Install Pebble Challenge Test Server - command: sudo -E /bin/bash ./scripts/test-suite-install-cts.sh - environment: - PEBBLECTS_VERSION: 2.3.1 - - - run: - name: Start Pebble Challenge Test Server - command: pebble-challtestsrv -dns01 ":8053" -tlsalpn01 ":5001" -http01 ":5002" -https01 ":5003" -defaultIPv4 "127.0.0.1" -defaultIPv6 "" - background: true - - install-pebble: - steps: - - run: - name: Install Pebble - command: sudo -E /bin/bash ./scripts/test-suite-install-pebble.sh - environment: - PEBBLE_VERSION: 2.3.1 - - - run: - name: Start Pebble - command: pebble -strict -config /etc/pebble/pebble.json -dnsserver "127.0.0.1:53" - background: true - environment: - PEBBLE_ALTERNATE_ROOTS: 2 - - - run: - name: Set up environment - command: | - echo 'export NODE_EXTRA_CA_CERTS="/etc/pebble/ca.cert.pem"' >> $BASH_ENV - echo 'export ACME_CA_CERT_PATH="/etc/pebble/ca.cert.pem"' >> $BASH_ENV - echo 'export ACME_DIRECTORY_URL="https://127.0.0.1:14000/dir"' >> $BASH_ENV - echo 'export ACME_PEBBLE_MANAGEMENT_URL="https://127.0.0.1:15000"' >> $BASH_ENV - - - run: - name: Wait for Pebble - command: /bin/bash ./scripts/test-suite-wait-for-ca.sh - - install-step: - steps: - - run: - name: Install Step Certificates - command: /bin/bash ./scripts/test-suite-install-step.sh - environment: - STEPCA_VERSION: 0.18.0 - STEPCLI_VERSION: 0.18.0 - - - run: - name: Start Step CA - command: /usr/bin/step-ca --resolver="127.0.0.1:53" --password-file="/tmp/password" ~/.step/config/ca.json - background: true - - - run: - name: Set up environment - command: | - echo 'export NODE_EXTRA_CA_CERTS="/home/circleci/.step/certs/root_ca.crt"' >> $BASH_ENV - echo 'export ACME_CA_CERT_PATH="/home/circleci/.step/certs/root_ca.crt"' >> $BASH_ENV - echo 'export ACME_DIRECTORY_URL="https://localhost:8443/acme/acme/directory"' >> $BASH_ENV - - echo 'export ACME_CAP_META_TOS_FIELD=0' >> $BASH_ENV - echo 'export ACME_CAP_UPDATE_ACCOUNT_KEY=0' >> $BASH_ENV - echo 'export ACME_CAP_ALTERNATE_CERT_ROOTS=0' >> $BASH_ENV - - - run: - name: Wait for Step CA - command: /bin/bash ./scripts/test-suite-wait-for-ca.sh - - install-coredns: - steps: - - run: - name: Install CoreDNS - command: sudo -E /bin/bash ./scripts/test-suite-install-coredns.sh - environment: - COREDNS_VERSION: 1.11.1 - PEBBLECTS_DNS_PORT: 8053 - - - run: - name: Start CoreDNS - command: sudo coredns -p 53 -conf /etc/coredns/Corefile - background: true - - - run: - name: Use CoreDNS in resolv.conf - command: echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf - - test: - steps: - - run: npm i - - run: npm run lint - - run: npm run lint-types - - run: npm run build-docs - - - run: - command: npm run test - environment: - ACME_DOMAIN_NAME: test.example.com - ACME_CHALLTESTSRV_URL: http://127.0.0.1:8055 - ACME_TLSALPN_PORT: 5001 - ACME_HTTP_PORT: 5002 - ACME_HTTPS_PORT: 5003 - -jobs: - v16: { docker: [{ image: cimg/node:16.20 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]} - v18: { docker: [{ image: cimg/node:18.19 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]} - v20: { docker: [{ image: cimg/node:20.11 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]} - eab-v16: { docker: [{ image: cimg/node:16.20 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]} - eab-v18: { docker: [{ image: cimg/node:18.19 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]} - eab-v20: { docker: [{ image: cimg/node:20.11 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]} - # step-v12: { docker: [{ image: cimg/node:12.22 }], steps: [ pre, install-cts, install-step, install-coredns, test ]} - -workflows: - test-suite: - jobs: - - v16 - - v18 - - v20 - - eab-v16 - - eab-v18 - - eab-v20 - # - step-v12 diff --git a/packages/core/acme-client/scripts/test-suite-install-coredns.sh b/packages/core/acme-client/.github/scripts/tests-install-coredns.sh similarity index 93% rename from packages/core/acme-client/scripts/test-suite-install-coredns.sh rename to packages/core/acme-client/.github/scripts/tests-install-coredns.sh index c4876beb..2dfc4ccb 100644 --- a/packages/core/acme-client/scripts/test-suite-install-coredns.sh +++ b/packages/core/acme-client/.github/scripts/tests-install-coredns.sh @@ -2,7 +2,7 @@ # # Install CoreDNS for testing. # -set -eu +set -euo pipefail # Download and install wget -nv "https://github.com/coredns/coredns/releases/download/v${COREDNS_VERSION}/coredns_${COREDNS_VERSION}_linux_amd64.tgz" -O /tmp/coredns.tgz @@ -39,18 +39,21 @@ tee /etc/coredns/Corefile << EOF example.com { errors log + bind 127.53.53.53 file /etc/coredns/db.example.com } test.example.com { errors log + bind 127.53.53.53 forward . 127.0.0.1:${PEBBLECTS_DNS_PORT} } . { errors log + bind 127.53.53.53 forward . 8.8.8.8 } EOF diff --git a/packages/core/acme-client/scripts/test-suite-install-cts.sh b/packages/core/acme-client/.github/scripts/tests-install-cts.sh similarity index 95% rename from packages/core/acme-client/scripts/test-suite-install-cts.sh rename to packages/core/acme-client/.github/scripts/tests-install-cts.sh index ab9a85c1..ac929c8b 100644 --- a/packages/core/acme-client/scripts/test-suite-install-cts.sh +++ b/packages/core/acme-client/.github/scripts/tests-install-cts.sh @@ -2,7 +2,7 @@ # # Install Pebble Challenge Test Server for testing. # -set -eu +set -euo pipefail # Download and install wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLECTS_VERSION}/pebble-challtestsrv_linux-amd64" -O /usr/local/bin/pebble-challtestsrv diff --git a/packages/core/acme-client/scripts/test-suite-install-pebble.sh b/packages/core/acme-client/.github/scripts/tests-install-pebble.sh similarity index 78% rename from packages/core/acme-client/scripts/test-suite-install-pebble.sh rename to packages/core/acme-client/.github/scripts/tests-install-pebble.sh index 9378250c..4b830e6d 100644 --- a/packages/core/acme-client/scripts/test-suite-install-pebble.sh +++ b/packages/core/acme-client/.github/scripts/tests-install-pebble.sh @@ -2,14 +2,14 @@ # # Install Pebble for testing. # -set -eu +set -euo pipefail -config_name="pebble-config.json" +CONFIG_NAME="pebble-config.json" # Use Pebble EAB config if enabled set +u -if [[ ! -z $ACME_CAP_EAB_ENABLED ]] && [[ $ACME_CAP_EAB_ENABLED -eq 1 ]]; then - config_name="pebble-config-external-account-bindings.json" +if [[ -n $ACME_CAP_EAB_ENABLED ]] && [[ $ACME_CAP_EAB_ENABLED -eq 1 ]]; then + CONFIG_NAME="pebble-config-external-account-bindings.json" fi set -u @@ -19,7 +19,7 @@ mkdir -p /etc/pebble wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/pebble.minica.pem" -O /etc/pebble/ca.cert.pem wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/localhost/cert.pem" -O /etc/pebble/cert.pem wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/localhost/key.pem" -O /etc/pebble/key.pem -wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/config/${config_name}" -O /etc/pebble/pebble.json +wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/config/${CONFIG_NAME}" -O /etc/pebble/pebble.json # Download and install Pebble wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLE_VERSION}/pebble_linux-amd64" -O /usr/local/bin/pebble diff --git a/packages/core/acme-client/scripts/test-suite-wait-for-ca.sh b/packages/core/acme-client/.github/scripts/tests-wait-for-ca.sh similarity index 79% rename from packages/core/acme-client/scripts/test-suite-wait-for-ca.sh rename to packages/core/acme-client/.github/scripts/tests-wait-for-ca.sh index a35cf6a1..b1f39e31 100644 --- a/packages/core/acme-client/scripts/test-suite-wait-for-ca.sh +++ b/packages/core/acme-client/.github/scripts/tests-wait-for-ca.sh @@ -2,13 +2,13 @@ # # Wait for ACME server to accept connections. # -set -eu +set -euo pipefail MAX_ATTEMPTS=15 ATTEMPT=0 # Loop until ready -while ! $(curl --cacert "${ACME_CA_CERT_PATH}" -s -D - "${ACME_DIRECTORY_URL}" | grep '^HTTP.*200' > /dev/null 2>&1); do +while ! curl --cacert "${ACME_CA_CERT_PATH}" -s -D - "${ACME_DIRECTORY_URL}" | grep '^HTTP.*200' > /dev/null 2>&1; do ATTEMPT=$((ATTEMPT + 1)) # Max attempts diff --git a/packages/core/acme-client/.github/workflows/tests.yml b/packages/core/acme-client/.github/workflows/tests.yml new file mode 100644 index 00000000..6f9e7c4b --- /dev/null +++ b/packages/core/acme-client/.github/workflows/tests.yml @@ -0,0 +1,94 @@ +--- +name: test +on: [push, pull_request] + +jobs: + test: + name: node=${{matrix.node}} eab=${{matrix.eab}} + runs-on: ubuntu-latest + + strategy: + matrix: + node: [16, 18, 20] + eab: [0, 1] + + + # + # Environment + # + + env: + FORCE_COLOR: 1 + NPM_CONFIG_COLOR: always + + PEBBLE_VERSION: 2.3.1 + PEBBLE_ALTERNATE_ROOTS: 2 + PEBBLECTS_VERSION: 2.3.1 + PEBBLECTS_DNS_PORT: 8053 + COREDNS_VERSION: 1.11.1 + + NODE_EXTRA_CA_CERTS: /etc/pebble/ca.cert.pem + ACME_CA_CERT_PATH: /etc/pebble/ca.cert.pem + + ACME_DIRECTORY_URL: https://127.0.0.1:14000/dir + ACME_CHALLTESTSRV_URL: http://127.0.0.1:8055 + ACME_PEBBLE_MANAGEMENT_URL: https://127.0.0.1:15000 + + ACME_DOMAIN_NAME: test.example.com + ACME_CAP_EAB_ENABLED: ${{matrix.eab}} + + ACME_TLSALPN_PORT: 5001 + ACME_HTTP_PORT: 5002 + ACME_HTTPS_PORT: 5003 + + + # + # Pipeline + # + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{matrix.node}} + + # Pebble Challenge Test Server + - name: Install Pebble Challenge Test Server + run: sudo -E /bin/bash ./.github/scripts/tests-install-cts.sh + + - name: Start Pebble Challenge Test Server + run: |- + nohup bash -c "pebble-challtestsrv \ + -dns01 :${PEBBLECTS_DNS_PORT} \ + -tlsalpn01 :${ACME_TLSALPN_PORT} \ + -http01 :${ACME_HTTP_PORT} \ + -https01 :${ACME_HTTPS_PORT} \ + -defaultIPv4 127.0.0.1 \ + -defaultIPv6 \"\" &" + + # Pebble + - name: Install Pebble + run: sudo -E /bin/bash ./.github/scripts/tests-install-pebble.sh + + - name: Start Pebble + run: nohup bash -c "pebble -strict -config /etc/pebble/pebble.json -dnsserver 127.53.53.53:53 &" + + - name: Wait for Pebble + run: /bin/bash ./.github/scripts/tests-wait-for-ca.sh + + # CoreDNS + - name: Install CoreDNS + run: sudo -E /bin/bash ./.github/scripts/tests-install-coredns.sh + + - name: Start CoreDNS + run: nohup bash -c "sudo coredns -p 53 -conf /etc/coredns/Corefile &" + + - name: Use CoreDNS for DNS resolution + run: echo "nameserver 127.53.53.53" | sudo tee /etc/resolv.conf + + # Run tests + - run: npm i + - run: npm run lint + - run: npm run lint-types + - run: npm run build-docs + - run: npm run test diff --git a/packages/core/acme-client/README.md b/packages/core/acme-client/README.md index 21388a88..7d9eb09e 100644 --- a/packages/core/acme-client/README.md +++ b/packages/core/acme-client/README.md @@ -1,4 +1,4 @@ -# acme-client [![CircleCI](https://circleci.com/gh/publishlab/node-acme-client.svg?style=svg)](https://circleci.com/gh/publishlab/node-acme-client) +# acme-client [![test](https://github.com/publishlab/node-acme-client/actions/workflows/tests.yml/badge.svg)](https://github.com/publishlab/node-acme-client/actions/workflows/tests.yml) *A simple and unopinionated ACME client.* diff --git a/packages/core/acme-client/examples/README.md b/packages/core/acme-client/examples/README.md new file mode 100644 index 00000000..1c26d611 --- /dev/null +++ b/packages/core/acme-client/examples/README.md @@ -0,0 +1,19 @@ +# Disclaimer + +These examples should not be used as is for any production environment, as they are just proof of concepts meant for testing and to get you started. The examples are naively written and purposefully avoids important topics since they will be specific to your application and how you choose to use `acme-client`, like for example: + +1. **Concurrency control** + * If implementing on-demand certificate generation + * What happens when multiple requests hit your domain at the same time? + * Ensure your application does not place multiple cert orders for the same domain at the same time by implementing some sort of exclusive lock +2. **Domain allow lists** + * If implementing on-demand certificate generation + * What happens when someone manipulates the `ServerName` or `Host` header to your service? + * Ensure your application is unable to place certificate orders for domains you do not intend, as this can quickly rate limit your account and cause a DoS +3. **Clustering** + * If using `acme-client` across a cluster of servers + * Ensure challenge responses are known to all servers in your cluster, perhaps using a database or shared storage +4. **Certificate and key storage** + * Where and how should the account key be stored and read? + * Where and how should certificates and cert keys be stored and read? + * How and when should they be renewed? diff --git a/packages/core/acme-client/examples/api.js b/packages/core/acme-client/examples/api.js index 998201e6..d2b7162a 100644 --- a/packages/core/acme-client/examples/api.js +++ b/packages/core/acme-client/examples/api.js @@ -4,7 +4,6 @@ const acme = require('./../'); - function log(m) { process.stdout.write(`${m}\n`); } diff --git a/packages/core/acme-client/examples/auto.js b/packages/core/acme-client/examples/auto.js index 1495043b..cd4295d7 100644 --- a/packages/core/acme-client/examples/auto.js +++ b/packages/core/acme-client/examples/auto.js @@ -5,7 +5,6 @@ // const fs = require('fs').promises; const acme = require('./../'); - function log(m) { process.stdout.write(`${m}\n`); } diff --git a/packages/core/acme-client/examples/fallback.crt b/packages/core/acme-client/examples/fallback.crt new file mode 100644 index 00000000..100e4781 --- /dev/null +++ b/packages/core/acme-client/examples/fallback.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDCTCCAfGgAwIBAgIUGwI6ZLE3HN7oRZ9BvWLde0Tsu7EwDQYJKoZIhvcNAQEL +BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDgwMTAwNTMzMVoXDTIyMDgz +MTAwNTMzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEA4c7zSiY6OEp9xYZHY42FUfOLREm03NstZhd9IxFFePwe +CTTirJjmi5teKQwzBmEok0SJkanJUaMsMlOHjEykWSc4SBO4QjD349Q60044i9WS +7KHzeSqpWTG+V9jF3HOJPw843VG9hXy3ulXKcysTXzumTVQwfatCODBNkpWqMju2 +N33biLgmpqwLbDSfKXS3uSVTfoHAKGT/oRepko7/0Hwr5oEmjXEbpRWRhU09KYjH +7jokRaiQRn0h216a0r4AKzSNGihNQtKJZIuwJvLFPMQYafsu9qBaCLPqDBXCwQWG +aYh6Cm3kTkADKzG1LVPB/7/Uh2d4Fck/ejR9qXRK3QIDAQABo1MwUTAdBgNVHQ4E +FgQUvyceAVDMPbW7wHwNF9px5dWfgd4wHwYDVR0jBBgwFoAUvyceAVDMPbW7wHwN +F9px5dWfgd4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAaYkz +AOHrRirPwfkwjb+uMliGHfANrmak8r5VDQA73RLTQLRhMpf1yrb1uhH7p/CUYKap +x1C8RGQAXujoQbQOslyZA7cVLA9ASSZS6Noq7NerfGBiqxeuye+x3lIIk1EOL/rH +aBu9rrYGmlU49PlGAQSfFHkwzXti2Mp1VQv8eMOBLR49ezZIXHiPE8S3gjNymZ0G +UA13wzZCT7SG1BLmQ/cBVASG2wvhlC8IG/4vF0Xe+boSOb1vGWUtHS+MnvvRK4n5 +TMUtrnxSQ/LA8AtobvzqgvQVKBSPLK6RzLE7I+Q9pWsbKTBqfyStuQrQFqafBOqN +eYfPUgiID9uvfrxLvA== +-----END CERTIFICATE----- diff --git a/packages/core/acme-client/examples/fallback.key b/packages/core/acme-client/examples/fallback.key new file mode 100644 index 00000000..1cbd8f66 --- /dev/null +++ b/packages/core/acme-client/examples/fallback.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhzvNKJjo4Sn3F +hkdjjYVR84tESbTc2y1mF30jEUV4/B4JNOKsmOaLm14pDDMGYSiTRImRqclRoywy +U4eMTKRZJzhIE7hCMPfj1DrTTjiL1ZLsofN5KqlZMb5X2MXcc4k/DzjdUb2FfLe6 +VcpzKxNfO6ZNVDB9q0I4ME2SlaoyO7Y3fduIuCamrAtsNJ8pdLe5JVN+gcAoZP+h +F6mSjv/QfCvmgSaNcRulFZGFTT0piMfuOiRFqJBGfSHbXprSvgArNI0aKE1C0olk +i7Am8sU8xBhp+y72oFoIs+oMFcLBBYZpiHoKbeROQAMrMbUtU8H/v9SHZ3gVyT96 +NH2pdErdAgMBAAECggEBAImI0FxQblOM45AkmmTDdPmWWjPspNGEWeF92wU55tOq +0+yNnqa7tmg/6JkdyhJPqTQRoazr+ifUN/4rLDtDDzMSFVCpWihOxR2qTW4YjY52 +NjgU6EPbvSwLhUDiUplUcbrL3bnHqKSecxV2XYnKKdFudntRFPvmDL5GhWkL6Y8P +9KiQaYuPf4av8PR0NlWBMiZs+CBjLlnSTMAWRYj5mRSyFSEOMT7+Lvr3TqrO2/nh +0H30LXxrXXXuCbQXnVy3oSNf7TrathT2ADIrUUTdRHsLscvkEA35VtFQtWdJLtEg +sso1J7viV9YDU4niPSdHPj3ubBjAExej4qCOzatsIQ0CgYEA8L5S3ojy89g7q6vB +QuusIrjGkyM1yebDWqhEnjvlMpfrU1hCS90BM1ozZ28bjz/7PBimKL+A8BO+W0m4 +2s9YbZP5aGwo18Iq86XEdtDgWtQ3NXbYkb8F8LNtyevC/UlAI/xyIRr7hDYlr/1v +jJg16DXiNLyk+uj4Q3EuwzNl8n8CgYEA8B5UUkOiufPtm+ZOq9AlBpIa+NYaahZM +h52jzMTKsFB18xsZU/ufvpKvXEu1sTeCDRo3JAHmiA6AG292Zc7W+uWRtMtlmQWE +wnoZ6hKvEkFnArLCY6Nm5Qqm1wipLwDVO3dD/CDL86siHrXK4wU7Q+bp6xbt8lDi +itz5F7p7HKMCgYAoj8iimexlTU9wczXSsqaECyHZ9JrBc9ICWkuFZY4OYi5SEpLI ++WmUX2Q9zyiTkDIiQ/zq7KkqygjOlLNCmqDJhZ8GCwMupxZZitp5MmQ6qXrL1URT ++h1kGrcqyEBIMKlP5t7L2SH7eqwK5OaAh7y9bSa5v/cEF3CM3GsGlIhevQKBgBGU +RtwW84zlnNmzDMNrY6qNe8gH9LsbktLC6cEOD0DFQz1fGIWbgGB1YL1DFbQ5uh23 +c54BPZ1sYlif2m0trXOE5xvzYCbJzqRmSAto/sQ5YY9DAxREXD4cf4ZyreAxEWtf +Ge0VgZj/SGozKP1h3qrj9vAtJ5J79XnxH5NrJaQ9AoGBAM2rQrt8H2kizg4wMGRZ +0G3709W7xxlbPdm+i/jFVDayJswCr0+eMm4gGyyZL3135D0fcijxytKgg3/OpOJF +jC9vsHsE2K1ATp6eYvYjrhqJHI1m44aq/h46SfajytZQjwMT/jaApULDP2/fCBm5 +6eS2WCyHyrYJyrgoYQF56nsT +-----END PRIVATE KEY----- diff --git a/packages/core/acme-client/examples/tls-alpn-01/README.md b/packages/core/acme-client/examples/tls-alpn-01/README.md new file mode 100644 index 00000000..40863519 --- /dev/null +++ b/packages/core/acme-client/examples/tls-alpn-01/README.md @@ -0,0 +1,44 @@ +# tls-alpn-01 + +Responding to `tls-alpn-01` challenges using Node.js is a bit more involved than the other two challenge types, and requires a proxy (f.ex. [Nginx](https://nginx.org) or [HAProxy](https://www.haproxy.org)) in front of the Node.js service. The reason for this is that `tls-alpn-01` is solved by responding to the ACME challenge using self-signed certificates with an ALPN extension containing the challenge response. + +Since we don't want users of our application to be served with these self-signed certificates, we need to split the HTTPS traffic into two different Node.js backends - one that only serves ALPN certificates for challenge responses, and the other for actual end-user traffic that serves certificates retrieved from the ACME provider. As far as I *(library author)* know, routing HTTPS traffic based on ALPN protocol can not be done purely using Node.js. + +The end result should look something like this: + +```text +Nginx or HAProxy (0.0.0.0:443) + *inspect requests SSL ALPN protocol* + If ALPN == acme-tls/1 + -> Node.js ALPN responder (127.0.0.1:4444) + Else + -> Node.js HTTPS server (127.0.0.1:4443) +``` + +Example proxy configuration: + +* [haproxy.cfg](haproxy.cfg) *(requires HAProxy >= v1.9.1)* +* [nginx.conf](nginx.conf) *(requires [ngx_stream_ssl_preread_module](https://nginx.org/en/docs/stream/ngx_stream_ssl_preread_module.html))* + +Big thanks to [acme.sh](https://github.com/acmesh-official/acme.sh) and [dehydrated](https://github.com/dehydrated-io/dehydrated) for doing the legwork and providing Nginx and HAProxy config examples. + +## How it works + +When solving `tls-alpn-01` challenges, you prove ownership of a domain name by serving a specially crafted certificate over HTTPS. The ACME authority provides the client with a token that is placed into the certificates `id-pe-acmeIdentifier` extension along with a thumbprint of your account key. + +Once the order is finalized, the ACME authority will verify by sending HTTPS requests to your domain with the `acme-tls/1` ALPN protocol, indicating to the server that it should serve the challenge response certificate. If the `id-pe-acmeIdentifier` extension contains the correct payload, the challenge is valid. + +## Pros and cons + +* Challenge must be satisfied using port 443 (HTTPS) +* Useful in instances where port 80 is unavailable +* Can not be used to issue wildcard certificates +* More complex than `http-01`, can not be solved purely using Node.js +* If using multiple web servers, all of them need to respond with the correct certificate + +## External links + +* [https://letsencrypt.org/docs/challenge-types/#tls-alpn-01](https://letsencrypt.org/docs/challenge-types/#tls-alpn-01) +* [https://github.com/dehydrated-io/dehydrated/blob/master/docs/tls-alpn.md](https://github.com/dehydrated-io/dehydrated/blob/master/docs/tls-alpn.md) +* [https://github.com/acmesh-official/acme.sh/wiki/TLS-ALPN-without-downtime](https://github.com/acmesh-official/acme.sh/wiki/TLS-ALPN-without-downtime) +* [https://datatracker.ietf.org/doc/html/rfc8737](https://datatracker.ietf.org/doc/html/rfc8737) diff --git a/packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg b/packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg new file mode 100644 index 00000000..5a99be7c --- /dev/null +++ b/packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg @@ -0,0 +1,23 @@ +## +# HTTPS listener +# - Send to ALPN responder port 4444 if protocol is acme-tls/1 +# - Default to HTTPS backend port 4443 +## + +frontend https + mode tcp + bind :443 + tcp-request inspect-delay 5s + tcp-request content accept if { req_ssl_hello_type 1 } + use_backend alpnresp if { req.ssl_alpn acme-tls/1 } + default_backend https + +# Default HTTPS backend +backend https + mode tcp + server https 127.0.0.1:4443 + +# ACME tls-alpn-01 responder backend +backend alpnresp + mode tcp + server acmesh 127.0.0.1:4444 diff --git a/packages/core/acme-client/examples/tls-alpn-01/nginx.conf b/packages/core/acme-client/examples/tls-alpn-01/nginx.conf new file mode 100644 index 00000000..cb4aa7b6 --- /dev/null +++ b/packages/core/acme-client/examples/tls-alpn-01/nginx.conf @@ -0,0 +1,19 @@ +## +# HTTPS server +# - Send to ALPN responder port 4444 if protocol is acme-tls/1 +# - Default to HTTPS backend port 4443 +## + +stream { + map $ssl_preread_alpn_protocols $tls_port { + ~\bacme-tls/1\b 4444; + default 4443; + } + + server { + listen 443; + listen [::]:443; + proxy_pass 127.0.0.1:$tls_port; + ssl_preread on; + } +} diff --git a/packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js b/packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js new file mode 100644 index 00000000..e04d73c1 --- /dev/null +++ b/packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js @@ -0,0 +1,180 @@ +/** + * Example using tls-alpn-01 challenge to generate certificates on-demand + */ + +const fs = require('fs'); +const path = require('path'); +const https = require('https'); +const tls = require('tls'); +const acme = require('./../../'); + +const HTTPS_SERVER_PORT = 4443; +const ALPN_RESPONDER_PORT = 4444; +const VALID_DOMAINS = ['example.com', 'example.org']; +const FALLBACK_KEY = fs.readFileSync(path.join(__dirname, '..', 'fallback.key')); +const FALLBACK_CERT = fs.readFileSync(path.join(__dirname, '..', 'fallback.crt')); + +const pendingDomains = {}; +const alpnResponses = {}; +const certificateStore = {}; + +function log(m) { + process.stdout.write(`${(new Date()).toISOString()} ${m}\n`); +} + + +/** + * On-demand certificate generation using tls-alpn-01 + */ + +async function getCertOnDemand(client, servername, attempt = 0) { + /* Invalid domain */ + if (!VALID_DOMAINS.includes(servername)) { + throw new Error(`Invalid domain: ${servername}`); + } + + /* Certificate exists */ + if (servername in certificateStore) { + return certificateStore[servername]; + } + + /* Waiting on certificate order to go through */ + if (servername in pendingDomains) { + if (attempt >= 10) { + throw new Error(`Gave up waiting on certificate for ${servername}`); + } + + await new Promise((resolve) => { setTimeout(resolve, 1000); }); + return getCertOnDemand(client, servername, (attempt + 1)); + } + + /* Create CSR */ + log(`Creating CSR for ${servername}`); + const [key, csr] = await acme.crypto.createCsr({ + commonName: servername + }); + + /* Order certificate */ + log(`Ordering certificate for ${servername}`); + const cert = await client.auto({ + csr, + email: 'test@example.com', + termsOfServiceAgreed: true, + challengePriority: ['tls-alpn-01'], + challengeCreateFn: async (authz, challenge, keyAuthorization) => { + alpnResponses[authz.identifier.value] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization); + }, + challengeRemoveFn: (authz) => { + delete alpnResponses[authz.identifier.value]; + } + }); + + /* Done, store certificate */ + log(`Certificate for ${servername} created successfully`); + certificateStore[servername] = [key, cert]; + delete pendingDomains[servername]; + return certificateStore[servername]; +} + + +/** + * Main + */ + +(async () => { + try { + /** + * Initialize ACME client + */ + + log('Initializing ACME client'); + const client = new acme.Client({ + directoryUrl: acme.directory.letsencrypt.staging, + accountKey: await acme.crypto.createPrivateKey() + }); + + + /** + * ALPN responder + */ + + const alpnResponder = https.createServer({ + /* Fallback cert */ + key: FALLBACK_KEY, + cert: FALLBACK_CERT, + + /* Allow acme-tls/1 ALPN protocol */ + ALPNProtocols: ['acme-tls/1'], + + /* Serve ALPN certificate based on servername */ + SNICallback: async (servername, cb) => { + try { + log(`Handling ALPN SNI request for ${servername}`); + if (!Object.keys(alpnResponses).includes(servername)) { + throw new Error(`No ALPN certificate found for ${servername}`); + } + + /* Serve ALPN challenge response */ + log(`Found ALPN certificate for ${servername}, serving secure context`); + cb(null, tls.createSecureContext({ + key: alpnResponses[servername][0], + cert: alpnResponses[servername][1] + })); + } + catch (e) { + log(`[ERROR] ${e.message}`); + cb(e.message); + } + } + }); + + /* Terminate once TLS handshake has been established */ + alpnResponder.on('secureConnection', (socket) => { + socket.end(); + }); + + alpnResponder.listen(ALPN_RESPONDER_PORT, () => { + log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`); + }); + + + /** + * HTTPS server + */ + + const requestListener = (req, res) => { + log(`HTTP 200 ${req.headers.host}${req.url}`); + res.writeHead(200); + res.end('Hello world\n'); + }; + + const httpsServer = https.createServer({ + /* Fallback cert */ + key: FALLBACK_KEY, + cert: FALLBACK_CERT, + + /* Serve certificate based on servername */ + SNICallback: async (servername, cb) => { + try { + log(`Handling SNI request for ${servername}`); + const [key, cert] = await getCertOnDemand(client, servername); + + log(`Found certificate for ${servername}, serving secure context`); + cb(null, tls.createSecureContext({ key, cert })); + } + catch (e) { + log(`[ERROR] ${e.message}`); + cb(e.message); + } + } + }, requestListener); + + httpsServer.listen(HTTPS_SERVER_PORT, () => { + log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`); + }); + } + catch (e) { + log(`[FATAL] ${e.message}`); + process.exit(1); + } +})(); diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json index 86bd44bc..771bb55f 100644 --- a/packages/core/acme-client/package.json +++ b/packages/core/acme-client/package.json @@ -37,8 +37,7 @@ "lint": "eslint .", "lint-types": "tsd", "prepublishOnly": "npm run build-docs", - "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"", - "test-local": "/bin/bash scripts/run-tests.sh" + "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"" }, "repository": { "type": "git", diff --git a/packages/core/acme-client/scripts/run-tests.sh b/packages/core/acme-client/scripts/run-tests.sh deleted file mode 100644 index c2f01f0d..00000000 --- a/packages/core/acme-client/scripts/run-tests.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/bin/bash -# -# Run test suite locally using CircleCI CLI. -# -set -eu - -JOBS=("$@") - -CIRCLECI_CLI_URL="https://github.com/CircleCI-Public/circleci-cli/releases/download/v0.1.29936/circleci-cli_0.1.29936_linux_amd64.tar.gz" -CIRCLECI_CLI_SHASUM="fdc8da76111facae4a10f3717502eeb5d78db0256ef94a2f8d53078978175d40" -CIRCLECI_CLI_PATH="/tmp/circleci-cli" -CIRCLECI_CLI_BIN="${CIRCLECI_CLI_PATH}/circleci" - -PROJECT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && cd .. && pwd )" -CONFIG_PATH="${PROJECT_DIR}/.circleci/.temp.yml" - -# Run all jobs by default -if [[ ${#JOBS[@]} -eq 0 ]]; then - JOBS=( - "v16" - "v18" - "v20" - "eab-v16" - "eab-v18" - "eab-v20" - ) -fi - -# Download CircleCI CLI -if [[ ! -f "${CIRCLECI_CLI_BIN}" ]]; then - echo "[-] Downloading CircleCI cli" - mkdir -p "${CIRCLECI_CLI_PATH}" - wget -nv "${CIRCLECI_CLI_URL}" -O "${CIRCLECI_CLI_PATH}/circleci-cli.tar.gz" - echo "${CIRCLECI_CLI_SHASUM} *${CIRCLECI_CLI_PATH}/circleci-cli.tar.gz" | sha256sum -c - tar zxvf "${CIRCLECI_CLI_PATH}/circleci-cli.tar.gz" -C "${CIRCLECI_CLI_PATH}" --strip-components=1 -fi - -# Disable CircleCI update checks and telemetry -export CIRCLECI_CLI_SKIP_UPDATE_CHECK="true" -export CIRCLECI_CLI_TELEMETRY_OPTOUT="1" - -# Run test suite -echo "[-] Running test suite" -$CIRCLECI_CLI_BIN config process "${PROJECT_DIR}/.circleci/config.yml" > "${CONFIG_PATH}" -$CIRCLECI_CLI_BIN config validate -c "${CONFIG_PATH}" - -for job in "${JOBS[@]}"; do - echo "[-] Running job: ${job}" - $CIRCLECI_CLI_BIN local execute -c "${CONFIG_PATH}" "${job}" - echo "[+] ${job} completed successfully" -done - -# Clean up -if [[ -f "${CONFIG_PATH}" ]]; then - rm "${CONFIG_PATH}" -fi - -echo "[+] Test suite ran successfully!" -exit 0 diff --git a/packages/core/acme-client/scripts/test-suite-install-step.sh b/packages/core/acme-client/scripts/test-suite-install-step.sh deleted file mode 100644 index 5de092a5..00000000 --- a/packages/core/acme-client/scripts/test-suite-install-step.sh +++ /dev/null @@ -1,20 +0,0 @@ -#!/bin/bash -# -# Install and init step-ca for testing. -# -set -eu - -# Download and install -wget -nv "https://dl.step.sm/gh-release/certificates/gh-release-header/v${STEPCA_VERSION}/step-ca_${STEPCA_VERSION}_amd64.deb" -O /tmp/step-ca.deb -wget -nv "https://dl.step.sm/gh-release/cli/gh-release-header/v${STEPCLI_VERSION}/step-cli_${STEPCLI_VERSION}_amd64.deb" -O /tmp/step-cli.deb - -sudo dpkg -i /tmp/step-ca.deb -sudo dpkg -i /tmp/step-cli.deb - -# Initialize -echo "hunter2" > /tmp/password - -step ca init --name="Example Inc." --dns="localhost" --address="127.0.0.1:8443" --provisioner="test@example.com" --password-file="/tmp/password" -step ca provisioner add acme --type ACME - -exit 0 diff --git a/packages/core/acme-client/test/00-pebble.spec.js b/packages/core/acme-client/test/00-pebble.spec.js index 7a26c3b7..992fc6bd 100644 --- a/packages/core/acme-client/test/00-pebble.spec.js +++ b/packages/core/acme-client/test/00-pebble.spec.js @@ -199,7 +199,7 @@ describe('pebble', () => { }); it('should timeout challenge response', async () => { - await assert.isRejected(retrieveTlsAlpnCertificate('example.org', tlsAlpnPort, 500), /timed out/); + await assert.isRejected(retrieveTlsAlpnCertificate('example.org', tlsAlpnPort, 500)); }); it('should add challenge response', async () => {