@certd/acme-client 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +88 -25
- package/package.json +24 -29
- package/src/api.js +15 -8
- package/src/auto.js +102 -112
- package/src/client.js +67 -48
- package/src/crypto/forge.js +9 -7
- package/src/crypto/index.js +526 -0
- package/src/http.js +126 -49
- package/src/index.js +15 -0
- package/src/logger.js +30 -0
- package/src/util.js +148 -63
- package/src/verify.js +58 -27
- package/types/index.d.ts +52 -3
- package/types/test.ts +2 -2
- package/CHANGELOG.md +0 -152
- package/src/util.log.js +0 -8
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -8,21 +8,33 @@ This module is written to handle communication with a Boulder/Let's Encrypt-styl
|
|
|
8
8
|
* Boulder divergences from ACME: [https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md](https://github.com/letsencrypt/boulder/blob/master/docs/acme-divergences.md)
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
## Important upgrade notice
|
|
12
|
+
|
|
13
|
+
On September 15, 2022, Let's Encrypt will stop accepting Certificate Signing Requests signed using the obsolete SHA-1 hash. This change affects all `acme-client` versions lower than `3.3.2` and `4.2.4`. Please upgrade ASAP to ensure that your certificates can still be issued following this date.
|
|
14
|
+
|
|
15
|
+
A more detailed explanation can be found [at the Let's Encrypt forums](https://community.letsencrypt.org/t/rejecting-sha-1-csrs-and-validation-using-tls-1-0-1-1-urls/175144).
|
|
16
|
+
|
|
17
|
+
|
|
11
18
|
### Compatibility
|
|
12
19
|
|
|
13
|
-
| acme-client |
|
|
14
|
-
| ------------- | --------- |
|
|
15
|
-
|
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
20
|
+
| acme-client | Node.js | |
|
|
21
|
+
| ------------- | --------- | ----------------------------------------- |
|
|
22
|
+
| v5.x | >= v16 | [Upgrade guide](docs/upgrade-v5.md) |
|
|
23
|
+
| v4.x | >= v10 | [Changelog](CHANGELOG.md#v400-2020-05-29) |
|
|
24
|
+
| v3.x | >= v8 | [Changelog](CHANGELOG.md#v300-2019-07-13) |
|
|
25
|
+
| v2.x | >= v4 | [Changelog](CHANGELOG.md#v200-2018-04-02) |
|
|
26
|
+
| v1.x | >= v4 | [Changelog](CHANGELOG.md#v100-2017-10-20) |
|
|
19
27
|
|
|
20
28
|
|
|
21
29
|
### Table of contents
|
|
22
30
|
|
|
23
31
|
* [Installation](#installation)
|
|
24
32
|
* [Usage](#usage)
|
|
33
|
+
* [Directory URLs](#directory-urls)
|
|
34
|
+
* [External account binding](#external-account-binding)
|
|
35
|
+
* [Specifying the account URL](#specifying-the-account-url)
|
|
25
36
|
* [Cryptography](#cryptography)
|
|
37
|
+
* [Legacy .forge interface](#legacy-forge-interface)
|
|
26
38
|
* [Auto mode](#auto-mode)
|
|
27
39
|
* [Challenge priority](#challenge-priority)
|
|
28
40
|
* [Internal challenge verification](#internal-challenge-verification)
|
|
@@ -56,42 +68,87 @@ const client = new acme.Client({
|
|
|
56
68
|
### Directory URLs
|
|
57
69
|
|
|
58
70
|
```js
|
|
71
|
+
acme.directory.buypass.staging;
|
|
72
|
+
acme.directory.buypass.production;
|
|
73
|
+
|
|
59
74
|
acme.directory.letsencrypt.staging;
|
|
60
75
|
acme.directory.letsencrypt.production;
|
|
76
|
+
|
|
77
|
+
acme.directory.zerossl.production;
|
|
61
78
|
```
|
|
62
79
|
|
|
63
80
|
|
|
64
|
-
|
|
81
|
+
### External account binding
|
|
82
|
+
|
|
83
|
+
To enable [external account binding](https://tools.ietf.org/html/rfc8555#section-7.3.4) when creating your ACME account, provide your KID and HMAC key to the client constructor.
|
|
84
|
+
|
|
85
|
+
```js
|
|
86
|
+
const client = new acme.Client({
|
|
87
|
+
directoryUrl: 'https://acme-provider.example.com/directory-url',
|
|
88
|
+
accountKey: accountPrivateKey,
|
|
89
|
+
externalAccountBinding: {
|
|
90
|
+
kid: 'YOUR-EAB-KID',
|
|
91
|
+
hmacKey: 'YOUR-EAB-HMAC-KEY'
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
```
|
|
65
95
|
|
|
66
|
-
For key pair generation and Certificate Signing Requests, `acme-client` uses [node-forge](https://www.npmjs.com/package/node-forge), a pure JavaScript implementation of the TLS protocol.
|
|
67
96
|
|
|
68
|
-
|
|
97
|
+
### Specifying the account URL
|
|
69
98
|
|
|
70
|
-
|
|
99
|
+
During the ACME account creation process, the server will check the supplied account key and either create a new account if the key is unused, or return the existing ACME account bound to that key.
|
|
71
100
|
|
|
101
|
+
In some cases, for example with some EAB providers, this account creation step may be prohibited and might require you to manually specify the account URL beforehand. This can be done through `accountUrl` in the client constructor.
|
|
72
102
|
|
|
73
|
-
|
|
103
|
+
```js
|
|
104
|
+
const client = new acme.Client({
|
|
105
|
+
directoryUrl: acme.directory.letsencrypt.staging,
|
|
106
|
+
accountKey: accountPrivateKey,
|
|
107
|
+
accountUrl: 'https://acme-v02.api.letsencrypt.org/acme/acct/12345678'
|
|
108
|
+
});
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
You can fetch the clients current account URL, either after creating an account or supplying it through the constructor, using `getAccountUrl()`:
|
|
74
112
|
|
|
75
113
|
```js
|
|
76
|
-
const
|
|
114
|
+
const myAccountUrl = client.getAccountUrl();
|
|
115
|
+
```
|
|
77
116
|
|
|
78
|
-
|
|
117
|
+
|
|
118
|
+
## Cryptography
|
|
119
|
+
|
|
120
|
+
For key pairs `acme-client` utilizes native Node.js cryptography APIs, supporting signing and generation of both RSA and ECDSA keys. The module [jsrsasign](https://www.npmjs.com/package/jsrsasign) is used to generate and parse Certificate Signing Requests.
|
|
121
|
+
|
|
122
|
+
These utility methods are exposed through `.crypto`.
|
|
123
|
+
|
|
124
|
+
* __Documentation: [docs/crypto.md](docs/crypto.md)__
|
|
125
|
+
|
|
126
|
+
```js
|
|
127
|
+
const privateRsaKey = await acme.crypto.createPrivateRsaKey();
|
|
128
|
+
const privateEcdsaKey = await acme.crypto.createPrivateEcdsaKey();
|
|
129
|
+
|
|
130
|
+
const [certificateKey, certificateCsr] = await acme.crypto.createCsr({
|
|
79
131
|
commonName: '*.example.com',
|
|
80
132
|
altNames: ['example.com']
|
|
81
133
|
});
|
|
82
134
|
```
|
|
83
135
|
|
|
84
136
|
|
|
85
|
-
|
|
137
|
+
### Legacy `.forge` interface
|
|
86
138
|
|
|
87
|
-
|
|
139
|
+
The legacy `node-forge` crypto interface is still available for backward compatibility, however this interface is now considered deprecated and will be removed in a future major version of `acme-client`.
|
|
88
140
|
|
|
89
|
-
|
|
141
|
+
You should consider migrating to the new `.crypto` API at your earliest convenience. More details can be found in the [acme-client v5 upgrade guide](docs/upgrade-v5.md).
|
|
90
142
|
|
|
91
|
-
__Documentation: [docs/
|
|
143
|
+
* __Documentation: [docs/forge.md](docs/forge.md)__
|
|
92
144
|
|
|
93
145
|
|
|
94
|
-
|
|
146
|
+
## Auto mode
|
|
147
|
+
|
|
148
|
+
For convenience an `auto()` method is included in the client that takes a single config object. This method will handle the entire process of getting a certificate for one or multiple domains.
|
|
149
|
+
|
|
150
|
+
* __Documentation: [docs/client.md#AcmeClient+auto](docs/client.md#AcmeClient+auto)__
|
|
151
|
+
* __Full example: [examples/auto.js](examples/auto.js)__
|
|
95
152
|
|
|
96
153
|
```js
|
|
97
154
|
const autoOpts = {
|
|
@@ -142,12 +199,8 @@ await client.auto({
|
|
|
142
199
|
|
|
143
200
|
For more fine-grained control you can interact with the ACME API using the methods documented below.
|
|
144
201
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
__API documentation: [docs/client.md](docs/client.md)__
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
#### Example
|
|
202
|
+
* __Documentation: [docs/client.md](docs/client.md)__
|
|
203
|
+
* __Full example: [examples/api.js](examples/api.js)__
|
|
151
204
|
|
|
152
205
|
```js
|
|
153
206
|
const account = await client.createAccount({
|
|
@@ -187,7 +240,17 @@ A complete list of axios options and documentation can be found at:
|
|
|
187
240
|
|
|
188
241
|
## Debugging
|
|
189
242
|
|
|
190
|
-
`acme-client`
|
|
243
|
+
To get a better grasp of what `acme-client` is doing behind the scenes, you can either pass it a logger function, or enable debugging through an environment variable.
|
|
244
|
+
|
|
245
|
+
Setting a logger function may for example be useful for passing messages on to another logging system, or just dumping them to the console.
|
|
246
|
+
|
|
247
|
+
```js
|
|
248
|
+
acme.setLogger((message) => {
|
|
249
|
+
console.log(message);
|
|
250
|
+
});
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Debugging to the console can also be enabled through [debug](https://www.npmjs.com/package/debug) by setting an environment variable.
|
|
191
254
|
|
|
192
255
|
```bash
|
|
193
256
|
DEBUG=acme-client node index.js
|
package/package.json
CHANGED
|
@@ -1,50 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@certd/acme-client",
|
|
3
|
-
"description": "Simple and unopinionated ACME client
|
|
4
|
-
"author": "
|
|
5
|
-
"version": "0.
|
|
3
|
+
"description": "Simple and unopinionated ACME client",
|
|
4
|
+
"author": "nmorsman",
|
|
5
|
+
"version": "0.3.1",
|
|
6
6
|
"main": "src/index.js",
|
|
7
7
|
"types": "types",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"homepage": "https://github.com/publishlab/node-acme-client",
|
|
10
10
|
"engines": {
|
|
11
|
-
"node": ">=
|
|
11
|
+
"node": ">= 16"
|
|
12
12
|
},
|
|
13
13
|
"files": [
|
|
14
14
|
"src",
|
|
15
15
|
"types"
|
|
16
16
|
],
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"axios": "0.
|
|
19
|
-
"backo2": "^1.0.0",
|
|
20
|
-
"bluebird": "^3.5.0",
|
|
18
|
+
"axios": "0.27.2",
|
|
21
19
|
"debug": "^4.1.1",
|
|
22
|
-
"
|
|
23
|
-
"node-forge": "^
|
|
20
|
+
"jsrsasign": "^10.5.26",
|
|
21
|
+
"node-forge": "^1.3.1"
|
|
24
22
|
},
|
|
25
23
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^
|
|
27
|
-
"chai": "^4.
|
|
28
|
-
"chai-as-promised": "^7.
|
|
29
|
-
"dtslint": "^4.
|
|
30
|
-
"eslint": "^
|
|
31
|
-
"eslint-config-airbnb-base": "^
|
|
32
|
-
"eslint-plugin-import": "^2.
|
|
33
|
-
"jsdoc-to-markdown": "^
|
|
34
|
-
"mocha": "^
|
|
35
|
-
"nock": "^13.
|
|
36
|
-
"typescript": "^4.
|
|
37
|
-
"uuid": "^8.
|
|
38
|
-
"webpack-cli": "^4.3.1"
|
|
24
|
+
"@types/node": "^18.6.1",
|
|
25
|
+
"chai": "^4.3.6",
|
|
26
|
+
"chai-as-promised": "^7.1.1",
|
|
27
|
+
"dtslint": "^4.2.1",
|
|
28
|
+
"eslint": "^8.11.0",
|
|
29
|
+
"eslint-config-airbnb-base": "^15.0.0",
|
|
30
|
+
"eslint-plugin-import": "^2.25.4",
|
|
31
|
+
"jsdoc-to-markdown": "^7.1.1",
|
|
32
|
+
"mocha": "^10.0.0",
|
|
33
|
+
"nock": "^13.2.4",
|
|
34
|
+
"typescript": "^4.8.4",
|
|
35
|
+
"uuid": "^8.3.2"
|
|
39
36
|
},
|
|
40
37
|
"scripts": {
|
|
41
|
-
"build-docs": "jsdoc2md src/client.js > docs/client.md && jsdoc2md src/crypto/forge.js > docs/forge.md",
|
|
38
|
+
"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",
|
|
42
39
|
"lint": "eslint .",
|
|
43
40
|
"lint-types": "dtslint types",
|
|
44
|
-
"
|
|
45
|
-
"test": "mocha -
|
|
46
|
-
"test-local": "/bin/bash scripts/run-tests.sh"
|
|
47
|
-
"build": "webpack ./src/index.js ./dist/bundle.js"
|
|
41
|
+
"prepublishOnly": "npm run build-docs",
|
|
42
|
+
"test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
|
|
43
|
+
"test-local": "/bin/bash scripts/run-tests.sh"
|
|
48
44
|
},
|
|
49
45
|
"repository": {
|
|
50
46
|
"type": "git",
|
|
@@ -60,6 +56,5 @@
|
|
|
60
56
|
],
|
|
61
57
|
"bugs": {
|
|
62
58
|
"url": "https://github.com/publishlab/node-acme-client/issues"
|
|
63
|
-
}
|
|
64
|
-
"gitHead": "5fbd7742665c0a949333d805153e9b6af91c0a71"
|
|
59
|
+
}
|
|
65
60
|
}
|
package/src/api.js
CHANGED
|
@@ -42,13 +42,15 @@ class AcmeApi {
|
|
|
42
42
|
* @param {string} url Request URL
|
|
43
43
|
* @param {object} [payload] Request payload, default: `null`
|
|
44
44
|
* @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
|
|
45
|
-
* @param {
|
|
45
|
+
* @param {object} [opts]
|
|
46
|
+
* @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
|
|
47
|
+
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
|
|
46
48
|
* @returns {Promise<object>} HTTP response
|
|
47
49
|
*/
|
|
48
50
|
|
|
49
|
-
async apiRequest(url, payload = null, validStatusCodes = [],
|
|
50
|
-
const kid =
|
|
51
|
-
const resp = await this.http.signedRequest(url, payload, kid);
|
|
51
|
+
async apiRequest(url, payload = null, validStatusCodes = [], { includeJwsKid = true, includeExternalAccountBinding = false } = {}) {
|
|
52
|
+
const kid = includeJwsKid ? this.getAccountUrl() : null;
|
|
53
|
+
const resp = await this.http.signedRequest(url, payload, { kid, includeExternalAccountBinding });
|
|
52
54
|
|
|
53
55
|
if (validStatusCodes.length && (validStatusCodes.indexOf(resp.status) === -1)) {
|
|
54
56
|
throw new Error(util.formatResponseError(resp));
|
|
@@ -65,13 +67,15 @@ class AcmeApi {
|
|
|
65
67
|
* @param {string} resource Request resource name
|
|
66
68
|
* @param {object} [payload] Request payload, default: `null`
|
|
67
69
|
* @param {array} [validStatusCodes] Array of valid HTTP response status codes, default: `[]`
|
|
68
|
-
* @param {
|
|
70
|
+
* @param {object} [opts]
|
|
71
|
+
* @param {boolean} [opts.includeJwsKid] Include KID instead of JWK in JWS header, default: `true`
|
|
72
|
+
* @param {boolean} [opts.includeExternalAccountBinding] Include EAB in request, default: `false`
|
|
69
73
|
* @returns {Promise<object>} HTTP response
|
|
70
74
|
*/
|
|
71
75
|
|
|
72
|
-
async apiResourceRequest(resource, payload = null, validStatusCodes = [],
|
|
76
|
+
async apiResourceRequest(resource, payload = null, validStatusCodes = [], { includeJwsKid = true, includeExternalAccountBinding = false } = {}) {
|
|
73
77
|
const resourceUrl = await this.http.getResourceUrl(resource);
|
|
74
|
-
return this.apiRequest(resourceUrl, payload, validStatusCodes,
|
|
78
|
+
return this.apiRequest(resourceUrl, payload, validStatusCodes, { includeJwsKid, includeExternalAccountBinding });
|
|
75
79
|
}
|
|
76
80
|
|
|
77
81
|
|
|
@@ -98,7 +102,10 @@ class AcmeApi {
|
|
|
98
102
|
*/
|
|
99
103
|
|
|
100
104
|
async createAccount(data) {
|
|
101
|
-
const resp = await this.apiResourceRequest('newAccount', data, [200, 201],
|
|
105
|
+
const resp = await this.apiResourceRequest('newAccount', data, [200, 201], {
|
|
106
|
+
includeJwsKid: false,
|
|
107
|
+
includeExternalAccountBinding: (data.onlyReturnExisting !== true)
|
|
108
|
+
});
|
|
102
109
|
|
|
103
110
|
/* Set account URL */
|
|
104
111
|
if (resp.headers.location) {
|
package/src/auto.js
CHANGED
|
@@ -2,11 +2,8 @@
|
|
|
2
2
|
* ACME auto helper
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
|
|
8
|
-
const debug = logger.info;
|
|
9
|
-
const forge = require('./crypto/forge');
|
|
5
|
+
const { readCsrDomains } = require('./crypto');
|
|
6
|
+
const { log } = require('./logger');
|
|
10
7
|
|
|
11
8
|
const defaultOpts = {
|
|
12
9
|
csr: null,
|
|
@@ -20,13 +17,6 @@ const defaultOpts = {
|
|
|
20
17
|
};
|
|
21
18
|
|
|
22
19
|
|
|
23
|
-
function sleep(time) {
|
|
24
|
-
return new Promise((resovle) => {
|
|
25
|
-
setTimeout(() => {
|
|
26
|
-
resovle();
|
|
27
|
-
}, time);
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
20
|
/**
|
|
31
21
|
* ACME client auto mode
|
|
32
22
|
*
|
|
@@ -52,14 +42,14 @@ module.exports = async function(client, userOpts) {
|
|
|
52
42
|
* Register account
|
|
53
43
|
*/
|
|
54
44
|
|
|
55
|
-
|
|
45
|
+
log('[auto] Checking account');
|
|
56
46
|
|
|
57
47
|
try {
|
|
58
48
|
client.getAccountUrl();
|
|
59
|
-
|
|
49
|
+
log('[auto] Account URL already exists, skipping account registration');
|
|
60
50
|
}
|
|
61
51
|
catch (e) {
|
|
62
|
-
|
|
52
|
+
log('[auto] Registering account');
|
|
63
53
|
await client.createAccount(accountPayload);
|
|
64
54
|
}
|
|
65
55
|
|
|
@@ -68,136 +58,136 @@ module.exports = async function(client, userOpts) {
|
|
|
68
58
|
* Parse domains from CSR
|
|
69
59
|
*/
|
|
70
60
|
|
|
71
|
-
|
|
72
|
-
const csrDomains =
|
|
61
|
+
log('[auto] Parsing domains from Certificate Signing Request');
|
|
62
|
+
const csrDomains = readCsrDomains(opts.csr);
|
|
73
63
|
const domains = [csrDomains.commonName].concat(csrDomains.altNames);
|
|
64
|
+
const uniqueDomains = Array.from(new Set(domains));
|
|
74
65
|
|
|
75
|
-
|
|
66
|
+
log(`[auto] Resolved ${uniqueDomains.length} unique domains from parsing the Certificate Signing Request`);
|
|
76
67
|
|
|
77
68
|
|
|
78
69
|
/**
|
|
79
70
|
* Place order
|
|
80
71
|
*/
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
const orderPayload = { identifiers:
|
|
73
|
+
log('[auto] Placing new certificate order with ACME provider');
|
|
74
|
+
const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: 'dns', value: d })) };
|
|
84
75
|
const order = await client.createOrder(orderPayload);
|
|
85
76
|
const authorizations = await client.getAuthorizations(order);
|
|
86
77
|
|
|
87
|
-
|
|
78
|
+
log(`[auto] Placed certificate order successfully, received ${authorizations.length} identity authorizations`);
|
|
88
79
|
|
|
89
80
|
|
|
90
81
|
/**
|
|
91
82
|
* Resolve and satisfy challenges
|
|
92
83
|
*/
|
|
93
84
|
|
|
94
|
-
|
|
85
|
+
log('[auto] Resolving and satisfying authorization challenges');
|
|
95
86
|
|
|
96
|
-
|
|
97
|
-
const challengePromises = authorizations.map(async (authz) => {
|
|
87
|
+
const challengeFunc = async (authz) => {
|
|
98
88
|
const d = authz.identifier.value;
|
|
89
|
+
let challengeCompleted = false;
|
|
99
90
|
|
|
100
|
-
/*
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (aidx === -1) return 1;
|
|
106
|
-
if (bidx === -1) return -1;
|
|
107
|
-
return aidx - bidx;
|
|
108
|
-
}).slice(0, 1)[0];
|
|
109
|
-
|
|
110
|
-
if (!challenge) {
|
|
111
|
-
throw new Error(`Unable to select challenge for ${d}, no challenge found`);
|
|
91
|
+
/* Skip authz that already has valid status */
|
|
92
|
+
if (authz.status === 'valid') {
|
|
93
|
+
log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`);
|
|
94
|
+
return;
|
|
112
95
|
}
|
|
113
96
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
97
|
+
try {
|
|
98
|
+
/* Select challenge based on priority */
|
|
99
|
+
const challenge = authz.challenges.sort((a, b) => {
|
|
100
|
+
const aidx = opts.challengePriority.indexOf(a.type);
|
|
101
|
+
const bidx = opts.challengePriority.indexOf(b.type);
|
|
102
|
+
|
|
103
|
+
if (aidx === -1) return 1;
|
|
104
|
+
if (bidx === -1) return -1;
|
|
105
|
+
return aidx - bidx;
|
|
106
|
+
}).slice(0, 1)[0];
|
|
107
|
+
|
|
108
|
+
if (!challenge) {
|
|
109
|
+
throw new Error(`Unable to select challenge for ${d}, no challenge found`);
|
|
110
|
+
}
|
|
122
111
|
|
|
123
|
-
|
|
124
|
-
const challengeInfos = await Promise.all(challengePromises);
|
|
112
|
+
log(`[auto] [${d}] Found ${authz.challenges.length} challenges, selected type: ${challenge.type}`);
|
|
125
113
|
|
|
114
|
+
/* Trigger challengeCreateFn() */
|
|
115
|
+
log(`[auto] [${d}] Trigger challengeCreateFn()`);
|
|
116
|
+
const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
|
|
126
117
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
118
|
+
try {
|
|
119
|
+
await opts.challengeCreateFn(authz, challenge, keyAuthorization);
|
|
120
|
+
|
|
121
|
+
/* Challenge verification */
|
|
122
|
+
if (opts.skipChallengeVerification === true) {
|
|
123
|
+
log(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
log(`[auto] [${d}] Running challenge verification`);
|
|
127
|
+
await client.verifyChallenge(authz, challenge);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* Complete challenge and wait for valid status */
|
|
131
|
+
log(`[auto] [${d}] Completing challenge with ACME provider and waiting for valid status`);
|
|
132
|
+
await client.completeChallenge(challenge);
|
|
133
|
+
challengeCompleted = true;
|
|
134
|
+
|
|
135
|
+
await client.waitForValidStatus(challenge);
|
|
136
|
+
}
|
|
137
|
+
finally {
|
|
138
|
+
/* Trigger challengeRemoveFn(), suppress errors */
|
|
139
|
+
log(`[auto] [${d}] Trigger challengeRemoveFn()`);
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await opts.challengeRemoveFn(authz, challenge, keyAuthorization);
|
|
143
|
+
}
|
|
144
|
+
catch (e) {
|
|
145
|
+
log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
135
148
|
}
|
|
136
149
|
catch (e) {
|
|
137
|
-
|
|
138
|
-
|
|
150
|
+
/* Deactivate pending authz when unable to complete challenge */
|
|
151
|
+
if (!challengeCompleted) {
|
|
152
|
+
log(`[auto] [${d}] Unable to complete challenge: ${e.message}`);
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
log(`[auto] [${d}] Deactivating failed authorization`);
|
|
156
|
+
await client.deactivateAuthorization(authz);
|
|
157
|
+
}
|
|
158
|
+
catch (f) {
|
|
159
|
+
/* Suppress deactivateAuthorization() errors */
|
|
160
|
+
log(`[auto] [${d}] Authorization deactivation threw error: ${f.message}`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
throw e;
|
|
139
165
|
}
|
|
140
|
-
}
|
|
166
|
+
};
|
|
141
167
|
|
|
142
|
-
|
|
143
|
-
|
|
168
|
+
const challengePromises = authorizations.map((authz) => async () => {
|
|
169
|
+
await challengeFunc(authz);
|
|
170
|
+
});
|
|
144
171
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const hasError = challengeRecordInfos.filter((item) => item instanceof Error);
|
|
148
|
-
if (hasError.length > 0) {
|
|
149
|
-
throw new Error(hasError[0]);
|
|
150
|
-
}
|
|
151
|
-
// 等待30秒,尽量等dns记录更新到远端
|
|
152
|
-
logger.info('[auto] 等待30秒');
|
|
153
|
-
await sleep(30000);
|
|
154
|
-
// 开始验证
|
|
155
|
-
const verifys = challengeRecordInfos.map(async (challengeInfo) => {
|
|
156
|
-
const { domain, authz, challenge } = challengeInfo;
|
|
157
|
-
const d = domain;
|
|
158
|
-
/* Challenge verification */
|
|
159
|
-
if (opts.skipChallengeVerification === true) {
|
|
160
|
-
logger.info(`[auto] [${d}] Skipping challenge verification since skipChallengeVerification=true`);
|
|
161
|
-
}
|
|
162
|
-
else {
|
|
163
|
-
logger.info(`[auto] [${d}] Running challenge verification`);
|
|
164
|
-
await client.verifyChallenge(authz, challenge);
|
|
165
|
-
}
|
|
172
|
+
log('[auto] Waiting for challenge valid status');
|
|
173
|
+
// await Promise.all(challengePromises);
|
|
166
174
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
175
|
+
log('开始challenge');
|
|
176
|
+
let promise = Promise.resolve();
|
|
177
|
+
function runPromisesSerially(tasks) {
|
|
178
|
+
tasks.forEach((task) => {
|
|
179
|
+
promise = promise.then(task);
|
|
171
180
|
});
|
|
172
|
-
|
|
173
|
-
await Promise.all(verifys);
|
|
174
|
-
// 验证成功
|
|
175
|
-
logger.info('[auto] challenge verify success');
|
|
176
|
-
}
|
|
177
|
-
catch (e) {
|
|
178
|
-
logger.error('申请时出错:', e);
|
|
179
|
-
throw e;
|
|
181
|
+
return promise;
|
|
180
182
|
}
|
|
181
|
-
finally {
|
|
182
|
-
// 删除record
|
|
183
|
-
const willRemovePromises = challengeRecordInfos.filter((item) => item && !(item instanceof Error)).map(async (challengeInfo, index) => {
|
|
184
|
-
await sleep(index * 2000); // 延迟2秒再请求下一个
|
|
185
|
-
const { authz, challenge, keyAuthorization, record, domain } = challengeInfo;
|
|
186
|
-
/* Trigger challengeRemoveFn(), suppress errors */
|
|
187
|
-
logger.info(`[auto] [${domain}] Trigger challengeRemoveFn()`);
|
|
188
183
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
await Promise.all(willRemovePromises);
|
|
198
|
-
}
|
|
184
|
+
await runPromisesSerially(challengePromises);
|
|
185
|
+
log('challenge结束');
|
|
186
|
+
/**
|
|
187
|
+
* Finalize order and download certificate
|
|
188
|
+
*/
|
|
199
189
|
|
|
200
|
-
|
|
201
|
-
await client.finalizeOrder(order, opts.csr);
|
|
202
|
-
return client.getCertificate(
|
|
190
|
+
log('[auto] Finalizing order and downloading certificate');
|
|
191
|
+
const finalized = await client.finalizeOrder(order, opts.csr);
|
|
192
|
+
return client.getCertificate(finalized, opts.preferredChain);
|
|
203
193
|
};
|