Compare commits

...

5 Commits

Author SHA1 Message Date
GitHub Actions Bot
e5edfbfa6d 🔱: [acme] sync upgrade with 6 commits [trident-sync]
Bump v5.4.0
Bump dependencies
Retry HTTP requests on server errors or when rate limited
Forgot to refresh directory timestamp after successful get
Add utility method tests
2024-07-16 19:24:08 +00:00
GitHub Actions Bot
86e64af35c 🔱: [acme] sync upgrade with 5 commits [trident-sync]
Temp remove Node v22 from matrix, broke CNAME tests
Invalidate ACME directory cache after 24 hours
Directory URLs for Google ACME provider
Bump Pebble v2.6.0
2024-07-15 19:24:17 +00:00
GitHub Actions Bot
162e10909b 🔱: [acme] sync upgrade with 7 commits [trident-sync]
Small crypto docs fix 2
Small crypto docs fix
Bump v5.3.1
Discourage use of cert subject common name, examples and docs
Style refactor docs and examples
Bump dependencies
2024-05-23 19:24:12 +00:00
GitHub Actions Bot
0f1ae6ccd9 🔱: [acme] sync upgrade with 3 commits [trident-sync]
Clean up eslintrc, style refactor and formatting fixes
Update auto.js

see https://github.com/publishlab/node-acme-client/issues/88#issuecomment-2105255828
2024-05-22 19:24:07 +00:00
GitHub Actions Bot
c9d5cda953 🔱: [acme] sync upgrade with 7 commits [trident-sync]
Add Node v22 to test matrix
Postpone Pebble bump, v2.5.1 broke EAB tests
Bump Pebble v2.5.1
Allow client.auto() being called with an empty CSR common name
Carry EAB over to new HttpClient when updating account key
Ignore actrc
2024-05-21 19:24:05 +00:00
44 changed files with 687 additions and 569 deletions

View File

@@ -9,15 +9,8 @@ env:
rules: rules:
indent: [2, 4, { SwitchCase: 1, VariableDeclarator: 1 }] indent: [2, 4, { SwitchCase: 1, VariableDeclarator: 1 }]
brace-style: [2, 'stroustrup', { allowSingleLine: true }] brace-style: [2, 'stroustrup', { allowSingleLine: true }]
space-before-function-paren: [2, { anonymous: 'never', named: 'never' }]
func-names: 0 func-names: 0
prefer-destructuring: 0
object-curly-newline: 0
class-methods-use-this: 0 class-methods-use-this: 0
wrap-iife: [2, 'inside']
no-param-reassign: 0 no-param-reassign: 0
comma-dangle: [2, 'never']
max-len: [1, 200, 2, { ignoreUrls: true, ignoreComments: false }] max-len: [1, 200, 2, { ignoreUrls: true, ignoreComments: false }]
no-multiple-empty-lines: [2, { max: 2, maxBOF: 0, maxEOF: 0 }]
prefer-object-spread: 0
import/no-useless-path-segments: 0 import/no-useless-path-segments: 0

View File

@@ -5,8 +5,10 @@
set -euo pipefail set -euo pipefail
# Download and install # 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 wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLECTS_VERSION}/pebble-challtestsrv-linux-amd64.tar.gz" -O /tmp/pebble-challtestsrv.tar.gz
tar zxvf /tmp/pebble-challtestsrv.tar.gz -C /tmp
mv /tmp/pebble-challtestsrv-linux-amd64/linux/amd64/pebble-challtestsrv /usr/local/bin/pebble-challtestsrv
chown root:root /usr/local/bin/pebble-challtestsrv chown root:root /usr/local/bin/pebble-challtestsrv
chmod 0755 /usr/local/bin/pebble-challtestsrv chmod 0755 /usr/local/bin/pebble-challtestsrv

View File

@@ -22,12 +22,14 @@ wget -nv "https://raw.githubusercontent.com/letsencrypt/pebble/v${PEBBLE_VERSION
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 # Download and install Pebble
wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLE_VERSION}/pebble_linux-amd64" -O /usr/local/bin/pebble wget -nv "https://github.com/letsencrypt/pebble/releases/download/v${PEBBLE_VERSION}/pebble-linux-amd64.tar.gz" -O /tmp/pebble.tar.gz
tar zxvf /tmp/pebble.tar.gz -C /tmp
mv /tmp/pebble-linux-amd64/linux/amd64/pebble /usr/local/bin/pebble
chown root:root /usr/local/bin/pebble chown root:root /usr/local/bin/pebble
chmod 0755 /usr/local/bin/pebble chmod 0755 /usr/local/bin/pebble
# Config # Config
sed -i 's/test\/certs\/localhost/\/etc\/pebble/' /etc/pebble/pebble.json sed -i 's#test/certs/localhost#/etc/pebble#' /etc/pebble/pebble.json
exit 0 exit 0

View File

@@ -1,4 +1,3 @@
---
name: test name: test
on: [push, pull_request] on: [push, pull_request]
@@ -12,7 +11,6 @@ jobs:
node: [16, 18, 20] node: [16, 18, 20]
eab: [0, 1] eab: [0, 1]
# #
# Environment # Environment
# #
@@ -21,9 +19,9 @@ jobs:
FORCE_COLOR: 1 FORCE_COLOR: 1
NPM_CONFIG_COLOR: always NPM_CONFIG_COLOR: always
PEBBLE_VERSION: 2.3.1 PEBBLE_VERSION: 2.6.0
PEBBLE_ALTERNATE_ROOTS: 2 PEBBLE_ALTERNATE_ROOTS: 2
PEBBLECTS_VERSION: 2.3.1 PEBBLECTS_VERSION: 2.6.0
PEBBLECTS_DNS_PORT: 8053 PEBBLECTS_DNS_PORT: 8053
COREDNS_VERSION: 1.11.1 COREDNS_VERSION: 1.11.1
@@ -41,7 +39,6 @@ jobs:
ACME_HTTP_PORT: 5002 ACME_HTTP_PORT: 5002
ACME_HTTPS_PORT: 5003 ACME_HTTPS_PORT: 5003
# #
# Pipeline # Pipeline
# #

View File

@@ -1,3 +1,4 @@
.actrc
.vscode/ .vscode/
node_modules/ node_modules/
npm-debug.log npm-debug.log

View File

@@ -1,9 +1,20 @@
# Changelog # Changelog
## v5.4.0 (2024-07-16)
* `added` Directory URLs for [Google](https://cloud.google.com/certificate-manager/docs/overview) ACME provider
* `fixed` Invalidate ACME provider directory cache after 24 hours
* `fixed` Retry HTTP requests on server errors or when rate limited - [#89](https://github.com/publishlab/node-acme-client/issues/89)
## v5.3.1 (2024-05-22)
* `fixed` Allow `client.auto()` being called with an empty CSR common name
* `fixed` Bug when calling `updateAccountKey()` with external account binding
## v5.3.0 (2024-02-05) ## v5.3.0 (2024-02-05)
* `added` Support and tests for satisfying `tls-alpn-01` challenges * `added` Support and tests for satisfying `tls-alpn-01` challenges
* `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR generation and parsing * `changed` Replace `jsrsasign` with `@peculiar/x509` for certificate and CSR handling
* `changed` Method `getChallengeKeyAuthorization()` now returns `$token.$thumbprint` when called with a `tls-alpn-01` challenge * `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 * Previously returned base64url encoded SHA256 digest of `$token.$thumbprint` erroneously
* This change is not considered breaking since the previous behavior was incorrect * This change is not considered breaking since the previous behavior was incorrect
@@ -32,7 +43,7 @@
* `fixed` Upgrade `axios@0.26.1` * `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) * `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) ## v4.2.4 (2022-03-19)
* `fixed` Use SHA-256 when signing CSRs * `fixed` Use SHA-256 when signing CSRs

View File

@@ -10,7 +10,7 @@ This module is written to handle communication with a Boulder/Let's Encrypt-styl
## Compatibility ## Compatibility
| acme-client | Node.js | | | acme-client | Node.js | |
| ------------- | --------- | ----------------------------------------- | | ----------- | ------- | ----------------------------------------- |
| v5.x | >= v16 | [Upgrade guide](docs/upgrade-v5.md) | | v5.x | >= v16 | [Upgrade guide](docs/upgrade-v5.md) |
| v4.x | >= v10 | [Changelog](CHANGELOG.md#v400-2020-05-29) | | v4.x | >= v10 | [Changelog](CHANGELOG.md#v400-2020-05-29) |
| v3.x | >= v8 | [Changelog](CHANGELOG.md#v300-2019-07-13) | | v3.x | >= v8 | [Changelog](CHANGELOG.md#v300-2019-07-13) |
@@ -49,7 +49,7 @@ const accountPrivateKey = '<PEM encoded private key>';
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging, directoryUrl: acme.directory.letsencrypt.staging,
accountKey: accountPrivateKey accountKey: accountPrivateKey,
}); });
``` ```
@@ -59,6 +59,9 @@ const client = new acme.Client({
acme.directory.buypass.staging; acme.directory.buypass.staging;
acme.directory.buypass.production; acme.directory.buypass.production;
acme.directory.google.staging;
acme.directory.google.production;
acme.directory.letsencrypt.staging; acme.directory.letsencrypt.staging;
acme.directory.letsencrypt.production; acme.directory.letsencrypt.production;
@@ -75,8 +78,8 @@ const client = new acme.Client({
accountKey: accountPrivateKey, accountKey: accountPrivateKey,
externalAccountBinding: { externalAccountBinding: {
kid: 'YOUR-EAB-KID', kid: 'YOUR-EAB-KID',
hmacKey: 'YOUR-EAB-HMAC-KEY' hmacKey: 'YOUR-EAB-HMAC-KEY',
} },
}); });
``` ```
@@ -90,7 +93,7 @@ In some cases, for example with some EAB providers, this account creation step m
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging, directoryUrl: acme.directory.letsencrypt.staging,
accountKey: accountPrivateKey, accountKey: accountPrivateKey,
accountUrl: 'https://acme-v02.api.letsencrypt.org/acme/acct/12345678' accountUrl: 'https://acme-v02.api.letsencrypt.org/acme/acct/12345678',
}); });
``` ```
@@ -113,8 +116,7 @@ const privateRsaKey = await acme.crypto.createPrivateRsaKey();
const privateEcdsaKey = await acme.crypto.createPrivateEcdsaKey(); const privateEcdsaKey = await acme.crypto.createPrivateEcdsaKey();
const [certificateKey, certificateCsr] = await acme.crypto.createCsr({ const [certificateKey, certificateCsr] = await acme.crypto.createCsr({
commonName: '*.example.com', altNames: ['example.com', '*.example.com'],
altNames: ['example.com']
}); });
``` ```
@@ -139,7 +141,7 @@ const autoOpts = {
email: 'test@example.com', email: 'test@example.com',
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: async (authz, challenge, keyAuthorization) => {}, challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
challengeRemoveFn: async (authz, challenge, keyAuthorization) => {} challengeRemoveFn: async (authz, challenge, keyAuthorization) => {},
}; };
const certificate = await client.auto(autoOpts); const certificate = await client.auto(autoOpts);
@@ -156,7 +158,7 @@ To modify challenge priority, provide a list of challenge types in `challengePri
```js ```js
await client.auto({ await client.auto({
..., ...,
challengePriority: ['http-01', 'dns-01'] challengePriority: ['http-01', 'dns-01'],
}); });
``` ```
@@ -171,7 +173,7 @@ To completely disable `acme-client`s internal challenge verification, enable `sk
```js ```js
await client.auto({ await client.auto({
..., ...,
skipChallengeVerification: true skipChallengeVerification: true,
}); });
``` ```
@@ -185,14 +187,14 @@ For more fine-grained control you can interact with the ACME API using the metho
```js ```js
const account = await client.createAccount({ const account = await client.createAccount({
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
contact: ['mailto:test@example.com'] contact: ['mailto:test@example.com'],
}); });
const order = await client.createOrder({ const order = await client.createOrder({
identifiers: [ identifiers: [
{ type: 'dns', value: 'example.com' }, { type: 'dns', value: 'example.com' },
{ type: 'dns', value: '*.example.com' } { type: 'dns', value: '*.example.com' },
] ],
}); });
``` ```
@@ -207,7 +209,7 @@ const acme = require('acme-client');
acme.axios.defaults.proxy = { acme.axios.defaults.proxy = {
host: '127.0.0.1', host: '127.0.0.1',
port: 9000 port: 9000,
}; };
``` ```

View File

@@ -63,7 +63,7 @@ Create ACME client instance
```js ```js
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging, directoryUrl: acme.directory.letsencrypt.staging,
accountKey: 'Private key goes here' accountKey: 'Private key goes here',
}); });
``` ```
**Example** **Example**
@@ -75,7 +75,7 @@ const client = new acme.Client({
accountUrl: 'Optional account URL goes here', accountUrl: 'Optional account URL goes here',
backoffAttempts: 10, backoffAttempts: 10,
backoffMin: 5000, backoffMin: 5000,
backoffMax: 30000 backoffMax: 30000,
}); });
``` ```
**Example** **Example**
@@ -86,8 +86,8 @@ const client = new acme.Client({
accountKey: 'Private key goes here', accountKey: 'Private key goes here',
externalAccountBinding: { externalAccountBinding: {
kid: 'YOUR-EAB-KID', kid: 'YOUR-EAB-KID',
hmacKey: 'YOUR-EAB-HMAC-KEY' hmacKey: 'YOUR-EAB-HMAC-KEY',
} },
}); });
``` ```
<a name="AcmeClient+getTermsOfServiceUrl"></a> <a name="AcmeClient+getTermsOfServiceUrl"></a>
@@ -145,7 +145,7 @@ https://datatracker.ietf.org/doc/html/rfc8555#section-7.3
Create a new account Create a new account
```js ```js
const account = await client.createAccount({ const account = await client.createAccount({
termsOfServiceAgreed: true termsOfServiceAgreed: true,
}); });
``` ```
**Example** **Example**
@@ -153,7 +153,7 @@ Create a new account with contact info
```js ```js
const account = await client.createAccount({ const account = await client.createAccount({
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
contact: ['mailto:test@example.com'] contact: ['mailto:test@example.com'],
}); });
``` ```
<a name="AcmeClient+updateAccount"></a> <a name="AcmeClient+updateAccount"></a>
@@ -174,7 +174,7 @@ https://datatracker.ietf.org/doc/html/rfc8555#section-7.3.2
Update existing account Update existing account
```js ```js
const account = await client.updateAccount({ const account = await client.updateAccount({
contact: ['mailto:foo@example.com'] contact: ['mailto:foo@example.com'],
}); });
``` ```
<a name="AcmeClient+updateAccountKey"></a> <a name="AcmeClient+updateAccountKey"></a>
@@ -218,8 +218,8 @@ Create a new order
const order = await client.createOrder({ const order = await client.createOrder({
identifiers: [ identifiers: [
{ type: 'dns', value: 'example.com' }, { type: 'dns', value: 'example.com' },
{ type: 'dns', value: 'test.example.com' } { type: 'dns', value: 'test.example.com' },
] ],
}); });
``` ```
<a name="AcmeClient+getOrder"></a> <a name="AcmeClient+getOrder"></a>
@@ -452,7 +452,7 @@ Revoke certificate with reason
```js ```js
const certificate = { ... }; // Previously created certificate const certificate = { ... }; // Previously created certificate
const result = await client.revokeCertificate(certificate, { const result = await client.revokeCertificate(certificate, {
reason: 4 reason: 4,
}); });
``` ```
<a name="AcmeClient+auto"></a> <a name="AcmeClient+auto"></a>
@@ -479,7 +479,7 @@ Auto mode
Order a certificate using auto mode Order a certificate using auto mode
```js ```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com' altNames: ['test.example.com'],
}); });
const certificate = await client.auto({ const certificate = await client.auto({
@@ -491,14 +491,14 @@ const certificate = await client.auto({
}, },
challengeRemoveFn: async (authz, challenge, keyAuthorization) => { challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
// Clean up challenge here // Clean up challenge here
} },
}); });
``` ```
**Example** **Example**
Order a certificate using auto mode with preferred chain Order a certificate using auto mode with preferred chain
```js ```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com' altNames: ['test.example.com'],
}); });
const certificate = await client.auto({ const certificate = await client.auto({
@@ -507,7 +507,7 @@ const certificate = await client.auto({
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
preferredChain: 'DST Root CA X3', preferredChain: 'DST Root CA X3',
challengeCreateFn: async () => {}, challengeCreateFn: async () => {},
challengeRemoveFn: async () => {} challengeRemoveFn: async () => {},
}); });
``` ```
<a name="Client"></a> <a name="Client"></a>

View File

@@ -239,29 +239,30 @@ Create a Certificate Signing Request
Create a Certificate Signing Request Create a Certificate Signing Request
```js ```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com' altNames: ['test.example.com'],
}); });
``` ```
**Example** **Example**
Certificate Signing Request with both common and alternative names Certificate Signing Request with both common and alternative names
> *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
```js ```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
keySize: 4096, keySize: 4096,
commonName: 'test.example.com', commonName: 'test.example.com',
altNames: ['foo.example.com', 'bar.example.com'] altNames: ['foo.example.com', 'bar.example.com'],
}); });
``` ```
**Example** **Example**
Certificate Signing Request with additional information Certificate Signing Request with additional information
```js ```js
const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com', altNames: ['test.example.com'],
country: 'US', country: 'US',
state: 'California', state: 'California',
locality: 'Los Angeles', locality: 'Los Angeles',
organization: 'The Company Inc.', organization: 'The Company Inc.',
organizationUnit: 'IT Department', organizationUnit: 'IT Department',
emailAddress: 'contact@example.com' emailAddress: 'contact@example.com',
}); });
``` ```
**Example** **Example**
@@ -270,8 +271,9 @@ Certificate Signing Request with ECDSA private key
const certificateKey = await acme.crypto.createPrivateEcdsaKey(); const certificateKey = await acme.crypto.createPrivateEcdsaKey();
const [, certificateRequest] = await acme.crypto.createCsr({ const [, certificateRequest] = await acme.crypto.createCsr({
commonName: 'test.example.com' altNames: ['test.example.com'],
}, certificateKey); }, certificateKey);
```
<a name="createAlpnCertificate"></a> <a name="createAlpnCertificate"></a>
## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ <code>Promise.&lt;Array.&lt;buffer&gt;&gt;</code> ## createAlpnCertificate(authz, keyAuthorization, [keyPem]) ⇒ <code>Promise.&lt;Array.&lt;buffer&gt;&gt;</code>
@@ -298,6 +300,7 @@ Create a ALPN certificate with ECDSA private key
```js ```js
const alpnKey = await acme.crypto.createPrivateEcdsaKey(); const alpnKey = await acme.crypto.createPrivateEcdsaKey();
const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey); const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
```
<a name="isAlpnCertificateAuthorizationValid"></a> <a name="isAlpnCertificateAuthorizationValid"></a>
## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ <code>boolean</code> ## isAlpnCertificateAuthorizationValid(certPem, keyAuthorization) ⇒ <code>boolean</code>

View File

@@ -222,29 +222,30 @@ Create a Certificate Signing Request
Create a Certificate Signing Request Create a Certificate Signing Request
```js ```js
const [certificateKey, certificateRequest] = await acme.forge.createCsr({ const [certificateKey, certificateRequest] = await acme.forge.createCsr({
commonName: 'test.example.com' altNames: ['test.example.com'],
}); });
``` ```
**Example** **Example**
Certificate Signing Request with both common and alternative names Certificate Signing Request with both common and alternative names
> *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
```js ```js
const [certificateKey, certificateRequest] = await acme.forge.createCsr({ const [certificateKey, certificateRequest] = await acme.forge.createCsr({
keySize: 4096, keySize: 4096,
commonName: 'test.example.com', commonName: 'test.example.com',
altNames: ['foo.example.com', 'bar.example.com'] altNames: ['foo.example.com', 'bar.example.com'],
}); });
``` ```
**Example** **Example**
Certificate Signing Request with additional information Certificate Signing Request with additional information
```js ```js
const [certificateKey, certificateRequest] = await acme.forge.createCsr({ const [certificateKey, certificateRequest] = await acme.forge.createCsr({
commonName: 'test.example.com', altNames: ['test.example.com'],
country: 'US', country: 'US',
state: 'California', state: 'California',
locality: 'Los Angeles', locality: 'Los Angeles',
organization: 'The Company Inc.', organization: 'The Company Inc.',
organizationUnit: 'IT Department', organizationUnit: 'IT Department',
emailAddress: 'contact@example.com' emailAddress: 'contact@example.com',
}); });
``` ```
**Example** **Example**
@@ -253,5 +254,5 @@ Certificate Signing Request with predefined private key
const certificateKey = await acme.forge.createPrivateKey(); const certificateKey = await acme.forge.createPrivateKey();
const [, certificateRequest] = await acme.forge.createCsr({ const [, certificateRequest] = await acme.forge.createCsr({
commonName: 'test.example.com' altNames: ['test.example.com'],
}, certificateKey); }, certificateKey);

View File

@@ -8,7 +8,6 @@ function log(m) {
process.stdout.write(`${m}\n`); process.stdout.write(`${m}\n`);
} }
/** /**
* Function used to satisfy an ACME challenge * Function used to satisfy an ACME challenge
* *
@@ -25,7 +24,6 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
log(keyAuthorization); log(keyAuthorization);
} }
/** /**
* Function used to remove an ACME challenge response * Function used to remove an ACME challenge response
* *
@@ -41,30 +39,29 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
log(keyAuthorization); log(keyAuthorization);
} }
/** /**
* Main * Main
*/ */
module.exports = async function() { module.exports = async () => {
/* Init client */ /* Init client */
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging, directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey() accountKey: await acme.crypto.createPrivateKey(),
}); });
/* Register account */ /* Register account */
await client.createAccount({ await client.createAccount({
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
contact: ['mailto:test@example.com'] contact: ['mailto:test@example.com'],
}); });
/* Place new order */ /* Place new order */
const order = await client.createOrder({ const order = await client.createOrder({
identifiers: [ identifiers: [
{ type: 'dns', value: 'example.com' }, { type: 'dns', value: 'example.com' },
{ type: 'dns', value: '*.example.com' } { type: 'dns', value: '*.example.com' },
] ],
}); });
/** /**
@@ -138,8 +135,7 @@ module.exports = async function() {
/* Finalize order */ /* Finalize order */
const [key, csr] = await acme.crypto.createCsr({ const [key, csr] = await acme.crypto.createCsr({
commonName: '*.example.com', altNames: ['example.com', '*.example.com'],
altNames: ['example.com']
}); });
const finalized = await client.finalizeOrder(order, csr); const finalized = await client.finalizeOrder(order, csr);

View File

@@ -9,7 +9,6 @@ function log(m) {
process.stdout.write(`${m}\n`); process.stdout.write(`${m}\n`);
} }
/** /**
* Function used to satisfy an ACME challenge * Function used to satisfy an ACME challenge
* *
@@ -47,7 +46,6 @@ async function challengeCreateFn(authz, challenge, keyAuthorization) {
} }
} }
/** /**
* Function used to remove an ACME challenge response * Function used to remove an ACME challenge response
* *
@@ -80,25 +78,24 @@ async function challengeRemoveFn(authz, challenge, keyAuthorization) {
/* Replace this */ /* Replace this */
log(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`); log(`Would remove TXT record "${dnsRecord}" with value "${recordValue}"`);
// await dnsProvider.removeRecord(dnsRecord, 'TXT'); // await dnsProvider.removeRecord(dnsRecord, 'TXT', recordValue);
} }
} }
/** /**
* Main * Main
*/ */
module.exports = async function() { module.exports = async () => {
/* Init client */ /* Init client */
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging, directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey() accountKey: await acme.crypto.createPrivateKey(),
}); });
/* Create CSR */ /* Create CSR */
const [key, csr] = await acme.crypto.createCsr({ const [key, csr] = await acme.crypto.createCsr({
commonName: 'example.com' altNames: ['example.com'],
}); });
/* Certificate */ /* Certificate */
@@ -107,7 +104,7 @@ module.exports = async function() {
email: 'test@example.com', email: 'test@example.com',
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn, challengeCreateFn,
challengeRemoveFn challengeRemoveFn,
}); });
/* Done */ /* Done */

View File

@@ -19,7 +19,6 @@ function log(m) {
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`); process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
} }
/** /**
* Main * Main
*/ */
@@ -33,18 +32,16 @@ function log(m) {
log('Initializing ACME client'); log('Initializing ACME client');
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging, directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey() accountKey: await acme.crypto.createPrivateKey(),
}); });
/** /**
* Order wildcard certificate * Order wildcard certificate
*/ */
log(`Creating CSR for ${WILDCARD_DOMAIN}`); log(`Creating CSR for ${WILDCARD_DOMAIN}`);
const [key, csr] = await acme.crypto.createCsr({ const [key, csr] = await acme.crypto.createCsr({
commonName: WILDCARD_DOMAIN, altNames: [WILDCARD_DOMAIN, `*.${WILDCARD_DOMAIN}`],
altNames: [`*.${WILDCARD_DOMAIN}`]
}); });
log(`Ordering certificate for ${WILDCARD_DOMAIN}`); log(`Ordering certificate for ${WILDCARD_DOMAIN}`);
@@ -60,12 +57,11 @@ function log(m) {
challengeRemoveFn: (authz, challenge, keyAuthorization) => { challengeRemoveFn: (authz, challenge, keyAuthorization) => {
/* TODO: Implement this */ /* TODO: Implement this */
log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`); log(`[TODO] Remove TXT record key=_acme-challenge.${authz.identifier.value} value=${keyAuthorization}`);
} },
}); });
log(`Certificate for ${WILDCARD_DOMAIN} created successfully`); log(`Certificate for ${WILDCARD_DOMAIN} created successfully`);
/** /**
* HTTPS server * HTTPS server
*/ */
@@ -78,7 +74,7 @@ function log(m) {
const httpsServer = https.createServer({ const httpsServer = https.createServer({
key, key,
cert cert,
}, requestListener); }, requestListener);
httpsServer.listen(HTTPS_SERVER_PORT, () => { httpsServer.listen(HTTPS_SERVER_PORT, () => {

View File

@@ -23,7 +23,6 @@ function log(m) {
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`); process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
} }
/** /**
* On-demand certificate generation using http-01 * On-demand certificate generation using http-01
*/ */
@@ -52,7 +51,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
/* Create CSR */ /* Create CSR */
log(`Creating CSR for ${servername}`); log(`Creating CSR for ${servername}`);
const [key, csr] = await acme.crypto.createCsr({ const [key, csr] = await acme.crypto.createCsr({
commonName: servername altNames: [servername],
}); });
/* Order certificate */ /* Order certificate */
@@ -67,7 +66,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
}, },
challengeRemoveFn: (authz, challenge) => { challengeRemoveFn: (authz, challenge) => {
delete challengeResponses[challenge.token]; delete challengeResponses[challenge.token];
} },
}); });
/* Done, store certificate */ /* Done, store certificate */
@@ -77,7 +76,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
return certificateStore[servername]; return certificateStore[servername];
} }
/** /**
* Main * Main
*/ */
@@ -91,10 +89,9 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log('Initializing ACME client'); log('Initializing ACME client');
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging, directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey() accountKey: await acme.crypto.createPrivateKey(),
}); });
/** /**
* HTTP server * HTTP server
*/ */
@@ -129,7 +126,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`HTTP server listening on port ${HTTP_SERVER_PORT}`); log(`HTTP server listening on port ${HTTP_SERVER_PORT}`);
}); });
/** /**
* HTTPS server * HTTPS server
*/ */
@@ -158,7 +154,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`[ERROR] ${e.message}`); log(`[ERROR] ${e.message}`);
cb(e.message); cb(e.message);
} }
} },
}, requestListener); }, requestListener);
httpsServer.listen(HTTPS_SERVER_PORT, () => { httpsServer.listen(HTTPS_SERVER_PORT, () => {

View File

@@ -22,7 +22,6 @@ function log(m) {
process.stdout.write(`${(new Date()).toISOString()} ${m}\n`); process.stdout.write(`${(new Date()).toISOString()} ${m}\n`);
} }
/** /**
* On-demand certificate generation using tls-alpn-01 * On-demand certificate generation using tls-alpn-01
*/ */
@@ -51,7 +50,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
/* Create CSR */ /* Create CSR */
log(`Creating CSR for ${servername}`); log(`Creating CSR for ${servername}`);
const [key, csr] = await acme.crypto.createCsr({ const [key, csr] = await acme.crypto.createCsr({
commonName: servername altNames: [servername],
}); });
/* Order certificate */ /* Order certificate */
@@ -66,7 +65,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
}, },
challengeRemoveFn: (authz) => { challengeRemoveFn: (authz) => {
delete alpnResponses[authz.identifier.value]; delete alpnResponses[authz.identifier.value];
} },
}); });
/* Done, store certificate */ /* Done, store certificate */
@@ -76,7 +75,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
return certificateStore[servername]; return certificateStore[servername];
} }
/** /**
* Main * Main
*/ */
@@ -90,10 +88,9 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log('Initializing ACME client'); log('Initializing ACME client');
const client = new acme.Client({ const client = new acme.Client({
directoryUrl: acme.directory.letsencrypt.staging, directoryUrl: acme.directory.letsencrypt.staging,
accountKey: await acme.crypto.createPrivateKey() accountKey: await acme.crypto.createPrivateKey(),
}); });
/** /**
* ALPN responder * ALPN responder
*/ */
@@ -118,14 +115,14 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`Found ALPN certificate for ${servername}, serving secure context`); log(`Found ALPN certificate for ${servername}, serving secure context`);
cb(null, tls.createSecureContext({ cb(null, tls.createSecureContext({
key: alpnResponses[servername][0], key: alpnResponses[servername][0],
cert: alpnResponses[servername][1] cert: alpnResponses[servername][1],
})); }));
} }
catch (e) { catch (e) {
log(`[ERROR] ${e.message}`); log(`[ERROR] ${e.message}`);
cb(e.message); cb(e.message);
} }
} },
}); });
/* Terminate once TLS handshake has been established */ /* Terminate once TLS handshake has been established */
@@ -137,7 +134,6 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`); log(`ALPN responder listening on port ${ALPN_RESPONDER_PORT}`);
}); });
/** /**
* HTTPS server * HTTPS server
*/ */
@@ -166,7 +162,7 @@ async function getCertOnDemand(client, servername, attempt = 0) {
log(`[ERROR] ${e.message}`); log(`[ERROR] ${e.message}`);
cb(e.message); cb(e.message);
} }
} },
}, requestListener); }, requestListener);
httpsServer.listen(HTTPS_SERVER_PORT, () => { httpsServer.listen(HTTPS_SERVER_PORT, () => {

View File

@@ -2,7 +2,7 @@
"name": "acme-client", "name": "acme-client",
"description": "Simple and unopinionated ACME client", "description": "Simple and unopinionated ACME client",
"author": "nmorsman", "author": "nmorsman",
"version": "5.3.0", "version": "5.4.0",
"main": "src/index.js", "main": "src/index.js",
"types": "types/index.d.ts", "types": "types/index.d.ts",
"license": "MIT", "license": "MIT",
@@ -15,23 +15,23 @@
"types" "types"
], ],
"dependencies": { "dependencies": {
"@peculiar/x509": "^1.9.7", "@peculiar/x509": "^1.11.0",
"asn1js": "^3.0.5", "asn1js": "^3.0.5",
"axios": "^1.6.5", "axios": "^1.7.2",
"debug": "^4.1.1", "debug": "^4.3.5",
"node-forge": "^1.3.1" "node-forge": "^1.3.1"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.11.5", "@types/node": "^20.14.10",
"chai": "^4.4.1", "chai": "^4.4.1",
"chai-as-promised": "^7.1.1", "chai-as-promised": "^7.1.2",
"eslint": "^8.56.0", "eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-base": "^15.0.0",
"eslint-plugin-import": "^2.29.1", "eslint-plugin-import": "^2.29.1",
"jsdoc-to-markdown": "^8.0.0", "jsdoc-to-markdown": "^8.0.1",
"mocha": "^10.2.0", "mocha": "^10.6.0",
"nock": "^13.5.0", "nock": "^13.5.4",
"tsd": "^0.30.4" "tsd": "^0.31.1"
}, },
"scripts": { "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", "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",

View File

@@ -4,7 +4,6 @@
const util = require('./util'); const util = require('./util');
/** /**
* AcmeApi * AcmeApi
* *
@@ -18,7 +17,6 @@ class AcmeApi {
this.accountUrl = accountUrl; this.accountUrl = accountUrl;
} }
/** /**
* Get account URL * Get account URL
* *
@@ -34,7 +32,6 @@ class AcmeApi {
return this.accountUrl; return this.accountUrl;
} }
/** /**
* ACME API request * ACME API request
* *
@@ -59,7 +56,6 @@ class AcmeApi {
return resp; return resp;
} }
/** /**
* ACME API request by resource name helper * ACME API request by resource name helper
* *
@@ -78,7 +74,6 @@ class AcmeApi {
return this.apiRequest(resourceUrl, payload, validStatusCodes, { includeJwsKid, includeExternalAccountBinding }); return this.apiRequest(resourceUrl, payload, validStatusCodes, { includeJwsKid, includeExternalAccountBinding });
} }
/** /**
* Get Terms of Service URL if available * Get Terms of Service URL if available
* *
@@ -91,7 +86,6 @@ class AcmeApi {
return this.http.getMetaField('termsOfService'); return this.http.getMetaField('termsOfService');
} }
/** /**
* Create new account * Create new account
* *
@@ -104,7 +98,7 @@ class AcmeApi {
async createAccount(data) { async createAccount(data) {
const resp = await this.apiResourceRequest('newAccount', data, [200, 201], { const resp = await this.apiResourceRequest('newAccount', data, [200, 201], {
includeJwsKid: false, includeJwsKid: false,
includeExternalAccountBinding: (data.onlyReturnExisting !== true) includeExternalAccountBinding: (data.onlyReturnExisting !== true),
}); });
/* Set account URL */ /* Set account URL */
@@ -115,7 +109,6 @@ class AcmeApi {
return resp; return resp;
} }
/** /**
* Update account * Update account
* *
@@ -129,7 +122,6 @@ class AcmeApi {
return this.apiRequest(this.getAccountUrl(), data, [200, 202]); return this.apiRequest(this.getAccountUrl(), data, [200, 202]);
} }
/** /**
* Update account key * Update account key
* *
@@ -143,7 +135,6 @@ class AcmeApi {
return this.apiResourceRequest('keyChange', data, [200]); return this.apiResourceRequest('keyChange', data, [200]);
} }
/** /**
* Create new order * Create new order
* *
@@ -157,7 +148,6 @@ class AcmeApi {
return this.apiResourceRequest('newOrder', data, [201]); return this.apiResourceRequest('newOrder', data, [201]);
} }
/** /**
* Get order * Get order
* *
@@ -171,7 +161,6 @@ class AcmeApi {
return this.apiRequest(url, null, [200]); return this.apiRequest(url, null, [200]);
} }
/** /**
* Finalize order * Finalize order
* *
@@ -186,7 +175,6 @@ class AcmeApi {
return this.apiRequest(url, data, [200]); return this.apiRequest(url, data, [200]);
} }
/** /**
* Get identifier authorization * Get identifier authorization
* *
@@ -200,7 +188,6 @@ class AcmeApi {
return this.apiRequest(url, null, [200]); return this.apiRequest(url, null, [200]);
} }
/** /**
* Update identifier authorization * Update identifier authorization
* *
@@ -215,7 +202,6 @@ class AcmeApi {
return this.apiRequest(url, data, [200]); return this.apiRequest(url, data, [200]);
} }
/** /**
* Complete challenge * Complete challenge
* *
@@ -230,7 +216,6 @@ class AcmeApi {
return this.apiRequest(url, data, [200]); return this.apiRequest(url, data, [200]);
} }
/** /**
* Revoke certificate * Revoke certificate
* *
@@ -245,6 +230,5 @@ class AcmeApi {
} }
} }
/* Export API */ /* Export API */
module.exports = AcmeApi; module.exports = AcmeApi;

View File

@@ -13,10 +13,9 @@ const defaultOpts = {
skipChallengeVerification: false, skipChallengeVerification: false,
challengePriority: ['http-01', 'dns-01'], challengePriority: ['http-01', 'dns-01'],
challengeCreateFn: async () => { throw new Error('Missing challengeCreateFn()'); }, challengeCreateFn: async () => { throw new Error('Missing challengeCreateFn()'); },
challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()'); } challengeRemoveFn: async () => { throw new Error('Missing challengeRemoveFn()'); },
}; };
/** /**
* ACME client auto mode * ACME client auto mode
* *
@@ -25,8 +24,8 @@ const defaultOpts = {
* @returns {Promise<buffer>} Certificate * @returns {Promise<buffer>} Certificate
*/ */
module.exports = async function(client, userOpts) { module.exports = async (client, userOpts) => {
const opts = Object.assign({}, defaultOpts, userOpts); const opts = { ...defaultOpts, ...userOpts };
const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed }; const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed };
if (!Buffer.isBuffer(opts.csr)) { if (!Buffer.isBuffer(opts.csr)) {
@@ -37,7 +36,6 @@ module.exports = async function(client, userOpts) {
accountPayload.contact = [`mailto:${opts.email}`]; accountPayload.contact = [`mailto:${opts.email}`];
} }
/** /**
* Register account * Register account
*/ */
@@ -53,19 +51,16 @@ module.exports = async function(client, userOpts) {
await client.createAccount(accountPayload); await client.createAccount(accountPayload);
} }
/** /**
* Parse domains from CSR * Parse domains from CSR
*/ */
log('[auto] Parsing domains from Certificate Signing Request'); log('[auto] Parsing domains from Certificate Signing Request');
const csrDomains = readCsrDomains(opts.csr); const { commonName, altNames } = readCsrDomains(opts.csr);
const domains = [csrDomains.commonName].concat(csrDomains.altNames); const uniqueDomains = Array.from(new Set([commonName].concat(altNames).filter((d) => d)));
const uniqueDomains = Array.from(new Set(domains));
log(`[auto] Resolved ${uniqueDomains.length} unique domains from parsing the Certificate Signing Request`); log(`[auto] Resolved ${uniqueDomains.length} unique domains from parsing the Certificate Signing Request`);
/** /**
* Place order * Place order
*/ */
@@ -77,7 +72,6 @@ module.exports = async function(client, userOpts) {
log(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`); log(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`);
/** /**
* Resolve and satisfy challenges * Resolve and satisfy challenges
*/ */
@@ -165,7 +159,6 @@ module.exports = async function(client, userOpts) {
} }
}); });
/** /**
* Wait for all challenge promises to settle * Wait for all challenge promises to settle
*/ */
@@ -179,7 +172,6 @@ module.exports = async function(client, userOpts) {
throw e; throw e;
} }
/** /**
* Finalize order and download certificate * Finalize order and download certificate
*/ */

View File

@@ -3,11 +3,14 @@
*/ */
const axios = require('axios'); const axios = require('axios');
const { parseRetryAfterHeader } = require('./util');
const { log } = require('./logger');
const pkg = require('./../package.json'); const pkg = require('./../package.json');
const { AxiosError } = axios;
/** /**
* Instance * Defaults
*/ */
const instance = axios.create(); const instance = axios.create();
@@ -19,9 +22,11 @@ instance.defaults.headers.common['User-Agent'] = `node-${pkg.name}/${pkg.version
instance.defaults.acmeSettings = { instance.defaults.acmeSettings = {
httpChallengePort: 80, httpChallengePort: 80,
httpsChallengePort: 443, httpsChallengePort: 443,
tlsAlpnChallengePort: 443 tlsAlpnChallengePort: 443,
};
retryMaxAttempts: 5,
retryDefaultDelay: 5,
};
/** /**
* Explicitly set Node as default HTTP adapter * Explicitly set Node as default HTTP adapter
@@ -32,6 +37,84 @@ instance.defaults.acmeSettings = {
instance.defaults.adapter = 'http'; instance.defaults.adapter = 'http';
/**
* Retry requests on server errors or when rate limited
*
* https://datatracker.ietf.org/doc/html/rfc8555#section-6.6
*/
function isRetryableError(error) {
return (error.code !== 'ECONNABORTED')
&& (error.code !== 'ERR_NOCK_NO_MATCH')
&& (!error.response
|| (error.response.status === 429)
|| ((error.response.status >= 500) && (error.response.status <= 599)));
}
/* https://github.com/axios/axios/blob/main/lib/core/settle.js */
function validateStatus(response) {
const validator = response.config.retryValidateStatus;
if (!response.status || !validator || validator(response.status)) {
return response;
}
throw new AxiosError(
`Request failed with status code ${response.status}`,
(Math.floor(response.status / 100) === 4) ? AxiosError.ERR_BAD_REQUEST : AxiosError.ERR_BAD_RESPONSE,
response.config,
response.request,
response,
);
}
/* Pass all responses through the error interceptor */
instance.interceptors.request.use((config) => {
if (!('retryValidateStatus' in config)) {
config.retryValidateStatus = config.validateStatus;
}
config.validateStatus = () => false;
return config;
});
/* Handle request retries if applicable */
instance.interceptors.response.use(null, async (error) => {
const { config, response } = error;
if (!config) {
return Promise.reject(error);
}
/* Pick up errors we want to retry */
if (isRetryableError(error)) {
const { retryMaxAttempts, retryDefaultDelay } = instance.defaults.acmeSettings;
config.retryAttempt = ('retryAttempt' in config) ? (config.retryAttempt + 1) : 1;
if (config.retryAttempt <= retryMaxAttempts) {
const code = response ? `HTTP ${response.status}` : error.code;
log(`Caught ${code}, retry attempt ${config.retryAttempt}/${retryMaxAttempts} to URL ${config.url}`);
/* Attempt to parse Retry-After header, fallback to default delay */
let retryAfter = response ? parseRetryAfterHeader(response.headers['retry-after']) : 0;
if (retryAfter > 0) {
log(`Found retry-after response header with value: ${response.headers['retry-after']}, waiting ${retryAfter} seconds`);
}
else {
retryAfter = (retryDefaultDelay * config.retryAttempt);
log(`Unable to locate or parse retry-after response header, waiting ${retryAfter} seconds`);
}
/* Wait and retry the request */
await new Promise((resolve) => { setTimeout(resolve, (retryAfter * 1000)); });
return instance(config);
}
}
/* Validate and return response */
return validateStatus(response);
});
/** /**
* Export instance * Export instance

View File

@@ -13,7 +13,6 @@ const verify = require('./verify');
const util = require('./util'); const util = require('./util');
const auto = require('./auto'); const auto = require('./auto');
/** /**
* ACME states * ACME states
* *
@@ -24,7 +23,6 @@ const validStates = ['ready', 'valid'];
const pendingStates = ['pending', 'processing']; const pendingStates = ['pending', 'processing'];
const invalidStates = ['invalid']; const invalidStates = ['invalid'];
/** /**
* Default options * Default options
* *
@@ -38,10 +36,9 @@ const defaultOpts = {
externalAccountBinding: {}, externalAccountBinding: {},
backoffAttempts: 10, backoffAttempts: 10,
backoffMin: 5000, backoffMin: 5000,
backoffMax: 30000 backoffMax: 30000,
}; };
/** /**
* AcmeClient * AcmeClient
* *
@@ -61,7 +58,7 @@ const defaultOpts = {
* ```js * ```js
* const client = new acme.Client({ * const client = new acme.Client({
* directoryUrl: acme.directory.letsencrypt.staging, * directoryUrl: acme.directory.letsencrypt.staging,
* accountKey: 'Private key goes here' * accountKey: 'Private key goes here',
* }); * });
* ``` * ```
* *
@@ -73,7 +70,7 @@ const defaultOpts = {
* accountUrl: 'Optional account URL goes here', * accountUrl: 'Optional account URL goes here',
* backoffAttempts: 10, * backoffAttempts: 10,
* backoffMin: 5000, * backoffMin: 5000,
* backoffMax: 30000 * backoffMax: 30000,
* }); * });
* ``` * ```
* *
@@ -84,8 +81,8 @@ const defaultOpts = {
* accountKey: 'Private key goes here', * accountKey: 'Private key goes here',
* externalAccountBinding: { * externalAccountBinding: {
* kid: 'YOUR-EAB-KID', * kid: 'YOUR-EAB-KID',
* hmacKey: 'YOUR-EAB-HMAC-KEY' * hmacKey: 'YOUR-EAB-HMAC-KEY',
* } * },
* }); * });
* ``` * ```
*/ */
@@ -96,19 +93,17 @@ class AcmeClient {
opts.accountKey = Buffer.from(opts.accountKey); opts.accountKey = Buffer.from(opts.accountKey);
} }
this.opts = Object.assign({}, defaultOpts, opts); this.opts = { ...defaultOpts, ...opts };
this.backoffOpts = { this.backoffOpts = {
attempts: this.opts.backoffAttempts, attempts: this.opts.backoffAttempts,
min: this.opts.backoffMin, min: this.opts.backoffMin,
max: this.opts.backoffMax max: this.opts.backoffMax,
}; };
this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey, this.opts.externalAccountBinding); this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey, this.opts.externalAccountBinding);
this.api = new AcmeApi(this.http, this.opts.accountUrl); this.api = new AcmeApi(this.http, this.opts.accountUrl);
} }
/** /**
* Get Terms of Service URL if available * Get Terms of Service URL if available
* *
@@ -128,7 +123,6 @@ class AcmeClient {
return this.api.getTermsOfServiceUrl(); return this.api.getTermsOfServiceUrl();
} }
/** /**
* Get current account URL * Get current account URL
* *
@@ -150,7 +144,6 @@ class AcmeClient {
return this.api.getAccountUrl(); return this.api.getAccountUrl();
} }
/** /**
* Create a new account * Create a new account
* *
@@ -162,7 +155,7 @@ class AcmeClient {
* @example Create a new account * @example Create a new account
* ```js * ```js
* const account = await client.createAccount({ * const account = await client.createAccount({
* termsOfServiceAgreed: true * termsOfServiceAgreed: true,
* }); * });
* ``` * ```
* *
@@ -170,7 +163,7 @@ class AcmeClient {
* ```js * ```js
* const account = await client.createAccount({ * const account = await client.createAccount({
* termsOfServiceAgreed: true, * termsOfServiceAgreed: true,
* contact: ['mailto:test@example.com'] * contact: ['mailto:test@example.com'],
* }); * });
* ``` * ```
*/ */
@@ -196,7 +189,6 @@ class AcmeClient {
} }
} }
/** /**
* Update existing account * Update existing account
* *
@@ -208,7 +200,7 @@ class AcmeClient {
* @example Update existing account * @example Update existing account
* ```js * ```js
* const account = await client.updateAccount({ * const account = await client.updateAccount({
* contact: ['mailto:foo@example.com'] * contact: ['mailto:foo@example.com'],
* }); * });
* ``` * ```
*/ */
@@ -236,7 +228,6 @@ class AcmeClient {
return resp.data; return resp.data;
} }
/** /**
* Update account private key * Update account private key
* *
@@ -261,7 +252,7 @@ class AcmeClient {
const accountUrl = this.api.getAccountUrl(); const accountUrl = this.api.getAccountUrl();
/* Create new HTTP and API clients using new key */ /* Create new HTTP and API clients using new key */
const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey); const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey, this.opts.externalAccountBinding);
const newApiClient = new AcmeApi(newHttpClient, accountUrl); const newApiClient = new AcmeApi(newHttpClient, accountUrl);
/* Get old JWK */ /* Get old JWK */
@@ -282,7 +273,6 @@ class AcmeClient {
return resp.data; return resp.data;
} }
/** /**
* Create a new order * Create a new order
* *
@@ -296,8 +286,8 @@ class AcmeClient {
* const order = await client.createOrder({ * const order = await client.createOrder({
* identifiers: [ * identifiers: [
* { type: 'dns', value: 'example.com' }, * { type: 'dns', value: 'example.com' },
* { type: 'dns', value: 'test.example.com' } * { type: 'dns', value: 'test.example.com' },
* ] * ],
* }); * });
* ``` * ```
*/ */
@@ -314,7 +304,6 @@ class AcmeClient {
return resp.data; return resp.data;
} }
/** /**
* Refresh order object from CA * Refresh order object from CA
* *
@@ -376,7 +365,6 @@ class AcmeClient {
return resp.data; return resp.data;
} }
/** /**
* Get identifier authorizations from order * Get identifier authorizations from order
* *
@@ -406,7 +394,6 @@ class AcmeClient {
})); }));
} }
/** /**
* Deactivate identifier authorization * Deactivate identifier authorization
* *
@@ -427,10 +414,7 @@ class AcmeClient {
throw new Error('Unable to deactivate identifier authorization, URL not found'); throw new Error('Unable to deactivate identifier authorization, URL not found');
} }
const data = { const data = { status: 'deactivated' };
status: 'deactivated'
};
const resp = await this.api.updateAuthorization(authz.url, data); const resp = await this.api.updateAuthorization(authz.url, data);
/* Add URL to response */ /* Add URL to response */
@@ -438,7 +422,6 @@ class AcmeClient {
return resp.data; return resp.data;
} }
/** /**
* Get key authorization for ACME challenge * Get key authorization for ACME challenge
* *
@@ -480,7 +463,6 @@ class AcmeClient {
throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`); throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
} }
/** /**
* Verify that ACME challenge is satisfied * Verify that ACME challenge is satisfied
* *
@@ -515,7 +497,6 @@ class AcmeClient {
return util.retry(verifyFn, this.backoffOpts); return util.retry(verifyFn, this.backoffOpts);
} }
/** /**
* Notify CA that challenge has been completed * Notify CA that challenge has been completed
* *
@@ -536,7 +517,6 @@ class AcmeClient {
return resp.data; return resp.data;
} }
/** /**
* Wait for ACME provider to verify status on a order, authorization or challenge * Wait for ACME provider to verify status on a order, authorization or challenge
* *
@@ -593,7 +573,6 @@ class AcmeClient {
return util.retry(verifyFn, this.backoffOpts); return util.retry(verifyFn, this.backoffOpts);
} }
/** /**
* Get certificate from ACME order * Get certificate from ACME order
* *
@@ -640,7 +619,6 @@ class AcmeClient {
return resp.data; return resp.data;
} }
/** /**
* Revoke certificate * Revoke certificate
* *
@@ -660,7 +638,7 @@ class AcmeClient {
* ```js * ```js
* const certificate = { ... }; // Previously created certificate * const certificate = { ... }; // Previously created certificate
* const result = await client.revokeCertificate(certificate, { * const result = await client.revokeCertificate(certificate, {
* reason: 4 * reason: 4,
* }); * });
* ``` * ```
*/ */
@@ -671,7 +649,6 @@ class AcmeClient {
return resp.data; return resp.data;
} }
/** /**
* Auto mode * Auto mode
* *
@@ -689,7 +666,7 @@ class AcmeClient {
* @example Order a certificate using auto mode * @example Order a certificate using auto mode
* ```js * ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com' * altNames: ['test.example.com'],
* }); * });
* *
* const certificate = await client.auto({ * const certificate = await client.auto({
@@ -701,14 +678,14 @@ class AcmeClient {
* }, * },
* challengeRemoveFn: async (authz, challenge, keyAuthorization) => { * challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
* // Clean up challenge here * // Clean up challenge here
* } * },
* }); * });
* ``` * ```
* *
* @example Order a certificate using auto mode with preferred chain * @example Order a certificate using auto mode with preferred chain
* ```js * ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com' * altNames: ['test.example.com'],
* }); * });
* *
* const certificate = await client.auto({ * const certificate = await client.auto({
@@ -717,7 +694,7 @@ class AcmeClient {
* termsOfServiceAgreed: true, * termsOfServiceAgreed: true,
* preferredChain: 'DST Root CA X3', * preferredChain: 'DST Root CA X3',
* challengeCreateFn: async () => {}, * challengeCreateFn: async () => {},
* challengeRemoveFn: async () => {} * challengeRemoveFn: async () => {},
* }); * });
* ``` * ```
*/ */
@@ -727,6 +704,5 @@ class AcmeClient {
} }
} }
/* Export client */ /* Export client */
module.exports = AcmeClient; module.exports = AcmeClient;

View File

@@ -13,7 +13,6 @@ const forge = require('node-forge');
const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair); const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
/** /**
* Attempt to parse forge object from PEM encoded string * Attempt to parse forge object from PEM encoded string
* *
@@ -54,7 +53,6 @@ function forgeObjectFromPem(input) {
return result; return result;
} }
/** /**
* Parse domain names from a certificate or CSR * Parse domain names from a certificate or CSR
* *
@@ -93,11 +91,10 @@ function parseDomains(obj) {
return { return {
commonName, commonName,
altNames altNames,
}; };
} }
/** /**
* Generate a private RSA key * Generate a private RSA key
* *
@@ -123,7 +120,6 @@ async function createPrivateKey(size = 2048) {
exports.createPrivateKey = createPrivateKey; exports.createPrivateKey = createPrivateKey;
/** /**
* Create public key from a private RSA key * Create public key from a private RSA key
* *
@@ -136,14 +132,13 @@ exports.createPrivateKey = createPrivateKey;
* ``` * ```
*/ */
exports.createPublicKey = async function(key) { exports.createPublicKey = async (key) => {
const privateKey = forge.pki.privateKeyFromPem(key); const privateKey = forge.pki.privateKeyFromPem(key);
const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e); const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
const pemKey = forge.pki.publicKeyToPem(publicKey); const pemKey = forge.pki.publicKeyToPem(publicKey);
return Buffer.from(pemKey); return Buffer.from(pemKey);
}; };
/** /**
* Parse body of PEM encoded object from buffer or string * Parse body of PEM encoded object from buffer or string
* If multiple objects are chained, the first body will be returned * If multiple objects are chained, the first body will be returned
@@ -157,7 +152,6 @@ exports.getPemBody = (str) => {
return forge.util.encode64(msg.body); return forge.util.encode64(msg.body);
}; };
/** /**
* Split chain of PEM encoded objects from buffer or string into array * Split chain of PEM encoded objects from buffer or string into array
* *
@@ -167,7 +161,6 @@ exports.getPemBody = (str) => {
exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode); exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode);
/** /**
* Get modulus * Get modulus
* *
@@ -182,7 +175,7 @@ exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode);
* ``` * ```
*/ */
exports.getModulus = async function(input) { exports.getModulus = async (input) => {
if (!Buffer.isBuffer(input)) { if (!Buffer.isBuffer(input)) {
input = Buffer.from(input); input = Buffer.from(input);
} }
@@ -191,7 +184,6 @@ exports.getModulus = async function(input) {
return Buffer.from(forge.util.hexToBytes(obj.n.toString(16)), 'binary'); return Buffer.from(forge.util.hexToBytes(obj.n.toString(16)), 'binary');
}; };
/** /**
* Get public exponent * Get public exponent
* *
@@ -206,7 +198,7 @@ exports.getModulus = async function(input) {
* ``` * ```
*/ */
exports.getPublicExponent = async function(input) { exports.getPublicExponent = async (input) => {
if (!Buffer.isBuffer(input)) { if (!Buffer.isBuffer(input)) {
input = Buffer.from(input); input = Buffer.from(input);
} }
@@ -215,7 +207,6 @@ exports.getPublicExponent = async function(input) {
return Buffer.from(forge.util.hexToBytes(obj.e.toString(16)), 'binary'); return Buffer.from(forge.util.hexToBytes(obj.e.toString(16)), 'binary');
}; };
/** /**
* Read domains from a Certificate Signing Request * Read domains from a Certificate Signing Request
* *
@@ -231,7 +222,7 @@ exports.getPublicExponent = async function(input) {
* ``` * ```
*/ */
exports.readCsrDomains = async function(csr) { exports.readCsrDomains = async (csr) => {
if (!Buffer.isBuffer(csr)) { if (!Buffer.isBuffer(csr)) {
csr = Buffer.from(csr); csr = Buffer.from(csr);
} }
@@ -240,7 +231,6 @@ exports.readCsrDomains = async function(csr) {
return parseDomains(obj); return parseDomains(obj);
}; };
/** /**
* Read information from a certificate * Read information from a certificate
* *
@@ -260,7 +250,7 @@ exports.readCsrDomains = async function(csr) {
* ``` * ```
*/ */
exports.readCertificateInfo = async function(cert) { exports.readCertificateInfo = async (cert) => {
if (!Buffer.isBuffer(cert)) { if (!Buffer.isBuffer(cert)) {
cert = Buffer.from(cert); cert = Buffer.from(cert);
} }
@@ -270,15 +260,14 @@ exports.readCertificateInfo = async function(cert) {
return { return {
issuer: { issuer: {
commonName: issuerCn ? issuerCn.value : null commonName: issuerCn ? issuerCn.value : null,
}, },
domains: parseDomains(obj), domains: parseDomains(obj),
notAfter: obj.validity.notAfter, notAfter: obj.validity.notAfter,
notBefore: obj.validity.notBefore notBefore: obj.validity.notBefore,
}; };
}; };
/** /**
* Determine ASN.1 type for CSR subject short name * Determine ASN.1 type for CSR subject short name
* Note: https://datatracker.ietf.org/doc/html/rfc5280 * Note: https://datatracker.ietf.org/doc/html/rfc5280
@@ -299,7 +288,6 @@ function getCsrValueTagClass(shortName) {
} }
} }
/** /**
* Create array of short names and values for Certificate Signing Request subjects * Create array of short names and values for Certificate Signing Request subjects
* *
@@ -319,7 +307,6 @@ function createCsrSubject(subjectObj) {
}, []); }, []);
} }
/** /**
* Create array of alt names for Certificate Signing Requests * Create array of alt names for Certificate Signing Requests
* Note: https://github.com/digitalbazaar/forge/blob/dfdde475677a8a25c851e33e8f81dca60d90cfb9/lib/x509.js#L1444-L1454 * Note: https://github.com/digitalbazaar/forge/blob/dfdde475677a8a25c851e33e8f81dca60d90cfb9/lib/x509.js#L1444-L1454
@@ -336,7 +323,6 @@ function formatCsrAltNames(altNames) {
}); });
} }
/** /**
* Create a Certificate Signing Request * Create a Certificate Signing Request
* *
@@ -356,29 +342,30 @@ function formatCsrAltNames(altNames) {
* @example Create a Certificate Signing Request * @example Create a Certificate Signing Request
* ```js * ```js
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
* commonName: 'test.example.com' * altNames: ['test.example.com'],
* }); * });
* ``` * ```
* *
* @example Certificate Signing Request with both common and alternative names * @example Certificate Signing Request with both common and alternative names
* > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
* ```js * ```js
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
* keySize: 4096, * keySize: 4096,
* commonName: 'test.example.com', * commonName: 'test.example.com',
* altNames: ['foo.example.com', 'bar.example.com'] * altNames: ['foo.example.com', 'bar.example.com'],
* }); * });
* ``` * ```
* *
* @example Certificate Signing Request with additional information * @example Certificate Signing Request with additional information
* ```js * ```js
* const [certificateKey, certificateRequest] = await acme.forge.createCsr({ * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
* commonName: 'test.example.com', * altNames: ['test.example.com'],
* country: 'US', * country: 'US',
* state: 'California', * state: 'California',
* locality: 'Los Angeles', * locality: 'Los Angeles',
* organization: 'The Company Inc.', * organization: 'The Company Inc.',
* organizationUnit: 'IT Department', * organizationUnit: 'IT Department',
* emailAddress: 'contact@example.com' * emailAddress: 'contact@example.com',
* }); * });
* ``` * ```
* *
@@ -387,11 +374,11 @@ function formatCsrAltNames(altNames) {
* const certificateKey = await acme.forge.createPrivateKey(); * const certificateKey = await acme.forge.createPrivateKey();
* *
* const [, certificateRequest] = await acme.forge.createCsr({ * const [, certificateRequest] = await acme.forge.createCsr({
* commonName: 'test.example.com' * altNames: ['test.example.com'],
* }, certificateKey); * }, certificateKey);
*/ */
exports.createCsr = async function(data, key = null) { exports.createCsr = async (data, key = null) => {
if (!key) { if (!key) {
key = await createPrivateKey(data.keySize); key = await createPrivateKey(data.keySize);
} }
@@ -423,7 +410,7 @@ exports.createCsr = async function(data, key = null) {
L: data.locality, L: data.locality,
O: data.organization, O: data.organization,
OU: data.organizationUnit, OU: data.organizationUnit,
E: data.emailAddress E: data.emailAddress,
}); });
csr.setSubject(subject); csr.setSubject(subject);
@@ -434,8 +421,8 @@ exports.createCsr = async function(data, key = null) {
name: 'extensionRequest', name: 'extensionRequest',
extensions: [{ extensions: [{
name: 'subjectAltName', name: 'subjectAltName',
altNames: formatCsrAltNames(data.altNames) altNames: formatCsrAltNames(data.altNames),
}] }],
}]); }]);
} }

View File

@@ -22,7 +22,6 @@ const subjectAltNameOID = '2.5.29.17';
/* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */ /* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31'; const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31';
/** /**
* Determine key type and info by attempting to derive public key * Determine key type and info by attempting to derive public key
* *
@@ -35,7 +34,7 @@ function getKeyInfo(keyPem) {
const result = { const result = {
isRSA: false, isRSA: false,
isECDSA: false, isECDSA: false,
publicKey: crypto.createPublicKey(keyPem) publicKey: crypto.createPublicKey(keyPem),
}; };
if (result.publicKey.asymmetricKeyType === 'rsa') { if (result.publicKey.asymmetricKeyType === 'rsa') {
@@ -51,7 +50,6 @@ function getKeyInfo(keyPem) {
return result; return result;
} }
/** /**
* Generate a private RSA key * Generate a private RSA key
* *
@@ -74,8 +72,8 @@ async function createPrivateRsaKey(modulusLength = 2048) {
modulusLength, modulusLength,
privateKeyEncoding: { privateKeyEncoding: {
type: 'pkcs8', type: 'pkcs8',
format: 'pem' format: 'pem',
} },
}); });
return Buffer.from(pair.privateKey); return Buffer.from(pair.privateKey);
@@ -83,7 +81,6 @@ async function createPrivateRsaKey(modulusLength = 2048) {
exports.createPrivateRsaKey = createPrivateRsaKey; exports.createPrivateRsaKey = createPrivateRsaKey;
/** /**
* Alias of `createPrivateRsaKey()` * Alias of `createPrivateRsaKey()`
* *
@@ -92,7 +89,6 @@ exports.createPrivateRsaKey = createPrivateRsaKey;
exports.createPrivateKey = createPrivateRsaKey; exports.createPrivateKey = createPrivateRsaKey;
/** /**
* Generate a private ECDSA key * Generate a private ECDSA key
* *
@@ -115,14 +111,13 @@ exports.createPrivateEcdsaKey = async (namedCurve = 'P-256') => {
namedCurve, namedCurve,
privateKeyEncoding: { privateKeyEncoding: {
type: 'pkcs8', type: 'pkcs8',
format: 'pem' format: 'pem',
} },
}); });
return Buffer.from(pair.privateKey); return Buffer.from(pair.privateKey);
}; };
/** /**
* Get a public key derived from a RSA or ECDSA key * Get a public key derived from a RSA or ECDSA key
* *
@@ -140,13 +135,12 @@ exports.getPublicKey = (keyPem) => {
const publicKey = info.publicKey.export({ const publicKey = info.publicKey.export({
type: info.isECDSA ? 'spki' : 'pkcs1', type: info.isECDSA ? 'spki' : 'pkcs1',
format: 'pem' format: 'pem',
}); });
return Buffer.from(publicKey); return Buffer.from(publicKey);
}; };
/** /**
* Get a JSON Web Key derived from a RSA or ECDSA key * Get a JSON Web Key derived from a RSA or ECDSA key
* *
@@ -163,7 +157,7 @@ exports.getPublicKey = (keyPem) => {
function getJwk(keyPem) { function getJwk(keyPem) {
const jwk = crypto.createPublicKey(keyPem).export({ const jwk = crypto.createPublicKey(keyPem).export({
format: 'jwk' format: 'jwk',
}); });
/* Sort keys */ /* Sort keys */
@@ -175,7 +169,6 @@ function getJwk(keyPem) {
exports.getJwk = getJwk; exports.getJwk = getJwk;
/** /**
* Produce CryptoKeyPair and signing algorithm from a PEM encoded private key * Produce CryptoKeyPair and signing algorithm from a PEM encoded private key
* *
@@ -191,7 +184,7 @@ async function getWebCryptoKeyPair(keyPem) {
/* Signing algorithm */ /* Signing algorithm */
const sigalg = { const sigalg = {
name: 'RSASSA-PKCS1-v1_5', name: 'RSASSA-PKCS1-v1_5',
hash: { name: 'SHA-256' } hash: { name: 'SHA-256' },
}; };
if (info.isECDSA) { if (info.isECDSA) {
@@ -215,7 +208,6 @@ async function getWebCryptoKeyPair(keyPem) {
return [{ privateKey, publicKey }, sigalg]; return [{ privateKey, publicKey }, sigalg];
} }
/** /**
* Split chain of PEM encoded objects from string into array * Split chain of PEM encoded objects from string into array
* *
@@ -235,7 +227,6 @@ function splitPemChain(chainPem) {
exports.splitPemChain = splitPemChain; exports.splitPemChain = splitPemChain;
/** /**
* Parse body of PEM encoded object and return a Base64URL string * Parse body of PEM encoded object and return a Base64URL string
* If multiple objects are chained, the first body will be returned * If multiple objects are chained, the first body will be returned
@@ -256,7 +247,6 @@ exports.getPemBodyAsB64u = (pem) => {
return Buffer.from(dec).toString('base64url'); return Buffer.from(dec).toString('base64url');
}; };
/** /**
* Parse domains from a certificate or CSR * Parse domains from a certificate or CSR
* *
@@ -277,11 +267,10 @@ function parseDomains(input) {
return { return {
commonName, commonName,
altNames altNames,
}; };
} }
/** /**
* Read domains from a Certificate Signing Request * Read domains from a Certificate Signing Request
* *
@@ -307,7 +296,6 @@ exports.readCsrDomains = (csrPem) => {
return parseDomains(csr); return parseDomains(csr);
}; };
/** /**
* Read information from a certificate * Read information from a certificate
* If multiple certificates are chained, the first will be read * If multiple certificates are chained, the first will be read
@@ -338,15 +326,14 @@ exports.readCertificateInfo = (certPem) => {
return { return {
issuer: { issuer: {
commonName: cert.issuerName.getField('CN').pop() || null commonName: cert.issuerName.getField('CN').pop() || null,
}, },
domains: parseDomains(cert), domains: parseDomains(cert),
notBefore: cert.notBefore, notBefore: cert.notBefore,
notAfter: cert.notAfter notAfter: cert.notAfter,
}; };
}; };
/** /**
* Determine ASN.1 character string type for CSR subject field name * Determine ASN.1 character string type for CSR subject field name
* *
@@ -369,7 +356,6 @@ function getCsrAsn1CharStringType(field) {
} }
} }
/** /**
* Create array of subject fields for a Certificate Signing Request * Create array of subject fields for a Certificate Signing Request
* *
@@ -391,7 +377,6 @@ function createCsrSubject(input) {
}, []); }, []);
} }
/** /**
* Create x509 subject alternate name extension * Create x509 subject alternate name extension
* *
@@ -409,7 +394,6 @@ function createSubjectAltNameExtension(altNames) {
})); }));
} }
/** /**
* Create a Certificate Signing Request * Create a Certificate Signing Request
* *
@@ -429,29 +413,30 @@ function createSubjectAltNameExtension(altNames) {
* @example Create a Certificate Signing Request * @example Create a Certificate Signing Request
* ```js * ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com' * altNames: ['test.example.com'],
* }); * });
* ``` * ```
* *
* @example Certificate Signing Request with both common and alternative names * @example Certificate Signing Request with both common and alternative names
* > *Warning*: Certificate subject common name has been [deprecated](https://letsencrypt.org/docs/glossary/#def-CN) and its use is [discouraged](https://cabforum.org/uploads/BRv1.2.3.pdf).
* ```js * ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* keySize: 4096, * keySize: 4096,
* commonName: 'test.example.com', * commonName: 'test.example.com',
* altNames: ['foo.example.com', 'bar.example.com'] * altNames: ['foo.example.com', 'bar.example.com'],
* }); * });
* ``` * ```
* *
* @example Certificate Signing Request with additional information * @example Certificate Signing Request with additional information
* ```js * ```js
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({ * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com', * altNames: ['test.example.com'],
* country: 'US', * country: 'US',
* state: 'California', * state: 'California',
* locality: 'Los Angeles', * locality: 'Los Angeles',
* organization: 'The Company Inc.', * organization: 'The Company Inc.',
* organizationUnit: 'IT Department', * organizationUnit: 'IT Department',
* emailAddress: 'contact@example.com' * emailAddress: 'contact@example.com',
* }); * });
* ``` * ```
* *
@@ -460,8 +445,9 @@ function createSubjectAltNameExtension(altNames) {
* const certificateKey = await acme.crypto.createPrivateEcdsaKey(); * const certificateKey = await acme.crypto.createPrivateEcdsaKey();
* *
* const [, certificateRequest] = await acme.crypto.createCsr({ * const [, certificateRequest] = await acme.crypto.createCsr({
* commonName: 'test.example.com' * altNames: ['test.example.com'],
* }, certificateKey); * }, certificateKey);
* ```
*/ */
exports.createCsr = async (data, keyPem = null) => { exports.createCsr = async (data, keyPem = null) => {
@@ -489,7 +475,7 @@ exports.createCsr = async (data, keyPem = null) => {
new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise 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 */ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
createSubjectAltNameExtension(data.altNames) createSubjectAltNameExtension(data.altNames),
]; ];
/* Create CSR */ /* Create CSR */
@@ -504,8 +490,8 @@ exports.createCsr = async (data, keyPem = null) => {
L: data.locality, L: data.locality,
O: data.organization, O: data.organization,
OU: data.organizationUnit, OU: data.organizationUnit,
E: data.emailAddress E: data.emailAddress,
}) }),
}); });
/* Done */ /* Done */
@@ -513,7 +499,6 @@ exports.createCsr = async (data, keyPem = null) => {
return [keyPem, Buffer.from(pem)]; return [keyPem, Buffer.from(pem)];
}; };
/** /**
* Create a self-signed ALPN certificate for TLS-ALPN-01 challenges * Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
* *
@@ -533,6 +518,7 @@ exports.createCsr = async (data, keyPem = null) => {
* ```js * ```js
* const alpnKey = await acme.crypto.createPrivateEcdsaKey(); * const alpnKey = await acme.crypto.createPrivateEcdsaKey();
* const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey); * const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
* ```
*/ */
exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => { exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
@@ -564,7 +550,7 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
await x509.SubjectKeyIdentifierExtension.create(keys.publicKey), await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
/* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
createSubjectAltNameExtension([commonName]) createSubjectAltNameExtension([commonName]),
]; ];
/* ALPN extension */ /* ALPN extension */
@@ -581,8 +567,8 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
notBefore: now, notBefore: now,
notAfter: now, notAfter: now,
name: createCsrSubject({ name: createCsrSubject({
CN: commonName CN: commonName,
}) }),
}); });
/* Done */ /* Done */
@@ -590,7 +576,6 @@ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) =
return [keyPem, Buffer.from(pem)]; return [keyPem, Buffer.from(pem)];
}; };
/** /**
* Validate that a ALPN certificate contains the expected key authorization * Validate that a ALPN certificate contains the expected key authorization
* *

View File

@@ -7,7 +7,6 @@ const { getJwk } = require('./crypto');
const { log } = require('./logger'); const { log } = require('./logger');
const axios = require('./axios'); const axios = require('./axios');
/** /**
* ACME HTTP client * ACME HTTP client
* *
@@ -26,10 +25,12 @@ class HttpClient {
this.externalAccountBinding = externalAccountBinding; this.externalAccountBinding = externalAccountBinding;
this.maxBadNonceRetries = 5; this.maxBadNonceRetries = 5;
this.directory = null;
this.jwk = null; this.jwk = null;
}
this.directoryCache = null;
this.directoryMaxAge = 86400;
this.directoryTimestamp = 0;
}
/** /**
* HTTP request * HTTP request
@@ -60,17 +61,20 @@ class HttpClient {
return resp; return resp;
} }
/** /**
* Ensure provider directory exists * Get ACME provider directory
* *
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
* *
* @returns {Promise} * @returns {Promise<object>} ACME directory contents
*/ */
async getDirectory() { async getDirectory() {
if (!this.directory) { const now = Math.floor(Date.now() / 1000);
const age = (now - this.directoryTimestamp);
if (!this.directoryCache || (age > this.directoryMaxAge)) {
log(`Refreshing ACME directory, age: ${age}`);
const resp = await this.request(this.directoryUrl, 'get'); const resp = await this.request(this.directoryUrl, 'get');
if (resp.status >= 400) { if (resp.status >= 400) {
@@ -81,10 +85,12 @@ class HttpClient {
throw new Error('Attempting to read ACME directory returned no data'); throw new Error('Attempting to read ACME directory returned no data');
} }
this.directory = resp.data; this.directoryCache = resp.data;
} this.directoryTimestamp = now;
} }
return this.directoryCache;
}
/** /**
* Get JSON Web Key * Get JSON Web Key
@@ -100,13 +106,12 @@ class HttpClient {
return this.jwk; return this.jwk;
} }
/** /**
* Get nonce from directory API endpoint * Get nonce from directory API endpoint
* *
* https://datatracker.ietf.org/doc/html/rfc8555#section-7.2 * https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
* *
* @returns {Promise<string>} nonce * @returns {Promise<string>} Nonce
*/ */
async getNonce() { async getNonce() {
@@ -120,7 +125,6 @@ class HttpClient {
return resp.headers['replay-nonce']; return resp.headers['replay-nonce'];
} }
/** /**
* Get URL for a directory resource * Get URL for a directory resource
* *
@@ -129,16 +133,15 @@ class HttpClient {
*/ */
async getResourceUrl(resource) { async getResourceUrl(resource) {
await this.getDirectory(); const dir = await this.getDirectory();
if (!this.directory[resource]) { if (!dir[resource]) {
throw new Error(`Unable to locate API resource URL in ACME directory: "${resource}"`); throw new Error(`Unable to locate API resource URL in ACME directory: "${resource}"`);
} }
return this.directory[resource]; return dir[resource];
} }
/** /**
* Get directory meta field * Get directory meta field
* *
@@ -147,16 +150,15 @@ class HttpClient {
*/ */
async getMetaField(field) { async getMetaField(field) {
await this.getDirectory(); const dir = await this.getDirectory();
if (('meta' in this.directory) && (field in this.directory.meta)) { if (('meta' in dir) && (field in dir.meta)) {
return this.directory.meta[field]; return dir.meta[field];
} }
return null; return null;
} }
/** /**
* Prepare HTTP request body for signature * Prepare HTTP request body for signature
* *
@@ -189,11 +191,10 @@ class HttpClient {
/* Body */ /* Body */
return { return {
payload: payload ? Buffer.from(JSON.stringify(payload)).toString('base64url') : '', payload: payload ? Buffer.from(JSON.stringify(payload)).toString('base64url') : '',
protected: Buffer.from(JSON.stringify(header)).toString('base64url') protected: Buffer.from(JSON.stringify(header)).toString('base64url'),
}; };
} }
/** /**
* Create JWS HTTP request body using HMAC * Create JWS HTTP request body using HMAC
* *
@@ -216,7 +217,6 @@ class HttpClient {
return result; return result;
} }
/** /**
* Create JWS HTTP request body using RSA or ECC * Create JWS HTTP request body using RSA or ECC
* *
@@ -257,13 +257,12 @@ class HttpClient {
result.signature = signer.sign({ result.signature = signer.sign({
key: this.accountKey, key: this.accountKey,
padding: RSA_PKCS1_PADDING, padding: RSA_PKCS1_PADDING,
dsaEncoding: 'ieee-p1363' dsaEncoding: 'ieee-p1363',
}, 'base64url'); }, 'base64url');
return result; return result;
} }
/** /**
* Signed HTTP request * Signed HTTP request
* *
@@ -299,7 +298,7 @@ class HttpClient {
const data = this.createSignedBody(url, payload, { nonce, kid }); const data = this.createSignedBody(url, payload, { nonce, kid });
const resp = await this.request(url, 'post', { data }); const resp = await this.request(url, 'post', { data });
/* Retry on bad nonce - https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-10#section-6.4 */ /* Retry on bad nonce - https://datatracker.ietf.org/doc/html/rfc8555#section-6.5 */
if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) { 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; nonce = resp.headers['replay-nonce'] || null;
attempts += 1; attempts += 1;
@@ -313,6 +312,5 @@ class HttpClient {
} }
} }
/* Export client */ /* Export client */
module.exports = HttpClient; module.exports = HttpClient;

View File

@@ -4,7 +4,6 @@
exports.Client = require('./client'); exports.Client = require('./client');
/** /**
* Directory URLs * Directory URLs
*/ */
@@ -12,18 +11,21 @@ exports.Client = require('./client');
exports.directory = { exports.directory = {
buypass: { buypass: {
staging: 'https://api.test4.buypass.no/acme/directory', staging: 'https://api.test4.buypass.no/acme/directory',
production: 'https://api.buypass.com/acme/directory' production: 'https://api.buypass.com/acme/directory',
},
google: {
staging: 'https://dv.acme-v02.test-api.pki.goog/directory',
production: 'https://dv.acme-v02.api.pki.goog/directory',
}, },
letsencrypt: { letsencrypt: {
staging: 'https://acme-staging-v02.api.letsencrypt.org/directory', staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
production: 'https://acme-v02.api.letsencrypt.org/directory' production: 'https://acme-v02.api.letsencrypt.org/directory',
}, },
zerossl: { zerossl: {
production: 'https://acme.zerossl.com/v2/DV90' production: 'https://acme.zerossl.com/v2/DV90',
} },
}; };
/** /**
* Crypto * Crypto
*/ */
@@ -31,14 +33,12 @@ exports.directory = {
exports.crypto = require('./crypto'); exports.crypto = require('./crypto');
exports.forge = require('./crypto/forge'); exports.forge = require('./crypto/forge');
/** /**
* Axios * Axios
*/ */
exports.axios = require('./axios'); exports.axios = require('./axios');
/** /**
* Logger * Logger
*/ */

View File

@@ -6,7 +6,6 @@ const debug = require('debug')('acme-client');
let logger = () => {}; let logger = () => {};
/** /**
* Set logger function * Set logger function
* *
@@ -17,11 +16,10 @@ exports.setLogger = (fn) => {
logger = fn; logger = fn;
}; };
/** /**
* Log message * Log message
* *
* @param {string} Message * @param {string} msg Message
*/ */
exports.log = (msg) => { exports.log = (msg) => {

View File

@@ -7,7 +7,6 @@ const dns = require('dns').promises;
const { readCertificateInfo, splitPemChain } = require('./crypto'); const { readCertificateInfo, splitPemChain } = require('./crypto');
const { log } = require('./logger'); const { log } = require('./logger');
/** /**
* Exponential backoff * Exponential backoff
* *
@@ -26,7 +25,6 @@ class Backoff {
this.attempts = 0; this.attempts = 0;
} }
/** /**
* Get backoff duration * Get backoff duration
* *
@@ -40,7 +38,6 @@ class Backoff {
} }
} }
/** /**
* Retry promise * Retry promise
* *
@@ -70,7 +67,6 @@ async function retryPromise(fn, attempts, backoff) {
} }
} }
/** /**
* Retry promise * Retry promise
* *
@@ -87,11 +83,13 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
return retryPromise(fn, attempts, backoff); return retryPromise(fn, attempts, backoff);
} }
/** /**
* Parse URLs from link header * Parse URLs from Link header
* *
* @param {string} header Link header contents * https://datatracker.ietf.org/doc/html/rfc8555#section-7.4.2
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link
*
* @param {string} header Header contents
* @param {string} rel Link relation, default: `alternate` * @param {string} rel Link relation, default: `alternate`
* @returns {string[]} Array of URLs * @returns {string[]} Array of URLs
*/ */
@@ -107,6 +105,36 @@ function parseLinkHeader(header, rel = 'alternate') {
return results.filter((r) => r); return results.filter((r) => r);
} }
/**
* Parse date or duration from Retry-After header
*
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
*
* @param {string} header Header contents
* @returns {number} Retry duration in seconds
*/
function parseRetryAfterHeader(header) {
const sec = parseInt(header, 10);
const date = new Date(header);
/* Seconds into the future */
if (Number.isSafeInteger(sec) && (sec > 0)) {
return sec;
}
/* Future date string */
if (date instanceof Date && !Number.isNaN(date)) {
const now = new Date();
const diff = Math.ceil((date.getTime() - now.getTime()) / 1000);
if (diff > 0) {
return diff;
}
}
return 0;
}
/** /**
* Find certificate chain with preferred issuer common name * Find certificate chain with preferred issuer common name
@@ -157,7 +185,6 @@ function findCertificateChainForIssuer(chains, issuer) {
return chains[0]; return chains[0];
} }
/** /**
* Find and format error in response object * Find and format error in response object
* *
@@ -168,16 +195,17 @@ function findCertificateChainForIssuer(chains, issuer) {
function formatResponseError(resp) { function formatResponseError(resp) {
let result; let result;
if (resp.data) {
if (resp.data.error) { if (resp.data.error) {
result = resp.data.error.detail || resp.data.error; result = resp.data.error.detail || resp.data.error;
} }
else { else {
result = resp.data.detail || JSON.stringify(resp.data); result = resp.data.detail || JSON.stringify(resp.data);
} }
return result.replace(/\n/g, '');
} }
return (result || '').replace(/\n/g, '');
}
/** /**
* Resolve root domain name by looking for SOA record * Resolve root domain name by looking for SOA record
@@ -204,7 +232,6 @@ async function resolveDomainBySoaRecord(recordName) {
} }
} }
/** /**
* Get DNS resolver using domains authoritative NS records * Get DNS resolver using domains authoritative NS records
* *
@@ -245,7 +272,6 @@ async function getAuthoritativeDnsResolver(recordName) {
return resolver; return resolver;
} }
/** /**
* Attempt to retrieve TLS ALPN certificate from peer * Attempt to retrieve TLS ALPN certificate from peer
* *
@@ -267,7 +293,7 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
port, port,
servername: host, servername: host,
rejectUnauthorized: false, rejectUnauthorized: false,
ALPNProtocols: ['acme-tls/1'] ALPNProtocols: ['acme-tls/1'],
}); });
socket.setTimeout(timeout); socket.setTimeout(timeout);
@@ -299,7 +325,6 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
}); });
} }
/** /**
* Export utils * Export utils
*/ */
@@ -307,8 +332,9 @@ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
module.exports = { module.exports = {
retry, retry,
parseLinkHeader, parseLinkHeader,
parseRetryAfterHeader,
findCertificateChainForIssuer, findCertificateChainForIssuer,
formatResponseError, formatResponseError,
getAuthoritativeDnsResolver, getAuthoritativeDnsResolver,
retrieveTlsAlpnCertificate retrieveTlsAlpnCertificate,
}; };

View File

@@ -9,7 +9,6 @@ const axios = require('./axios');
const util = require('./util'); const util = require('./util');
const { isAlpnCertificateAuthorizationValid } = require('./crypto'); const { isAlpnCertificateAuthorizationValid } = require('./crypto');
/** /**
* Verify ACME HTTP challenge * Verify ACME HTTP challenge
* *
@@ -43,7 +42,6 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
return true; return true;
} }
/** /**
* Walk DNS until TXT records are found * Walk DNS until TXT records are found
*/ */
@@ -81,7 +79,6 @@ async function walkDnsChallengeRecord(recordName, resolver = dns) {
throw new Error(`No TXT records found for name: ${recordName}`); throw new Error(`No TXT records found for name: ${recordName}`);
} }
/** /**
* Verify ACME DNS challenge * Verify ACME DNS challenge
* *
@@ -121,7 +118,6 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
return true; return true;
} }
/** /**
* Verify ACME TLS ALPN challenge * Verify ACME TLS ALPN challenge
* *
@@ -149,7 +145,6 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
return true; return true;
} }
/** /**
* Export API * Export API
*/ */
@@ -157,5 +152,5 @@ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
module.exports = { module.exports = {
'http-01': verifyHttpChallenge, 'http-01': verifyHttpChallenge,
'dns-01': verifyDnsChallenge, 'dns-01': verifyDnsChallenge,
'tls-alpn-01': verifyTlsAlpnChallenge 'tls-alpn-01': verifyTlsAlpnChallenge,
}; };

View File

@@ -16,7 +16,6 @@ const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443; const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443; const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
describe('pebble', () => { describe('pebble', () => {
const httpsAgent = new https.Agent({ rejectUnauthorized: false }); const httpsAgent = new https.Agent({ rejectUnauthorized: false });
@@ -39,7 +38,6 @@ describe('pebble', () => {
const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`; const testTlsAlpn01ChallengeHost = `${uuid()}.${domainName}`;
const testTlsAlpn01ChallengeValue = uuid(); const testTlsAlpn01ChallengeValue = uuid();
/** /**
* Pebble CTS required * Pebble CTS required
*/ */
@@ -50,7 +48,6 @@ describe('pebble', () => {
} }
}); });
/** /**
* DNS mocking * DNS mocking
*/ */
@@ -92,7 +89,6 @@ describe('pebble', () => {
}); });
}); });
/** /**
* HTTP-01 challenge response * HTTP-01 challenge response
*/ */
@@ -118,7 +114,6 @@ describe('pebble', () => {
}); });
}); });
/** /**
* HTTPS-01 challenge response * HTTPS-01 challenge response
*/ */
@@ -143,7 +138,7 @@ describe('pebble', () => {
/* Assert HTTP 302 */ /* Assert HTTP 302 */
const resp = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, { const resp = await axios.get(`http://${testHttps01ChallengeHost}:${httpPort}/.well-known/acme-challenge/${testHttps01ChallengeToken}`, {
maxRedirects: 0, maxRedirects: 0,
validateStatus: null validateStatus: null,
}); });
assert.strictEqual(resp.status, 302); assert.strictEqual(resp.status, 302);
@@ -165,7 +160,6 @@ describe('pebble', () => {
}); });
}); });
/** /**
* DNS-01 challenge response * DNS-01 challenge response
*/ */
@@ -188,7 +182,6 @@ describe('pebble', () => {
}); });
}); });
/** /**
* TLS-ALPN-01 challenge response * TLS-ALPN-01 challenge response
*/ */

View File

@@ -9,41 +9,17 @@ const axios = require('./../src/axios');
const HttpClient = require('./../src/http'); const HttpClient = require('./../src/http');
const pkg = require('./../package.json'); const pkg = require('./../package.json');
describe('http', () => { describe('http', () => {
let testClient; let testClient;
const endpoint = `http://${uuid()}.example.com`;
const defaultUserAgent = `node-${pkg.name}/${pkg.version}`; const defaultUserAgent = `node-${pkg.name}/${pkg.version}`;
const customUserAgent = 'custom-ua-123'; const customUserAgent = 'custom-ua-123';
const primaryEndpoint = `http://${uuid()}.example.com`; afterEach(() => {
const defaultUaEndpoint = `http://${uuid()}.example.com`; nock.cleanAll();
const customUaEndpoint = `http://${uuid()}.example.com`;
/**
* HTTP mocking
*/
before(() => {
const defaultUaOpts = { reqheaders: { 'User-Agent': defaultUserAgent } };
const customUaOpts = { reqheaders: { 'User-Agent': customUserAgent } };
nock(primaryEndpoint)
.persist().get('/').reply(200, 'ok');
nock(defaultUaEndpoint, defaultUaOpts)
.persist().get('/').reply(200, 'ok');
nock(customUaEndpoint, customUaOpts)
.persist().get('/').reply(200, 'ok');
}); });
after(() => {
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
});
/** /**
* Initialize * Initialize
*/ */
@@ -52,47 +28,94 @@ describe('http', () => {
testClient = new HttpClient(); testClient = new HttpClient();
}); });
/** /**
* HTTP verbs * HTTP verbs
*/ */
it('should http get', async () => { it('should http get', async () => {
const resp = await testClient.request(primaryEndpoint, 'get'); nock(endpoint).get('/').reply(200, 'ok');
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp); assert.isObject(resp);
assert.strictEqual(resp.status, 200); assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok'); assert.strictEqual(resp.data, 'ok');
}); });
/** /**
* User-Agent * User-Agent
*/ */
it('should request using default user-agent', async () => { it('should request using default user-agent', async () => {
const resp = await testClient.request(defaultUaEndpoint, 'get'); nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok');
axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp); assert.isObject(resp);
assert.strictEqual(resp.status, 200); assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok'); assert.strictEqual(resp.data, 'ok');
}); });
it('should not request using custom user-agent', async () => { it('should reject using custom user-agent', async () => {
await assert.isRejected(testClient.request(customUaEndpoint, 'get')); nock(endpoint).matchHeader('user-agent', defaultUserAgent).get('/').reply(200, 'ok');
axios.defaults.headers.common['User-Agent'] = customUserAgent;
await assert.isRejected(testClient.request(endpoint, 'get'));
}); });
it('should request using custom user-agent', async () => { it('should request using custom user-agent', async () => {
nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok');
axios.defaults.headers.common['User-Agent'] = customUserAgent; axios.defaults.headers.common['User-Agent'] = customUserAgent;
const resp = await testClient.request(customUaEndpoint, 'get'); const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp); assert.isObject(resp);
assert.strictEqual(resp.status, 200); assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok'); assert.strictEqual(resp.data, 'ok');
}); });
it('should not request using default user-agent', async () => { it('should reject using default user-agent', async () => {
axios.defaults.headers.common['User-Agent'] = customUserAgent; nock(endpoint).matchHeader('user-agent', customUserAgent).get('/').reply(200, 'ok');
await assert.isRejected(testClient.request(defaultUaEndpoint, 'get')); axios.defaults.headers.common['User-Agent'] = defaultUserAgent;
await assert.isRejected(testClient.request(endpoint, 'get'));
});
/**
* Retry on HTTP errors
*/
it('should retry on 429 rate limit', async () => {
let rateLimitCount = 0;
nock(endpoint).persist().get('/').reply(() => {
rateLimitCount += 1;
if (rateLimitCount < 3) {
return [429, 'Rate Limit Exceeded', { 'Retry-After': 1 }];
}
return [200, 'ok'];
});
assert.strictEqual(rateLimitCount, 0);
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 200);
assert.strictEqual(resp.data, 'ok');
assert.strictEqual(rateLimitCount, 3);
});
it('should retry on 5xx server error', async () => {
let serverErrorCount = 0;
nock(endpoint).persist().get('/').reply(() => {
serverErrorCount += 1;
return [500, 'Internal Server Error', { 'Retry-After': 1 }];
});
assert.strictEqual(serverErrorCount, 0);
const resp = await testClient.request(endpoint, 'get');
assert.isObject(resp);
assert.strictEqual(resp.status, 500);
assert.strictEqual(serverErrorCount, 4);
}); });
}); });

View File

@@ -5,7 +5,6 @@
const { assert } = require('chai'); const { assert } = require('chai');
const logger = require('./../src/logger'); const logger = require('./../src/logger');
describe('logger', () => { describe('logger', () => {
let lastLogMessage = null; let lastLogMessage = null;
@@ -13,7 +12,6 @@ describe('logger', () => {
lastLogMessage = msg; lastLogMessage = msg;
} }
/** /**
* Logger * Logger
*/ */
@@ -23,7 +21,6 @@ describe('logger', () => {
assert.isNull(lastLogMessage); assert.isNull(lastLogMessage);
}); });
it('should log with custom logger', () => { it('should log with custom logger', () => {
logger.setLogger(customLoggerFn); logger.setLogger(customLoggerFn);

View File

@@ -0,0 +1,145 @@
/**
* Utility method tests
*/
const dns = require('dns').promises;
const fs = require('fs').promises;
const path = require('path');
const { assert } = require('chai');
const util = require('./../src/util');
const { readCertificateInfo } = require('./../src/crypto');
describe('util', () => {
const testCertPath1 = path.join(__dirname, 'fixtures', 'certificate.crt');
const testCertPath2 = path.join(__dirname, 'fixtures', 'letsencrypt.crt');
it('retry()', async () => {
let attempts = 0;
const backoffOpts = {
min: 100,
max: 500,
};
await assert.isRejected(util.retry(() => {
throw new Error('oops');
}, backoffOpts));
const r = await util.retry(() => {
attempts += 1;
if (attempts < 3) {
throw new Error('oops');
}
return 'abc';
}, backoffOpts);
assert.strictEqual(r, 'abc');
assert.strictEqual(attempts, 3);
});
it('parseLinkHeader()', () => {
const r1 = util.parseLinkHeader('<https://example.com/a>;rel="alternate"');
assert.isArray(r1);
assert.strictEqual(r1.length, 1);
assert.strictEqual(r1[0], 'https://example.com/a');
const r2 = util.parseLinkHeader('<https://example.com/b>;rel="test"');
assert.isArray(r2);
assert.strictEqual(r2.length, 0);
const r3 = util.parseLinkHeader('<http://example.com/c>; rel="test"', 'test');
assert.isArray(r3);
assert.strictEqual(r3.length, 1);
assert.strictEqual(r3[0], 'http://example.com/c');
const r4 = util.parseLinkHeader(`<https://example.com/a>; rel="alternate",
<https://example.com/x>; rel="nope",
<https://example.com/b>;rel="alternate",
<https://example.com/c>; rel="alternate"`);
assert.isArray(r4);
assert.strictEqual(r4.length, 3);
assert.strictEqual(r4[0], 'https://example.com/a');
assert.strictEqual(r4[1], 'https://example.com/b');
assert.strictEqual(r4[2], 'https://example.com/c');
});
it('parseRetryAfterHeader()', () => {
const r1 = util.parseRetryAfterHeader('');
assert.strictEqual(r1, 0);
const r2 = util.parseRetryAfterHeader('abcdef');
assert.strictEqual(r2, 0);
const r3 = util.parseRetryAfterHeader('123');
assert.strictEqual(r3, 123);
const r4 = util.parseRetryAfterHeader('123.456');
assert.strictEqual(r4, 123);
const r5 = util.parseRetryAfterHeader('-555');
assert.strictEqual(r5, 0);
const r6 = util.parseRetryAfterHeader('Wed, 21 Oct 2015 07:28:00 GMT');
assert.strictEqual(r6, 0);
const now = new Date();
const future = new Date(now.getTime() + 123000);
const r7 = util.parseRetryAfterHeader(future.toUTCString());
assert.isTrue(r7 > 100);
});
it('findCertificateChainForIssuer()', async () => {
const certs = [
(await fs.readFile(testCertPath1)).toString(),
(await fs.readFile(testCertPath2)).toString(),
];
const r1 = util.findCertificateChainForIssuer(certs, 'abc123');
const r2 = util.findCertificateChainForIssuer(certs, 'example.com');
const r3 = util.findCertificateChainForIssuer(certs, 'E6');
[r1, r2, r3].forEach((r) => {
assert.isString(r);
assert.isNotEmpty(r);
});
assert.strictEqual(readCertificateInfo(r1).issuer.commonName, 'example.com');
assert.strictEqual(readCertificateInfo(r2).issuer.commonName, 'example.com');
assert.strictEqual(readCertificateInfo(r3).issuer.commonName, 'E6');
});
it('formatResponseError()', () => {
const e1 = util.formatResponseError({ data: { error: 'aaa' } });
assert.strictEqual(e1, 'aaa');
const e2 = util.formatResponseError({ data: { error: { detail: 'bbb' } } });
assert.strictEqual(e2, 'bbb');
const e3 = util.formatResponseError({ data: { detail: 'ccc' } });
assert.strictEqual(e3, 'ccc');
const e4 = util.formatResponseError({ data: { a: 123 } });
assert.strictEqual(e4, '{"a":123}');
const e5 = util.formatResponseError({});
assert.isString(e5);
assert.isEmpty(e5);
});
it('getAuthoritativeDnsResolver()', async () => {
/* valid domain - should not use global default */
const r1 = await util.getAuthoritativeDnsResolver('example.com');
assert.instanceOf(r1, dns.Resolver);
assert.isNotEmpty(r1.getServers());
assert.notDeepEqual(r1.getServers(), dns.getServers());
/* invalid domain - fallback to global default */
const r2 = await util.getAuthoritativeDnsResolver('invalid.xtldx');
assert.instanceOf(r2, dns.Resolver);
assert.deepStrictEqual(r2.getServers(), dns.getServers());
});
/* TODO: Figure out how to test this */
it('retrieveTlsAlpnCertificate()');
});

View File

@@ -9,7 +9,6 @@ const verify = require('./../src/verify');
const domainName = process.env.ACME_DOMAIN_NAME || 'example.com'; const domainName = process.env.ACME_DOMAIN_NAME || 'example.com';
describe('verify', () => { describe('verify', () => {
const challengeTypes = ['http-01', 'dns-01']; const challengeTypes = ['http-01', 'dns-01'];
@@ -30,7 +29,6 @@ describe('verify', () => {
const testTlsAlpn01Challenge = { type: 'dns-01', status: 'pending', token: uuid() }; const testTlsAlpn01Challenge = { type: 'dns-01', status: 'pending', token: uuid() };
const testTlsAlpn01Key = uuid(); const testTlsAlpn01Key = uuid();
/** /**
* Pebble CTS required * Pebble CTS required
*/ */
@@ -41,7 +39,6 @@ describe('verify', () => {
} }
}); });
/** /**
* API * API
*/ */
@@ -50,7 +47,6 @@ describe('verify', () => {
assert.containsAllKeys(verify, challengeTypes); assert.containsAllKeys(verify, challengeTypes);
}); });
/** /**
* http-01 * http-01
*/ */
@@ -81,7 +77,6 @@ describe('verify', () => {
}); });
}); });
/** /**
* https-01 * https-01
*/ */
@@ -102,7 +97,6 @@ describe('verify', () => {
}); });
}); });
/** /**
* dns-01 * dns-01
*/ */
@@ -133,7 +127,6 @@ describe('verify', () => {
}); });
}); });
/** /**
* tls-alpn-01 * tls-alpn-01
*/ */

View File

@@ -9,10 +9,9 @@ const spec = require('./spec');
const forge = require('./../src/crypto/forge'); const forge = require('./../src/crypto/forge');
const cryptoEngines = { const cryptoEngines = {
forge forge,
}; };
describe('crypto-legacy', () => { describe('crypto-legacy', () => {
let testPemKey; let testPemKey;
let testCert; let testCert;
@@ -28,7 +27,6 @@ describe('crypto-legacy', () => {
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt'); const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt'); const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
/** /**
* Fixtures * Fixtures
*/ */
@@ -50,7 +48,6 @@ describe('crypto-legacy', () => {
}); });
}); });
/** /**
* Engines * Engines
*/ */
@@ -62,7 +59,6 @@ describe('crypto-legacy', () => {
let testNonCnCsr; let testNonCnCsr;
let testNonAsciiCsr; let testNonAsciiCsr;
/** /**
* Key generation * Key generation
*/ */
@@ -83,14 +79,13 @@ describe('crypto-legacy', () => {
publicKeyStore.push(key.toString().replace(/[\r\n]/gm, '')); publicKeyStore.push(key.toString().replace(/[\r\n]/gm, ''));
}); });
/** /**
* Certificate Signing Request * Certificate Signing Request
*/ */
it('should generate a csr', async () => { it('should generate a csr', async () => {
const [key, csr] = await engine.createCsr({ const [key, csr] = await engine.createCsr({
commonName: testCsrDomain commonName: testCsrDomain,
}); });
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -102,7 +97,7 @@ describe('crypto-legacy', () => {
it('should generate a san csr', async () => { it('should generate a san csr', async () => {
const [key, csr] = await engine.createCsr({ const [key, csr] = await engine.createCsr({
commonName: testSanCsrDomains[0], commonName: testSanCsrDomains[0],
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length) altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length),
}); });
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -113,7 +108,7 @@ describe('crypto-legacy', () => {
it('should generate a csr without common name', async () => { it('should generate a csr without common name', async () => {
const [key, csr] = await engine.createCsr({ const [key, csr] = await engine.createCsr({
altNames: testSanCsrDomains altNames: testSanCsrDomains,
}); });
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -126,7 +121,7 @@ describe('crypto-legacy', () => {
const [key, csr] = await engine.createCsr({ const [key, csr] = await engine.createCsr({
commonName: testCsrDomain, commonName: testCsrDomain,
organization: '大安區', organization: '大安區',
organizationUnit: '中文部門' organizationUnit: '中文部門',
}); });
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -167,7 +162,6 @@ describe('crypto-legacy', () => {
assert.deepStrictEqual(result.altNames, [testCsrDomain]); assert.deepStrictEqual(result.altNames, [testCsrDomain]);
}); });
/** /**
* Certificate * Certificate
*/ */
@@ -188,7 +182,6 @@ describe('crypto-legacy', () => {
assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length)); assert.deepEqual(info.domains.altNames, testSanCsrDomains.slice(1, testSanCsrDomains.length));
}); });
/** /**
* PEM utils * PEM utils
*/ */
@@ -214,7 +207,6 @@ describe('crypto-legacy', () => {
}); });
}); });
/** /**
* Modulus and exponent * Modulus and exponent
*/ */
@@ -246,7 +238,6 @@ describe('crypto-legacy', () => {
}); });
}); });
/** /**
* Verify identical results * Verify identical results
*/ */

View File

@@ -50,7 +50,6 @@ dGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0aW5ndGVzdGluZ3Rlc3Rpbmd0ZXN0
-----END TEST----- -----END TEST-----
`; `;
describe('crypto', () => { describe('crypto', () => {
const testCsrDomain = 'example.com'; const testCsrDomain = 'example.com';
const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com']; const testSanCsrDomains = ['example.com', 'test.example.com', 'abc.example.com'];
@@ -58,7 +57,6 @@ describe('crypto', () => {
const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt'); const testCertPath = path.join(__dirname, 'fixtures', 'certificate.crt');
const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt'); const testSanCertPath = path.join(__dirname, 'fixtures', 'san-certificate.crt');
/** /**
* Key types * Key types
*/ */
@@ -68,24 +66,23 @@ describe('crypto', () => {
createKeyFns: { createKeyFns: {
s1024: () => crypto.createPrivateRsaKey(1024), s1024: () => crypto.createPrivateRsaKey(1024),
s2048: () => crypto.createPrivateRsaKey(), s2048: () => crypto.createPrivateRsaKey(),
s4096: () => crypto.createPrivateRsaKey(4096) s4096: () => crypto.createPrivateRsaKey(4096),
}, },
jwkSpecFn: spec.jwk.rsa jwkSpecFn: spec.jwk.rsa,
}, },
ecdsa: { ecdsa: {
createKeyFns: { createKeyFns: {
p256: () => crypto.createPrivateEcdsaKey(), p256: () => crypto.createPrivateEcdsaKey(),
p384: () => crypto.createPrivateEcdsaKey('P-384'), p384: () => crypto.createPrivateEcdsaKey('P-384'),
p521: () => crypto.createPrivateEcdsaKey('P-521') p521: () => crypto.createPrivateEcdsaKey('P-521'),
},
jwkSpecFn: spec.jwk.ecdsa,
}, },
jwkSpecFn: spec.jwk.ecdsa
}
}).forEach(([name, { createKeyFns, jwkSpecFn }]) => { }).forEach(([name, { createKeyFns, jwkSpecFn }]) => {
describe(name, () => { describe(name, () => {
const testPrivateKeys = {}; const testPrivateKeys = {};
const testPublicKeys = {}; const testPublicKeys = {};
/** /**
* Iterate through all generator variations * Iterate through all generator variations
*/ */
@@ -97,7 +94,6 @@ describe('crypto', () => {
let testNonAsciiCsr; let testNonAsciiCsr;
let testAlpnCertificate; let testAlpnCertificate;
/** /**
* Keys and JWK * Keys and JWK
*/ */
@@ -132,14 +128,13 @@ describe('crypto', () => {
jwkSpecFn(jwk); jwkSpecFn(jwk);
}); });
/** /**
* Certificate Signing Request * Certificate Signing Request
*/ */
it(`${n}/should generate a csr`, async () => { it(`${n}/should generate a csr`, async () => {
const [key, csr] = await crypto.createCsr({ const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain commonName: testCsrDomain,
}, testPrivateKeys[n]); }, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -151,7 +146,7 @@ describe('crypto', () => {
it(`${n}/should generate a san csr`, async () => { it(`${n}/should generate a san csr`, async () => {
const [key, csr] = await crypto.createCsr({ const [key, csr] = await crypto.createCsr({
commonName: testSanCsrDomains[0], commonName: testSanCsrDomains[0],
altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length) altNames: testSanCsrDomains.slice(1, testSanCsrDomains.length),
}, testPrivateKeys[n]); }, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -162,7 +157,7 @@ describe('crypto', () => {
it(`${n}/should generate a csr without common name`, async () => { it(`${n}/should generate a csr without common name`, async () => {
const [key, csr] = await crypto.createCsr({ const [key, csr] = await crypto.createCsr({
altNames: testSanCsrDomains altNames: testSanCsrDomains,
}, testPrivateKeys[n]); }, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -175,7 +170,7 @@ describe('crypto', () => {
const [key, csr] = await crypto.createCsr({ const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain, commonName: testCsrDomain,
organization: '大安區', organization: '大安區',
organizationUnit: '中文部門' organizationUnit: '中文部門',
}, testPrivateKeys[n]); }, testPrivateKeys[n]);
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -186,7 +181,7 @@ describe('crypto', () => {
it(`${n}/should generate a csr with key as string`, async () => { it(`${n}/should generate a csr with key as string`, async () => {
const [key, csr] = await crypto.createCsr({ const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain commonName: testCsrDomain,
}, testPrivateKeys[n].toString()); }, testPrivateKeys[n].toString());
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
@@ -195,11 +190,10 @@ describe('crypto', () => {
it(`${n}/should throw with invalid key`, async () => { it(`${n}/should throw with invalid key`, async () => {
await assert.isRejected(crypto.createCsr({ await assert.isRejected(crypto.createCsr({
commonName: testCsrDomain commonName: testCsrDomain,
}, testPublicKeys[n])); }, testPublicKeys[n]));
}); });
/** /**
* Domain and info resolver * Domain and info resolver
*/ */
@@ -243,7 +237,6 @@ describe('crypto', () => {
}); });
}); });
/** /**
* ALPN * ALPN
*/ */
@@ -284,7 +277,6 @@ describe('crypto', () => {
}); });
}); });
/** /**
* Common functionality * Common functionality
*/ */
@@ -294,7 +286,6 @@ describe('crypto', () => {
let testCert; let testCert;
let testSanCert; let testSanCert;
it('should read private key fixture', async () => { it('should read private key fixture', async () => {
testPemKey = await fs.readFile(testKeyPath); testPemKey = await fs.readFile(testKeyPath);
assert.isTrue(Buffer.isBuffer(testPemKey)); assert.isTrue(Buffer.isBuffer(testPemKey));
@@ -310,21 +301,19 @@ describe('crypto', () => {
assert.isTrue(Buffer.isBuffer(testSanCert)); assert.isTrue(Buffer.isBuffer(testSanCert));
}); });
/** /**
* CSR with auto-generated key * CSR with auto-generated key
*/ */
it('should generate a csr with default key', async () => { it('should generate a csr with default key', async () => {
const [key, csr] = await crypto.createCsr({ const [key, csr] = await crypto.createCsr({
commonName: testCsrDomain commonName: testCsrDomain,
}); });
assert.isTrue(Buffer.isBuffer(key)); assert.isTrue(Buffer.isBuffer(key));
assert.isTrue(Buffer.isBuffer(csr)); assert.isTrue(Buffer.isBuffer(csr));
}); });
/** /**
* Certificate * Certificate
*/ */
@@ -352,7 +341,6 @@ describe('crypto', () => {
}); });
}); });
/** /**
* ALPN * ALPN
*/ */
@@ -365,7 +353,6 @@ describe('crypto', () => {
assert.isTrue(Buffer.isBuffer(cert)); assert.isTrue(Buffer.isBuffer(cert));
}); });
/** /**
* PEM utils * PEM utils
*/ */

View File

@@ -20,24 +20,22 @@ const clientOpts = {
directoryUrl, directoryUrl,
backoffAttempts: 5, backoffAttempts: 5,
backoffMin: 1000, backoffMin: 1000,
backoffMax: 5000 backoffMax: 5000,
}; };
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) { if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
clientOpts.externalAccountBinding = { clientOpts.externalAccountBinding = {
kid: process.env.ACME_EAB_KID, kid: process.env.ACME_EAB_KID,
hmacKey: process.env.ACME_EAB_HMAC_KEY hmacKey: process.env.ACME_EAB_HMAC_KEY,
}; };
} }
describe('client', () => { describe('client', () => {
const testDomain = `${uuid()}.${domainName}`; const testDomain = `${uuid()}.${domainName}`;
const testDomainAlpn = `${uuid()}.${domainName}`; const testDomainAlpn = `${uuid()}.${domainName}`;
const testDomainWildcard = `*.${testDomain}`; const testDomainWildcard = `*.${testDomain}`;
const testContact = `mailto:test-${uuid()}@nope.com`; const testContact = `mailto:test-${uuid()}@nope.com`;
/** /**
* Pebble CTS required * Pebble CTS required
*/ */
@@ -48,7 +46,6 @@ describe('client', () => {
} }
}); });
/** /**
* Key types * Key types
*/ */
@@ -58,18 +55,18 @@ describe('client', () => {
createKeyFn: () => acme.crypto.createPrivateRsaKey(), createKeyFn: () => acme.crypto.createPrivateRsaKey(),
createKeyAltFns: { createKeyAltFns: {
s1024: () => acme.crypto.createPrivateRsaKey(1024), s1024: () => acme.crypto.createPrivateRsaKey(1024),
s4096: () => acme.crypto.createPrivateRsaKey(4096) s4096: () => acme.crypto.createPrivateRsaKey(4096),
}, },
jwkSpecFn: spec.jwk.rsa jwkSpecFn: spec.jwk.rsa,
}, },
ecdsa: { ecdsa: {
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(), createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
createKeyAltFns: { createKeyAltFns: {
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'), p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
p521: () => acme.crypto.createPrivateEcdsaKey('P-521') p521: () => acme.crypto.createPrivateEcdsaKey('P-521'),
},
jwkSpecFn: spec.jwk.ecdsa,
}, },
jwkSpecFn: spec.jwk.ecdsa
}
}).forEach(([name, { createKeyFn, createKeyAltFns, jwkSpecFn }]) => { }).forEach(([name, { createKeyFn, createKeyAltFns, jwkSpecFn }]) => {
describe(name, () => { describe(name, () => {
let testIssuers; let testIssuers;
@@ -97,7 +94,6 @@ describe('client', () => {
let testCertificateAlpn; let testCertificateAlpn;
let testCertificateWildcard; let testCertificateWildcard;
/** /**
* Fixtures * Fixtures
*/ */
@@ -114,8 +110,8 @@ describe('client', () => {
it('should generate certificate signing request', async () => { it('should generate certificate signing request', async () => {
[, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn()); [, testCsr] = await acme.crypto.createCsr({ commonName: testDomain }, await createKeyFn());
[, testCsrAlpn] = await acme.crypto.createCsr({ commonName: testDomainAlpn }, await createKeyFn()); [, testCsrAlpn] = await acme.crypto.createCsr({ altNames: [testDomainAlpn] }, await createKeyFn());
[, testCsrWildcard] = await acme.crypto.createCsr({ commonName: testDomainWildcard }, await createKeyFn()); [, testCsrWildcard] = await acme.crypto.createCsr({ altNames: [testDomainWildcard] }, await createKeyFn());
}); });
it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () { it('should resolve certificate issuers [ACME_CAP_ALTERNATE_CERT_ROOTS]', async function () {
@@ -134,7 +130,6 @@ describe('client', () => {
}); });
}); });
/** /**
* Initialize clients * Initialize clients
*/ */
@@ -142,7 +137,7 @@ describe('client', () => {
it('should initialize client', () => { it('should initialize client', () => {
testClient = new acme.Client({ testClient = new acme.Client({
...clientOpts, ...clientOpts,
accountKey: testAccountKey accountKey: testAccountKey,
}); });
}); });
@@ -151,7 +146,6 @@ describe('client', () => {
jwkSpecFn(jwk); jwkSpecFn(jwk);
}); });
/** /**
* Terms of Service * Terms of Service
*/ */
@@ -174,7 +168,6 @@ describe('client', () => {
assert.isNull(tos); assert.isNull(tos);
}); });
/** /**
* Create account * Create account
*/ */
@@ -195,17 +188,17 @@ describe('client', () => {
const client = new acme.Client({ const client = new acme.Client({
...clientOpts, ...clientOpts,
accountKey: testAccountKey, accountKey: testAccountKey,
externalAccountBinding: null externalAccountBinding: null,
}); });
await assert.isRejected(client.createAccount({ await assert.isRejected(client.createAccount({
termsOfServiceAgreed: true termsOfServiceAgreed: true,
})); }));
}); });
it('should create an account', async () => { it('should create an account', async () => {
testAccount = await testClient.createAccount({ testAccount = await testClient.createAccount({
termsOfServiceAgreed: true termsOfServiceAgreed: true,
}); });
spec.rfc8555.account(testAccount); spec.rfc8555.account(testAccount);
@@ -217,7 +210,6 @@ describe('client', () => {
assert.isString(testAccountUrl); assert.isString(testAccountUrl);
}); });
/** /**
* Create account with alternate key sizes * Create account with alternate key sizes
*/ */
@@ -226,11 +218,11 @@ describe('client', () => {
it(`should create account with key=${k}`, async () => { it(`should create account with key=${k}`, async () => {
const client = new acme.Client({ const client = new acme.Client({
...clientOpts, ...clientOpts,
accountKey: await altKeyFn() accountKey: await altKeyFn(),
}); });
const account = await client.createAccount({ const account = await client.createAccount({
termsOfServiceAgreed: true termsOfServiceAgreed: true,
}); });
spec.rfc8555.account(account); spec.rfc8555.account(account);
@@ -238,7 +230,6 @@ describe('client', () => {
}); });
}); });
/** /**
* Find existing account using secondary client * Find existing account using secondary client
*/ */
@@ -246,22 +237,22 @@ describe('client', () => {
it('should throw when trying to find account using invalid account key', async () => { it('should throw when trying to find account using invalid account key', async () => {
const client = new acme.Client({ const client = new acme.Client({
...clientOpts, ...clientOpts,
accountKey: testAccountSecondaryKey accountKey: testAccountSecondaryKey,
}); });
await assert.isRejected(client.createAccount({ await assert.isRejected(client.createAccount({
onlyReturnExisting: true onlyReturnExisting: true,
})); }));
}); });
it('should find existing account using account key', async () => { it('should find existing account using account key', async () => {
const client = new acme.Client({ const client = new acme.Client({
...clientOpts, ...clientOpts,
accountKey: testAccountKey accountKey: testAccountKey,
}); });
const account = await client.createAccount({ const account = await client.createAccount({
onlyReturnExisting: true onlyReturnExisting: true,
}); });
spec.rfc8555.account(account); spec.rfc8555.account(account);
@@ -269,7 +260,6 @@ describe('client', () => {
assert.deepStrictEqual(account.key, testAccount.key); assert.deepStrictEqual(account.key, testAccount.key);
}); });
/** /**
* Account URL * Account URL
*/ */
@@ -278,7 +268,7 @@ describe('client', () => {
const client = new acme.Client({ const client = new acme.Client({
...clientOpts, ...clientOpts,
accountKey: testAccountKey, accountKey: testAccountKey,
accountUrl: 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/1' accountUrl: 'https://acme-staging-v02.api.letsencrypt.org/acme/acct/1',
}); });
await assert.isRejected(client.updateAccount()); await assert.isRejected(client.updateAccount());
@@ -288,11 +278,11 @@ describe('client', () => {
const client = new acme.Client({ const client = new acme.Client({
...clientOpts, ...clientOpts,
accountKey: testAccountKey, accountKey: testAccountKey,
accountUrl: testAccountUrl accountUrl: testAccountUrl,
}); });
const account = await client.createAccount({ const account = await client.createAccount({
onlyReturnExisting: true onlyReturnExisting: true,
}); });
spec.rfc8555.account(account); spec.rfc8555.account(account);
@@ -300,7 +290,6 @@ describe('client', () => {
assert.deepStrictEqual(account.key, testAccount.key); assert.deepStrictEqual(account.key, testAccount.key);
}); });
/** /**
* Update account contact info * Update account contact info
*/ */
@@ -316,7 +305,6 @@ describe('client', () => {
assert.include(account.contact, testContact); assert.include(account.contact, testContact);
}); });
/** /**
* Change account private key * Change account private key
*/ */
@@ -329,7 +317,7 @@ describe('client', () => {
await testClient.updateAccountKey(testAccountSecondaryKey); await testClient.updateAccountKey(testAccountSecondaryKey);
const account = await testClient.createAccount({ const account = await testClient.createAccount({
onlyReturnExisting: true onlyReturnExisting: true,
}); });
spec.rfc8555.account(account); spec.rfc8555.account(account);
@@ -337,7 +325,6 @@ describe('client', () => {
assert.notDeepEqual(account.key, testAccount.key); assert.notDeepEqual(account.key, testAccount.key);
}); });
/** /**
* Create new certificate order * Create new certificate order
*/ */
@@ -357,7 +344,6 @@ describe('client', () => {
}); });
}); });
/** /**
* Get status of existing certificate order * Get status of existing certificate order
*/ */
@@ -371,7 +357,6 @@ describe('client', () => {
})); }));
}); });
/** /**
* Get identifier authorization * Get identifier authorization
*/ */
@@ -401,7 +386,6 @@ describe('client', () => {
}); });
}); });
/** /**
* Generate challenge key authorization * Generate challenge key authorization
*/ */
@@ -418,7 +402,6 @@ describe('client', () => {
[testKeyAuthorization, testKeyAuthorizationAlpn, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k)); [testKeyAuthorization, testKeyAuthorizationAlpn, testKeyAuthorizationWildcard].forEach((k) => assert.isString(k));
}); });
/** /**
* Deactivate identifier authorization * Deactivate identifier authorization
*/ */
@@ -427,8 +410,8 @@ describe('client', () => {
const order = await testClient.createOrder({ const order = await testClient.createOrder({
identifiers: [ identifiers: [
{ type: 'dns', value: `${uuid()}.${domainName}` }, { type: 'dns', value: `${uuid()}.${domainName}` },
{ type: 'dns', value: `${uuid()}.${domainName}` } { type: 'dns', value: `${uuid()}.${domainName}` },
] ],
}); });
const authzCollection = await testClient.getAuthorizations(order); const authzCollection = await testClient.getAuthorizations(order);
@@ -445,7 +428,6 @@ describe('client', () => {
}); });
}); });
/** /**
* Verify satisfied challenge * Verify satisfied challenge
*/ */
@@ -460,7 +442,6 @@ describe('client', () => {
await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard); await testClient.verifyChallenge(testAuthzWildcard, testChallengeWildcard);
}); });
/** /**
* Complete challenge * Complete challenge
*/ */
@@ -474,7 +455,6 @@ describe('client', () => {
})); }));
}); });
/** /**
* Wait for valid challenge * Wait for valid challenge
*/ */
@@ -483,7 +463,6 @@ describe('client', () => {
await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c))); await Promise.all([testChallenge, testChallengeAlpn, testChallengeWildcard].map(async (c) => testClient.waitForValidStatus(c)));
}); });
/** /**
* Finalize order * Finalize order
*/ */
@@ -500,7 +479,6 @@ describe('client', () => {
assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url); assert.strictEqual(testOrderWildcard.url, finalizeWildcard.url);
}); });
/** /**
* Wait for valid order * Wait for valid order
*/ */
@@ -509,7 +487,6 @@ describe('client', () => {
await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o))); await Promise.all([testOrder, testOrderAlpn, testOrderWildcard].map(async (o) => testClient.waitForValidStatus(o)));
}); });
/** /**
* Get certificate * Get certificate
*/ */
@@ -551,7 +528,6 @@ describe('client', () => {
assert.strictEqual(testIssuers[0], info.issuer.commonName); assert.strictEqual(testIssuers[0], info.issuer.commonName);
}); });
/** /**
* Revoke certificate * Revoke certificate
*/ */
@@ -568,7 +544,6 @@ describe('client', () => {
await assert.isRejected(testClient.getCertificate(testOrderWildcard)); await assert.isRejected(testClient.getCertificate(testOrderWildcard));
}); });
/** /**
* Deactivate account * Deactivate account
*/ */
@@ -581,7 +556,6 @@ describe('client', () => {
assert.strictEqual(account.status, 'deactivated'); assert.strictEqual(account.status, 'deactivated');
}); });
/** /**
* Verify that no new orders can be made * Verify that no new orders can be made
*/ */

View File

@@ -18,17 +18,16 @@ const clientOpts = {
directoryUrl, directoryUrl,
backoffAttempts: 5, backoffAttempts: 5,
backoffMin: 1000, backoffMin: 1000,
backoffMax: 5000 backoffMax: 5000,
}; };
if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) { if (capEabEnabled && process.env.ACME_EAB_KID && process.env.ACME_EAB_HMAC_KEY) {
clientOpts.externalAccountBinding = { clientOpts.externalAccountBinding = {
kid: process.env.ACME_EAB_KID, kid: process.env.ACME_EAB_KID,
hmacKey: process.env.ACME_EAB_HMAC_KEY hmacKey: process.env.ACME_EAB_HMAC_KEY,
}; };
} }
describe('client.auto', () => { describe('client.auto', () => {
const testDomain = `${uuid()}.${domainName}`; const testDomain = `${uuid()}.${domainName}`;
const testHttpDomain = `${uuid()}.${domainName}`; const testHttpDomain = `${uuid()}.${domainName}`;
@@ -40,10 +39,9 @@ describe('client.auto', () => {
const testSanDomains = [ const testSanDomains = [
`${uuid()}.${domainName}`, `${uuid()}.${domainName}`,
`${uuid()}.${domainName}`, `${uuid()}.${domainName}`,
`${uuid()}.${domainName}` `${uuid()}.${domainName}`,
]; ];
/** /**
* Pebble CTS required * Pebble CTS required
*/ */
@@ -54,7 +52,6 @@ describe('client.auto', () => {
} }
}); });
/** /**
* Key types * Key types
*/ */
@@ -64,16 +61,16 @@ describe('client.auto', () => {
createKeyFn: () => acme.crypto.createPrivateRsaKey(), createKeyFn: () => acme.crypto.createPrivateRsaKey(),
createKeyAltFns: { createKeyAltFns: {
s1024: () => acme.crypto.createPrivateRsaKey(1024), s1024: () => acme.crypto.createPrivateRsaKey(1024),
s4096: () => acme.crypto.createPrivateRsaKey(4096) s4096: () => acme.crypto.createPrivateRsaKey(4096),
} },
}, },
ecdsa: { ecdsa: {
createKeyFn: () => acme.crypto.createPrivateEcdsaKey(), createKeyFn: () => acme.crypto.createPrivateEcdsaKey(),
createKeyAltFns: { createKeyAltFns: {
p384: () => acme.crypto.createPrivateEcdsaKey('P-384'), p384: () => acme.crypto.createPrivateEcdsaKey('P-384'),
p521: () => acme.crypto.createPrivateEcdsaKey('P-521') p521: () => acme.crypto.createPrivateEcdsaKey('P-521'),
} },
} },
}).forEach(([name, { createKeyFn, createKeyAltFns }]) => { }).forEach(([name, { createKeyFn, createKeyAltFns }]) => {
describe(name, () => { describe(name, () => {
let testIssuers; let testIssuers;
@@ -82,7 +79,6 @@ describe('client.auto', () => {
let testSanCertificate; let testSanCertificate;
let testWildcardCertificate; let testWildcardCertificate;
/** /**
* Fixtures * Fixtures
*/ */
@@ -103,7 +99,6 @@ describe('client.auto', () => {
}); });
}); });
/** /**
* Initialize client * Initialize client
*/ */
@@ -111,31 +106,30 @@ describe('client.auto', () => {
it('should initialize client', async () => { it('should initialize client', async () => {
testClient = new acme.Client({ testClient = new acme.Client({
...clientOpts, ...clientOpts,
accountKey: await createKeyFn() accountKey: await createKeyFn(),
}); });
}); });
/** /**
* Invalid challenge response * Invalid challenge response
*/ */
it('should throw on invalid challenge response', async () => { it('should throw on invalid challenge response', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}` commonName: `${uuid()}.${domainName}`,
}, await createKeyFn()); }, await createKeyFn());
await assert.isRejected(testClient.auto({ await assert.isRejected(testClient.auto({
csr, csr,
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeNoopFn, challengeCreateFn: cts.challengeNoopFn,
challengeRemoveFn: cts.challengeNoopFn challengeRemoveFn: cts.challengeNoopFn,
}), /^authorization not found/i); }), /^authorization not found/i);
}); });
it('should throw on invalid challenge response with opts.skipChallengeVerification=true', async () => { it('should throw on invalid challenge response with opts.skipChallengeVerification=true', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}` commonName: `${uuid()}.${domainName}`,
}, await createKeyFn()); }, await createKeyFn());
await assert.isRejected(testClient.auto({ await assert.isRejected(testClient.auto({
@@ -143,38 +137,37 @@ describe('client.auto', () => {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
skipChallengeVerification: true, skipChallengeVerification: true,
challengeCreateFn: cts.challengeNoopFn, challengeCreateFn: cts.challengeNoopFn,
challengeRemoveFn: cts.challengeNoopFn challengeRemoveFn: cts.challengeNoopFn,
})); }));
}); });
/** /**
* Challenge function exceptions * Challenge function exceptions
*/ */
it('should throw on challengeCreate exception', async () => { it('should throw on challengeCreate exception', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}` commonName: `${uuid()}.${domainName}`,
}, await createKeyFn()); }, await createKeyFn());
await assert.isRejected(testClient.auto({ await assert.isRejected(testClient.auto({
csr, csr,
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeThrowFn, challengeCreateFn: cts.challengeThrowFn,
challengeRemoveFn: cts.challengeNoopFn challengeRemoveFn: cts.challengeNoopFn,
}), /^oops$/); }), /^oops$/);
}); });
it('should not throw on challengeRemove exception', async () => { it('should not throw on challengeRemove exception', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}` commonName: `${uuid()}.${domainName}`,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
csr, csr,
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn, challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeThrowFn challengeRemoveFn: cts.challengeThrowFn,
}); });
assert.isString(cert); assert.isString(cert);
@@ -188,8 +181,8 @@ describe('client.auto', () => {
`${uuid()}.${domainName}`, `${uuid()}.${domainName}`,
`${uuid()}.${domainName}`, `${uuid()}.${domainName}`,
`${uuid()}.${domainName}`, `${uuid()}.${domainName}`,
`${uuid()}.${domainName}` `${uuid()}.${domainName}`,
] ],
}, await createKeyFn()); }, await createKeyFn());
await assert.isRejected(testClient.auto({ await assert.isRejected(testClient.auto({
@@ -205,28 +198,27 @@ describe('client.auto', () => {
results.push(true); results.push(true);
return cts.challengeCreateFn(...args); return cts.challengeCreateFn(...args);
}, },
challengeRemoveFn: cts.challengeRemoveFn challengeRemoveFn: cts.challengeRemoveFn,
})); }));
assert.strictEqual(results.length, 5); assert.strictEqual(results.length, 5);
assert.deepStrictEqual(results, [false, false, false, true, true]); assert.deepStrictEqual(results, [false, false, false, true, true]);
}); });
/** /**
* Order certificates * Order certificates
*/ */
it('should order certificate', async () => { it('should order certificate', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: testDomain commonName: testDomain,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
csr, csr,
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn, challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn challengeRemoveFn: cts.challengeRemoveFn,
}); });
assert.isString(cert); assert.isString(cert);
@@ -235,7 +227,7 @@ describe('client.auto', () => {
it('should order certificate using http-01', async () => { it('should order certificate using http-01', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: testHttpDomain commonName: testHttpDomain,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
@@ -243,7 +235,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.assertHttpChallengeCreateFn, challengeCreateFn: cts.assertHttpChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn, challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['http-01'] challengePriority: ['http-01'],
}); });
assert.isString(cert); assert.isString(cert);
@@ -251,7 +243,7 @@ describe('client.auto', () => {
it('should order certificate using https-01', async () => { it('should order certificate using https-01', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: testHttpsDomain commonName: testHttpsDomain,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
@@ -259,7 +251,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.assertHttpsChallengeCreateFn, challengeCreateFn: cts.assertHttpsChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn, challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['http-01'] challengePriority: ['http-01'],
}); });
assert.isString(cert); assert.isString(cert);
@@ -267,7 +259,7 @@ describe('client.auto', () => {
it('should order certificate using dns-01', async () => { it('should order certificate using dns-01', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: testDnsDomain commonName: testDnsDomain,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
@@ -275,7 +267,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.assertDnsChallengeCreateFn, challengeCreateFn: cts.assertDnsChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn, challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['dns-01'] challengePriority: ['dns-01'],
}); });
assert.isString(cert); assert.isString(cert);
@@ -283,7 +275,7 @@ describe('client.auto', () => {
it('should order certificate using tls-alpn-01', async () => { it('should order certificate using tls-alpn-01', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: testAlpnDomain commonName: testAlpnDomain,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
@@ -291,7 +283,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn, challengeCreateFn: cts.assertTlsAlpnChallengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn, challengeRemoveFn: cts.challengeRemoveFn,
challengePriority: ['tls-alpn-01'] challengePriority: ['tls-alpn-01'],
}); });
assert.isString(cert); assert.isString(cert);
@@ -299,15 +291,14 @@ describe('client.auto', () => {
it('should order san certificate', async () => { it('should order san certificate', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: testSanDomains[0], altNames: testSanDomains,
altNames: testSanDomains
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
csr, csr,
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn, challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn challengeRemoveFn: cts.challengeRemoveFn,
}); });
assert.isString(cert); assert.isString(cert);
@@ -316,15 +307,14 @@ describe('client.auto', () => {
it('should order wildcard certificate', async () => { it('should order wildcard certificate', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: testWildcardDomain, altNames: [testWildcardDomain, `*.${testWildcardDomain}`],
altNames: [`*.${testWildcardDomain}`]
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
csr, csr,
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn, challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn challengeRemoveFn: cts.challengeRemoveFn,
}); });
assert.isString(cert); assert.isString(cert);
@@ -333,7 +323,7 @@ describe('client.auto', () => {
it('should order certificate with opts.skipChallengeVerification=true', async () => { it('should order certificate with opts.skipChallengeVerification=true', async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}` commonName: `${uuid()}.${domainName}`,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
@@ -341,7 +331,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
skipChallengeVerification: true, skipChallengeVerification: true,
challengeCreateFn: cts.challengeCreateFn, challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn challengeRemoveFn: cts.challengeRemoveFn,
}); });
assert.isString(cert); assert.isString(cert);
@@ -354,7 +344,7 @@ describe('client.auto', () => {
await Promise.all(testIssuers.map(async (issuer) => { await Promise.all(testIssuers.map(async (issuer) => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}` commonName: `${uuid()}.${domainName}`,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
@@ -362,7 +352,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
preferredChain: issuer, preferredChain: issuer,
challengeCreateFn: cts.challengeCreateFn, challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn challengeRemoveFn: cts.challengeRemoveFn,
}); });
const rootCert = acme.crypto.splitPemChain(cert).pop(); const rootCert = acme.crypto.splitPemChain(cert).pop();
@@ -378,7 +368,7 @@ describe('client.auto', () => {
} }
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: `${uuid()}.${domainName}` commonName: `${uuid()}.${domainName}`,
}, await createKeyFn()); }, await createKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
@@ -386,7 +376,7 @@ describe('client.auto', () => {
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
preferredChain: uuid(), preferredChain: uuid(),
challengeCreateFn: cts.challengeCreateFn, challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn challengeRemoveFn: cts.challengeRemoveFn,
}); });
const rootCert = acme.crypto.splitPemChain(cert).pop(); const rootCert = acme.crypto.splitPemChain(cert).pop();
@@ -395,7 +385,6 @@ describe('client.auto', () => {
assert.strictEqual(testIssuers[0], info.issuer.commonName); assert.strictEqual(testIssuers[0], info.issuer.commonName);
}); });
/** /**
* Order certificate with alternate key sizes * Order certificate with alternate key sizes
*/ */
@@ -403,21 +392,20 @@ describe('client.auto', () => {
Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => { Object.entries(createKeyAltFns).forEach(([k, altKeyFn]) => {
it(`should order certificate with key=${k}`, async () => { it(`should order certificate with key=${k}`, async () => {
const [, csr] = await acme.crypto.createCsr({ const [, csr] = await acme.crypto.createCsr({
commonName: testDomain commonName: testDomain,
}, await altKeyFn()); }, await altKeyFn());
const cert = await testClient.auto({ const cert = await testClient.auto({
csr, csr,
termsOfServiceAgreed: true, termsOfServiceAgreed: true,
challengeCreateFn: cts.challengeCreateFn, challengeCreateFn: cts.challengeCreateFn,
challengeRemoveFn: cts.challengeRemoveFn challengeRemoveFn: cts.challengeRemoveFn,
}); });
assert.isString(cert); assert.isString(cert);
}); });
}); });
/** /**
* Read certificates * Read certificates
*/ */
@@ -426,7 +414,7 @@ describe('client.auto', () => {
const info = acme.crypto.readCertificateInfo(testCertificate); const info = acme.crypto.readCertificateInfo(testCertificate);
spec.crypto.certificateInfo(info); spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testDomain); assert.isNull(info.domains.commonName);
assert.deepStrictEqual(info.domains.altNames, [testDomain]); assert.deepStrictEqual(info.domains.altNames, [testDomain]);
}); });
@@ -434,7 +422,7 @@ describe('client.auto', () => {
const info = acme.crypto.readCertificateInfo(testSanCertificate); const info = acme.crypto.readCertificateInfo(testSanCertificate);
spec.crypto.certificateInfo(info); spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testSanDomains[0]); assert.isNull(info.domains.commonName);
assert.deepStrictEqual(info.domains.altNames, testSanDomains); assert.deepStrictEqual(info.domains.altNames, testSanDomains);
}); });
@@ -442,7 +430,7 @@ describe('client.auto', () => {
const info = acme.crypto.readCertificateInfo(testWildcardCertificate); const info = acme.crypto.readCertificateInfo(testWildcardCertificate);
spec.crypto.certificateInfo(info); spec.crypto.certificateInfo(info);
assert.strictEqual(info.domains.commonName, testWildcardDomain); assert.isNull(info.domains.commonName);
assert.deepStrictEqual(info.domains.altNames, [testWildcardDomain, `*.${testWildcardDomain}`]); assert.deepStrictEqual(info.domains.altNames, [testWildcardDomain, `*.${testWildcardDomain}`]);
}); });
}); });

View File

@@ -8,7 +8,6 @@ const axios = require('./../src/axios');
const apiBaseUrl = process.env.ACME_CHALLTESTSRV_URL || null; const apiBaseUrl = process.env.ACME_CHALLTESTSRV_URL || null;
const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443; const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
/** /**
* Send request * Send request
*/ */
@@ -21,20 +20,18 @@ async function request(apiPath, data = {}) {
await axios.request({ await axios.request({
url: `${apiBaseUrl}/${apiPath}`, url: `${apiBaseUrl}/${apiPath}`,
method: 'post', method: 'post',
data data,
}); });
return true; return true;
} }
/** /**
* State * State
*/ */
exports.isEnabled = () => !!apiBaseUrl; exports.isEnabled = () => !!apiBaseUrl;
/** /**
* DNS * DNS
*/ */
@@ -42,7 +39,6 @@ exports.isEnabled = () => !!apiBaseUrl;
exports.addDnsARecord = async (host, addresses) => request('add-a', { host, addresses }); exports.addDnsARecord = async (host, addresses) => request('add-a', { host, addresses });
exports.setDnsCnameRecord = async (host, target) => request('set-cname', { host, target }); exports.setDnsCnameRecord = async (host, target) => request('set-cname', { host, target });
/** /**
* Challenge response * Challenge response
*/ */
@@ -55,7 +51,7 @@ async function addHttps01ChallengeResponse(token, content, targetHostname) {
await addHttp01ChallengeResponse(token, content); await addHttp01ChallengeResponse(token, content);
return request('add-redirect', { return request('add-redirect', {
path: `/.well-known/acme-challenge/${token}`, path: `/.well-known/acme-challenge/${token}`,
targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}` targetURL: `https://${targetHostname}:${httpsPort}/.well-known/acme-challenge/${token}`,
}); });
} }
@@ -72,7 +68,6 @@ exports.addHttps01ChallengeResponse = addHttps01ChallengeResponse;
exports.addDns01ChallengeResponse = addDns01ChallengeResponse; exports.addDns01ChallengeResponse = addDns01ChallengeResponse;
exports.addTlsAlpn01ChallengeResponse = addTlsAlpn01ChallengeResponse; exports.addTlsAlpn01ChallengeResponse = addTlsAlpn01ChallengeResponse;
/** /**
* Challenge response mock functions * Challenge response mock functions
*/ */

View File

@@ -0,0 +1,23 @@
-----BEGIN CERTIFICATE-----
MIIDzzCCA1WgAwIBAgISA0ghDoSv5DpT3Pd3lqwjbVDDMAoGCCqGSM49BAMDMDIx
CzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQDEwJF
NjAeFw0yNDA2MTAxNzEyMjZaFw0yNDA5MDgxNzEyMjVaMBQxEjAQBgNVBAMTCWxl
bmNyLm9yZzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABEHJ3DjN7pYV3mftHzaP
V/WI0RhOJnSI5AIFEPFHDi8UowOINRGIfm9FHGIDqrb4Rmyvr9JrrqBdFGDen8BW
6OGjggJnMIICYzAOBgNVHQ8BAf8EBAMCB4AwHQYDVR0lBBYwFAYIKwYBBQUHAwEG
CCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYDVR0OBBYEFIdCTnxqmpOELDyzPaEM
seB36lUOMB8GA1UdIwQYMBaAFJMnRpgDqVFojpjWxEJI2yO/WJTSMFUGCCsGAQUF
BwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDovL2U2Lm8ubGVuY3Iub3JnMCIGCCsG
AQUFBzAChhZodHRwOi8vZTYuaS5sZW5jci5vcmcvMG8GA1UdEQRoMGaCCWxlbmNy
Lm9yZ4IPbGV0c2VuY3J5cHQuY29tgg9sZXRzZW5jcnlwdC5vcmeCDXd3dy5sZW5j
ci5vcmeCE3d3dy5sZXRzZW5jcnlwdC5jb22CE3d3dy5sZXRzZW5jcnlwdC5vcmcw
EwYDVR0gBAwwCjAIBgZngQwBAgEwggEFBgorBgEEAdZ5AgQCBIH2BIHzAPEAdgA/
F0tP1yJHWJQdZRyEvg0S7ZA3fx+FauvBvyiF7PhkbgAAAZADWfneAAAEAwBHMEUC
IGlp+dPU2hLT2suTMYkYMlt/xbzSnKLZDA/wYSsPACP7AiEAxbAzx6mkzn0cs0hh
ti6sLf0pcbmDhxHdlJRjuo6SQZEAdwDf4VbrqgWvtZwPhnGNqMAyTq5W2W6n9aVq
AdHBO75SXAAAAZADWfqrAAAEAwBIMEYCIQCrAmDUrlX3oGhri1qCIb65Cuf8h2GR
LC1VfXBenX7dCAIhALXwbhCQ1vO1WLv4CqyihMHOwFaICYqN/N6ylaBlVAM4MAoG
CCqGSM49BAMDA2gAMGUCMFdgjOXGl+hE2ABDsAeuNq8wi34yTMUHk0KMTOjRAfy9
rOCGQqvP0myoYlyzXOH9uQIxAMdkG1ZWBZS1dHavbPf1I/MjYpzX6gy0jVHIXXu5
aYWylBi/Uf2RPj0LWFZh8tNa1Q==
-----END CERTIFICATE-----

View File

@@ -7,7 +7,6 @@ const util = require('./../src/util');
const pebbleManagementUrl = process.env.ACME_PEBBLE_MANAGEMENT_URL || null; const pebbleManagementUrl = process.env.ACME_PEBBLE_MANAGEMENT_URL || null;
/** /**
* Pebble * Pebble
*/ */
@@ -26,7 +25,6 @@ async function getPebbleCertIssuers() {
return info.map((i) => i.issuer.commonName); return info.map((i) => i.issuer.commonName);
} }
/** /**
* Get certificate issuers * Get certificate issuers
*/ */

View File

@@ -7,14 +7,12 @@ const chai = require('chai');
const chaiAsPromised = require('chai-as-promised'); const chaiAsPromised = require('chai-as-promised');
const axios = require('./../src/axios'); const axios = require('./../src/axios');
/** /**
* Add promise support to Chai * Add promise support to Chai
*/ */
chai.use(chaiAsPromised); chai.use(chaiAsPromised);
/** /**
* Challenge test server ports * Challenge test server ports
*/ */
@@ -31,6 +29,12 @@ if (process.env.ACME_TLSALPN_PORT) {
axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT; axios.defaults.acmeSettings.tlsAlpnChallengePort = process.env.ACME_TLSALPN_PORT;
} }
/**
* Greatly reduce retry duration while testing
*/
axios.defaults.acmeSettings.retryMaxAttempts = 3;
axios.defaults.acmeSettings.retryDefaultDelay = 1;
/** /**
* External account binding * External account binding

View File

@@ -7,7 +7,6 @@ const { assert } = require('chai');
const spec = {}; const spec = {};
module.exports = spec; module.exports = spec;
/** /**
* ACME * ACME
*/ */
@@ -120,7 +119,6 @@ spec.rfc8555.challenge = (obj) => {
} }
}; };
/** /**
* Crypto * Crypto
*/ */
@@ -150,7 +148,6 @@ spec.crypto.certificateInfo = (obj) => {
assert.strictEqual(Object.prototype.toString.call(obj.notAfter), '[object Date]'); assert.strictEqual(Object.prototype.toString.call(obj.notAfter), '[object Date]');
}; };
/** /**
* JWK * JWK
*/ */

View File

@@ -15,7 +15,6 @@ export type PublicKeyString = string;
export type CertificateString = string; export type CertificateString = string;
export type CsrString = string; export type CsrString = string;
/** /**
* Augmented ACME interfaces * Augmented ACME interfaces
*/ */
@@ -28,7 +27,6 @@ export interface Authorization extends rfc8555.Authorization {
url: string; url: string;
} }
/** /**
* Client * Client
*/ */
@@ -80,7 +78,6 @@ export class Client {
auto(opts: ClientAutoOptions): Promise<string>; auto(opts: ClientAutoOptions): Promise<string>;
} }
/** /**
* Directory URLs * Directory URLs
*/ */
@@ -90,6 +87,10 @@ export const directory: {
staging: string, staging: string,
production: string production: string
}, },
google: {
staging: string,
production: string
},
letsencrypt: { letsencrypt: {
staging: string, staging: string,
production: string production: string
@@ -99,7 +100,6 @@ export const directory: {
} }
}; };
/** /**
* Crypto * Crypto
*/ */
@@ -177,14 +177,12 @@ export interface CryptoLegacyInterface {
export const forge: CryptoLegacyInterface; export const forge: CryptoLegacyInterface;
/** /**
* Axios * Axios
*/ */
export const axios: AxiosInstance; export const axios: AxiosInstance;
/** /**
* Logger * Logger
*/ */

View File

@@ -4,7 +4,6 @@
import * as acme from 'acme-client'; import * as acme from 'acme-client';
(async () => { (async () => {
/* Client */ /* Client */
const accountKey = await acme.crypto.createPrivateKey(); const accountKey = await acme.crypto.createPrivateKey();

View File

@@ -27,7 +27,6 @@ export interface AccountUpdateRequest {
termsOfServiceAgreed?: boolean; termsOfServiceAgreed?: boolean;
} }
/** /**
* Order * Order
* *
@@ -53,7 +52,6 @@ export interface OrderCreateRequest {
notAfter?: string; notAfter?: string;
} }
/** /**
* Authorization * Authorization
* *
@@ -73,7 +71,6 @@ export interface Identifier {
value: string; value: string;
} }
/** /**
* Challenge * Challenge
* *
@@ -102,7 +99,6 @@ export interface DnsChallenge extends ChallengeAbstract {
export type Challenge = HttpChallenge | DnsChallenge; export type Challenge = HttpChallenge | DnsChallenge;
/** /**
* Certificate * Certificate
* *