@certd/acme-client 1.20.2 → 1.20.6

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.
@@ -7,10 +7,21 @@
7
7
  const net = require('net');
8
8
  const { promisify } = require('util');
9
9
  const crypto = require('crypto');
10
- const jsrsasign = require('jsrsasign');
10
+ const asn1js = require('asn1js');
11
+ const x509 = require('@peculiar/x509');
11
12
 
13
+ const randomInt = promisify(crypto.randomInt);
12
14
  const generateKeyPair = promisify(crypto.generateKeyPair);
13
15
 
16
+ /* Use Node.js Web Crypto API */
17
+ x509.cryptoProvider.set(crypto.webcrypto);
18
+
19
+ /* id-ce-subjectAltName - https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
20
+ const subjectAltNameOID = '2.5.29.17';
21
+
22
+ /* id-pe-acmeIdentifier - https://datatracker.ietf.org/doc/html/rfc8737#section-6.1 */
23
+ const alpnAcmeIdentifierOID = '1.3.6.1.5.5.7.1.31';
24
+
14
25
 
15
26
  /**
16
27
  * Determine key type and info by attempting to derive public key
@@ -24,17 +35,14 @@ function getKeyInfo(keyPem) {
24
35
  const result = {
25
36
  isRSA: false,
26
37
  isECDSA: false,
27
- signatureAlgorithm: null,
28
38
  publicKey: crypto.createPublicKey(keyPem)
29
39
  };
30
40
 
31
41
  if (result.publicKey.asymmetricKeyType === 'rsa') {
32
42
  result.isRSA = true;
33
- result.signatureAlgorithm = 'SHA256withRSA';
34
43
  }
35
44
  else if (result.publicKey.asymmetricKeyType === 'ec') {
36
45
  result.isECDSA = true;
37
- result.signatureAlgorithm = 'SHA256withECDSA';
38
46
  }
39
47
  else {
40
48
  throw new Error('Unable to parse key information, unknown format');
@@ -169,24 +177,42 @@ exports.getJwk = getJwk;
169
177
 
170
178
 
171
179
  /**
172
- * Fix missing support for NIST curve names in jsrsasign
180
+ * Produce CryptoKeyPair and signing algorithm from a PEM encoded private key
173
181
  *
174
182
  * @private
175
- * @param {string} crv NIST curve name
176
- * @returns {string} SECG curve name
183
+ * @param {buffer|string} keyPem PEM encoded private key
184
+ * @returns {Promise<array>} [keyPair, signingAlgorithm]
177
185
  */
178
186
 
179
- function convertNistCurveNameToSecg(nistName) {
180
- switch (nistName) {
181
- case 'P-256':
182
- return 'secp256r1';
183
- case 'P-384':
184
- return 'secp384r1';
185
- case 'P-521':
186
- return 'secp521r1';
187
- default:
188
- return nistName;
187
+ async function getWebCryptoKeyPair(keyPem) {
188
+ const info = getKeyInfo(keyPem);
189
+ const jwk = getJwk(keyPem);
190
+
191
+ /* Signing algorithm */
192
+ const sigalg = {
193
+ name: 'RSASSA-PKCS1-v1_5',
194
+ hash: { name: 'SHA-256' }
195
+ };
196
+
197
+ if (info.isECDSA) {
198
+ sigalg.name = 'ECDSA';
199
+ sigalg.namedCurve = jwk.crv;
200
+
201
+ if (jwk.crv === 'P-384') {
202
+ sigalg.hash.name = 'SHA-384';
203
+ }
204
+
205
+ if (jwk.crv === 'P-521') {
206
+ sigalg.hash.name = 'SHA-512';
207
+ }
189
208
  }
209
+
210
+ /* Decode PEM and import into CryptoKeyPair */
211
+ const privateKeyDec = x509.PemConverter.decodeFirst(keyPem.toString());
212
+ const privateKey = await crypto.webcrypto.subtle.importKey('pkcs8', privateKeyDec, sigalg, true, ['sign']);
213
+ const publicKey = await crypto.webcrypto.subtle.importKey('jwk', jwk, sigalg, true, ['verify']);
214
+
215
+ return [{ privateKey, publicKey }, sigalg];
190
216
  }
191
217
 
192
218
 
@@ -194,7 +220,7 @@ function convertNistCurveNameToSecg(nistName) {
194
220
  * Split chain of PEM encoded objects from string into array
195
221
  *
196
222
  * @param {buffer|string} chainPem PEM encoded object chain
197
- * @returns {array} Array of PEM objects including headers
223
+ * @returns {string[]} Array of PEM objects including headers
198
224
  */
199
225
 
200
226
  function splitPemChain(chainPem) {
@@ -202,15 +228,9 @@ function splitPemChain(chainPem) {
202
228
  chainPem = chainPem.toString();
203
229
  }
204
230
 
205
- return chainPem
206
- /* Split chain into chunks, starting at every header */
207
- .split(/\s*(?=-----BEGIN [A-Z0-9- ]+-----\r?\n?)/g)
208
- /* Match header, PEM body and footer */
209
- .map((pem) => pem.match(/\s*-----BEGIN ([A-Z0-9- ]+)-----\r?\n?([\S\s]+)\r?\n?-----END \1-----/))
210
- /* Filter out non-matches or empty bodies */
211
- .filter((pem) => pem && pem[2] && pem[2].replace(/[\r\n]+/g, '').trim())
212
- /* Decode to hex, and back to PEM for formatting etc */
213
- .map(([pem, header]) => jsrsasign.hextopem(jsrsasign.pemtohex(pem, header), header));
231
+ /* Decode into array and re-encode */
232
+ return x509.PemConverter.decodeWithHeaders(chainPem)
233
+ .map((params) => x509.PemConverter.encode([params]));
214
234
  }
215
235
 
216
236
  exports.splitPemChain = splitPemChain;
@@ -231,43 +251,28 @@ exports.getPemBodyAsB64u = (pem) => {
231
251
  throw new Error('Unable to parse PEM body from string');
232
252
  }
233
253
 
234
- /* First object, hex and back to b64 without new lines */
235
- return jsrsasign.hextob64u(jsrsasign.pemtohex(chain[0]));
254
+ /* Select first object, extract body and convert to b64u */
255
+ const dec = x509.PemConverter.decodeFirst(chain[0]);
256
+ return Buffer.from(dec).toString('base64url');
236
257
  };
237
258
 
238
259
 
239
- /**
240
- * Parse common name from a subject object
241
- *
242
- * @private
243
- * @param {object} subj Subject returned from jsrsasign
244
- * @returns {string} Common name value
245
- */
246
-
247
- function parseCommonName(subj) {
248
- const subjectArr = (subj && subj.array) ? subj.array : [];
249
- const cnArr = subjectArr.find((s) => (s[0] && s[0].type && s[0].value && (s[0].type === 'CN')));
250
- return (cnArr && cnArr.length && cnArr[0].value) ? cnArr[0].value : null;
251
- }
252
-
253
-
254
260
  /**
255
261
  * Parse domains from a certificate or CSR
256
262
  *
257
263
  * @private
258
- * @param {object} params Certificate or CSR params returned from jsrsasign
264
+ * @param {object} input x509.Certificate or x509.Pkcs10CertificateRequest
259
265
  * @returns {object} {commonName, altNames}
260
266
  */
261
267
 
262
- function parseDomains(params) {
263
- const commonName = parseCommonName(params.subject);
264
- const extensionArr = (params.ext || params.extreq || []);
268
+ function parseDomains(input) {
269
+ const commonName = input.subjectName.getField('CN').pop() || null;
270
+ const altNamesRaw = input.getExtension(subjectAltNameOID);
265
271
  let altNames = [];
266
272
 
267
- if (extensionArr && extensionArr.length) {
268
- const altNameExt = extensionArr.find((e) => (e.extname && (e.extname === 'subjectAltName')));
269
- const altNameArr = (altNameExt && altNameExt.array && altNameExt.array.length) ? altNameExt.array : [];
270
- altNames = altNameArr.map((a) => Object.values(a)[0] || null).filter((a) => a);
273
+ if (altNamesRaw) {
274
+ const altNamesExt = new x509.SubjectAlternativeNameExtension(altNamesRaw.rawData);
275
+ altNames = altNames.concat(altNamesExt.names.items.map((i) => i.value));
271
276
  }
272
277
 
273
278
  return {
@@ -297,9 +302,9 @@ exports.readCsrDomains = (csrPem) => {
297
302
  csrPem = csrPem.toString();
298
303
  }
299
304
 
300
- /* Parse CSR */
301
- const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csrPem);
302
- return parseDomains(params);
305
+ const dec = x509.PemConverter.decodeFirst(csrPem);
306
+ const csr = new x509.Pkcs10CertificateRequest(dec);
307
+ return parseDomains(csr);
303
308
  };
304
309
 
305
310
 
@@ -324,48 +329,43 @@ exports.readCsrDomains = (csrPem) => {
324
329
  */
325
330
 
326
331
  exports.readCertificateInfo = (certPem) => {
327
- const chain = splitPemChain(certPem);
328
-
329
- if (!chain.length) {
330
- throw new Error('Unable to parse PEM body from string');
332
+ if (Buffer.isBuffer(certPem)) {
333
+ certPem = certPem.toString();
331
334
  }
332
335
 
333
- /* Parse certificate */
334
- const obj = new jsrsasign.X509();
335
- obj.readCertPEM(chain[0]);
336
- const params = obj.getParam();
336
+ const dec = x509.PemConverter.decodeFirst(certPem);
337
+ const cert = new x509.X509Certificate(dec);
337
338
 
338
339
  return {
339
340
  issuer: {
340
- commonName: parseCommonName(params.issuer)
341
+ commonName: cert.issuerName.getField('CN').pop() || null
341
342
  },
342
- domains: parseDomains(params),
343
- notBefore: jsrsasign.zulutodate(params.notbefore),
344
- notAfter: jsrsasign.zulutodate(params.notafter)
343
+ domains: parseDomains(cert),
344
+ notBefore: cert.notBefore,
345
+ notAfter: cert.notAfter
345
346
  };
346
347
  };
347
348
 
348
349
 
349
350
  /**
350
- * Determine ASN.1 character string type for CSR subject field
351
+ * Determine ASN.1 character string type for CSR subject field name
351
352
  *
352
- * https://tools.ietf.org/html/rfc5280
353
- * https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/x509-1.1.js#L2404-L2412
354
- * https://github.com/kjur/jsrsasign/blob/2613c64559768b91dde9793dfa318feacb7c3b8a/src/asn1x509-1.0.js#L3526-L3535
353
+ * https://datatracker.ietf.org/doc/html/rfc5280
354
+ * https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71
355
355
  *
356
356
  * @private
357
- * @param {string} field CSR subject field
358
- * @returns {string} ASN.1 jsrsasign character string type
357
+ * @param {string} field CSR subject field name
358
+ * @returns {string} ASN.1 character string type
359
359
  */
360
360
 
361
361
  function getCsrAsn1CharStringType(field) {
362
362
  switch (field) {
363
363
  case 'C':
364
- return 'prn';
364
+ return 'printableString';
365
365
  case 'E':
366
- return 'ia5';
366
+ return 'ia5String';
367
367
  default:
368
- return 'utf8';
368
+ return 'utf8String';
369
369
  }
370
370
  }
371
371
 
@@ -373,6 +373,8 @@ function getCsrAsn1CharStringType(field) {
373
373
  /**
374
374
  * Create array of subject fields for a Certificate Signing Request
375
375
  *
376
+ * https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/name.ts#L65-L71
377
+ *
376
378
  * @private
377
379
  * @param {object} input Key-value of subject fields
378
380
  * @returns {object[]} Certificate Signing Request subject array
@@ -382,7 +384,7 @@ function createCsrSubject(input) {
382
384
  return Object.entries(input).reduce((result, [type, value]) => {
383
385
  if (value) {
384
386
  const ds = getCsrAsn1CharStringType(type);
385
- result.push([{ type, value, ds }]);
387
+ result.push({ [type]: [{ [ds]: value }] });
386
388
  }
387
389
 
388
390
  return result;
@@ -391,20 +393,20 @@ function createCsrSubject(input) {
391
393
 
392
394
 
393
395
  /**
394
- * Create array of alt names for Certificate Signing Requests
396
+ * Create x509 subject alternate name extension
395
397
  *
396
- * https://github.com/kjur/jsrsasign/blob/3edc0070846922daea98d9588978e91d855577ec/src/x509-1.1.js#L1355-L1410
398
+ * https://github.com/PeculiarVentures/x509/blob/ecf78224fd594abbc2fa83c41565d79874f88e00/src/extensions/subject_alt_name.ts
397
399
  *
398
400
  * @private
399
401
  * @param {string[]} altNames Array of alt names
400
- * @returns {object[]} Certificate Signing Request alt names array
402
+ * @returns {x509.SubjectAlternativeNameExtension} Subject alternate name extension
401
403
  */
402
404
 
403
- function formatCsrAltNames(altNames) {
404
- return altNames.map((value) => {
405
- const key = net.isIP(value) ? 'ip' : 'dns';
406
- return { [key]: value };
407
- });
405
+ function createSubjectAltNameExtension(altNames) {
406
+ return new x509.SubjectAlternativeNameExtension(altNames.map((value) => {
407
+ const type = net.isIP(value) ? 'ip' : 'dns';
408
+ return { type, value };
409
+ }));
408
410
  }
409
411
 
410
412
 
@@ -414,14 +416,14 @@ function formatCsrAltNames(altNames) {
414
416
  * @param {object} data
415
417
  * @param {number} [data.keySize] Size of newly created RSA private key modulus in bits, default: `2048`
416
418
  * @param {string} [data.commonName] FQDN of your server
417
- * @param {array} [data.altNames] SAN (Subject Alternative Names), default: `[]`
419
+ * @param {string[]} [data.altNames] SAN (Subject Alternative Names), default: `[]`
418
420
  * @param {string} [data.country] 2 letter country code
419
421
  * @param {string} [data.state] State or province
420
422
  * @param {string} [data.locality] City
421
423
  * @param {string} [data.organization] Organization name
422
424
  * @param {string} [data.organizationUnit] Organizational unit name
423
425
  * @param {string} [data.emailAddress] Email address
424
- * @param {string} [keyPem] PEM encoded CSR private key
426
+ * @param {buffer|string} [keyPem] PEM encoded CSR private key
425
427
  * @returns {Promise<buffer[]>} [privateKey, certificateSigningRequest]
426
428
  *
427
429
  * @example Create a Certificate Signing Request
@@ -474,53 +476,144 @@ exports.createCsr = async (data, keyPem = null) => {
474
476
  data.altNames = [];
475
477
  }
476
478
 
477
- /* Get key info and JWK */
478
- const info = getKeyInfo(keyPem);
479
- const jwk = getJwk(keyPem);
480
- const extensionRequests = [];
481
-
482
- /* Missing support for NIST curve names in jsrsasign - https://github.com/kjur/jsrsasign/blob/master/src/asn1x509-1.0.js#L4388-L4393 */
483
- if (jwk.crv && (jwk.kty === 'EC')) {
484
- jwk.crv = convertNistCurveNameToSecg(jwk.crv);
485
- }
486
-
487
479
  /* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */
488
480
  if (data.commonName && !data.altNames.includes(data.commonName)) {
489
481
  data.altNames.unshift(data.commonName);
490
482
  }
491
483
 
492
- /* Subject */
493
- const subject = createCsrSubject({
494
- CN: data.commonName,
495
- C: data.country,
496
- ST: data.state,
497
- L: data.locality,
498
- O: data.organization,
499
- OU: data.organizationUnit,
500
- E: data.emailAddress
501
- });
484
+ /* CryptoKeyPair and signing algorithm from private key */
485
+ const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem);
502
486
 
503
- /* SAN extension */
504
- if (data.altNames.length) {
505
- extensionRequests.push({
506
- extname: 'subjectAltName',
507
- array: formatCsrAltNames(data.altNames)
508
- });
509
- }
487
+ const extensions = [
488
+ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
489
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature | x509.KeyUsageFlags.keyEncipherment), // eslint-disable-line no-bitwise
490
+
491
+ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
492
+ createSubjectAltNameExtension(data.altNames)
493
+ ];
510
494
 
511
495
  /* Create CSR */
512
- const csr = new jsrsasign.KJUR.asn1.csr.CertificationRequest({
513
- subject: { array: subject },
514
- sigalg: info.signatureAlgorithm,
515
- sbjprvkey: keyPem.toString(),
516
- sbjpubkey: jwk,
517
- extreq: extensionRequests
496
+ const csr = await x509.Pkcs10CertificateRequestGenerator.create({
497
+ keys,
498
+ extensions,
499
+ signingAlgorithm,
500
+ name: createCsrSubject({
501
+ CN: data.commonName,
502
+ C: data.country,
503
+ ST: data.state,
504
+ L: data.locality,
505
+ O: data.organization,
506
+ OU: data.organizationUnit,
507
+ E: data.emailAddress
508
+ })
518
509
  });
519
510
 
520
- /* Sign CSR, get PEM */
521
- csr.sign();
522
- const pem = csr.getPEM();
511
+ /* Done */
512
+ const pem = csr.toString('pem');
513
+ return [keyPem, Buffer.from(pem)];
514
+ };
515
+
516
+
517
+ /**
518
+ * Create a self-signed ALPN certificate for TLS-ALPN-01 challenges
519
+ *
520
+ * https://datatracker.ietf.org/doc/html/rfc8737
521
+ *
522
+ * @param {object} authz Identifier authorization
523
+ * @param {string} keyAuthorization Challenge key authorization
524
+ * @param {buffer|string} [keyPem] PEM encoded CSR private key
525
+ * @returns {Promise<buffer[]>} [privateKey, certificate]
526
+ *
527
+ * @example Create a ALPN certificate
528
+ * ```js
529
+ * const [alpnKey, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization);
530
+ * ```
531
+ *
532
+ * @example Create a ALPN certificate with ECDSA private key
533
+ * ```js
534
+ * const alpnKey = await acme.crypto.createPrivateEcdsaKey();
535
+ * const [, alpnCertificate] = await acme.crypto.createAlpnCertificate(authz, keyAuthorization, alpnKey);
536
+ */
537
+
538
+ exports.createAlpnCertificate = async (authz, keyAuthorization, keyPem = null) => {
539
+ if (!keyPem) {
540
+ keyPem = await createPrivateRsaKey();
541
+ }
542
+ else if (!Buffer.isBuffer(keyPem)) {
543
+ keyPem = Buffer.from(keyPem);
544
+ }
545
+
546
+ const now = new Date();
547
+ const commonName = authz.identifier.value;
548
+
549
+ /* Pseudo-random serial - max 20 bytes, 11 for epoch (year 5138), 9 random */
550
+ const random = await randomInt(1, 999999999);
551
+ const serialNumber = `${Math.floor(now.getTime() / 1000)}${random}`;
552
+
553
+ /* CryptoKeyPair and signing algorithm from private key */
554
+ const [keys, signingAlgorithm] = await getWebCryptoKeyPair(keyPem);
555
+
556
+ const extensions = [
557
+ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.3 */
558
+ new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), // eslint-disable-line no-bitwise
559
+
560
+ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.9 */
561
+ new x509.BasicConstraintsExtension(true, 2, true),
562
+
563
+ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.2 */
564
+ await x509.SubjectKeyIdentifierExtension.create(keys.publicKey),
565
+
566
+ /* https://datatracker.ietf.org/doc/html/rfc5280#section-4.2.1.6 */
567
+ createSubjectAltNameExtension([commonName])
568
+ ];
569
+
570
+ /* ALPN extension */
571
+ const payload = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
572
+ const octstr = new asn1js.OctetString({ valueHex: Buffer.from(payload, 'hex') });
573
+ extensions.push(new x509.Extension(alpnAcmeIdentifierOID, true, octstr.toBER()));
574
+
575
+ /* Self-signed ALPN certificate */
576
+ const cert = await x509.X509CertificateGenerator.createSelfSigned({
577
+ keys,
578
+ signingAlgorithm,
579
+ extensions,
580
+ serialNumber,
581
+ notBefore: now,
582
+ notAfter: now,
583
+ name: createCsrSubject({
584
+ CN: commonName
585
+ })
586
+ });
523
587
 
524
588
  /* Done */
589
+ const pem = cert.toString('pem');
525
590
  return [keyPem, Buffer.from(pem)];
526
591
  };
592
+
593
+
594
+ /**
595
+ * Validate that a ALPN certificate contains the expected key authorization
596
+ *
597
+ * @param {buffer|string} certPem PEM encoded certificate
598
+ * @param {string} keyAuthorization Expected challenge key authorization
599
+ * @returns {boolean} True when valid
600
+ */
601
+
602
+ exports.isAlpnCertificateAuthorizationValid = (certPem, keyAuthorization) => {
603
+ const expected = crypto.createHash('sha256').update(keyAuthorization).digest('hex');
604
+
605
+ /* Attempt to locate ALPN extension */
606
+ const cert = new x509.X509Certificate(certPem);
607
+ const ext = cert.getExtension(alpnAcmeIdentifierOID);
608
+
609
+ if (!ext) {
610
+ throw new Error('Unable to locate ALPN extension within parsed certificate');
611
+ }
612
+
613
+ /* Decode extension value */
614
+ const parsed = asn1js.fromBER(ext.value);
615
+ const result = Buffer.from(parsed.result.valueBlock.valueHexView).toString('hex');
616
+
617
+ /* Return true if match */
618
+ return (result === expected);
619
+ };
package/src/http.js CHANGED
@@ -3,10 +3,20 @@
3
3
  */
4
4
 
5
5
  const { createHmac, createSign, constants: { RSA_PKCS1_PADDING } } = require('crypto');
6
+ const { HttpsProxyAgent } = require('https-proxy-agent');
6
7
  const { getJwk } = require('./crypto');
7
8
  const { log } = require('./logger');
8
- const axios = require('./axios');
9
+ const axios1 = require('./axios');
9
10
 
11
+ const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
12
+ let httpsAgent = null;
13
+ if (httpsProxy) {
14
+ httpsAgent = new HttpsProxyAgent(httpsProxy);
15
+ }
16
+ const axios = axios1.create({
17
+ proxy: false,
18
+ httpsAgent
19
+ });
10
20
 
11
21
  /**
12
22
  * ACME HTTP client
@@ -64,7 +74,7 @@ class HttpClient {
64
74
  /**
65
75
  * Ensure provider directory exists
66
76
  *
67
- * https://tools.ietf.org/html/rfc8555#section-7.1.1
77
+ * https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1
68
78
  *
69
79
  * @returns {Promise}
70
80
  */
@@ -104,7 +114,7 @@ class HttpClient {
104
114
  /**
105
115
  * Get nonce from directory API endpoint
106
116
  *
107
- * https://tools.ietf.org/html/rfc8555#section-7.2
117
+ * https://datatracker.ietf.org/doc/html/rfc8555#section-7.2
108
118
  *
109
119
  * @returns {Promise<string>} nonce
110
120
  */
@@ -267,7 +277,7 @@ class HttpClient {
267
277
  /**
268
278
  * Signed HTTP request
269
279
  *
270
- * https://tools.ietf.org/html/rfc8555#section-6.2
280
+ * https://datatracker.ietf.org/doc/html/rfc8555#section-6.2
271
281
  *
272
282
  * @param {string} url Request URL
273
283
  * @param {object} payload Request payload
@@ -299,7 +309,7 @@ class HttpClient {
299
309
  const data = this.createSignedBody(url, payload, { nonce, kid });
300
310
  const resp = await this.request(url, 'post', { data });
301
311
 
302
- /* Retry on bad nonce - https://tools.ietf.org/html/draft-ietf-acme-acme-10#section-6.4 */
312
+ /* Retry on bad nonce - https://datatracker.ietf.org/doc/html/draft-ietf-acme-acme-10#section-6.4 */
303
313
  if (resp.data && resp.data.type && (resp.status === 400) && (resp.data.type === 'urn:ietf:params:acme:error:badNonce') && (attempts < this.maxBadNonceRetries)) {
304
314
  nonce = resp.headers['replay-nonce'] || null;
305
315
  attempts += 1;
package/src/util.js CHANGED
@@ -2,6 +2,7 @@
2
2
  * Utility methods
3
3
  */
4
4
 
5
+ const tls = require('tls');
5
6
  const dns = require('dns').promises;
6
7
  const { readCertificateInfo, splitPemChain } = require('./crypto');
7
8
  const { log } = require('./logger');
@@ -92,7 +93,7 @@ function retry(fn, { attempts = 5, min = 5000, max = 30000 } = {}) {
92
93
  *
93
94
  * @param {string} header Link header contents
94
95
  * @param {string} rel Link relation, default: `alternate`
95
- * @returns {array} Array of URLs
96
+ * @returns {string[]} Array of URLs
96
97
  */
97
98
 
98
99
  function parseLinkHeader(header, rel = 'alternate') {
@@ -112,7 +113,7 @@ function parseLinkHeader(header, rel = 'alternate') {
112
113
  * - If issuer is found in multiple chains, the closest to root wins
113
114
  * - If issuer can not be located, the first chain will be returned
114
115
  *
115
- * @param {array} certificates Array of PEM encoded certificate chains
116
+ * @param {string[]} certificates Array of PEM encoded certificate chains
116
117
  * @param {string} issuer Preferred certificate issuer
117
118
  * @returns {string} PEM encoded certificate chain
118
119
  */
@@ -245,6 +246,60 @@ async function getAuthoritativeDnsResolver(recordName) {
245
246
  }
246
247
 
247
248
 
249
+ /**
250
+ * Attempt to retrieve TLS ALPN certificate from peer
251
+ *
252
+ * https://nodejs.org/api/tls.html#tlsconnectoptions-callback
253
+ *
254
+ * @param {string} host Host the TLS client should connect to
255
+ * @param {number} port Port the client should connect to
256
+ * @param {string} servername Server name for the SNI (Server Name Indication)
257
+ * @returns {Promise<string>} PEM encoded certificate
258
+ */
259
+
260
+ async function retrieveTlsAlpnCertificate(host, port, timeout = 30000) {
261
+ return new Promise((resolve, reject) => {
262
+ let result;
263
+
264
+ /* TLS connection */
265
+ const socket = tls.connect({
266
+ host,
267
+ port,
268
+ servername: host,
269
+ rejectUnauthorized: false,
270
+ ALPNProtocols: ['acme-tls/1']
271
+ });
272
+
273
+ socket.setTimeout(timeout);
274
+ socket.setEncoding('utf-8');
275
+
276
+ /* Grab certificate once connected and close */
277
+ socket.on('secureConnect', () => {
278
+ result = socket.getPeerX509Certificate();
279
+ socket.end();
280
+ });
281
+
282
+ /* Errors */
283
+ socket.on('error', (err) => {
284
+ reject(err);
285
+ });
286
+
287
+ socket.on('timeout', () => {
288
+ socket.destroy(new Error('TLS ALPN certificate lookup request timed out'));
289
+ });
290
+
291
+ /* Done, return cert as PEM if found */
292
+ socket.on('end', () => {
293
+ if (result) {
294
+ return resolve(result.toString());
295
+ }
296
+
297
+ return reject(new Error('TLS ALPN lookup failed to retrieve certificate'));
298
+ });
299
+ });
300
+ }
301
+
302
+
248
303
  /**
249
304
  * Export utils
250
305
  */
@@ -254,5 +309,6 @@ module.exports = {
254
309
  parseLinkHeader,
255
310
  findCertificateChainForIssuer,
256
311
  formatResponseError,
257
- getAuthoritativeDnsResolver
312
+ getAuthoritativeDnsResolver,
313
+ retrieveTlsAlpnCertificate
258
314
  };