@certd/acme-client 0.2.0 → 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/LICENSE +1 -1
- package/README.md +88 -25
- package/package.json +24 -29
- package/src/api.js +15 -8
- package/src/auto.js +93 -114
- package/src/client.js +67 -48
- package/src/crypto/forge.js +9 -7
- package/src/crypto/index.js +526 -0
- package/src/http.js +126 -49
- package/src/index.js +15 -0
- package/src/logger.js +30 -0
- package/src/util.js +148 -63
- package/src/verify.js +58 -27
- package/types/index.d.ts +51 -2
- package/types/test.ts +2 -2
- package/CHANGELOG.md +0 -152
- package/src/util.log.js +0 -8
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native Node.js crypto interface
|
|
3
|
+
*
|
|
4
|
+
* @namespace crypto
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const net = require('net');
|
|
8
|
+
const { promisify } = require('util');
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
const jsrsasign = require('jsrsasign');
|
|
11
|
+
|
|
12
|
+
const generateKeyPair = promisify(crypto.generateKeyPair);
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Determine key type and info by attempting to derive public key
|
|
17
|
+
*
|
|
18
|
+
* @private
|
|
19
|
+
* @param {buffer|string} keyPem PEM encoded private or public key
|
|
20
|
+
* @returns {object}
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
function getKeyInfo(keyPem) {
|
|
24
|
+
const result = {
|
|
25
|
+
isRSA: false,
|
|
26
|
+
isECDSA: false,
|
|
27
|
+
signatureAlgorithm: null,
|
|
28
|
+
publicKey: crypto.createPublicKey(keyPem)
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (result.publicKey.asymmetricKeyType === 'rsa') {
|
|
32
|
+
result.isRSA = true;
|
|
33
|
+
result.signatureAlgorithm = 'SHA256withRSA';
|
|
34
|
+
}
|
|
35
|
+
else if (result.publicKey.asymmetricKeyType === 'ec') {
|
|
36
|
+
result.isECDSA = true;
|
|
37
|
+
result.signatureAlgorithm = 'SHA256withECDSA';
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
throw new Error('Unable to parse key information, unknown format');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Generate a private RSA key
|
|
49
|
+
*
|
|
50
|
+
* @param {number} [modulusLength] Size of the keys modulus in bits, default: `2048`
|
|
51
|
+
* @returns {Promise<buffer>} PEM encoded private RSA key
|
|
52
|
+
*
|
|
53
|
+
* @example Generate private RSA key
|
|
54
|
+
* ```js
|
|
55
|
+
* const privateKey = await acme.crypto.createPrivateRsaKey();
|
|
56
|
+
* ```
|
|
57
|
+
*
|
|
58
|
+
* @example Private RSA key with modulus size 4096
|
|
59
|
+
* ```js
|
|
60
|
+
* const privateKey = await acme.crypto.createPrivateRsaKey(4096);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
async function createPrivateRsaKey(modulusLength = 2048) {
|
|
65
|
+
const pair = await generateKeyPair('rsa', {
|
|
66
|
+
modulusLength,
|
|
67
|
+
privateKeyEncoding: {
|
|
68
|
+
type: 'pkcs8',
|
|
69
|
+
format: 'pem'
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return Buffer.from(pair.privateKey);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
exports.createPrivateRsaKey = createPrivateRsaKey;
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Alias of `createPrivateRsaKey()`
|
|
81
|
+
*
|
|
82
|
+
* @function
|
|
83
|
+
*/
|
|
84
|
+
|
|
85
|
+
exports.createPrivateKey = createPrivateRsaKey;
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Generate a private ECDSA key
|
|
90
|
+
*
|
|
91
|
+
* @param {string} [namedCurve] ECDSA curve name (P-256, P-384 or P-521), default `P-256`
|
|
92
|
+
* @returns {Promise<buffer>} PEM encoded private ECDSA key
|
|
93
|
+
*
|
|
94
|
+
* @example Generate private ECDSA key
|
|
95
|
+
* ```js
|
|
96
|
+
* const privateKey = await acme.crypto.createPrivateEcdsaKey();
|
|
97
|
+
* ```
|
|
98
|
+
*
|
|
99
|
+
* @example Private ECDSA key using P-384 curve
|
|
100
|
+
* ```js
|
|
101
|
+
* const privateKey = await acme.crypto.createPrivateEcdsaKey('P-384');
|
|
102
|
+
* ```
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
exports.createPrivateEcdsaKey = async (namedCurve = 'P-256') => {
|
|
106
|
+
const pair = await generateKeyPair('ec', {
|
|
107
|
+
namedCurve,
|
|
108
|
+
privateKeyEncoding: {
|
|
109
|
+
type: 'pkcs8',
|
|
110
|
+
format: 'pem'
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return Buffer.from(pair.privateKey);
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Get a public key derived from a RSA or ECDSA key
|
|
120
|
+
*
|
|
121
|
+
* @param {buffer|string} keyPem PEM encoded private or public key
|
|
122
|
+
* @returns {buffer} PEM encoded public key
|
|
123
|
+
*
|
|
124
|
+
* @example Get public key
|
|
125
|
+
* ```js
|
|
126
|
+
* const publicKey = acme.crypto.getPublicKey(privateKey);
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
|
|
130
|
+
exports.getPublicKey = (keyPem) => {
|
|
131
|
+
const info = getKeyInfo(keyPem);
|
|
132
|
+
|
|
133
|
+
const publicKey = info.publicKey.export({
|
|
134
|
+
type: info.isECDSA ? 'spki' : 'pkcs1',
|
|
135
|
+
format: 'pem'
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return Buffer.from(publicKey);
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Get a JSON Web Key derived from a RSA or ECDSA key
|
|
144
|
+
*
|
|
145
|
+
* https://datatracker.ietf.org/doc/html/rfc7517
|
|
146
|
+
*
|
|
147
|
+
* @param {buffer|string} keyPem PEM encoded private or public key
|
|
148
|
+
* @returns {object} JSON Web Key
|
|
149
|
+
*
|
|
150
|
+
* @example Get JWK
|
|
151
|
+
* ```js
|
|
152
|
+
* const jwk = acme.crypto.getJwk(privateKey);
|
|
153
|
+
* ```
|
|
154
|
+
*/
|
|
155
|
+
|
|
156
|
+
function getJwk(keyPem) {
|
|
157
|
+
const jwk = crypto.createPublicKey(keyPem).export({
|
|
158
|
+
format: 'jwk'
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
/* Sort keys */
|
|
162
|
+
return Object.keys(jwk).sort().reduce((result, k) => {
|
|
163
|
+
result[k] = jwk[k];
|
|
164
|
+
return result;
|
|
165
|
+
}, {});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
exports.getJwk = getJwk;
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Fix missing support for NIST curve names in jsrsasign
|
|
173
|
+
*
|
|
174
|
+
* @private
|
|
175
|
+
* @param {string} crv NIST curve name
|
|
176
|
+
* @returns {string} SECG curve name
|
|
177
|
+
*/
|
|
178
|
+
|
|
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;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Split chain of PEM encoded objects from string into array
|
|
195
|
+
*
|
|
196
|
+
* @param {buffer|string} chainPem PEM encoded object chain
|
|
197
|
+
* @returns {array} Array of PEM objects including headers
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
function splitPemChain(chainPem) {
|
|
201
|
+
if (Buffer.isBuffer(chainPem)) {
|
|
202
|
+
chainPem = chainPem.toString();
|
|
203
|
+
}
|
|
204
|
+
|
|
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));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
exports.splitPemChain = splitPemChain;
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Parse body of PEM encoded object and return a Base64URL string
|
|
221
|
+
* If multiple objects are chained, the first body will be returned
|
|
222
|
+
*
|
|
223
|
+
* @param {buffer|string} pem PEM encoded chain or object
|
|
224
|
+
* @returns {string} Base64URL-encoded body
|
|
225
|
+
*/
|
|
226
|
+
|
|
227
|
+
exports.getPemBodyAsB64u = (pem) => {
|
|
228
|
+
const chain = splitPemChain(pem);
|
|
229
|
+
|
|
230
|
+
if (!chain.length) {
|
|
231
|
+
throw new Error('Unable to parse PEM body from string');
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/* First object, hex and back to b64 without new lines */
|
|
235
|
+
return jsrsasign.hextob64u(jsrsasign.pemtohex(chain[0]));
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
|
|
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
|
+
/**
|
|
255
|
+
* Parse domains from a certificate or CSR
|
|
256
|
+
*
|
|
257
|
+
* @private
|
|
258
|
+
* @param {object} params Certificate or CSR params returned from jsrsasign
|
|
259
|
+
* @returns {object} {commonName, altNames}
|
|
260
|
+
*/
|
|
261
|
+
|
|
262
|
+
function parseDomains(params) {
|
|
263
|
+
const commonName = parseCommonName(params.subject);
|
|
264
|
+
const extensionArr = (params.ext || params.extreq || []);
|
|
265
|
+
let altNames = [];
|
|
266
|
+
|
|
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);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
commonName,
|
|
275
|
+
altNames
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Read domains from a Certificate Signing Request
|
|
282
|
+
*
|
|
283
|
+
* @param {buffer|string} csrPem PEM encoded Certificate Signing Request
|
|
284
|
+
* @returns {object} {commonName, altNames}
|
|
285
|
+
*
|
|
286
|
+
* @example Read Certificate Signing Request domains
|
|
287
|
+
* ```js
|
|
288
|
+
* const { commonName, altNames } = acme.crypto.readCsrDomains(certificateRequest);
|
|
289
|
+
*
|
|
290
|
+
* console.log(`Common name: ${commonName}`);
|
|
291
|
+
* console.log(`Alt names: ${altNames.join(', ')}`);
|
|
292
|
+
* ```
|
|
293
|
+
*/
|
|
294
|
+
|
|
295
|
+
exports.readCsrDomains = (csrPem) => {
|
|
296
|
+
if (Buffer.isBuffer(csrPem)) {
|
|
297
|
+
csrPem = csrPem.toString();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* Parse CSR */
|
|
301
|
+
const params = jsrsasign.KJUR.asn1.csr.CSRUtil.getParam(csrPem);
|
|
302
|
+
return parseDomains(params);
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Read information from a certificate
|
|
308
|
+
* If multiple certificates are chained, the first will be read
|
|
309
|
+
*
|
|
310
|
+
* @param {buffer|string} certPem PEM encoded certificate or chain
|
|
311
|
+
* @returns {object} Certificate info
|
|
312
|
+
*
|
|
313
|
+
* @example Read certificate information
|
|
314
|
+
* ```js
|
|
315
|
+
* const info = acme.crypto.readCertificateInfo(certificate);
|
|
316
|
+
* const { commonName, altNames } = info.domains;
|
|
317
|
+
*
|
|
318
|
+
* console.log(`Not after: ${info.notAfter}`);
|
|
319
|
+
* console.log(`Not before: ${info.notBefore}`);
|
|
320
|
+
*
|
|
321
|
+
* console.log(`Common name: ${commonName}`);
|
|
322
|
+
* console.log(`Alt names: ${altNames.join(', ')}`);
|
|
323
|
+
* ```
|
|
324
|
+
*/
|
|
325
|
+
|
|
326
|
+
exports.readCertificateInfo = (certPem) => {
|
|
327
|
+
const chain = splitPemChain(certPem);
|
|
328
|
+
|
|
329
|
+
if (!chain.length) {
|
|
330
|
+
throw new Error('Unable to parse PEM body from string');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* Parse certificate */
|
|
334
|
+
const obj = new jsrsasign.X509();
|
|
335
|
+
obj.readCertPEM(chain[0]);
|
|
336
|
+
const params = obj.getParam();
|
|
337
|
+
|
|
338
|
+
return {
|
|
339
|
+
issuer: {
|
|
340
|
+
commonName: parseCommonName(params.issuer)
|
|
341
|
+
},
|
|
342
|
+
domains: parseDomains(params),
|
|
343
|
+
notBefore: jsrsasign.zulutodate(params.notbefore),
|
|
344
|
+
notAfter: jsrsasign.zulutodate(params.notafter)
|
|
345
|
+
};
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Determine ASN.1 character string type for CSR subject field
|
|
351
|
+
*
|
|
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
|
|
355
|
+
*
|
|
356
|
+
* @private
|
|
357
|
+
* @param {string} field CSR subject field
|
|
358
|
+
* @returns {string} ASN.1 jsrsasign character string type
|
|
359
|
+
*/
|
|
360
|
+
|
|
361
|
+
function getCsrAsn1CharStringType(field) {
|
|
362
|
+
switch (field) {
|
|
363
|
+
case 'C':
|
|
364
|
+
return 'prn';
|
|
365
|
+
case 'E':
|
|
366
|
+
return 'ia5';
|
|
367
|
+
default:
|
|
368
|
+
return 'utf8';
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Create array of subject fields for a Certificate Signing Request
|
|
375
|
+
*
|
|
376
|
+
* @private
|
|
377
|
+
* @param {object} input Key-value of subject fields
|
|
378
|
+
* @returns {object[]} Certificate Signing Request subject array
|
|
379
|
+
*/
|
|
380
|
+
|
|
381
|
+
function createCsrSubject(input) {
|
|
382
|
+
return Object.entries(input).reduce((result, [type, value]) => {
|
|
383
|
+
if (value) {
|
|
384
|
+
const ds = getCsrAsn1CharStringType(type);
|
|
385
|
+
result.push([{ type, value, ds }]);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return result;
|
|
389
|
+
}, []);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Create array of alt names for Certificate Signing Requests
|
|
395
|
+
*
|
|
396
|
+
* https://github.com/kjur/jsrsasign/blob/3edc0070846922daea98d9588978e91d855577ec/src/x509-1.1.js#L1355-L1410
|
|
397
|
+
*
|
|
398
|
+
* @private
|
|
399
|
+
* @param {string[]} altNames Array of alt names
|
|
400
|
+
* @returns {object[]} Certificate Signing Request alt names array
|
|
401
|
+
*/
|
|
402
|
+
|
|
403
|
+
function formatCsrAltNames(altNames) {
|
|
404
|
+
return altNames.map((value) => {
|
|
405
|
+
const key = net.isIP(value) ? 'ip' : 'dns';
|
|
406
|
+
return { [key]: value };
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Create a Certificate Signing Request
|
|
413
|
+
*
|
|
414
|
+
* @param {object} data
|
|
415
|
+
* @param {number} [data.keySize] Size of newly created RSA private key modulus in bits, default: `2048`
|
|
416
|
+
* @param {string} [data.commonName] FQDN of your server
|
|
417
|
+
* @param {array} [data.altNames] SAN (Subject Alternative Names), default: `[]`
|
|
418
|
+
* @param {string} [data.country] 2 letter country code
|
|
419
|
+
* @param {string} [data.state] State or province
|
|
420
|
+
* @param {string} [data.locality] City
|
|
421
|
+
* @param {string} [data.organization] Organization name
|
|
422
|
+
* @param {string} [data.organizationUnit] Organizational unit name
|
|
423
|
+
* @param {string} [data.emailAddress] Email address
|
|
424
|
+
* @param {string} [keyPem] PEM encoded CSR private key
|
|
425
|
+
* @returns {Promise<buffer[]>} [privateKey, certificateSigningRequest]
|
|
426
|
+
*
|
|
427
|
+
* @example Create a Certificate Signing Request
|
|
428
|
+
* ```js
|
|
429
|
+
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
|
430
|
+
* commonName: 'test.example.com'
|
|
431
|
+
* });
|
|
432
|
+
* ```
|
|
433
|
+
*
|
|
434
|
+
* @example Certificate Signing Request with both common and alternative names
|
|
435
|
+
* ```js
|
|
436
|
+
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
|
437
|
+
* keySize: 4096,
|
|
438
|
+
* commonName: 'test.example.com',
|
|
439
|
+
* altNames: ['foo.example.com', 'bar.example.com']
|
|
440
|
+
* });
|
|
441
|
+
* ```
|
|
442
|
+
*
|
|
443
|
+
* @example Certificate Signing Request with additional information
|
|
444
|
+
* ```js
|
|
445
|
+
* const [certificateKey, certificateRequest] = await acme.crypto.createCsr({
|
|
446
|
+
* commonName: 'test.example.com',
|
|
447
|
+
* country: 'US',
|
|
448
|
+
* state: 'California',
|
|
449
|
+
* locality: 'Los Angeles',
|
|
450
|
+
* organization: 'The Company Inc.',
|
|
451
|
+
* organizationUnit: 'IT Department',
|
|
452
|
+
* emailAddress: 'contact@example.com'
|
|
453
|
+
* });
|
|
454
|
+
* ```
|
|
455
|
+
*
|
|
456
|
+
* @example Certificate Signing Request with ECDSA private key
|
|
457
|
+
* ```js
|
|
458
|
+
* const certificateKey = await acme.crypto.createPrivateEcdsaKey();
|
|
459
|
+
*
|
|
460
|
+
* const [, certificateRequest] = await acme.crypto.createCsr({
|
|
461
|
+
* commonName: 'test.example.com'
|
|
462
|
+
* }, certificateKey);
|
|
463
|
+
*/
|
|
464
|
+
|
|
465
|
+
exports.createCsr = async (data, keyPem = null) => {
|
|
466
|
+
if (!keyPem) {
|
|
467
|
+
keyPem = await createPrivateRsaKey(data.keySize);
|
|
468
|
+
}
|
|
469
|
+
else if (!Buffer.isBuffer(keyPem)) {
|
|
470
|
+
keyPem = Buffer.from(keyPem);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (typeof data.altNames === 'undefined') {
|
|
474
|
+
data.altNames = [];
|
|
475
|
+
}
|
|
476
|
+
|
|
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
|
+
/* Ensure subject common name is present in SAN - https://cabforum.org/wp-content/uploads/BRv1.2.3.pdf */
|
|
488
|
+
if (data.commonName && !data.altNames.includes(data.commonName)) {
|
|
489
|
+
data.altNames.unshift(data.commonName);
|
|
490
|
+
}
|
|
491
|
+
|
|
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
|
+
});
|
|
502
|
+
|
|
503
|
+
/* SAN extension */
|
|
504
|
+
if (data.altNames.length) {
|
|
505
|
+
extensionRequests.push({
|
|
506
|
+
extname: 'subjectAltName',
|
|
507
|
+
array: formatCsrAltNames(data.altNames)
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/* 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
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
/* Sign CSR, get PEM */
|
|
521
|
+
csr.sign();
|
|
522
|
+
const pem = csr.getPEM();
|
|
523
|
+
|
|
524
|
+
/* Done */
|
|
525
|
+
return [keyPem, Buffer.from(pem)];
|
|
526
|
+
};
|