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 db3ef431..00000000
--- a/packages/core/acme-client/.circleci/config.yml
+++ /dev/null
@@ -1,133 +0,0 @@
----
-version: 2.1
-
-commands:
- pre:
- steps:
- - run: node --version
- - run: npm --version
- - run: yarn --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.8.6
- PEBBLECTS_DNS_PORT: 8053
-
- - run:
- name: Start CoreDNS
- command: sudo coredns -p 53 -conf /etc/coredns/Corefile
- background: true
-
- test:
- steps:
- - run: yarn --color
- - run: yarn run lint --color
- - run: yarn run lint-types
- - run: yarn run build-docs
-
- - run:
- command: yarn run test --color
- environment:
- ACME_DOMAIN_NAME: test.example.com
- ACME_CHALLTESTSRV_URL: http://127.0.0.1:8055
- ACME_DNS_RESOLVER: 127.0.0.1
- ACME_TLSALPN_PORT: 5001
- ACME_HTTP_PORT: 5002
- ACME_HTTPS_PORT: 5003
-
-jobs:
- v16: { docker: [{ image: cimg/node:16.16 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]}
- v18: { docker: [{ image: cimg/node:18.4 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]}
- eab-v16: { docker: [{ image: cimg/node:16.16 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]}
- eab-v18: { docker: [{ image: cimg/node:18.4 }], 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
- - eab-v16
- - eab-v18
- # - step-v12
diff --git a/packages/core/acme-client/.editorconfig b/packages/core/acme-client/.editorconfig
index f95adc8f..7660a90c 100644
--- a/packages/core/acme-client/.editorconfig
+++ b/packages/core/acme-client/.editorconfig
@@ -5,7 +5,7 @@
root = true
[*]
-indent_style = spaces
+indent_style = space
indent_size = 4
trim_trailing_whitespace = true
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/.gitignore b/packages/core/acme-client/.gitignore
index cbb386fa..6a3d9710 100644
--- a/packages/core/acme-client/.gitignore
+++ b/packages/core/acme-client/.gitignore
@@ -1,7 +1,5 @@
.vscode/
node_modules/
npm-debug.log
-yarn-error.log
-yarn.lock
package-lock.json
/.idea/
diff --git a/packages/core/acme-client/.yarnrc b/packages/core/acme-client/.yarnrc
deleted file mode 100644
index 8d6f153a..00000000
--- a/packages/core/acme-client/.yarnrc
+++ /dev/null
@@ -1,2 +0,0 @@
-ignore-engines true
-ignore-optional true
diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md
index e9de6db1..6fc2a30b 100644
--- a/packages/core/acme-client/CHANGELOG.md
+++ b/packages/core/acme-client/CHANGELOG.md
@@ -1,81 +1,22 @@
-# Change Log
-
-All notable changes to this project will be documented in this file.
-See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
-
-## [1.20.2](https://github.com/publishlab/node-acme-client/compare/v1.2.1...v1.20.2) (2024-02-28)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.2.1](https://github.com/publishlab/node-acme-client/compare/v1.2.0...v1.2.1) (2023-12-12)
-
-**Note:** Version bump only for package @certd/acme-client
-
-**Note:** Version bump only for package @certd/acme-client
-
-# [1.2.0](https://github.com/publishlab/node-acme-client/compare/v1.1.6...v1.2.0) (2023-10-27)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.1.6](https://github.com/publishlab/node-acme-client/compare/v1.1.5...v1.1.6) (2023-07-10)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.1.5](https://github.com/publishlab/node-acme-client/compare/v1.1.4...v1.1.5) (2023-07-03)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.1.4](https://github.com/publishlab/node-acme-client/compare/v1.1.3...v1.1.4) (2023-07-03)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.1.3](https://github.com/publishlab/node-acme-client/compare/v1.1.2...v1.1.3) (2023-07-03)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.1.2](https://github.com/publishlab/node-acme-client/compare/v1.1.1...v1.1.2) (2023-07-03)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.1.1](https://github.com/publishlab/node-acme-client/compare/v1.1.0...v1.1.1) (2023-06-28)
-
-**Note:** Version bump only for package @certd/acme-client
-
-# [1.1.0](https://github.com/publishlab/node-acme-client/compare/v1.0.6...v1.1.0) (2023-06-28)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.0.6](https://github.com/publishlab/node-acme-client/compare/v1.0.5...v1.0.6) (2023-05-25)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.0.5](https://github.com/publishlab/node-acme-client/compare/v1.0.4...v1.0.5) (2023-05-25)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.0.4](https://github.com/publishlab/node-acme-client/compare/v1.0.3...v1.0.4) (2023-05-25)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.0.3](https://github.com/publishlab/node-acme-client/compare/v1.0.2...v1.0.3) (2023-05-25)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.0.2](https://github.com/publishlab/node-acme-client/compare/v1.0.1...v1.0.2) (2023-05-24)
-
-**Note:** Version bump only for package @certd/acme-client
-
-## [1.0.1](https://github.com/publishlab/node-acme-client/compare/v1.0.0...v1.0.1) (2023-05-24)
-
-**Note:** Version bump only for package @certd/acme-client
-
# Changelog
-## Important upgrade notice
+## v5.3.0 (2024-02-05)
-On September 15, 2022, Let's Encrypt will stop accepting Certificate Signing Requests signed using the obsolete SHA-1 hash. This change affects all `acme-client` versions lower than `3.3.2` and `4.2.4`. Please upgrade ASAP to ensure that your certificates can still be issued following this date.
+* `added` Support and tests for satisfying `tls-alpn-01` challenges
+* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR generation and parsing
+* `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge
+ * Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
+ * This change is not considered breaking since the previous behavior was incorrect
-A more detailed explanation can be found [at the Let's Encrypt forums](https://community.letsencrypt.org/t/rejecting-sha-1-csrs-and-validation-using-tls-1-0-1-1-urls/175144).
+## v5.2.0 (2024-01-22)
+
+* `fixed` Allow self-signed or invalid certs when validating `http-01` challenges that redirect to HTTPS - [#65](https://github.com/publishlab/node-acme-client/issues/65)
+* `fixed` Wait for all challenge promises to settle before rejecting `client.auto()` - [#75](https://github.com/publishlab/node-acme-client/issues/75)
+
+## v5.1.0 (2024-01-20)
+
+* `fixed` Upgrade `jsrsasign@11.0.0` - [GHSA-rh63-9qcf-83gf](https://github.com/kjur/jsrsasign/security/advisories/GHSA-rh63-9qcf-83gf)
+* `fixed` Upgrade `axios@1.6.5` - [CVE-2023-45857](https://cve.mitre.org/cgi-bin/cvename.cgi?name=2023-45857)
## v5.0.0 (2022-07-28)
@@ -114,13 +55,13 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
## v4.2.0 (2022-01-06)
-* `added` Support for external account binding - [RFC 8555 Section 7.3.4](https://tools.ietf.org/html/rfc8555#section-7.3.4)
+* `added` Support for external account binding - [RFC 8555 Section 7.3.4](https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4)
* `added` Ability to pass through custom logger function
* `changed` Increase default `backoffAttempts` to 10
* `fixed` Deactivate authorizations where challenges can not be completed
* `fixed` Attempt authoritative name servers when verifying `dns-01` challenges
* `fixed` Error verbosity when failing to read ACME directory
-* `fixed` Correctly recognize `ready` and `processing` states - [RFC 8555 Section 7.1.6](https://tools.ietf.org/html/rfc8555#section-7.1.6)
+* `fixed` Correctly recognize `ready` and `processing` states - [RFC 8555 Section 7.1.6](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.6)
## v4.1.4 (2021-12-23)
@@ -170,7 +111,7 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
## v3.3.0 (2019-12-19)
* `added` TypeScript definitions
-* `fixed` Allow missing ACME directory meta field - [RFC 8555 Section 7.1.1](https://tools.ietf.org/html/rfc8555#section-7.1.1)
+* `fixed` Allow missing ACME directory meta field - [RFC 8555 Section 7.1.1](https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1)
## v3.2.1 (2019-11-14)
@@ -181,10 +122,10 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
* `added` More extensive testing using [letsencrypt/pebble](https://github.com/letsencrypt/pebble)
* `changed` When creating a CSR, `commonName` no longer defaults to `'localhost'`
* This change is not considered breaking since `commonName: 'localhost'` will result in an error when ordering a certificate
-* `fixed` Retry signed API requests on `urn:ietf:params:acme:error:badNonce` - [RFC 8555 Section 6.5](https://tools.ietf.org/html/rfc8555#section-6.5)
+* `fixed` Retry signed API requests on `urn:ietf:params:acme:error:badNonce` - [RFC 8555 Section 6.5](https://datatracker.ietf.org/doc/html/rfc8555#section-6.5)
* `fixed` Minor bugs related to `POST-as-GET` when calling `updateAccount()`
* `fixed` Ensure subject common name is present in SAN when creating a CSR - [CAB v1.2.3 Section 9.2.2](https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf)
-* `fixed` Send empty JSON body when responding to challenges - [RFC 8555 Section 7.5.1](https://tools.ietf.org/html/rfc8555#section-7.5.1)
+* `fixed` Send empty JSON body when responding to challenges - [RFC 8555 Section 7.5.1](https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1)
## v2.3.1 (2019-08-26)
@@ -193,8 +134,8 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
## v3.1.0 (2019-08-21)
-* `added` UTF-8 support when generating a CSR subject using forge - [RFC 5280](https://tools.ietf.org/html/rfc5280)
-* `fixed` Implement `POST-as-GET` for all ACME API requests - [RFC 8555 Section 6.3](https://tools.ietf.org/html/rfc8555#section-6.3)
+* `added` UTF-8 support when generating a CSR subject using forge - [RFC 5280](https://datatracker.ietf.org/doc/html/rfc5280)
+* `fixed` Implement `POST-as-GET` for all ACME API requests - [RFC 8555 Section 6.3](https://datatracker.ietf.org/doc/html/rfc8555#section-6.3)
## v2.3.0 (2019-08-21)
@@ -231,7 +172,7 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
## v2.0.1 (2018-08-17)
-* `fixed` Key rollover in compliance with [draft-ietf-acme-13](https://tools.ietf.org/html/draft-ietf-acme-acme-13)
+* `fixed` Key rollover in compliance with [draft-ietf-acme-13](https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-13)
## v2.0.0 (2018-04-02)
diff --git a/packages/core/acme-client/LICENSE b/packages/core/acme-client/LICENSE
index 7c8adf14..7f47c1a7 100644
--- a/packages/core/acme-client/LICENSE
+++ b/packages/core/acme-client/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017-2022 Publish Lab
+Copyright (c) 2017-2024 Labrador CMS AS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/core/acme-client/README.md b/packages/core/acme-client/README.md
index 88af9efe..3dff3cfa 100644
--- a/packages/core/acme-client/README.md
+++ b/packages/core/acme-client/README.md
@@ -1,21 +1,13 @@
-# acme-client [](https://circleci.com/gh/publishlab/node-acme-client)
+# acme-client [](https://github.com/publishlab/node-acme-client/actions/workflows/tests.yml)
*A simple and unopinionated ACME client.*
This module is written to handle communication with a Boulder/Let's Encrypt-style ACME API.
-* RFC 8555 - Automatic Certificate Management Environment (ACME): [https://tools.ietf.org/html/rfc8555](https://tools.ietf.org/html/rfc8555)
+* RFC 8555 - Automatic Certificate Management Environment (ACME): [https://datatracker.ietf.org/doc/html/rfc8555](https://datatracker.ietf.org/doc/html/rfc8555)
* Boulder divergences from ACME: [https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md)
-
-## Important upgrade notice
-
-On September 15, 2022, Let's Encrypt will stop accepting Certificate Signing Requests signed using the obsolete SHA-1 hash. This change affects all `acme-client` versions lower than `3.3.2` and `4.2.4`. Please upgrade ASAP to ensure that your certificates can still be issued following this date.
-
-A more detailed explanation can be found [at the Let's Encrypt forums](https://community.letsencrypt.org/t/rejecting-sha-1-csrs-and-validation-using-tls-1-0-1-1-urls/175144).
-
-
-### Compatibility
+## Compatibility
| acme-client | Node.js | |
| ------------- | --------- | ----------------------------------------- |
@@ -25,8 +17,7 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
| v2.x | >= v4 | [Changelog](CHANGELOG.md#v200-2018-04-02) |
| v1.x | >= v4 | [Changelog](CHANGELOG.md#v100-2017-10-20) |
-
-### Table of contents
+## Table of contents
* [Installation](#installation)
* [Usage](#usage)
@@ -43,14 +34,12 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
* [Debugging](#debugging)
* [License](#license)
-
## Installation
```bash
$ npm install acme-client
```
-
## Usage
```js
@@ -64,7 +53,6 @@ const client = new acme.Client({
});
```
-
### Directory URLs
```js
@@ -77,10 +65,9 @@ acme.directory.letsencrypt.production;
acme.directory.zerossl.production;
```
-
### External account binding
-To enable [external account binding](https://tools.ietf.org/html/rfc8555#section-7.3.4) when creating your ACME account, provide your KID and HMAC key to the client constructor.
+To enable [external account binding](https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.4) when creating your ACME account, provide your KID and HMAC key to the client constructor.
```js
const client = new acme.Client({
@@ -93,7 +80,6 @@ const client = new acme.Client({
});
```
-
### Specifying the account URL
During the ACME account creation process, the server will check the supplied account key and either create a new account if the key is unused, or return the existing ACME account bound to that key.
@@ -114,14 +100,13 @@ You can fetch the clients current account URL, either after creating an account
const myAccountUrl = client.getAccountUrl();
```
-
## Cryptography
-For key pairs `acme-client` utilizes native Node.js cryptography APIs, supporting signing and generation of both RSA and ECDSA keys. The module [jsrsasign](https://www.npmjs.com/package/jsrsasign) is used to generate and parse Certificate Signing Requests.
+For key pairs `acme-client` utilizes native Node.js cryptography APIs, supporting signing and generation of both RSA and ECDSA keys. The module [@peculiar/x509](https://www.npmjs.com/package/@peculiar/x509) is used to generate and parse Certificate Signing Requests.
These utility methods are exposed through `.crypto`.
-* __Documentation: [docs/crypto.md](docs/crypto.md)__
+* **Documentation: [docs/crypto.md](docs/crypto.md)**
```js
const privateRsaKey = await acme.crypto.createPrivateRsaKey();
@@ -133,22 +118,20 @@ const [certificateKey, certificateCsr] = await acme.crypto.createCsr({
});
```
-
### Legacy `.forge` interface
The legacy `node-forge` crypto interface is still available for backward compatibility, however this interface is now considered deprecated and will be removed in a future major version of `acme-client`.
You should consider migrating to the new `.crypto` API at your earliest convenience. More details can be found in the [acme-client v5 upgrade guide](docs/upgrade-v5.md).
-* __Documentation: [docs/forge.md](docs/forge.md)__
-
+* **Documentation: [docs/forge.md](docs/forge.md)**
## Auto mode
For convenience an `auto()` method is included in the client that takes a single config object. This method will handle the entire process of getting a certificate for one or multiple domains.
-* __Documentation: [docs/client.md#AcmeClient+auto](docs/client.md#AcmeClient+auto)__
-* __Full example: [examples/auto.js](examples/auto.js)__
+* **Documentation: [docs/client.md#AcmeClient+auto](docs/client.md#AcmeClient+auto)**
+* **Full example: [examples/auto.js](examples/auto.js)**
```js
const autoOpts = {
@@ -162,12 +145,11 @@ const autoOpts = {
const certificate = await client.auto(autoOpts);
```
-
### Challenge priority
When ordering a certificate using auto mode, `acme-client` uses a priority list when selecting challenges to respond to. Its default value is `['http-01', 'dns-01']` which translates to "use `http-01` if any challenges exist, otherwise fall back to `dns-01`".
-While most challenges can be validated using the method of your choosing, please note that __wildcard certificates can only be validated through `dns-01`__. More information regarding Let's Encrypt challenge types [can be found here](https://letsencrypt.org/docs/challenge-types/).
+While most challenges can be validated using the method of your choosing, please note that **wildcard certificates can only be validated through `dns-01`**. More information regarding Let's Encrypt challenge types [can be found here](https://letsencrypt.org/docs/challenge-types/).
To modify challenge priority, provide a list of challenge types in `challengePriority`:
@@ -178,7 +160,6 @@ await client.auto({
});
```
-
### Internal challenge verification
When using auto mode, `acme-client` will first validate that challenges are satisfied internally before completing the challenge at the ACME provider. In some cases (firewalls, etc) this internal challenge verification might not be possible to complete.
@@ -194,13 +175,12 @@ await client.auto({
});
```
-
## API
For more fine-grained control you can interact with the ACME API using the methods documented below.
-* __Documentation: [docs/client.md](docs/client.md)__
-* __Full example: [examples/api.js](examples/api.js)__
+* **Documentation: [docs/client.md](docs/client.md)**
+* **Full example: [examples/api.js](examples/api.js)**
```js
const account = await client.createAccount({
@@ -216,7 +196,6 @@ const order = await client.createOrder({
});
```
-
## HTTP client defaults
This module uses [axios](https://github.com/axios/axios) when communicating with the ACME HTTP API, and exposes the client instance through `.axios`.
@@ -237,7 +216,6 @@ A complete list of axios options and documentation can be found at:
* [https://github.com/axios/axios#request-config](https://github.com/axios/axios#request-config)
* [https://github.com/axios/axios#custom-instance-defaults](https://github.com/axios/axios#custom-instance-defaults)
-
## Debugging
To get a better grasp of what `acme-client` is doing behind the scenes, you can either pass it a logger function, or enable debugging through an environment variable.
@@ -256,7 +234,6 @@ Debugging to the console can also be enabled through [debug](https://www.npmjs.c
DEBUG=acme-client node index.js
```
-
## License
[MIT](LICENSE)
diff --git a/packages/core/acme-client/docs/client.md b/packages/core/acme-client/docs/client.md
index 65dc325a..f5f29d22 100644
--- a/packages/core/acme-client/docs/client.md
+++ b/packages/core/acme-client/docs/client.md
@@ -132,7 +132,7 @@ catch (e) {
### acmeClient.createAccount([data]) ⇒ Promise.<object>
Create a new account
-https://tools.ietf.org/html/rfc8555#section-7.3
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Account
@@ -161,7 +161,7 @@ const account = await client.createAccount({
### acmeClient.updateAccount([data]) ⇒ Promise.<object>
Update existing account
-https://tools.ietf.org/html/rfc8555#section-7.3.2
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Account
@@ -182,7 +182,7 @@ const account = await client.updateAccount({
### acmeClient.updateAccountKey(newAccountKey, [data]) ⇒ Promise.<object>
Update account private key
-https://tools.ietf.org/html/rfc8555#section-7.3.5
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Account
@@ -203,7 +203,7 @@ const result = await client.updateAccountKey(newAccountKey);
### acmeClient.createOrder(data) ⇒ Promise.<object>
Create a new order
-https://tools.ietf.org/html/rfc8555#section-7.4
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Order
@@ -227,7 +227,7 @@ const order = await client.createOrder({
### acmeClient.getOrder(order) ⇒ Promise.<object>
Refresh order object from CA
-https://tools.ietf.org/html/rfc8555#section-7.4
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Order
@@ -246,7 +246,7 @@ const result = await client.getOrder(order);
### acmeClient.finalizeOrder(order, csr) ⇒ Promise.<object>
Finalize order
-https://tools.ietf.org/html/rfc8555#section-7.4
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Order
@@ -268,7 +268,7 @@ const result = await client.finalizeOrder(order, csr);
### acmeClient.getAuthorizations(order) ⇒ Promise.<Array.<object>>
Get identifier authorizations from order
-https://tools.ietf.org/html/rfc8555#section-7.5
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<Array.<object>>
- Authorizations
@@ -292,7 +292,7 @@ authorizations.forEach((authz) => {
### acmeClient.deactivateAuthorization(authz) ⇒ Promise.<object>
Deactivate identifier authorization
-https://tools.ietf.org/html/rfc8555#section-7.5.2
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Authorization
@@ -312,7 +312,7 @@ const result = await client.deactivateAuthorization(authz);
### acmeClient.getChallengeKeyAuthorization(challenge) ⇒ Promise.<string>
Get key authorization for ACME challenge
-https://tools.ietf.org/html/rfc8555#section-8.1
+https://datatracker.ietf.org/doc/html/rfc8555#section-8.1
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<string>
- Key authorization
@@ -353,7 +353,7 @@ await client.verifyChallenge(authz, challenge);
### acmeClient.completeChallenge(challenge) ⇒ Promise.<object>
Notify CA that challenge has been completed
-https://tools.ietf.org/html/rfc8555#section-7.5.1
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Challenge
@@ -373,7 +373,7 @@ const result = await client.completeChallenge(challenge);
### acmeClient.waitForValidStatus(item) ⇒ Promise.<object>
Wait for ACME provider to verify status on a order, authorization or challenge
-https://tools.ietf.org/html/rfc8555#section-7.5.1
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Valid order, authorization or challenge
@@ -389,7 +389,7 @@ const challenge = { ... };
await client.waitForValidStatus(challenge);
```
**Example**
-Wait for valid authoriation status
+Wait for valid authorization status
```js
const authz = { ... };
await client.waitForValidStatus(authz);
@@ -405,7 +405,7 @@ await client.waitForValidStatus(order);
### acmeClient.getCertificate(order, [preferredChain]) ⇒ Promise.<string>
Get certificate from ACME order
-https://tools.ietf.org/html/rfc8555#section-7.4.2
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<string>
- Certificate
@@ -432,7 +432,7 @@ const certificate = await client.getCertificate(order, 'DST Root CA X3');
### acmeClient.revokeCertificate(cert, [data]) ⇒ Promise
Revoke certificate
-https://tools.ietf.org/html/rfc8555#section-7.6
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
**Kind**: instance method of [AcmeClient
](#AcmeClient)
diff --git a/packages/core/acme-client/docs/crypto.md b/packages/core/acme-client/docs/crypto.md
index 1391263c..84660f68 100644
--- a/packages/core/acme-client/docs/crypto.md
+++ b/packages/core/acme-client/docs/crypto.md
@@ -25,7 +25,7 @@
Get a JSON Web Key derived from a RSA or ECDSA key
array
Array.<string>
Split chain of PEM encoded objects from string into array
string
Promise.<Array.<buffer>>
Create a Certificate Signing Request
Promise.<Array.<buffer>>
Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
+ +boolean
Validate that a ALPN certificate contains the expected key authorization
+array
+## splitPemChain(chainPem) ⇒ Array.<string>
Split chain of PEM encoded objects from string into array
**Kind**: global function
-**Returns**: array
- Array of PEM objects including headers
+**Returns**: Array.<string>
- Array of PEM objects including headers
| Param | Type | Description |
| --- | --- | --- |
@@ -219,14 +226,14 @@ Create a Certificate Signing Request
| data | object
| |
| [data.keySize] | number
| Size of newly created RSA private key modulus in bits, default: `2048` |
| [data.commonName] | string
| FQDN of your server |
-| [data.altNames] | array
| SAN (Subject Alternative Names), default: `[]` |
+| [data.altNames] | Array.<string>
| SAN (Subject Alternative Names), default: `[]` |
| [data.country] | string
| 2 letter country code |
| [data.state] | string
| State or province |
| [data.locality] | string
| City |
| [data.organization] | string
| Organization name |
| [data.organizationUnit] | string
| Organizational unit name |
| [data.emailAddress] | string
| Email address |
-| [keyPem] | string
| PEM encoded CSR private key |
+| [keyPem] | buffer
\| string
| PEM encoded CSR private key |
**Example**
Create a Certificate Signing Request
@@ -265,3 +272,42 @@ const certificateKey = await acme.crypto.createPrivateEcdsaKey();
const [, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com'
}, certificateKey);
+
+
+## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ Promise.<Array.<buffer>>
+Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
+
+https://datatracker.ietf.org/doc/html/rfc8737
+
+**Kind**: global function
+**Returns**: Promise.<Array.<buffer>>
- [privateKey, certificate]
+
+| Param | Type | Description |
+| --- | --- | --- |
+| authz | object
| Identifier authorization |
+| keyAuthorization | string
| Challenge key authorization |
+| [keyPem] | buffer
\| string
| PEM encoded CSR private key |
+
+**Example**
+Create a ALPN certificate
+```js
+const [alpnKey, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
+```
+**Example**
+Create a ALPN certificate with ECDSA private key
+```js
+const alpnKey = await acme.crypto.createPrivateEcdsaKey();
+const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
+
+
+## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ boolean
+Validate that a ALPN certificate contains the expected key authorization
+
+**Kind**: global function
+**Returns**: boolean
- True when valid
+
+| Param | Type | Description |
+| --- | --- | --- |
+| certPem | buffer
\| string
| PEM encoded certificate |
+| keyAuthorization | string
| Expected challenge key authorization |
+
diff --git a/packages/core/acme-client/docs/forge.md b/packages/core/acme-client/docs/forge.md
index 2d7601fd..09a44de1 100644
--- a/packages/core/acme-client/docs/forge.md
+++ b/packages/core/acme-client/docs/forge.md
@@ -209,7 +209,7 @@ Create a Certificate Signing Request
| data | object
| |
| [data.keySize] | number
| Size of newly created private key, default: `2048` |
| [data.commonName] | string
| |
-| [data.altNames] | array
| default: `[]` |
+| [data.altNames] | Array.<string>
| default: `[]` |
| [data.country] | string
| |
| [data.state] | string
| |
| [data.locality] | string
| |
diff --git a/packages/core/acme-client/docs/upgrade-v5.md b/packages/core/acme-client/docs/upgrade-v5.md
index a9156244..f34fd6e6 100644
--- a/packages/core/acme-client/docs/upgrade-v5.md
+++ b/packages/core/acme-client/docs/upgrade-v5.md
@@ -4,10 +4,9 @@ This document outlines the breaking changes introduced in v5 of `acme-client`, w
First off this release drops support for Node LTS v10, v12 and v14, and the reason for that is a new native crypto interface - more on that below. Since Node v14 is still currently in maintenance mode, `acme-client` v4 will continue to receive security updates and bugfixes until (at least) Node v14 reaches its end-of-line.
-
## New native crypto interface
-A new crypto interface has been introduced with v5, which you can find under `acme.crypto`. It uses native Node.js cryptography APIs to generate private keys, JSON Web Keys and signatures, and finally enables support for ECC/ECDSA (P-256, P384 and P521), both for account private keys and certificates. The [jsrsasign](https://www.npmjs.com/package/jsrsasign) module is used to handle generation and parsing of Certificate Signing Requests.
+A new crypto interface has been introduced with v5, which you can find under `acme.crypto`. It uses native Node.js cryptography APIs to generate private keys, JSON Web Keys and signatures, and finally enables support for ECC/ECDSA (P-256, P384 and P521), both for account private keys and certificates. The [@peculiar/x509](https://www.npmjs.com/package/@peculiar/x509) module is used to handle generation and parsing of Certificate Signing Requests.
Full documentation of `acme.crypto` can be [found here](crypto.md).
@@ -17,9 +16,9 @@ Below you will find a table summarizing the current `acme.forge` methods, and th
*Note: The now deprecated `acme.forge` interface is still available for use in v5, and will not be removed until a future major version, most likely v6. Should you not wish to change to the new interface right away, the following breaking changes will not immediately affect you.*
-- :green_circle: = API functionality unchanged between `acme.forge` and `acme.crypto`
-- :orange_circle: = Slight API changes, like depromising or renaming, action may be required
-- :red_circle: = Breaking API changes or removal, action required if using these methods
+* :green_circle: = API functionality unchanged between `acme.forge` and `acme.crypto`
+* :orange_circle: = Slight API changes, like depromising or renaming, action may be required
+* :red_circle: = Breaking API changes or removal, action required if using these methods
| Deprecated `.forge` API | New `.crypto` API | State |
| ----------------------------- | ----------------------------- | --------------------- |
@@ -33,7 +32,6 @@ Below you will find a table summarizing the current `acme.forge` methods, and th
| `await readCertificateInfo()` | `readCertificateInfo()` | :orange_circle: (4) |
| `await createCsr()` | `await createCsr()` | :green_circle: |
-
### 1. `createPublicKey` renamed and depromised
* The method `createPublicKey()` has been renamed to `getPublicKey()`
@@ -49,7 +47,6 @@ const publicKey = await acme.forge.createPublicKey(privateKey);
const publicKey = acme.crypto.getPublicKey(privateKey);
```
-
### 2. `getPemBody` renamed, now returns Base64URL
* Method `getPemBody()` has been renamed to `getPemBodyAsB64u()`
@@ -64,7 +61,6 @@ const body = acme.forge.getPemBody(pem);
const body = acme.crypto.getPemBodyAsB64u(pem);
```
-
### 3. `getModulus` and `getPublicExponent` merged into `getJwk`
* Methods `getModulus()` and `getPublicExponent()` have been removed
@@ -80,7 +76,6 @@ const exp = await acme.forge.getPublicExponent(key);
const { e, n } = acme.crypto.getJwk(key);
```
-
### 4. `readCsrDomains` and `readCertificateInfo` depromised
* Methods `readCsrDomains()` and `readCertificateInfo()` no longer return promises, but their resulting payloads directly
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/dns-01/README.md b/packages/core/acme-client/examples/dns-01/README.md
new file mode 100644
index 00000000..4b55dc4c
--- /dev/null
+++ b/packages/core/acme-client/examples/dns-01/README.md
@@ -0,0 +1,21 @@
+# dns-01
+
+The greatest benefit of `dns-01` is that it is the only challenge type that can be used to issue ACME wildcard certificates, however it also has a few downsides. Your DNS provider needs to offer some sort of API you can use to automate adding and removing the required `TXT` DNS records. Additionally, solving DNS challenges will be much slower than the other challenge types because of DNS propagation delays.
+
+## How it works
+
+When solving `dns-01` challenges, you prove ownership of a domain by serving a specific payload within a specific DNS `TXT` record from the domains authoritative nameservers. The ACME authority provides the client with a token that, along with a thumbprint of your account key, is used to generate a `base64url` encoded `SHA256` digest. This payload is then placed as a `TXT` record under DNS name `_acme-challenge.$YOUR_DOMAIN`.
+
+Once the order is finalized, the ACME authority will lookup your domains DNS record to verify that the payload is correct. `CNAME` and `NS` records are followed, should you wish to delegate challenge response to another DNS zone or record.
+
+## Pros and cons
+
+* Only challenge type that can be used to issue wildcard certificates
+* Your DNS provider needs to supply an API that can be used
+* DNS propagation time may be slow
+* Useful in instances where both port 80 and 443 are unavailable
+
+## External links
+
+* [https://letsencrypt.org/docs/challenge-types/#dns-01-challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge)
+* [https://datatracker.ietf.org/doc/html/rfc8555#section-8.4](https://datatracker.ietf.org/doc/html/rfc8555#section-8.4)
diff --git a/packages/core/acme-client/examples/dns-01/dns-01.js b/packages/core/acme-client/examples/dns-01/dns-01.js
new file mode 100644
index 00000000..68bed46e
--- /dev/null
+++ b/packages/core/acme-client/examples/dns-01/dns-01.js
@@ -0,0 +1,92 @@
+/**
+ * Example using dns-01 challenge to generate certificates
+ *
+ * NOTE: This example is incomplete as the DNS challenge response implementation
+ * will be specific to your DNS providers API.
+ *
+ * NOTE: This example does not order certificates on-demand, as solving dns-01
+ * will likely be too slow for it to make sense. Instead, it orders a wildcard
+ * certificate on init before starting the HTTPS server as a demonstration.
+ */
+
+const https = require('https');
+const acme = require('./../../');
+
+const HTTPS_SERVER_PORT = 443;
+const WILDCARD_DOMAIN = 'example.com';
+
+function log(m) {
+ process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
+}
+
+
+/**
+ * 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()
+ });
+
+
+ /**
+ * Order wildcard certificate
+ */
+
+ log(`Creating CSR for ${WILDCARD_DOMAIN}`);
+ const [key, csr] = await acme.crypto.createCsr({
+ commonName: WILDCARD_DOMAIN,
+ altNames: [`*.${WILDCARD_DOMAIN}`]
+ });
+
+ log(`Ordering certificate for ${WILDCARD_DOMAIN}`);
+ const cert = await client.auto({
+ csr,
+ email: 'test@example.com',
+ termsOfServiceAgreed: true,
+ challengePriority: ['dns-01'],
+ challengeCreateFn: (authz, challenge, keyAuthorization) => {
+ /* TODO: Implement this */
+ log(`[TODO] Add TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
+ },
+ challengeRemoveFn: (authz, challenge, keyAuthorization) => {
+ /* TODO: Implement this */
+ log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
+ }
+ });
+
+ log(`Certificate for ${WILDCARD_DOMAIN} created successfully`);
+
+
+ /**
+ * 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({
+ key,
+ cert
+ }, 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/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/http-01/README.md b/packages/core/acme-client/examples/http-01/README.md
new file mode 100644
index 00000000..a08cace1
--- /dev/null
+++ b/packages/core/acme-client/examples/http-01/README.md
@@ -0,0 +1,21 @@
+# http-01
+
+The `http-01` challenge type is the simplest to implement and should likely be your default choice, unless you either require wildcard certificates or if port 80 is unavailable for use.
+
+## How it works
+
+When solving `http-01` challenges, you prove ownership of a domain name by serving a specific payload from a specific URL. The ACME authority provides the client with a token that is used to generate the URL and file contents. The file must exist at `http://$YOUR_DOMAIN/.well-known/acme-challenge/$TOKEN` and contain the token and a thumbprint of your account key.
+
+Once the order is finalized, the ACME authority will verify that the URL responds with the correct payload by sending HTTP requests before the challenge is valid. HTTP redirects are followed, and Let's Encrypt allows redirecting to HTTPS although this diverges from the ACME spec.
+
+## Pros and cons
+
+* Challenge must be satisfied using port 80 (HTTP)
+* The simplest challenge type to implement
+* Can not be used to issue wildcard certificates
+* If using multiple web servers, all of them need to respond with the correct token
+
+## External links
+
+* [https://letsencrypt.org/docs/challenge-types/#http-01-challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge)
+* [https://datatracker.ietf.org/doc/html/rfc8555#section-8.3](https://datatracker.ietf.org/doc/html/rfc8555#section-8.3)
diff --git a/packages/core/acme-client/examples/http-01/http-01.js b/packages/core/acme-client/examples/http-01/http-01.js
new file mode 100644
index 00000000..75153385
--- /dev/null
+++ b/packages/core/acme-client/examples/http-01/http-01.js
@@ -0,0 +1,172 @@
+/**
+ * Example using http-01 challenge to generate certificates on-demand
+ */
+
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+const https = require('https');
+const tls = require('tls');
+const acme = require('./../../');
+
+const HTTP_SERVER_PORT = 80;
+const HTTPS_SERVER_PORT = 443;
+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 challengeResponses = {};
+const certificateStore = {};
+
+function log(m) {
+ process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
+}
+
+
+/**
+ * On-demand certificate generation using http-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: ['http-01'],
+ challengeCreateFn: (authz, challenge, keyAuthorization) => {
+ challengeResponses[challenge.token] = keyAuthorization;
+ },
+ challengeRemoveFn: (authz, challenge) => {
+ delete challengeResponses[challenge.token];
+ }
+ });
+
+ /* 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()
+ });
+
+
+ /**
+ * HTTP server
+ */
+
+ const httpServer = http.createServer((req, res) => {
+ if (req.url.match(/\/\.well-known\/acme-challenge\/.+/)) {
+ const token = req.url.split('/').pop();
+ log(`Received challenge request for token=${token}`);
+
+ /* ACME challenge response */
+ if (token in challengeResponses) {
+ log(`Serving challenge response HTTP 200 token=${token}`);
+ res.writeHead(200);
+ res.end(challengeResponses[token]);
+ return;
+ }
+
+ /* Challenge response not found */
+ log(`Oops, challenge response not found for token=${token}`);
+ res.writeHead(404);
+ res.end();
+ return;
+ }
+
+ /* HTTP 302 redirect */
+ log(`HTTP 302 ${req.headers.host}${req.url}`);
+ res.writeHead(302, { Location: `https://${req.headers.host}${req.url}` });
+ res.end();
+ });
+
+ httpServer.listen(HTTP_SERVER_PORT, () => {
+ log(`HTTP server listening on port ${HTTP_SERVER_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 certificate */
+ 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/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 7a2230d7..10521187 100644
--- a/packages/core/acme-client/package.json
+++ b/packages/core/acme-client/package.json
@@ -5,7 +5,7 @@
"author": "nmorsman",
"version": "1.20.2",
"main": "src/index.js",
- "types": "types",
+ "types": "types/index.d.ts",
"license": "MIT",
"homepage": "https://github.com/publishlab/node-acme-client",
"engines": {
@@ -16,32 +16,32 @@
"types"
],
"dependencies": {
- "axios": "0.27.2",
+ "@peculiar/x509": "^1.9.7",
+ "asn1js": "^3.0.5",
+ "axios": "^1.6.5",
"debug": "^4.1.1",
- "jsrsasign": "^10.5.26",
"node-forge": "^1.3.1"
},
"devDependencies": {
- "@types/node": "^18.6.1",
- "chai": "^4.3.6",
+ "@types/node": "^20.11.5",
+ "chai": "^4.4.1",
"chai-as-promised": "^7.1.1",
- "dtslint": "^4.2.1",
- "eslint": "^8.11.0",
+ "eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
- "eslint-plugin-import": "^2.25.4",
- "jsdoc-to-markdown": "^7.1.1",
- "mocha": "^10.0.0",
- "nock": "^13.2.4",
+ "eslint-plugin-import": "^2.29.1",
+ "jsdoc-to-markdown": "^8.0.0",
+ "mocha": "^10.2.0",
+ "nock": "^13.5.0",
+ "tsd": "^0.30.4",
"typescript": "^4.8.4",
"uuid": "^8.3.2"
},
"scripts": {
"build-docs": "jsdoc2md src/client.js > docs/client.md && jsdoc2md src/crypto/index.js > docs/crypto.md && jsdoc2md src/crypto/forge.js > docs/forge.md",
"lint": "eslint .",
- "lint-types": "dtslint types",
+ "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 b0613ac2..00000000
--- a/packages/core/acme-client/scripts/run-tests.sh
+++ /dev/null
@@ -1,56 +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.16947/circleci-cli_0.1.16947_linux_amd64.tar.gz"
-CIRCLECI_CLI_SHASUM="c6f9a3276445c69ae40439acfed07e2c53502216a96bfacc4556e1d862d1019a"
-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"
- "eab-v16"
- "eab-v18"
- )
-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
-
-# Skip CircleCI update checks
-export CIRCLECI_CLI_SKIP_UPDATE_CHECK="true"
-
-# 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 "${job}" --skip-checkout
- 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/src/api.js b/packages/core/acme-client/src/api.js
index 84fe0f88..31c06f52 100644
--- a/packages/core/acme-client/src/api.js
+++ b/packages/core/acme-client/src/api.js
@@ -41,7 +41,7 @@ class AcmeApi {
* @private
* @param {string} url Request URL
* @param {object} [payload] Request payload, default: `null`
- * @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
+ * @param {number[]} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
* @param {object} [opts]
* @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
@@ -66,7 +66,7 @@ class AcmeApi {
* @private
* @param {string} resource Request resource name
* @param {object} [payload] Request payload, default: `null`
- * @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
+ * @param {number[]} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
* @param {object} [opts]
* @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
@@ -82,7 +82,7 @@ class AcmeApi {
/**
* Get Terms of Service URL if available
*
- * https://tools.ietf.org/html/rfc8555#section-7.1.1
+ * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
*
* @returns {Promise