Merge remote-tracking branch 'origin/acme_sync' into v2

# Conflicts:
#	packages/core/acme-client/CHANGELOG.md
#	packages/core/acme-client/package.json
#	packages/core/acme-client/src/auto.js
pull/68/head
xiaojunnuo 2024-03-06 18:35:44 +08:00
commit 09c9b42cab
55 changed files with 1646 additions and 668 deletions

View File

@ -1 +0,0 @@
.temp.yml

View File

@ -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

View File

@ -5,7 +5,7 @@
root = true
[*]
indent_style = spaces
indent_style = space
indent_size = 4
trim_trailing_whitespace = true

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -1,7 +1,5 @@
.vscode/
node_modules/
npm-debug.log
yarn-error.log
yarn.lock
package-lock.json
/.idea/

View File

@ -1,2 +0,0 @@
ignore-engines true
ignore-optional true

View File

@ -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)

View File

@ -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

View File

@ -1,21 +1,13 @@
# 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.*
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)

View File

@ -132,7 +132,7 @@ catch (e) {
### acmeClient.createAccount([data]) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
@ -161,7 +161,7 @@ const account = await client.createAccount({
### acmeClient.updateAccount([data]) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
@ -182,7 +182,7 @@ const account = await client.updateAccount({
### acmeClient.updateAccountKey(newAccountKey, [data]) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Account
@ -203,7 +203,7 @@ const result = await client.updateAccountKey(newAccountKey);
### acmeClient.createOrder(data) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
@ -227,7 +227,7 @@ const order = await client.createOrder({
### acmeClient.getOrder(order) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
@ -246,7 +246,7 @@ const result = await client.getOrder(order);
### acmeClient.finalizeOrder(order, csr) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Order
@ -268,7 +268,7 @@ const result = await client.finalizeOrder(order, csr);
### acmeClient.getAuthorizations(order) ⇒ <code>Promise.&lt;Array.&lt;object&gt;&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;Array.&lt;object&gt;&gt;</code> - Authorizations
@ -292,7 +292,7 @@ authorizations.forEach((authz) => {
### acmeClient.deactivateAuthorization(authz) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Authorization
@ -312,7 +312,7 @@ const result = await client.deactivateAuthorization(authz);
### acmeClient.getChallengeKeyAuthorization(challenge) ⇒ <code>Promise.&lt;string&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;string&gt;</code> - Key authorization
@ -353,7 +353,7 @@ await client.verifyChallenge(authz, challenge);
### acmeClient.completeChallenge(challenge) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - Challenge
@ -373,7 +373,7 @@ const result = await client.completeChallenge(challenge);
### acmeClient.waitForValidStatus(item) ⇒ <code>Promise.&lt;object&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;object&gt;</code> - 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]) ⇒ <code>Promise.&lt;string&gt;</code>
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 [<code>AcmeClient</code>](#AcmeClient)
**Returns**: <code>Promise.&lt;string&gt;</code> - Certificate
@ -432,7 +432,7 @@ const certificate = await client.getCertificate(order, 'DST Root CA X3');
### acmeClient.revokeCertificate(cert, [data]) ⇒ <code>Promise</code>
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 [<code>AcmeClient</code>](#AcmeClient)

View File

@ -25,7 +25,7 @@
<dd><p>Get a JSON Web Key derived from a RSA or ECDSA key</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc7517">https://datatracker.ietf.org/doc/html/rfc7517</a></p>
</dd>
<dt><a href="#splitPemChain">splitPemChain(chainPem)</a><code>array</code></dt>
<dt><a href="#splitPemChain">splitPemChain(chainPem)</a><code>Array.&lt;string&gt;</code></dt>
<dd><p>Split chain of PEM encoded objects from string into array</p>
</dd>
<dt><a href="#getPemBodyAsB64u">getPemBodyAsB64u(pem)</a><code>string</code></dt>
@ -42,6 +42,13 @@ If multiple certificates are chained, the first will be read</p>
<dt><a href="#createCsr">createCsr(data, [keyPem])</a><code>Promise.&lt;Array.&lt;buffer&gt;&gt;</code></dt>
<dd><p>Create a Certificate Signing Request</p>
</dd>
<dt><a href="#createAlpnCertificate">createAlpnCertificate(authz, keyAuthorization, [keyPem])</a><code>Promise.&lt;Array.&lt;buffer&gt;&gt;</code></dt>
<dd><p>Create a self-signed ALPN certificate for TLS-ALPN-01 challenges</p>
<p><a href="https://datatracker.ietf.org/doc/html/rfc8737">https://datatracker.ietf.org/doc/html/rfc8737</a></p>
</dd>
<dt><a href="#isAlpnCertificateAuthorizationValid">isAlpnCertificateAuthorizationValid(certPem, keyAuthorization)</a><code>boolean</code></dt>
<dd><p>Validate that a ALPN certificate contains the expected key authorization</p>
</dd>
</dl>
<a name="crypto"></a>
@ -138,11 +145,11 @@ const jwk = acme.crypto.getJwk(privateKey);
```
<a name="splitPemChain"></a>
## splitPemChain(chainPem) ⇒ <code>array</code>
## splitPemChain(chainPem) ⇒ <code>Array.&lt;string&gt;</code>
Split chain of PEM encoded objects from string into array
**Kind**: global function
**Returns**: <code>array</code> - Array of PEM objects including headers
**Returns**: <code>Array.&lt;string&gt;</code> - Array of PEM objects including headers
| Param | Type | Description |
| --- | --- | --- |
@ -219,14 +226,14 @@ Create a Certificate Signing Request
| data | <code>object</code> | |
| [data.keySize] | <code>number</code> | Size of newly created RSA private key modulus in bits, default: `2048` |
| [data.commonName] | <code>string</code> | FQDN of your server |
| [data.altNames] | <code>array</code> | SAN (Subject Alternative Names), default: `[]` |
| [data.altNames] | <code>Array.&lt;string&gt;</code> | SAN (Subject Alternative Names), default: `[]` |
| [data.country] | <code>string</code> | 2 letter country code |
| [data.state] | <code>string</code> | State or province |
| [data.locality] | <code>string</code> | City |
| [data.organization] | <code>string</code> | Organization name |
| [data.organizationUnit] | <code>string</code> | Organizational unit name |
| [data.emailAddress] | <code>string</code> | Email address |
| [keyPem] | <code>string</code> | PEM encoded CSR private key |
| [keyPem] | <code>buffer</code> \| <code>string</code> | 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);
<a name="createAlpnCertificate"></a>
## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ <code>Promise.&lt;Array.&lt;buffer&gt;&gt;</code>
Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
https://datatracker.ietf.org/doc/html/rfc8737
**Kind**: global function
**Returns**: <code>Promise.&lt;Array.&lt;buffer&gt;&gt;</code> - [privateKey, certificate]
| Param | Type | Description |
| --- | --- | --- |
| authz | <code>object</code> | Identifier authorization |
| keyAuthorization | <code>string</code> | Challenge key authorization |
| [keyPem] | <code>buffer</code> \| <code>string</code> | 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);
<a name="isAlpnCertificateAuthorizationValid"></a>
## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ <code>boolean</code>
Validate that a ALPN certificate contains the expected key authorization
**Kind**: global function
**Returns**: <code>boolean</code> - True when valid
| Param | Type | Description |
| --- | --- | --- |
| certPem | <code>buffer</code> \| <code>string</code> | PEM encoded certificate |
| keyAuthorization | <code>string</code> | Expected challenge key authorization |

View File

@ -209,7 +209,7 @@ Create a Certificate Signing Request
| data | <code>object</code> | |
| [data.keySize] | <code>number</code> | Size of newly created private key, default: `2048` |
| [data.commonName] | <code>string</code> | |
| [data.altNames] | <code>array</code> | default: `[]` |
| [data.altNames] | <code>Array.&lt;string&gt;</code> | default: `[]` |
| [data.country] | <code>string</code> | |
| [data.state] | <code>string</code> | |
| [data.locality] | <code>string</code> | |

View File

@ -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

View File

@ -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?

View File

@ -4,7 +4,6 @@
const acme = require('./../');
function log(m) {
process.stdout.write(`${m}\n`);
}

View File

@ -5,7 +5,6 @@
// const fs = require('fs').promises;
const acme = require('./../');
function log(m) {
process.stdout.write(`${m}\n`);
}

View File

@ -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)

View File

@ -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);
}
})();

View File

@ -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-----

View File

@ -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-----

View File

@ -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)

View File

@ -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);
}
})();

View File

@ -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)

View File

@ -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

View File

@ -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;
}
}

View File

@ -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);
}
})();

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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<string|null>} ToS URL
*/
@ -95,7 +95,7 @@ class AcmeApi {
/**
* Create new account
*
* https://tools.ietf.org/html/rfc8555#section-7.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
*
* @param {object} data Request payload
* @returns {Promise<object>} HTTP response
@ -119,7 +119,7 @@ class AcmeApi {
/**
* Update account
*
* https://tools.ietf.org/html/rfc8555#section-7.3.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
*
* @param {object} data Request payload
* @returns {Promise<object>} HTTP response
@ -133,7 +133,7 @@ class AcmeApi {
/**
* Update account key
*
* https://tools.ietf.org/html/rfc8555#section-7.3.5
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5
*
* @param {object} data Request payload
* @returns {Promise<object>} HTTP response
@ -147,7 +147,7 @@ class AcmeApi {
/**
* Create new order
*
* https://tools.ietf.org/html/rfc8555#section-7.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*
* @param {object} data Request payload
* @returns {Promise<object>} HTTP response
@ -161,7 +161,7 @@ class AcmeApi {
/**
* Get order
*
* https://tools.ietf.org/html/rfc8555#section-7.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*
* @param {string} url Order URL
* @returns {Promise<object>} HTTP response
@ -175,7 +175,7 @@ class AcmeApi {
/**
* Finalize order
*
* https://tools.ietf.org/html/rfc8555#section-7.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*
* @param {string} url Finalization URL
* @param {object} data Request payload
@ -190,7 +190,7 @@ class AcmeApi {
/**
* Get identifier authorization
*
* https://tools.ietf.org/html/rfc8555#section-7.5
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
*
* @param {string} url Authorization URL
* @returns {Promise<object>} HTTP response
@ -204,7 +204,7 @@ class AcmeApi {
/**
* Update identifier authorization
*
* https://tools.ietf.org/html/rfc8555#section-7.5.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
*
* @param {string} url Authorization URL
* @param {object} data Request payload
@ -219,7 +219,7 @@ class AcmeApi {
/**
* Complete challenge
*
* https://tools.ietf.org/html/rfc8555#section-7.5.1
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
*
* @param {string} url Challenge URL
* @param {object} data Request payload
@ -234,7 +234,7 @@ class AcmeApi {
/**
* Revoke certificate
*
* https://tools.ietf.org/html/rfc8555#section-7.6
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
*
* @param {object} data Request payload
* @returns {Promise<object>} HTTP response

View File

@ -182,6 +182,7 @@ module.exports = async function(client, userOpts) {
return promise;
}
await runPromisesSerially(challengePromises);
log('challenge结束');

View File

@ -3,7 +3,6 @@
*/
const axios = require('axios');
const adapter = require('axios/lib/adapters/http');
const pkg = require('./../package.json');
@ -19,7 +18,8 @@ instance.defaults.headers.common['User-Agent'] = `node-${pkg.name}/${pkg.version
/* Default ACME settings */
instance.defaults.acmeSettings = {
httpChallengePort: 80,
bypassCustomDnsResolver: false
httpsChallengePort: 443,
tlsAlpnChallengePort: 443
};
@ -30,7 +30,7 @@ instance.defaults.acmeSettings = {
* https://stackoverflow.com/questions/42677387
*/
instance.defaults.adapter = adapter;
instance.defaults.adapter = 'http';
/**

View File

@ -154,7 +154,7 @@ class AcmeClient {
/**
* Create a new account
*
* https://tools.ietf.org/html/rfc8555#section-7.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
*
* @param {object} [data] Request data
* @returns {Promise<object>} Account
@ -200,7 +200,7 @@ class AcmeClient {
/**
* Update existing account
*
* https://tools.ietf.org/html/rfc8555#section-7.3.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
*
* @param {object} [data] Request data
* @returns {Promise<object>} Account
@ -240,7 +240,7 @@ class AcmeClient {
/**
* 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
*
* @param {buffer|string} newAccountKey New PEM encoded private key
* @param {object} [data] Additional request data
@ -286,7 +286,7 @@ class AcmeClient {
/**
* Create a new order
*
* https://tools.ietf.org/html/rfc8555#section-7.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*
* @param {object} data Request data
* @returns {Promise<object>} Order
@ -318,7 +318,7 @@ class AcmeClient {
/**
* Refresh order object from CA
*
* https://tools.ietf.org/html/rfc8555#section-7.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*
* @param {object} order Order object
* @returns {Promise<object>} Order
@ -345,7 +345,7 @@ class AcmeClient {
/**
* Finalize order
*
* https://tools.ietf.org/html/rfc8555#section-7.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*
* @param {object} order Order object
* @param {buffer|string} csr PEM encoded Certificate Signing Request
@ -380,7 +380,7 @@ class AcmeClient {
/**
* Get identifier authorizations from order
*
* https://tools.ietf.org/html/rfc8555#section-7.5
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
*
* @param {object} order Order
* @returns {Promise<object[]>} Authorizations
@ -410,7 +410,7 @@ class AcmeClient {
/**
* Deactivate identifier authorization
*
* https://tools.ietf.org/html/rfc8555#section-7.5.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
*
* @param {object} authz Identifier authorization
* @returns {Promise<object>} Authorization
@ -442,7 +442,7 @@ class AcmeClient {
/**
* 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
*
* @param {object} challenge Challenge object returned by API
* @returns {Promise<string>} Key authorization
@ -462,22 +462,19 @@ class AcmeClient {
const thumbprint = keysum.digest('base64url');
const result = `${challenge.token}.${thumbprint}`;
/**
* https://tools.ietf.org/html/rfc8555#section-8.3
*/
/* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3 */
if (challenge.type === 'http-01') {
return result;
}
/**
* https://tools.ietf.org/html/rfc8555#section-8.4
* https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
*/
/* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4 */
if (challenge.type === 'dns-01') {
return createHash('sha256').update(result).digest('base64url');
}
if ((challenge.type === 'dns-01') || (challenge.type === 'tls-alpn-01')) {
const shasum = createHash('sha256').update(result);
return shasum.digest('base64url');
/* https://datatracker.ietf.org/doc/html/rfc8737 */
if (challenge.type === 'tls-alpn-01') {
return result;
}
throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
@ -522,7 +519,7 @@ class AcmeClient {
/**
* 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
*
* @param {object} challenge Challenge object returned by API
* @returns {Promise<object>} Challenge
@ -543,7 +540,7 @@ class AcmeClient {
/**
* 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
*
* @param {object} item An order, authorization or challenge object
* @returns {Promise<object>} Valid order, authorization or challenge
@ -554,7 +551,7 @@ class AcmeClient {
* await client.waitForValidStatus(challenge);
* ```
*
* @example Wait for valid authoriation status
* @example Wait for valid authorization status
* ```js
* const authz = { ... };
* await client.waitForValidStatus(authz);
@ -600,7 +597,7 @@ class AcmeClient {
/**
* 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
*
* @param {object} order Order object
* @param {string} [preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null`
@ -647,7 +644,7 @@ class AcmeClient {
/**
* Revoke certificate
*
* https://tools.ietf.org/html/rfc8555#section-7.6
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
*
* @param {buffer|string} cert PEM encoded certificate
* @param {object} [data] Additional request data

View File

@ -281,7 +281,7 @@ exports.readCertificateInfo = async function(cert) {
/**
* Determine ASN.1 type for CSR subject short name
* Note: https://tools.ietf.org/html/rfc5280
* Note: https://datatracker.ietf.org/doc/html/rfc5280
*
* @private
* @param {string} shortName CSR subject short name
@ -343,7 +343,7 @@ function formatCsrAltNames(altNames) {
* @param {object} data
* @param {number} [data.keySize] Size of newly created private key, default: `2048`
* @param {string} [data.commonName]
* @param {array} [data.altNames] default: `[]`
* @param {string[]} [data.altNames] default: `[]`
* @param {string} [data.country]
* @param {string} [data.state]
* @param {string} [data.locality]

View File

@ -7,10 +7,21 @@
const net = require('net');
const { promisify } = require('util');
const crypto = require('crypto');
const jsrsasign = require('jsrsasign');
const asn1js = require('asn1js');
const x509 = require('@peculiar/x509');
const randomInt = promisify(crypto.randomInt);
const generateKeyPair = promisify(crypto.generateKeyPair);
/* Use Node.js Web Crypto API */
x509.cryptoProvider.set(crypto.webcrypto);
/* id-ce-subjectAltName - https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
const subjectAltNameOID = '2.5.29.17';
/* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31';
/**
* Determine key type and info by attempting to derive public key
@ -24,17 +35,14 @@ function getKeyInfo(keyPem) {
const result = {
isRSA: false,
isECDSA: false,
signatureAlgorithm: null,
publicKey: crypto.createPublicKey(keyPem)
};
if (result.publicKey.asymmetricKeyType === 'rsa') {
result.isRSA = true;
result.signatureAlgorithm = 'SHA256withRSA';
}
else if (result.publicKey.asymmetricKeyType === 'ec') {
result.isECDSA = true;
result.signatureAlgorithm = 'SHA256withECDSA';
}
else {
throw new Error('Unable to parse key information, unknown format');
@ -169,24 +177,42 @@ exports.getJwk = getJwk;
/**
* Fix missing support for NIST curve names in jsrsasign
* Produce CryptoKeyPair and signing algorithm from a PEM encoded private key
*
* @private
* @param {string} crv NIST curve name
* @returns {string} SECG curve name
* @param {buffer|string} keyPem PEM encoded private key
* @returns {Promise<array>} [keyPair, signingAlgorithm]
*/
function convertNistCurveNameToSecg(nistName) {
switch (nistName) {
case 'P-256':
return 'secp256r1';
case 'P-384':
return 'secp384r1';
case 'P-521':
return 'secp521r1';
default:
return nistName;
async function getWebCryptoKeyPair(keyPem) {
const info = getKeyInfo(keyPem);
const jwk = getJwk(keyPem);
/* Signing algorithm */
const sigalg = {
name: 'RSASSA-PKCS1-v1_5',
hash: { name: 'SHA-256' }
};
if (info.isECDSA) {
sigalg.name = 'ECDSA';
sigalg.namedCurve = jwk.crv;
if (jwk.crv === 'P-384') {
sigalg.hash.name = 'SHA-384';
}
if (jwk.crv === 'P-521') {
sigalg.hash.name = 'SHA-512';
}
}
/* Decode PEM and import into CryptoKeyPair */
const privateKeyDec = x509.PemConverter.decodeFirst(keyPem.toString());
const privateKey = await crypto.webcrypto.subtle.importKey('pkcs8', privateKeyDec, sigalg, true, ['sign']);
const publicKey = await crypto.webcrypto.subtle.importKey('jwk', jwk, sigalg, true, ['verify']);
return [{ privateKey, publicKey }, sigalg];
}
@ -194,7 +220,7 @@ function convertNistCurveNameToSecg(nistName) {
* Split chain of PEM encoded objects from string into array
*
* @param {buffer|string} chainPem PEM encoded object chain
* @returns {array} Array of PEM objects including headers
* @returns {string[]} Array of PEM objects including headers
*/
function splitPemChain(chainPem) {
@ -202,15 +228,9 @@ function splitPemChain(chainPem) {
chainPem = chainPem.toString();
}
return chainPem
/* Split chain into chunks, starting at every header */
.split(/\s*(?=-----BEGIN [A-Z0-9- ]+-----\r?\n?)/g)
/* Match header, PEM body and footer */
.map((pem) => pem.match(/\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\S\s]+)\r?\n?-----END \1-----/))
/* Filter out non-matches or empty bodies */
.filter((pem) => pem && pem[2] && pem[2].replace(/[\r\n]+/g, '').trim())
/* Decode to hex, and back to PEM for formatting etc */
.map(([pem, header]) => jsrsasign.hextopem(jsrsasign.pemtohex(pem, header), header));
/* Decode into array and re-encode */
return x509.PemConverter.decodeWithHeaders(chainPem)
.map((params) => x509.PemConverter.encode([params]));
}
exports.splitPemChain = splitPemChain;
@ -231,43 +251,28 @@ exports.getPemBodyAsB64u = (pem) => {
throw new Error('Unable to parse PEM body from string');
}
/* First object, hex and back to b64 without new lines */
return jsrsasign.hextob64u(jsrsasign.pemtohex(chain[0]));
/* Select first object, extract body and convert to b64u */
const dec = x509.PemConverter.decodeFirst(chain[0]);
return Buffer.from(dec).toString('base64url');
};
/**
* Parse common name from a subject object
*
* @private
* @param {object} subj Subject returned from jsrsasign
* @returns {string} Common name value
*/
function parseCommonName(subj) {
const subjectArr = (subj && subj.array) ? subj.array : [];
const cnArr = subjectArr.find((s) => (s[0] && s[0].type && s[0].value && (s[0].type === 'CN')));
return (cnArr && cnArr.length && cnArr[0].value) ? cnArr[0].value : null;
}
/**
* Parse domains from a certificate or CSR
*
* @private
* @param {object} params Certificate or CSR params returned from jsrsasign
* @param {object} input x509.Certificate or x509.Pkcs10CertificateRequest
* @returns {object} {commonName, altNames}
*/
function parseDomains(params) {
const commonName = parseCommonName(params.subject);
const extensionArr = (params.ext || params.extreq || []);
function parseDomains(input) {
const commonName = input.subjectName.getField('CN').pop() || null;
const altNamesRaw = input.getExtension(subjectAltNameOID);
let altNames = [];
if (extensionArr && extensionArr.length) {
const altNameExt = extensionArr.find((e) => (e.extname && (e.extname === 'subjectAltName')));
const altNameArr = (altNameExt && altNameExt.array && altNameExt.array.length) ? altNameExt.array : [];
altNames = altNameArr.map((a) => Object.values(a)[0] || null).filter((a) => a);
if (altNamesRaw) {
const altNamesExt = new x509.SubjectAlternativeNameExtension(altNamesRaw.rawData);
altNames = altNames.concat(altNamesExt.names.items.map((i) => i.value));
}
return {
@ -297,9 +302,9 @@ exports.readCsrDomains = (csrPem) => {
csrPem = csrPem.toString();
}
/* Parse CSR */
const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csrPem);
return parseDomains(params);
const dec = x509.PemConverter.decodeFirst(csrPem);
const csr = new x509.Pkcs10CertificateRequest(dec);
return parseDomains(csr);
};
@ -324,48 +329,43 @@ exports.readCsrDomains = (csrPem) => {
*/
exports.readCertificateInfo = (certPem) => {
const chain = splitPemChain(certPem);
if (!chain.length) {
throw new Error('Unable to parse PEM body from string');
if (Buffer.isBuffer(certPem)) {
certPem = certPem.toString();
}
/* Parse certificate */
const obj = new jsrsasign.X509();
obj.readCertPEM(chain[0]);
const params = obj.getParam();
const dec = x509.PemConverter.decodeFirst(certPem);
const cert = new x509.X509Certificate(dec);
return {
issuer: {
commonName: parseCommonName(params.issuer)
commonName: cert.issuerName.getField('CN').pop() || null
},
domains: parseDomains(params),
notBefore: jsrsasign.zulutodate(params.notbefore),
notAfter: jsrsasign.zulutodate(params.notafter)
domains: parseDomains(cert),
notBefore: cert.notBefore,
notAfter: cert.notAfter
};
};
/**
* Determine ASN.1 character string type for CSR subject field
* Determine ASN.1 character string type for CSR subject field name
*
* https://tools.ietf.org/html/rfc5280
* https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/x509-1.1.js#L2404-L2412
* https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/asn1x509-1.0.js#L3526-L3535
* https://datatracker.ietf.org/doc/html/rfc5280
* https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71
*
* @private
* @param {string} field CSR subject field
* @returns {string} ASN.1 jsrsasign character string type
* @param {string} field CSR subject field name
* @returns {string} ASN.1 character string type
*/
function getCsrAsn1CharStringType(field) {
switch (field) {
case 'C':
return 'prn';
return 'printableString';
case 'E':
return 'ia5';
return 'ia5String';
default:
return 'utf8';
return 'utf8String';
}
}
@ -373,6 +373,8 @@ function getCsrAsn1CharStringType(field) {
/**
* Create array of subject fields for a Certificate Signing Request
*
* https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71
*
* @private
* @param {object} input Key-value of subject fields
* @returns {object[]} Certificate Signing Request subject array
@ -382,7 +384,7 @@ function createCsrSubject(input) {
return Object.entries(input).reduce((result, [type, value]) => {
if (value) {
const ds = getCsrAsn1CharStringType(type);
result.push([{ type, value, ds }]);
result.push({ [type]: [{ [ds]: value }] });
}
return result;
@ -391,20 +393,20 @@ function createCsrSubject(input) {
/**
* Create array of alt names for Certificate Signing Requests
* Create x509 subject alternate name extension
*
* https://github.com/kjur/jsrsasign/blob/3edc0070846922daea98d9588978e91d855577ec/src/x509-1.1.js#L1355-L1410
* https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/extensions/subject_alt_name.ts
*
* @private
* @param {string[]} altNames Array of alt names
* @returns {object[]} Certificate Signing Request alt names array
* @returns {x509.SubjectAlternativeNameExtension} Subject alternate name extension
*/
function formatCsrAltNames(altNames) {
return altNames.map((value) => {
const key = net.isIP(value) ? 'ip' : 'dns';
return { [key]: value };
});
function createSubjectAltNameExtension(altNames) {
return new x509.SubjectAlternativeNameExtension(altNames.map((value) => {
const type = net.isIP(value) ? 'ip' : 'dns';
return { type, value };
}));
}
@ -414,14 +416,14 @@ function formatCsrAltNames(altNames) {
* @param {object} data
* @param {number} [data.keySize] Size of newly created RSA private key modulus in bits, default: `2048`
* @param {string} [data.commonName] FQDN of your server
* @param {array} [data.altNames] SAN (Subject Alternative Names), default: `[]`
* @param {string[]} [data.altNames] SAN (Subject Alternative Names), default: `[]`
* @param {string} [data.country] 2 letter country code
* @param {string} [data.state] State or province
* @param {string} [data.locality] City
* @param {string} [data.organization] Organization name
* @param {string} [data.organizationUnit] Organizational unit name
* @param {string} [data.emailAddress] Email address
* @param {string} [keyPem] PEM encoded CSR private key
* @param {buffer|string} [keyPem] PEM encoded CSR private key
* @returns {Promise<buffer[]>} [privateKey, certificateSigningRequest]
*
* @example Create a Certificate Signing Request
@ -474,53 +476,144 @@ exports.createCsr = async (data, keyPem = null) => {
data.altNames = [];
}
/* Get key info and JWK */
const info = getKeyInfo(keyPem);
const jwk = getJwk(keyPem);
const extensionRequests = [];
/* Missing support for NIST curve names in jsrsasign - https://github.com/kjur/jsrsasign/blob/master/src/asn1x509-1.0.js#L4388-L4393 */
if (jwk.crv && (jwk.kty === 'EC')) {
jwk.crv = convertNistCurveNameToSecg(jwk.crv);
}
/* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */
if (data.commonName && !data.altNames.includes(data.commonName)) {
data.altNames.unshift(data.commonName);
}
/* Subject */
const subject = createCsrSubject({
CN: data.commonName,
C: data.country,
ST: data.state,
L: data.locality,
O: data.organization,
OU: data.organizationUnit,
E: data.emailAddress
});
/* CryptoKeyPair and signing algorithm from private key */
const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem);
/* SAN extension */
if (data.altNames.length) {
extensionRequests.push({
extname: 'subjectAltName',
array: formatCsrAltNames(data.altNames)
});
}
const extensions = [
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
createSubjectAltNameExtension(data.altNames)
];
/* Create CSR */
const csr = new jsrsasign.KJUR.asn1.csr.CertificationRequest({
subject: { array: subject },
sigalg: info.signatureAlgorithm,
sbjprvkey: keyPem.toString(),
sbjpubkey: jwk,
extreq: extensionRequests
const csr = await x509.Pkcs10CertificateRequestGenerator.create({
keys,
extensions,
signingAlgorithm,
name: createCsrSubject({
CN: data.commonName,
C: data.country,
ST: data.state,
L: data.locality,
O: data.organization,
OU: data.organizationUnit,
E: data.emailAddress
})
});
/* Sign CSR, get PEM */
csr.sign();
const pem = csr.getPEM();
/* Done */
const pem = csr.toString('pem');
return [keyPem, Buffer.from(pem)];
};
/**
* Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
*
* https://datatracker.ietf.org/doc/html/rfc8737
*
* @param {object} authz Identifier authorization
* @param {string} keyAuthorization Challenge key authorization
* @param {buffer|string} [keyPem] PEM encoded CSR private key
* @returns {Promise<buffer[]>} [privateKey, certificate]
*
* @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);
*/
exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
if (!keyPem) {
keyPem = await createPrivateRsaKey();
}
else if (!Buffer.isBuffer(keyPem)) {
keyPem = Buffer.from(keyPem);
}
const now = new Date();
const commonName = authz.identifier.value;
/* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */
const random = await randomInt(1, 999999999);
const serialNumber = `${Math.floor(now.getTime() / 1000)}${random}`;
/* CryptoKeyPair and signing algorithm from private key */
const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem);
const extensions = [
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), // eslint-disable-line no-bitwise
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 */
new x509.BasicConstraintsExtension(true, 2, true),
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 */
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
createSubjectAltNameExtension([commonName])
];
/* ALPN extension */
const payload = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
const octstr = new asn1js.OctetString({ valueHex: Buffer.from(payload, 'hex') });
extensions.push(new x509.Extension(alpnAcmeIdentifierOID, true, octstr.toBER()));
/* Self-signed ALPN certificate */
const cert = await x509.X509CertificateGenerator.createSelfSigned({
keys,
signingAlgorithm,
extensions,
serialNumber,
notBefore: now,
notAfter: now,
name: createCsrSubject({
CN: commonName
})
});
/* Done */
const pem = cert.toString('pem');
return [keyPem, Buffer.from(pem)];
};
/**
* Validate that a ALPN certificate contains the expected key authorization
*
* @param {buffer|string} certPem PEM encoded certificate
* @param {string} keyAuthorization Expected challenge key authorization
* @returns {boolean} True when valid
*/
exports.isAlpnCertificateAuthorizationValid = (certPem, keyAuthorization) => {
const expected = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
/* Attempt to locate ALPN extension */
const cert = new x509.X509Certificate(certPem);
const ext = cert.getExtension(alpnAcmeIdentifierOID);
if (!ext) {
throw new Error('Unable to locate ALPN extension within parsed certificate');
}
/* Decode extension value */
const parsed = asn1js.fromBER(ext.value);
const result = Buffer.from(parsed.result.valueBlock.valueHexView).toString('hex');
/* Return true if match */
return (result === expected);
};

View File

@ -64,7 +64,7 @@ class HttpClient {
/**
* Ensure provider directory exists
*
* https://tools.ietf.org/html/rfc8555#section-7.1.1
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
*
* @returns {Promise}
*/
@ -104,7 +104,7 @@ class HttpClient {
/**
* Get nonce from directory API endpoint
*
* https://tools.ietf.org/html/rfc8555#section-7.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
*
* @returns {Promise<string>} nonce
*/
@ -267,7 +267,7 @@ class HttpClient {
/**
* Signed HTTP request
*
* https://tools.ietf.org/html/rfc8555#section-6.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.2
*
* @param {string} url Request URL
* @param {object} payload Request payload
@ -299,7 +299,7 @@ class HttpClient {
const data = this.createSignedBody(url, payload, { nonce, kid });
const resp = await this.request(url, 'post', { data });
/* Retry on bad nonce - https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.4 */
/* Retry on bad nonce - https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-10#section-6.4 */
if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) {
nonce = resp.headers['replay-nonce'] || null;
attempts += 1;

View File

@ -2,6 +2,7 @@
* Utility methods
*/
const tls = require('tls');
const dns = require('dns').promises;
const { readCertificateInfo, splitPemChain } = require('./crypto');
const { log } = require('./logger');
@ -92,7 +93,7 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
*
* @param {string} header Link header contents
* @param {string} rel Link relation, default: `alternate`
* @returns {array} Array of URLs
* @returns {string[]} Array of URLs
*/
function parseLinkHeader(header, rel = 'alternate') {
@ -112,7 +113,7 @@ function parseLinkHeader(header, rel = 'alternate') {
* - If issuer is found in multiple chains, the closest to root wins
* - If issuer can not be located, the first chain will be returned
*
* @param {array} certificates Array of PEM encoded certificate chains
* @param {string[]} certificates Array of PEM encoded certificate chains
* @param {string} issuer Preferred certificate issuer
* @returns {string} PEM encoded certificate chain
*/
@ -245,6 +246,60 @@ async function getAuthoritativeDnsResolver(recordName) {
}
/**
* Attempt to retrieve TLS ALPN certificate from peer
*
* https://nodejs.org/api/tls.html#tlsconnectoptions-callback
*
* @param {string} host Host the TLS client should connect to
* @param {number} port Port the client should connect to
* @param {string} servername Server name for the SNI (Server Name Indication)
* @returns {Promise<string>} PEM encoded certificate
*/
async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
return new Promise((resolve, reject) => {
let result;
/* TLS connection */
const socket = tls.connect({
host,
port,
servername: host,
rejectUnauthorized: false,
ALPNProtocols: ['acme-tls/1']
});
socket.setTimeout(timeout);
socket.setEncoding('utf-8');
/* Grab certificate once connected and close */
socket.on('secureConnect', () => {
result = socket.getPeerX509Certificate();
socket.end();
});
/* Errors */
socket.on('error', (err) => {
reject(err);
});
socket.on('timeout', () => {
socket.destroy(new Error('TLS ALPN certificate lookup request timed out'));
});
/* Done, return cert as PEM if found */
socket.on('end', () => {
if (result) {
return resolve(result.toString());
}
return reject(new Error('TLS ALPN lookup failed to retrieve certificate'));
});
});
}
/**
* Export utils
*/
@ -254,5 +309,6 @@ module.exports = {
parseLinkHeader,
findCertificateChainForIssuer,
formatResponseError,
getAuthoritativeDnsResolver
getAuthoritativeDnsResolver,
retrieveTlsAlpnCertificate
};

View File

@ -3,15 +3,17 @@
*/
const dns = require('dns').promises;
const https = require('https');
const { log } = require('./logger');
const axios = require('./axios');
const util = require('./util');
const { isAlpnCertificateAuthorizationValid } = require('./crypto');
/**
* Verify ACME HTTP challenge
*
* https://tools.ietf.org/html/rfc8555#section-8.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
*
* @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge
@ -24,8 +26,11 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
const challengeUrl = `http://${authz.identifier.value}:${httpPort}${suffix}`;
/* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
const resp = await axios.get(challengeUrl);
const resp = await axios.get(challengeUrl, { httpsAgent });
const data = (resp.data || '').replace(/\s+$/, '');
log(`Query successful, HTTP status code: ${resp.status}`);
@ -80,7 +85,7 @@ async function walkDnsChallengeRecord(recordName, resolver = dns) {
/**
* Verify ACME DNS challenge
*
* https://tools.ietf.org/html/rfc8555#section-8.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
*
* @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge
@ -117,11 +122,40 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
}
/**
* Verify ACME TLS ALPN challenge
*
* https://datatracker.ietf.org/doc/html/rfc8737
*
* @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge
* @param {string} keyAuthorization Challenge key authorization
* @returns {Promise<boolean>}
*/
async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
const host = authz.identifier.value;
log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
log('Certificate received from server successfully, matching key authorization in ALPN');
if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
}
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
return true;
}
/**
* Export API
*/
module.exports = {
'http-01': verifyHttpChallenge,
'dns-01': verifyDnsChallenge
'dns-01': verifyDnsChallenge,
'tls-alpn-01': verifyTlsAlpnChallenge
};

View File

@ -3,16 +3,23 @@
*/
const dns = require('dns').promises;
const { randomUUID: uuid } = require('crypto');
const https = require('https');
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
const cts = require('./challtestsrv');
const axios = require('./../src/axios');
const { retrieveTlsAlpnCertificate } = require('./../src/util');
const { isAlpnCertificateAuthorizationValid } = require('./../src/crypto');
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
describe('pebble', () => {
const httpsAgent = new https.Agent({ rejectUnauthorized: false });
const testAHost = `${uuid()}.${domainName}`;
const testARecords = ['1.1.1.1', '2.2.2.2'];
const testCnameHost = `${uuid()}.${domainName}`;
@ -21,9 +28,17 @@ describe('pebble', () => {
const testHttp01ChallengeHost = `${uuid()}.${domainName}`;
const testHttp01ChallengeToken = uuid();
const testHttp01ChallengeContent = uuid();
const testHttps01ChallengeHost = `${uuid()}.${domainName}`;
const testHttps01ChallengeToken = uuid();
const testHttps01ChallengeContent = uuid();
const testDns01ChallengeHost = `_acme-challenge.${uuid()}.${domainName}.`;
const testDns01ChallengeValue = uuid();
const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`;
const testTlsAlpn01ChallengeValue = uuid();
/**
* Pebble CTS required
@ -79,43 +94,122 @@ describe('pebble', () => {
/**
* Challenge response
* HTTP-01 challenge response
*/
describe('challenges', () => {
it('should not locate http-01 challenge response', async () => {
describe('http-01', () => {
it('should not locate challenge response', async () => {
const resp = await axios.get(`http://${testHttp01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttp01ChallengeToken}`);
assert.isString(resp.data);
assert.notEqual(resp.data, testHttp01ChallengeContent);
});
it('should add http-01 challenge response', async () => {
it('should add challenge response', async () => {
const resp = await cts.addHttp01ChallengeResponse(testHttp01ChallengeToken, testHttp01ChallengeContent);
assert.isTrue(resp);
});
it('should locate http-01 challenge response', async () => {
it('should locate challenge response', async () => {
const resp = await axios.get(`http://${testHttp01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttp01ChallengeToken}`);
assert.isString(resp.data);
assert.strictEqual(resp.data, testHttp01ChallengeContent);
});
});
it('should not locate dns-01 challenge response', async () => {
/**
* HTTPS-01 challenge response
*/
describe('https-01', () => {
it('should not locate challenge response', async () => {
const r1 = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent });
const r2 = await axios.get(`https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent });
[r1, r2].forEach((resp) => {
assert.isString(resp.data);
assert.notEqual(resp.data, testHttps01ChallengeContent);
});
});
it('should add challenge response', async () => {
const resp = await cts.addHttps01ChallengeResponse(testHttps01ChallengeToken, testHttps01ChallengeContent, testHttps01ChallengeHost);
assert.isTrue(resp);
});
it('should 302 with self-signed cert', async () => {
/* Assert HTTP 302 */
const resp = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, {
maxRedirects: 0,
validateStatus: null
});
assert.strictEqual(resp.status, 302);
assert.strictEqual(resp.headers.location, `https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`);
/* Self-signed cert test */
await assert.isRejected(axios.get(`https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`));
await assert.isFulfilled(axios.get(`https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent }));
});
it('should locate challenge response', async () => {
const r1 = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent });
const r2 = await axios.get(`https://${testHttps01ChallengeHost}:${httpsPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { httpsAgent });
[r1, r2].forEach((resp) => {
assert.isString(resp.data);
assert.strictEqual(resp.data, testHttps01ChallengeContent);
});
});
});
/**
* DNS-01 challenge response
*/
describe('dns-01', () => {
it('should not locate challenge response', async () => {
await assert.isRejected(dns.resolveTxt(testDns01ChallengeHost));
});
it('should add dns-01 challenge response', async () => {
it('should add challenge response', async () => {
const resp = await cts.addDns01ChallengeResponse(testDns01ChallengeHost, testDns01ChallengeValue);
assert.isTrue(resp);
});
it('should locate dns-01 challenge response', async () => {
it('should locate challenge response', async () => {
const resp = await dns.resolveTxt(testDns01ChallengeHost);
assert.isArray(resp);
assert.deepStrictEqual(resp, [[testDns01ChallengeValue]]);
});
});
/**
* TLS-ALPN-01 challenge response
*/
describe('tls-alpn-01', () => {
it('should not locate challenge response', async () => {
await assert.isRejected(retrieveTlsAlpnCertificate(testTlsAlpn01ChallengeHost, tlsAlpnPort), /(failed to retrieve)|(ssl3_read_bytes:tlsv1 alert internal error)/);
});
it('should timeout challenge response', async () => {
await assert.isRejected(retrieveTlsAlpnCertificate('example.org', tlsAlpnPort, 500));
});
it('should add challenge response', async () => {
const resp = await cts.addTlsAlpn01ChallengeResponse(testTlsAlpn01ChallengeHost, testTlsAlpn01ChallengeValue);
assert.isTrue(resp);
});
it('should locate challenge response', async () => {
const resp = await retrieveTlsAlpnCertificate(testTlsAlpn01ChallengeHost, tlsAlpnPort);
assert.isTrue(isAlpnCertificateAuthorizationValid(resp, testTlsAlpn01ChallengeValue));
});
});
});

View File

@ -2,8 +2,8 @@
* HTTP client tests
*/
const { randomUUID: uuid } = require('crypto');
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
const nock = require('nock');
const axios = require('./../src/axios');
const HttpClient = require('./../src/http');
@ -26,8 +26,6 @@ describe('http', () => {
*/
before(() => {
axios.defaults.acmeSettings.bypassCustomDnsResolver = true;
const defaultUaOpts = { reqheaders: { 'User-Agent': defaultUserAgent } };
const customUaOpts = { reqheaders: { 'User-Agent': customUserAgent } };
@ -43,7 +41,6 @@ describe('http', () => {
after(() => {
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
axios.defaults.acmeSettings.bypassCustomDnsResolver = false;
});

View File

@ -2,8 +2,8 @@
* Challenge verification tests
*/
const { randomUUID: uuid } = require('crypto');
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
const cts = require('./challtestsrv');
const verify = require('./../src/verify');
@ -17,11 +17,19 @@ describe('verify', () => {
const testHttp01Challenge = { type: 'http-01', status: 'pending', token: uuid() };
const testHttp01Key = uuid();
const testHttps01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } };
const testHttps01Challenge = { type: 'http-01', status: 'pending', token: uuid() };
const testHttps01Key = uuid();
const testDns01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } };
const testDns01Challenge = { type: 'dns-01', status: 'pending', token: uuid() };
const testDns01Key = uuid();
const testDns01Cname = `${uuid()}.${domainName}`;
const testTlsAlpn01Authz = { identifier: { type: 'dns', value: `${uuid()}.${domainName}` } };
const testTlsAlpn01Challenge = { type: 'dns-01', status: 'pending', token: uuid() };
const testTlsAlpn01Key = uuid();
/**
* Pebble CTS required
@ -74,6 +82,27 @@ describe('verify', () => {
});
/**
* https-01
*/
describe('https-01', () => {
it('should reject challenge', async () => {
await assert.isRejected(verify['http-01'](testHttps01Authz, testHttps01Challenge, testHttps01Key));
});
it('should mock challenge response', async () => {
const resp = await cts.addHttps01ChallengeResponse(testHttps01Challenge.token, testHttps01Key, testHttps01Authz.identifier.value);
assert.isTrue(resp);
});
it('should verify challenge', async () => {
const resp = await verify['http-01'](testHttps01Authz, testHttps01Challenge, testHttps01Key);
assert.isTrue(resp);
});
});
/**
* dns-01
*/
@ -103,4 +132,25 @@ describe('verify', () => {
assert.isTrue(resp);
});
});
/**
* tls-alpn-01
*/
describe('tls-alpn-01', () => {
it('should reject challenge', async () => {
await assert.isRejected(verify['tls-alpn-01'](testTlsAlpn01Authz, testTlsAlpn01Challenge, testTlsAlpn01Key));
});
it('should mock challenge response', async () => {
const resp = await cts.addTlsAlpn01ChallengeResponse(testTlsAlpn01Authz.identifier.value, testTlsAlpn01Key);
assert.isTrue(resp);
});
it('should verify challenge', async () => {
const resp = await verify['tls-alpn-01'](testTlsAlpn01Authz, testTlsAlpn01Challenge, testTlsAlpn01Key);
assert.isTrue(resp);
});
});
});

View File

@ -10,10 +10,10 @@ const { crypto } = require('./../');
const emptyBodyChain1 = `
-----BEGIN TEST-----
a
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST-----
-----BEGIN TEST-----
b
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST-----
-----BEGIN TEST-----
@ -22,7 +22,7 @@ b
-----BEGIN TEST-----
c
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST-----
`;
@ -38,15 +38,15 @@ const emptyBodyChain2 = `
-----END TEST-----
-----BEGIN TEST-----
a
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST-----
-----BEGIN TEST-----
b
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST-----
-----BEGIN TEST-----
c
dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZw==
-----END TEST-----
`;
@ -95,6 +95,7 @@ describe('crypto', () => {
let testSanCsr;
let testNonCnCsr;
let testNonAsciiCsr;
let testAlpnCertificate;
/**
@ -111,6 +112,11 @@ describe('crypto', () => {
assert.isTrue(Buffer.isBuffer(testPublicKeys[n]));
});
it(`${n}/should get public key from string`, () => {
testPublicKeys[n] = crypto.getPublicKey(testPrivateKeys[n].toString());
assert.isTrue(Buffer.isBuffer(testPublicKeys[n]));
});
it(`${n}/should get jwk from private key`, () => {
const jwk = crypto.getJwk(testPrivateKeys[n]);
jwkSpecFn(jwk);
@ -121,6 +127,11 @@ describe('crypto', () => {
jwkSpecFn(jwk);
});
it(`${n}/should get jwk from string`, () => {
const jwk = crypto.getJwk(testPrivateKeys[n].toString());
jwkSpecFn(jwk);
});
/**
* Certificate Signing Request
@ -173,6 +184,15 @@ describe('crypto', () => {
testNonAsciiCsr = csr;
});
it(`${n}/should generate a csr with key as string`, async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain
}, testPrivateKeys[n].toString());
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr));
});
it(`${n}/should throw with invalid key`, async () => {
await assert.isRejected(crypto.createCsr({
commonName: testCsrDomain
@ -215,6 +235,51 @@ describe('crypto', () => {
assert.strictEqual(result.commonName, testCsrDomain);
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
});
it(`${n}/should resolve domains from csr string`, () => {
[testCsr, testSanCsr, testNonCnCsr, testNonAsciiCsr].forEach((csr) => {
const result = crypto.readCsrDomains(csr.toString());
spec.crypto.csrDomains(result);
});
});
/**
* ALPN
*/
it(`${n}/should generate alpn certificate`, async () => {
const authz = { identifier: { value: 'test.example.com' } };
const [key, cert] = await crypto.createAlpnCertificate(authz, 'super-secret.12345', await createFn());
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(cert));
testAlpnCertificate = cert;
});
it(`${n}/should generate alpn certificate with key as string`, async () => {
const k = await createFn();
const authz = { identifier: { value: 'test.example.com' } };
const [key, cert] = await crypto.createAlpnCertificate(authz, 'super-secret.12345', k.toString());
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(cert));
});
it(`${n}/should not validate invalid alpn certificate key authorization`, () => {
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'aaaaaaa'));
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'bbbbbbb'));
assert.isFalse(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'ccccccc'));
});
it(`${n}/should validate valid alpn certificate key authorization`, () => {
assert.isTrue(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate, 'super-secret.12345'));
});
it(`${n}/should validate valid alpn certificate with cert as string`, () => {
assert.isTrue(crypto.isAlpnCertificateAuthorizationValid(testAlpnCertificate.toString(), 'super-secret.12345'));
});
});
});
});
@ -250,7 +315,7 @@ describe('crypto', () => {
* CSR with auto-generated key
*/
it('should generate a csr with auto-generated key', async () => {
it('should generate a csr with default key', async () => {
const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain
});
@ -280,6 +345,26 @@ describe('crypto', () => {
assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length));
});
it('should read certificate info from string', () => {
[testCert, testSanCert].forEach((cert) => {
const info = crypto.readCertificateInfo(cert.toString());
spec.crypto.certificateInfo(info);
});
});
/**
* ALPN
*/
it('should generate alpn certificate with default key', async () => {
const authz = { identifier: { value: 'test.example.com' } };
const [key, cert] = await crypto.createAlpnCertificate(authz, 'abc123');
assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(cert));
});
/**
* PEM utils
@ -296,6 +381,17 @@ describe('crypto', () => {
});
});
it('should get pem body as b64u from string', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const body = crypto.getPemBodyAsB64u(pem.toString());
assert.isString(body);
assert.notInclude(body, '\r');
assert.notInclude(body, '\n');
assert.notInclude(body, '\r\n');
});
});
it('should split pem chain', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const chain = crypto.splitPemChain(pem);
@ -306,6 +402,16 @@ describe('crypto', () => {
});
});
it('should split pem chain from string', () => {
[testPemKey, testCert, testSanCert].forEach((pem) => {
const chain = crypto.splitPemChain(pem.toString());
assert.isArray(chain);
assert.isNotEmpty(chain);
chain.forEach((c) => assert.isString(c));
});
});
it('should split pem chain with empty bodies', () => {
const c1 = crypto.splitPemChain(emptyBodyChain1);
const c2 = crypto.splitPemChain(emptyBodyChain2);

View File

@ -2,8 +2,8 @@
* ACME client tests
*/
const { randomUUID: uuid } = require('crypto');
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
const cts = require('./challtestsrv');
const getCertIssuers = require('./get-cert-issuers');
const spec = require('./spec');
@ -33,6 +33,7 @@ if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY)
describe('client', () => {
const testDomain = `${uuid()}.${domainName}`;
const testDomainAlpn = `${uuid()}.${domainName}`;
const testDomainWildcard = `*.${testDomain}`;
const testContact = `mailto:test-${uuid()}@nope.com`;
@ -78,16 +79,22 @@ describe('client', () => {
let testAccount;
let testAccountUrl;
let testOrder;
let testOrderAlpn;
let testOrderWildcard;
let testAuthz;
let testAuthzAlpn;
let testAuthzWildcard;
let testChallenge;
let testChallengeAlpn;
let testChallengeWildcard;
let testKeyAuthorization;
let testKeyAuthorizationAlpn;
let testKeyAuthorizationWildcard;
let testCsr;
let testCsrAlpn;
let testCsrWildcard;
let testCertificate;
let testCertificateAlpn;
let testCertificateWildcard;
@ -107,6 +114,7 @@ describe('client', () => {
it('should generate certificate signing request', async () => {
[, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn());
[, testCsrAlpn] = await acme.crypto.createCsr({ commonName: testDomainAlpn }, await createKeyFn());
[, testCsrWildcard] = await acme.crypto.createCsr({ commonName: testDomainWildcard }, await createKeyFn());
});
@ -336,12 +344,14 @@ describe('client', () => {
it('should create new order', async () => {
const data1 = { identifiers: [{ type: 'dns', value: testDomain }] };
const data2 = { identifiers: [{ type: 'dns', value: testDomainWildcard }] };
const data2 = { identifiers: [{ type: 'dns', value: testDomainAlpn }] };
const data3 = { identifiers: [{ type: 'dns', value: testDomainWildcard }] };
testOrder = await testClient.createOrder(data1);
testOrderWildcard = await testClient.createOrder(data2);
testOrderAlpn = await testClient.createOrder(data2);
testOrderWildcard = await testClient.createOrder(data3);
[testOrder, testOrderWildcard].forEach((item) => {
[testOrder, testOrderAlpn, testOrderWildcard].forEach((item) => {
spec.rfc8555.order(item);
assert.strictEqual(item.status, 'pending');
});
@ -353,7 +363,7 @@ describe('client', () => {
*/
it('should get existing order', async () => {
await Promise.all([testOrder, testOrderWildcard].map(async (existing) => {
await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (existing) => {
const result = await testClient.getOrder(existing);
spec.rfc8555.order(result);
@ -368,9 +378,10 @@ describe('client', () => {
it('should get identifier authorization', async () => {
const orderAuthzCollection = await testClient.getAuthorizations(testOrder);
const alpnAuthzCollection = await testClient.getAuthorizations(testOrderAlpn);
const wildcardAuthzCollection = await testClient.getAuthorizations(testOrderWildcard);
[orderAuthzCollection, wildcardAuthzCollection].forEach((collection) => {
[orderAuthzCollection, alpnAuthzCollection, wildcardAuthzCollection].forEach((collection) => {
assert.isArray(collection);
assert.isNotEmpty(collection);
@ -381,9 +392,10 @@ describe('client', () => {
});
testAuthz = orderAuthzCollection.pop();
testAuthzAlpn = alpnAuthzCollection.pop();
testAuthzWildcard = wildcardAuthzCollection.pop();
testAuthz.challenges.concat(testAuthzWildcard.challenges).forEach((item) => {
testAuthz.challenges.concat(testAuthzAlpn.challenges).concat(testAuthzWildcard.challenges).forEach((item) => {
spec.rfc8555.challenge(item);
assert.strictEqual(item.status, 'pending');
});
@ -396,12 +408,14 @@ describe('client', () => {
it('should get challenge key authorization', async () => {
testChallenge = testAuthz.challenges.find((c) => (c.type === 'http-01'));
testChallengeAlpn = testAuthzAlpn.challenges.find((c) => (c.type === 'tls-alpn-01'));
testChallengeWildcard = testAuthzWildcard.challenges.find((c) => (c.type === 'dns-01'));
testKeyAuthorization = await testClient.getChallengeKeyAuthorization(testChallenge);
testKeyAuthorizationAlpn = await testClient.getChallengeKeyAuthorization(testChallengeAlpn);
testKeyAuthorizationWildcard = await testClient.getChallengeKeyAuthorization(testChallengeWildcard);
[testKeyAuthorization, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
[testKeyAuthorization, testKeyAuthorizationAlpn, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
});
@ -438,9 +452,11 @@ describe('client', () => {
it('should verify challenge', async () => {
await cts.assertHttpChallengeCreateFn(testAuthz, testChallenge, testKeyAuthorization);
await cts.assertTlsAlpnChallengeCreateFn(testAuthzAlpn, testChallengeAlpn, testKeyAuthorizationAlpn);
await cts.assertDnsChallengeCreateFn(testAuthzWildcard, testChallengeWildcard, testKeyAuthorizationWildcard);
await testClient.verifyChallenge(testAuthz, testChallenge);
await testClient.verifyChallenge(testAuthzAlpn, testChallengeAlpn);
await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard);
});
@ -450,7 +466,7 @@ describe('client', () => {
*/
it('should complete challenge', async () => {
await Promise.all([testChallenge, testChallengeWildcard].map(async (challenge) => {
await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (challenge) => {
const result = await testClient.completeChallenge(challenge);
spec.rfc8555.challenge(result);
@ -464,7 +480,7 @@ describe('client', () => {
*/
it('should wait for valid challenge status', async () => {
await Promise.all([testChallenge, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
});
@ -474,11 +490,13 @@ describe('client', () => {
it('should finalize order', async () => {
const finalize = await testClient.finalizeOrder(testOrder, testCsr);
const finalizeAlpn = await testClient.finalizeOrder(testOrderAlpn, testCsrAlpn);
const finalizeWildcard = await testClient.finalizeOrder(testOrderWildcard, testCsrWildcard);
[finalize, finalizeWildcard].forEach((f) => spec.rfc8555.order(f));
[finalize, finalizeAlpn, finalizeWildcard].forEach((f) => spec.rfc8555.order(f));
assert.strictEqual(testOrder.url, finalize.url);
assert.strictEqual(testOrderAlpn.url, finalizeAlpn.url);
assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url);
});
@ -488,7 +506,7 @@ describe('client', () => {
*/
it('should wait for valid order status', async () => {
await Promise.all([testOrder, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
});
@ -498,9 +516,10 @@ describe('client', () => {
it('should get certificate', async () => {
testCertificate = await testClient.getCertificate(testOrder);
testCertificateAlpn = await testClient.getCertificate(testOrderAlpn);
testCertificateWildcard = await testClient.getCertificate(testOrderWildcard);
[testCertificate, testCertificateWildcard].forEach((cert) => {
[testCertificate, testCertificateAlpn, testCertificateWildcard].forEach((cert) => {
assert.isString(cert);
acme.crypto.readCertificateInfo(cert);
});
@ -539,11 +558,13 @@ describe('client', () => {
it('should revoke certificate', async () => {
await testClient.revokeCertificate(testCertificate);
await testClient.revokeCertificate(testCertificateAlpn, { reason: 0 });
await testClient.revokeCertificate(testCertificateWildcard, { reason: 4 });
});
it('should not allow getting revoked certificate', async () => {
await assert.isRejected(testClient.getCertificate(testOrder));
await assert.isRejected(testClient.getCertificate(testOrderAlpn));
await assert.isRejected(testClient.getCertificate(testOrderWildcard));
});

View File

@ -2,8 +2,8 @@
* ACME client.auto tests
*/
const { randomUUID: uuid } = require('crypto');
const { assert } = require('chai');
const { v4: uuid } = require('uuid');
const cts = require('./challtestsrv');
const getCertIssuers = require('./get-cert-issuers');
const spec = require('./spec');
@ -32,7 +32,9 @@ if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY)
describe('client.auto', () => {
const testDomain = `${uuid()}.${domainName}`;
const testHttpDomain = `${uuid()}.${domainName}`;
const testHttpsDomain = `${uuid()}.${domainName}`;
const testDnsDomain = `${uuid()}.${domainName}`;
const testAlpnDomain = `${uuid()}.${domainName}`;
const testWildcardDomain = `${uuid()}.${domainName}`;
const testSanDomains = [
@ -178,6 +180,38 @@ describe('client.auto', () => {
assert.isString(cert);
});
it('should settle all challenges before rejecting', async () => {
const results = [];
const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}`,
altNames: [
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`,
`${uuid()}.${domainName}`
]
}, await createKeyFn());
await assert.isRejected(testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: async (...args) => {
if ([0, 1, 2].includes(results.length)) {
results.push(false);
throw new Error('oops');
}
await new Promise((resolve) => { setTimeout(resolve, 500); });
results.push(true);
return cts.challengeCreateFn(...args);
},
challengeRemoveFn: cts.challengeRemoveFn
}));
assert.strictEqual(results.length, 5);
assert.deepStrictEqual(results, [false, false, false, true, true]);
});
/**
* Order certificates
@ -215,6 +249,22 @@ describe('client.auto', () => {
assert.isString(cert);
});
it('should order certificate using https-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testHttpsDomain
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertHttpsChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['http-01']
});
assert.isString(cert);
});
it('should order certificate using dns-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testDnsDomain
@ -231,6 +281,22 @@ describe('client.auto', () => {
assert.isString(cert);
});
it('should order certificate using tls-alpn-01', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testAlpnDomain
}, await createKeyFn());
const cert = await testClient.auto({
csr,
termsOfServiceAgreed: true,
challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['tls-alpn-01']
});
assert.isString(cert);
});
it('should order san certificate', async () => {
const [, csr] = await acme.crypto.createCsr({
commonName: testSanDomains[0],

View File

@ -6,6 +6,7 @@ const { assert } = require('chai');
const axios = require('./../src/axios');
const apiBaseUrl = process.env.ACME_CHALLTESTSRV_URL || null;
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
/**
@ -50,12 +51,26 @@ async function addHttp01ChallengeResponse(token, content) {
return request('add-http01', { token, content });
}
async function addHttps01ChallengeResponse(token, content, targetHostname) {
await addHttp01ChallengeResponse(token, content);
return request('add-redirect', {
path: `/.well-known/acme-challenge/${token}`,
targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}`
});
}
async function addDns01ChallengeResponse(host, value) {
return request('set-txt', { host, value });
}
async function addTlsAlpn01ChallengeResponse(host, content) {
return request('add-tlsalpn01', { host, content });
}
exports.addHttp01ChallengeResponse = addHttp01ChallengeResponse;
exports.addHttps01ChallengeResponse = addHttps01ChallengeResponse;
exports.addDns01ChallengeResponse = addDns01ChallengeResponse;
exports.addTlsAlpn01ChallengeResponse = addTlsAlpn01ChallengeResponse;
/**
@ -67,11 +82,21 @@ async function assertHttpChallengeCreateFn(authz, challenge, keyAuthorization) {
return addHttp01ChallengeResponse(challenge.token, keyAuthorization);
}
async function assertHttpsChallengeCreateFn(authz, challenge, keyAuthorization) {
assert.strictEqual(challenge.type, 'http-01');
return addHttps01ChallengeResponse(challenge.token, keyAuthorization, authz.identifier.value);
}
async function assertDnsChallengeCreateFn(authz, challenge, keyAuthorization) {
assert.strictEqual(challenge.type, 'dns-01');
return addDns01ChallengeResponse(`_acme-challenge.${authz.identifier.value}.`, keyAuthorization);
}
async function assertTlsAlpnChallengeCreateFn(authz, challenge, keyAuthorization) {
assert.strictEqual(challenge.type, 'tls-alpn-01');
return addTlsAlpn01ChallengeResponse(authz.identifier.value, keyAuthorization);
}
async function challengeCreateFn(authz, challenge, keyAuthorization) {
if (challenge.type === 'http-01') {
return assertHttpChallengeCreateFn(authz, challenge, keyAuthorization);
@ -81,6 +106,10 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
return assertDnsChallengeCreateFn(authz, challenge, keyAuthorization);
}
if (challenge.type === 'tls-alpn-01') {
return assertTlsAlpnChallengeCreateFn(authz, challenge, keyAuthorization);
}
throw new Error(`Unsupported challenge type ${challenge.type}`);
}
@ -89,5 +118,7 @@ exports.challengeNoopFn = async () => true;
exports.challengeThrowFn = async () => { throw new Error('oops'); };
exports.assertHttpChallengeCreateFn = assertHttpChallengeCreateFn;
exports.assertHttpsChallengeCreateFn = assertHttpsChallengeCreateFn;
exports.assertDnsChallengeCreateFn = assertDnsChallengeCreateFn;
exports.assertTlsAlpnChallengeCreateFn = assertTlsAlpnChallengeCreateFn;
exports.challengeCreateFn = challengeCreateFn;

View File

@ -2,10 +2,7 @@
* Setup testing
*/
const url = require('url');
const net = require('net');
const fs = require('fs');
const dns = require('dns').promises;
const chai = require('chai');
const chaiAsPromised = require('chai-as-promised');
const axios = require('./../src/axios');
@ -19,13 +16,21 @@ chai.use(chaiAsPromised);
/**
* HTTP challenge port
* Challenge test server ports
*/
if (process.env.ACME_HTTP_PORT) {
axios.defaults.acmeSettings.httpChallengePort = process.env.ACME_HTTP_PORT;
}
if (process.env.ACME_HTTPS_PORT) {
axios.defaults.acmeSettings.httpsChallengePort = process.env.ACME_HTTPS_PORT;
}
if (process.env.ACME_TLSALPN_PORT) {
axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT;
}
/**
* External account binding
@ -38,50 +43,3 @@ if (('ACME_CAP_EAB_ENABLED' in process.env) && (process.env.ACME_CAP_EAB_ENABLED
process.env.ACME_EAB_KID = kid;
process.env.ACME_EAB_HMAC_KEY = hmacKey;
}
/**
* Custom DNS resolver
*/
if (process.env.ACME_DNS_RESOLVER) {
dns.setServers([process.env.ACME_DNS_RESOLVER]);
/**
* Axios DNS resolver
*/
axios.interceptors.request.use(async (config) => {
const urlObj = url.parse(config.url);
/* Bypass */
if (axios.defaults.acmeSettings.bypassCustomDnsResolver === true) {
return config;
}
/* Skip IP addresses and localhost */
if (net.isIP(urlObj.hostname) || (urlObj.hostname === 'localhost')) {
return config;
}
/* Lookup hostname */
const result = await dns.resolve4(urlObj.hostname);
if (!result.length) {
throw new Error(`Unable to lookup address: ${urlObj.hostname}`);
}
/* Place hostname in header */
config.headers = config.headers || {};
config.headers.Host = urlObj.hostname;
/* Inject address into URL */
delete urlObj.host;
urlObj.hostname = result[0];
config.url = url.format(urlObj);
/* Done */
return config;
});
}

View File

@ -156,6 +156,8 @@ export interface CryptoInterface {
readCsrDomains(csrPem: CsrBuffer | CsrString): CertificateDomains;
readCertificateInfo(certPem: CertificateBuffer | CertificateString): CertificateInfo;
createCsr(data: CsrOptions, keyPem?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CsrBuffer]>;
createAlpnCertificate(authz: Authorization, keyAuthorization: string, keyPem?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CertificateBuffer]>;
isAlpnCertificateAuthorizationValid(certPem: CertificateBuffer | CertificateString, keyAuthorization: string): boolean;
}
export const crypto: CryptoInterface;

View File

@ -1,9 +1,9 @@
/**
* Account
*
* https://tools.ietf.org/html/rfc8555#section-7.1.2
* https://tools.ietf.org/html/rfc8555#section-7.3
* https://tools.ietf.org/html/rfc8555#section-7.3.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.2
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
*/
export interface Account {
@ -31,8 +31,8 @@ export interface AccountUpdateRequest {
/**
* Order
*
* https://tools.ietf.org/html/rfc8555#section-7.1.3
* https://tools.ietf.org/html/rfc8555#section-7.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
*/
export interface Order {
@ -57,7 +57,7 @@ export interface OrderCreateRequest {
/**
* Authorization
*
* https://tools.ietf.org/html/rfc8555#section-7.1.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.4
*/
export interface Authorization {
@ -77,9 +77,9 @@ export interface Identifier {
/**
* Challenge
*
* https://tools.ietf.org/html/rfc8555#section-8
* https://tools.ietf.org/html/rfc8555#section-8.3
* https://tools.ietf.org/html/rfc8555#section-8.4
* https://datatracker.ietf.org/doc/html/rfc8555#section-8
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
*/
export interface ChallengeAbstract {
@ -106,7 +106,7 @@ export type Challenge = HttpChallenge | DnsChallenge;
/**
* Certificate
*
* https://tools.ietf.org/html/rfc8555#section-7.6
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
*/
export enum CertificateRevocationReason {

View File

@ -1,6 +0,0 @@
{
"extends": "dtslint/dtslint.json",
"rules": {
"no-consecutive-blank-lines": [true, 2]
}
}