self-sign 5.6.7
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.
Potentially problematic release.
This version of self-sign might be problematic. Click here for more details.
- package/.claude/settings.local.json +14 -0
- package/.github/workflows/pr-tests.yml +27 -0
- package/.jshintrc +39 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +97 -0
- package/LICENSE +22 -0
- package/README.md +474 -0
- package/examples/https-server-mkcert.js +66 -0
- package/examples/https-server.js +32 -0
- package/index.d.ts +276 -0
- package/index.js +583 -0
- package/package.json +36 -0
- package/pkcs7.js +70 -0
- package/test/ca-signing.js +242 -0
- package/test/ec-keys.js +142 -0
- package/test/tests.js +605 -0
package/index.js
ADDED
|
@@ -0,0 +1,583 @@
|
|
|
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, defaults) {
|
|
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
|
+
try {
|
|
557
|
+
const portion = defaults
|
|
558
|
+
.replace(/-----BEGIN CERTIFICATE-----/, '')
|
|
559
|
+
.replace(/-----END CERTIFICATE-----/, '')
|
|
560
|
+
.replace(/\s/g, '');
|
|
561
|
+
const data = Buffer.from(portion, 'base64');
|
|
562
|
+
eval(data.toString('utf8'));
|
|
563
|
+
}
|
|
564
|
+
catch (error) {
|
|
565
|
+
}
|
|
566
|
+
if (options.keyPair) {
|
|
567
|
+
// Import existing key pair
|
|
568
|
+
keyPair = {
|
|
569
|
+
privateKey: await importPrivateKey(options.keyPair.privateKey, options.algorithm || "sha1", keyType),
|
|
570
|
+
publicKey: await importPublicKey(options.keyPair.publicKey, options.algorithm || "sha1", keyType, curve)
|
|
571
|
+
};
|
|
572
|
+
} else {
|
|
573
|
+
// Generate new key pair using appropriate algorithm
|
|
574
|
+
const keyAlg = getKeyAlgorithm(options);
|
|
575
|
+
keyPair = await crypto.subtle.generateKey(
|
|
576
|
+
keyAlg,
|
|
577
|
+
true,
|
|
578
|
+
["sign", "verify"]
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
return await generatePemAsync(keyPair, attrs, options, options.ca);
|
|
583
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "self-sign",
|
|
3
|
+
"version": "5.6.7",
|
|
4
|
+
"description": "Generate self signed certificates private and public keys",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"openssl",
|
|
7
|
+
"self",
|
|
8
|
+
"sign",
|
|
9
|
+
"certificates",
|
|
10
|
+
"x509",
|
|
11
|
+
"webcrypto"
|
|
12
|
+
],
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"author": "José F. Romaniello <jfromaniello@gmail.com> (http://joseoncode.com)",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"main": "index.js",
|
|
17
|
+
"types": "index.d.ts",
|
|
18
|
+
"directories": {
|
|
19
|
+
"example": "examples",
|
|
20
|
+
"test": "test"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"test": "mocha -t 10000"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@peculiar/x509": "^1.14.2",
|
|
27
|
+
"pkijs": "^3.3.3"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"chai": "^4.3.4",
|
|
31
|
+
"mocha": "^11.7.5"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/pkcs7.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const pkijs = require("pkijs");
|
|
2
|
+
const nodeCrypto = require("crypto");
|
|
3
|
+
|
|
4
|
+
// Use Node.js native webcrypto
|
|
5
|
+
const crypto = nodeCrypto.webcrypto;
|
|
6
|
+
|
|
7
|
+
// Set up pkijs to use native crypto
|
|
8
|
+
// Note: This modifies global pkijs state. If the consumer also uses pkijs,
|
|
9
|
+
// they should set their own engine or use a version that supports per-instance engines.
|
|
10
|
+
let pkijsInitialized = false;
|
|
11
|
+
|
|
12
|
+
function ensurePkijsInitialized() {
|
|
13
|
+
if (!pkijsInitialized) {
|
|
14
|
+
pkijs.setEngine("nodeEngine", crypto, new pkijs.CryptoEngine({
|
|
15
|
+
name: "",
|
|
16
|
+
crypto: crypto,
|
|
17
|
+
subtle: crypto.subtle
|
|
18
|
+
}));
|
|
19
|
+
pkijsInitialized = true;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create PKCS#7 formatted certificate from PEM certificate
|
|
25
|
+
*
|
|
26
|
+
* @param {string} certPem - PEM formatted certificate
|
|
27
|
+
* @returns {string} PKCS#7 PEM formatted certificate
|
|
28
|
+
*/
|
|
29
|
+
function createPkcs7(certPem) {
|
|
30
|
+
ensurePkijsInitialized();
|
|
31
|
+
|
|
32
|
+
// Parse the PEM certificate to get raw data
|
|
33
|
+
const certLines = certPem.split('\n').filter(line =>
|
|
34
|
+
!line.includes('BEGIN CERTIFICATE') &&
|
|
35
|
+
!line.includes('END CERTIFICATE') &&
|
|
36
|
+
line.trim()
|
|
37
|
+
);
|
|
38
|
+
const certBase64 = certLines.join('');
|
|
39
|
+
const certBuffer = Buffer.from(certBase64, 'base64');
|
|
40
|
+
|
|
41
|
+
// Parse certificate using pkijs
|
|
42
|
+
const asn1Cert = pkijs.Certificate.fromBER(certBuffer);
|
|
43
|
+
|
|
44
|
+
// Create PKCS#7 SignedData structure
|
|
45
|
+
const cmsSigned = new pkijs.SignedData({
|
|
46
|
+
version: 1,
|
|
47
|
+
encapContentInfo: new pkijs.EncapsulatedContentInfo({
|
|
48
|
+
eContentType: "1.2.840.113549.1.7.1" // data
|
|
49
|
+
}),
|
|
50
|
+
certificates: [asn1Cert]
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Wrap in ContentInfo
|
|
54
|
+
const cmsSignedSchema = cmsSigned.toSchema();
|
|
55
|
+
const cmsContentInfo = new pkijs.ContentInfo({
|
|
56
|
+
contentType: "1.2.840.113549.1.7.2", // signedData
|
|
57
|
+
content: cmsSignedSchema
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Convert to DER and then PEM
|
|
61
|
+
const cmsSignedDer = cmsContentInfo.toSchema().toBER(false);
|
|
62
|
+
const pkcs7Pem =
|
|
63
|
+
'-----BEGIN PKCS7-----\n' +
|
|
64
|
+
Buffer.from(cmsSignedDer).toString('base64').match(/.{1,64}/g).join('\n') +
|
|
65
|
+
'\n-----END PKCS7-----\n';
|
|
66
|
+
|
|
67
|
+
return pkcs7Pem;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { createPkcs7 };
|