@certd/acme-client 1.39.11 → 1.39.13
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/dist/api.d.ts +151 -0
- package/{src → dist}/api.js +2 -38
- package/dist/auto.d.ts +9 -0
- package/{src → dist}/auto.js +45 -79
- package/dist/axios.d.ts +8 -0
- package/{src → dist}/axios.js +3 -31
- package/dist/client.d.ts +396 -0
- package/{src → dist}/client.js +16 -108
- package/dist/crypto/forge.d.ts +174 -0
- package/{src → dist}/crypto/forge.js +7 -63
- package/dist/crypto/index.d.ts +215 -0
- package/{src → dist}/crypto/index.js +2 -87
- package/dist/error.d.ts +3 -0
- package/{src → dist}/error.js +1 -3
- package/dist/http.d.ts +135 -0
- package/{src → dist}/http.js +3 -65
- package/dist/index.d.ts +58 -0
- package/dist/index.js +90 -0
- package/dist/logger.d.ts +15 -0
- package/{src → dist}/logger.js +4 -9
- package/dist/rfc8555.d.ts +106 -0
- package/dist/rfc8555.js +25 -0
- package/dist/types.d.ts +117 -0
- package/dist/types.js +1 -0
- package/dist/util.d.ts +85 -0
- package/{src → dist}/util.js +11 -76
- package/dist/verify.d.ts +14 -0
- package/dist/verify.js +214 -0
- package/dist/wait.d.ts +1 -0
- package/{src → dist}/wait.js +1 -0
- package/package.json +18 -12
- package/types/index.d.ts +15 -4
- package/types/index.test-d.ts +10 -3
- package/src/index.js +0 -106
- package/src/verify.js +0 -231
package/src/verify.js
DELETED
|
@@ -1,231 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ACME challenge verification
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import dnsSdk from "dns"
|
|
6
|
-
import https from 'https'
|
|
7
|
-
import {log as defaultLog} from './logger.js'
|
|
8
|
-
import axios from './axios.js'
|
|
9
|
-
import * as util from './util.js'
|
|
10
|
-
import {isAlpnCertificateAuthorizationValid} from './crypto/index.js'
|
|
11
|
-
import {utils} from '@certd/basic'
|
|
12
|
-
|
|
13
|
-
const dns = dnsSdk.promises
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
export function createChallengeFn(opts = {}){
|
|
17
|
-
const logger = opts?.logger || {info:defaultLog,error:defaultLog,warn:defaultLog,debug:defaultLog}
|
|
18
|
-
|
|
19
|
-
const log = function(...args){
|
|
20
|
-
logger.info(...args)
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Verify ACME HTTP challenge
|
|
24
|
-
*
|
|
25
|
-
* https://datatracker.ietf.org/doc/html/rfc8555#section-8.3
|
|
26
|
-
*
|
|
27
|
-
* @param {object} authz Identifier authorization
|
|
28
|
-
* @param {object} challenge Authorization challenge
|
|
29
|
-
* @param {string} keyAuthorization Challenge key authorization
|
|
30
|
-
* @param {string} [suffix] URL suffix
|
|
31
|
-
* @returns {Promise<boolean>}
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
|
|
35
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
53
|
-
|
|
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
|
|
59
|
-
|
|
60
|
-
}
|
|
61
|
-
|
|
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
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Walk DNS until TXT records are found
|
|
85
|
-
*/
|
|
86
|
-
|
|
87
|
-
async function walkDnsChallengeRecord(recordName, resolver = dns,deep = 0) {
|
|
88
|
-
|
|
89
|
-
let records = [];
|
|
90
|
-
|
|
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);
|
|
99
|
-
}
|
|
100
|
-
} catch (e) {
|
|
101
|
-
log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
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);
|
|
116
|
-
}
|
|
117
|
-
}else{
|
|
118
|
-
log(`没有CNAME映射(${recordName})`);
|
|
119
|
-
}
|
|
120
|
-
} catch (e) {
|
|
121
|
-
log(`检查CNAME出错(${recordName}) :${e.message}`);
|
|
122
|
-
}
|
|
123
|
-
return records
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
async function walkTxtRecord(recordName,deep = 0) {
|
|
127
|
-
if(deep >5){
|
|
128
|
-
log(`walkTxtRecord too deep (#${deep}) , skip walk`)
|
|
129
|
-
return []
|
|
130
|
-
}
|
|
131
|
-
|
|
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)
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
} catch (e) {
|
|
144
|
-
log(`本地获取TXT解析记录失败:${e.message}`)
|
|
145
|
-
}
|
|
146
|
-
|
|
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)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
189
|
-
}
|
|
190
|
-
|
|
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
|
-
*/
|
|
205
|
-
|
|
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}`);
|
|
210
|
-
|
|
211
|
-
const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
|
|
212
|
-
log('Certificate received from server successfully, matching key authorization in ALPN');
|
|
213
|
-
|
|
214
|
-
if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
|
|
215
|
-
throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
|
219
|
-
return true;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
challenges:{
|
|
224
|
-
'http-01': verifyHttpChallenge,
|
|
225
|
-
'dns-01': verifyDnsChallenge,
|
|
226
|
-
'tls-alpn-01': verifyTlsAlpnChallenge,
|
|
227
|
-
},
|
|
228
|
-
walkTxtRecord,
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
}
|