@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.
- package/LICENSE +1 -1
- package/README.md +13 -36
- package/package.json +17 -16
- package/src/api.js +13 -13
- package/src/auto.js +30 -10
- package/src/axios.js +7 -4
- package/src/client.js +22 -25
- package/src/crypto/forge.js +2 -2
- package/src/crypto/index.js +216 -123
- package/src/http.js +15 -5
- package/src/util.js +59 -3
- package/src/verify.js +38 -4
- package/types/index.d.ts +2 -0
- package/types/rfc8555.d.ts +10 -10
- package/types/tsconfig.json +0 -11
- package/types/tslint.json +0 -6
- /package/types/{test.ts → index.test-d.ts} +0 -0
package/src/crypto/index.js
CHANGED
|
@@ -7,10 +7,21 @@
|
|
|
7
7
|
const net = require('net');
|
|
8
8
|
const { promisify } = require('util');
|
|
9
9
|
const crypto = require('crypto');
|
|
10
|
-
const
|
|
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
|
-
*
|
|
180
|
+
* Produce CryptoKeyPair and signing algorithm from a PEM encoded private key
|
|
173
181
|
*
|
|
174
182
|
* @private
|
|
175
|
-
* @param {string}
|
|
176
|
-
* @returns {
|
|
183
|
+
* @param {buffer|string} keyPem PEM encoded private key
|
|
184
|
+
* @returns {Promise<array>} [keyPair, signingAlgorithm]
|
|
177
185
|
*/
|
|
178
186
|
|
|
179
|
-
function
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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 {
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
.
|
|
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
|
-
/*
|
|
235
|
-
|
|
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}
|
|
264
|
+
* @param {object} input x509.Certificate or x509.Pkcs10CertificateRequest
|
|
259
265
|
* @returns {object} {commonName, altNames}
|
|
260
266
|
*/
|
|
261
267
|
|
|
262
|
-
function parseDomains(
|
|
263
|
-
const commonName =
|
|
264
|
-
const
|
|
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 (
|
|
268
|
-
const
|
|
269
|
-
|
|
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
|
-
|
|
301
|
-
const
|
|
302
|
-
return parseDomains(
|
|
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
|
-
|
|
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
|
-
|
|
334
|
-
const
|
|
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:
|
|
341
|
+
commonName: cert.issuerName.getField('CN').pop() || null
|
|
341
342
|
},
|
|
342
|
-
domains: parseDomains(
|
|
343
|
-
notBefore:
|
|
344
|
-
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://
|
|
353
|
-
* https://github.com/
|
|
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
|
|
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 '
|
|
364
|
+
return 'printableString';
|
|
365
365
|
case 'E':
|
|
366
|
-
return '
|
|
366
|
+
return 'ia5String';
|
|
367
367
|
default:
|
|
368
|
-
return '
|
|
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(
|
|
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
|
|
396
|
+
* Create x509 subject alternate name extension
|
|
395
397
|
*
|
|
396
|
-
* https://github.com/
|
|
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 {
|
|
402
|
+
* @returns {x509.SubjectAlternativeNameExtension} Subject alternate name extension
|
|
401
403
|
*/
|
|
402
404
|
|
|
403
|
-
function
|
|
404
|
-
return altNames.map((value) => {
|
|
405
|
-
const
|
|
406
|
-
return {
|
|
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 {
|
|
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
|
-
/*
|
|
493
|
-
const
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
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 =
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
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
|
-
/*
|
|
521
|
-
csr.
|
|
522
|
-
|
|
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
|
|
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://
|
|
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://
|
|
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://
|
|
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://
|
|
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 {
|
|
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 {
|
|
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
|
};
|