@certd/acme-client 0.2.0 → 0.3.1

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
@@ -4,28 +4,39 @@
4
4
  * @namespace Client
5
5
  */
6
6
 
7
- const crypto = require('crypto');
8
- const logger = require('./util.log.js');
9
-
10
- const debug = logger.info;
11
- const Promise = require('bluebird');
7
+ const { createHash } = require('crypto');
8
+ const { getPemBodyAsB64u } = require('./crypto');
9
+ const { log } = require('./logger');
12
10
  const HttpClient = require('./http');
13
11
  const AcmeApi = require('./api');
14
12
  const verify = require('./verify');
15
13
  const util = require('./util');
16
14
  const auto = require('./auto');
17
- const forge = require('./crypto/forge');
15
+
16
+
17
+ /**
18
+ * ACME states
19
+ *
20
+ * @private
21
+ */
22
+
23
+ const validStates = ['ready', 'valid'];
24
+ const pendingStates = ['pending', 'processing'];
25
+ const invalidStates = ['invalid'];
18
26
 
19
27
 
20
28
  /**
21
29
  * Default options
30
+ *
31
+ * @private
22
32
  */
23
33
 
24
34
  const defaultOpts = {
25
35
  directoryUrl: undefined,
26
36
  accountKey: undefined,
27
37
  accountUrl: null,
28
- backoffAttempts: 5,
38
+ externalAccountBinding: {},
39
+ backoffAttempts: 10,
29
40
  backoffMin: 5000,
30
41
  backoffMax: 30000
31
42
  };
@@ -39,7 +50,10 @@ const defaultOpts = {
39
50
  * @param {string} opts.directoryUrl ACME directory URL
40
51
  * @param {buffer|string} opts.accountKey PEM encoded account private key
41
52
  * @param {string} [opts.accountUrl] Account URL, default: `null`
42
- * @param {number} [opts.backoffAttempts] Maximum number of backoff attempts, default: `5`
53
+ * @param {object} [opts.externalAccountBinding]
54
+ * @param {string} [opts.externalAccountBinding.kid] External account binding KID
55
+ * @param {string} [opts.externalAccountBinding.hmacKey] External account binding HMAC key
56
+ * @param {number} [opts.backoffAttempts] Maximum number of backoff attempts, default: `10`
43
57
  * @param {number} [opts.backoffMin] Minimum backoff attempt delay in milliseconds, default: `5000`
44
58
  * @param {number} [opts.backoffMax] Maximum backoff attempt delay in milliseconds, default: `30000`
45
59
  *
@@ -57,11 +71,23 @@ const defaultOpts = {
57
71
  * directoryUrl: acme.directory.letsencrypt.staging,
58
72
  * accountKey: 'Private key goes here',
59
73
  * accountUrl: 'Optional account URL goes here',
60
- * backoffAttempts: 5,
74
+ * backoffAttempts: 10,
61
75
  * backoffMin: 5000,
62
76
  * backoffMax: 30000
63
77
  * });
64
78
  * ```
79
+ *
80
+ * @example Create ACME client with external account binding
81
+ * ```js
82
+ * const client = new acme.Client({
83
+ * directoryUrl: 'https://acme-provider.example.com/directory-url',
84
+ * accountKey: 'Private key goes here',
85
+ * externalAccountBinding: {
86
+ * kid: 'YOUR-EAB-KID',
87
+ * hmacKey: 'YOUR-EAB-HMAC-KEY'
88
+ * }
89
+ * });
90
+ * ```
65
91
  */
66
92
 
67
93
  class AcmeClient {
@@ -78,7 +104,7 @@ class AcmeClient {
78
104
  max: this.opts.backoffMax
79
105
  };
80
106
 
81
- this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey);
107
+ this.http = new HttpClient(this.opts.directoryUrl, this.opts.accountKey, this.opts.externalAccountBinding);
82
108
  this.api = new AcmeApi(this.http, this.opts.accountUrl);
83
109
  }
84
110
 
@@ -154,17 +180,17 @@ class AcmeClient {
154
180
  this.getAccountUrl();
155
181
 
156
182
  /* Account URL exists */
157
- logger.info('Account URL exists, returning updateAccount()');
183
+ log('Account URL exists, returning updateAccount()');
158
184
  return this.updateAccount(data);
159
185
  }
160
186
  catch (e) {
161
187
  const resp = await this.api.createAccount(data);
162
- // TODO 先注释,可加快速度
188
+
163
189
  /* 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
- // }
190
+ if (resp.status === 200) {
191
+ log('Account already exists (HTTP 200), returning updateAccount()');
192
+ return this.updateAccount(data);
193
+ }
168
194
 
169
195
  return resp.data;
170
196
  }
@@ -192,7 +218,7 @@ class AcmeClient {
192
218
  this.api.getAccountUrl();
193
219
  }
194
220
  catch (e) {
195
- logger.info('No account URL found, returning createAccount()');
221
+ log('No account URL found, returning createAccount()');
196
222
  return this.createAccount(data);
197
223
  }
198
224
 
@@ -238,16 +264,13 @@ class AcmeClient {
238
264
  const newHttpClient = new HttpClient(this.opts.directoryUrl, newAccountKey);
239
265
  const newApiClient = new AcmeApi(newHttpClient, accountUrl);
240
266
 
241
- /* Get new JWK */
267
+ /* Get old JWK */
242
268
  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();
269
+ data.oldKey = this.http.getJwk();
247
270
 
248
271
  /* Get signed request body from new client */
249
272
  const url = await newHttpClient.getResourceUrl('keyChange');
250
- const body = await newHttpClient.createSignedBody(url, data);
273
+ const body = newHttpClient.createSignedBody(url, data);
251
274
 
252
275
  /* Change key using old client */
253
276
  const resp = await this.api.updateAccountKey(body);
@@ -345,9 +368,7 @@ class AcmeClient {
345
368
  csr = Buffer.from(csr);
346
369
  }
347
370
 
348
- const body = forge.getPemBody(csr);
349
- const data = { csr: util.b64escape(body) };
350
-
371
+ const data = { csr: getPemBodyAsB64u(csr) };
351
372
  const resp = await this.api.finalizeOrder(order.finalize, data);
352
373
 
353
374
  /* Add URL to response */
@@ -376,13 +397,13 @@ class AcmeClient {
376
397
  */
377
398
 
378
399
  async getAuthorizations(order) {
379
- return Promise.map((order.authorizations || []), async (url) => {
400
+ return Promise.all((order.authorizations || []).map(async (url) => {
380
401
  const resp = await this.api.getAuthorization(url);
381
402
 
382
403
  /* Add URL to response */
383
404
  resp.data.url = url;
384
405
  return resp.data;
385
- });
406
+ }));
386
407
  }
387
408
 
388
409
 
@@ -436,9 +457,9 @@ class AcmeClient {
436
457
  */
437
458
 
438
459
  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'));
460
+ const jwk = this.http.getJwk();
461
+ const keysum = createHash('sha256').update(JSON.stringify(jwk));
462
+ const thumbprint = keysum.digest('base64url');
442
463
  const result = `${challenge.token}.${thumbprint}`;
443
464
 
444
465
  /**
@@ -455,8 +476,8 @@ class AcmeClient {
455
476
  */
456
477
 
457
478
  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'));
479
+ const shasum = createHash('sha256').update(result);
480
+ return shasum.digest('base64url');
460
481
  }
461
482
 
462
483
  throw new Error(`Unable to produce key authorization, unknown challenge type: ${challenge.type}`);
@@ -493,7 +514,7 @@ class AcmeClient {
493
514
  await verify[challenge.type](authz, challenge, keyAuthorization);
494
515
  };
495
516
 
496
- logger.info('Waiting for ACME challenge verification', this.backoffOpts);
517
+ log('Waiting for ACME challenge verification', this.backoffOpts);
497
518
  return util.retry(verifyFn, this.backoffOpts);
498
519
  }
499
520
 
@@ -555,23 +576,23 @@ class AcmeClient {
555
576
  const resp = await this.api.apiRequest(item.url, null, [200]);
556
577
 
557
578
  /* Verify status */
558
- logger.info(`Item has status: ${resp.data.status}`);
579
+ log(`Item has status: ${resp.data.status}`);
559
580
 
560
- if (resp.data.status === 'invalid') {
581
+ if (invalidStates.includes(resp.data.status)) {
561
582
  abort();
562
- throw new Error(`Operation is invalid:${util.formatResponseError(resp)}`);
583
+ throw new Error(util.formatResponseError(resp));
563
584
  }
564
- else if (resp.data.status === 'pending') {
565
- throw new Error('Operation is pending');
585
+ else if (pendingStates.includes(resp.data.status)) {
586
+ throw new Error('Operation is pending or processing');
566
587
  }
567
- else if (resp.data.status === 'valid') {
588
+ else if (validStates.includes(resp.data.status)) {
568
589
  return resp.data;
569
590
  }
570
591
 
571
592
  throw new Error(`Unexpected item status: ${resp.data.status}`);
572
593
  };
573
594
 
574
- logger.info(`Waiting for valid status from: ${item.url}`, this.backoffOpts);
595
+ log(`Waiting for valid status from: ${item.url}`, this.backoffOpts);
575
596
  return util.retry(verifyFn, this.backoffOpts);
576
597
  }
577
598
 
@@ -599,7 +620,7 @@ class AcmeClient {
599
620
  */
600
621
 
601
622
  async getCertificate(order, preferredChain = null) {
602
- if (order.status !== 'valid') {
623
+ if (!validStates.includes(order.status)) {
603
624
  order = await this.waitForValidStatus(order);
604
625
  }
605
626
 
@@ -612,7 +633,7 @@ class AcmeClient {
612
633
  /* Handle alternate certificate chains */
613
634
  if (preferredChain && resp.headers.link) {
614
635
  const alternateLinks = util.parseLinkHeader(resp.headers.link);
615
- const alternates = await Promise.map(alternateLinks, async (link) => this.api.apiRequest(link, null, [200]));
636
+ const alternates = await Promise.all(alternateLinks.map(async (link) => this.api.apiRequest(link, null, [200])));
616
637
  const certificates = [resp].concat(alternates).map((c) => c.data);
617
638
 
618
639
  return util.findCertificateChainForIssuer(certificates, preferredChain);
@@ -648,9 +669,7 @@ class AcmeClient {
648
669
  */
649
670
 
650
671
  async revokeCertificate(cert, data = {}) {
651
- const body = forge.getPemBody(cert);
652
- data.certificate = util.b64escape(body);
653
-
672
+ data.certificate = getPemBodyAsB64u(cert);
654
673
  const resp = await this.api.revokeCert(data);
655
674
  return resp.data;
656
675
  }
@@ -672,7 +691,7 @@ class AcmeClient {
672
691
  *
673
692
  * @example Order a certificate using auto mode
674
693
  * ```js
675
- * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
694
+ * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
676
695
  * commonName: 'test.example.com'
677
696
  * });
678
697
  *
@@ -691,7 +710,7 @@ class AcmeClient {
691
710
  *
692
711
  * @example Order a certificate using auto mode with preferred chain
693
712
  * ```js
694
- * const [certificateKey, certificateRequest] = await acme.forge.createCsr({
713
+ * const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
695
714
  * commonName: 'test.example.com'
696
715
  * });
697
716
  *
@@ -1,14 +1,17 @@
1
1
  /**
2
- * node-forge crypto engine
2
+ * Legacy node-forge crypto interface
3
+ *
4
+ * DEPRECATION WARNING: This crypto interface is deprecated and will be removed from acme-client in a future
5
+ * major release. Please migrate to the new `acme.crypto` interface at your earliest convenience.
3
6
  *
4
7
  * @namespace forge
5
8
  */
6
9
 
7
10
  const net = require('net');
8
- const Promise = require('bluebird');
11
+ const { promisify } = require('util');
9
12
  const forge = require('node-forge');
10
13
 
11
- const generateKeyPair = Promise.promisify(forge.pki.rsa.generateKeyPair);
14
+ const generateKeyPair = promisify(forge.pki.rsa.generateKeyPair);
12
15
 
13
16
 
14
17
  /**
@@ -123,7 +126,6 @@ async function createPrivateKey(size = 2048) {
123
126
 
124
127
  // convert a PKCS#8 ASN.1 PrivateKeyInfo to PEM
125
128
  var pemKey = forge.pki.privateKeyInfoToPem(privateKeyInfo);
126
- console.log('privatekey ', pemKey)
127
129
  return Buffer.from(pemKey);
128
130
  }
129
131
 
@@ -152,7 +154,7 @@ exports.createPublicKey = async function (key) {
152
154
 
153
155
 
154
156
  /**
155
- * Parse body of PEM encoded object form buffer or string
157
+ * Parse body of PEM encoded object from buffer or string
156
158
  * If multiple objects are chained, the first body will be returned
157
159
  *
158
160
  * @param {buffer|string} str PEM encoded buffer or string
@@ -445,8 +447,8 @@ exports.createCsr = async function (data, key = null) {
445
447
  }]);
446
448
  }
447
449
 
448
- /* Sign CSR */
449
- csr.sign(privateKey);
450
+ /* Sign CSR using SHA-256 */
451
+ csr.sign(privateKey, forge.md.sha256.create());
450
452
 
451
453
  /* Done */
452
454
  const pemCsr = forge.pki.certificationRequestToPem(csr);