@certd/acme-client 0.1.10 → 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/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
  /**
@@ -71,8 +74,7 @@ function parseDomains(obj) {
71
74
 
72
75
  if (rootAltNames && rootAltNames.altNames && rootAltNames.altNames.length) {
73
76
  altNamesDict = rootAltNames.altNames;
74
- }
75
- else if (rootExtensions && rootExtensions.extensions && rootExtensions.extensions.length) {
77
+ } else if (rootExtensions && rootExtensions.extensions && rootExtensions.extensions.length) {
76
78
  const extAltNames = rootExtensions.extensions.find((e) => 'altNames' in e);
77
79
 
78
80
  if (extAltNames && extAltNames.altNames && extAltNames.altNames.length) {
@@ -113,11 +115,21 @@ function parseDomains(obj) {
113
115
  */
114
116
 
115
117
  async function createPrivateKey(size = 2048) {
116
- const keyPair = await generateKeyPair({ bits: size });
117
- const pemKey = forge.pki.privateKeyToPem(keyPair.privateKey);
118
+ const keyPair = await generateKeyPair({bits: size});
119
+ // const privateKey = forge.pki.privateKeyToPem(keyPair.privateKey);
120
+
121
+ // convert a Forge private key to an ASN.1 RSAPrivateKey
122
+ var rsaPrivateKey = forge.pki.privateKeyToAsn1(keyPair.privateKey);
123
+
124
+ // wrap an RSAPrivateKey ASN.1 object in a PKCS#8 ASN.1 PrivateKeyInfo
125
+ var privateKeyInfo = forge.pki.wrapRsaPrivateKey(rsaPrivateKey);
126
+
127
+ // convert a PKCS#8 ASN.1 PrivateKeyInfo to PEM
128
+ var pemKey = forge.pki.privateKeyInfoToPem(privateKeyInfo);
118
129
  return Buffer.from(pemKey);
119
130
  }
120
131
 
132
+
121
133
  exports.createPrivateKey = createPrivateKey;
122
134
 
123
135
 
@@ -133,7 +145,7 @@ exports.createPrivateKey = createPrivateKey;
133
145
  * ```
134
146
  */
135
147
 
136
- exports.createPublicKey = async function(key) {
148
+ exports.createPublicKey = async function (key) {
137
149
  const privateKey = forge.pki.privateKeyFromPem(key);
138
150
  const publicKey = forge.pki.rsa.setPublicKey(privateKey.n, privateKey.e);
139
151
  const pemKey = forge.pki.publicKeyToPem(publicKey);
@@ -142,7 +154,7 @@ exports.createPublicKey = async function(key) {
142
154
 
143
155
 
144
156
  /**
145
- * Parse body of PEM encoded object form buffer or string
157
+ * Parse body of PEM encoded object from buffer or string
146
158
  * If multiple objects are chained, the first body will be returned
147
159
  *
148
160
  * @param {buffer|string} str PEM encoded buffer or string
@@ -179,7 +191,7 @@ exports.splitPemChain = (str) => forge.pem.decode(str).map(forge.pem.encode);
179
191
  * ```
180
192
  */
181
193
 
182
- exports.getModulus = async function(input) {
194
+ exports.getModulus = async function (input) {
183
195
  if (!Buffer.isBuffer(input)) {
184
196
  input = Buffer.from(input);
185
197
  }
@@ -203,7 +215,7 @@ exports.getModulus = async function(input) {
203
215
  * ```
204
216
  */
205
217
 
206
- exports.getPublicExponent = async function(input) {
218
+ exports.getPublicExponent = async function (input) {
207
219
  if (!Buffer.isBuffer(input)) {
208
220
  input = Buffer.from(input);
209
221
  }
@@ -228,7 +240,7 @@ exports.getPublicExponent = async function(input) {
228
240
  * ```
229
241
  */
230
242
 
231
- exports.readCsrDomains = async function(csr) {
243
+ exports.readCsrDomains = async function (csr) {
232
244
  if (!Buffer.isBuffer(csr)) {
233
245
  csr = Buffer.from(csr);
234
246
  }
@@ -257,7 +269,7 @@ exports.readCsrDomains = async function(csr) {
257
269
  * ```
258
270
  */
259
271
 
260
- exports.readCertificateInfo = async function(cert) {
272
+ exports.readCertificateInfo = async function (cert) {
261
273
  if (!Buffer.isBuffer(cert)) {
262
274
  cert = Buffer.from(cert);
263
275
  }
@@ -309,7 +321,7 @@ function createCsrSubject(subjectObj) {
309
321
  return Object.entries(subjectObj).reduce((result, [shortName, value]) => {
310
322
  if (value) {
311
323
  const valueTagClass = getCsrValueTagClass(shortName);
312
- result.push({ shortName, value, valueTagClass });
324
+ result.push({shortName, value, valueTagClass});
313
325
  }
314
326
 
315
327
  return result;
@@ -329,7 +341,7 @@ function createCsrSubject(subjectObj) {
329
341
  function formatCsrAltNames(altNames) {
330
342
  return altNames.map((value) => {
331
343
  const type = net.isIP(value) ? 7 : 2;
332
- return { type, value };
344
+ return {type, value};
333
345
  });
334
346
  }
335
347
 
@@ -388,11 +400,10 @@ function formatCsrAltNames(altNames) {
388
400
  * }, certificateKey);
389
401
  */
390
402
 
391
- exports.createCsr = async function(data, key = null) {
403
+ exports.createCsr = async function (data, key = null) {
392
404
  if (!key) {
393
405
  key = await createPrivateKey(data.keySize);
394
- }
395
- else if (!Buffer.isBuffer(key)) {
406
+ } else if (!Buffer.isBuffer(key)) {
396
407
  key = Buffer.from(key);
397
408
  }
398
409
 
@@ -436,8 +447,8 @@ exports.createCsr = async function(data, key = null) {
436
447
  }]);
437
448
  }
438
449
 
439
- /* Sign CSR */
440
- csr.sign(privateKey);
450
+ /* Sign CSR using SHA-256 */
451
+ csr.sign(privateKey, forge.md.sha256.create());
441
452
 
442
453
  /* Done */
443
454
  const pemCsr = forge.pki.certificationRequestToPem(csr);