@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/http.js CHANGED
@@ -1,241 +1,241 @@
1
- /**
2
- * ACME HTTP client
3
- */
4
-
5
- const crypto = require('crypto');
6
- const logger = require('./util.log.js');
7
-
8
- const debug = logger.info;
9
- const axios = require('./axios');
10
- const util = require('./util');
11
- const forge = require('./crypto/forge');
12
-
13
-
14
- /**
15
- * ACME HTTP client
16
- *
17
- * @class
18
- * @param {string} directoryUrl ACME directory URL
19
- * @param {buffer} accountKey PEM encoded account private key
20
- */
21
-
22
- class HttpClient {
23
- constructor(directoryUrl, accountKey) {
24
- this.directoryUrl = directoryUrl;
25
- this.accountKey = accountKey;
26
-
27
- this.maxBadNonceRetries = 5;
28
- this.directory = null;
29
- this.jwk = null;
30
- }
31
-
32
-
33
- /**
34
- * HTTP request
35
- *
36
- * @param {string} url HTTP URL
37
- * @param {string} method HTTP method
38
- * @param {object} [opts] Request options
39
- * @returns {Promise<object>} HTTP response
40
- */
41
-
42
- async request(url, method, opts = {}) {
43
- opts.url = url;
44
- opts.method = method;
45
- opts.validateStatus = null;
46
-
47
- /* Headers */
48
- if (typeof opts.headers === 'undefined') {
49
- opts.headers = {};
50
- }
51
-
52
- opts.headers['Content-Type'] = 'application/jose+json';
53
-
54
- /* Request */
55
- logger.info(`HTTP request: ${method} ${url}`);
56
- const resp = await axios.request(opts);
57
-
58
- logger.info(`RESP ${resp.status} ${method} ${url}`);
59
- return resp;
60
- }
61
-
62
-
63
- /**
64
- * Ensure provider directory exists
65
- *
66
- * https://tools.ietf.org/html/rfc8555#section-7.1.1
67
- *
68
- * @returns {Promise}
69
- */
70
-
71
- async getDirectory() {
72
- if (!this.directory) {
73
- const resp = await this.request(this.directoryUrl, 'get');
74
- this.directory = resp.data;
75
- }
76
- }
77
-
78
-
79
- /**
80
- * Get JSON Web Key
81
- *
82
- * @returns {Promise<object>} {e, kty, n}
83
- */
84
-
85
- async getJwk() {
86
- if (this.jwk) {
87
- return this.jwk;
88
- }
89
-
90
- const exponent = await forge.getPublicExponent(this.accountKey);
91
- const modulus = await forge.getModulus(this.accountKey);
92
-
93
- this.jwk = {
94
- e: util.b64encode(exponent),
95
- kty: 'RSA',
96
- n: util.b64encode(modulus)
97
- };
98
-
99
- return this.jwk;
100
- }
101
-
102
-
103
- /**
104
- * Get nonce from directory API endpoint
105
- *
106
- * https://tools.ietf.org/html/rfc8555#section-7.2
107
- *
108
- * @returns {Promise<string>} nonce
109
- */
110
-
111
- async getNonce() {
112
- const url = await this.getResourceUrl('newNonce');
113
- const resp = await this.request(url, 'head');
114
-
115
- if (!resp.headers['replay-nonce']) {
116
- throw new Error('Failed to get nonce from ACME provider');
117
- }
118
-
119
- return resp.headers['replay-nonce'];
120
- }
121
-
122
-
123
- /**
124
- * Get URL for a directory resource
125
- *
126
- * @param {string} resource API resource name
127
- * @returns {Promise<string>} URL
128
- */
129
-
130
- async getResourceUrl(resource) {
131
- await this.getDirectory();
132
-
133
- if (!this.directory[resource]) {
134
- throw new Error(`Could not resolve URL for API resource: "${resource}"`);
135
- }
136
-
137
- return this.directory[resource];
138
- }
139
-
140
-
141
- /**
142
- * Get directory meta field
143
- *
144
- * @param {string} field Meta field name
145
- * @returns {Promise<string|null>} Meta field value
146
- */
147
-
148
- async getMetaField(field) {
149
- await this.getDirectory();
150
-
151
- if (('meta' in this.directory) && (field in this.directory.meta)) {
152
- return this.directory.meta[field];
153
- }
154
-
155
- return null;
156
- }
157
-
158
-
159
- /**
160
- * Create signed HTTP request body
161
- *
162
- * @param {string} url Request URL
163
- * @param {object} payload Request payload
164
- * @param {string} [nonce] Request nonce
165
- * @param {string} [kid] Request KID
166
- * @returns {Promise<object>} Signed HTTP request body
167
- */
168
-
169
- async createSignedBody(url, payload = null, nonce = null, kid = null) {
170
- /* JWS header */
171
- const header = {
172
- url,
173
- alg: 'RS256'
174
- };
175
-
176
- if (nonce) {
177
- logger.info(`Using nonce: ${nonce}`);
178
- header.nonce = nonce;
179
- }
180
-
181
- /* KID or JWK */
182
- if (kid) {
183
- header.kid = kid;
184
- }
185
- else {
186
- header.jwk = await this.getJwk();
187
- }
188
-
189
- /* Request payload */
190
- const result = {
191
- payload: payload ? util.b64encode(JSON.stringify(payload)) : '',
192
- protected: util.b64encode(JSON.stringify(header))
193
- };
194
-
195
- /* Signature */
196
- const signer = crypto.createSign('RSA-SHA256').update(`${result.protected}.${result.payload}`, 'utf8');
197
- result.signature = util.b64escape(signer.sign(this.accountKey, 'base64'));
198
-
199
- return result;
200
- }
201
-
202
-
203
- /**
204
- * Signed HTTP request
205
- *
206
- * https://tools.ietf.org/html/rfc8555#section-6.2
207
- *
208
- * @param {string} url Request URL
209
- * @param {object} payload Request payload
210
- * @param {string} [kid] Request KID
211
- * @param {string} [nonce] Request anti-replay nonce
212
- * @param {number} [attempts] Request attempt counter
213
- * @returns {Promise<object>} HTTP response
214
- */
215
-
216
- async signedRequest(url, payload, kid = null, nonce = null, attempts = 0) {
217
- if (!nonce) {
218
- nonce = await this.getNonce();
219
- }
220
-
221
- /* Sign body and send request */
222
- const data = await this.createSignedBody(url, payload, nonce, kid);
223
- const resp = await this.request(url, 'post', { data });
224
-
225
- /* Retry on bad nonce - https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.4 */
226
- if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) {
227
- const newNonce = resp.headers['replay-nonce'] || null;
228
- attempts += 1;
229
-
230
- logger.info(`Caught invalid nonce error, retrying (${attempts}/${this.maxBadNonceRetries}) signed request to: ${url}`);
231
- return this.signedRequest(url, payload, kid, newNonce, attempts);
232
- }
233
-
234
- /* Return response */
235
- return resp;
236
- }
237
- }
238
-
239
-
240
- /* Export client */
241
- module.exports = HttpClient;
1
+ /**
2
+ * ACME HTTP client
3
+ */
4
+
5
+ const crypto = require('crypto');
6
+ const logger = require('./util.log.js');
7
+
8
+ const debug = logger.info;
9
+ const axios = require('./axios');
10
+ const util = require('./util');
11
+ const forge = require('./crypto/forge');
12
+
13
+
14
+ /**
15
+ * ACME HTTP client
16
+ *
17
+ * @class
18
+ * @param {string} directoryUrl ACME directory URL
19
+ * @param {buffer} accountKey PEM encoded account private key
20
+ */
21
+
22
+ class HttpClient {
23
+ constructor(directoryUrl, accountKey) {
24
+ this.directoryUrl = directoryUrl;
25
+ this.accountKey = accountKey;
26
+
27
+ this.maxBadNonceRetries = 5;
28
+ this.directory = null;
29
+ this.jwk = null;
30
+ }
31
+
32
+
33
+ /**
34
+ * HTTP request
35
+ *
36
+ * @param {string} url HTTP URL
37
+ * @param {string} method HTTP method
38
+ * @param {object} [opts] Request options
39
+ * @returns {Promise<object>} HTTP response
40
+ */
41
+
42
+ async request(url, method, opts = {}) {
43
+ opts.url = url;
44
+ opts.method = method;
45
+ opts.validateStatus = null;
46
+
47
+ /* Headers */
48
+ if (typeof opts.headers === 'undefined') {
49
+ opts.headers = {};
50
+ }
51
+
52
+ opts.headers['Content-Type'] = 'application/jose+json';
53
+
54
+ /* Request */
55
+ logger.info(`HTTP request: ${method} ${url}`);
56
+ const resp = await axios.request(opts);
57
+
58
+ logger.info(`RESP ${resp.status} ${method} ${url}`);
59
+ return resp;
60
+ }
61
+
62
+
63
+ /**
64
+ * Ensure provider directory exists
65
+ *
66
+ * https://tools.ietf.org/html/rfc8555#section-7.1.1
67
+ *
68
+ * @returns {Promise}
69
+ */
70
+
71
+ async getDirectory() {
72
+ if (!this.directory) {
73
+ const resp = await this.request(this.directoryUrl, 'get');
74
+ this.directory = resp.data;
75
+ }
76
+ }
77
+
78
+
79
+ /**
80
+ * Get JSON Web Key
81
+ *
82
+ * @returns {Promise<object>} {e, kty, n}
83
+ */
84
+
85
+ async getJwk() {
86
+ if (this.jwk) {
87
+ return this.jwk;
88
+ }
89
+
90
+ const exponent = await forge.getPublicExponent(this.accountKey);
91
+ const modulus = await forge.getModulus(this.accountKey);
92
+
93
+ this.jwk = {
94
+ e: util.b64encode(exponent),
95
+ kty: 'RSA',
96
+ n: util.b64encode(modulus)
97
+ };
98
+
99
+ return this.jwk;
100
+ }
101
+
102
+
103
+ /**
104
+ * Get nonce from directory API endpoint
105
+ *
106
+ * https://tools.ietf.org/html/rfc8555#section-7.2
107
+ *
108
+ * @returns {Promise<string>} nonce
109
+ */
110
+
111
+ async getNonce() {
112
+ const url = await this.getResourceUrl('newNonce');
113
+ const resp = await this.request(url, 'head');
114
+
115
+ if (!resp.headers['replay-nonce']) {
116
+ throw new Error('Failed to get nonce from ACME provider');
117
+ }
118
+
119
+ return resp.headers['replay-nonce'];
120
+ }
121
+
122
+
123
+ /**
124
+ * Get URL for a directory resource
125
+ *
126
+ * @param {string} resource API resource name
127
+ * @returns {Promise<string>} URL
128
+ */
129
+
130
+ async getResourceUrl(resource) {
131
+ await this.getDirectory();
132
+
133
+ if (!this.directory[resource]) {
134
+ throw new Error(`Could not resolve URL for API resource: "${resource}"`);
135
+ }
136
+
137
+ return this.directory[resource];
138
+ }
139
+
140
+
141
+ /**
142
+ * Get directory meta field
143
+ *
144
+ * @param {string} field Meta field name
145
+ * @returns {Promise<string|null>} Meta field value
146
+ */
147
+
148
+ async getMetaField(field) {
149
+ await this.getDirectory();
150
+
151
+ if (('meta' in this.directory) && (field in this.directory.meta)) {
152
+ return this.directory.meta[field];
153
+ }
154
+
155
+ return null;
156
+ }
157
+
158
+
159
+ /**
160
+ * Create signed HTTP request body
161
+ *
162
+ * @param {string} url Request URL
163
+ * @param {object} payload Request payload
164
+ * @param {string} [nonce] Request nonce
165
+ * @param {string} [kid] Request KID
166
+ * @returns {Promise<object>} Signed HTTP request body
167
+ */
168
+
169
+ async createSignedBody(url, payload = null, nonce = null, kid = null) {
170
+ /* JWS header */
171
+ const header = {
172
+ url,
173
+ alg: 'RS256'
174
+ };
175
+
176
+ if (nonce) {
177
+ logger.info(`Using nonce: ${nonce}`);
178
+ header.nonce = nonce;
179
+ }
180
+
181
+ /* KID or JWK */
182
+ if (kid) {
183
+ header.kid = kid;
184
+ }
185
+ else {
186
+ header.jwk = await this.getJwk();
187
+ }
188
+
189
+ /* Request payload */
190
+ const result = {
191
+ payload: payload ? util.b64encode(JSON.stringify(payload)) : '',
192
+ protected: util.b64encode(JSON.stringify(header))
193
+ };
194
+
195
+ /* Signature */
196
+ const signer = crypto.createSign('RSA-SHA256').update(`${result.protected}.${result.payload}`, 'utf8');
197
+ result.signature = util.b64escape(signer.sign(this.accountKey, 'base64'));
198
+
199
+ return result;
200
+ }
201
+
202
+
203
+ /**
204
+ * Signed HTTP request
205
+ *
206
+ * https://tools.ietf.org/html/rfc8555#section-6.2
207
+ *
208
+ * @param {string} url Request URL
209
+ * @param {object} payload Request payload
210
+ * @param {string} [kid] Request KID
211
+ * @param {string} [nonce] Request anti-replay nonce
212
+ * @param {number} [attempts] Request attempt counter
213
+ * @returns {Promise<object>} HTTP response
214
+ */
215
+
216
+ async signedRequest(url, payload, kid = null, nonce = null, attempts = 0) {
217
+ if (!nonce) {
218
+ nonce = await this.getNonce();
219
+ }
220
+
221
+ /* Sign body and send request */
222
+ const data = await this.createSignedBody(url, payload, nonce, kid);
223
+ const resp = await this.request(url, 'post', { data });
224
+
225
+ /* Retry on bad nonce - https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.4 */
226
+ if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) {
227
+ const newNonce = resp.headers['replay-nonce'] || null;
228
+ attempts += 1;
229
+
230
+ logger.info(`Caught invalid nonce error, retrying (${attempts}/${this.maxBadNonceRetries}) signed request to: ${url}`);
231
+ return this.signedRequest(url, payload, kid, newNonce, attempts);
232
+ }
233
+
234
+ /* Return response */
235
+ return resp;
236
+ }
237
+ }
238
+
239
+
240
+ /* Export client */
241
+ module.exports = HttpClient;
package/src/index.js CHANGED
@@ -1,31 +1,31 @@
1
- /**
2
- * acme-client
3
- */
4
-
5
- exports.Client = require('./client');
6
-
7
-
8
- /**
9
- * Directory URLs
10
- */
11
-
12
- exports.directory = {
13
- letsencrypt: {
14
- staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
15
- production: 'https://acme-v02.api.letsencrypt.org/directory'
16
- }
17
- };
18
-
19
-
20
- /**
21
- * Crypto
22
- */
23
-
24
- exports.forge = require('./crypto/forge');
25
-
26
-
27
- /**
28
- * Axios
29
- */
30
-
31
- exports.axios = require('./axios');
1
+ /**
2
+ * acme-client
3
+ */
4
+
5
+ exports.Client = require('./client');
6
+
7
+
8
+ /**
9
+ * Directory URLs
10
+ */
11
+
12
+ exports.directory = {
13
+ letsencrypt: {
14
+ staging: 'https://acme-staging-v02.api.letsencrypt.org/directory',
15
+ production: 'https://acme-v02.api.letsencrypt.org/directory'
16
+ }
17
+ };
18
+
19
+
20
+ /**
21
+ * Crypto
22
+ */
23
+
24
+ exports.forge = require('./crypto/forge');
25
+
26
+
27
+ /**
28
+ * Axios
29
+ */
30
+
31
+ exports.axios = require('./axios');