@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/verify.js ADDED
@@ -0,0 +1,214 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * ACME challenge verification
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
+ const dns = dnsSdk.promises;
13
+ let walkFromAuthoritative = true;
14
+ export function setWalkFromAuthoritative(value = true) {
15
+ walkFromAuthoritative = value;
16
+ }
17
+ export function createChallengeFn(opts = {}) {
18
+ const logger = opts?.logger || { info: defaultLog, error: defaultLog, warn: defaultLog, debug: defaultLog };
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
+ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
34
+ async function doQuery(challengeUrl) {
35
+ log(`正在测试请求 ${challengeUrl} `);
36
+ // const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
37
+ // const challengeUrl = `https://${authz.identifier.value}:${httpsPort}${suffix}`;
38
+ /* May redirect to HTTPS with invalid/self-signed cert - https://letsencrypt.org/docs/challenge-types/#http-01-challenge */
39
+ const httpsAgent = new https.Agent({ rejectUnauthorized: false });
40
+ log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
41
+ let data = "";
42
+ try {
43
+ const resp = await axios.get(challengeUrl, { httpsAgent });
44
+ data = (resp.data || '').replace(/\s+$/, '');
45
+ }
46
+ catch (e) {
47
+ log(`[error] HTTP request error from ${authz.identifier.value}`, e.message);
48
+ return false;
49
+ }
50
+ if (!data || (data !== keyAuthorization)) {
51
+ log(`[error] Authorization not found in HTTP response from ${authz.identifier.value}`);
52
+ return false;
53
+ }
54
+ return true;
55
+ }
56
+ const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
57
+ let host = authz.identifier.value;
58
+ if (utils.domain.isIpv6(host)) {
59
+ host = `[${host}]`;
60
+ }
61
+ const challengeUrl = `http://${host}:${httpPort}${suffix}`;
62
+ if (!await doQuery(challengeUrl)) {
63
+ const httpsPort = axios.defaults.acmeSettings.httpsChallengePort || 443;
64
+ const httpsChallengeUrl = `https://${host}:${httpsPort}${suffix}`;
65
+ const res = await doQuery(httpsChallengeUrl);
66
+ if (!res) {
67
+ throw new Error(`[error] 验证失败,请检查以上测试url是否可以正常访问`);
68
+ }
69
+ }
70
+ log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
71
+ return true;
72
+ }
73
+ /**
74
+ * Walk DNS until TXT records are found
75
+ */
76
+ async function walkDnsChallengeRecord(recordName, resolver = dns, deep = 0) {
77
+ let records = [];
78
+ const isAuthoritative = resolver === dns;
79
+ /* Resolve TXT records */
80
+ try {
81
+ log(`检查域名 ${recordName} 的TXT记录(from ${isAuthoritative ? '本地DNS' : '权威DNS服务器'})`);
82
+ const txtRecords = await resolver.resolveTxt(recordName);
83
+ if (txtRecords && txtRecords.length) {
84
+ log(`找到 ${txtRecords.length} 条 TXT记录( ${recordName})`);
85
+ log(`TXT records: ${JSON.stringify(txtRecords)}`);
86
+ records = records.concat(...txtRecords);
87
+ }
88
+ }
89
+ catch (e) {
90
+ log(`解析 TXT 记录出错, ${recordName} :${e.message}`);
91
+ }
92
+ /* Resolve CNAME record first */
93
+ try {
94
+ log(`检查是否存在CNAME映射: ${recordName}`);
95
+ const cnameRecords = await resolver.resolveCname(recordName);
96
+ if (cnameRecords.length) {
97
+ const cnameRecord = cnameRecords[0];
98
+ log(`已找到${recordName}的CNAME记录,将检查: ${cnameRecord}`);
99
+ let res = await walkTxtRecord(cnameRecord, deep + 1);
100
+ if (res && res.length) {
101
+ log(`从CNAME中找到TXT记录: ${JSON.stringify(res)}`);
102
+ records = records.concat(...res);
103
+ }
104
+ }
105
+ else {
106
+ log(`没有CNAME映射(${recordName})`);
107
+ }
108
+ }
109
+ catch (e) {
110
+ log(`检查CNAME出错(${recordName}) :${e.message}`);
111
+ }
112
+ return records;
113
+ }
114
+ async function walkTxtRecord(recordName, deep = 0) {
115
+ if (deep > 5) {
116
+ log(`walkTxtRecord too deep (#${deep}) , skip walk`);
117
+ return [];
118
+ }
119
+ const txtRecords = [];
120
+ try {
121
+ /* Default DNS resolver first */
122
+ log('从本地DNS服务器获取TXT解析记录');
123
+ const res = await walkDnsChallengeRecord(recordName, dns, deep);
124
+ if (res && res.length > 0) {
125
+ for (const item of res) {
126
+ txtRecords.push(item);
127
+ }
128
+ }
129
+ }
130
+ catch (e) {
131
+ log(`本地获取TXT解析记录失败:${e.message}`);
132
+ }
133
+ if (walkFromAuthoritative !== false) {
134
+ try {
135
+ /* Authoritative DNS resolver */
136
+ log(`从域名权威服务器获取TXT解析记录`);
137
+ const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName, log);
138
+ const res = await walkDnsChallengeRecord(recordName, authoritativeResolver, deep);
139
+ if (res && res.length > 0) {
140
+ for (const item of res) {
141
+ txtRecords.push(item);
142
+ }
143
+ }
144
+ }
145
+ catch (e) {
146
+ log(`权威服务器获取TXT解析记录失败:${e.message}`);
147
+ }
148
+ }
149
+ else {
150
+ log(`跳过从权威服务器获取TXT解析记录`);
151
+ }
152
+ if (txtRecords.length === 0) {
153
+ throw new Error(`没有找到TXT解析记录(${recordName})`);
154
+ }
155
+ return txtRecords;
156
+ }
157
+ /**
158
+ * Verify ACME DNS challenge
159
+ *
160
+ * https://datatracker.ietf.org/doc/html/rfc8555#section-8.4
161
+ *
162
+ * @param {object} authz Identifier authorization
163
+ * @param {object} challenge Authorization challenge
164
+ * @param {string} keyAuthorization Challenge key authorization
165
+ * @param {string} [prefix] DNS prefix
166
+ * @returns {Promise<boolean>}
167
+ */
168
+ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
169
+ const recordName = `${prefix}${authz.identifier.value}`;
170
+ log(`本地校验TXT记录): ${recordName}`);
171
+ let recordValues = await walkTxtRecord(recordName, 0, walkFromAuthoritative);
172
+ //去重
173
+ recordValues = [...new Set(recordValues)];
174
+ log(`DNS查询成功, 找到 ${recordValues.length} 条TXT记录:${recordValues}`);
175
+ if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
176
+ const err = `没有找到需要的DNS TXT记录: ${recordName},期望:${keyAuthorization},结果:${recordValues}`;
177
+ throw new Error(err);
178
+ }
179
+ log(`关键授权匹配成功(${challenge.type}/${recordName}):${keyAuthorization},校验成功, ACME challenge verified`);
180
+ return true;
181
+ }
182
+ /**
183
+ * Verify ACME TLS ALPN challenge
184
+ *
185
+ * https://datatracker.ietf.org/doc/html/rfc8737
186
+ *
187
+ * @param {object} authz Identifier authorization
188
+ * @param {object} challenge Authorization challenge
189
+ * @param {string} keyAuthorization Challenge key authorization
190
+ * @returns {Promise<boolean>}
191
+ */
192
+ async function verifyTlsAlpnChallenge(authz, challenge, keyAuthorization) {
193
+ const tlsAlpnPort = axios.defaults.acmeSettings.tlsAlpnChallengePort || 443;
194
+ const host = authz.identifier.value;
195
+ log(`Establishing TLS connection with host: ${host}:${tlsAlpnPort}`);
196
+ const certificate = await util.retrieveTlsAlpnCertificate(host, tlsAlpnPort);
197
+ log('Certificate received from server successfully, matching key authorization in ALPN');
198
+ if (!isAlpnCertificateAuthorizationValid(certificate, keyAuthorization)) {
199
+ throw new Error(`Authorization not found in certificate from ${authz.identifier.value}`);
200
+ }
201
+ log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
202
+ return true;
203
+ }
204
+ return {
205
+ challenges: {
206
+ 'http-01': verifyHttpChallenge,
207
+ 'dns-01': verifyDnsChallenge,
208
+ 'tls-alpn-01': verifyTlsAlpnChallenge,
209
+ },
210
+ walkTxtRecord,
211
+ walkDnsChallengeRecord,
212
+ };
213
+ }
214
+ // createChallengeFn({logger:{info:console.log}}).walkDnsChallengeRecord("handsfree.work")
package/dist/wait.d.ts ADDED
@@ -0,0 +1 @@
1
+ export declare function wait(ms: any): Promise<unknown>;
@@ -1,3 +1,4 @@
1
+ // @ts-nocheck
1
2
  export async function wait(ms) {
2
3
  return new Promise((resolve) => {
3
4
  setTimeout(resolve, ms);
package/package.json CHANGED
@@ -3,22 +3,22 @@
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.13",
7
7
  "type": "module",
8
- "module": "scr/index.js",
9
- "main": "src/index.js",
10
- "types": "types/index.d.ts",
8
+ "module": "./dist/index.js",
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
11
  "license": "MIT",
12
12
  "homepage": "https://github.com/publishlab/node-acme-client",
13
13
  "engines": {
14
14
  "node": ">= 18"
15
15
  },
16
16
  "files": [
17
- "src",
17
+ "dist",
18
18
  "types"
19
19
  ],
20
20
  "dependencies": {
21
- "@certd/basic": "^1.39.11",
21
+ "@certd/basic": "^1.39.13",
22
22
  "@peculiar/x509": "^1.11.0",
23
23
  "asn1js": "^3.0.5",
24
24
  "axios": "^1.9.0",
@@ -35,10 +35,12 @@
35
35
  "@typescript-eslint/parser": "^8.26.1",
36
36
  "chai": "^4.4.1",
37
37
  "chai-as-promised": "^7.1.2",
38
+ "cross-env": "^7.0.3",
38
39
  "eslint": "^8.57.0",
39
40
  "eslint-config-prettier": "^8.5.0",
40
41
  "eslint-plugin-import": "^2.29.1",
41
42
  "eslint-plugin-prettier": "^4.2.1",
43
+ "esmock": "^2.7.5",
42
44
  "jsdoc-to-markdown": "^8.0.1",
43
45
  "mocha": "^10.6.0",
44
46
  "nock": "^13.5.4",
@@ -47,13 +49,17 @@
47
49
  "typescript": "^5.4.2"
48
50
  },
49
51
  "scripts": {
50
- "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",
51
- "lint": "eslint .",
52
- "lint-types": "tsd",
53
- "prepublishOnly": "npm run build-docs",
52
+ "before-build": "node -e \"const fs=require('fs');fs.rmSync('dist',{recursive:true,force:true});fs.rmSync('tsconfig.tsbuildinfo',{force:true});\"",
53
+ "build": "npm run before-build && tsc --skipLibCheck",
54
+ "build-docs": "jsdoc2md dist/client.js > docs/client.md && jsdoc2md dist/crypto/index.js > docs/crypto.md && jsdoc2md dist/crypto/forge.js > docs/forge.md",
55
+ "lint": "eslint \"src/**/*.ts\" \"types/**/*.ts\"",
56
+ "lint-types": "tsd --files \"types/index.test-d.ts\"",
57
+ "prepublishOnly": "npm run build && npm run build-docs",
54
58
  "test": "mocha -t 60000 \"test/setup.js\" \"test/**/*.spec.js\"",
59
+ "before-test:unit": "node -e \"const fs=require('fs');fs.rmSync('dist-test',{recursive:true,force:true});fs.rmSync('tsconfig.test.tsbuildinfo',{force:true});\"",
60
+ "test:unit": "cross-env NODE_ENV=unittest npm run before-test:unit && cross-env NODE_ENV=unittest tsc -p tsconfig.test.json --skipLibCheck && cross-env NODE_ENV=unittest mocha -t 60000 \"dist-test/**/*.test.js\"",
55
61
  "pub": "npm publish",
56
- "compile": "echo '1'"
62
+ "compile": "tsc --skipLibCheck --watch"
57
63
  },
58
64
  "repository": {
59
65
  "type": "git",
@@ -70,5 +76,5 @@
70
76
  "bugs": {
71
77
  "url": "https://github.com/publishlab/node-acme-client/issues"
72
78
  },
73
- "gitHead": "ec466dc818eace59825d8ae2ebbc9fc75a94a6b0"
79
+ "gitHead": "9f7d766cb386b299d4098141f4a47d23e16975e3"
74
80
  }
package/types/index.d.ts CHANGED
@@ -4,8 +4,6 @@
4
4
 
5
5
  import { AxiosInstance } from 'axios';
6
6
  import * as rfc8555 from './rfc8555';
7
- import {CancelError} from '../src/error.js'
8
- export * from '../src/error.js'
9
7
 
10
8
  export type PrivateKeyBuffer = Buffer;
11
9
  export type PublicKeyBuffer = Buffer;
@@ -115,6 +113,15 @@ export const directory: {
115
113
  zerossl: {
116
114
  staging: string,
117
115
  production: string
116
+ },
117
+ sslcom: {
118
+ staging: string,
119
+ production: string,
120
+ ec: string
121
+ },
122
+ litessl: {
123
+ staging: string,
124
+ production: string
118
125
  }
119
126
  };
120
127
 
@@ -211,12 +218,16 @@ export const agents: any;
211
218
  * Logger
212
219
  */
213
220
 
221
+ export class CancelError extends Error {
222
+ constructor(message?: string);
223
+ }
224
+
214
225
  export function setLogger(fn: (message: any, ...args: any[]) => void): void;
215
226
 
216
227
  export function createChallengeFn(opts?: {logger?:any}): any;
217
228
  // export function walkTxtRecord(record: any): Promise<string[]>;
218
229
  export function getAuthoritativeDnsResolver(record:string): Promise<any>;
219
230
 
220
- export const CancelError: typeof CancelError;
231
+ export function resolveDomainBySoaRecord(domain: string): Promise<string>;
221
232
 
222
- export function resolveDomainBySoaRecord(domain: string): Promise<string>;
233
+ export function setWalkFromAuthoritative(value?: boolean): void;
@@ -2,7 +2,7 @@
2
2
  * acme-client type definition tests
3
3
  */
4
4
 
5
- import * as acme from 'acme-client';
5
+ import * as acme from '..';
6
6
 
7
7
  (async () => {
8
8
  /* Client */
@@ -10,6 +10,7 @@ import * as acme from 'acme-client';
10
10
 
11
11
  const client = new acme.Client({
12
12
  accountKey,
13
+ sslProvider: 'letsencrypt',
13
14
  directoryUrl: acme.directory.letsencrypt.staging
14
15
  });
15
16
 
@@ -52,7 +53,10 @@ import * as acme from 'acme-client';
52
53
  /* Auto */
53
54
  await client.auto({
54
55
  csr: certCsr,
55
- challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
56
+ challengeCreateFn: async (authz, keyAuthorization) => ({
57
+ challenge: authz.challenges[0],
58
+ keyAuthorization: await keyAuthorization(authz.challenges[0])
59
+ }),
56
60
  challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}
57
61
  });
58
62
 
@@ -63,7 +67,10 @@ import * as acme from 'acme-client';
63
67
  skipChallengeVerification: false,
64
68
  challengePriority: ['http-01', 'dns-01'],
65
69
  preferredChain: 'DST Root CA X3',
66
- challengeCreateFn: async (authz, challenge, keyAuthorization) => {},
70
+ challengeCreateFn: async (authz, keyAuthorization) => ({
71
+ challenge: authz.challenges[0],
72
+ keyAuthorization: await keyAuthorization(authz.challenges[0])
73
+ }),
67
74
  challengeRemoveFn: async (authz, challenge, keyAuthorization) => {}
68
75
  });
69
76
  })();
package/src/index.js DELETED
@@ -1,106 +0,0 @@
1
- /**
2
- * acme-client
3
- */
4
- import AcmeClinet from './client.js'
5
- export const Client = AcmeClinet
6
-
7
- /**
8
- * Directory URLs
9
- */
10
-
11
- export const directory = {
12
- buypass: {
13
- staging: 'https://api.test4.buypass.no/acme/directory',
14
- production: 'https://api.buypass.com/acme/directory',
15
- },
16
- google: {
17
- staging: 'https://dv.acme-v02.test-api.pki.goog/directory',
18
- production: 'https://dv.acme-v02.api.pki.goog/directory',
19
- },
20
- letsencrypt: {
21
- staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
22
- production: 'https://acme-v02.api.letsencrypt.org/directory',
23
- },
24
- letsencrypt_staging: {
25
- production: 'https://acme-staging-v02.api.letsencrypt.org/directory',
26
- },
27
- zerossl: {
28
- staging: 'https://acme.zerossl.com/v2/DV90',
29
- production: 'https://acme.zerossl.com/v2/DV90',
30
- },
31
- sslcom:{
32
- staging: 'https://acme.ssl.com/sslcom-dv-rsa',
33
- production: 'https://acme.ssl.com/sslcom-dv-rsa',
34
- ec: 'https://acme.ssl.com/sslcom-dv-ecc',
35
- },
36
- litessl: {
37
- staging: 'https://acme.litessl.com/acme/v2/directory',
38
- production: 'https://acme.litessl.com/acme/v2/directory',
39
- },
40
- };
41
-
42
- export function getDirectoryUrl(opts) {
43
- const {sslProvider, pkType} = opts
44
- const list= directory[sslProvider]
45
- if (!list) {
46
- throw new Error(`sslProvider ${sslProvider} not found`)
47
- }
48
- let pkTypePrefix = pkType || 'rsa'
49
- if (pkType) {
50
- pkTypePrefix = pkType.toLowerCase().split("_")[0]
51
- }
52
-
53
- if (pkTypePrefix && list[pkTypePrefix]) {
54
- return list[pkTypePrefix]
55
- }
56
-
57
- return list.production
58
- }
59
-
60
-
61
- export function getAllSslProviderDomains() {
62
- const list = Object.values(directory).map((item) => {
63
- let url = item.production.replace('https://', '')
64
- url = url.substring(0, url.indexOf('/'))
65
- return url
66
- })
67
- return list
68
- }
69
-
70
- let sslProviderReverseProxies = {}
71
-
72
- function initSslProviderReverseProxies() {
73
- for (const sslProvider of getAllSslProviderDomains()) {
74
- sslProviderReverseProxies[sslProvider] = ""
75
- }
76
- }
77
- initSslProviderReverseProxies()
78
-
79
- export function getSslProviderReverseProxies() {
80
- return sslProviderReverseProxies
81
- }
82
- export function setSslProviderReverseProxies(reverseProxies) {
83
- Object.assign(sslProviderReverseProxies, reverseProxies)
84
- }
85
-
86
- /**
87
- * Crypto
88
- */
89
-
90
- export * as crypto from './crypto/index.js'
91
- export * as forge from './crypto/forge.js'
92
-
93
- /**
94
- * Axios
95
- */
96
-
97
- export * from './axios.js'
98
- /**
99
- * Logger
100
- */
101
-
102
- export * from './logger.js'
103
- export * from './verify.js'
104
- export * from './error.js'
105
-
106
- export * from './util.js'