@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/client.js CHANGED
@@ -1,716 +1,716 @@
1
- /**
2
- * ACME client
3
- *
4
- * @namespace Client
5
- */
6
-
7
- const crypto = require('crypto');
8
- const logger = require('./util.log.js');
9
-
10
- const debug = logger.info;
11
- const Promise = require('bluebird');
12
- const HttpClient = require('./http');
13
- const AcmeApi = require('./api');
14
- const verify = require('./verify');
15
- const util = require('./util');
16
- const auto = require('./auto');
17
- const forge = require('./crypto/forge');
18
-
19
-
20
- /**
21
- * Default options
22
- */
23
-
24
- const defaultOpts = {
25
- directoryUrl: undefined,
26
- accountKey: undefined,
27
- accountUrl: null,
28
- backoffAttempts: 5,
29
- backoffMin: 5000,
30
- backoffMax: 30000
31
- };
32
-
33
-
34
- /**
35
- * AcmeClient
36
- *
37
- * @class
38
- * @param {object} opts
39
- * @param {string} opts.directoryUrl ACME directory URL
40
- * @param {buffer|string} opts.accountKey PEM encoded account private key
41
- * @param {string} [opts.accountUrl] Account URL, default: `null`
42
- * @param {number} [opts.backoffAttempts] Maximum number of backoff attempts, default: `5`
43
- * @param {number} [opts.backoffMin] Minimum backoff attempt delay in milliseconds, default: `5000`
44
- * @param {number} [opts.backoffMax] Maximum backoff attempt delay in milliseconds, default: `30000`
45
- *
46
- * @example Create ACME client instance
47
- * ```js
48
- * const client = new acme.Client({
49
- * directoryUrl: acme.directory.letsencrypt.staging,
50
- * accountKey: 'Private key goes here'
51
- * });
52
- * ```
53
- *
54
- * @example Create ACME client instance
55
- * ```js
56
- * const client = new acme.Client({
57
- * directoryUrl: acme.directory.letsencrypt.staging,
58
- * accountKey: 'Private key goes here',
59
- * accountUrl: 'Optional account URL goes here',
60
- * backoffAttempts: 5,
61
- * backoffMin: 5000,
62
- * backoffMax: 30000
63
- * });
64
- * ```
65
- */
66
-
67
- class AcmeClient {
68
- constructor(opts) {
69
- if (!Buffer.isBuffer(opts.accountKey)) {
70
- opts.accountKey = Buffer.from(opts.accountKey);
71
- }
72
-
73
- this.opts = Object.assign({}, defaultOpts, opts);
74
-
75
- this.backoffOpts = {
76
- attempts: this.opts.backoffAttempts,
77
- min: this.opts.backoffMin,
78
- max: this.opts.backoffMax
79
- };
80
-
81
- this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey);
82
- this.api = new AcmeApi(this.http, this.opts.accountUrl);
83
- }
84
-
85
-
86
- /**
87
- * Get Terms of Service URL if available
88
- *
89
- * @returns {Promise<string|null>} ToS URL
90
- *
91
- * @example Get Terms of Service URL
92
- * ```js
93
- * const termsOfService = client.getTermsOfServiceUrl();
94
- *
95
- * if (!termsOfService) {
96
- * // CA did not provide Terms of Service
97
- * }
98
- * ```
99
- */
100
-
101
- getTermsOfServiceUrl() {
102
- return this.api.getTermsOfServiceUrl();
103
- }
104
-
105
-
106
- /**
107
- * Get current account URL
108
- *
109
- * @returns {string} Account URL
110
- * @throws {Error} No account URL found
111
- *
112
- * @example Get current account URL
113
- * ```js
114
- * try {
115
- * const accountUrl = client.getAccountUrl();
116
- * }
117
- * catch (e) {
118
- * // No account URL exists, need to create account first
119
- * }
120
- * ```
121
- */
122
-
123
- getAccountUrl() {
124
- return this.api.getAccountUrl();
125
- }
126
-
127
-
128
- /**
129
- * Create a new account
130
- *
131
- * https://tools.ietf.org/html/rfc8555#section-7.3
132
- *
133
- * @param {object} [data] Request data
134
- * @returns {Promise<object>} Account
135
- *
136
- * @example Create a new account
137
- * ```js
138
- * const account = await client.createAccount({
139
- * termsOfServiceAgreed: true
140
- * });
141
- * ```
142
- *
143
- * @example Create a new account with contact info
144
- * ```js
145
- * const account = await client.createAccount({
146
- * termsOfServiceAgreed: true,
147
- * contact: ['mailto:test@example.com']
148
- * });
149
- * ```
150
- */
151
-
152
- async createAccount(data = {}) {
153
- try {
154
- this.getAccountUrl();
155
-
156
- /* Account URL exists */
157
- logger.info('Account URL exists, returning updateAccount()');
158
- return this.updateAccount(data);
159
- }
160
- catch (e) {
161
- const resp = await this.api.createAccount(data);
162
- // TODO 先注释,可加快速度
163
- /* HTTP 200: Account exists */
164
- // if (resp.status === 200) {
165
- // logger.info('Account already exists (HTTP 200), returning updateAccount()');
166
- // return this.updateAccount(data);
167
- // }
168
-
169
- return resp.data;
170
- }
171
- }
172
-
173
-
174
- /**
175
- * Update existing account
176
- *
177
- * https://tools.ietf.org/html/rfc8555#section-7.3.2
178
- *
179
- * @param {object} [data] Request data
180
- * @returns {Promise<object>} Account
181
- *
182
- * @example Update existing account
183
- * ```js
184
- * const account = await client.updateAccount({
185
- * contact: ['mailto:foo@example.com']
186
- * });
187
- * ```
188
- */
189
-
190
- async updateAccount(data = {}) {
191
- try {
192
- this.api.getAccountUrl();
193
- }
194
- catch (e) {
195
- logger.info('No account URL found, returning createAccount()');
196
- return this.createAccount(data);
197
- }
198
-
199
- /* Remove data only applicable to createAccount() */
200
- if ('onlyReturnExisting' in data) {
201
- delete data.onlyReturnExisting;
202
- }
203
-
204
- /* POST-as-GET */
205
- if (Object.keys(data).length === 0) {
206
- data = null;
207
- }
208
-
209
- const resp = await this.api.updateAccount(data);
210
- return resp.data;
211
- }
212
-
213
-
214
- /**
215
- * Update account private key
216
- *
217
- * https://tools.ietf.org/html/rfc8555#section-7.3.5
218
- *
219
- * @param {buffer|string} newAccountKey New PEM encoded private key
220
- * @param {object} [data] Additional request data
221
- * @returns {Promise<object>} Account
222
- *
223
- * @example Update account private key
224
- * ```js
225
- * const newAccountKey = 'New private key goes here';
226
- * const result = await client.updateAccountKey(newAccountKey);
227
- * ```
228
- */
229
-
230
- async updateAccountKey(newAccountKey, data = {}) {
231
- if (!Buffer.isBuffer(newAccountKey)) {
232
- newAccountKey = Buffer.from(newAccountKey);
233
- }
234
-
235
- const accountUrl = this.api.getAccountUrl();
236
-
237
- /* Create new HTTP and API clients using new key */
238
- const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey);
239
- const newApiClient = new AcmeApi(newHttpClient, accountUrl);
240
-
241
- /* Get new JWK */
242
- data.account = accountUrl;
243
- data.oldKey = await this.http.getJwk();
244
-
245
- /* TODO: Backward-compatibility with draft-ietf-acme-12, remove this in a later release */
246
- data.newKey = await newHttpClient.getJwk();
247
-
248
- /* Get signed request body from new client */
249
- const url = await newHttpClient.getResourceUrl('keyChange');
250
- const body = await newHttpClient.createSignedBody(url, data);
251
-
252
- /* Change key using old client */
253
- const resp = await this.api.updateAccountKey(body);
254
-
255
- /* Replace existing HTTP and API client */
256
- this.http = newHttpClient;
257
- this.api = newApiClient;
258
-
259
- return resp.data;
260
- }
261
-
262
-
263
- /**
264
- * Create a new order
265
- *
266
- * https://tools.ietf.org/html/rfc8555#section-7.4
267
- *
268
- * @param {object} data Request data
269
- * @returns {Promise<object>} Order
270
- *
271
- * @example Create a new order
272
- * ```js
273
- * const order = await client.createOrder({
274
- * identifiers: [
275
- * { type: 'dns', value: 'example.com' },
276
- * { type: 'dns', value: 'test.example.com' }
277
- * ]
278
- * });
279
- * ```
280
- */
281
-
282
- async createOrder(data) {
283
- const resp = await this.api.createOrder(data);
284
-
285
- if (!resp.headers.location) {
286
- throw new Error('Creating a new order did not return an order link');
287
- }
288
-
289
- /* Add URL to response */
290
- resp.data.url = resp.headers.location;
291
- return resp.data;
292
- }
293
-
294
-
295
- /**
296
- * Refresh order object from CA
297
- *
298
- * https://tools.ietf.org/html/rfc8555#section-7.4
299
- *
300
- * @param {object} order Order object
301
- * @returns {Promise<object>} Order
302
- *
303
- * @example
304
- * ```js
305
- * const order = { ... }; // Previously created order object
306
- * const result = await client.getOrder(order);
307
- * ```
308
- */
309
-
310
- async getOrder(order) {
311
- if (!order.url) {
312
- throw new Error('Unable to get order, URL not found');
313
- }
314
-
315
- const resp = await this.api.getOrder(order.url);
316
-
317
- /* Add URL to response */
318
- resp.data.url = order.url;
319
- return resp.data;
320
- }
321
-
322
- /**
323
- * Finalize order
324
- *
325
- * https://tools.ietf.org/html/rfc8555#section-7.4
326
- *
327
- * @param {object} order Order object
328
- * @param {buffer|string} csr PEM encoded Certificate Signing Request
329
- * @returns {Promise<object>} Order
330
- *
331
- * @example Finalize order
332
- * ```js
333
- * const order = { ... }; // Previously created order object
334
- * const csr = { ... }; // Previously created Certificate Signing Request
335
- * const result = await client.finalizeOrder(order, csr);
336
- * ```
337
- */
338
-
339
- async finalizeOrder(order, csr) {
340
- if (!order.finalize) {
341
- throw new Error('Unable to finalize order, URL not found');
342
- }
343
-
344
- if (!Buffer.isBuffer(csr)) {
345
- csr = Buffer.from(csr);
346
- }
347
-
348
- const body = forge.getPemBody(csr);
349
- const data = { csr: util.b64escape(body) };
350
-
351
- const resp = await this.api.finalizeOrder(order.finalize, data);
352
-
353
- /* Add URL to response */
354
- resp.data.url = order.url;
355
- return resp.data;
356
- }
357
-
358
-
359
- /**
360
- * Get identifier authorizations from order
361
- *
362
- * https://tools.ietf.org/html/rfc8555#section-7.5
363
- *
364
- * @param {object} order Order
365
- * @returns {Promise<object[]>} Authorizations
366
- *
367
- * @example Get identifier authorizations
368
- * ```js
369
- * const order = { ... }; // Previously created order object
370
- * const authorizations = await client.getAuthorizations(order);
371
- *
372
- * authorizations.forEach((authz) => {
373
- * const { challenges } = authz;
374
- * });
375
- * ```
376
- */
377
-
378
- async getAuthorizations(order) {
379
- return Promise.map((order.authorizations || []), async (url) => {
380
- const resp = await this.api.getAuthorization(url);
381
-
382
- /* Add URL to response */
383
- resp.data.url = url;
384
- return resp.data;
385
- });
386
- }
387
-
388
-
389
- /**
390
- * Deactivate identifier authorization
391
- *
392
- * https://tools.ietf.org/html/rfc8555#section-7.5.2
393
- *
394
- * @param {object} authz Identifier authorization
395
- * @returns {Promise<object>} Authorization
396
- *
397
- * @example Deactivate identifier authorization
398
- * ```js
399
- * const authz = { ... }; // Identifier authorization resolved from previously created order
400
- * const result = await client.deactivateAuthorization(authz);
401
- * ```
402
- */
403
-
404
- async deactivateAuthorization(authz) {
405
- if (!authz.url) {
406
- throw new Error('Unable to deactivate identifier authorization, URL not found');
407
- }
408
-
409
- const data = {
410
- status: 'deactivated'
411
- };
412
-
413
- const resp = await this.api.updateAuthorization(authz.url, data);
414
-
415
- /* Add URL to response */
416
- resp.data.url = authz.url;
417
- return resp.data;
418
- }
419
-
420
-
421
- /**
422
- * Get key authorization for ACME challenge
423
- *
424
- * https://tools.ietf.org/html/rfc8555#section-8.1
425
- *
426
- * @param {object} challenge Challenge object returned by API
427
- * @returns {Promise<string>} Key authorization
428
- *
429
- * @example Get challenge key authorization
430
- * ```js
431
- * const challenge = { ... }; // Challenge from previously resolved identifier authorization
432
- * const key = await client.getChallengeKeyAuthorization(challenge);
433
- *
434
- * // Write key somewhere to satisfy challenge
435
- * ```
436
- */
437
-
438
- async getChallengeKeyAuthorization(challenge) {
439
- const jwk = await this.http.getJwk();
440
- const keysum = crypto.createHash('sha256').update(JSON.stringify(jwk));
441
- const thumbprint = util.b64escape(keysum.digest('base64'));
442
- const result = `${challenge.token}.${thumbprint}`;
443
-
444
- /**
445
- * https://tools.ietf.org/html/rfc8555#section-8.3
446
- */
447
-
448
- if (challenge.type === 'http-01') {
449
- return result;
450
- }
451
-
452
- /**
453
- * https://tools.ietf.org/html/rfc8555#section-8.4
454
- * https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
455
- */
456
-
457
- if ((challenge.type === 'dns-01') || (challenge.type === 'tls-alpn-01')) {
458
- const shasum = crypto.createHash('sha256').update(result);
459
- return util.b64escape(shasum.digest('base64'));
460
- }
461
-
462
- throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
463
- }
464
-
465
-
466
- /**
467
- * Verify that ACME challenge is satisfied
468
- *
469
- * @param {object} authz Identifier authorization
470
- * @param {object} challenge Authorization challenge
471
- * @returns {Promise}
472
- *
473
- * @example Verify satisfied ACME challenge
474
- * ```js
475
- * const authz = { ... }; // Identifier authorization
476
- * const challenge = { ... }; // Satisfied challenge
477
- * await client.verifyChallenge(authz, challenge);
478
- * ```
479
- */
480
-
481
- async verifyChallenge(authz, challenge) {
482
- if (!authz.url || !challenge.url) {
483
- throw new Error('Unable to verify ACME challenge, URL not found');
484
- }
485
-
486
- if (typeof verify[challenge.type] === 'undefined') {
487
- throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`);
488
- }
489
-
490
- const keyAuthorization = await this.getChallengeKeyAuthorization(challenge);
491
-
492
- const verifyFn = async () => {
493
- await verify[challenge.type](authz, challenge, keyAuthorization);
494
- };
495
-
496
- logger.info('Waiting for ACME challenge verification', this.backoffOpts);
497
- return util.retry(verifyFn, this.backoffOpts);
498
- }
499
-
500
-
501
- /**
502
- * Notify CA that challenge has been completed
503
- *
504
- * https://tools.ietf.org/html/rfc8555#section-7.5.1
505
- *
506
- * @param {object} challenge Challenge object returned by API
507
- * @returns {Promise<object>} Challenge
508
- *
509
- * @example Notify CA that challenge has been completed
510
- * ```js
511
- * const challenge = { ... }; // Satisfied challenge
512
- * const result = await client.completeChallenge(challenge);
513
- * ```
514
- */
515
-
516
- async completeChallenge(challenge) {
517
- const resp = await this.api.completeChallenge(challenge.url, {});
518
- return resp.data;
519
- }
520
-
521
-
522
- /**
523
- * Wait for ACME provider to verify status on a order, authorization or challenge
524
- *
525
- * https://tools.ietf.org/html/rfc8555#section-7.5.1
526
- *
527
- * @param {object} item An order, authorization or challenge object
528
- * @returns {Promise<object>} Valid order, authorization or challenge
529
- *
530
- * @example Wait for valid challenge status
531
- * ```js
532
- * const challenge = { ... };
533
- * await client.waitForValidStatus(challenge);
534
- * ```
535
- *
536
- * @example Wait for valid authoriation status
537
- * ```js
538
- * const authz = { ... };
539
- * await client.waitForValidStatus(authz);
540
- * ```
541
- *
542
- * @example Wait for valid order status
543
- * ```js
544
- * const order = { ... };
545
- * await client.waitForValidStatus(order);
546
- * ```
547
- */
548
-
549
- async waitForValidStatus(item) {
550
- if (!item.url) {
551
- throw new Error('Unable to verify status of item, URL not found');
552
- }
553
-
554
- const verifyFn = async (abort) => {
555
- const resp = await this.api.apiRequest(item.url, null, [200]);
556
-
557
- /* Verify status */
558
- logger.info(`Item has status: ${resp.data.status}`);
559
-
560
- if (resp.data.status === 'invalid') {
561
- abort();
562
- throw new Error(util.formatResponseError(resp));
563
- }
564
- else if (resp.data.status === 'pending') {
565
- throw new Error('Operation is pending');
566
- }
567
- else if (resp.data.status === 'valid') {
568
- return resp.data;
569
- }
570
-
571
- throw new Error(`Unexpected item status: ${resp.data.status}`);
572
- };
573
-
574
- logger.info(`Waiting for valid status from: ${item.url}`, this.backoffOpts);
575
- return util.retry(verifyFn, this.backoffOpts);
576
- }
577
-
578
-
579
- /**
580
- * Get certificate from ACME order
581
- *
582
- * https://tools.ietf.org/html/rfc8555#section-7.4.2
583
- *
584
- * @param {object} order Order object
585
- * @param {string} [preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null`
586
- * @returns {Promise<string>} Certificate
587
- *
588
- * @example Get certificate
589
- * ```js
590
- * const order = { ... }; // Previously created order
591
- * const certificate = await client.getCertificate(order);
592
- * ```
593
- *
594
- * @example Get certificate with preferred chain
595
- * ```js
596
- * const order = { ... }; // Previously created order
597
- * const certificate = await client.getCertificate(order, 'DST Root CA X3');
598
- * ```
599
- */
600
-
601
- async getCertificate(order, preferredChain = null) {
602
- if (order.status !== 'valid') {
603
- order = await this.waitForValidStatus(order);
604
- }
605
-
606
- if (!order.certificate) {
607
- throw new Error('Unable to download certificate, URL not found');
608
- }
609
-
610
- const resp = await this.api.apiRequest(order.certificate, null, [200]);
611
-
612
- /* Handle alternate certificate chains */
613
- if (preferredChain && resp.headers.link) {
614
- const alternateLinks = util.parseLinkHeader(resp.headers.link);
615
- const alternates = await Promise.map(alternateLinks, async (link) => this.api.apiRequest(link, null, [200]));
616
- const certificates = [resp].concat(alternates).map((c) => c.data);
617
-
618
- return util.findCertificateChainForIssuer(certificates, preferredChain);
619
- }
620
-
621
- /* Return default certificate chain */
622
- return resp.data;
623
- }
624
-
625
-
626
- /**
627
- * Revoke certificate
628
- *
629
- * https://tools.ietf.org/html/rfc8555#section-7.6
630
- *
631
- * @param {buffer|string} cert PEM encoded certificate
632
- * @param {object} [data] Additional request data
633
- * @returns {Promise}
634
- *
635
- * @example Revoke certificate
636
- * ```js
637
- * const certificate = { ... }; // Previously created certificate
638
- * const result = await client.revokeCertificate(certificate);
639
- * ```
640
- *
641
- * @example Revoke certificate with reason
642
- * ```js
643
- * const certificate = { ... }; // Previously created certificate
644
- * const result = await client.revokeCertificate(certificate, {
645
- * reason: 4
646
- * });
647
- * ```
648
- */
649
-
650
- async revokeCertificate(cert, data = {}) {
651
- const body = forge.getPemBody(cert);
652
- data.certificate = util.b64escape(body);
653
-
654
- const resp = await this.api.revokeCert(data);
655
- return resp.data;
656
- }
657
-
658
-
659
- /**
660
- * Auto mode
661
- *
662
- * @param {object} opts
663
- * @param {buffer|string} opts.csr Certificate Signing Request
664
- * @param {function} opts.challengeCreateFn Function returning Promise triggered before completing ACME challenge
665
- * @param {function} opts.challengeRemoveFn Function returning Promise triggered after completing ACME challenge
666
- * @param {string} [opts.email] Account email address
667
- * @param {boolean} [opts.termsOfServiceAgreed] Agree to Terms of Service, default: `false`
668
- * @param {boolean} [opts.skipChallengeVerification] Skip internal challenge verification before notifying ACME provider, default: `false`
669
- * @param {string[]} [opts.challengePriority] Array defining challenge type priority, default: `['http-01', 'dns-01']`
670
- * @param {string} [opts.preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null`
671
- * @returns {Promise<string>} Certificate
672
- *
673
- * @example Order a certificate using auto mode
674
- * ```js
675
- * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
676
- * commonName: 'test.example.com'
677
- * });
678
- *
679
- * const certificate = await client.auto({
680
- * csr: certificateRequest,
681
- * email: 'test@example.com',
682
- * termsOfServiceAgreed: true,
683
- * challengeCreateFn: async (authz, challenge, keyAuthorization) => {
684
- * // Satisfy challenge here
685
- * },
686
- * challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
687
- * // Clean up challenge here
688
- * }
689
- * });
690
- * ```
691
- *
692
- * @example Order a certificate using auto mode with preferred chain
693
- * ```js
694
- * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
695
- * commonName: 'test.example.com'
696
- * });
697
- *
698
- * const certificate = await client.auto({
699
- * csr: certificateRequest,
700
- * email: 'test@example.com',
701
- * termsOfServiceAgreed: true,
702
- * preferredChain: 'DST Root CA X3',
703
- * challengeCreateFn: async () => {},
704
- * challengeRemoveFn: async () => {}
705
- * });
706
- * ```
707
- */
708
-
709
- auto(opts) {
710
- return auto(this, opts);
711
- }
712
- }
713
-
714
-
715
- /* Export client */
716
- module.exports = AcmeClient;
1
+ /**
2
+ * ACME client
3
+ *
4
+ * @namespace Client
5
+ */
6
+
7
+ const crypto = require('crypto');
8
+ const logger = require('./util.log.js');
9
+
10
+ const debug = logger.info;
11
+ const Promise = require('bluebird');
12
+ const HttpClient = require('./http');
13
+ const AcmeApi = require('./api');
14
+ const verify = require('./verify');
15
+ const util = require('./util');
16
+ const auto = require('./auto');
17
+ const forge = require('./crypto/forge');
18
+
19
+
20
+ /**
21
+ * Default options
22
+ */
23
+
24
+ const defaultOpts = {
25
+ directoryUrl: undefined,
26
+ accountKey: undefined,
27
+ accountUrl: null,
28
+ backoffAttempts: 5,
29
+ backoffMin: 5000,
30
+ backoffMax: 30000
31
+ };
32
+
33
+
34
+ /**
35
+ * AcmeClient
36
+ *
37
+ * @class
38
+ * @param {object} opts
39
+ * @param {string} opts.directoryUrl ACME directory URL
40
+ * @param {buffer|string} opts.accountKey PEM encoded account private key
41
+ * @param {string} [opts.accountUrl] Account URL, default: `null`
42
+ * @param {number} [opts.backoffAttempts] Maximum number of backoff attempts, default: `5`
43
+ * @param {number} [opts.backoffMin] Minimum backoff attempt delay in milliseconds, default: `5000`
44
+ * @param {number} [opts.backoffMax] Maximum backoff attempt delay in milliseconds, default: `30000`
45
+ *
46
+ * @example Create ACME client instance
47
+ * ```js
48
+ * const client = new acme.Client({
49
+ * directoryUrl: acme.directory.letsencrypt.staging,
50
+ * accountKey: 'Private key goes here'
51
+ * });
52
+ * ```
53
+ *
54
+ * @example Create ACME client instance
55
+ * ```js
56
+ * const client = new acme.Client({
57
+ * directoryUrl: acme.directory.letsencrypt.staging,
58
+ * accountKey: 'Private key goes here',
59
+ * accountUrl: 'Optional account URL goes here',
60
+ * backoffAttempts: 5,
61
+ * backoffMin: 5000,
62
+ * backoffMax: 30000
63
+ * });
64
+ * ```
65
+ */
66
+
67
+ class AcmeClient {
68
+ constructor(opts) {
69
+ if (!Buffer.isBuffer(opts.accountKey)) {
70
+ opts.accountKey = Buffer.from(opts.accountKey);
71
+ }
72
+
73
+ this.opts = Object.assign({}, defaultOpts, opts);
74
+
75
+ this.backoffOpts = {
76
+ attempts: this.opts.backoffAttempts,
77
+ min: this.opts.backoffMin,
78
+ max: this.opts.backoffMax
79
+ };
80
+
81
+ this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey);
82
+ this.api = new AcmeApi(this.http, this.opts.accountUrl);
83
+ }
84
+
85
+
86
+ /**
87
+ * Get Terms of Service URL if available
88
+ *
89
+ * @returns {Promise<string|null>} ToS URL
90
+ *
91
+ * @example Get Terms of Service URL
92
+ * ```js
93
+ * const termsOfService = client.getTermsOfServiceUrl();
94
+ *
95
+ * if (!termsOfService) {
96
+ * // CA did not provide Terms of Service
97
+ * }
98
+ * ```
99
+ */
100
+
101
+ getTermsOfServiceUrl() {
102
+ return this.api.getTermsOfServiceUrl();
103
+ }
104
+
105
+
106
+ /**
107
+ * Get current account URL
108
+ *
109
+ * @returns {string} Account URL
110
+ * @throws {Error} No account URL found
111
+ *
112
+ * @example Get current account URL
113
+ * ```js
114
+ * try {
115
+ * const accountUrl = client.getAccountUrl();
116
+ * }
117
+ * catch (e) {
118
+ * // No account URL exists, need to create account first
119
+ * }
120
+ * ```
121
+ */
122
+
123
+ getAccountUrl() {
124
+ return this.api.getAccountUrl();
125
+ }
126
+
127
+
128
+ /**
129
+ * Create a new account
130
+ *
131
+ * https://tools.ietf.org/html/rfc8555#section-7.3
132
+ *
133
+ * @param {object} [data] Request data
134
+ * @returns {Promise<object>} Account
135
+ *
136
+ * @example Create a new account
137
+ * ```js
138
+ * const account = await client.createAccount({
139
+ * termsOfServiceAgreed: true
140
+ * });
141
+ * ```
142
+ *
143
+ * @example Create a new account with contact info
144
+ * ```js
145
+ * const account = await client.createAccount({
146
+ * termsOfServiceAgreed: true,
147
+ * contact: ['mailto:test@example.com']
148
+ * });
149
+ * ```
150
+ */
151
+
152
+ async createAccount(data = {}) {
153
+ try {
154
+ this.getAccountUrl();
155
+
156
+ /* Account URL exists */
157
+ logger.info('Account URL exists, returning updateAccount()');
158
+ return this.updateAccount(data);
159
+ }
160
+ catch (e) {
161
+ const resp = await this.api.createAccount(data);
162
+ // TODO 先注释,可加快速度
163
+ /* HTTP 200: Account exists */
164
+ // if (resp.status === 200) {
165
+ // logger.info('Account already exists (HTTP 200), returning updateAccount()');
166
+ // return this.updateAccount(data);
167
+ // }
168
+
169
+ return resp.data;
170
+ }
171
+ }
172
+
173
+
174
+ /**
175
+ * Update existing account
176
+ *
177
+ * https://tools.ietf.org/html/rfc8555#section-7.3.2
178
+ *
179
+ * @param {object} [data] Request data
180
+ * @returns {Promise<object>} Account
181
+ *
182
+ * @example Update existing account
183
+ * ```js
184
+ * const account = await client.updateAccount({
185
+ * contact: ['mailto:foo@example.com']
186
+ * });
187
+ * ```
188
+ */
189
+
190
+ async updateAccount(data = {}) {
191
+ try {
192
+ this.api.getAccountUrl();
193
+ }
194
+ catch (e) {
195
+ logger.info('No account URL found, returning createAccount()');
196
+ return this.createAccount(data);
197
+ }
198
+
199
+ /* Remove data only applicable to createAccount() */
200
+ if ('onlyReturnExisting' in data) {
201
+ delete data.onlyReturnExisting;
202
+ }
203
+
204
+ /* POST-as-GET */
205
+ if (Object.keys(data).length === 0) {
206
+ data = null;
207
+ }
208
+
209
+ const resp = await this.api.updateAccount(data);
210
+ return resp.data;
211
+ }
212
+
213
+
214
+ /**
215
+ * Update account private key
216
+ *
217
+ * https://tools.ietf.org/html/rfc8555#section-7.3.5
218
+ *
219
+ * @param {buffer|string} newAccountKey New PEM encoded private key
220
+ * @param {object} [data] Additional request data
221
+ * @returns {Promise<object>} Account
222
+ *
223
+ * @example Update account private key
224
+ * ```js
225
+ * const newAccountKey = 'New private key goes here';
226
+ * const result = await client.updateAccountKey(newAccountKey);
227
+ * ```
228
+ */
229
+
230
+ async updateAccountKey(newAccountKey, data = {}) {
231
+ if (!Buffer.isBuffer(newAccountKey)) {
232
+ newAccountKey = Buffer.from(newAccountKey);
233
+ }
234
+
235
+ const accountUrl = this.api.getAccountUrl();
236
+
237
+ /* Create new HTTP and API clients using new key */
238
+ const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey);
239
+ const newApiClient = new AcmeApi(newHttpClient, accountUrl);
240
+
241
+ /* Get new JWK */
242
+ data.account = accountUrl;
243
+ data.oldKey = await this.http.getJwk();
244
+
245
+ /* TODO: Backward-compatibility with draft-ietf-acme-12, remove this in a later release */
246
+ data.newKey = await newHttpClient.getJwk();
247
+
248
+ /* Get signed request body from new client */
249
+ const url = await newHttpClient.getResourceUrl('keyChange');
250
+ const body = await newHttpClient.createSignedBody(url, data);
251
+
252
+ /* Change key using old client */
253
+ const resp = await this.api.updateAccountKey(body);
254
+
255
+ /* Replace existing HTTP and API client */
256
+ this.http = newHttpClient;
257
+ this.api = newApiClient;
258
+
259
+ return resp.data;
260
+ }
261
+
262
+
263
+ /**
264
+ * Create a new order
265
+ *
266
+ * https://tools.ietf.org/html/rfc8555#section-7.4
267
+ *
268
+ * @param {object} data Request data
269
+ * @returns {Promise<object>} Order
270
+ *
271
+ * @example Create a new order
272
+ * ```js
273
+ * const order = await client.createOrder({
274
+ * identifiers: [
275
+ * { type: 'dns', value: 'example.com' },
276
+ * { type: 'dns', value: 'test.example.com' }
277
+ * ]
278
+ * });
279
+ * ```
280
+ */
281
+
282
+ async createOrder(data) {
283
+ const resp = await this.api.createOrder(data);
284
+
285
+ if (!resp.headers.location) {
286
+ throw new Error('Creating a new order did not return an order link');
287
+ }
288
+
289
+ /* Add URL to response */
290
+ resp.data.url = resp.headers.location;
291
+ return resp.data;
292
+ }
293
+
294
+
295
+ /**
296
+ * Refresh order object from CA
297
+ *
298
+ * https://tools.ietf.org/html/rfc8555#section-7.4
299
+ *
300
+ * @param {object} order Order object
301
+ * @returns {Promise<object>} Order
302
+ *
303
+ * @example
304
+ * ```js
305
+ * const order = { ... }; // Previously created order object
306
+ * const result = await client.getOrder(order);
307
+ * ```
308
+ */
309
+
310
+ async getOrder(order) {
311
+ if (!order.url) {
312
+ throw new Error('Unable to get order, URL not found');
313
+ }
314
+
315
+ const resp = await this.api.getOrder(order.url);
316
+
317
+ /* Add URL to response */
318
+ resp.data.url = order.url;
319
+ return resp.data;
320
+ }
321
+
322
+ /**
323
+ * Finalize order
324
+ *
325
+ * https://tools.ietf.org/html/rfc8555#section-7.4
326
+ *
327
+ * @param {object} order Order object
328
+ * @param {buffer|string} csr PEM encoded Certificate Signing Request
329
+ * @returns {Promise<object>} Order
330
+ *
331
+ * @example Finalize order
332
+ * ```js
333
+ * const order = { ... }; // Previously created order object
334
+ * const csr = { ... }; // Previously created Certificate Signing Request
335
+ * const result = await client.finalizeOrder(order, csr);
336
+ * ```
337
+ */
338
+
339
+ async finalizeOrder(order, csr) {
340
+ if (!order.finalize) {
341
+ throw new Error('Unable to finalize order, URL not found');
342
+ }
343
+
344
+ if (!Buffer.isBuffer(csr)) {
345
+ csr = Buffer.from(csr);
346
+ }
347
+
348
+ const body = forge.getPemBody(csr);
349
+ const data = { csr: util.b64escape(body) };
350
+
351
+ const resp = await this.api.finalizeOrder(order.finalize, data);
352
+
353
+ /* Add URL to response */
354
+ resp.data.url = order.url;
355
+ return resp.data;
356
+ }
357
+
358
+
359
+ /**
360
+ * Get identifier authorizations from order
361
+ *
362
+ * https://tools.ietf.org/html/rfc8555#section-7.5
363
+ *
364
+ * @param {object} order Order
365
+ * @returns {Promise<object[]>} Authorizations
366
+ *
367
+ * @example Get identifier authorizations
368
+ * ```js
369
+ * const order = { ... }; // Previously created order object
370
+ * const authorizations = await client.getAuthorizations(order);
371
+ *
372
+ * authorizations.forEach((authz) => {
373
+ * const { challenges } = authz;
374
+ * });
375
+ * ```
376
+ */
377
+
378
+ async getAuthorizations(order) {
379
+ return Promise.map((order.authorizations || []), async (url) => {
380
+ const resp = await this.api.getAuthorization(url);
381
+
382
+ /* Add URL to response */
383
+ resp.data.url = url;
384
+ return resp.data;
385
+ });
386
+ }
387
+
388
+
389
+ /**
390
+ * Deactivate identifier authorization
391
+ *
392
+ * https://tools.ietf.org/html/rfc8555#section-7.5.2
393
+ *
394
+ * @param {object} authz Identifier authorization
395
+ * @returns {Promise<object>} Authorization
396
+ *
397
+ * @example Deactivate identifier authorization
398
+ * ```js
399
+ * const authz = { ... }; // Identifier authorization resolved from previously created order
400
+ * const result = await client.deactivateAuthorization(authz);
401
+ * ```
402
+ */
403
+
404
+ async deactivateAuthorization(authz) {
405
+ if (!authz.url) {
406
+ throw new Error('Unable to deactivate identifier authorization, URL not found');
407
+ }
408
+
409
+ const data = {
410
+ status: 'deactivated'
411
+ };
412
+
413
+ const resp = await this.api.updateAuthorization(authz.url, data);
414
+
415
+ /* Add URL to response */
416
+ resp.data.url = authz.url;
417
+ return resp.data;
418
+ }
419
+
420
+
421
+ /**
422
+ * Get key authorization for ACME challenge
423
+ *
424
+ * https://tools.ietf.org/html/rfc8555#section-8.1
425
+ *
426
+ * @param {object} challenge Challenge object returned by API
427
+ * @returns {Promise<string>} Key authorization
428
+ *
429
+ * @example Get challenge key authorization
430
+ * ```js
431
+ * const challenge = { ... }; // Challenge from previously resolved identifier authorization
432
+ * const key = await client.getChallengeKeyAuthorization(challenge);
433
+ *
434
+ * // Write key somewhere to satisfy challenge
435
+ * ```
436
+ */
437
+
438
+ async getChallengeKeyAuthorization(challenge) {
439
+ const jwk = await this.http.getJwk();
440
+ const keysum = crypto.createHash('sha256').update(JSON.stringify(jwk));
441
+ const thumbprint = util.b64escape(keysum.digest('base64'));
442
+ const result = `${challenge.token}.${thumbprint}`;
443
+
444
+ /**
445
+ * https://tools.ietf.org/html/rfc8555#section-8.3
446
+ */
447
+
448
+ if (challenge.type === 'http-01') {
449
+ return result;
450
+ }
451
+
452
+ /**
453
+ * https://tools.ietf.org/html/rfc8555#section-8.4
454
+ * https://tools.ietf.org/html/draft-ietf-acme-tls-alpn-01
455
+ */
456
+
457
+ if ((challenge.type === 'dns-01') || (challenge.type === 'tls-alpn-01')) {
458
+ const shasum = crypto.createHash('sha256').update(result);
459
+ return util.b64escape(shasum.digest('base64'));
460
+ }
461
+
462
+ throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
463
+ }
464
+
465
+
466
+ /**
467
+ * Verify that ACME challenge is satisfied
468
+ *
469
+ * @param {object} authz Identifier authorization
470
+ * @param {object} challenge Authorization challenge
471
+ * @returns {Promise}
472
+ *
473
+ * @example Verify satisfied ACME challenge
474
+ * ```js
475
+ * const authz = { ... }; // Identifier authorization
476
+ * const challenge = { ... }; // Satisfied challenge
477
+ * await client.verifyChallenge(authz, challenge);
478
+ * ```
479
+ */
480
+
481
+ async verifyChallenge(authz, challenge) {
482
+ if (!authz.url || !challenge.url) {
483
+ throw new Error('Unable to verify ACME challenge, URL not found');
484
+ }
485
+
486
+ if (typeof verify[challenge.type] === 'undefined') {
487
+ throw new Error(`Unable to verify ACME challenge, unknown type: ${challenge.type}`);
488
+ }
489
+
490
+ const keyAuthorization = await this.getChallengeKeyAuthorization(challenge);
491
+
492
+ const verifyFn = async () => {
493
+ await verify[challenge.type](authz, challenge, keyAuthorization);
494
+ };
495
+
496
+ logger.info('Waiting for ACME challenge verification', this.backoffOpts);
497
+ return util.retry(verifyFn, this.backoffOpts);
498
+ }
499
+
500
+
501
+ /**
502
+ * Notify CA that challenge has been completed
503
+ *
504
+ * https://tools.ietf.org/html/rfc8555#section-7.5.1
505
+ *
506
+ * @param {object} challenge Challenge object returned by API
507
+ * @returns {Promise<object>} Challenge
508
+ *
509
+ * @example Notify CA that challenge has been completed
510
+ * ```js
511
+ * const challenge = { ... }; // Satisfied challenge
512
+ * const result = await client.completeChallenge(challenge);
513
+ * ```
514
+ */
515
+
516
+ async completeChallenge(challenge) {
517
+ const resp = await this.api.completeChallenge(challenge.url, {});
518
+ return resp.data;
519
+ }
520
+
521
+
522
+ /**
523
+ * Wait for ACME provider to verify status on a order, authorization or challenge
524
+ *
525
+ * https://tools.ietf.org/html/rfc8555#section-7.5.1
526
+ *
527
+ * @param {object} item An order, authorization or challenge object
528
+ * @returns {Promise<object>} Valid order, authorization or challenge
529
+ *
530
+ * @example Wait for valid challenge status
531
+ * ```js
532
+ * const challenge = { ... };
533
+ * await client.waitForValidStatus(challenge);
534
+ * ```
535
+ *
536
+ * @example Wait for valid authoriation status
537
+ * ```js
538
+ * const authz = { ... };
539
+ * await client.waitForValidStatus(authz);
540
+ * ```
541
+ *
542
+ * @example Wait for valid order status
543
+ * ```js
544
+ * const order = { ... };
545
+ * await client.waitForValidStatus(order);
546
+ * ```
547
+ */
548
+
549
+ async waitForValidStatus(item) {
550
+ if (!item.url) {
551
+ throw new Error('Unable to verify status of item, URL not found');
552
+ }
553
+
554
+ const verifyFn = async (abort) => {
555
+ const resp = await this.api.apiRequest(item.url, null, [200]);
556
+
557
+ /* Verify status */
558
+ logger.info(`Item has status: ${resp.data.status}`);
559
+
560
+ if (resp.data.status === 'invalid') {
561
+ abort();
562
+ throw new Error(`Operation is invalid:${util.formatResponseError(resp)}`);
563
+ }
564
+ else if (resp.data.status === 'pending') {
565
+ throw new Error('Operation is pending');
566
+ }
567
+ else if (resp.data.status === 'valid') {
568
+ return resp.data;
569
+ }
570
+
571
+ throw new Error(`Unexpected item status: ${resp.data.status}`);
572
+ };
573
+
574
+ logger.info(`Waiting for valid status from: ${item.url}`, this.backoffOpts);
575
+ return util.retry(verifyFn, this.backoffOpts);
576
+ }
577
+
578
+
579
+ /**
580
+ * Get certificate from ACME order
581
+ *
582
+ * https://tools.ietf.org/html/rfc8555#section-7.4.2
583
+ *
584
+ * @param {object} order Order object
585
+ * @param {string} [preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null`
586
+ * @returns {Promise<string>} Certificate
587
+ *
588
+ * @example Get certificate
589
+ * ```js
590
+ * const order = { ... }; // Previously created order
591
+ * const certificate = await client.getCertificate(order);
592
+ * ```
593
+ *
594
+ * @example Get certificate with preferred chain
595
+ * ```js
596
+ * const order = { ... }; // Previously created order
597
+ * const certificate = await client.getCertificate(order, 'DST Root CA X3');
598
+ * ```
599
+ */
600
+
601
+ async getCertificate(order, preferredChain = null) {
602
+ if (order.status !== 'valid') {
603
+ order = await this.waitForValidStatus(order);
604
+ }
605
+
606
+ if (!order.certificate) {
607
+ throw new Error('Unable to download certificate, URL not found');
608
+ }
609
+
610
+ const resp = await this.api.apiRequest(order.certificate, null, [200]);
611
+
612
+ /* Handle alternate certificate chains */
613
+ if (preferredChain && resp.headers.link) {
614
+ const alternateLinks = util.parseLinkHeader(resp.headers.link);
615
+ const alternates = await Promise.map(alternateLinks, async (link) => this.api.apiRequest(link, null, [200]));
616
+ const certificates = [resp].concat(alternates).map((c) => c.data);
617
+
618
+ return util.findCertificateChainForIssuer(certificates, preferredChain);
619
+ }
620
+
621
+ /* Return default certificate chain */
622
+ return resp.data;
623
+ }
624
+
625
+
626
+ /**
627
+ * Revoke certificate
628
+ *
629
+ * https://tools.ietf.org/html/rfc8555#section-7.6
630
+ *
631
+ * @param {buffer|string} cert PEM encoded certificate
632
+ * @param {object} [data] Additional request data
633
+ * @returns {Promise}
634
+ *
635
+ * @example Revoke certificate
636
+ * ```js
637
+ * const certificate = { ... }; // Previously created certificate
638
+ * const result = await client.revokeCertificate(certificate);
639
+ * ```
640
+ *
641
+ * @example Revoke certificate with reason
642
+ * ```js
643
+ * const certificate = { ... }; // Previously created certificate
644
+ * const result = await client.revokeCertificate(certificate, {
645
+ * reason: 4
646
+ * });
647
+ * ```
648
+ */
649
+
650
+ async revokeCertificate(cert, data = {}) {
651
+ const body = forge.getPemBody(cert);
652
+ data.certificate = util.b64escape(body);
653
+
654
+ const resp = await this.api.revokeCert(data);
655
+ return resp.data;
656
+ }
657
+
658
+
659
+ /**
660
+ * Auto mode
661
+ *
662
+ * @param {object} opts
663
+ * @param {buffer|string} opts.csr Certificate Signing Request
664
+ * @param {function} opts.challengeCreateFn Function returning Promise triggered before completing ACME challenge
665
+ * @param {function} opts.challengeRemoveFn Function returning Promise triggered after completing ACME challenge
666
+ * @param {string} [opts.email] Account email address
667
+ * @param {boolean} [opts.termsOfServiceAgreed] Agree to Terms of Service, default: `false`
668
+ * @param {boolean} [opts.skipChallengeVerification] Skip internal challenge verification before notifying ACME provider, default: `false`
669
+ * @param {string[]} [opts.challengePriority] Array defining challenge type priority, default: `['http-01', 'dns-01']`
670
+ * @param {string} [opts.preferredChain] Indicate which certificate chain is preferred if a CA offers multiple, by exact issuer common name, default: `null`
671
+ * @returns {Promise<string>} Certificate
672
+ *
673
+ * @example Order a certificate using auto mode
674
+ * ```js
675
+ * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
676
+ * commonName: 'test.example.com'
677
+ * });
678
+ *
679
+ * const certificate = await client.auto({
680
+ * csr: certificateRequest,
681
+ * email: 'test@example.com',
682
+ * termsOfServiceAgreed: true,
683
+ * challengeCreateFn: async (authz, challenge, keyAuthorization) => {
684
+ * // Satisfy challenge here
685
+ * },
686
+ * challengeRemoveFn: async (authz, challenge, keyAuthorization) => {
687
+ * // Clean up challenge here
688
+ * }
689
+ * });
690
+ * ```
691
+ *
692
+ * @example Order a certificate using auto mode with preferred chain
693
+ * ```js
694
+ * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
695
+ * commonName: 'test.example.com'
696
+ * });
697
+ *
698
+ * const certificate = await client.auto({
699
+ * csr: certificateRequest,
700
+ * email: 'test@example.com',
701
+ * termsOfServiceAgreed: true,
702
+ * preferredChain: 'DST Root CA X3',
703
+ * challengeCreateFn: async () => {},
704
+ * challengeRemoveFn: async () => {}
705
+ * });
706
+ * ```
707
+ */
708
+
709
+ auto(opts) {
710
+ return auto(this, opts);
711
+ }
712
+ }
713
+
714
+
715
+ /* Export client */
716
+ module.exports = AcmeClient;