@certd/acme-client 1.31.11 → 1.33.0

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/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "Simple and unopinionated ACME client",
4
4
  "private": false,
5
5
  "author": "nmorsman",
6
- "version": "1.31.11",
6
+ "version": "1.33.0",
7
7
  "type": "module",
8
8
  "module": "scr/index.js",
9
9
  "main": "src/index.js",
@@ -18,7 +18,7 @@
18
18
  "types"
19
19
  ],
20
20
  "dependencies": {
21
- "@certd/basic": "^1.31.11",
21
+ "@certd/basic": "^1.33.0",
22
22
  "@peculiar/x509": "^1.11.0",
23
23
  "asn1js": "^3.0.5",
24
24
  "axios": "^1.7.2",
@@ -67,5 +67,5 @@
67
67
  "bugs": {
68
68
  "url": "https://github.com/publishlab/node-acme-client/issues"
69
69
  },
70
- "gitHead": "8374c3941a9d1398989b8f38fd4bfa2a2f29937b"
70
+ "gitHead": "13ddd7c5f9c511181374ca5cbdd376b793b14c27"
71
71
  }
package/src/auto.js CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
2
  * ACME auto helper
3
3
  */
4
- import { readCsrDomains } from './crypto/index.js';
5
- import { log } from './logger.js';
6
- import { wait } from './wait.js';
7
- import { CancelError } from './error.js';
4
+ import { readCsrDomains } from "./crypto/index.js";
5
+ import { log } from "./logger.js";
6
+ import { wait } from "./wait.js";
7
+ import { CancelError } from "./error.js";
8
8
 
9
9
 
10
10
  const defaultOpts = {
@@ -13,13 +13,13 @@ const defaultOpts = {
13
13
  preferredChain: null,
14
14
  termsOfServiceAgreed: false,
15
15
  skipChallengeVerification: false,
16
- challengePriority: ['http-01', 'dns-01'],
16
+ challengePriority: ["http-01", "dns-01"],
17
17
  challengeCreateFn: async () => {
18
- throw new Error('Missing challengeCreateFn()');
18
+ throw new Error("Missing challengeCreateFn()");
19
19
  },
20
20
  challengeRemoveFn: async () => {
21
- throw new Error('Missing challengeRemoveFn()');
22
- },
21
+ throw new Error("Missing challengeRemoveFn()");
22
+ }
23
23
  };
24
24
 
25
25
  /**
@@ -30,7 +30,7 @@ const defaultOpts = {
30
30
  * @returns {Promise<buffer>} Certificate
31
31
  */
32
32
 
33
- export default async (client, userOpts) => {
33
+ export default async (client, userOpts) => {
34
34
  const opts = { ...defaultOpts, ...userOpts };
35
35
  const accountPayload = { termsOfServiceAgreed: opts.termsOfServiceAgreed };
36
36
 
@@ -49,14 +49,13 @@ export default async (client, userOpts) => {
49
49
  * Register account
50
50
  */
51
51
 
52
- log('[auto] Checking account');
52
+ log("[auto] Checking account");
53
53
 
54
54
  try {
55
55
  client.getAccountUrl();
56
- log('[auto] Account URL already exists, skipping account registration( 证书申请账户已存在,跳过注册 )');
57
- }
58
- catch (e) {
59
- log('[auto] Registering account (注册证书申请账户)');
56
+ log("[auto] Account URL already exists, skipping account registration( 证书申请账户已存在,跳过注册 )");
57
+ } catch (e) {
58
+ log("[auto] Registering account (注册证书申请账户)");
60
59
  await client.createAccount(accountPayload);
61
60
  }
62
61
 
@@ -64,7 +63,7 @@ export default async (client, userOpts) => {
64
63
  * Parse domains from CSR
65
64
  */
66
65
 
67
- log('[auto] Parsing domains from Certificate Signing Request ');
66
+ log("[auto] Parsing domains from Certificate Signing Request ");
68
67
  const { commonName, altNames } = readCsrDomains(opts.csr);
69
68
  const uniqueDomains = Array.from(new Set([commonName].concat(altNames).filter((d) => d)));
70
69
 
@@ -74,8 +73,8 @@ export default async (client, userOpts) => {
74
73
  * Place order
75
74
  */
76
75
 
77
- log('[auto] Placing new certificate order with ACME provider');
78
- const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: 'dns', value: d })) };
76
+ log("[auto] Placing new certificate order with ACME provider");
77
+ const orderPayload = { identifiers: uniqueDomains.map((d) => ({ type: "dns", value: d })) };
79
78
  const order = await client.createOrder(orderPayload);
80
79
  const authorizations = await client.getAuthorizations(order);
81
80
 
@@ -85,82 +84,81 @@ export default async (client, userOpts) => {
85
84
  * Resolve and satisfy challenges
86
85
  */
87
86
 
88
- log('[auto] Resolving and satisfying authorization challenges');
87
+ log("[auto] Resolving and satisfying authorization challenges");
89
88
 
90
89
  const clearTasks = [];
90
+ const localVerifyTasks = [];
91
+ const completeChallengeTasks = [];
91
92
 
92
93
  const challengeFunc = async (authz) => {
93
94
  const d = authz.identifier.value;
94
95
  let challengeCompleted = false;
95
96
 
96
97
  /* Skip authz that already has valid status */
97
- if (authz.status === 'valid') {
98
+ if (authz.status === "valid") {
98
99
  log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`);
99
100
  return;
100
101
  }
101
102
 
102
103
  const keyAuthorizationGetter = async (challenge) => {
103
104
  return await client.getChallengeKeyAuthorization(challenge);
104
- }
105
+ };
105
106
 
106
- try {
107
- log(`[auto] [${d}] Trigger challengeCreateFn()`);
107
+ async function deactivateAuth(e) {
108
+ log(`[auto] [${d}] Unable to complete challenge: ${e.message}`);
108
109
  try {
109
- const { recordReq, recordRes, dnsProvider,challenge ,keyAuthorization} = await opts.challengeCreateFn(authz, keyAuthorizationGetter);
110
- clearTasks.push(async () => {
111
- /* Trigger challengeRemoveFn(), suppress errors */
112
- log(`[auto] [${d}] Trigger challengeRemoveFn()`);
113
- try {
114
- await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider);
115
- }
116
- catch (e) {
117
- log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
118
- }
119
- });
120
- // throw new Error('测试异常');
121
- /* Challenge verification */
122
- if (opts.skipChallengeVerification === true) {
123
- log(`[auto] [${d}] 跳过本地验证(skipChallengeVerification=true),等待 60s`);
124
- await wait(60 * 1000);
125
- }
126
- else {
127
- log(`[auto] [${d}] 开始本地验证, type = ${challenge.type}`);
128
- try {
129
- await client.verifyChallenge(authz, challenge);
130
- }
131
- catch (e) {
132
- log(`[auto] [${d}] 本地验证失败,尝试请求ACME提供商获取状态: ${e.message}`);
133
- }
134
- }
135
- /* Complete challenge and wait for valid status */
136
- log(`[auto] [${d}] 请求ACME提供商完成验证,等待返回valid状态`);
137
- await client.completeChallenge(challenge);
138
- challengeCompleted = true;
139
-
140
- await client.waitForValidStatus(challenge);
141
- }
142
- catch (e) {
143
- log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`);
144
- throw e;
110
+ log(`[auto] [${d}] Deactivating failed authorization`);
111
+ await client.deactivateAuthorization(authz);
112
+ } catch (f) {
113
+ /* Suppress deactivateAuthorization() errors */
114
+ log(`[auto] [${d}] Authorization deactivation threw error: ${f.message}`);
145
115
  }
146
116
  }
147
- catch (e) {
148
- /* Deactivate pending authz when unable to complete challenge */
149
- if (!challengeCompleted) {
150
- log(`[auto] [${d}] Unable to complete challenge: ${e.message}`);
151
117
 
118
+ log(`[auto] [${d}] Trigger challengeCreateFn()`);
119
+ try {
120
+ const { recordReq, recordRes, dnsProvider, challenge, keyAuthorization } = await opts.challengeCreateFn(authz, keyAuthorizationGetter);
121
+ clearTasks.push(async () => {
122
+ /* Trigger challengeRemoveFn(), suppress errors */
123
+ log(`[auto] [${d}] Trigger challengeRemoveFn()`);
152
124
  try {
153
- log(`[auto] [${d}] Deactivating failed authorization`);
154
- await client.deactivateAuthorization(authz);
125
+ await opts.challengeRemoveFn(authz, challenge, keyAuthorization, recordReq, recordRes, dnsProvider);
126
+ } catch (e) {
127
+ log(`[auto] [${d}] challengeRemoveFn threw error: ${e.message}`);
155
128
  }
156
- catch (f) {
157
- /* Suppress deactivateAuthorization() errors */
158
- log(`[auto] [${d}] Authorization deactivation threw error: ${f.message}`);
129
+ });
130
+
131
+ localVerifyTasks.push(async () => {
132
+ /* Challenge verification */
133
+ log(`[auto] [${d}] 开始本地验证, type = ${challenge.type}`);
134
+ try {
135
+ await client.verifyChallenge(authz, challenge);
136
+ } catch (e) {
137
+ log(`[auto] [${d}] 本地验证失败,尝试请求ACME提供商获取状态: ${e.message}`);
159
138
  }
160
- }
139
+ });
140
+
141
+ completeChallengeTasks.push(async () => {
142
+ /* Complete challenge and wait for valid status */
143
+ log(`[auto] [${d}] 请求ACME提供商完成验证`);
144
+ try{
145
+ await client.completeChallenge(challenge);
146
+ }catch (e) {
147
+ await deactivateAuth(e);
148
+ throw e;
149
+ }
150
+ challengeCompleted = true;
151
+ log(`[auto] [${d}] 等待返回valid状态`);
152
+ await client.waitForValidStatus(challenge,d);
153
+ });
154
+
161
155
 
156
+ } catch (e) {
157
+ log(`[auto] [${d}] challengeCreateFn threw error: ${e.message}`);
158
+ await deactivateAuth(e);
162
159
  throw e;
163
160
  }
161
+
164
162
  };
165
163
  const domainSets = [];
166
164
 
@@ -168,7 +166,7 @@ export default async (client, userOpts) => {
168
166
  const d = authz.identifier.value;
169
167
  log(`authorization:domain = ${d}, value = ${JSON.stringify(authz)}`);
170
168
 
171
- if (authz.status === 'valid') {
169
+ if (authz.status === "valid") {
172
170
  log(`[auto] [${d}] Authorization already has valid status, no need to complete challenges`);
173
171
  return;
174
172
  }
@@ -192,8 +190,9 @@ export default async (client, userOpts) => {
192
190
 
193
191
  const allChallengePromises = [];
194
192
  // eslint-disable-next-line no-restricted-syntax
193
+ const challengePromises = [];
194
+ allChallengePromises.push(challengePromises);
195
195
  for (const domainSet of domainSets) {
196
- const challengePromises = [];
197
196
  // eslint-disable-next-line guard-for-in,no-restricted-syntax
198
197
  for (const domain in domainSet) {
199
198
  const authz = domainSet[domain];
@@ -202,12 +201,11 @@ export default async (client, userOpts) => {
202
201
  await challengeFunc(authz);
203
202
  });
204
203
  }
205
- allChallengePromises.push(challengePromises);
206
204
  }
207
205
 
208
206
  log(`[auto] challengeGroups:${allChallengePromises.length}`);
209
207
 
210
- function runAllPromise(tasks) {
208
+ async function runAllPromise(tasks) {
211
209
  let promise = Promise.resolve();
212
210
  tasks.forEach((task) => {
213
211
  promise = promise.then(task);
@@ -215,73 +213,60 @@ export default async (client, userOpts) => {
215
213
  return promise;
216
214
  }
217
215
 
218
- async function runPromisePa(tasks) {
216
+ async function runPromisePa(tasks, waitTime = 5000) {
219
217
  const results = [];
220
218
  // eslint-disable-next-line no-await-in-loop,no-restricted-syntax
221
219
  for (const task of tasks) {
222
220
  results.push(task());
223
221
  // eslint-disable-next-line no-await-in-loop
224
- await wait(10000);
222
+ await wait(waitTime);
225
223
  }
226
224
  return Promise.all(results);
227
225
  }
228
226
 
229
- try {
230
- log(`开始challenge,共${allChallengePromises.length}组`);
231
- let i = 0;
232
- // eslint-disable-next-line no-restricted-syntax
233
- for (const challengePromises of allChallengePromises) {
234
- i += 1;
235
- log(`开始第${i}组`);
236
- if (opts.signal && opts.signal.aborted) {
237
- throw new CancelError('用户取消');
238
- }
227
+ log(`开始challenge,共${allChallengePromises.length}组`);
228
+ let i = 0;
229
+ // eslint-disable-next-line no-restricted-syntax
230
+ for (const challengePromises of allChallengePromises) {
231
+ i += 1;
232
+ log(`开始第${i}组`);
233
+ if (opts.signal && opts.signal.aborted) {
234
+ throw new CancelError("用户取消");
235
+ }
239
236
 
240
- try {
241
- // eslint-disable-next-line no-await-in-loop
242
- await runPromisePa(challengePromises);
243
- }
244
- catch (e) {
245
- log(`证书申请失败${e.message}`);
246
- throw e;
247
- }
248
- finally {
249
- if (client.opts.sslProvider !== 'google') {
250
- // letsencrypt 如果同时检出两个TXT记录,会以第一个为准,就会校验失败,所以需要提前删除
251
- // zerossl 此方式测试无问题
252
- log(`清理challenge痕迹,length:${clearTasks.length}`);
253
- try {
254
- // eslint-disable-next-line no-await-in-loop
255
- await runAllPromise(clearTasks);
256
- }
257
- catch (e) {
258
- log('清理challenge失败');
259
- log(e);
260
- }
261
- }
237
+ try {
238
+ // eslint-disable-next-line no-await-in-loop
239
+ await runPromisePa(challengePromises);
240
+ if (opts.skipChallengeVerification === true) {
241
+ log(`跳过本地验证(skipChallengeVerification=true),等待 60s`);
242
+ await wait(60 * 1000);
243
+ } else {
244
+ await runPromisePa(localVerifyTasks, 1000);
245
+ log("本地校验完成,等待30s")
246
+ await wait(30 * 1000)
262
247
  }
263
- }
264
- }
265
- finally {
266
- if (client.opts.sslProvider === 'google') {
267
- // google 相同的域名txt记录是一样的,不能提前删除,否则校验失败,报错如下
268
- // Error: The TXT record retrieved from _acme-challenge.bbc.handsfree.work.
269
- // at the time the challenge was validated did not contain JshHVu7dt_DT6uYILWhokHefFVad2Q6Mw1L-fNZFcq8
270
- // (the base64url-encoded SHA-256 digest of RlJZNBR0LWnxNK_xd2zqtYVvCiNJOKJ3J1NmCjU_9BjaUJgL3k-qSpIhQ-uF4FBS.NRyqT8fRiq6THzzrvkgzgR5Xai2LsA2SyGLAq_wT3qc).
271
- // See https://tools.ietf.org/html/rfc8555#section-8.4 for more information.
248
+
249
+ log("开始向提供商请求挑战验证");
250
+ await runPromisePa(completeChallengeTasks, 1000);
251
+ } catch (e) {
252
+ log(`证书申请失败${e.message}`);
253
+ throw e;
254
+ } finally {
255
+ // letsencrypt 如果同时检出两个TXT记录,会以第一个为准,就会校验失败,所以需要提前删除
256
+ // zerossl 此方式测试无问题
272
257
  log(`清理challenge痕迹,length:${clearTasks.length}`);
273
258
  try {
274
- // eslint-disable-next-line no-await-in-loop
259
+ // eslint-disable-next-line no-await-in-loop
275
260
  await runAllPromise(clearTasks);
276
- }
277
- catch (e) {
278
- log('清理challenge失败');
261
+ } catch (e) {
262
+ log("清理challenge失败");
279
263
  log(e);
280
264
  }
281
265
  }
282
266
  }
283
267
 
284
- log('challenge结束');
268
+
269
+ log("challenge结束");
285
270
 
286
271
  // log('[auto] Waiting for challenge valid status');
287
272
  // await Promise.all(challengePromises);
@@ -289,7 +274,7 @@ export default async (client, userOpts) => {
289
274
  * Finalize order and download certificate
290
275
  */
291
276
 
292
- log('[auto] Finalizing order and downloading certificate');
277
+ log("[auto] Finalizing order and downloading certificate");
293
278
  const finalized = await client.finalizeOrder(order, opts.csr);
294
279
  const res = await client.getCertificate(finalized, opts.preferredChain);
295
280
  return res;
package/src/client.js CHANGED
@@ -554,9 +554,9 @@ class AcmeClient {
554
554
  * ```
555
555
  */
556
556
 
557
- async waitForValidStatus(item) {
557
+ async waitForValidStatus(item,d) {
558
558
  if (!item.url) {
559
- throw new Error('Unable to verify status of item, URL not found');
559
+ throw new Error(`[${d}] Unable to verify status of item, URL not found`);
560
560
  }
561
561
 
562
562
  const verifyFn = async (abort) => {
@@ -568,23 +568,23 @@ class AcmeClient {
568
568
  const resp = await this.api.apiRequest(item.url, null, [200]);
569
569
 
570
570
  /* Verify status */
571
- log(`Item has status(挑战状态): ${resp.data.status}`);
571
+ log(`[${d}] Item has status(挑战状态): ${resp.data.status}`);
572
572
 
573
573
  if (invalidStates.includes(resp.data.status)) {
574
574
  abort();
575
575
  throw new Error(util.formatResponseError(resp));
576
576
  }
577
577
  else if (pendingStates.includes(resp.data.status)) {
578
- throw new Error('Operation is pending or processing(当前仍然在等待状态)');
578
+ throw new Error(`[${d}] Operation is pending or processing(当前仍然在等待状态)`);
579
579
  }
580
580
  else if (validStates.includes(resp.data.status)) {
581
581
  return resp.data;
582
582
  }
583
583
 
584
- throw new Error(`Unexpected item status: ${resp.data.status}`);
584
+ throw new Error(`[${d}] Unexpected item status: ${resp.data.status}`);
585
585
  };
586
586
 
587
- log(`Waiting for valid status (等待valid状态): ${item.url}`, this.backoffOpts);
587
+ log(`[${d}] Waiting for valid status (等待valid状态): ${item.url}`, this.backoffOpts);
588
588
  return util.retry(verifyFn, this.backoffOpts);
589
589
  }
590
590
 
package/src/verify.js CHANGED
@@ -98,7 +98,7 @@ export async function walkTxtRecord(recordName,deep = 0) {
98
98
  try {
99
99
  /* Default DNS resolver first */
100
100
  log('从本地DNS服务器获取TXT解析记录');
101
- const res = await walkDnsChallengeRecord(recordName,null,deep);
101
+ const res = await walkDnsChallengeRecord(recordName,dns,deep);
102
102
  if (res && res.length > 0) {
103
103
  for (const item of res) {
104
104
  txtRecords.push(item)
@@ -147,12 +147,12 @@ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '
147
147
  let recordValues = await walkTxtRecord(recordName);
148
148
  //去重
149
149
  recordValues = [...new Set(recordValues)];
150
- log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录`);
150
+ log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
151
151
  if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
152
152
  throw new Error(`没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`);
153
153
  }
154
154
 
155
- log(`关键授权匹配成功(${challenge.type}/${recordName}),校验成功, ACME challenge verified`);
155
+ log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
156
156
  return true;
157
157
  }
158
158