@certd/acme-client 0.2.0 → 0.3.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/LICENSE +1 -1
- package/README.md +88 -25
- package/package.json +24 -29
- package/src/api.js +15 -8
- package/src/auto.js +93 -114
- 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 +51 -2
- package/types/test.ts +2 -2
- package/CHANGELOG.md +0 -152
- package/src/util.log.js +0 -8
package/src/verify.js
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
* ACME challenge verification
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
const
|
|
6
|
-
const
|
|
7
|
-
const logger = require('./util.log.js');
|
|
8
|
-
|
|
9
|
-
const debug = logger.info;
|
|
5
|
+
const dns = require('dns').promises;
|
|
6
|
+
const { log } = require('./logger');
|
|
10
7
|
const axios = require('./axios');
|
|
8
|
+
const util = require('./util');
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
/**
|
|
@@ -26,21 +24,59 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
|
|
|
26
24
|
const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
|
|
27
25
|
const challengeUrl = `http://${authz.identifier.value}:${httpPort}${suffix}`;
|
|
28
26
|
|
|
29
|
-
|
|
27
|
+
log(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
|
|
30
28
|
const resp = await axios.get(challengeUrl);
|
|
31
29
|
const data = (resp.data || '').replace(/\s+$/, '');
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
log(`Query successful, HTTP status code: ${resp.status}`);
|
|
34
32
|
|
|
35
33
|
if (!data || (data !== keyAuthorization)) {
|
|
36
34
|
throw new Error(`Authorization not found in HTTP response from ${authz.identifier.value}`);
|
|
37
35
|
}
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
log(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
|
|
40
38
|
return true;
|
|
41
39
|
}
|
|
42
40
|
|
|
43
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Walk DNS until TXT records are found
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
async function walkDnsChallengeRecord(recordName, resolver = dns) {
|
|
47
|
+
/* Resolve CNAME record first */
|
|
48
|
+
try {
|
|
49
|
+
log(`Checking name for CNAME records: ${recordName}`);
|
|
50
|
+
const cnameRecords = await resolver.resolveCname(recordName);
|
|
51
|
+
|
|
52
|
+
if (cnameRecords.length) {
|
|
53
|
+
log(`CNAME record found at ${recordName}, new challenge record name: ${cnameRecords[0]}`);
|
|
54
|
+
return walkDnsChallengeRecord(cnameRecords[0]);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
log(`No CNAME records found for name: ${recordName}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Resolve TXT records */
|
|
62
|
+
try {
|
|
63
|
+
log(`Checking name for TXT records: ${recordName}`);
|
|
64
|
+
const txtRecords = await resolver.resolveTxt(recordName);
|
|
65
|
+
|
|
66
|
+
if (txtRecords.length) {
|
|
67
|
+
log(`Found ${txtRecords.length} TXT records at ${recordName}`);
|
|
68
|
+
return [].concat(...txtRecords);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
catch (e) {
|
|
72
|
+
log(`No TXT records found for name: ${recordName}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* Found nothing */
|
|
76
|
+
throw new Error(`No TXT records found for name: ${recordName}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
44
80
|
/**
|
|
45
81
|
* Verify ACME DNS challenge
|
|
46
82
|
*
|
|
@@ -54,34 +90,29 @@ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix =
|
|
|
54
90
|
*/
|
|
55
91
|
|
|
56
92
|
async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
|
|
57
|
-
|
|
58
|
-
|
|
93
|
+
let recordValues = [];
|
|
94
|
+
const recordName = `${prefix}${authz.identifier.value}`;
|
|
95
|
+
log(`Resolving DNS TXT from record: ${recordName}`);
|
|
59
96
|
|
|
60
97
|
try {
|
|
61
|
-
/*
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (cnameRecords.length) {
|
|
66
|
-
logger.info(`CNAME found at ${challengeRecord}, new challenge record: ${cnameRecords[0]}`);
|
|
67
|
-
challengeRecord = cnameRecords[0];
|
|
68
|
-
}
|
|
98
|
+
/* Default DNS resolver first */
|
|
99
|
+
log('Attempting to resolve TXT with default DNS resolver first');
|
|
100
|
+
recordValues = await walkDnsChallengeRecord(recordName);
|
|
69
101
|
}
|
|
70
102
|
catch (e) {
|
|
71
|
-
|
|
103
|
+
/* Authoritative DNS resolver */
|
|
104
|
+
log(`Error using default resolver, attempting to resolve TXT with authoritative NS: ${e.message}`);
|
|
105
|
+
const authoritativeResolver = await util.getAuthoritativeDnsResolver(recordName);
|
|
106
|
+
recordValues = await walkDnsChallengeRecord(recordName, authoritativeResolver);
|
|
72
107
|
}
|
|
73
108
|
|
|
74
|
-
|
|
75
|
-
const result = await dns.resolveTxtAsync(challengeRecord);
|
|
76
|
-
const records = [].concat(...result);
|
|
77
|
-
|
|
78
|
-
logger.info(`Query successful, found ${records.length} DNS TXT records`);
|
|
109
|
+
log(`DNS query finished successfully, found ${recordValues.length} TXT records`);
|
|
79
110
|
|
|
80
|
-
if (
|
|
81
|
-
throw new Error(`Authorization not found in DNS TXT
|
|
111
|
+
if (!recordValues.length || !recordValues.includes(keyAuthorization)) {
|
|
112
|
+
throw new Error(`Authorization not found in DNS TXT record: ${recordName}`);
|
|
82
113
|
}
|
|
83
114
|
|
|
84
|
-
|
|
115
|
+
log(`Key authorization match for ${challenge.type}/${recordName}, ACME challenge verified`);
|
|
85
116
|
return true;
|
|
86
117
|
}
|
|
87
118
|
|
package/types/index.d.ts
CHANGED
|
@@ -37,11 +37,17 @@ export interface ClientOptions {
|
|
|
37
37
|
directoryUrl: string;
|
|
38
38
|
accountKey: PrivateKeyBuffer | PrivateKeyString;
|
|
39
39
|
accountUrl?: string;
|
|
40
|
+
externalAccountBinding?: ClientExternalAccountBindingOptions;
|
|
40
41
|
backoffAttempts?: number;
|
|
41
42
|
backoffMin?: number;
|
|
42
43
|
backoffMax?: number;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
export interface ClientExternalAccountBindingOptions {
|
|
47
|
+
kid: string;
|
|
48
|
+
hmacKey: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
45
51
|
export interface ClientAutoOptions {
|
|
46
52
|
csr: CsrBuffer | CsrString;
|
|
47
53
|
challengeCreateFn: (authz: Authorization, challenge: rfc8555.Challenge, keyAuthorization: string) => Promise<any>;
|
|
@@ -69,7 +75,7 @@ export class Client {
|
|
|
69
75
|
verifyChallenge(authz: Authorization, challenge: rfc8555.Challenge): Promise<boolean>;
|
|
70
76
|
completeChallenge(challenge: rfc8555.Challenge): Promise<rfc8555.Challenge>;
|
|
71
77
|
waitForValidStatus<T = Order | Authorization | rfc8555.Challenge>(item: T): Promise<T>;
|
|
72
|
-
getCertificate(order: Order, preferredChain?: string
|
|
78
|
+
getCertificate(order: Order, preferredChain?: string): Promise<string>;
|
|
73
79
|
revokeCertificate(cert: CertificateBuffer | CertificateString, data?: rfc8555.CertificateRevocationRequest): Promise<void>;
|
|
74
80
|
auto(opts: ClientAutoOptions): Promise<string>;
|
|
75
81
|
}
|
|
@@ -80,9 +86,16 @@ export class Client {
|
|
|
80
86
|
*/
|
|
81
87
|
|
|
82
88
|
export const directory: {
|
|
89
|
+
buypass: {
|
|
90
|
+
staging: string,
|
|
91
|
+
production: string
|
|
92
|
+
},
|
|
83
93
|
letsencrypt: {
|
|
84
94
|
staging: string,
|
|
85
95
|
production: string
|
|
96
|
+
},
|
|
97
|
+
zerossl: {
|
|
98
|
+
production: string
|
|
86
99
|
}
|
|
87
100
|
};
|
|
88
101
|
|
|
@@ -119,7 +132,36 @@ export interface CsrOptions {
|
|
|
119
132
|
emailAddress?: string;
|
|
120
133
|
}
|
|
121
134
|
|
|
135
|
+
export interface RsaPublicJwk {
|
|
136
|
+
e: string;
|
|
137
|
+
kty: string;
|
|
138
|
+
n: string;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export interface EcdsaPublicJwk {
|
|
142
|
+
crv: string;
|
|
143
|
+
kty: string;
|
|
144
|
+
x: string;
|
|
145
|
+
y: string;
|
|
146
|
+
}
|
|
147
|
+
|
|
122
148
|
export interface CryptoInterface {
|
|
149
|
+
createPrivateKey(keySize?: number): Promise<PrivateKeyBuffer>;
|
|
150
|
+
createPrivateRsaKey(keySize?: number): Promise<PrivateKeyBuffer>;
|
|
151
|
+
createPrivateEcdsaKey(namedCurve?: 'P-256' | 'P-384' | 'P-521'): Promise<PrivateKeyBuffer>;
|
|
152
|
+
getPublicKey(keyPem: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString): PublicKeyBuffer;
|
|
153
|
+
getJwk(keyPem: PrivateKeyBuffer | PrivateKeyString | PublicKeyBuffer | PublicKeyString): RsaPublicJwk | EcdsaPublicJwk;
|
|
154
|
+
splitPemChain(chainPem: CertificateBuffer | CertificateString): string[];
|
|
155
|
+
getPemBodyAsB64u(pem: CertificateBuffer | CertificateString): string;
|
|
156
|
+
readCsrDomains(csrPem: CsrBuffer | CsrString): CertificateDomains;
|
|
157
|
+
readCertificateInfo(certPem: CertificateBuffer | CertificateString): CertificateInfo;
|
|
158
|
+
createCsr(data: CsrOptions, keyPem?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CsrBuffer]>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const crypto: CryptoInterface;
|
|
162
|
+
|
|
163
|
+
/* TODO: LEGACY */
|
|
164
|
+
export interface CryptoLegacyInterface {
|
|
123
165
|
createPrivateKey(size?: number): Promise<PrivateKeyBuffer>;
|
|
124
166
|
createPublicKey(key: PrivateKeyBuffer | PrivateKeyString): Promise<PublicKeyBuffer>;
|
|
125
167
|
getPemBody(str: string): string;
|
|
@@ -131,7 +173,7 @@ export interface CryptoInterface {
|
|
|
131
173
|
createCsr(data: CsrOptions, key?: PrivateKeyBuffer | PrivateKeyString): Promise<[PrivateKeyBuffer, CsrBuffer]>;
|
|
132
174
|
}
|
|
133
175
|
|
|
134
|
-
export const forge:
|
|
176
|
+
export const forge: CryptoLegacyInterface;
|
|
135
177
|
|
|
136
178
|
|
|
137
179
|
/**
|
|
@@ -139,3 +181,10 @@ export const forge: CryptoInterface;
|
|
|
139
181
|
*/
|
|
140
182
|
|
|
141
183
|
export const axios: AxiosInstance;
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Logger
|
|
188
|
+
*/
|
|
189
|
+
|
|
190
|
+
export function setLogger(fn: (msg: string) => void): void;
|
package/types/test.ts
CHANGED
|
@@ -7,7 +7,7 @@ import * as acme from 'acme-client';
|
|
|
7
7
|
|
|
8
8
|
(async () => {
|
|
9
9
|
/* Client */
|
|
10
|
-
const accountKey = await acme.
|
|
10
|
+
const accountKey = await acme.crypto.createPrivateKey();
|
|
11
11
|
|
|
12
12
|
const client = new acme.Client({
|
|
13
13
|
accountKey,
|
|
@@ -41,7 +41,7 @@ import * as acme from 'acme-client';
|
|
|
41
41
|
await client.waitForValidStatus(challenge);
|
|
42
42
|
|
|
43
43
|
/* Finalize */
|
|
44
|
-
const [certKey, certCsr] = await acme.
|
|
44
|
+
const [certKey, certCsr] = await acme.crypto.createCsr({
|
|
45
45
|
commonName: 'example.com',
|
|
46
46
|
altNames: ['example.com', '*.example.com']
|
|
47
47
|
});
|
package/CHANGELOG.md
DELETED
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## v4.1.2 (2020-11-16)
|
|
4
|
-
|
|
5
|
-
* `fixed` Bug when encoding PEM payloads, potentially causing malformed requests
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
## v4.1.1 (2020-11-13)
|
|
9
|
-
|
|
10
|
-
* `fixed` Missing TypeScript definitions
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
## v4.1.0 (2020-11-12)
|
|
14
|
-
|
|
15
|
-
* `added` Option `preferredChain` added to `client.getCertificate()` and `client.auto()` to indicate which certificate chain is preferred if a CA offers multiple
|
|
16
|
-
* Related: [https://community.letsencrypt.org/t/transition-to-isrgs-root-delayed-until-jan-11-2021/125516](https://community.letsencrypt.org/t/transition-to-isrgs-root-delayed-until-jan-11-2021/125516)
|
|
17
|
-
* `added` Method `client.getOrder()` to refresh order from CA
|
|
18
|
-
* `fixed` Upgrade `axios@0.21.0`
|
|
19
|
-
* `fixed` Error when attempting to revoke a certificate chain
|
|
20
|
-
* `fixed` Missing URL augmentation in `client.finalizeOrder()` and `client.deactivateAuthorization()`
|
|
21
|
-
* `fixed` Add certificate issuer to response from `forge.readCertificateInfo()`
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
## v4.0.2 (2020-10-09)
|
|
25
|
-
|
|
26
|
-
* `fixed` Explicitly set default `axios` HTTP adapter - [axios/axios#1180](https://github.com/axios/axios/issues/1180)
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
## v4.0.1 (2020-09-15)
|
|
30
|
-
|
|
31
|
-
* `fixed` Upgrade `node-forge@0.10.0` - [CVE-2020-7720](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2020-7720)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
## v4.0.0 (2020-05-29)
|
|
35
|
-
|
|
36
|
-
* `fixed` Incorrect TypeScript `CertificateInfo` definitions
|
|
37
|
-
* `fixed` Allow trailing whitespace character in `http-01` challenge response
|
|
38
|
-
* `breaking` Remove support for Node v8
|
|
39
|
-
* `breaking` Remove deprecated `openssl` crypto module
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
## v3.3.1 (2020-01-07)
|
|
43
|
-
|
|
44
|
-
* `fixed` Improvements to TypeScript definitions
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
## v3.3.0 (2019-12-19)
|
|
48
|
-
|
|
49
|
-
* `added` TypeScript definitions
|
|
50
|
-
* `fixed` Allow missing ACME directory meta field - [RFC 8555 Section 7.1.1](https://tools.ietf.org/html/rfc8555#section-7.1.1)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
## v3.2.1 (2019-11-14)
|
|
54
|
-
|
|
55
|
-
* `added` New option `skipChallengeVerification` added to `client.auto()` to bypass internal challenge verification
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
## v3.2.0 (2019-08-26)
|
|
59
|
-
|
|
60
|
-
* `added` More extensive testing using [letsencrypt/pebble](https://github.com/letsencrypt/pebble)
|
|
61
|
-
* `changed` When creating a CSR, `commonName` no longer defaults to `'localhost'`
|
|
62
|
-
* This change is not considered breaking since `commonName: 'localhost'` will result in an error when ordering a certificate
|
|
63
|
-
* `fixed` Retry signed API requests on `urn:ietf:params:acme:error:badNonce` - [RFC 8555 Section 6.5](https://tools.ietf.org/html/rfc8555#section-6.5)
|
|
64
|
-
* `fixed` Minor bugs related to `POST-as-GET` when calling `updateAccount()`
|
|
65
|
-
* `fixed` Ensure subject common name is present in SAN when creating a CSR - [CAB v1.2.3 Section 9.2.2](https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf)
|
|
66
|
-
* `fixed` Send empty JSON body when responding to challenges - [RFC 8555 Section 7.5.1](https://tools.ietf.org/html/rfc8555#section-7.5.1)
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
## v2.3.1 (2019-08-26)
|
|
70
|
-
|
|
71
|
-
* `backport` Minor bugs related to `POST-as-GET` when calling `client.updateAccount()`
|
|
72
|
-
* `backport` Send empty JSON body when responding to challenges
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
## v3.1.0 (2019-08-21)
|
|
76
|
-
|
|
77
|
-
* `added` UTF-8 support when generating a CSR subject using forge - [RFC 5280](https://tools.ietf.org/html/rfc5280)
|
|
78
|
-
* `fixed` Implemented `POST-as-GET` for all ACME API requests - [RFC 8555 Section 6.3](https://tools.ietf.org/html/rfc8555#section-6.3)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
## v2.3.0 (2019-08-21)
|
|
82
|
-
|
|
83
|
-
* `backport` Implemented `POST-as-GET` for all ACME API requests
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
## v3.0.0 (2019-07-13)
|
|
87
|
-
|
|
88
|
-
* `added` Expose `axios` instance to allow manipulating HTTP client defaults
|
|
89
|
-
* `breaking` Remove support for Node v4 and v6
|
|
90
|
-
* `breaking` Remove Babel transpilation
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
## v2.2.3 (2019-01-25)
|
|
94
|
-
|
|
95
|
-
* `added` DNS CNAME detection when verifying `dns-01` challenges
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
## v2.2.2 (2019-01-07)
|
|
99
|
-
|
|
100
|
-
* `added` Support for `tls-alpn-01` challenge key authorization
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
## v2.2.1 (2019-01-04)
|
|
104
|
-
|
|
105
|
-
* `fixed` Handle and throw errors from OpenSSL process
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
## v2.2.0 (2018-11-06)
|
|
109
|
-
|
|
110
|
-
* `added` New [node-forge](https://www.npmjs.com/package/node-forge) crypto engine, removes OpenSSL CLI dependency
|
|
111
|
-
* `added` Support native `crypto.generateKeyPair()` API when generating key pairs
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
## v2.1.0 (2018-10-21)
|
|
115
|
-
|
|
116
|
-
* `added` Ability to set and get current account URL
|
|
117
|
-
* `fixed` Replace HTTP client `request` with `axios`
|
|
118
|
-
* `fixed` Auto-mode no longer tries to create account when account URL exists
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
## v2.0.1 (2018-08-17)
|
|
122
|
-
|
|
123
|
-
* `fixed` Key rollover in compliance with [draft-ietf-acme-13](https://tools.ietf.org/html/draft-ietf-acme-acme-13)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
## v2.0.0 (2018-04-02)
|
|
127
|
-
|
|
128
|
-
* `breaking` ACMEv2
|
|
129
|
-
* `breaking` API changes
|
|
130
|
-
* `breaking` Rewrite to ES6
|
|
131
|
-
* `breaking` Promises instead of callbacks
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
## v1.0.0 (2017-10-20)
|
|
135
|
-
|
|
136
|
-
* API stable
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
## v0.2.1 (2017-09-27)
|
|
140
|
-
|
|
141
|
-
* `fixed` Bug causing invalid anti-replay nonce
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
## v0.2.0 (2017-09-21)
|
|
145
|
-
|
|
146
|
-
* `breaking` OpenSSL method `readCsrDomains` and `readCertificateInfo` now return domains as an object
|
|
147
|
-
* `fixed` Added and fixed some tests
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
## v0.1.0 (2017-09-14)
|
|
151
|
-
|
|
152
|
-
* `acme-client` released
|
package/src/util.log.js
DELETED