From d22a25d2607b52ef62c91d272ab112e2a0b1e3cd Mon Sep 17 00:00:00 2001
From: GitHub Actions Bot
Date: Sat, 20 Jan 2024 19:24:14 +0000
Subject: [PATCH 1/8] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20w?=
=?UTF-8?q?ith=2010=20commits=20[trident-sync]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bump v5.1.0
Bump dep axios@1.6.5
Bump dep jsrsasign@11.0.0
Bump dev deps, typo in editorconfig
Replace uuid devdep with crypto.randomUUID
LICENSE, docs formatting, remove upgrade notice
Fix package.json typo
Replace deprecated dtslint with tsd, bump types
Add Node v20 to matrix, bump misc CI stuff
---
.../core/acme-client/.circleci/config.yml | 14 ++++---
packages/core/acme-client/.editorconfig | 2 +-
packages/core/acme-client/CHANGELOG.md | 42 ++-----------------
packages/core/acme-client/LICENSE | 2 +-
packages/core/acme-client/README.md | 41 ++++--------------
packages/core/acme-client/docs/upgrade-v5.md | 11 ++---
packages/core/acme-client/package.json | 28 ++++++-------
.../core/acme-client/scripts/run-tests.sh | 11 +++--
packages/core/acme-client/src/axios.js | 3 +-
.../core/acme-client/test/00-pebble.spec.js | 2 +-
.../core/acme-client/test/10-http.spec.js | 2 +-
.../core/acme-client/test/10-verify.spec.js | 2 +-
.../core/acme-client/test/50-client.spec.js | 2 +-
.../core/acme-client/test/70-auto.spec.js | 2 +-
.../acme-client/{types => }/tsconfig.json | 0
.../types/{test.ts => index.test-d.ts} | 0
packages/core/acme-client/types/tslint.json | 6 ---
17 files changed, 52 insertions(+), 118 deletions(-)
rename packages/core/acme-client/{types => }/tsconfig.json (100%)
rename packages/core/acme-client/types/{test.ts => index.test-d.ts} (100%)
delete mode 100644 packages/core/acme-client/types/tslint.json
diff --git a/packages/core/acme-client/.circleci/config.yml b/packages/core/acme-client/.circleci/config.yml
index db3ef431..7c32a0d3 100644
--- a/packages/core/acme-client/.circleci/config.yml
+++ b/packages/core/acme-client/.circleci/config.yml
@@ -91,7 +91,7 @@ commands:
name: Install CoreDNS
command: sudo -E /bin/bash ./scripts/test-suite-install-coredns.sh
environment:
- COREDNS_VERSION: 1.8.6
+ COREDNS_VERSION: 1.11.1
PEBBLECTS_DNS_PORT: 8053
- run:
@@ -117,10 +117,12 @@ commands:
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 ]}
+ v16: { docker: [{ image: cimg/node:16.20 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]}
+ v18: { docker: [{ image: cimg/node:18.19 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]}
+ v20: { docker: [{ image: cimg/node:20.11 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]}
+ eab-v16: { docker: [{ image: cimg/node:16.20 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]}
+ eab-v18: { docker: [{ image: cimg/node:18.19 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]}
+ eab-v20: { docker: [{ image: cimg/node:20.11 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]}
# step-v12: { docker: [{ image: cimg/node:12.22 }], steps: [ pre, install-cts, install-step, install-coredns, test ]}
workflows:
@@ -128,6 +130,8 @@ workflows:
jobs:
- v16
- v18
+ - v20
- eab-v16
- eab-v18
+ - eab-v20
# - step-v12
diff --git a/packages/core/acme-client/.editorconfig b/packages/core/acme-client/.editorconfig
index f95adc8f..7660a90c 100644
--- a/packages/core/acme-client/.editorconfig
+++ b/packages/core/acme-client/.editorconfig
@@ -5,7 +5,7 @@
root = true
[*]
-indent_style = spaces
+indent_style = space
indent_size = 4
trim_trailing_whitespace = true
diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md
index 05e46a01..fad2966c 100644
--- a/packages/core/acme-client/CHANGELOG.md
+++ b/packages/core/acme-client/CHANGELOG.md
@@ -1,11 +1,9 @@
# Changelog
-## 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).
+## 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)
@@ -16,39 +14,32 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
* `changed` Replace `bluebird` dependency with native promise APIs
* `changed` Replace `backo2` dependency with internal utility
-
## v4.2.5 (2022-03-21)
* `fixed` Upgrade `axios@0.26.1`
* `fixed` Upgrade `node-forge@1.3.0` - [CVE-2022-24771](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24771), [CVE-2022-24772](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24772), [CVE-2022-24773](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-24773)
-
## 4.2.4 (2022-03-19)
* `fixed` Use SHA-256 when signing CSRs
-
## v3.3.2 (2022-03-19)
* `backport` Use SHA-256 when signing CSRs
-
## v4.2.3 (2022-01-11)
* `added` Directory URLs for ACME providers [Buypass](https://www.buypass.com) and [ZeroSSL](https://zerossl.com)
* `fixed` Skip already valid authorizations when using `client.auto()`
-
## v4.2.2 (2022-01-10)
* `fixed` Upgrade `node-forge@1.2.0`
-
## v4.2.1 (2022-01-10)
* `fixed` ZeroSSL `duplicate_domains_in_array` error when using `client.auto()`
-
## 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)
@@ -59,27 +50,22 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
* `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)
-
## v4.1.4 (2021-12-23)
* `fixed` Upgrade `axios@0.21.4` - [CVE-2021-3749](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-3749)
-
## v4.1.3 (2021-02-22)
* `fixed` Upgrade `axios@0.21.1` - [CVE-2020-28168](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-28168)
-
## v4.1.2 (2020-11-16)
* `fixed` Bug when encoding PEM payloads, potentially causing malformed requests
-
## v4.1.1 (2020-11-13)
* `fixed` Missing TypeScript definitions
-
## v4.1.0 (2020-11-12)
* `added` Option `preferredChain` added to `client.getCertificate()` and `client.auto()` to indicate which certificate chain is preferred if a CA offers multiple
@@ -90,17 +76,14 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
* `fixed` Missing URL augmentation in `client.finalizeOrder()` and `client.deactivateAuthorization()`
* `fixed` Add certificate issuer to response from `forge.readCertificateInfo()`
-
## v4.0.2 (2020-10-09)
* `fixed` Explicitly set default `axios` HTTP adapter - [axios/axios#1180](https://github.com/axios/axios/issues/1180)
-
## v4.0.1 (2020-09-15)
* `fixed` Upgrade `node-forge@0.10.0` - [CVE-2020-7720](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-7720)
-
## v4.0.0 (2020-05-29)
* `breaking` Remove support for Node v8
@@ -108,23 +91,19 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
* `fixed` Incorrect TypeScript `CertificateInfo` definitions
* `fixed` Allow trailing whitespace character in `http-01` challenge response
-
## v3.3.1 (2020-01-07)
* `fixed` Improvements to TypeScript definitions
-
## 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)
-
## v3.2.1 (2019-11-14)
* `added` New option `skipChallengeVerification` added to `client.auto()` to bypass internal challenge verification
-
## v3.2.0 (2019-08-26)
* `added` More extensive testing using [letsencrypt/pebble](https://github.com/letsencrypt/pebble)
@@ -135,64 +114,53 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
* `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)
-
## v2.3.1 (2019-08-26)
* `backport` Minor bugs related to `POST-as-GET` when calling `client.updateAccount()`
* `backport` Send empty JSON body when responding to challenges
-
## 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)
-
## v2.3.0 (2019-08-21)
* `backport` Implement `POST-as-GET` for all ACME API requests
-
## v3.0.0 (2019-07-13)
* `added` Expose `axios` instance to allow manipulating HTTP client defaults
* `breaking` Remove support for Node v4 and v6
* `breaking` Remove Babel transpilation
-
## v2.2.3 (2019-01-25)
* `added` DNS CNAME detection when verifying `dns-01` challenges
-
## v2.2.2 (2019-01-07)
* `added` Support for `tls-alpn-01` challenge key authorization
-
## v2.2.1 (2019-01-04)
* `fixed` Handle and throw errors from OpenSSL process
-
## v2.2.0 (2018-11-06)
* `added` New [node-forge](https://www.npmjs.com/package/node-forge) crypto interface, removes OpenSSL CLI dependency
* `added` Support native `crypto.generateKeyPair()` API when generating key pairs
-
## v2.1.0 (2018-10-21)
* `added` Ability to set and get current account URL
* `fixed` Replace HTTP client `request` with `axios`
* `fixed` Auto-mode no longer tries to create account when account URL exists
-
## 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)
-
## v2.0.0 (2018-04-02)
* `breaking` ACMEv2
@@ -200,23 +168,19 @@ A more detailed explanation can be found [at the Let's Encrypt forums](https://c
* `breaking` Rewrite to ES6
* `breaking` Promises instead of callbacks
-
## v1.0.0 (2017-10-20)
* API stable
-
## v0.2.1 (2017-09-27)
* `fixed` Bug causing invalid anti-replay nonce
-
## v0.2.0 (2017-09-21)
* `breaking` OpenSSL method `readCsrDomains` and `readCertificateInfo` now return domains as an object
* `fixed` Added and fixed some tests
-
## v0.1.0 (2017-09-14)
* `acme-client` released
diff --git a/packages/core/acme-client/LICENSE b/packages/core/acme-client/LICENSE
index 7c8adf14..7f47c1a7 100644
--- a/packages/core/acme-client/LICENSE
+++ b/packages/core/acme-client/LICENSE
@@ -1,6 +1,6 @@
MIT License
-Copyright (c) 2017-2022 Publish Lab
+Copyright (c) 2017-2024 Labrador CMS AS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/packages/core/acme-client/README.md b/packages/core/acme-client/README.md
index 88af9efe..21388a88 100644
--- a/packages/core/acme-client/README.md
+++ b/packages/core/acme-client/README.md
@@ -7,15 +7,7 @@ This module is written to handle communication with a Boulder/Let's Encrypt-styl
* RFC 8555 - Automatic Certificate Management Environment (ACME): [https://tools.ietf.org/html/rfc8555](https://tools.ietf.org/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,7 +65,6 @@ 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.
@@ -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.
These utility methods are exposed through `.crypto`.
-* __Documentation: [docs/crypto.md](docs/crypto.md)__
+* **Documentation: [docs/crypto.md](docs/crypto.md)**
```js
const privateRsaKey = await acme.crypto.createPrivateRsaKey();
@@ -133,22 +118,20 @@ const [certificateKey, certificateCsr] = await acme.crypto.createCsr({
});
```
-
### Legacy `.forge` interface
The legacy `node-forge` crypto interface is still available for backward compatibility, however this interface is now considered deprecated and will be removed in a future major version of `acme-client`.
You should consider migrating to the new `.crypto` API at your earliest convenience. More details can be found in the [acme-client v5 upgrade guide](docs/upgrade-v5.md).
-* __Documentation: [docs/forge.md](docs/forge.md)__
-
+* **Documentation: [docs/forge.md](docs/forge.md)**
## Auto mode
For convenience an `auto()` method is included in the client that takes a single config object. This method will handle the entire process of getting a certificate for one or multiple domains.
-* __Documentation: [docs/client.md#AcmeClient+auto](docs/client.md#AcmeClient+auto)__
-* __Full example: [examples/auto.js](examples/auto.js)__
+* **Documentation: [docs/client.md#AcmeClient+auto](docs/client.md#AcmeClient+auto)**
+* **Full example: [examples/auto.js](examples/auto.js)**
```js
const autoOpts = {
@@ -162,12 +145,11 @@ const autoOpts = {
const certificate = await client.auto(autoOpts);
```
-
### Challenge priority
When ordering a certificate using auto mode, `acme-client` uses a priority list when selecting challenges to respond to. Its default value is `['http-01', 'dns-01']` which translates to "use `http-01` if any challenges exist, otherwise fall back to `dns-01`".
-While most challenges can be validated using the method of your choosing, please note that __wildcard certificates can only be validated through `dns-01`__. More information regarding Let's Encrypt challenge types [can be found here](https://letsencrypt.org/docs/challenge-types/).
+While most challenges can be validated using the method of your choosing, please note that **wildcard certificates can only be validated through `dns-01`**. More information regarding Let's Encrypt challenge types [can be found here](https://letsencrypt.org/docs/challenge-types/).
To modify challenge priority, provide a list of challenge types in `challengePriority`:
@@ -178,7 +160,6 @@ await client.auto({
});
```
-
### Internal challenge verification
When using auto mode, `acme-client` will first validate that challenges are satisfied internally before completing the challenge at the ACME provider. In some cases (firewalls, etc) this internal challenge verification might not be possible to complete.
@@ -194,13 +175,12 @@ await client.auto({
});
```
-
## API
For more fine-grained control you can interact with the ACME API using the methods documented below.
-* __Documentation: [docs/client.md](docs/client.md)__
-* __Full example: [examples/api.js](examples/api.js)__
+* **Documentation: [docs/client.md](docs/client.md)**
+* **Full example: [examples/api.js](examples/api.js)**
```js
const account = await client.createAccount({
@@ -216,7 +196,6 @@ const order = await client.createOrder({
});
```
-
## HTTP client defaults
This module uses [axios](https://github.com/axios/axios) when communicating with the ACME HTTP API, and exposes the client instance through `.axios`.
@@ -237,7 +216,6 @@ A complete list of axios options and documentation can be found at:
* [https://github.com/axios/axios#request-config](https://github.com/axios/axios#request-config)
* [https://github.com/axios/axios#custom-instance-defaults](https://github.com/axios/axios#custom-instance-defaults)
-
## Debugging
To get a better grasp of what `acme-client` is doing behind the scenes, you can either pass it a logger function, or enable debugging through an environment variable.
@@ -256,7 +234,6 @@ Debugging to the console can also be enabled through [debug](https://www.npmjs.c
DEBUG=acme-client node index.js
```
-
## License
[MIT](LICENSE)
diff --git a/packages/core/acme-client/docs/upgrade-v5.md b/packages/core/acme-client/docs/upgrade-v5.md
index a9156244..a89bb79f 100644
--- a/packages/core/acme-client/docs/upgrade-v5.md
+++ b/packages/core/acme-client/docs/upgrade-v5.md
@@ -4,7 +4,6 @@ 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.
@@ -17,9 +16,9 @@ Below you will find a table summarizing the current `acme.forge` methods, and th
*Note: The now deprecated `acme.forge` interface is still available for use in v5, and will not be removed until a future major version, most likely v6. Should you not wish to change to the new interface right away, the following breaking changes will not immediately affect you.*
-- :green_circle: = API functionality unchanged between `acme.forge` and `acme.crypto`
-- :orange_circle: = Slight API changes, like depromising or renaming, action may be required
-- :red_circle: = Breaking API changes or removal, action required if using these methods
+* :green_circle: = API functionality unchanged between `acme.forge` and `acme.crypto`
+* :orange_circle: = Slight API changes, like depromising or renaming, action may be required
+* :red_circle: = Breaking API changes or removal, action required if using these methods
| Deprecated `.forge` API | New `.crypto` API | State |
| ----------------------------- | ----------------------------- | --------------------- |
@@ -33,7 +32,6 @@ Below you will find a table summarizing the current `acme.forge` methods, and th
| `await readCertificateInfo()` | `readCertificateInfo()` | :orange_circle: (4) |
| `await createCsr()` | `await createCsr()` | :green_circle: |
-
### 1. `createPublicKey` renamed and depromised
* The method `createPublicKey()` has been renamed to `getPublicKey()`
@@ -49,7 +47,6 @@ const publicKey = await acme.forge.createPublicKey(privateKey);
const publicKey = acme.crypto.getPublicKey(privateKey);
```
-
### 2. `getPemBody` renamed, now returns Base64URL
* Method `getPemBody()` has been renamed to `getPemBodyAsB64u()`
@@ -64,7 +61,6 @@ const body = acme.forge.getPemBody(pem);
const body = acme.crypto.getPemBodyAsB64u(pem);
```
-
### 3. `getModulus` and `getPublicExponent` merged into `getJwk`
* Methods `getModulus()` and `getPublicExponent()` have been removed
@@ -80,7 +76,6 @@ const exp = await acme.forge.getPublicExponent(key);
const { e, n } = acme.crypto.getJwk(key);
```
-
### 4. `readCsrDomains` and `readCertificateInfo` depromised
* Methods `readCsrDomains()` and `readCertificateInfo()` no longer return promises, but their resulting payloads directly
diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json
index 9f905f5d..ecf39dc9 100644
--- a/packages/core/acme-client/package.json
+++ b/packages/core/acme-client/package.json
@@ -2,9 +2,9 @@
"name": "acme-client",
"description": "Simple and unopinionated ACME client",
"author": "nmorsman",
- "version": "5.0.0",
+ "version": "5.1.0",
"main": "src/index.js",
- "types": "types",
+ "types": "types/index.d.ts",
"license": "MIT",
"homepage": "https://github.com/publishlab/node-acme-client",
"engines": {
@@ -15,29 +15,27 @@
"types"
],
"dependencies": {
- "axios": "0.27.2",
+ "axios": "^1.6.5",
"debug": "^4.1.1",
- "jsrsasign": "^10.5.26",
+ "jsrsasign": "^11.0.0",
"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",
- "typescript": "^4.6.2",
- "uuid": "^8.3.2"
+ "eslint-plugin-import": "^2.29.1",
+ "jsdoc-to-markdown": "^8.0.0",
+ "mocha": "^10.2.0",
+ "nock": "^13.5.0",
+ "tsd": "^0.30.4"
},
"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"
diff --git a/packages/core/acme-client/scripts/run-tests.sh b/packages/core/acme-client/scripts/run-tests.sh
index b0613ac2..c2f01f0d 100644
--- a/packages/core/acme-client/scripts/run-tests.sh
+++ b/packages/core/acme-client/scripts/run-tests.sh
@@ -6,8 +6,8 @@ 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_URL="https://github.com/CircleCI-Public/circleci-cli/releases/download/v0.1.29936/circleci-cli_0.1.29936_linux_amd64.tar.gz"
+CIRCLECI_CLI_SHASUM="fdc8da76111facae4a10f3717502eeb5d78db0256ef94a2f8d53078978175d40"
CIRCLECI_CLI_PATH="/tmp/circleci-cli"
CIRCLECI_CLI_BIN="${CIRCLECI_CLI_PATH}/circleci"
@@ -19,8 +19,10 @@ if [[ ${#JOBS[@]} -eq 0 ]]; then
JOBS=(
"v16"
"v18"
+ "v20"
"eab-v16"
"eab-v18"
+ "eab-v20"
)
fi
@@ -33,8 +35,9 @@ if [[ ! -f "${CIRCLECI_CLI_BIN}" ]]; then
tar zxvf "${CIRCLECI_CLI_PATH}/circleci-cli.tar.gz" -C "${CIRCLECI_CLI_PATH}" --strip-components=1
fi
-# Skip CircleCI update checks
+# Disable CircleCI update checks and telemetry
export CIRCLECI_CLI_SKIP_UPDATE_CHECK="true"
+export CIRCLECI_CLI_TELEMETRY_OPTOUT="1"
# Run test suite
echo "[-] Running test suite"
@@ -43,7 +46,7 @@ $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
+ $CIRCLECI_CLI_BIN local execute -c "${CONFIG_PATH}" "${job}"
echo "[+] ${job} completed successfully"
done
diff --git a/packages/core/acme-client/src/axios.js b/packages/core/acme-client/src/axios.js
index b9974852..f0cbdef6 100644
--- a/packages/core/acme-client/src/axios.js
+++ b/packages/core/acme-client/src/axios.js
@@ -3,7 +3,6 @@
*/
const axios = require('axios');
-const adapter = require('axios/lib/adapters/http');
const pkg = require('./../package.json');
@@ -30,7 +29,7 @@ instance.defaults.acmeSettings = {
* https://stackoverflow.com/questions/42677387
*/
-instance.defaults.adapter = adapter;
+instance.defaults.adapter = 'http';
/**
diff --git a/packages/core/acme-client/test/00-pebble.spec.js b/packages/core/acme-client/test/00-pebble.spec.js
index 789c0a4c..805421b9 100644
--- a/packages/core/acme-client/test/00-pebble.spec.js
+++ b/packages/core/acme-client/test/00-pebble.spec.js
@@ -3,8 +3,8 @@
*/
const dns = require('dns').promises;
+const { randomUUID: uuid } = require('crypto');
const { assert } = require('chai');
-const { v4: uuid } = require('uuid');
const cts = require('./challtestsrv');
const axios = require('./../src/axios');
diff --git a/packages/core/acme-client/test/10-http.spec.js b/packages/core/acme-client/test/10-http.spec.js
index e8163af5..22cd7b24 100644
--- a/packages/core/acme-client/test/10-http.spec.js
+++ b/packages/core/acme-client/test/10-http.spec.js
@@ -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');
diff --git a/packages/core/acme-client/test/10-verify.spec.js b/packages/core/acme-client/test/10-verify.spec.js
index adbd370e..5a0b39e3 100644
--- a/packages/core/acme-client/test/10-verify.spec.js
+++ b/packages/core/acme-client/test/10-verify.spec.js
@@ -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');
diff --git a/packages/core/acme-client/test/50-client.spec.js b/packages/core/acme-client/test/50-client.spec.js
index cb162644..4d61ce84 100644
--- a/packages/core/acme-client/test/50-client.spec.js
+++ b/packages/core/acme-client/test/50-client.spec.js
@@ -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');
diff --git a/packages/core/acme-client/test/70-auto.spec.js b/packages/core/acme-client/test/70-auto.spec.js
index 5b0b6f18..8e710004 100644
--- a/packages/core/acme-client/test/70-auto.spec.js
+++ b/packages/core/acme-client/test/70-auto.spec.js
@@ -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');
diff --git a/packages/core/acme-client/types/tsconfig.json b/packages/core/acme-client/tsconfig.json
similarity index 100%
rename from packages/core/acme-client/types/tsconfig.json
rename to packages/core/acme-client/tsconfig.json
diff --git a/packages/core/acme-client/types/test.ts b/packages/core/acme-client/types/index.test-d.ts
similarity index 100%
rename from packages/core/acme-client/types/test.ts
rename to packages/core/acme-client/types/index.test-d.ts
diff --git a/packages/core/acme-client/types/tslint.json b/packages/core/acme-client/types/tslint.json
deleted file mode 100644
index e650aab5..00000000
--- a/packages/core/acme-client/types/tslint.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "extends": "dtslint/dtslint.json",
- "rules": {
- "no-consecutive-blank-lines": [true, 2]
- }
-}
From 18865f0931fcd75bbffffc89a23b745c042b2573 Mon Sep 17 00:00:00 2001
From: GitHub Actions Bot
Date: Sun, 21 Jan 2024 19:24:13 +0000
Subject: [PATCH 2/8] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20w?=
=?UTF-8?q?ith=203=20commits=20[trident-sync]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Add https-01 challenge test server support
Inject CoreDNS into resolv.conf while testing, remove interceptor hack
---
.../core/acme-client/.circleci/config.yml | 5 +-
packages/core/acme-client/src/axios.js | 2 +-
.../core/acme-client/test/00-pebble.spec.js | 79 +++++++++++++++++--
.../core/acme-client/test/10-http.spec.js | 3 -
.../core/acme-client/test/challtestsrv.js | 9 +++
packages/core/acme-client/test/setup.js | 56 ++-----------
6 files changed, 90 insertions(+), 64 deletions(-)
diff --git a/packages/core/acme-client/.circleci/config.yml b/packages/core/acme-client/.circleci/config.yml
index 7c32a0d3..8865b201 100644
--- a/packages/core/acme-client/.circleci/config.yml
+++ b/packages/core/acme-client/.circleci/config.yml
@@ -99,6 +99,10 @@ commands:
command: sudo coredns -p 53 -conf /etc/coredns/Corefile
background: true
+ - run:
+ name: Use CoreDNS in resolv.conf
+ command: echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
+
test:
steps:
- run: yarn --color
@@ -111,7 +115,6 @@ commands:
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
diff --git a/packages/core/acme-client/src/axios.js b/packages/core/acme-client/src/axios.js
index f0cbdef6..c66d3258 100644
--- a/packages/core/acme-client/src/axios.js
+++ b/packages/core/acme-client/src/axios.js
@@ -18,7 +18,7 @@ instance.defaults.headers.common['User-Agent'] = `node-${pkg.name}/${pkg.version
/* Default ACME settings */
instance.defaults.acmeSettings = {
httpChallengePort: 80,
- bypassCustomDnsResolver: false
+ httpsChallengePort: 443
};
diff --git a/packages/core/acme-client/test/00-pebble.spec.js b/packages/core/acme-client/test/00-pebble.spec.js
index 805421b9..ce99d7a8 100644
--- a/packages/core/acme-client/test/00-pebble.spec.js
+++ b/packages/core/acme-client/test/00-pebble.spec.js
@@ -4,15 +4,19 @@
const dns = require('dns').promises;
const { randomUUID: uuid } = require('crypto');
+const https = require('https');
const { assert } = require('chai');
const cts = require('./challtestsrv');
const axios = require('./../src/axios');
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
+const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 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,6 +25,11 @@ 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();
@@ -79,39 +88,93 @@ 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, httpsPort);
+ 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);
diff --git a/packages/core/acme-client/test/10-http.spec.js b/packages/core/acme-client/test/10-http.spec.js
index 22cd7b24..a2a54de4 100644
--- a/packages/core/acme-client/test/10-http.spec.js
+++ b/packages/core/acme-client/test/10-http.spec.js
@@ -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;
});
diff --git a/packages/core/acme-client/test/challtestsrv.js b/packages/core/acme-client/test/challtestsrv.js
index 1a372959..a823c304 100644
--- a/packages/core/acme-client/test/challtestsrv.js
+++ b/packages/core/acme-client/test/challtestsrv.js
@@ -50,11 +50,20 @@ async function addHttp01ChallengeResponse(token, content) {
return request('add-http01', { token, content });
}
+async function addHttps01ChallengeResponse(token, content, targetHostname, targetPort = 443) {
+ await addHttp01ChallengeResponse(token, content);
+ return request('add-redirect', {
+ path: `/.well-known/acme-challenge/${token}`,
+ targetURL: `https://${targetHostname}:${targetPort}/.well-known/acme-challenge/${token}`
+ });
+}
+
async function addDns01ChallengeResponse(host, value) {
return request('set-txt', { host, value });
}
exports.addHttp01ChallengeResponse = addHttp01ChallengeResponse;
+exports.addHttps01ChallengeResponse = addHttps01ChallengeResponse;
exports.addDns01ChallengeResponse = addDns01ChallengeResponse;
diff --git a/packages/core/acme-client/test/setup.js b/packages/core/acme-client/test/setup.js
index e6f18647..a09f2801 100644
--- a/packages/core/acme-client/test/setup.js
+++ b/packages/core/acme-client/test/setup.js
@@ -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,17 @@ 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;
+}
+
/**
* External account binding
@@ -38,50 +39,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;
- });
-}
From 08c1f338d5e085d304e5bdb81294ef9d997ca180 Mon Sep 17 00:00:00 2001
From: GitHub Actions Bot
Date: Mon, 22 Jan 2024 19:24:37 +0000
Subject: [PATCH 3/8] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20w?=
=?UTF-8?q?ith=2010=20commits=20[trident-sync]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bump v5.2.0 - package.json
Bump v5.2.0
yarn -> npm
CHANGELOG and tests for #76
Fix tests
Update auto.js: wait for all challenge promises before exit
Fixes #75
CHANGELOG and tests for #66
Fix lint errors
Allow self-signed or invalid certificate when evaluating verifyHttpChallenge
---
.../core/acme-client/.circleci/config.yml | 17 ++++---
packages/core/acme-client/.gitignore | 2 -
packages/core/acme-client/.yarnrc | 2 -
packages/core/acme-client/CHANGELOG.md | 5 ++
packages/core/acme-client/package.json | 2 +-
packages/core/acme-client/src/auto.js | 15 +++++-
packages/core/acme-client/src/verify.js | 6 ++-
.../core/acme-client/test/00-pebble.spec.js | 2 +-
.../core/acme-client/test/10-verify.spec.js | 25 ++++++++++
.../core/acme-client/test/70-auto.spec.js | 49 +++++++++++++++++++
.../core/acme-client/test/challtestsrv.js | 11 ++++-
11 files changed, 119 insertions(+), 17 deletions(-)
delete mode 100644 packages/core/acme-client/.yarnrc
diff --git a/packages/core/acme-client/.circleci/config.yml b/packages/core/acme-client/.circleci/config.yml
index 8865b201..589f75c5 100644
--- a/packages/core/acme-client/.circleci/config.yml
+++ b/packages/core/acme-client/.circleci/config.yml
@@ -4,9 +4,14 @@ version: 2.1
commands:
pre:
steps:
+ - run:
+ name: Setup environment
+ command: |
+ echo 'export FORCE_COLOR=1' >> $BASH_ENV
+ echo 'export NPM_CONFIG_COLOR="always"' >> $BASH_ENV
+
- run: node --version
- run: npm --version
- - run: yarn --version
- checkout
enable-eab:
@@ -105,13 +110,13 @@ commands:
test:
steps:
- - run: yarn --color
- - run: yarn run lint --color
- - run: yarn run lint-types
- - run: yarn run build-docs
+ - run: npm i
+ - run: npm run lint
+ - run: npm run lint-types
+ - run: npm run build-docs
- run:
- command: yarn run test --color
+ command: npm run test
environment:
ACME_DOMAIN_NAME: test.example.com
ACME_CHALLTESTSRV_URL: http://127.0.0.1:8055
diff --git a/packages/core/acme-client/.gitignore b/packages/core/acme-client/.gitignore
index 61a8d616..a261ab9a 100644
--- a/packages/core/acme-client/.gitignore
+++ b/packages/core/acme-client/.gitignore
@@ -1,6 +1,4 @@
.vscode/
node_modules/
npm-debug.log
-yarn-error.log
-yarn.lock
package-lock.json
diff --git a/packages/core/acme-client/.yarnrc b/packages/core/acme-client/.yarnrc
deleted file mode 100644
index 8d6f153a..00000000
--- a/packages/core/acme-client/.yarnrc
+++ /dev/null
@@ -1,2 +0,0 @@
-ignore-engines true
-ignore-optional true
diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md
index fad2966c..fcfa31f7 100644
--- a/packages/core/acme-client/CHANGELOG.md
+++ b/packages/core/acme-client/CHANGELOG.md
@@ -1,5 +1,10 @@
# Changelog
+## 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)
diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json
index ecf39dc9..86bd44bc 100644
--- a/packages/core/acme-client/package.json
+++ b/packages/core/acme-client/package.json
@@ -2,7 +2,7 @@
"name": "acme-client",
"description": "Simple and unopinionated ACME client",
"author": "nmorsman",
- "version": "5.1.0",
+ "version": "5.2.0",
"main": "src/index.js",
"types": "types/index.d.ts",
"license": "MIT",
diff --git a/packages/core/acme-client/src/auto.js b/packages/core/acme-client/src/auto.js
index 2b009d7f..9e7dc05d 100644
--- a/packages/core/acme-client/src/auto.js
+++ b/packages/core/acme-client/src/auto.js
@@ -165,8 +165,19 @@ module.exports = async function(client, userOpts) {
}
});
- log('[auto] Waiting for challenge valid status');
- await Promise.all(challengePromises);
+
+ /**
+ * Wait for all challenge promises to settle
+ */
+
+ try {
+ log('[auto] Waiting for challenge valid status');
+ await Promise.all(challengePromises);
+ }
+ catch (e) {
+ await Promise.allSettled(challengePromises);
+ throw e;
+ }
/**
diff --git a/packages/core/acme-client/src/verify.js b/packages/core/acme-client/src/verify.js
index fe76cab8..5c9da2df 100644
--- a/packages/core/acme-client/src/verify.js
+++ b/packages/core/acme-client/src/verify.js
@@ -3,6 +3,7 @@
*/
const dns = require('dns').promises;
+const https = require('https');
const { log } = require('./logger');
const axios = require('./axios');
const util = require('./util');
@@ -24,8 +25,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}`);
diff --git a/packages/core/acme-client/test/00-pebble.spec.js b/packages/core/acme-client/test/00-pebble.spec.js
index ce99d7a8..70376c9a 100644
--- a/packages/core/acme-client/test/00-pebble.spec.js
+++ b/packages/core/acme-client/test/00-pebble.spec.js
@@ -129,7 +129,7 @@ describe('pebble', () => {
});
it('should add challenge response', async () => {
- const resp = await cts.addHttps01ChallengeResponse(testHttps01ChallengeToken, testHttps01ChallengeContent, testHttps01ChallengeHost, httpsPort);
+ const resp = await cts.addHttps01ChallengeResponse(testHttps01ChallengeToken, testHttps01ChallengeContent, testHttps01ChallengeHost);
assert.isTrue(resp);
});
diff --git a/packages/core/acme-client/test/10-verify.spec.js b/packages/core/acme-client/test/10-verify.spec.js
index 5a0b39e3..55e98c54 100644
--- a/packages/core/acme-client/test/10-verify.spec.js
+++ b/packages/core/acme-client/test/10-verify.spec.js
@@ -17,6 +17,10 @@ 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();
@@ -74,6 +78,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
*/
diff --git a/packages/core/acme-client/test/70-auto.spec.js b/packages/core/acme-client/test/70-auto.spec.js
index 8e710004..eb80c483 100644
--- a/packages/core/acme-client/test/70-auto.spec.js
+++ b/packages/core/acme-client/test/70-auto.spec.js
@@ -32,6 +32,7 @@ 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 testWildcardDomain = `${uuid()}.${domainName}`;
@@ -178,6 +179,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 +248,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
diff --git a/packages/core/acme-client/test/challtestsrv.js b/packages/core/acme-client/test/challtestsrv.js
index a823c304..c3013365 100644
--- a/packages/core/acme-client/test/challtestsrv.js
+++ b/packages/core/acme-client/test/challtestsrv.js
@@ -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,11 +51,11 @@ async function addHttp01ChallengeResponse(token, content) {
return request('add-http01', { token, content });
}
-async function addHttps01ChallengeResponse(token, content, targetHostname, targetPort = 443) {
+async function addHttps01ChallengeResponse(token, content, targetHostname) {
await addHttp01ChallengeResponse(token, content);
return request('add-redirect', {
path: `/.well-known/acme-challenge/${token}`,
- targetURL: `https://${targetHostname}:${targetPort}/.well-known/acme-challenge/${token}`
+ targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}`
});
}
@@ -76,6 +77,11 @@ 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);
@@ -98,5 +104,6 @@ exports.challengeNoopFn = async () => true;
exports.challengeThrowFn = async () => { throw new Error('oops'); };
exports.assertHttpChallengeCreateFn = assertHttpChallengeCreateFn;
+exports.assertHttpsChallengeCreateFn = assertHttpsChallengeCreateFn;
exports.assertDnsChallengeCreateFn = assertDnsChallengeCreateFn;
exports.challengeCreateFn = challengeCreateFn;
From fc9e71bed29bd0159e667853fb0068e65cfc55b8 Mon Sep 17 00:00:00 2001
From: GitHub Actions Bot
Date: Tue, 30 Jan 2024 19:24:20 +0000
Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20w?=
=?UTF-8?q?ith=207=20commits=20[trident-sync]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
CHANGELOG
Fix tls-alpn-01 pebble test on Node v18+
Return correct tls-alpn-01 key authorization, tests
Support tls-alpn-01 internal challenge verification
Add tls-alpn-01 challenge test server support
Add ALPN crypto utility methods
---
packages/core/acme-client/CHANGELOG.md | 7 +
packages/core/acme-client/src/axios.js | 3 +-
packages/core/acme-client/src/client.js | 19 ++-
packages/core/acme-client/src/crypto/index.js | 132 ++++++++++++++++--
packages/core/acme-client/src/util.js | 58 +++++++-
packages/core/acme-client/src/verify.js | 32 ++++-
.../core/acme-client/test/00-pebble.spec.js | 31 ++++
.../core/acme-client/test/10-verify.spec.js | 25 ++++
.../core/acme-client/test/20-crypto.spec.js | 41 +++++-
.../core/acme-client/test/50-client.spec.js | 45 ++++--
.../core/acme-client/test/70-auto.spec.js | 17 +++
.../core/acme-client/test/challtestsrv.js | 15 ++
packages/core/acme-client/test/setup.js | 4 +
packages/core/acme-client/types/index.d.ts | 2 +
14 files changed, 389 insertions(+), 42 deletions(-)
diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md
index fcfa31f7..894dac23 100644
--- a/packages/core/acme-client/CHANGELOG.md
+++ b/packages/core/acme-client/CHANGELOG.md
@@ -1,5 +1,12 @@
# Changelog
+## v5.3.0
+
+* `added` Support and tests for satisfying `tls-alpn-01` challenges
+* `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
+
## 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)
diff --git a/packages/core/acme-client/src/axios.js b/packages/core/acme-client/src/axios.js
index c66d3258..83c632ce 100644
--- a/packages/core/acme-client/src/axios.js
+++ b/packages/core/acme-client/src/axios.js
@@ -18,7 +18,8 @@ instance.defaults.headers.common['User-Agent'] = `node-${pkg.name}/${pkg.version
/* Default ACME settings */
instance.defaults.acmeSettings = {
httpChallengePort: 80,
- httpsChallengePort: 443
+ httpsChallengePort: 443,
+ tlsAlpnChallengePort: 443
};
diff --git a/packages/core/acme-client/src/client.js b/packages/core/acme-client/src/client.js
index 6547522e..2d3fa89c 100644
--- a/packages/core/acme-client/src/client.js
+++ b/packages/core/acme-client/src/client.js
@@ -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://tools.ietf.org/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://tools.ietf.org/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://tools.ietf.org/html/rfc8737 */
+ if (challenge.type === 'tls-alpn-01') {
+ return result;
}
throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
diff --git a/packages/core/acme-client/src/crypto/index.js b/packages/core/acme-client/src/crypto/index.js
index 582b2d11..d7a13b7d 100644
--- a/packages/core/acme-client/src/crypto/index.js
+++ b/packages/core/acme-client/src/crypto/index.js
@@ -9,8 +9,12 @@ const { promisify } = require('util');
const crypto = require('crypto');
const jsrsasign = require('jsrsasign');
+const randomInt = promisify(crypto.randomInt);
const generateKeyPair = promisify(crypto.generateKeyPair);
+/* 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
@@ -231,7 +235,7 @@ exports.getPemBodyAsB64u = (pem) => {
throw new Error('Unable to parse PEM body from string');
}
- /* First object, hex and back to b64 without new lines */
+ /* Select first object, decode to hex and b64u */
return jsrsasign.hextob64u(jsrsasign.pemtohex(chain[0]));
};
@@ -303,6 +307,28 @@ exports.readCsrDomains = (csrPem) => {
};
+/**
+ * Parse params from a single or chain of PEM encoded certificates
+ *
+ * @private
+ * @param {buffer|string} certPem PEM encoded certificate or chain
+ * @returns {object} Certificate params
+ */
+
+function getCertificateParams(certPem) {
+ const chain = splitPemChain(certPem);
+
+ if (!chain.length) {
+ throw new Error('Unable to parse PEM body from string');
+ }
+
+ /* Parse certificate */
+ const obj = new jsrsasign.X509();
+ obj.readCertPEM(chain[0]);
+ return obj.getParam();
+}
+
+
/**
* Read information from a certificate
* If multiple certificates are chained, the first will be read
@@ -324,16 +350,7 @@ exports.readCsrDomains = (csrPem) => {
*/
exports.readCertificateInfo = (certPem) => {
- const chain = splitPemChain(certPem);
-
- if (!chain.length) {
- throw new Error('Unable to parse PEM body from string');
- }
-
- /* Parse certificate */
- const obj = new jsrsasign.X509();
- obj.readCertPEM(chain[0]);
- const params = obj.getParam();
+ const params = getCertificateParams(certPem);
return {
issuer: {
@@ -462,7 +479,7 @@ function formatCsrAltNames(altNames) {
* }, certificateKey);
*/
-exports.createCsr = async (data, keyPem = null) => {
+async function createCsr(data, keyPem = null) {
if (!keyPem) {
keyPem = await createPrivateRsaKey(data.keySize);
}
@@ -517,10 +534,95 @@ exports.createCsr = async (data, keyPem = null) => {
extreq: extensionRequests
});
- /* Sign CSR, get PEM */
- csr.sign();
+ /* Done */
const pem = csr.getPEM();
+ return [keyPem, Buffer.from(pem)];
+}
+
+exports.createCsr = createCsr;
+
+
+/**
+ * Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
+ *
+ * https://tools.ietf.org/html/rfc8737
+ *
+ * @param {object} authz Identifier authorization
+ * @param {string} keyAuthorization Challenge key authorization
+ * @param {string} [keyPem] PEM encoded CSR private key
+ * @returns {Promise} [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) => {
+ /* Create CSR first */
+ const now = new Date();
+ const commonName = authz.identifier.value;
+ const [key, csr] = await createCsr({ commonName }, keyPem);
+
+ /* Parse params and grab stuff we need */
+ const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csr.toString());
+ const { subject, sbjpubkey, extreq, sigalg } = params;
+
+ /* ALPN extension */
+ const alpnExt = {
+ critical: true,
+ extname: alpnAcmeIdentifierOID,
+ extn: new jsrsasign.KJUR.asn1.DEROctetString({
+ hex: crypto.createHash('sha256').update(keyAuthorization).digest('hex')
+ })
+ };
+
+ /* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */
+ const random = await randomInt(1, 999999999);
+ const serial = `${Math.floor(now.getTime() / 1000)}${random}`;
+
+ /* Self-signed ALPN certificate */
+ const certificate = new jsrsasign.KJUR.asn1.x509.Certificate({
+ subject,
+ sbjpubkey,
+ sigalg,
+ version: 3,
+ serial: { hex: Buffer.from(serial).toString('hex') },
+ issuer: subject,
+ notbefore: jsrsasign.datetozulu(now),
+ notafter: jsrsasign.datetozulu(now),
+ cakey: key.toString(),
+ ext: extreq.concat([alpnExt])
+ });
/* Done */
- return [keyPem, Buffer.from(pem)];
+ const pem = certificate.getPEM();
+ return [key, 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 params = getCertificateParams(certPem);
+ const expectedHex = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
+ const acmeExt = (params.ext || []).find((e) => (e && e.extname && (e.extname === alpnAcmeIdentifierOID)));
+
+ if (!acmeExt || !acmeExt.extn || !acmeExt.extn.octstr || !acmeExt.extn.octstr.hex) {
+ throw new Error('Unable to locate ALPN extension within parsed certificate');
+ }
+
+ /* Return true if match */
+ return (acmeExt.extn.octstr.hex === expectedHex);
};
diff --git a/packages/core/acme-client/src/util.js b/packages/core/acme-client/src/util.js
index 95d08e81..73ecdd3b 100644
--- a/packages/core/acme-client/src/util.js
+++ b/packages/core/acme-client/src/util.js
@@ -2,6 +2,7 @@
* Utility methods
*/
+const tls = require('tls');
const dns = require('dns').promises;
const { readCertificateInfo, splitPemChain } = require('./crypto');
const { log } = require('./logger');
@@ -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} 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
};
diff --git a/packages/core/acme-client/src/verify.js b/packages/core/acme-client/src/verify.js
index 5c9da2df..2ab95e8f 100644
--- a/packages/core/acme-client/src/verify.js
+++ b/packages/core/acme-client/src/verify.js
@@ -7,6 +7,7 @@ const https = require('https');
const { log } = require('./logger');
const axios = require('./axios');
const util = require('./util');
+const { isAlpnCertificateAuthorizationValid } = require('./crypto');
/**
@@ -121,11 +122,40 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
}
+/**
+ * Verify ACME TLS ALPN challenge
+ *
+ * https://tools.ietf.org/html/rfc8737
+ *
+ * @param {object} authz Identifier authorization
+ * @param {object} challenge Authorization challenge
+ * @param {string} keyAuthorization Challenge key authorization
+ * @returns {Promise}
+ */
+
+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
};
diff --git a/packages/core/acme-client/test/00-pebble.spec.js b/packages/core/acme-client/test/00-pebble.spec.js
index 70376c9a..7a26c3b7 100644
--- a/packages/core/acme-client/test/00-pebble.spec.js
+++ b/packages/core/acme-client/test/00-pebble.spec.js
@@ -8,10 +8,13 @@ const https = require('https');
const { assert } = require('chai');
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', () => {
@@ -33,6 +36,9 @@ describe('pebble', () => {
const testDns01ChallengeHost = `_acme-challenge.${uuid()}.${domainName}.`;
const testDns01ChallengeValue = uuid();
+ const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`;
+ const testTlsAlpn01ChallengeValue = uuid();
+
/**
* Pebble CTS required
@@ -181,4 +187,29 @@ describe('pebble', () => {
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), /timed out/);
+ });
+
+ 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));
+ });
+ });
});
diff --git a/packages/core/acme-client/test/10-verify.spec.js b/packages/core/acme-client/test/10-verify.spec.js
index 55e98c54..284cc3a0 100644
--- a/packages/core/acme-client/test/10-verify.spec.js
+++ b/packages/core/acme-client/test/10-verify.spec.js
@@ -26,6 +26,10 @@ describe('verify', () => {
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
@@ -128,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);
+ });
+ });
});
diff --git a/packages/core/acme-client/test/20-crypto.spec.js b/packages/core/acme-client/test/20-crypto.spec.js
index 8441af55..cbcd9f39 100644
--- a/packages/core/acme-client/test/20-crypto.spec.js
+++ b/packages/core/acme-client/test/20-crypto.spec.js
@@ -95,6 +95,7 @@ describe('crypto', () => {
let testSanCsr;
let testNonCnCsr;
let testNonAsciiCsr;
+ let testAlpnCertificate;
/**
@@ -215,6 +216,31 @@ describe('crypto', () => {
assert.strictEqual(result.commonName, testCsrDomain);
assert.deepStrictEqual(result.altNames, [testCsrDomain]);
});
+
+
+ /**
+ * 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 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'));
+ });
});
});
});
@@ -250,7 +276,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
});
@@ -281,6 +307,19 @@ describe('crypto', () => {
});
+ /**
+ * 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
*/
diff --git a/packages/core/acme-client/test/50-client.spec.js b/packages/core/acme-client/test/50-client.spec.js
index 4d61ce84..31784ca3 100644
--- a/packages/core/acme-client/test/50-client.spec.js
+++ b/packages/core/acme-client/test/50-client.spec.js
@@ -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));
});
diff --git a/packages/core/acme-client/test/70-auto.spec.js b/packages/core/acme-client/test/70-auto.spec.js
index eb80c483..9d5742c9 100644
--- a/packages/core/acme-client/test/70-auto.spec.js
+++ b/packages/core/acme-client/test/70-auto.spec.js
@@ -34,6 +34,7 @@ describe('client.auto', () => {
const testHttpDomain = `${uuid()}.${domainName}`;
const testHttpsDomain = `${uuid()}.${domainName}`;
const testDnsDomain = `${uuid()}.${domainName}`;
+ const testAlpnDomain = `${uuid()}.${domainName}`;
const testWildcardDomain = `${uuid()}.${domainName}`;
const testSanDomains = [
@@ -280,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],
diff --git a/packages/core/acme-client/test/challtestsrv.js b/packages/core/acme-client/test/challtestsrv.js
index c3013365..71ed91b6 100644
--- a/packages/core/acme-client/test/challtestsrv.js
+++ b/packages/core/acme-client/test/challtestsrv.js
@@ -63,9 +63,14 @@ 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;
/**
@@ -87,6 +92,11 @@ async function assertDnsChallengeCreateFn(authz, challenge, keyAuthorization) {
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);
@@ -96,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}`);
}
@@ -106,4 +120,5 @@ exports.challengeThrowFn = async () => { throw new Error('oops'); };
exports.assertHttpChallengeCreateFn = assertHttpChallengeCreateFn;
exports.assertHttpsChallengeCreateFn = assertHttpsChallengeCreateFn;
exports.assertDnsChallengeCreateFn = assertDnsChallengeCreateFn;
+exports.assertTlsAlpnChallengeCreateFn = assertTlsAlpnChallengeCreateFn;
exports.challengeCreateFn = challengeCreateFn;
diff --git a/packages/core/acme-client/test/setup.js b/packages/core/acme-client/test/setup.js
index a09f2801..4e0f5bee 100644
--- a/packages/core/acme-client/test/setup.js
+++ b/packages/core/acme-client/test/setup.js
@@ -27,6 +27,10 @@ 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
diff --git a/packages/core/acme-client/types/index.d.ts b/packages/core/acme-client/types/index.d.ts
index 1e3aa2ef..6fe2a328 100644
--- a/packages/core/acme-client/types/index.d.ts
+++ b/packages/core/acme-client/types/index.d.ts
@@ -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;
From 7e8842b4525ec07b4f6c4c0bad77d59b479ea36b Mon Sep 17 00:00:00 2001
From: GitHub Actions Bot
Date: Thu, 1 Feb 2024 19:24:13 +0000
Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20w?=
=?UTF-8?q?ith=204=20commits=20[trident-sync]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Example for on-demand tls-alpn-01
Example disclaimer, fallback cert
Replace CircleCI with GitHub Actions
---
.../core/acme-client/.circleci/.gitignore | 1 -
.../core/acme-client/.circleci/config.yml | 145 --------------
.../scripts/tests-install-coredns.sh} | 5 +-
.../scripts/tests-install-cts.sh} | 2 +-
.../scripts/tests-install-pebble.sh} | 10 +-
.../scripts/tests-wait-for-ca.sh} | 4 +-
.../acme-client/.github/workflows/tests.yml | 94 +++++++++
packages/core/acme-client/README.md | 2 +-
packages/core/acme-client/examples/README.md | 19 ++
packages/core/acme-client/examples/api.js | 1 -
packages/core/acme-client/examples/auto.js | 1 -
.../core/acme-client/examples/fallback.crt | 19 ++
.../core/acme-client/examples/fallback.key | 28 +++
.../examples/tls-alpn-01/README.md | 44 +++++
.../examples/tls-alpn-01/haproxy.cfg | 23 +++
.../examples/tls-alpn-01/nginx.conf | 19 ++
.../examples/tls-alpn-01/tls-alpn-01.js | 180 ++++++++++++++++++
packages/core/acme-client/package.json | 3 +-
.../core/acme-client/scripts/run-tests.sh | 59 ------
.../scripts/test-suite-install-step.sh | 20 --
.../core/acme-client/test/00-pebble.spec.js | 2 +-
21 files changed, 441 insertions(+), 240 deletions(-)
delete mode 100644 packages/core/acme-client/.circleci/.gitignore
delete mode 100644 packages/core/acme-client/.circleci/config.yml
rename packages/core/acme-client/{scripts/test-suite-install-coredns.sh => .github/scripts/tests-install-coredns.sh} (93%)
rename packages/core/acme-client/{scripts/test-suite-install-cts.sh => .github/scripts/tests-install-cts.sh} (95%)
rename packages/core/acme-client/{scripts/test-suite-install-pebble.sh => .github/scripts/tests-install-pebble.sh} (78%)
rename packages/core/acme-client/{scripts/test-suite-wait-for-ca.sh => .github/scripts/tests-wait-for-ca.sh} (79%)
create mode 100644 packages/core/acme-client/.github/workflows/tests.yml
create mode 100644 packages/core/acme-client/examples/README.md
create mode 100644 packages/core/acme-client/examples/fallback.crt
create mode 100644 packages/core/acme-client/examples/fallback.key
create mode 100644 packages/core/acme-client/examples/tls-alpn-01/README.md
create mode 100644 packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg
create mode 100644 packages/core/acme-client/examples/tls-alpn-01/nginx.conf
create mode 100644 packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js
delete mode 100644 packages/core/acme-client/scripts/run-tests.sh
delete mode 100644 packages/core/acme-client/scripts/test-suite-install-step.sh
diff --git a/packages/core/acme-client/.circleci/.gitignore b/packages/core/acme-client/.circleci/.gitignore
deleted file mode 100644
index 0ae14133..00000000
--- a/packages/core/acme-client/.circleci/.gitignore
+++ /dev/null
@@ -1 +0,0 @@
-.temp.yml
diff --git a/packages/core/acme-client/.circleci/config.yml b/packages/core/acme-client/.circleci/config.yml
deleted file mode 100644
index 589f75c5..00000000
--- a/packages/core/acme-client/.circleci/config.yml
+++ /dev/null
@@ -1,145 +0,0 @@
----
-version: 2.1
-
-commands:
- pre:
- steps:
- - run:
- name: Setup environment
- command: |
- echo 'export FORCE_COLOR=1' >> $BASH_ENV
- echo 'export NPM_CONFIG_COLOR="always"' >> $BASH_ENV
-
- - run: node --version
- - run: npm --version
- - checkout
-
- enable-eab:
- steps:
- - run:
- name: Enable EAB through environment
- command: |
- echo 'export ACME_CAP_EAB_ENABLED=1' >> $BASH_ENV
-
- install-cts:
- steps:
- - run:
- name: Install Pebble Challenge Test Server
- command: sudo -E /bin/bash ./scripts/test-suite-install-cts.sh
- environment:
- PEBBLECTS_VERSION: 2.3.1
-
- - run:
- name: Start Pebble Challenge Test Server
- command: pebble-challtestsrv -dns01 ":8053" -tlsalpn01 ":5001" -http01 ":5002" -https01 ":5003" -defaultIPv4 "127.0.0.1" -defaultIPv6 ""
- background: true
-
- install-pebble:
- steps:
- - run:
- name: Install Pebble
- command: sudo -E /bin/bash ./scripts/test-suite-install-pebble.sh
- environment:
- PEBBLE_VERSION: 2.3.1
-
- - run:
- name: Start Pebble
- command: pebble -strict -config /etc/pebble/pebble.json -dnsserver "127.0.0.1:53"
- background: true
- environment:
- PEBBLE_ALTERNATE_ROOTS: 2
-
- - run:
- name: Set up environment
- command: |
- echo 'export NODE_EXTRA_CA_CERTS="/etc/pebble/ca.cert.pem"' >> $BASH_ENV
- echo 'export ACME_CA_CERT_PATH="/etc/pebble/ca.cert.pem"' >> $BASH_ENV
- echo 'export ACME_DIRECTORY_URL="https://127.0.0.1:14000/dir"' >> $BASH_ENV
- echo 'export ACME_PEBBLE_MANAGEMENT_URL="https://127.0.0.1:15000"' >> $BASH_ENV
-
- - run:
- name: Wait for Pebble
- command: /bin/bash ./scripts/test-suite-wait-for-ca.sh
-
- install-step:
- steps:
- - run:
- name: Install Step Certificates
- command: /bin/bash ./scripts/test-suite-install-step.sh
- environment:
- STEPCA_VERSION: 0.18.0
- STEPCLI_VERSION: 0.18.0
-
- - run:
- name: Start Step CA
- command: /usr/bin/step-ca --resolver="127.0.0.1:53" --password-file="/tmp/password" ~/.step/config/ca.json
- background: true
-
- - run:
- name: Set up environment
- command: |
- echo 'export NODE_EXTRA_CA_CERTS="/home/circleci/.step/certs/root_ca.crt"' >> $BASH_ENV
- echo 'export ACME_CA_CERT_PATH="/home/circleci/.step/certs/root_ca.crt"' >> $BASH_ENV
- echo 'export ACME_DIRECTORY_URL="https://localhost:8443/acme/acme/directory"' >> $BASH_ENV
-
- echo 'export ACME_CAP_META_TOS_FIELD=0' >> $BASH_ENV
- echo 'export ACME_CAP_UPDATE_ACCOUNT_KEY=0' >> $BASH_ENV
- echo 'export ACME_CAP_ALTERNATE_CERT_ROOTS=0' >> $BASH_ENV
-
- - run:
- name: Wait for Step CA
- command: /bin/bash ./scripts/test-suite-wait-for-ca.sh
-
- install-coredns:
- steps:
- - run:
- name: Install CoreDNS
- command: sudo -E /bin/bash ./scripts/test-suite-install-coredns.sh
- environment:
- COREDNS_VERSION: 1.11.1
- PEBBLECTS_DNS_PORT: 8053
-
- - run:
- name: Start CoreDNS
- command: sudo coredns -p 53 -conf /etc/coredns/Corefile
- background: true
-
- - run:
- name: Use CoreDNS in resolv.conf
- command: echo "nameserver 127.0.0.1" | sudo tee /etc/resolv.conf
-
- test:
- steps:
- - run: npm i
- - run: npm run lint
- - run: npm run lint-types
- - run: npm run build-docs
-
- - run:
- command: npm run test
- environment:
- ACME_DOMAIN_NAME: test.example.com
- ACME_CHALLTESTSRV_URL: http://127.0.0.1:8055
- ACME_TLSALPN_PORT: 5001
- ACME_HTTP_PORT: 5002
- ACME_HTTPS_PORT: 5003
-
-jobs:
- v16: { docker: [{ image: cimg/node:16.20 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]}
- v18: { docker: [{ image: cimg/node:18.19 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]}
- v20: { docker: [{ image: cimg/node:20.11 }], steps: [ pre, install-cts, install-pebble, install-coredns, test ]}
- eab-v16: { docker: [{ image: cimg/node:16.20 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]}
- eab-v18: { docker: [{ image: cimg/node:18.19 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]}
- eab-v20: { docker: [{ image: cimg/node:20.11 }], steps: [ pre, enable-eab, install-cts, install-pebble, install-coredns, test ]}
- # step-v12: { docker: [{ image: cimg/node:12.22 }], steps: [ pre, install-cts, install-step, install-coredns, test ]}
-
-workflows:
- test-suite:
- jobs:
- - v16
- - v18
- - v20
- - eab-v16
- - eab-v18
- - eab-v20
- # - step-v12
diff --git a/packages/core/acme-client/scripts/test-suite-install-coredns.sh b/packages/core/acme-client/.github/scripts/tests-install-coredns.sh
similarity index 93%
rename from packages/core/acme-client/scripts/test-suite-install-coredns.sh
rename to packages/core/acme-client/.github/scripts/tests-install-coredns.sh
index c4876beb..2dfc4ccb 100644
--- a/packages/core/acme-client/scripts/test-suite-install-coredns.sh
+++ b/packages/core/acme-client/.github/scripts/tests-install-coredns.sh
@@ -2,7 +2,7 @@
#
# Install CoreDNS for testing.
#
-set -eu
+set -euo pipefail
# Download and install
wget -nv "https://github.com/coredns/coredns/releases/download/v${COREDNS_VERSION}/coredns_${COREDNS_VERSION}_linux_amd64.tgz" -O /tmp/coredns.tgz
@@ -39,18 +39,21 @@ tee /etc/coredns/Corefile << EOF
example.com {
errors
log
+ bind 127.53.53.53
file /etc/coredns/db.example.com
}
test.example.com {
errors
log
+ bind 127.53.53.53
forward . 127.0.0.1:${PEBBLECTS_DNS_PORT}
}
. {
errors
log
+ bind 127.53.53.53
forward . 8.8.8.8
}
EOF
diff --git a/packages/core/acme-client/scripts/test-suite-install-cts.sh b/packages/core/acme-client/.github/scripts/tests-install-cts.sh
similarity index 95%
rename from packages/core/acme-client/scripts/test-suite-install-cts.sh
rename to packages/core/acme-client/.github/scripts/tests-install-cts.sh
index ab9a85c1..ac929c8b 100644
--- a/packages/core/acme-client/scripts/test-suite-install-cts.sh
+++ b/packages/core/acme-client/.github/scripts/tests-install-cts.sh
@@ -2,7 +2,7 @@
#
# Install Pebble Challenge Test Server for testing.
#
-set -eu
+set -euo pipefail
# Download and install
wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLECTS_VERSION}/pebble-challtestsrv_linux-amd64" -O /usr/local/bin/pebble-challtestsrv
diff --git a/packages/core/acme-client/scripts/test-suite-install-pebble.sh b/packages/core/acme-client/.github/scripts/tests-install-pebble.sh
similarity index 78%
rename from packages/core/acme-client/scripts/test-suite-install-pebble.sh
rename to packages/core/acme-client/.github/scripts/tests-install-pebble.sh
index 9378250c..4b830e6d 100644
--- a/packages/core/acme-client/scripts/test-suite-install-pebble.sh
+++ b/packages/core/acme-client/.github/scripts/tests-install-pebble.sh
@@ -2,14 +2,14 @@
#
# Install Pebble for testing.
#
-set -eu
+set -euo pipefail
-config_name="pebble-config.json"
+CONFIG_NAME="pebble-config.json"
# Use Pebble EAB config if enabled
set +u
-if [[ ! -z $ACME_CAP_EAB_ENABLED ]] && [[ $ACME_CAP_EAB_ENABLED -eq 1 ]]; then
- config_name="pebble-config-external-account-bindings.json"
+if [[ -n $ACME_CAP_EAB_ENABLED ]] && [[ $ACME_CAP_EAB_ENABLED -eq 1 ]]; then
+ CONFIG_NAME="pebble-config-external-account-bindings.json"
fi
set -u
@@ -19,7 +19,7 @@ mkdir -p /etc/pebble
wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/pebble.minica.pem" -O /etc/pebble/ca.cert.pem
wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/localhost/cert.pem" -O /etc/pebble/cert.pem
wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/certs/localhost/key.pem" -O /etc/pebble/key.pem
-wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/config/${config_name}" -O /etc/pebble/pebble.json
+wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION}/test/config/${CONFIG_NAME}" -O /etc/pebble/pebble.json
# Download and install Pebble
wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLE_VERSION}/pebble_linux-amd64" -O /usr/local/bin/pebble
diff --git a/packages/core/acme-client/scripts/test-suite-wait-for-ca.sh b/packages/core/acme-client/.github/scripts/tests-wait-for-ca.sh
similarity index 79%
rename from packages/core/acme-client/scripts/test-suite-wait-for-ca.sh
rename to packages/core/acme-client/.github/scripts/tests-wait-for-ca.sh
index a35cf6a1..b1f39e31 100644
--- a/packages/core/acme-client/scripts/test-suite-wait-for-ca.sh
+++ b/packages/core/acme-client/.github/scripts/tests-wait-for-ca.sh
@@ -2,13 +2,13 @@
#
# Wait for ACME server to accept connections.
#
-set -eu
+set -euo pipefail
MAX_ATTEMPTS=15
ATTEMPT=0
# Loop until ready
-while ! $(curl --cacert "${ACME_CA_CERT_PATH}" -s -D - "${ACME_DIRECTORY_URL}" | grep '^HTTP.*200' > /dev/null 2>&1); do
+while ! curl --cacert "${ACME_CA_CERT_PATH}" -s -D - "${ACME_DIRECTORY_URL}" | grep '^HTTP.*200' > /dev/null 2>&1; do
ATTEMPT=$((ATTEMPT + 1))
# Max attempts
diff --git a/packages/core/acme-client/.github/workflows/tests.yml b/packages/core/acme-client/.github/workflows/tests.yml
new file mode 100644
index 00000000..6f9e7c4b
--- /dev/null
+++ b/packages/core/acme-client/.github/workflows/tests.yml
@@ -0,0 +1,94 @@
+---
+name: test
+on: [push, pull_request]
+
+jobs:
+ test:
+ name: node=${{matrix.node}} eab=${{matrix.eab}}
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ node: [16, 18, 20]
+ eab: [0, 1]
+
+
+ #
+ # Environment
+ #
+
+ env:
+ FORCE_COLOR: 1
+ NPM_CONFIG_COLOR: always
+
+ PEBBLE_VERSION: 2.3.1
+ PEBBLE_ALTERNATE_ROOTS: 2
+ PEBBLECTS_VERSION: 2.3.1
+ PEBBLECTS_DNS_PORT: 8053
+ COREDNS_VERSION: 1.11.1
+
+ NODE_EXTRA_CA_CERTS: /etc/pebble/ca.cert.pem
+ ACME_CA_CERT_PATH: /etc/pebble/ca.cert.pem
+
+ ACME_DIRECTORY_URL: https://127.0.0.1:14000/dir
+ ACME_CHALLTESTSRV_URL: http://127.0.0.1:8055
+ ACME_PEBBLE_MANAGEMENT_URL: https://127.0.0.1:15000
+
+ ACME_DOMAIN_NAME: test.example.com
+ ACME_CAP_EAB_ENABLED: ${{matrix.eab}}
+
+ ACME_TLSALPN_PORT: 5001
+ ACME_HTTP_PORT: 5002
+ ACME_HTTPS_PORT: 5003
+
+
+ #
+ # Pipeline
+ #
+
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-node@v4
+ with:
+ node-version: ${{matrix.node}}
+
+ # Pebble Challenge Test Server
+ - name: Install Pebble Challenge Test Server
+ run: sudo -E /bin/bash ./.github/scripts/tests-install-cts.sh
+
+ - name: Start Pebble Challenge Test Server
+ run: |-
+ nohup bash -c "pebble-challtestsrv \
+ -dns01 :${PEBBLECTS_DNS_PORT} \
+ -tlsalpn01 :${ACME_TLSALPN_PORT} \
+ -http01 :${ACME_HTTP_PORT} \
+ -https01 :${ACME_HTTPS_PORT} \
+ -defaultIPv4 127.0.0.1 \
+ -defaultIPv6 \"\" &"
+
+ # Pebble
+ - name: Install Pebble
+ run: sudo -E /bin/bash ./.github/scripts/tests-install-pebble.sh
+
+ - name: Start Pebble
+ run: nohup bash -c "pebble -strict -config /etc/pebble/pebble.json -dnsserver 127.53.53.53:53 &"
+
+ - name: Wait for Pebble
+ run: /bin/bash ./.github/scripts/tests-wait-for-ca.sh
+
+ # CoreDNS
+ - name: Install CoreDNS
+ run: sudo -E /bin/bash ./.github/scripts/tests-install-coredns.sh
+
+ - name: Start CoreDNS
+ run: nohup bash -c "sudo coredns -p 53 -conf /etc/coredns/Corefile &"
+
+ - name: Use CoreDNS for DNS resolution
+ run: echo "nameserver 127.53.53.53" | sudo tee /etc/resolv.conf
+
+ # Run tests
+ - run: npm i
+ - run: npm run lint
+ - run: npm run lint-types
+ - run: npm run build-docs
+ - run: npm run test
diff --git a/packages/core/acme-client/README.md b/packages/core/acme-client/README.md
index 21388a88..7d9eb09e 100644
--- a/packages/core/acme-client/README.md
+++ b/packages/core/acme-client/README.md
@@ -1,4 +1,4 @@
-# acme-client [](https://circleci.com/gh/publishlab/node-acme-client)
+# acme-client [](https://github.com/publishlab/node-acme-client/actions/workflows/tests.yml)
*A simple and unopinionated ACME client.*
diff --git a/packages/core/acme-client/examples/README.md b/packages/core/acme-client/examples/README.md
new file mode 100644
index 00000000..1c26d611
--- /dev/null
+++ b/packages/core/acme-client/examples/README.md
@@ -0,0 +1,19 @@
+# Disclaimer
+
+These examples should not be used as is for any production environment, as they are just proof of concepts meant for testing and to get you started. The examples are naively written and purposefully avoids important topics since they will be specific to your application and how you choose to use `acme-client`, like for example:
+
+1. **Concurrency control**
+ * If implementing on-demand certificate generation
+ * What happens when multiple requests hit your domain at the same time?
+ * Ensure your application does not place multiple cert orders for the same domain at the same time by implementing some sort of exclusive lock
+2. **Domain allow lists**
+ * If implementing on-demand certificate generation
+ * What happens when someone manipulates the `ServerName` or `Host` header to your service?
+ * Ensure your application is unable to place certificate orders for domains you do not intend, as this can quickly rate limit your account and cause a DoS
+3. **Clustering**
+ * If using `acme-client` across a cluster of servers
+ * Ensure challenge responses are known to all servers in your cluster, perhaps using a database or shared storage
+4. **Certificate and key storage**
+ * Where and how should the account key be stored and read?
+ * Where and how should certificates and cert keys be stored and read?
+ * How and when should they be renewed?
diff --git a/packages/core/acme-client/examples/api.js b/packages/core/acme-client/examples/api.js
index 998201e6..d2b7162a 100644
--- a/packages/core/acme-client/examples/api.js
+++ b/packages/core/acme-client/examples/api.js
@@ -4,7 +4,6 @@
const acme = require('./../');
-
function log(m) {
process.stdout.write(`${m}\n`);
}
diff --git a/packages/core/acme-client/examples/auto.js b/packages/core/acme-client/examples/auto.js
index 1495043b..cd4295d7 100644
--- a/packages/core/acme-client/examples/auto.js
+++ b/packages/core/acme-client/examples/auto.js
@@ -5,7 +5,6 @@
// const fs = require('fs').promises;
const acme = require('./../');
-
function log(m) {
process.stdout.write(`${m}\n`);
}
diff --git a/packages/core/acme-client/examples/fallback.crt b/packages/core/acme-client/examples/fallback.crt
new file mode 100644
index 00000000..100e4781
--- /dev/null
+++ b/packages/core/acme-client/examples/fallback.crt
@@ -0,0 +1,19 @@
+-----BEGIN CERTIFICATE-----
+MIIDCTCCAfGgAwIBAgIUGwI6ZLE3HN7oRZ9BvWLde0Tsu7EwDQYJKoZIhvcNAQEL
+BQAwFDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIyMDgwMTAwNTMzMVoXDTIyMDgz
+MTAwNTMzMVowFDESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG9w0BAQEF
+AAOCAQ8AMIIBCgKCAQEA4c7zSiY6OEp9xYZHY42FUfOLREm03NstZhd9IxFFePwe
+CTTirJjmi5teKQwzBmEok0SJkanJUaMsMlOHjEykWSc4SBO4QjD349Q60044i9WS
+7KHzeSqpWTG+V9jF3HOJPw843VG9hXy3ulXKcysTXzumTVQwfatCODBNkpWqMju2
+N33biLgmpqwLbDSfKXS3uSVTfoHAKGT/oRepko7/0Hwr5oEmjXEbpRWRhU09KYjH
+7jokRaiQRn0h216a0r4AKzSNGihNQtKJZIuwJvLFPMQYafsu9qBaCLPqDBXCwQWG
+aYh6Cm3kTkADKzG1LVPB/7/Uh2d4Fck/ejR9qXRK3QIDAQABo1MwUTAdBgNVHQ4E
+FgQUvyceAVDMPbW7wHwNF9px5dWfgd4wHwYDVR0jBBgwFoAUvyceAVDMPbW7wHwN
+F9px5dWfgd4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAaYkz
+AOHrRirPwfkwjb+uMliGHfANrmak8r5VDQA73RLTQLRhMpf1yrb1uhH7p/CUYKap
+x1C8RGQAXujoQbQOslyZA7cVLA9ASSZS6Noq7NerfGBiqxeuye+x3lIIk1EOL/rH
+aBu9rrYGmlU49PlGAQSfFHkwzXti2Mp1VQv8eMOBLR49ezZIXHiPE8S3gjNymZ0G
+UA13wzZCT7SG1BLmQ/cBVASG2wvhlC8IG/4vF0Xe+boSOb1vGWUtHS+MnvvRK4n5
+TMUtrnxSQ/LA8AtobvzqgvQVKBSPLK6RzLE7I+Q9pWsbKTBqfyStuQrQFqafBOqN
+eYfPUgiID9uvfrxLvA==
+-----END CERTIFICATE-----
diff --git a/packages/core/acme-client/examples/fallback.key b/packages/core/acme-client/examples/fallback.key
new file mode 100644
index 00000000..1cbd8f66
--- /dev/null
+++ b/packages/core/acme-client/examples/fallback.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDhzvNKJjo4Sn3F
+hkdjjYVR84tESbTc2y1mF30jEUV4/B4JNOKsmOaLm14pDDMGYSiTRImRqclRoywy
+U4eMTKRZJzhIE7hCMPfj1DrTTjiL1ZLsofN5KqlZMb5X2MXcc4k/DzjdUb2FfLe6
+VcpzKxNfO6ZNVDB9q0I4ME2SlaoyO7Y3fduIuCamrAtsNJ8pdLe5JVN+gcAoZP+h
+F6mSjv/QfCvmgSaNcRulFZGFTT0piMfuOiRFqJBGfSHbXprSvgArNI0aKE1C0olk
+i7Am8sU8xBhp+y72oFoIs+oMFcLBBYZpiHoKbeROQAMrMbUtU8H/v9SHZ3gVyT96
+NH2pdErdAgMBAAECggEBAImI0FxQblOM45AkmmTDdPmWWjPspNGEWeF92wU55tOq
+0+yNnqa7tmg/6JkdyhJPqTQRoazr+ifUN/4rLDtDDzMSFVCpWihOxR2qTW4YjY52
+NjgU6EPbvSwLhUDiUplUcbrL3bnHqKSecxV2XYnKKdFudntRFPvmDL5GhWkL6Y8P
+9KiQaYuPf4av8PR0NlWBMiZs+CBjLlnSTMAWRYj5mRSyFSEOMT7+Lvr3TqrO2/nh
+0H30LXxrXXXuCbQXnVy3oSNf7TrathT2ADIrUUTdRHsLscvkEA35VtFQtWdJLtEg
+sso1J7viV9YDU4niPSdHPj3ubBjAExej4qCOzatsIQ0CgYEA8L5S3ojy89g7q6vB
+QuusIrjGkyM1yebDWqhEnjvlMpfrU1hCS90BM1ozZ28bjz/7PBimKL+A8BO+W0m4
+2s9YbZP5aGwo18Iq86XEdtDgWtQ3NXbYkb8F8LNtyevC/UlAI/xyIRr7hDYlr/1v
+jJg16DXiNLyk+uj4Q3EuwzNl8n8CgYEA8B5UUkOiufPtm+ZOq9AlBpIa+NYaahZM
+h52jzMTKsFB18xsZU/ufvpKvXEu1sTeCDRo3JAHmiA6AG292Zc7W+uWRtMtlmQWE
+wnoZ6hKvEkFnArLCY6Nm5Qqm1wipLwDVO3dD/CDL86siHrXK4wU7Q+bp6xbt8lDi
+itz5F7p7HKMCgYAoj8iimexlTU9wczXSsqaECyHZ9JrBc9ICWkuFZY4OYi5SEpLI
++WmUX2Q9zyiTkDIiQ/zq7KkqygjOlLNCmqDJhZ8GCwMupxZZitp5MmQ6qXrL1URT
++h1kGrcqyEBIMKlP5t7L2SH7eqwK5OaAh7y9bSa5v/cEF3CM3GsGlIhevQKBgBGU
+RtwW84zlnNmzDMNrY6qNe8gH9LsbktLC6cEOD0DFQz1fGIWbgGB1YL1DFbQ5uh23
+c54BPZ1sYlif2m0trXOE5xvzYCbJzqRmSAto/sQ5YY9DAxREXD4cf4ZyreAxEWtf
+Ge0VgZj/SGozKP1h3qrj9vAtJ5J79XnxH5NrJaQ9AoGBAM2rQrt8H2kizg4wMGRZ
+0G3709W7xxlbPdm+i/jFVDayJswCr0+eMm4gGyyZL3135D0fcijxytKgg3/OpOJF
+jC9vsHsE2K1ATp6eYvYjrhqJHI1m44aq/h46SfajytZQjwMT/jaApULDP2/fCBm5
+6eS2WCyHyrYJyrgoYQF56nsT
+-----END PRIVATE KEY-----
diff --git a/packages/core/acme-client/examples/tls-alpn-01/README.md b/packages/core/acme-client/examples/tls-alpn-01/README.md
new file mode 100644
index 00000000..40863519
--- /dev/null
+++ b/packages/core/acme-client/examples/tls-alpn-01/README.md
@@ -0,0 +1,44 @@
+# tls-alpn-01
+
+Responding to `tls-alpn-01` challenges using Node.js is a bit more involved than the other two challenge types, and requires a proxy (f.ex. [Nginx](https://nginx.org) or [HAProxy](https://www.haproxy.org)) in front of the Node.js service. The reason for this is that `tls-alpn-01` is solved by responding to the ACME challenge using self-signed certificates with an ALPN extension containing the challenge response.
+
+Since we don't want users of our application to be served with these self-signed certificates, we need to split the HTTPS traffic into two different Node.js backends - one that only serves ALPN certificates for challenge responses, and the other for actual end-user traffic that serves certificates retrieved from the ACME provider. As far as I *(library author)* know, routing HTTPS traffic based on ALPN protocol can not be done purely using Node.js.
+
+The end result should look something like this:
+
+```text
+Nginx or HAProxy (0.0.0.0:443)
+ *inspect requests SSL ALPN protocol*
+ If ALPN == acme-tls/1
+ -> Node.js ALPN responder (127.0.0.1:4444)
+ Else
+ -> Node.js HTTPS server (127.0.0.1:4443)
+```
+
+Example proxy configuration:
+
+* [haproxy.cfg](haproxy.cfg) *(requires HAProxy >= v1.9.1)*
+* [nginx.conf](nginx.conf) *(requires [ngx_stream_ssl_preread_module](https://nginx.org/en/docs/stream/ngx_stream_ssl_preread_module.html))*
+
+Big thanks to [acme.sh](https://github.com/acmesh-official/acme.sh) and [dehydrated](https://github.com/dehydrated-io/dehydrated) for doing the legwork and providing Nginx and HAProxy config examples.
+
+## How it works
+
+When solving `tls-alpn-01` challenges, you prove ownership of a domain name by serving a specially crafted certificate over HTTPS. The ACME authority provides the client with a token that is placed into the certificates `id-pe-acmeIdentifier` extension along with a thumbprint of your account key.
+
+Once the order is finalized, the ACME authority will verify by sending HTTPS requests to your domain with the `acme-tls/1` ALPN protocol, indicating to the server that it should serve the challenge response certificate. If the `id-pe-acmeIdentifier` extension contains the correct payload, the challenge is valid.
+
+## Pros and cons
+
+* Challenge must be satisfied using port 443 (HTTPS)
+* Useful in instances where port 80 is unavailable
+* Can not be used to issue wildcard certificates
+* More complex than `http-01`, can not be solved purely using Node.js
+* If using multiple web servers, all of them need to respond with the correct certificate
+
+## External links
+
+* [https://letsencrypt.org/docs/challenge-types/#tls-alpn-01](https://letsencrypt.org/docs/challenge-types/#tls-alpn-01)
+* [https://github.com/dehydrated-io/dehydrated/blob/master/docs/tls-alpn.md](https://github.com/dehydrated-io/dehydrated/blob/master/docs/tls-alpn.md)
+* [https://github.com/acmesh-official/acme.sh/wiki/TLS-ALPN-without-downtime](https://github.com/acmesh-official/acme.sh/wiki/TLS-ALPN-without-downtime)
+* [https://datatracker.ietf.org/doc/html/rfc8737](https://datatracker.ietf.org/doc/html/rfc8737)
diff --git a/packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg b/packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg
new file mode 100644
index 00000000..5a99be7c
--- /dev/null
+++ b/packages/core/acme-client/examples/tls-alpn-01/haproxy.cfg
@@ -0,0 +1,23 @@
+##
+# HTTPS listener
+# - Send to ALPN responder port 4444 if protocol is acme-tls/1
+# - Default to HTTPS backend port 4443
+##
+
+frontend https
+ mode tcp
+ bind :443
+ tcp-request inspect-delay 5s
+ tcp-request content accept if { req_ssl_hello_type 1 }
+ use_backend alpnresp if { req.ssl_alpn acme-tls/1 }
+ default_backend https
+
+# Default HTTPS backend
+backend https
+ mode tcp
+ server https 127.0.0.1:4443
+
+# ACME tls-alpn-01 responder backend
+backend alpnresp
+ mode tcp
+ server acmesh 127.0.0.1:4444
diff --git a/packages/core/acme-client/examples/tls-alpn-01/nginx.conf b/packages/core/acme-client/examples/tls-alpn-01/nginx.conf
new file mode 100644
index 00000000..cb4aa7b6
--- /dev/null
+++ b/packages/core/acme-client/examples/tls-alpn-01/nginx.conf
@@ -0,0 +1,19 @@
+##
+# HTTPS server
+# - Send to ALPN responder port 4444 if protocol is acme-tls/1
+# - Default to HTTPS backend port 4443
+##
+
+stream {
+ map $ssl_preread_alpn_protocols $tls_port {
+ ~\bacme-tls/1\b 4444;
+ default 4443;
+ }
+
+ server {
+ listen 443;
+ listen [::]:443;
+ proxy_pass 127.0.0.1:$tls_port;
+ ssl_preread on;
+ }
+}
diff --git a/packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js b/packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js
new file mode 100644
index 00000000..e04d73c1
--- /dev/null
+++ b/packages/core/acme-client/examples/tls-alpn-01/tls-alpn-01.js
@@ -0,0 +1,180 @@
+/**
+ * Example using tls-alpn-01 challenge to generate certificates on-demand
+ */
+
+const fs = require('fs');
+const path = require('path');
+const https = require('https');
+const tls = require('tls');
+const acme = require('./../../');
+
+const HTTPS_SERVER_PORT = 4443;
+const ALPN_RESPONDER_PORT = 4444;
+const VALID_DOMAINS = ['example.com', 'example.org'];
+const FALLBACK_KEY = fs.readFileSync(path.join(__dirname, '..', 'fallback.key'));
+const FALLBACK_CERT = fs.readFileSync(path.join(__dirname, '..', 'fallback.crt'));
+
+const pendingDomains = {};
+const alpnResponses = {};
+const certificateStore = {};
+
+function log(m) {
+ process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
+}
+
+
+/**
+ * On-demand certificate generation using tls-alpn-01
+ */
+
+async function getCertOnDemand(client, servername, attempt = 0) {
+ /* Invalid domain */
+ if (!VALID_DOMAINS.includes(servername)) {
+ throw new Error(`Invalid domain: ${servername}`);
+ }
+
+ /* Certificate exists */
+ if (servername in certificateStore) {
+ return certificateStore[servername];
+ }
+
+ /* Waiting on certificate order to go through */
+ if (servername in pendingDomains) {
+ if (attempt >= 10) {
+ throw new Error(`Gave up waiting on certificate for ${servername}`);
+ }
+
+ await new Promise((resolve) => { setTimeout(resolve, 1000); });
+ return getCertOnDemand(client, servername, (attempt + 1));
+ }
+
+ /* Create CSR */
+ log(`Creating CSR for ${servername}`);
+ const [key, csr] = await acme.crypto.createCsr({
+ commonName: servername
+ });
+
+ /* Order certificate */
+ log(`Ordering certificate for ${servername}`);
+ const cert = await client.auto({
+ csr,
+ email: 'test@example.com',
+ termsOfServiceAgreed: true,
+ challengePriority: ['tls-alpn-01'],
+ challengeCreateFn: async (authz, challenge, keyAuthorization) => {
+ alpnResponses[authz.identifier.value] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
+ },
+ challengeRemoveFn: (authz) => {
+ delete alpnResponses[authz.identifier.value];
+ }
+ });
+
+ /* Done, store certificate */
+ log(`Certificate for ${servername} created successfully`);
+ certificateStore[servername] = [key, cert];
+ delete pendingDomains[servername];
+ return certificateStore[servername];
+}
+
+
+/**
+ * Main
+ */
+
+(async () => {
+ try {
+ /**
+ * Initialize ACME client
+ */
+
+ log('Initializing ACME client');
+ const client = new acme.Client({
+ directoryUrl: acme.directory.letsencrypt.staging,
+ accountKey: await acme.crypto.createPrivateKey()
+ });
+
+
+ /**
+ * ALPN responder
+ */
+
+ const alpnResponder = https.createServer({
+ /* Fallback cert */
+ key: FALLBACK_KEY,
+ cert: FALLBACK_CERT,
+
+ /* Allow acme-tls/1 ALPN protocol */
+ ALPNProtocols: ['acme-tls/1'],
+
+ /* Serve ALPN certificate based on servername */
+ SNICallback: async (servername, cb) => {
+ try {
+ log(`Handling ALPN SNI request for ${servername}`);
+ if (!Object.keys(alpnResponses).includes(servername)) {
+ throw new Error(`No ALPN certificate found for ${servername}`);
+ }
+
+ /* Serve ALPN challenge response */
+ log(`Found ALPN certificate for ${servername}, serving secure context`);
+ cb(null, tls.createSecureContext({
+ key: alpnResponses[servername][0],
+ cert: alpnResponses[servername][1]
+ }));
+ }
+ catch (e) {
+ log(`[ERROR] ${e.message}`);
+ cb(e.message);
+ }
+ }
+ });
+
+ /* Terminate once TLS handshake has been established */
+ alpnResponder.on('secureConnection', (socket) => {
+ socket.end();
+ });
+
+ alpnResponder.listen(ALPN_RESPONDER_PORT, () => {
+ log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`);
+ });
+
+
+ /**
+ * HTTPS server
+ */
+
+ const requestListener = (req, res) => {
+ log(`HTTP 200 ${req.headers.host}${req.url}`);
+ res.writeHead(200);
+ res.end('Hello world\n');
+ };
+
+ const httpsServer = https.createServer({
+ /* Fallback cert */
+ key: FALLBACK_KEY,
+ cert: FALLBACK_CERT,
+
+ /* Serve certificate based on servername */
+ SNICallback: async (servername, cb) => {
+ try {
+ log(`Handling SNI request for ${servername}`);
+ const [key, cert] = await getCertOnDemand(client, servername);
+
+ log(`Found certificate for ${servername}, serving secure context`);
+ cb(null, tls.createSecureContext({ key, cert }));
+ }
+ catch (e) {
+ log(`[ERROR] ${e.message}`);
+ cb(e.message);
+ }
+ }
+ }, requestListener);
+
+ httpsServer.listen(HTTPS_SERVER_PORT, () => {
+ log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`);
+ });
+ }
+ catch (e) {
+ log(`[FATAL] ${e.message}`);
+ process.exit(1);
+ }
+})();
diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json
index 86bd44bc..771bb55f 100644
--- a/packages/core/acme-client/package.json
+++ b/packages/core/acme-client/package.json
@@ -37,8 +37,7 @@
"lint": "eslint .",
"lint-types": "tsd",
"prepublishOnly": "npm run build-docs",
- "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
- "test-local": "/bin/bash scripts/run-tests.sh"
+ "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\""
},
"repository": {
"type": "git",
diff --git a/packages/core/acme-client/scripts/run-tests.sh b/packages/core/acme-client/scripts/run-tests.sh
deleted file mode 100644
index c2f01f0d..00000000
--- a/packages/core/acme-client/scripts/run-tests.sh
+++ /dev/null
@@ -1,59 +0,0 @@
-#!/bin/bash
-#
-# Run test suite locally using CircleCI CLI.
-#
-set -eu
-
-JOBS=("$@")
-
-CIRCLECI_CLI_URL="https://github.com/CircleCI-Public/circleci-cli/releases/download/v0.1.29936/circleci-cli_0.1.29936_linux_amd64.tar.gz"
-CIRCLECI_CLI_SHASUM="fdc8da76111facae4a10f3717502eeb5d78db0256ef94a2f8d53078978175d40"
-CIRCLECI_CLI_PATH="/tmp/circleci-cli"
-CIRCLECI_CLI_BIN="${CIRCLECI_CLI_PATH}/circleci"
-
-PROJECT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && cd .. && pwd )"
-CONFIG_PATH="${PROJECT_DIR}/.circleci/.temp.yml"
-
-# Run all jobs by default
-if [[ ${#JOBS[@]} -eq 0 ]]; then
- JOBS=(
- "v16"
- "v18"
- "v20"
- "eab-v16"
- "eab-v18"
- "eab-v20"
- )
-fi
-
-# Download CircleCI CLI
-if [[ ! -f "${CIRCLECI_CLI_BIN}" ]]; then
- echo "[-] Downloading CircleCI cli"
- mkdir -p "${CIRCLECI_CLI_PATH}"
- wget -nv "${CIRCLECI_CLI_URL}" -O "${CIRCLECI_CLI_PATH}/circleci-cli.tar.gz"
- echo "${CIRCLECI_CLI_SHASUM} *${CIRCLECI_CLI_PATH}/circleci-cli.tar.gz" | sha256sum -c
- tar zxvf "${CIRCLECI_CLI_PATH}/circleci-cli.tar.gz" -C "${CIRCLECI_CLI_PATH}" --strip-components=1
-fi
-
-# Disable CircleCI update checks and telemetry
-export CIRCLECI_CLI_SKIP_UPDATE_CHECK="true"
-export CIRCLECI_CLI_TELEMETRY_OPTOUT="1"
-
-# Run test suite
-echo "[-] Running test suite"
-$CIRCLECI_CLI_BIN config process "${PROJECT_DIR}/.circleci/config.yml" > "${CONFIG_PATH}"
-$CIRCLECI_CLI_BIN config validate -c "${CONFIG_PATH}"
-
-for job in "${JOBS[@]}"; do
- echo "[-] Running job: ${job}"
- $CIRCLECI_CLI_BIN local execute -c "${CONFIG_PATH}" "${job}"
- echo "[+] ${job} completed successfully"
-done
-
-# Clean up
-if [[ -f "${CONFIG_PATH}" ]]; then
- rm "${CONFIG_PATH}"
-fi
-
-echo "[+] Test suite ran successfully!"
-exit 0
diff --git a/packages/core/acme-client/scripts/test-suite-install-step.sh b/packages/core/acme-client/scripts/test-suite-install-step.sh
deleted file mode 100644
index 5de092a5..00000000
--- a/packages/core/acme-client/scripts/test-suite-install-step.sh
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/bin/bash
-#
-# Install and init step-ca for testing.
-#
-set -eu
-
-# Download and install
-wget -nv "https://dl.step.sm/gh-release/certificates/gh-release-header/v${STEPCA_VERSION}/step-ca_${STEPCA_VERSION}_amd64.deb" -O /tmp/step-ca.deb
-wget -nv "https://dl.step.sm/gh-release/cli/gh-release-header/v${STEPCLI_VERSION}/step-cli_${STEPCLI_VERSION}_amd64.deb" -O /tmp/step-cli.deb
-
-sudo dpkg -i /tmp/step-ca.deb
-sudo dpkg -i /tmp/step-cli.deb
-
-# Initialize
-echo "hunter2" > /tmp/password
-
-step ca init --name="Example Inc." --dns="localhost" --address="127.0.0.1:8443" --provisioner="test@example.com" --password-file="/tmp/password"
-step ca provisioner add acme --type ACME
-
-exit 0
diff --git a/packages/core/acme-client/test/00-pebble.spec.js b/packages/core/acme-client/test/00-pebble.spec.js
index 7a26c3b7..992fc6bd 100644
--- a/packages/core/acme-client/test/00-pebble.spec.js
+++ b/packages/core/acme-client/test/00-pebble.spec.js
@@ -199,7 +199,7 @@ describe('pebble', () => {
});
it('should timeout challenge response', async () => {
- await assert.isRejected(retrieveTlsAlpnCertificate('example.org', tlsAlpnPort, 500), /timed out/);
+ await assert.isRejected(retrieveTlsAlpnCertificate('example.org', tlsAlpnPort, 500));
});
it('should add challenge response', async () => {
From a6bf19860493e51e068e38614452d7f790eb7194 Mon Sep 17 00:00:00 2001
From: GitHub Actions Bot
Date: Fri, 2 Feb 2024 19:24:16 +0000
Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20w?=
=?UTF-8?q?ith=202=20commits=20[trident-sync]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Example for on-demand http-01
---
.../acme-client/examples/http-01/README.md | 21 +++
.../acme-client/examples/http-01/http-01.js | 172 ++++++++++++++++++
2 files changed, 193 insertions(+)
create mode 100644 packages/core/acme-client/examples/http-01/README.md
create mode 100644 packages/core/acme-client/examples/http-01/http-01.js
diff --git a/packages/core/acme-client/examples/http-01/README.md b/packages/core/acme-client/examples/http-01/README.md
new file mode 100644
index 00000000..a08cace1
--- /dev/null
+++ b/packages/core/acme-client/examples/http-01/README.md
@@ -0,0 +1,21 @@
+# http-01
+
+The `http-01` challenge type is the simplest to implement and should likely be your default choice, unless you either require wildcard certificates or if port 80 is unavailable for use.
+
+## How it works
+
+When solving `http-01` challenges, you prove ownership of a domain name by serving a specific payload from a specific URL. The ACME authority provides the client with a token that is used to generate the URL and file contents. The file must exist at `http://$YOUR_DOMAIN/.well-known/acme-challenge/$TOKEN` and contain the token and a thumbprint of your account key.
+
+Once the order is finalized, the ACME authority will verify that the URL responds with the correct payload by sending HTTP requests before the challenge is valid. HTTP redirects are followed, and Let's Encrypt allows redirecting to HTTPS although this diverges from the ACME spec.
+
+## Pros and cons
+
+* Challenge must be satisfied using port 80 (HTTP)
+* The simplest challenge type to implement
+* Can not be used to issue wildcard certificates
+* If using multiple web servers, all of them need to respond with the correct token
+
+## External links
+
+* [https://letsencrypt.org/docs/challenge-types/#http-01-challenge](https://letsencrypt.org/docs/challenge-types/#http-01-challenge)
+* [https://datatracker.ietf.org/doc/html/rfc8555#section-8.3](https://datatracker.ietf.org/doc/html/rfc8555#section-8.3)
diff --git a/packages/core/acme-client/examples/http-01/http-01.js b/packages/core/acme-client/examples/http-01/http-01.js
new file mode 100644
index 00000000..75153385
--- /dev/null
+++ b/packages/core/acme-client/examples/http-01/http-01.js
@@ -0,0 +1,172 @@
+/**
+ * Example using http-01 challenge to generate certificates on-demand
+ */
+
+const fs = require('fs');
+const path = require('path');
+const http = require('http');
+const https = require('https');
+const tls = require('tls');
+const acme = require('./../../');
+
+const HTTP_SERVER_PORT = 80;
+const HTTPS_SERVER_PORT = 443;
+const VALID_DOMAINS = ['example.com', 'example.org'];
+const FALLBACK_KEY = fs.readFileSync(path.join(__dirname, '..', 'fallback.key'));
+const FALLBACK_CERT = fs.readFileSync(path.join(__dirname, '..', 'fallback.crt'));
+
+const pendingDomains = {};
+const challengeResponses = {};
+const certificateStore = {};
+
+function log(m) {
+ process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
+}
+
+
+/**
+ * On-demand certificate generation using http-01
+ */
+
+async function getCertOnDemand(client, servername, attempt = 0) {
+ /* Invalid domain */
+ if (!VALID_DOMAINS.includes(servername)) {
+ throw new Error(`Invalid domain: ${servername}`);
+ }
+
+ /* Certificate exists */
+ if (servername in certificateStore) {
+ return certificateStore[servername];
+ }
+
+ /* Waiting on certificate order to go through */
+ if (servername in pendingDomains) {
+ if (attempt >= 10) {
+ throw new Error(`Gave up waiting on certificate for ${servername}`);
+ }
+
+ await new Promise((resolve) => { setTimeout(resolve, 1000); });
+ return getCertOnDemand(client, servername, (attempt + 1));
+ }
+
+ /* Create CSR */
+ log(`Creating CSR for ${servername}`);
+ const [key, csr] = await acme.crypto.createCsr({
+ commonName: servername
+ });
+
+ /* Order certificate */
+ log(`Ordering certificate for ${servername}`);
+ const cert = await client.auto({
+ csr,
+ email: 'test@example.com',
+ termsOfServiceAgreed: true,
+ challengePriority: ['http-01'],
+ challengeCreateFn: (authz, challenge, keyAuthorization) => {
+ challengeResponses[challenge.token] = keyAuthorization;
+ },
+ challengeRemoveFn: (authz, challenge) => {
+ delete challengeResponses[challenge.token];
+ }
+ });
+
+ /* Done, store certificate */
+ log(`Certificate for ${servername} created successfully`);
+ certificateStore[servername] = [key, cert];
+ delete pendingDomains[servername];
+ return certificateStore[servername];
+}
+
+
+/**
+ * Main
+ */
+
+(async () => {
+ try {
+ /**
+ * Initialize ACME client
+ */
+
+ log('Initializing ACME client');
+ const client = new acme.Client({
+ directoryUrl: acme.directory.letsencrypt.staging,
+ accountKey: await acme.crypto.createPrivateKey()
+ });
+
+
+ /**
+ * HTTP server
+ */
+
+ const httpServer = http.createServer((req, res) => {
+ if (req.url.match(/\/\.well-known\/acme-challenge\/.+/)) {
+ const token = req.url.split('/').pop();
+ log(`Received challenge request for token=${token}`);
+
+ /* ACME challenge response */
+ if (token in challengeResponses) {
+ log(`Serving challenge response HTTP 200 token=${token}`);
+ res.writeHead(200);
+ res.end(challengeResponses[token]);
+ return;
+ }
+
+ /* Challenge response not found */
+ log(`Oops, challenge response not found for token=${token}`);
+ res.writeHead(404);
+ res.end();
+ return;
+ }
+
+ /* HTTP 302 redirect */
+ log(`HTTP 302 ${req.headers.host}${req.url}`);
+ res.writeHead(302, { Location: `https://${req.headers.host}${req.url}` });
+ res.end();
+ });
+
+ httpServer.listen(HTTP_SERVER_PORT, () => {
+ log(`HTTP server listening on port ${HTTP_SERVER_PORT}`);
+ });
+
+
+ /**
+ * HTTPS server
+ */
+
+ const requestListener = (req, res) => {
+ log(`HTTP 200 ${req.headers.host}${req.url}`);
+ res.writeHead(200);
+ res.end('Hello world\n');
+ };
+
+ const httpsServer = https.createServer({
+ /* Fallback certificate */
+ key: FALLBACK_KEY,
+ cert: FALLBACK_CERT,
+
+ /* Serve certificate based on servername */
+ SNICallback: async (servername, cb) => {
+ try {
+ log(`Handling SNI request for ${servername}`);
+ const [key, cert] = await getCertOnDemand(client, servername);
+
+ log(`Found certificate for ${servername}, serving secure context`);
+ cb(null, tls.createSecureContext({ key, cert }));
+ }
+ catch (e) {
+ log(`[ERROR] ${e.message}`);
+ cb(e.message);
+ }
+ }
+ }, requestListener);
+
+ httpsServer.listen(HTTPS_SERVER_PORT, () => {
+ log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`);
+ });
+ }
+ catch (e) {
+ log(`[FATAL] ${e.message}`);
+ process.exit(1);
+ }
+})();
From 80cd1bfc8e9d526b2a204bcfcb443ad8d5b35ddd Mon Sep 17 00:00:00 2001
From: GitHub Actions Bot
Date: Sat, 3 Feb 2024 19:24:11 +0000
Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20w?=
=?UTF-8?q?ith=205=20commits=20[trident-sync]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Update IETF links
Fix misc typos
Forgot SAN extension for self-signed ALPN certs
Replace jsrsasign dep with @peculiar/x509
---
packages/core/acme-client/CHANGELOG.md | 17 +-
packages/core/acme-client/README.md | 6 +-
packages/core/acme-client/docs/upgrade-v5.md | 2 +-
packages/core/acme-client/package.json | 3 +-
packages/core/acme-client/src/api.js | 26 +-
packages/core/acme-client/src/client.js | 34 +-
packages/core/acme-client/src/crypto/forge.js | 4 +-
packages/core/acme-client/src/crypto/index.js | 345 +++++++++---------
packages/core/acme-client/src/http.js | 8 +-
packages/core/acme-client/src/util.js | 4 +-
packages/core/acme-client/src/verify.js | 6 +-
.../core/acme-client/test/20-crypto.spec.js | 79 +++-
packages/core/acme-client/types/rfc8555.d.ts | 20 +-
13 files changed, 307 insertions(+), 247 deletions(-)
diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md
index 894dac23..a281ba52 100644
--- a/packages/core/acme-client/CHANGELOG.md
+++ b/packages/core/acme-client/CHANGELOG.md
@@ -3,6 +3,7 @@
## v5.3.0
* `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
@@ -54,13 +55,13 @@
## 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)
@@ -110,7 +111,7 @@
## 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)
@@ -121,10 +122,10 @@
* `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)
@@ -133,8 +134,8 @@
## 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)
@@ -171,7 +172,7 @@
## v2.0.1 (2018-08-17)
-* `fixed` Key rollover in compliance with [draft-ietf-acme-13](https://tools.ietf.org/html/draft-ietf-acme-acme-13)
+* `fixed` Key rollover in compliance with [draft-ietf-acme-13](https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-13)
## v2.0.0 (2018-04-02)
diff --git a/packages/core/acme-client/README.md b/packages/core/acme-client/README.md
index 7d9eb09e..3dff3cfa 100644
--- a/packages/core/acme-client/README.md
+++ b/packages/core/acme-client/README.md
@@ -4,7 +4,7 @@
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)
## Compatibility
@@ -67,7 +67,7 @@ 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({
@@ -102,7 +102,7 @@ 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`.
diff --git a/packages/core/acme-client/docs/upgrade-v5.md b/packages/core/acme-client/docs/upgrade-v5.md
index a89bb79f..f34fd6e6 100644
--- a/packages/core/acme-client/docs/upgrade-v5.md
+++ b/packages/core/acme-client/docs/upgrade-v5.md
@@ -6,7 +6,7 @@ First off this release drops support for Node LTS v10, v12 and v14, and the reas
## 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).
diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json
index 771bb55f..3cf35551 100644
--- a/packages/core/acme-client/package.json
+++ b/packages/core/acme-client/package.json
@@ -15,9 +15,10 @@
"types"
],
"dependencies": {
+ "@peculiar/x509": "^1.9.7",
+ "asn1js": "^3.0.5",
"axios": "^1.6.5",
"debug": "^4.1.1",
- "jsrsasign": "^11.0.0",
"node-forge": "^1.3.1"
},
"devDependencies": {
diff --git a/packages/core/acme-client/src/api.js b/packages/core/acme-client/src/api.js
index 84fe0f88..31c06f52 100644
--- a/packages/core/acme-client/src/api.js
+++ b/packages/core/acme-client/src/api.js
@@ -41,7 +41,7 @@ class AcmeApi {
* @private
* @param {string} url Request URL
* @param {object} [payload] Request payload, default: `null`
- * @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
+ * @param {number[]} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
* @param {object} [opts]
* @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
@@ -66,7 +66,7 @@ class AcmeApi {
* @private
* @param {string} resource Request resource name
* @param {object} [payload] Request payload, default: `null`
- * @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
+ * @param {number[]} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
* @param {object} [opts]
* @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
@@ -82,7 +82,7 @@ class AcmeApi {
/**
* Get Terms of Service URL if available
*
- * https://tools.ietf.org/html/rfc8555#section-7.1.1
+ * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
*
* @returns {Promise} 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} 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} 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} 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} 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} 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} 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} HTTP response
diff --git a/packages/core/acme-client/src/client.js b/packages/core/acme-client/src/client.js
index 2d3fa89c..fea97849 100644
--- a/packages/core/acme-client/src/client.js
+++ b/packages/core/acme-client/src/client.js
@@ -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} 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} 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} 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} 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} 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} 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} Key authorization
@@ -462,17 +462,17 @@ 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://datatracker.ietf.org/doc/html/rfc8555#section-8.4 */
if (challenge.type === 'dns-01') {
return createHash('sha256').update(result).digest('base64url');
}
- /* https://tools.ietf.org/html/rfc8737 */
+ /* https://datatracker.ietf.org/doc/html/rfc8737 */
if (challenge.type === 'tls-alpn-01') {
return result;
}
@@ -519,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} Challenge
@@ -540,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} Valid order, authorization or challenge
@@ -551,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);
@@ -597,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`
@@ -644,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
diff --git a/packages/core/acme-client/src/crypto/forge.js b/packages/core/acme-client/src/crypto/forge.js
index f22425de..5b66327a 100644
--- a/packages/core/acme-client/src/crypto/forge.js
+++ b/packages/core/acme-client/src/crypto/forge.js
@@ -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]
diff --git a/packages/core/acme-client/src/crypto/index.js b/packages/core/acme-client/src/crypto/index.js
index d7a13b7d..da0df321 100644
--- a/packages/core/acme-client/src/crypto/index.js
+++ b/packages/core/acme-client/src/crypto/index.js
@@ -7,12 +7,19 @@
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);
-/* https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
+/* 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';
@@ -28,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');
@@ -173,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} [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];
}
@@ -198,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) {
@@ -206,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;
@@ -235,43 +251,28 @@ exports.getPemBodyAsB64u = (pem) => {
throw new Error('Unable to parse PEM body from string');
}
- /* Select first object, decode to hex and b64u */
- 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 {
@@ -301,34 +302,12 @@ 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);
};
-/**
- * Parse params from a single or chain of PEM encoded certificates
- *
- * @private
- * @param {buffer|string} certPem PEM encoded certificate or chain
- * @returns {object} Certificate params
- */
-
-function getCertificateParams(certPem) {
- const chain = splitPemChain(certPem);
-
- if (!chain.length) {
- throw new Error('Unable to parse PEM body from string');
- }
-
- /* Parse certificate */
- const obj = new jsrsasign.X509();
- obj.readCertPEM(chain[0]);
- return obj.getParam();
-}
-
-
/**
* Read information from a certificate
* If multiple certificates are chained, the first will be read
@@ -350,39 +329,43 @@ function getCertificateParams(certPem) {
*/
exports.readCertificateInfo = (certPem) => {
- const params = getCertificateParams(certPem);
+ if (Buffer.isBuffer(certPem)) {
+ certPem = certPem.toString();
+ }
+
+ 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';
}
}
@@ -390,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
@@ -399,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;
@@ -408,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 };
+ }));
}
@@ -431,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} [privateKey, certificateSigningRequest]
*
* @example Create a Certificate Signing Request
@@ -479,7 +464,7 @@ function formatCsrAltNames(altNames) {
* }, certificateKey);
*/
-async function createCsr(data, keyPem = null) {
+exports.createCsr = async (data, keyPem = null) => {
if (!keyPem) {
keyPem = await createPrivateRsaKey(data.keySize);
}
@@ -491,65 +476,52 @@ async function createCsr(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
+ })
});
/* Done */
- const pem = csr.getPEM();
+ const pem = csr.toString('pem');
return [keyPem, Buffer.from(pem)];
-}
-
-exports.createCsr = createCsr;
+};
/**
* Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
*
- * https://tools.ietf.org/html/rfc8737
+ * https://datatracker.ietf.org/doc/html/rfc8737
*
* @param {object} authz Identifier authorization
* @param {string} keyAuthorization Challenge key authorization
- * @param {string} [keyPem] PEM encoded CSR private key
+ * @param {buffer|string} [keyPem] PEM encoded CSR private key
* @returns {Promise} [privateKey, certificate]
*
* @example Create a ALPN certificate
@@ -564,45 +536,58 @@ exports.createCsr = createCsr;
*/
exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
- /* Create CSR first */
+ if (!keyPem) {
+ keyPem = await createPrivateRsaKey();
+ }
+ else if (!Buffer.isBuffer(keyPem)) {
+ keyPem = Buffer.from(keyPem);
+ }
+
const now = new Date();
const commonName = authz.identifier.value;
- const [key, csr] = await createCsr({ commonName }, keyPem);
-
- /* Parse params and grab stuff we need */
- const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csr.toString());
- const { subject, sbjpubkey, extreq, sigalg } = params;
-
- /* ALPN extension */
- const alpnExt = {
- critical: true,
- extname: alpnAcmeIdentifierOID,
- extn: new jsrsasign.KJUR.asn1.DEROctetString({
- hex: crypto.createHash('sha256').update(keyAuthorization).digest('hex')
- })
- };
/* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */
const random = await randomInt(1, 999999999);
- const serial = `${Math.floor(now.getTime() / 1000)}${random}`;
+ 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 certificate = new jsrsasign.KJUR.asn1.x509.Certificate({
- subject,
- sbjpubkey,
- sigalg,
- version: 3,
- serial: { hex: Buffer.from(serial).toString('hex') },
- issuer: subject,
- notbefore: jsrsasign.datetozulu(now),
- notafter: jsrsasign.datetozulu(now),
- cakey: key.toString(),
- ext: extreq.concat([alpnExt])
+ const cert = await x509.X509CertificateGenerator.createSelfSigned({
+ keys,
+ signingAlgorithm,
+ extensions,
+ serialNumber,
+ notBefore: now,
+ notAfter: now,
+ name: createCsrSubject({
+ CN: commonName
+ })
});
/* Done */
- const pem = certificate.getPEM();
- return [key, Buffer.from(pem)];
+ const pem = cert.toString('pem');
+ return [keyPem, Buffer.from(pem)];
};
@@ -615,14 +600,20 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
*/
exports.isAlpnCertificateAuthorizationValid = (certPem, keyAuthorization) => {
- const params = getCertificateParams(certPem);
- const expectedHex = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
- const acmeExt = (params.ext || []).find((e) => (e && e.extname && (e.extname === alpnAcmeIdentifierOID)));
+ const expected = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
- if (!acmeExt || !acmeExt.extn || !acmeExt.extn.octstr || !acmeExt.extn.octstr.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 (acmeExt.extn.octstr.hex === expectedHex);
+ return (result === expected);
};
diff --git a/packages/core/acme-client/src/http.js b/packages/core/acme-client/src/http.js
index 49f79322..b3580263 100644
--- a/packages/core/acme-client/src/http.js
+++ b/packages/core/acme-client/src/http.js
@@ -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} 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;
diff --git a/packages/core/acme-client/src/util.js b/packages/core/acme-client/src/util.js
index 73ecdd3b..c0c56370 100644
--- a/packages/core/acme-client/src/util.js
+++ b/packages/core/acme-client/src/util.js
@@ -93,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') {
@@ -113,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
*/
diff --git a/packages/core/acme-client/src/verify.js b/packages/core/acme-client/src/verify.js
index 2ab95e8f..c0456497 100644
--- a/packages/core/acme-client/src/verify.js
+++ b/packages/core/acme-client/src/verify.js
@@ -13,7 +13,7 @@ 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
@@ -85,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
@@ -125,7 +125,7 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
/**
* Verify ACME TLS ALPN challenge
*
- * https://tools.ietf.org/html/rfc8737
+ * https://datatracker.ietf.org/doc/html/rfc8737
*
* @param {object} authz Identifier authorization
* @param {object} challenge Authorization challenge
diff --git a/packages/core/acme-client/test/20-crypto.spec.js b/packages/core/acme-client/test/20-crypto.spec.js
index cbcd9f39..71e5277f 100644
--- a/packages/core/acme-client/test/20-crypto.spec.js
+++ b/packages/core/acme-client/test/20-crypto.spec.js
@@ -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-----
`;
@@ -112,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);
@@ -122,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
@@ -174,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
@@ -217,6 +236,13 @@ describe('crypto', () => {
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
@@ -232,6 +258,15 @@ describe('crypto', () => {
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'));
@@ -241,6 +276,10 @@ describe('crypto', () => {
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'));
+ });
});
});
});
@@ -306,6 +345,13 @@ 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
@@ -335,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);
@@ -345,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);
diff --git a/packages/core/acme-client/types/rfc8555.d.ts b/packages/core/acme-client/types/rfc8555.d.ts
index 51b6f3d9..2f88ab75 100644
--- a/packages/core/acme-client/types/rfc8555.d.ts
+++ b/packages/core/acme-client/types/rfc8555.d.ts
@@ -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 {
From 960f61d158f40e9d08a5c76ce987f4618c536143 Mon Sep 17 00:00:00 2001
From: GitHub Actions Bot
Date: Mon, 5 Feb 2024 19:24:09 +0000
Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=94=B1:=20[acme]=20sync=20upgrade=20w?=
=?UTF-8?q?ith=203=20commits=20[trident-sync]?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Bump v5.3.0
Example for dns-01
---
packages/core/acme-client/CHANGELOG.md | 2 +-
packages/core/acme-client/docs/client.md | 28 +++---
packages/core/acme-client/docs/crypto.md | 56 ++++++++++-
packages/core/acme-client/docs/forge.md | 2 +-
.../acme-client/examples/dns-01/README.md | 21 +++++
.../acme-client/examples/dns-01/dns-01.js | 92 +++++++++++++++++++
packages/core/acme-client/package.json | 2 +-
7 files changed, 181 insertions(+), 22 deletions(-)
create mode 100644 packages/core/acme-client/examples/dns-01/README.md
create mode 100644 packages/core/acme-client/examples/dns-01/dns-01.js
diff --git a/packages/core/acme-client/CHANGELOG.md b/packages/core/acme-client/CHANGELOG.md
index a281ba52..6fc2a30b 100644
--- a/packages/core/acme-client/CHANGELOG.md
+++ b/packages/core/acme-client/CHANGELOG.md
@@ -1,6 +1,6 @@
# Changelog
-## v5.3.0
+## v5.3.0 (2024-02-05)
* `added` Support and tests for satisfying `tls-alpn-01` challenges
* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR generation and parsing
diff --git a/packages/core/acme-client/docs/client.md b/packages/core/acme-client/docs/client.md
index 65dc325a..f5f29d22 100644
--- a/packages/core/acme-client/docs/client.md
+++ b/packages/core/acme-client/docs/client.md
@@ -132,7 +132,7 @@ catch (e) {
### acmeClient.createAccount([data]) ⇒ Promise.<object>
Create a new account
-https://tools.ietf.org/html/rfc8555#section-7.3
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Account
@@ -161,7 +161,7 @@ const account = await client.createAccount({
### acmeClient.updateAccount([data]) ⇒ Promise.<object>
Update existing account
-https://tools.ietf.org/html/rfc8555#section-7.3.2
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Account
@@ -182,7 +182,7 @@ const account = await client.updateAccount({
### acmeClient.updateAccountKey(newAccountKey, [data]) ⇒ Promise.<object>
Update account private key
-https://tools.ietf.org/html/rfc8555#section-7.3.5
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.5
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Account
@@ -203,7 +203,7 @@ const result = await client.updateAccountKey(newAccountKey);
### acmeClient.createOrder(data) ⇒ Promise.<object>
Create a new order
-https://tools.ietf.org/html/rfc8555#section-7.4
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Order
@@ -227,7 +227,7 @@ const order = await client.createOrder({
### acmeClient.getOrder(order) ⇒ Promise.<object>
Refresh order object from CA
-https://tools.ietf.org/html/rfc8555#section-7.4
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Order
@@ -246,7 +246,7 @@ const result = await client.getOrder(order);
### acmeClient.finalizeOrder(order, csr) ⇒ Promise.<object>
Finalize order
-https://tools.ietf.org/html/rfc8555#section-7.4
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.4
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Order
@@ -268,7 +268,7 @@ const result = await client.finalizeOrder(order, csr);
### acmeClient.getAuthorizations(order) ⇒ Promise.<Array.<object>>
Get identifier authorizations from order
-https://tools.ietf.org/html/rfc8555#section-7.5
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.5
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<Array.<object>>
- Authorizations
@@ -292,7 +292,7 @@ authorizations.forEach((authz) => {
### acmeClient.deactivateAuthorization(authz) ⇒ Promise.<object>
Deactivate identifier authorization
-https://tools.ietf.org/html/rfc8555#section-7.5.2
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.2
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Authorization
@@ -312,7 +312,7 @@ const result = await client.deactivateAuthorization(authz);
### acmeClient.getChallengeKeyAuthorization(challenge) ⇒ Promise.<string>
Get key authorization for ACME challenge
-https://tools.ietf.org/html/rfc8555#section-8.1
+https://datatracker.ietf.org/doc/html/rfc8555#section-8.1
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<string>
- Key authorization
@@ -353,7 +353,7 @@ await client.verifyChallenge(authz, challenge);
### acmeClient.completeChallenge(challenge) ⇒ Promise.<object>
Notify CA that challenge has been completed
-https://tools.ietf.org/html/rfc8555#section-7.5.1
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Challenge
@@ -373,7 +373,7 @@ const result = await client.completeChallenge(challenge);
### acmeClient.waitForValidStatus(item) ⇒ Promise.<object>
Wait for ACME provider to verify status on a order, authorization or challenge
-https://tools.ietf.org/html/rfc8555#section-7.5.1
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.5.1
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<object>
- Valid order, authorization or challenge
@@ -389,7 +389,7 @@ const challenge = { ... };
await client.waitForValidStatus(challenge);
```
**Example**
-Wait for valid authoriation status
+Wait for valid authorization status
```js
const authz = { ... };
await client.waitForValidStatus(authz);
@@ -405,7 +405,7 @@ await client.waitForValidStatus(order);
### acmeClient.getCertificate(order, [preferredChain]) ⇒ Promise.<string>
Get certificate from ACME order
-https://tools.ietf.org/html/rfc8555#section-7.4.2
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
**Kind**: instance method of [AcmeClient
](#AcmeClient)
**Returns**: Promise.<string>
- Certificate
@@ -432,7 +432,7 @@ const certificate = await client.getCertificate(order, 'DST Root CA X3');
### acmeClient.revokeCertificate(cert, [data]) ⇒ Promise
Revoke certificate
-https://tools.ietf.org/html/rfc8555#section-7.6
+https://datatracker.ietf.org/doc/html/rfc8555#section-7.6
**Kind**: instance method of [AcmeClient
](#AcmeClient)
diff --git a/packages/core/acme-client/docs/crypto.md b/packages/core/acme-client/docs/crypto.md
index 1391263c..84660f68 100644
--- a/packages/core/acme-client/docs/crypto.md
+++ b/packages/core/acme-client/docs/crypto.md
@@ -25,7 +25,7 @@
Get a JSON Web Key derived from a RSA or ECDSA key
https://datatracker.ietf.org/doc/html/rfc7517
-splitPemChain(chainPem) ⇒ array
+splitPemChain(chainPem) ⇒ Array.<string>
Split chain of PEM encoded objects from string into array
getPemBodyAsB64u(pem) ⇒ string
@@ -42,6 +42,13 @@ If multiple certificates are chained, the first will be read
createCsr(data, [keyPem]) ⇒ Promise.<Array.<buffer>>
Create a Certificate Signing Request
+createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ Promise.<Array.<buffer>>
+Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
+https://datatracker.ietf.org/doc/html/rfc8737
+
+isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ boolean
+Validate that a ALPN certificate contains the expected key authorization
+
@@ -138,11 +145,11 @@ const jwk = acme.crypto.getJwk(privateKey);
```
-## splitPemChain(chainPem) ⇒ array
+## splitPemChain(chainPem) ⇒ Array.<string>
Split chain of PEM encoded objects from string into array
**Kind**: global function
-**Returns**: array
- Array of PEM objects including headers
+**Returns**: Array.<string>
- Array of PEM objects including headers
| Param | Type | Description |
| --- | --- | --- |
@@ -219,14 +226,14 @@ Create a Certificate Signing Request
| data | object
| |
| [data.keySize] | number
| Size of newly created RSA private key modulus in bits, default: `2048` |
| [data.commonName] | string
| FQDN of your server |
-| [data.altNames] | array
| SAN (Subject Alternative Names), default: `[]` |
+| [data.altNames] | Array.<string>
| SAN (Subject Alternative Names), default: `[]` |
| [data.country] | string
| 2 letter country code |
| [data.state] | string
| State or province |
| [data.locality] | string
| City |
| [data.organization] | string
| Organization name |
| [data.organizationUnit] | string
| Organizational unit name |
| [data.emailAddress] | string
| Email address |
-| [keyPem] | string
| PEM encoded CSR private key |
+| [keyPem] | buffer
\| string
| PEM encoded CSR private key |
**Example**
Create a Certificate Signing Request
@@ -265,3 +272,42 @@ const certificateKey = await acme.crypto.createPrivateEcdsaKey();
const [, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com'
}, certificateKey);
+
+
+## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ Promise.<Array.<buffer>>
+Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
+
+https://datatracker.ietf.org/doc/html/rfc8737
+
+**Kind**: global function
+**Returns**: Promise.<Array.<buffer>>
- [privateKey, certificate]
+
+| Param | Type | Description |
+| --- | --- | --- |
+| authz | object
| Identifier authorization |
+| keyAuthorization | string
| Challenge key authorization |
+| [keyPem] | buffer
\| string
| PEM encoded CSR private key |
+
+**Example**
+Create a ALPN certificate
+```js
+const [alpnKey, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
+```
+**Example**
+Create a ALPN certificate with ECDSA private key
+```js
+const alpnKey = await acme.crypto.createPrivateEcdsaKey();
+const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
+
+
+## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ boolean
+Validate that a ALPN certificate contains the expected key authorization
+
+**Kind**: global function
+**Returns**: boolean
- True when valid
+
+| Param | Type | Description |
+| --- | --- | --- |
+| certPem | buffer
\| string
| PEM encoded certificate |
+| keyAuthorization | string
| Expected challenge key authorization |
+
diff --git a/packages/core/acme-client/docs/forge.md b/packages/core/acme-client/docs/forge.md
index 2d7601fd..09a44de1 100644
--- a/packages/core/acme-client/docs/forge.md
+++ b/packages/core/acme-client/docs/forge.md
@@ -209,7 +209,7 @@ Create a Certificate Signing Request
| data | object
| |
| [data.keySize] | number
| Size of newly created private key, default: `2048` |
| [data.commonName] | string
| |
-| [data.altNames] | array
| default: `[]` |
+| [data.altNames] | Array.<string>
| default: `[]` |
| [data.country] | string
| |
| [data.state] | string
| |
| [data.locality] | string
| |
diff --git a/packages/core/acme-client/examples/dns-01/README.md b/packages/core/acme-client/examples/dns-01/README.md
new file mode 100644
index 00000000..4b55dc4c
--- /dev/null
+++ b/packages/core/acme-client/examples/dns-01/README.md
@@ -0,0 +1,21 @@
+# dns-01
+
+The greatest benefit of `dns-01` is that it is the only challenge type that can be used to issue ACME wildcard certificates, however it also has a few downsides. Your DNS provider needs to offer some sort of API you can use to automate adding and removing the required `TXT` DNS records. Additionally, solving DNS challenges will be much slower than the other challenge types because of DNS propagation delays.
+
+## How it works
+
+When solving `dns-01` challenges, you prove ownership of a domain by serving a specific payload within a specific DNS `TXT` record from the domains authoritative nameservers. The ACME authority provides the client with a token that, along with a thumbprint of your account key, is used to generate a `base64url` encoded `SHA256` digest. This payload is then placed as a `TXT` record under DNS name `_acme-challenge.$YOUR_DOMAIN`.
+
+Once the order is finalized, the ACME authority will lookup your domains DNS record to verify that the payload is correct. `CNAME` and `NS` records are followed, should you wish to delegate challenge response to another DNS zone or record.
+
+## Pros and cons
+
+* Only challenge type that can be used to issue wildcard certificates
+* Your DNS provider needs to supply an API that can be used
+* DNS propagation time may be slow
+* Useful in instances where both port 80 and 443 are unavailable
+
+## External links
+
+* [https://letsencrypt.org/docs/challenge-types/#dns-01-challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge)
+* [https://datatracker.ietf.org/doc/html/rfc8555#section-8.4](https://datatracker.ietf.org/doc/html/rfc8555#section-8.4)
diff --git a/packages/core/acme-client/examples/dns-01/dns-01.js b/packages/core/acme-client/examples/dns-01/dns-01.js
new file mode 100644
index 00000000..68bed46e
--- /dev/null
+++ b/packages/core/acme-client/examples/dns-01/dns-01.js
@@ -0,0 +1,92 @@
+/**
+ * Example using dns-01 challenge to generate certificates
+ *
+ * NOTE: This example is incomplete as the DNS challenge response implementation
+ * will be specific to your DNS providers API.
+ *
+ * NOTE: This example does not order certificates on-demand, as solving dns-01
+ * will likely be too slow for it to make sense. Instead, it orders a wildcard
+ * certificate on init before starting the HTTPS server as a demonstration.
+ */
+
+const https = require('https');
+const acme = require('./../../');
+
+const HTTPS_SERVER_PORT = 443;
+const WILDCARD_DOMAIN = 'example.com';
+
+function log(m) {
+ process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
+}
+
+
+/**
+ * Main
+ */
+
+(async () => {
+ try {
+ /**
+ * Initialize ACME client
+ */
+
+ log('Initializing ACME client');
+ const client = new acme.Client({
+ directoryUrl: acme.directory.letsencrypt.staging,
+ accountKey: await acme.crypto.createPrivateKey()
+ });
+
+
+ /**
+ * Order wildcard certificate
+ */
+
+ log(`Creating CSR for ${WILDCARD_DOMAIN}`);
+ const [key, csr] = await acme.crypto.createCsr({
+ commonName: WILDCARD_DOMAIN,
+ altNames: [`*.${WILDCARD_DOMAIN}`]
+ });
+
+ log(`Ordering certificate for ${WILDCARD_DOMAIN}`);
+ const cert = await client.auto({
+ csr,
+ email: 'test@example.com',
+ termsOfServiceAgreed: true,
+ challengePriority: ['dns-01'],
+ challengeCreateFn: (authz, challenge, keyAuthorization) => {
+ /* TODO: Implement this */
+ log(`[TODO] Add TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
+ },
+ challengeRemoveFn: (authz, challenge, keyAuthorization) => {
+ /* TODO: Implement this */
+ log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
+ }
+ });
+
+ log(`Certificate for ${WILDCARD_DOMAIN} created successfully`);
+
+
+ /**
+ * HTTPS server
+ */
+
+ const requestListener = (req, res) => {
+ log(`HTTP 200 ${req.headers.host}${req.url}`);
+ res.writeHead(200);
+ res.end('Hello world\n');
+ };
+
+ const httpsServer = https.createServer({
+ key,
+ cert
+ }, requestListener);
+
+ httpsServer.listen(HTTPS_SERVER_PORT, () => {
+ log(`HTTPS server listening on port ${HTTPS_SERVER_PORT}`);
+ });
+ }
+ catch (e) {
+ log(`[FATAL] ${e.message}`);
+ process.exit(1);
+ }
+})();
diff --git a/packages/core/acme-client/package.json b/packages/core/acme-client/package.json
index 3cf35551..a4aecc68 100644
--- a/packages/core/acme-client/package.json
+++ b/packages/core/acme-client/package.json
@@ -2,7 +2,7 @@
"name": "acme-client",
"description": "Simple and unopinionated ACME client",
"author": "nmorsman",
- "version": "5.2.0",
+ "version": "5.3.0",
"main": "src/index.js",
"types": "types/index.d.ts",
"license": "MIT",