@certd/acme-client 0.1.6 → 0.2.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/src/util.js CHANGED
@@ -1,172 +1,173 @@
1
- /**
2
- * Utility methods
3
- */
4
-
5
- const Promise = require('bluebird');
6
- const Backoff = require('backo2');
7
- const logger = require('./util.log.js');
8
-
9
- const debug = logger.info;
10
- const forge = require('./crypto/forge');
11
-
12
-
13
- /**
14
- * Retry promise
15
- *
16
- * @param {function} fn Function returning promise that should be retried
17
- * @param {number} attempts Maximum number of attempts
18
- * @param {Backoff} backoff Backoff instance
19
- * @returns {Promise}
20
- */
21
-
22
- async function retryPromise(fn, attempts, backoff) {
23
- let aborted = false;
24
-
25
- try {
26
- const data = await fn(() => { aborted = true; });
27
- return data;
28
- }
29
- catch (e) {
30
- if (aborted || ((backoff.attempts + 1) >= attempts)) {
31
- throw e;
32
- }
33
-
34
- const duration = backoff.duration();
35
- logger.info(`Promise rejected attempt #${backoff.attempts}, retrying in ${duration}ms: ${e.message}`);
36
-
37
- await Promise.delay(duration);
38
- return retryPromise(fn, attempts, backoff);
39
- }
40
- }
41
-
42
-
43
- /**
44
- * Retry promise
45
- *
46
- * @param {function} fn Function returning promise that should be retried
47
- * @param {object} [backoffOpts] Backoff options
48
- * @param {number} [backoffOpts.attempts] Maximum number of attempts, default: `5`
49
- * @param {number} [backoffOpts.min] Minimum attempt delay in milliseconds, default: `5000`
50
- * @param {number} [backoffOpts.max] Maximum attempt delay in milliseconds, default: `30000`
51
- * @returns {Promise}
52
- */
53
-
54
- function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
55
- const backoff = new Backoff({ min, max });
56
- return retryPromise(fn, attempts, backoff);
57
- }
58
-
59
-
60
- /**
61
- * Escape base64 encoded string
62
- *
63
- * @param {string} str Base64 encoded string
64
- * @returns {string} Escaped string
65
- */
66
-
67
- function b64escape(str) {
68
- return str.replace(/\+/g, '-')
69
- .replace(/\//g, '_')
70
- .replace(/=/g, '');
71
- }
72
-
73
-
74
- /**
75
- * Base64 encode and escape buffer or string
76
- *
77
- * @param {buffer|string} str Buffer or string to be encoded
78
- * @returns {string} Escaped base64 encoded string
79
- */
80
-
81
- function b64encode(str) {
82
- const buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
83
- return b64escape(buf.toString('base64'));
84
- }
85
-
86
-
87
- /**
88
- * Parse URLs from link header
89
- *
90
- * @param {string} header Link header contents
91
- * @param {string} rel Link relation, default: `alternate`
92
- * @returns {array} Array of URLs
93
- */
94
-
95
- function parseLinkHeader(header, rel = 'alternate') {
96
- const relRe = new RegExp(`\\s*rel\\s*=\\s*"?${rel}"?`, 'i');
97
-
98
- const results = (header || '').split(/,\s*</).map((link) => {
99
- const [, linkUrl, linkParts] = link.match(/<?([^>]*)>;(.*)/) || [];
100
- return (linkUrl && linkParts && linkParts.match(relRe)) ? linkUrl : null;
101
- });
102
-
103
- return results.filter((r) => r);
104
- }
105
-
106
-
107
- /**
108
- * Find certificate chain with preferred issuer
109
- * If issuer can not be located, the first certificate will be returned
110
- *
111
- * @param {array} certificates Array of PEM encoded certificate chains
112
- * @param {string} issuer Preferred certificate issuer
113
- * @returns {Promise<string>} PEM encoded certificate chain
114
- */
115
-
116
- async function findCertificateChainForIssuer(chains, issuer) {
117
- try {
118
- return await Promise.any(chains.map(async (chain) => {
119
- /* Look up all issuers */
120
- const certs = forge.splitPemChain(chain);
121
- const infoCollection = await Promise.map(certs, forge.readCertificateInfo);
122
- const issuerCollection = infoCollection.map((i) => i.issuer.commonName);
123
-
124
- /* Found match, return it */
125
- if (issuerCollection.includes(issuer)) {
126
- logger.info(`Found matching certificate for preferred issuer="${issuer}", issuers=${JSON.stringify(issuerCollection)}`);
127
- return chain;
128
- }
129
-
130
- /* No match, throw error */
131
- logger.info(`Unable to match certificate for preferred issuer="${issuer}", issuers=${JSON.stringify(issuerCollection)}`);
132
- throw new Error('Certificate issuer mismatch');
133
- }));
134
- }
135
- catch (e) {
136
- /* No certificates matched, return default */
137
- logger.info(`Found no match in ${chains.length} certificate chains for preferred issuer="${issuer}", returning default certificate chain`);
138
- return chains[0];
139
- }
140
- }
141
-
142
-
143
- /**
144
- * Find and format error in response object
145
- *
146
- * @param {object} resp HTTP response
147
- * @returns {string} Error message
148
- */
149
-
150
- function formatResponseError(resp) {
151
- let result;
152
-
153
- if (resp.data.error) {
154
- result = resp.data.error.detail || resp.data.error;
155
- }
156
- else {
157
- result = resp.data.detail || JSON.stringify(resp.data);
158
- }
159
-
160
- return result.replace(/\n/g, '');
161
- }
162
-
163
-
164
- /* Export utils */
165
- module.exports = {
166
- retry,
167
- b64escape,
168
- b64encode,
169
- parseLinkHeader,
170
- findCertificateChainForIssuer,
171
- formatResponseError
172
- };
1
+ /**
2
+ * Utility methods
3
+ */
4
+
5
+ const Promise = require('bluebird');
6
+ const Backoff = require('backo2');
7
+ const logger = require('./util.log.js');
8
+
9
+ const debug = logger.info;
10
+ const forge = require('./crypto/forge');
11
+
12
+
13
+ /**
14
+ * Retry promise
15
+ *
16
+ * @param {function} fn Function returning promise that should be retried
17
+ * @param {number} attempts Maximum number of attempts
18
+ * @param {Backoff} backoff Backoff instance
19
+ * @returns {Promise}
20
+ */
21
+
22
+ async function retryPromise(fn, attempts, backoff) {
23
+ let aborted = false;
24
+
25
+ try {
26
+ const data = await fn(() => { aborted = true; });
27
+ return data;
28
+ }
29
+ catch (e) {
30
+ if (aborted || ((backoff.attempts + 1) >= attempts)) {
31
+ logger.error(e);
32
+ throw e;
33
+ }
34
+
35
+ const duration = backoff.duration();
36
+ logger.info(`Promise rejected attempt #${backoff.attempts}, retrying in ${duration}ms: ${e.message}`);
37
+
38
+ await Promise.delay(duration);
39
+ return retryPromise(fn, attempts, backoff);
40
+ }
41
+ }
42
+
43
+
44
+ /**
45
+ * Retry promise
46
+ *
47
+ * @param {function} fn Function returning promise that should be retried
48
+ * @param {object} [backoffOpts] Backoff options
49
+ * @param {number} [backoffOpts.attempts] Maximum number of attempts, default: `5`
50
+ * @param {number} [backoffOpts.min] Minimum attempt delay in milliseconds, default: `5000`
51
+ * @param {number} [backoffOpts.max] Maximum attempt delay in milliseconds, default: `30000`
52
+ * @returns {Promise}
53
+ */
54
+
55
+ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
56
+ const backoff = new Backoff({ min, max });
57
+ return retryPromise(fn, attempts, backoff);
58
+ }
59
+
60
+
61
+ /**
62
+ * Escape base64 encoded string
63
+ *
64
+ * @param {string} str Base64 encoded string
65
+ * @returns {string} Escaped string
66
+ */
67
+
68
+ function b64escape(str) {
69
+ return str.replace(/\+/g, '-')
70
+ .replace(/\//g, '_')
71
+ .replace(/=/g, '');
72
+ }
73
+
74
+
75
+ /**
76
+ * Base64 encode and escape buffer or string
77
+ *
78
+ * @param {buffer|string} str Buffer or string to be encoded
79
+ * @returns {string} Escaped base64 encoded string
80
+ */
81
+
82
+ function b64encode(str) {
83
+ const buf = Buffer.isBuffer(str) ? str : Buffer.from(str);
84
+ return b64escape(buf.toString('base64'));
85
+ }
86
+
87
+
88
+ /**
89
+ * Parse URLs from link header
90
+ *
91
+ * @param {string} header Link header contents
92
+ * @param {string} rel Link relation, default: `alternate`
93
+ * @returns {array} Array of URLs
94
+ */
95
+
96
+ function parseLinkHeader(header, rel = 'alternate') {
97
+ const relRe = new RegExp(`\\s*rel\\s*=\\s*"?${rel}"?`, 'i');
98
+
99
+ const results = (header || '').split(/,\s*</).map((link) => {
100
+ const [, linkUrl, linkParts] = link.match(/<?([^>]*)>;(.*)/) || [];
101
+ return (linkUrl && linkParts && linkParts.match(relRe)) ? linkUrl : null;
102
+ });
103
+
104
+ return results.filter((r) => r);
105
+ }
106
+
107
+
108
+ /**
109
+ * Find certificate chain with preferred issuer
110
+ * If issuer can not be located, the first certificate will be returned
111
+ *
112
+ * @param {array} certificates Array of PEM encoded certificate chains
113
+ * @param {string} issuer Preferred certificate issuer
114
+ * @returns {Promise<string>} PEM encoded certificate chain
115
+ */
116
+
117
+ async function findCertificateChainForIssuer(chains, issuer) {
118
+ try {
119
+ return await Promise.any(chains.map(async (chain) => {
120
+ /* Look up all issuers */
121
+ const certs = forge.splitPemChain(chain);
122
+ const infoCollection = await Promise.map(certs, forge.readCertificateInfo);
123
+ const issuerCollection = infoCollection.map((i) => i.issuer.commonName);
124
+
125
+ /* Found match, return it */
126
+ if (issuerCollection.includes(issuer)) {
127
+ logger.info(`Found matching certificate for preferred issuer="${issuer}", issuers=${JSON.stringify(issuerCollection)}`);
128
+ return chain;
129
+ }
130
+
131
+ /* No match, throw error */
132
+ logger.info(`Unable to match certificate for preferred issuer="${issuer}", issuers=${JSON.stringify(issuerCollection)}`);
133
+ throw new Error('Certificate issuer mismatch');
134
+ }));
135
+ }
136
+ catch (e) {
137
+ /* No certificates matched, return default */
138
+ logger.info(`Found no match in ${chains.length} certificate chains for preferred issuer="${issuer}", returning default certificate chain`);
139
+ return chains[0];
140
+ }
141
+ }
142
+
143
+
144
+ /**
145
+ * Find and format error in response object
146
+ *
147
+ * @param {object} resp HTTP response
148
+ * @returns {string} Error message
149
+ */
150
+
151
+ function formatResponseError(resp) {
152
+ let result;
153
+
154
+ if (resp.data.error) {
155
+ result = resp.data.error.detail || resp.data.error;
156
+ }
157
+ else {
158
+ result = resp.data.detail || JSON.stringify(resp.data);
159
+ }
160
+
161
+ return result.replace(/\n/g, '');
162
+ }
163
+
164
+
165
+ /* Export utils */
166
+ module.exports = {
167
+ retry,
168
+ b64escape,
169
+ b64encode,
170
+ parseLinkHeader,
171
+ findCertificateChainForIssuer,
172
+ formatResponseError
173
+ };
package/src/util.log.js CHANGED
@@ -1,8 +1,8 @@
1
- const log4js = require('log4js');
2
-
3
- log4js.configure({
4
- appenders: { std: { type: 'stdout' } },
5
- categories: { default: { appenders: ['std'], level: 'info' } }
6
- });
7
- const logger = log4js.getLogger('certd');
8
- module.exports = logger;
1
+ const log4js = require('log4js');
2
+
3
+ log4js.configure({
4
+ appenders: { std: { type: 'stdout' } },
5
+ categories: { default: { appenders: ['std'], level: 'info' } }
6
+ });
7
+ const logger = log4js.getLogger('certd');
8
+ module.exports = logger;
package/src/verify.js CHANGED
@@ -1,96 +1,96 @@
1
- /**
2
- * ACME challenge verification
3
- */
4
-
5
- const Promise = require('bluebird');
6
- const dns = Promise.promisifyAll(require('dns'));
7
- const logger = require('./util.log.js');
8
-
9
- const debug = logger.info;
10
- const axios = require('./axios');
11
-
12
-
13
- /**
14
- * Verify ACME HTTP challenge
15
- *
16
- * https://tools.ietf.org/html/rfc8555#section-8.3
17
- *
18
- * @param {object} authz Identifier authorization
19
- * @param {object} challenge Authorization challenge
20
- * @param {string} keyAuthorization Challenge key authorization
21
- * @param {string} [suffix] URL suffix
22
- * @returns {Promise<boolean>}
23
- */
24
-
25
- async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
26
- const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
27
- const challengeUrl = `http://${authz.identifier.value}:${httpPort}${suffix}`;
28
-
29
- logger.info(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
30
- const resp = await axios.get(challengeUrl);
31
- const data = (resp.data || '').replace(/\s+$/, '');
32
-
33
- logger.info(`Query successful, HTTP status code: ${resp.status}`);
34
-
35
- if (!data || (data !== keyAuthorization)) {
36
- throw new Error(`Authorization not found in HTTP response from ${authz.identifier.value}`);
37
- }
38
-
39
- logger.info(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
40
- return true;
41
- }
42
-
43
-
44
- /**
45
- * Verify ACME DNS challenge
46
- *
47
- * https://tools.ietf.org/html/rfc8555#section-8.4
48
- *
49
- * @param {object} authz Identifier authorization
50
- * @param {object} challenge Authorization challenge
51
- * @param {string} keyAuthorization Challenge key authorization
52
- * @param {string} [prefix] DNS prefix
53
- * @returns {Promise<boolean>}
54
- */
55
-
56
- async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
57
- logger.info(`Resolving DNS TXT records for ${authz.identifier.value}, prefix: ${prefix}`);
58
- let challengeRecord = `${prefix}${authz.identifier.value}`;
59
-
60
- try {
61
- /* Attempt CNAME record first */
62
- logger.info(`Checking CNAME for record ${challengeRecord}`);
63
- const cnameRecords = await dns.resolveCnameAsync(challengeRecord);
64
-
65
- if (cnameRecords.length) {
66
- logger.info(`CNAME found at ${challengeRecord}, new challenge record: ${cnameRecords[0]}`);
67
- challengeRecord = cnameRecords[0];
68
- }
69
- }
70
- catch (e) {
71
- logger.info(`No CNAME found for record ${challengeRecord}`);
72
- }
73
-
74
- /* Read TXT record */
75
- const result = await dns.resolveTxtAsync(challengeRecord);
76
- const records = [].concat(...result);
77
-
78
- logger.info(`Query successful, found ${records.length} DNS TXT records`);
79
-
80
- if (records.indexOf(keyAuthorization) === -1) {
81
- throw new Error(`Authorization not found in DNS TXT records for ${authz.identifier.value}`);
82
- }
83
-
84
- logger.info(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
85
- return true;
86
- }
87
-
88
-
89
- /**
90
- * Export API
91
- */
92
-
93
- module.exports = {
94
- 'http-01': verifyHttpChallenge,
95
- 'dns-01': verifyDnsChallenge
96
- };
1
+ /**
2
+ * ACME challenge verification
3
+ */
4
+
5
+ const Promise = require('bluebird');
6
+ const dns = Promise.promisifyAll(require('dns'));
7
+ const logger = require('./util.log.js');
8
+
9
+ const debug = logger.info;
10
+ const axios = require('./axios');
11
+
12
+
13
+ /**
14
+ * Verify ACME HTTP challenge
15
+ *
16
+ * https://tools.ietf.org/html/rfc8555#section-8.3
17
+ *
18
+ * @param {object} authz Identifier authorization
19
+ * @param {object} challenge Authorization challenge
20
+ * @param {string} keyAuthorization Challenge key authorization
21
+ * @param {string} [suffix] URL suffix
22
+ * @returns {Promise<boolean>}
23
+ */
24
+
25
+ async function verifyHttpChallenge(authz, challenge, keyAuthorization, suffix = `/.well-known/acme-challenge/${challenge.token}`) {
26
+ const httpPort = axios.defaults.acmeSettings.httpChallengePort || 80;
27
+ const challengeUrl = `http://${authz.identifier.value}:${httpPort}${suffix}`;
28
+
29
+ logger.info(`Sending HTTP query to ${authz.identifier.value}, suffix: ${suffix}, port: ${httpPort}`);
30
+ const resp = await axios.get(challengeUrl);
31
+ const data = (resp.data || '').replace(/\s+$/, '');
32
+
33
+ logger.info(`Query successful, HTTP status code: ${resp.status}`);
34
+
35
+ if (!data || (data !== keyAuthorization)) {
36
+ throw new Error(`Authorization not found in HTTP response from ${authz.identifier.value}`);
37
+ }
38
+
39
+ logger.info(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
40
+ return true;
41
+ }
42
+
43
+
44
+ /**
45
+ * Verify ACME DNS challenge
46
+ *
47
+ * https://tools.ietf.org/html/rfc8555#section-8.4
48
+ *
49
+ * @param {object} authz Identifier authorization
50
+ * @param {object} challenge Authorization challenge
51
+ * @param {string} keyAuthorization Challenge key authorization
52
+ * @param {string} [prefix] DNS prefix
53
+ * @returns {Promise<boolean>}
54
+ */
55
+
56
+ async function verifyDnsChallenge(authz, challenge, keyAuthorization, prefix = '_acme-challenge.') {
57
+ logger.info(`Resolving DNS TXT records for ${authz.identifier.value}, prefix: ${prefix}`);
58
+ let challengeRecord = `${prefix}${authz.identifier.value}`;
59
+
60
+ try {
61
+ /* Attempt CNAME record first */
62
+ logger.info(`Checking CNAME for record ${challengeRecord}`);
63
+ const cnameRecords = await dns.resolveCnameAsync(challengeRecord);
64
+
65
+ if (cnameRecords.length) {
66
+ logger.info(`CNAME found at ${challengeRecord}, new challenge record: ${cnameRecords[0]}`);
67
+ challengeRecord = cnameRecords[0];
68
+ }
69
+ }
70
+ catch (e) {
71
+ logger.info(`No CNAME found for record ${challengeRecord}`);
72
+ }
73
+
74
+ /* Read TXT record */
75
+ const result = await dns.resolveTxtAsync(challengeRecord);
76
+ const records = [].concat(...result);
77
+
78
+ logger.info(`Query successful, found ${records.length} DNS TXT records`);
79
+
80
+ if (records.indexOf(keyAuthorization) === -1) {
81
+ throw new Error(`Authorization not found in DNS TXT records for ${authz.identifier.value}`);
82
+ }
83
+
84
+ logger.info(`Key authorization match for ${challenge.type}/${authz.identifier.value}, ACME challenge verified`);
85
+ return true;
86
+ }
87
+
88
+
89
+ /**
90
+ * Export API
91
+ */
92
+
93
+ module.exports = {
94
+ 'http-01': verifyHttpChallenge,
95
+ 'dns-01': verifyDnsChallenge
96
+ };