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
array
Array.<string>
Split chain of PEM encoded objects from string into array
string
Promise.<Array.<buffer>>
Create a Certificate Signing Request
Promise.<Array.<buffer>>
Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
+ +boolean
Validate that a ALPN certificate contains the expected key authorization
+array
+## splitPemChain(chainPem) ⇒ Array.<string>
Split chain of PEM encoded objects from string into array
**Kind**: global function
-**Returns**: array
- Array of PEM objects including headers
+**Returns**: Array.<string>
- Array of PEM objects including headers
| Param | Type | Description |
| --- | --- | --- |
@@ -219,14 +226,14 @@ Create a Certificate Signing Request
| data | object
| |
| [data.keySize] | number
| Size of newly created RSA private key modulus in bits, default: `2048` |
| [data.commonName] | string
| FQDN of your server |
-| [data.altNames] | array
| SAN (Subject Alternative Names), default: `[]` |
+| [data.altNames] | Array.<string>
| SAN (Subject Alternative Names), default: `[]` |
| [data.country] | string
| 2 letter country code |
| [data.state] | string
| State or province |
| [data.locality] | string
| City |
| [data.organization] | string
| Organization name |
| [data.organizationUnit] | string
| Organizational unit name |
| [data.emailAddress] | string
| Email address |
-| [keyPem] | string
| PEM encoded CSR private key |
+| [keyPem] | buffer
\| string
| PEM encoded CSR private key |
**Example**
Create a Certificate Signing Request
@@ -265,3 +272,42 @@ const certificateKey = await acme.crypto.createPrivateEcdsaKey();
const [, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com'
}, certificateKey);
+
+
+## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ Promise.<Array.<buffer>>
+Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
+
+https://datatracker.ietf.org/doc/html/rfc8737
+
+**Kind**: global function
+**Returns**: Promise.<Array.<buffer>>
- [privateKey, certificate]
+
+| Param | Type | Description |
+| --- | --- | --- |
+| authz | object
| Identifier authorization |
+| keyAuthorization | string
| Challenge key authorization |
+| [keyPem] | buffer
\| string
| PEM encoded CSR private key |
+
+**Example**
+Create a ALPN certificate
+```js
+const [alpnKey, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
+```
+**Example**
+Create a ALPN certificate with ECDSA private key
+```js
+const alpnKey = await acme.crypto.createPrivateEcdsaKey();
+const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
+
+
+## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ boolean
+Validate that a ALPN certificate contains the expected key authorization
+
+**Kind**: global function
+**Returns**: boolean
- True when valid
+
+| Param | Type | Description |
+| --- | --- | --- |
+| certPem | buffer
\| string
| PEM encoded certificate |
+| keyAuthorization | string
| Expected challenge key authorization |
+
diff --git a/packages/core/acme-client/docs/forge.md b/packages/core/acme-client/docs/forge.md
index 2d7601fd..09a44de1 100644
--- a/packages/core/acme-client/docs/forge.md
+++ b/packages/core/acme-client/docs/forge.md
@@ -209,7 +209,7 @@ Create a Certificate Signing Request
| data | object
| |
| [data.keySize] | number
| Size of newly created private key, default: `2048` |
| [data.commonName] | string
| |
-| [data.altNames] | array
| default: `[]` |
+| [data.altNames] | Array.<string>
| default: `[]` |
| [data.country] | string
| |
| [data.state] | string
| |
| [data.locality] | string
| |
diff --git a/packages/core/acme-client/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",