@certd/acme-client 1.39.11 → 1.39.12

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.39.11",
6
+ "version": "1.39.12",
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.39.11",
21
+ "@certd/basic": "^1.39.12",
22
22
  "@peculiar/x509": "^1.11.0",
23
23
  "asn1js": "^3.0.5",
24
24
  "axios": "^1.9.0",
@@ -70,5 +70,5 @@
70
70
  "bugs": {
71
71
  "url": "https://github.com/publishlab/node-acme-client/issues"
72
72
  },
73
- "gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
73
+ "gitHead": "898bc9b9f2f75df11ea0803b144862ba98b7511a"
74
74
  }
package/src/client.js CHANGED
@@ -494,7 +494,7 @@ class AcmeClient {
494
494
  throw new Error('Unable to verify ACME challenge, URL not found');
495
495
  }
496
496
 
497
- const {challenges} = createChallengeFn({logger:this.logger});
497
+ const {challenges} = createChallengeFn({logger:this.logger,walkFromAuthoritative: this.opts.walkFromAuthoritative});
498
498
 
499
499
  const verify = challenges
500
500
  if (typeof verify[challenge.type] === 'undefined') {
File without changes
package/src/util.js CHANGED
@@ -252,7 +252,7 @@ async function resolveDomainBySoaRecord(recordName, logger = log) {
252
252
 
253
253
  async function getAuthoritativeDnsResolver(recordName, logger = log) {
254
254
  logger(`获取域名${recordName}的权威NS服务器: `);
255
- const resolver = new dns.Resolver({ timeout: 10000,maxTimeout: 60000 });
255
+ const resolver = new dns.Resolver({timeout: 2000,tries: 2});
256
256
 
257
257
  try {
258
258
  /* Resolve root domain by SOA */
@@ -352,3 +352,5 @@ export {
352
352
  resolveDomainBySoaRecord
353
353
  };
354
354
 
355
+
356
+
package/src/verify.js CHANGED
@@ -4,19 +4,23 @@
4
4
 
5
5
  import dnsSdk from "dns"
6
6
  import https from 'https'
7
- import {log as defaultLog} from './logger.js'
7
+ import { log as defaultLog } from './logger.js'
8
8
  import axios from './axios.js'
9
9
  import * as util from './util.js'
10
- import {isAlpnCertificateAuthorizationValid} from './crypto/index.js'
11
- import {utils} from '@certd/basic'
10
+ import { isAlpnCertificateAuthorizationValid } from './crypto/index.js'
11
+ import { utils } from '@certd/basic'
12
12
 
13
13
  const dns = dnsSdk.promises
14
14
 
15
+ let walkFromAuthoritative = true
16
+ export function setWalkFromAuthoritative(value = true) {
17
+ walkFromAuthoritative = value
18
+ }
19
+
20
+ export function createChallengeFn(opts = {}) {
21
+ const logger = opts?.logger || { info: defaultLog, error: defaultLog, warn: defaultLog, debug: defaultLog }
15
22
 
16
- export function createChallengeFn(opts = {}){
17
- const logger = opts?.logger || {info:defaultLog,error:defaultLog,warn:defaultLog,debug:defaultLog}
18
-
19
- const log = function(...args){
23
+ const log = function (...args) {
20
24
  logger.info(...args)
21
25
  }
22
26
  /**
@@ -31,201 +35,212 @@ export function createChallengeFn(opts = {}){
31
35
  * @returns {Promise<boolean>}
32
36
  */
33
37
 
34
- async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
38
+ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
35
39
 
36
- async function doQuery(challengeUrl){
37
- log(`正在测试请求 ${challengeUrl} `)
38
- // const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
39
- // const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
40
+ async function doQuery(challengeUrl) {
41
+ log(`正在测试请求 ${challengeUrl} `)
42
+ // const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
43
+ // const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
40
44
 
41
- /* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
42
- const httpsAgent = new https.Agent({ rejectUnauthorized: false });
43
-
44
- log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
45
- let data = ""
46
- try{
47
- const resp = await axios.get(challengeUrl, { httpsAgent });
48
- data = (resp.data || '').replace(/\s+$/, '');
49
- }catch (e) {
50
- log(`[error] HTTP request error from ${authz.identifier.value}`,e.message);
51
- return false
52
- }
45
+ /* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
46
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
53
47
 
54
- if (!data || (data !== keyAuthorization)) {
55
- log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`);
56
- return false
57
- }
58
- return true
48
+ log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
49
+ let data = ""
50
+ try {
51
+ const resp = await axios.get(challengeUrl, { httpsAgent });
52
+ data = (resp.data || '').replace(/\s+$/, '');
53
+ } catch (e) {
54
+ log(`[error] HTTP request error from ${authz.identifier.value}`, e.message);
55
+ return false
56
+ }
59
57
 
60
- }
58
+ if (!data || (data !== keyAuthorization)) {
59
+ log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`);
60
+ return false
61
+ }
62
+ return true
61
63
 
62
- const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
63
- let host = authz.identifier.value;
64
- if(utils.domain.isIpv6(host)){
65
- host = `[${host}]`;
66
- }
67
- const challengeUrl = `http://${host}:${httpPort}${suffix}`;
68
-
69
- if (!await doQuery(challengeUrl)) {
70
- const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
71
- const httpsChallengeUrl = `https://${host}:${httpsPort}${suffix}`;
72
- const res = await doQuery(httpsChallengeUrl)
73
- if (!res) {
74
- throw new Error(`[error] 验证失败,请检查以上测试url是否可以正常访问`);
75
64
  }
76
- }
77
-
78
-
79
- log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
80
- return true;
81
- }
82
65
 
83
- /**
84
- * Walk DNS until TXT records are found
85
- */
66
+ const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
67
+ let host = authz.identifier.value;
68
+ if (utils.domain.isIpv6(host)) {
69
+ host = `[${host}]`;
70
+ }
71
+ const challengeUrl = `http://${host}:${httpPort}${suffix}`;
72
+
73
+ if (!await doQuery(challengeUrl)) {
74
+ const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
75
+ const httpsChallengeUrl = `https://${host}:${httpsPort}${suffix}`;
76
+ const res = await doQuery(httpsChallengeUrl)
77
+ if (!res) {
78
+ throw new Error(`[error] 验证失败,请检查以上测试url是否可以正常访问`);
79
+ }
80
+ }
86
81
 
87
- async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
88
82
 
89
- let records = [];
83
+ log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
84
+ return true;
85
+ }
90
86
 
91
- /* Resolve TXT records */
92
- try {
93
- log(`检查域名 ${recordName} 的TXT记录`);
94
- const txtRecords = await resolver.resolveTxt(recordName);
95
- if (txtRecords && txtRecords.length) {
96
- log(`找到 ${txtRecords.length} 条 TXT记录( ${recordName})`);
97
- log(`TXT records: ${JSON.stringify(txtRecords)}`);
98
- records = records.concat(...txtRecords);
87
+ /**
88
+ * Walk DNS until TXT records are found
89
+ */
90
+
91
+ async function walkDnsChallengeRecord(recordName, resolver = dns, deep = 0) {
92
+
93
+ let records = [];
94
+
95
+ const isAuthoritative = resolver === dns
96
+ /* Resolve TXT records */
97
+ try {
98
+ log(`检查域名 ${recordName} 的TXT记录(from ${isAuthoritative ? '本地DNS' : '权威DNS服务器'})`);
99
+ const txtRecords = await resolver.resolveTxt(recordName);
100
+ if (txtRecords && txtRecords.length) {
101
+ log(`找到 ${txtRecords.length} 条 TXT记录( ${recordName})`);
102
+ log(`TXT records: ${JSON.stringify(txtRecords)}`);
103
+ records = records.concat(...txtRecords);
104
+ }
105
+ } catch (e) {
106
+ log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
99
107
  }
100
- } catch (e) {
101
- log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
102
- }
103
108
 
104
- /* Resolve CNAME record first */
105
- try {
106
- log(`检查是否存在CNAME映射: ${recordName}`);
107
- const cnameRecords = await resolver.resolveCname(recordName);
108
-
109
- if (cnameRecords.length) {
110
- const cnameRecord = cnameRecords[0];
111
- log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`);
112
- let res= await walkTxtRecord(cnameRecord,deep+1);
113
- if (res && res.length) {
114
- log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
115
- records = records.concat(...res);
109
+ /* Resolve CNAME record first */
110
+ try {
111
+ log(`检查是否存在CNAME映射: ${recordName}`);
112
+ const cnameRecords = await resolver.resolveCname(recordName);
113
+
114
+ if (cnameRecords.length) {
115
+ const cnameRecord = cnameRecords[0];
116
+ log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`);
117
+ let res = await walkTxtRecord(cnameRecord, deep + 1);
118
+ if (res && res.length) {
119
+ log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
120
+ records = records.concat(...res);
121
+ }
122
+ } else {
123
+ log(`没有CNAME映射(${recordName})`);
116
124
  }
117
- }else{
118
- log(`没有CNAME映射(${recordName})`);
125
+ } catch (e) {
126
+ log(`检查CNAME出错(${recordName}) :${e.message}`);
119
127
  }
120
- } catch (e) {
121
- log(`检查CNAME出错(${recordName}) :${e.message}`);
128
+ return records
122
129
  }
123
- return records
124
- }
125
130
 
126
- async function walkTxtRecord(recordName,deep = 0) {
127
- if(deep >5){
128
- log(`walkTxtRecord too deep (#${deep}) , skip walk`)
129
- return []
130
- }
131
+ async function walkTxtRecord(recordName, deep = 0) {
132
+ if (deep > 5) {
133
+ log(`walkTxtRecord too deep (#${deep}) , skip walk`)
134
+ return []
135
+ }
131
136
 
132
- const txtRecords = []
133
- try {
134
- /* Default DNS resolver first */
135
- log('从本地DNS服务器获取TXT解析记录');
136
- const res = await walkDnsChallengeRecord(recordName,dns,deep);
137
- if (res && res.length > 0) {
138
- for (const item of res) {
139
- txtRecords.push(item)
137
+ const txtRecords = []
138
+ try {
139
+ /* Default DNS resolver first */
140
+ log('从本地DNS服务器获取TXT解析记录');
141
+ const res = await walkDnsChallengeRecord(recordName, dns, deep);
142
+ if (res && res.length > 0) {
143
+ for (const item of res) {
144
+ txtRecords.push(item)
145
+ }
140
146
  }
141
- }
142
147
 
143
- } catch (e) {
144
- log(`本地获取TXT解析记录失败:${e.message}`)
145
- }
148
+ } catch (e) {
149
+ log(`本地获取TXT解析记录失败:${e.message}`)
150
+ }
146
151
 
147
- try{
148
- /* Authoritative DNS resolver */
149
- log(`从域名权威服务器获取TXT解析记录`);
150
- const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName,log);
151
- const res = await walkDnsChallengeRecord(recordName, authoritativeResolver,deep);
152
- if (res && res.length > 0) {
153
- for (const item of res) {
154
- txtRecords.push(item)
152
+ if (walkFromAuthoritative !==false) {
153
+ try {
154
+ /* Authoritative DNS resolver */
155
+ log(`从域名权威服务器获取TXT解析记录`);
156
+ const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName, log);
157
+ const res = await walkDnsChallengeRecord(recordName, authoritativeResolver, deep);
158
+ if (res && res.length > 0) {
159
+ for (const item of res) {
160
+ txtRecords.push(item)
161
+ }
162
+ }
163
+ } catch (e) {
164
+ log(`权威服务器获取TXT解析记录失败:${e.message}`)
155
165
  }
166
+ }else{
167
+ log(`跳过从权威服务器获取TXT解析记录`);
156
168
  }
157
- }catch (e) {
158
- log(`权威服务器获取TXT解析记录失败:${e.message}`)
159
- }
160
-
161
- if (txtRecords.length === 0) {
162
- throw new Error(`没有找到TXT解析记录(${recordName})`);
163
- }
164
- return txtRecords;
165
- }
166
169
 
167
- /**
168
- * Verify ACME DNS challenge
169
- *
170
- * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
171
- *
172
- * @param {object} authz Identifier authorization
173
- * @param {object} challenge Authorization challenge
174
- * @param {string} keyAuthorization Challenge key authorization
175
- * @param {string} [prefix] DNS prefix
176
- * @returns {Promise<boolean>}
177
- */
178
170
 
179
- async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
180
- const recordName = `${prefix}${authz.identifier.value}`;
181
- log(`本地校验TXT记录): ${recordName}`);
182
- let recordValues = await walkTxtRecord(recordName);
183
- //去重
184
- recordValues = [...new Set(recordValues)];
185
- log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
186
- if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
187
- const err = `没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`
188
- throw new Error(err);
171
+ if (txtRecords.length === 0) {
172
+ throw new Error(`没有找到TXT解析记录(${recordName})`);
173
+ }
174
+ return txtRecords;
189
175
  }
190
176
 
191
- log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
192
- return true;
193
- }
194
-
195
- /**
196
- * Verify ACME TLS ALPN challenge
197
- *
198
- * https://datatracker.ietf.org/doc/html/rfc8737
199
- *
200
- * @param {object} authz Identifier authorization
201
- * @param {object} challenge Authorization challenge
202
- * @param {string} keyAuthorization Challenge key authorization
203
- * @returns {Promise<boolean>}
204
- */
177
+ /**
178
+ * Verify ACME DNS challenge
179
+ *
180
+ * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
181
+ *
182
+ * @param {object} authz Identifier authorization
183
+ * @param {object} challenge Authorization challenge
184
+ * @param {string} keyAuthorization Challenge key authorization
185
+ * @param {string} [prefix] DNS prefix
186
+ * @returns {Promise<boolean>}
187
+ */
188
+
189
+ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
190
+ const recordName = `${prefix}${authz.identifier.value}`;
191
+ log(`本地校验TXT记录): ${recordName}`);
192
+ let recordValues = await walkTxtRecord(recordName, 0, walkFromAuthoritative);
193
+ //去重
194
+ recordValues = [...new Set(recordValues)];
195
+ log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
196
+ if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
197
+ const err = `没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`
198
+ throw new Error(err);
199
+ }
205
200
 
206
- async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
207
- const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
208
- const host = authz.identifier.value;
209
- log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
201
+ log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
202
+ return true;
203
+ }
210
204
 
211
- const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
212
- log('Certificate received from server successfully, matching key authorization in ALPN');
205
+ /**
206
+ * Verify ACME TLS ALPN challenge
207
+ *
208
+ * https://datatracker.ietf.org/doc/html/rfc8737
209
+ *
210
+ * @param {object} authz Identifier authorization
211
+ * @param {object} challenge Authorization challenge
212
+ * @param {string} keyAuthorization Challenge key authorization
213
+ * @returns {Promise<boolean>}
214
+ */
215
+
216
+ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
217
+ const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
218
+ const host = authz.identifier.value;
219
+ log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
220
+
221
+ const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
222
+ log('Certificate received from server successfully, matching key authorization in ALPN');
223
+
224
+ if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
225
+ throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
226
+ }
213
227
 
214
- if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
215
- throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
228
+ log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
229
+ return true;
216
230
  }
217
231
 
218
- log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
219
- return true;
220
- }
221
-
222
232
  return {
223
- challenges:{
233
+ challenges: {
224
234
  'http-01': verifyHttpChallenge,
225
235
  'dns-01': verifyDnsChallenge,
226
236
  'tls-alpn-01': verifyTlsAlpnChallenge,
227
237
  },
228
238
  walkTxtRecord,
239
+ walkDnsChallengeRecord,
229
240
  }
230
241
 
231
- }
242
+ }
243
+
244
+
245
+
246
+ // createChallengeFn({logger:{info:console.log}}).walkDnsChallengeRecord("handsfree.work")
package/types/index.d.ts CHANGED
@@ -219,4 +219,6 @@ export function getAuthoritativeDnsResolver(record:string): Promise<any>;
219
219
 
220
220
  export const CancelError: typeof CancelError;
221
221
 
222
- export function resolveDomainBySoaRecord(domain: string): Promise<string>;
222
+ export function resolveDomainBySoaRecord(domain: string): Promise<string>;
223
+
224
+ export function setWalkFromAuthoritative(value = true): void;