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/test/tests.js
ADDED
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
var { assert } = require('chai');
|
|
2
|
+
var fs = require('fs');
|
|
3
|
+
var { promisify } = require('util');
|
|
4
|
+
var exec = promisify(require('child_process').exec);
|
|
5
|
+
var crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
describe('generate', function () {
|
|
8
|
+
|
|
9
|
+
var generate = require('../index').generate;
|
|
10
|
+
var { createPkcs7 } = require('../pkcs7');
|
|
11
|
+
|
|
12
|
+
it('should work without attrs/options', async function () {
|
|
13
|
+
var pems = await generate();
|
|
14
|
+
assert.ok(!!pems.private, 'has a private key');
|
|
15
|
+
assert.ok(!!pems.fingerprint, 'has fingerprint');
|
|
16
|
+
assert.ok(!!pems.public, 'has a public key');
|
|
17
|
+
assert.ok(!!pems.cert, 'has a certificate');
|
|
18
|
+
assert.ok(!pems.pkcs7, 'should not include a pkcs7 by default');
|
|
19
|
+
assert.ok(!pems.clientcert, 'should not include a client cert by default');
|
|
20
|
+
assert.ok(!pems.clientprivate, 'should not include a client private key by default');
|
|
21
|
+
assert.ok(!pems.clientpublic, 'should not include a client public key by default');
|
|
22
|
+
|
|
23
|
+
// Verify cert can be read by Node.js crypto
|
|
24
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
25
|
+
assert.ok(cert.subject, 'cert has a subject');
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('should generate client cert', async function () {
|
|
29
|
+
var pems = await generate(null, {clientCertificate: true},null);
|
|
30
|
+
|
|
31
|
+
assert.ok(!!pems.clientcert, 'should include a client cert when requested');
|
|
32
|
+
assert.ok(!!pems.clientprivate, 'should include a client private key when requested');
|
|
33
|
+
assert.ok(!!pems.clientpublic, 'should include a client public key when requested');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('should include pkcs7', async function () {
|
|
37
|
+
var pems = await generate([{ name: 'commonName', value: 'contoso.com' }]);
|
|
38
|
+
var pkcs7 = createPkcs7(pems.cert);
|
|
39
|
+
|
|
40
|
+
assert.ok(!!pkcs7, 'has a pkcs7');
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
fs.unlinkSync('/tmp/tmp.pkcs7');
|
|
44
|
+
} catch (er) {}
|
|
45
|
+
|
|
46
|
+
fs.writeFileSync('/tmp/tmp.pkcs7', pkcs7);
|
|
47
|
+
|
|
48
|
+
const { stdout, stderr } = await exec('openssl pkcs7 -print_certs -in /tmp/tmp.pkcs7');
|
|
49
|
+
|
|
50
|
+
if (stderr && stderr.length) {
|
|
51
|
+
throw new Error(stderr);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const expected = stdout.toString();
|
|
55
|
+
let [ subjectLine,issuerLine, ...cert ] = expected.split(/\r?\n/).filter(c => c);
|
|
56
|
+
cert = cert.filter(c => c);
|
|
57
|
+
assert.match(subjectLine, /subject=\/?CN\s?=\s?contoso.com/i);
|
|
58
|
+
assert.match(issuerLine, /issuer=\/?CN\s?=\s?contoso.com/i);
|
|
59
|
+
// Normalize line endings for comparison
|
|
60
|
+
const normalizedPemCert = pems.cert.replace(/\r\n/g, '\n').trim();
|
|
61
|
+
const normalizedExpected = cert.join('\n').trim();
|
|
62
|
+
assert.strictEqual(
|
|
63
|
+
normalizedPemCert,
|
|
64
|
+
normalizedExpected
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should support sha1 algorithm', async function () {
|
|
69
|
+
var pems_sha1 = await generate(null, { algorithm: 'sha1' },null);
|
|
70
|
+
const cert = new crypto.X509Certificate(pems_sha1.cert);
|
|
71
|
+
// SHA-1 with RSA signature
|
|
72
|
+
assert.ok(cert.publicKey, 'can generate sha1 certs');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should support sha256 algorithm', async function () {
|
|
76
|
+
var pems_sha256 = await generate(null, { algorithm: 'sha256' },null);
|
|
77
|
+
const cert = new crypto.X509Certificate(pems_sha256.cert);
|
|
78
|
+
// SHA-256 with RSA signature
|
|
79
|
+
assert.ok(cert.publicKey, 'can generate sha256 certs');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should default to 2048 bit keysize', async function () {
|
|
83
|
+
var pems = await generate();
|
|
84
|
+
const privateKey = crypto.createPrivateKey(pems.private);
|
|
85
|
+
const keyDetails = privateKey.asymmetricKeyDetails;
|
|
86
|
+
assert.strictEqual(keyDetails.modulusLength, 2048, 'default key size should be 2048 bits');
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('should default to 2048 bit keysize for client certificate', async function () {
|
|
90
|
+
var pems = await generate(null, {clientCertificate: true},null);
|
|
91
|
+
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
|
|
92
|
+
const keyDetails = clientPrivateKey.asymmetricKeyDetails;
|
|
93
|
+
assert.strictEqual(keyDetails.modulusLength, 2048, 'default client key size should be 2048 bits');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should support custom keySize', async function () {
|
|
97
|
+
var pems = await generate(null, { keySize: 4096 },null);
|
|
98
|
+
const privateKey = crypto.createPrivateKey(pems.private);
|
|
99
|
+
const keyDetails = privateKey.asymmetricKeyDetails;
|
|
100
|
+
assert.strictEqual(keyDetails.modulusLength, 4096, 'should support custom key size');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should support custom clientCertificateKeySize', async function () {
|
|
104
|
+
var pems = await generate(null, {
|
|
105
|
+
clientCertificate: true,
|
|
106
|
+
clientCertificateKeySize: 4096
|
|
107
|
+
},null);
|
|
108
|
+
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
|
|
109
|
+
const keyDetails = clientPrivateKey.asymmetricKeyDetails;
|
|
110
|
+
assert.strictEqual(keyDetails.modulusLength, 4096, 'should support custom client key size');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should support sha384 algorithm', async function () {
|
|
114
|
+
var pems = await generate(null, { algorithm: 'sha384' },null);
|
|
115
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
116
|
+
assert.ok(cert.publicKey, 'can generate sha384 certs');
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should support sha512 algorithm', async function () {
|
|
120
|
+
var pems = await generate(null, { algorithm: 'sha512' },null);
|
|
121
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
122
|
+
assert.ok(cert.publicKey, 'can generate sha512 certs');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should default to 365 days validity', async function () {
|
|
126
|
+
var pems = await generate();
|
|
127
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
128
|
+
|
|
129
|
+
const validFrom = new Date(cert.validFrom);
|
|
130
|
+
const validTo = new Date(cert.validTo);
|
|
131
|
+
const diffTime = Math.abs(validTo - validFrom);
|
|
132
|
+
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
133
|
+
|
|
134
|
+
assert.approximately(diffDays, 365, 1, 'certificate should default to 365 days validity');
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('should respect notBeforeDate option', async function () {
|
|
138
|
+
const customDate = new Date('2025-01-01T00:00:00Z');
|
|
139
|
+
var pems = await generate(null, { notBeforeDate: customDate },null);
|
|
140
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
141
|
+
|
|
142
|
+
const validFrom = new Date(cert.validFrom);
|
|
143
|
+
// Allow small difference for processing time
|
|
144
|
+
assert.approximately(validFrom.getTime(), customDate.getTime(), 5000, 'should use custom notBeforeDate');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should respect notAfterDate option', async function () {
|
|
148
|
+
const notBefore = new Date('2025-01-01T00:00:00Z');
|
|
149
|
+
const notAfter = new Date('2025-02-15T00:00:00Z');
|
|
150
|
+
var pems = await generate(null, { notBeforeDate: notBefore, notAfterDate: notAfter },null);
|
|
151
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
152
|
+
|
|
153
|
+
const validFrom = new Date(cert.validFrom);
|
|
154
|
+
const validTo = new Date(cert.validTo);
|
|
155
|
+
|
|
156
|
+
assert.approximately(validFrom.getTime(), notBefore.getTime(), 5000, 'should use custom notBeforeDate');
|
|
157
|
+
assert.approximately(validTo.getTime(), notAfter.getTime(), 5000, 'should use custom notAfterDate');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should generate valid fingerprint format', async function () {
|
|
161
|
+
var pems = await generate();
|
|
162
|
+
assert.match(pems.fingerprint, /^[0-9a-f]{2}(:[0-9a-f]{2}){19}$/i, 'fingerprint should be valid SHA-1 format');
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should support custom attributes', async function () {
|
|
166
|
+
const attrs = [
|
|
167
|
+
{ name: 'commonName', value: 'test.example.com' },
|
|
168
|
+
{ name: 'countryName', value: 'GB' },
|
|
169
|
+
{ shortName: 'ST', value: 'London' },
|
|
170
|
+
{ name: 'localityName', value: 'Westminster' },
|
|
171
|
+
{ name: 'organizationName', value: 'Test Corp' },
|
|
172
|
+
{ shortName: 'OU', value: 'Engineering' }
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
var pems = await generate(attrs);
|
|
176
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
177
|
+
|
|
178
|
+
assert.include(cert.subject, 'CN=test.example.com', 'should include custom CN');
|
|
179
|
+
assert.include(cert.subject, 'C=GB', 'should include custom country');
|
|
180
|
+
assert.include(cert.subject, 'O=Test Corp', 'should include custom organization');
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('should support custom clientCertificateCN (deprecated)', async function () {
|
|
184
|
+
var pems = await generate(null, {
|
|
185
|
+
clientCertificate: true,
|
|
186
|
+
clientCertificateCN: 'Custom User CN'
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
const clientCert = new crypto.X509Certificate(pems.clientcert);
|
|
190
|
+
assert.include(clientCert.subject, 'CN=Custom User CN', 'should use custom client CN');
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should support clientCertificate as options object with cn', async function () {
|
|
194
|
+
var pems = await generate(null, {
|
|
195
|
+
clientCertificate: {
|
|
196
|
+
cn: 'Client Options CN'
|
|
197
|
+
}
|
|
198
|
+
},null);
|
|
199
|
+
|
|
200
|
+
const clientCert = new crypto.X509Certificate(pems.clientcert);
|
|
201
|
+
assert.include(clientCert.subject, 'CN=Client Options CN', 'should use cn from clientCertificate options');
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should support clientCertificate.keySize', async function () {
|
|
205
|
+
var pems = await generate(null, {
|
|
206
|
+
clientCertificate: {
|
|
207
|
+
keySize: 4096
|
|
208
|
+
}
|
|
209
|
+
},null);
|
|
210
|
+
|
|
211
|
+
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
|
|
212
|
+
const keyDetails = clientPrivateKey.asymmetricKeyDetails;
|
|
213
|
+
assert.strictEqual(keyDetails.modulusLength, 4096, 'should use keySize from clientCertificate options');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should support clientCertificate.notBeforeDate and notAfterDate', async function () {
|
|
217
|
+
const notBefore = new Date('2025-06-01T00:00:00Z');
|
|
218
|
+
const notAfter = new Date('2025-06-30T00:00:00Z');
|
|
219
|
+
|
|
220
|
+
var pems = await generate(null, {
|
|
221
|
+
clientCertificate: {
|
|
222
|
+
notBeforeDate: notBefore,
|
|
223
|
+
notAfterDate: notAfter
|
|
224
|
+
}
|
|
225
|
+
},null);
|
|
226
|
+
|
|
227
|
+
const clientCert = new crypto.X509Certificate(pems.clientcert);
|
|
228
|
+
const validFrom = new Date(clientCert.validFrom);
|
|
229
|
+
const validTo = new Date(clientCert.validTo);
|
|
230
|
+
|
|
231
|
+
assert.approximately(validFrom.getTime(), notBefore.getTime(), 5000, 'should use notBeforeDate from clientCertificate options');
|
|
232
|
+
assert.approximately(validTo.getTime(), notAfter.getTime(), 5000, 'should use notAfterDate from clientCertificate options');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should support clientCertificate.algorithm', async function () {
|
|
236
|
+
var pems = await generate(null, {
|
|
237
|
+
algorithm: 'sha1', // main cert uses sha1
|
|
238
|
+
clientCertificate: {
|
|
239
|
+
algorithm: 'sha256' // client cert uses sha256
|
|
240
|
+
}
|
|
241
|
+
},null);
|
|
242
|
+
|
|
243
|
+
// Both certs should be valid
|
|
244
|
+
const serverCert = new crypto.X509Certificate(pems.cert);
|
|
245
|
+
const clientCert = new crypto.X509Certificate(pems.clientcert);
|
|
246
|
+
assert.ok(serverCert.publicKey, 'server cert should be valid');
|
|
247
|
+
assert.ok(clientCert.publicKey, 'client cert should be valid');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('clientCertificate options should take precedence over deprecated options', async function () {
|
|
251
|
+
var pems = await generate(null, {
|
|
252
|
+
clientCertificateCN: 'Deprecated CN',
|
|
253
|
+
clientCertificateKeySize: 2048,
|
|
254
|
+
clientCertificate: {
|
|
255
|
+
cn: 'New Options CN',
|
|
256
|
+
keySize: 4096
|
|
257
|
+
}
|
|
258
|
+
},null);
|
|
259
|
+
|
|
260
|
+
const clientCert = new crypto.X509Certificate(pems.clientcert);
|
|
261
|
+
assert.include(clientCert.subject, 'CN=New Options CN', 'clientCertificate.cn should take precedence');
|
|
262
|
+
|
|
263
|
+
const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
|
|
264
|
+
const keyDetails = clientPrivateKey.asymmetricKeyDetails;
|
|
265
|
+
assert.strictEqual(keyDetails.modulusLength, 4096, 'clientCertificate.keySize should take precedence');
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should generate valid key pair that work together', async function () {
|
|
269
|
+
var pems = await generate();
|
|
270
|
+
|
|
271
|
+
// Test data
|
|
272
|
+
const testData = 'Hello, World!';
|
|
273
|
+
|
|
274
|
+
// Create sign and verify objects
|
|
275
|
+
const privateKey = crypto.createPrivateKey(pems.private);
|
|
276
|
+
const publicKey = crypto.createPublicKey(pems.public);
|
|
277
|
+
|
|
278
|
+
// Sign with private key
|
|
279
|
+
const sign = crypto.createSign('SHA256');
|
|
280
|
+
sign.update(testData);
|
|
281
|
+
sign.end();
|
|
282
|
+
const signature = sign.sign(privateKey);
|
|
283
|
+
|
|
284
|
+
// Verify with public key
|
|
285
|
+
const verify = crypto.createVerify('SHA256');
|
|
286
|
+
verify.update(testData);
|
|
287
|
+
verify.end();
|
|
288
|
+
const isValid = verify.verify(publicKey, signature);
|
|
289
|
+
|
|
290
|
+
assert.isTrue(isValid, 'public key should verify signature from private key');
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should create client cert signed by server cert', async function () {
|
|
294
|
+
var pems = await generate(null, { clientCertificate: true },null);
|
|
295
|
+
|
|
296
|
+
const serverCert = new crypto.X509Certificate(pems.cert);
|
|
297
|
+
const clientCert = new crypto.X509Certificate(pems.clientcert);
|
|
298
|
+
|
|
299
|
+
// Client cert should have different subject than server
|
|
300
|
+
assert.notEqual(clientCert.subject, serverCert.subject, 'client and server should have different subjects');
|
|
301
|
+
|
|
302
|
+
// Both certs should be valid
|
|
303
|
+
assert.ok(serverCert.publicKey, 'server cert should be valid');
|
|
304
|
+
assert.ok(clientCert.publicKey, 'client cert should be valid');
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should support using existing keyPair', async function () {
|
|
308
|
+
// First generate a key pair
|
|
309
|
+
const firstPems = await generate();
|
|
310
|
+
|
|
311
|
+
// Reuse the key pair
|
|
312
|
+
const secondPems = await generate(null, {
|
|
313
|
+
keyPair: {
|
|
314
|
+
privateKey: firstPems.private,
|
|
315
|
+
publicKey: firstPems.public
|
|
316
|
+
}
|
|
317
|
+
},null);
|
|
318
|
+
|
|
319
|
+
// Keys should be identical
|
|
320
|
+
assert.strictEqual(firstPems.private, secondPems.private, 'should use provided private key');
|
|
321
|
+
assert.strictEqual(firstPems.public, secondPems.public, 'should use provided public key');
|
|
322
|
+
|
|
323
|
+
// Certificates will be different (different serial, dates) but keys are same
|
|
324
|
+
const firstCert = new crypto.X509Certificate(firstPems.cert);
|
|
325
|
+
const secondCert = new crypto.X509Certificate(secondPems.cert);
|
|
326
|
+
assert.strictEqual(firstCert.publicKey.export({ format: 'pem', type: 'spki' }),
|
|
327
|
+
secondCert.publicKey.export({ format: 'pem', type: 'spki' }),
|
|
328
|
+
'certificates should contain the same public key');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('should create PKCS#7 for client certificate', async function () {
|
|
332
|
+
var pems = await generate([{ name: 'commonName', value: 'server.example.com' }], {
|
|
333
|
+
clientCertificate: true
|
|
334
|
+
},null);
|
|
335
|
+
|
|
336
|
+
var clientPkcs7 = createPkcs7(pems.clientcert);
|
|
337
|
+
assert.ok(!!clientPkcs7, 'should create PKCS#7 for client cert');
|
|
338
|
+
assert.include(clientPkcs7, 'BEGIN PKCS7', 'should be valid PKCS#7 format');
|
|
339
|
+
|
|
340
|
+
// Verify with openssl
|
|
341
|
+
try {
|
|
342
|
+
fs.unlinkSync('/tmp/tmp-client.pkcs7');
|
|
343
|
+
} catch (er) {}
|
|
344
|
+
|
|
345
|
+
fs.writeFileSync('/tmp/tmp-client.pkcs7', clientPkcs7);
|
|
346
|
+
const { stdout, stderr } = await exec('openssl pkcs7 -print_certs -in /tmp/tmp-client.pkcs7');
|
|
347
|
+
|
|
348
|
+
if (stderr && stderr.length) {
|
|
349
|
+
throw new Error(stderr);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
assert.ok(stdout.toString().length > 0, 'openssl should be able to read client PKCS#7');
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should generate unique serial numbers', async function () {
|
|
356
|
+
const pems1 = await generate();
|
|
357
|
+
const pems2 = await generate();
|
|
358
|
+
|
|
359
|
+
const cert1 = new crypto.X509Certificate(pems1.cert);
|
|
360
|
+
const cert2 = new crypto.X509Certificate(pems2.cert);
|
|
361
|
+
|
|
362
|
+
assert.notEqual(cert1.serialNumber, cert2.serialNumber, 'serial numbers should be unique');
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should handle minimal attributes', async function () {
|
|
366
|
+
const attrs = [{ name: 'commonName', value: 'minimal.test' }];
|
|
367
|
+
var pems = await generate(attrs);
|
|
368
|
+
|
|
369
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
370
|
+
assert.include(cert.subject, 'CN=minimal.test', 'should work with minimal attributes');
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
describe('extensions', function () {
|
|
374
|
+
it('should support custom subjectAltName with IPv6 (issue #79)', async function () {
|
|
375
|
+
var pems = await generate(
|
|
376
|
+
[{ name: 'commonName', value: 'localhost' }],
|
|
377
|
+
{
|
|
378
|
+
algorithm: 'sha256',
|
|
379
|
+
extensions: [
|
|
380
|
+
{
|
|
381
|
+
name: 'basicConstraints',
|
|
382
|
+
cA: false
|
|
383
|
+
},
|
|
384
|
+
{
|
|
385
|
+
name: 'keyUsage',
|
|
386
|
+
digitalSignature: true,
|
|
387
|
+
keyEncipherment: true
|
|
388
|
+
},
|
|
389
|
+
{
|
|
390
|
+
name: 'subjectAltName',
|
|
391
|
+
altNames: [
|
|
392
|
+
{ type: 2, value: 'localhost' }, // DNS
|
|
393
|
+
{ type: 7, ip: '127.0.0.1' }, // IPv4
|
|
394
|
+
{ type: 7, ip: '::1' } // IPv6
|
|
395
|
+
]
|
|
396
|
+
}
|
|
397
|
+
]
|
|
398
|
+
}
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
402
|
+
assert.ok(cert.subjectAltName, 'should have subjectAltName');
|
|
403
|
+
assert.include(cert.subjectAltName, 'localhost', 'should include DNS name');
|
|
404
|
+
assert.include(cert.subjectAltName, '127.0.0.1', 'should include IPv4');
|
|
405
|
+
// IPv6 ::1 may be expanded to full form 0:0:0:0:0:0:0:1
|
|
406
|
+
const hasIPv6 = cert.subjectAltName.includes('::1') || cert.subjectAltName.includes('0:0:0:0:0:0:0:1');
|
|
407
|
+
assert.ok(hasIPv6, 'should include IPv6');
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it('should support basicConstraints with cA=true', async function () {
|
|
411
|
+
var pems = await generate(
|
|
412
|
+
[{ name: 'commonName', value: 'Test CA' }],
|
|
413
|
+
{
|
|
414
|
+
extensions: [
|
|
415
|
+
{
|
|
416
|
+
name: 'basicConstraints',
|
|
417
|
+
cA: true,
|
|
418
|
+
critical: true
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
name: 'keyUsage',
|
|
422
|
+
keyCertSign: true,
|
|
423
|
+
cRLSign: true,
|
|
424
|
+
critical: true
|
|
425
|
+
}
|
|
426
|
+
]
|
|
427
|
+
},null
|
|
428
|
+
);
|
|
429
|
+
|
|
430
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
431
|
+
assert.ok(cert.ca, 'certificate should be a CA');
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it('should support keyUsage extension', async function () {
|
|
435
|
+
var pems = await generate(
|
|
436
|
+
[{ name: 'commonName', value: 'test.example.com' }],
|
|
437
|
+
{
|
|
438
|
+
extensions: [
|
|
439
|
+
{
|
|
440
|
+
name: 'basicConstraints',
|
|
441
|
+
cA: false
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
name: 'keyUsage',
|
|
445
|
+
digitalSignature: true,
|
|
446
|
+
keyEncipherment: true,
|
|
447
|
+
dataEncipherment: true
|
|
448
|
+
}
|
|
449
|
+
]
|
|
450
|
+
},null
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
454
|
+
// Node.js X509Certificate doesn't expose keyUsage directly,
|
|
455
|
+
// but we can verify the cert is valid and can be used
|
|
456
|
+
assert.ok(cert.publicKey, 'should generate valid cert with keyUsage');
|
|
457
|
+
// Verify by using openssl to check extensions
|
|
458
|
+
const fs = require('fs');
|
|
459
|
+
fs.writeFileSync('/tmp/test-keyusage.crt', pems.cert);
|
|
460
|
+
const { execSync } = require('child_process');
|
|
461
|
+
const output = execSync('openssl x509 -in /tmp/test-keyusage.crt -text -noout').toString();
|
|
462
|
+
assert.include(output, 'Digital Signature', 'should have digitalSignature');
|
|
463
|
+
assert.include(output, 'Key Encipherment', 'should have keyEncipherment');
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
it('should support extKeyUsage extension', async function () {
|
|
467
|
+
var pems = await generate(
|
|
468
|
+
[{ name: 'commonName', value: 'test.example.com' }],
|
|
469
|
+
{
|
|
470
|
+
extensions: [
|
|
471
|
+
{
|
|
472
|
+
name: 'basicConstraints',
|
|
473
|
+
cA: false
|
|
474
|
+
},
|
|
475
|
+
{
|
|
476
|
+
name: 'extKeyUsage',
|
|
477
|
+
serverAuth: true,
|
|
478
|
+
clientAuth: true,
|
|
479
|
+
codeSigning: true
|
|
480
|
+
}
|
|
481
|
+
]
|
|
482
|
+
},null
|
|
483
|
+
);
|
|
484
|
+
|
|
485
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
486
|
+
// Node.js crypto doesn't expose extended key usage directly, but cert should be valid
|
|
487
|
+
assert.ok(cert.publicKey, 'should generate valid cert with extKeyUsage');
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('should support subjectAltName with DNS names', async function () {
|
|
491
|
+
var pems = await generate(
|
|
492
|
+
[{ name: 'commonName', value: 'example.com' }],
|
|
493
|
+
{
|
|
494
|
+
extensions: [
|
|
495
|
+
{
|
|
496
|
+
name: 'basicConstraints',
|
|
497
|
+
cA: false
|
|
498
|
+
},
|
|
499
|
+
{
|
|
500
|
+
name: 'subjectAltName',
|
|
501
|
+
altNames: [
|
|
502
|
+
{ type: 2, value: 'example.com' },
|
|
503
|
+
{ type: 2, value: 'www.example.com' },
|
|
504
|
+
{ type: 2, value: '*.example.com' }
|
|
505
|
+
]
|
|
506
|
+
}
|
|
507
|
+
]
|
|
508
|
+
},null
|
|
509
|
+
);
|
|
510
|
+
|
|
511
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
512
|
+
assert.include(cert.subjectAltName, 'example.com', 'should include example.com');
|
|
513
|
+
assert.include(cert.subjectAltName, 'www.example.com', 'should include www.example.com');
|
|
514
|
+
assert.include(cert.subjectAltName, '*.example.com', 'should include wildcard');
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
it('should support subjectAltName with email and URI', async function () {
|
|
518
|
+
var pems = await generate(
|
|
519
|
+
[{ name: 'commonName', value: 'test.example.com' }],
|
|
520
|
+
{
|
|
521
|
+
extensions: [
|
|
522
|
+
{
|
|
523
|
+
name: 'basicConstraints',
|
|
524
|
+
cA: false
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: 'subjectAltName',
|
|
528
|
+
altNames: [
|
|
529
|
+
{ type: 2, value: 'test.example.com' },
|
|
530
|
+
{ type: 1, value: 'admin@example.com' }, // email
|
|
531
|
+
{ type: 6, value: 'http://example.com/webid#me' } // URI
|
|
532
|
+
]
|
|
533
|
+
}
|
|
534
|
+
]
|
|
535
|
+
},null
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
539
|
+
assert.include(cert.subjectAltName, 'test.example.com', 'should include DNS');
|
|
540
|
+
assert.include(cert.subjectAltName, 'admin@example.com', 'should include email');
|
|
541
|
+
assert.include(cert.subjectAltName, 'http://example.com/webid#me', 'should include URI');
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
it('should use default extensions when extensions option is empty array', async function () {
|
|
545
|
+
var pems = await generate(
|
|
546
|
+
[{ name: 'commonName', value: 'localhost' }],
|
|
547
|
+
{
|
|
548
|
+
extensions: []
|
|
549
|
+
},null
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
553
|
+
// Default behavior includes localhost and 127.0.0.1
|
|
554
|
+
assert.include(cert.subjectAltName, 'localhost', 'should use default SAN');
|
|
555
|
+
assert.include(cert.subjectAltName, '127.0.0.1', 'should include default IP for localhost');
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
it('should use default extensions when extensions option is not provided', async function () {
|
|
559
|
+
var pems = await generate([{ name: 'commonName', value: 'myhost.local' }]);
|
|
560
|
+
|
|
561
|
+
const cert = new crypto.X509Certificate(pems.cert);
|
|
562
|
+
assert.include(cert.subjectAltName, 'myhost.local', 'should use commonName in default SAN');
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
it('should support passphrase for private key encryption', async function () {
|
|
567
|
+
const passphrase = 'my-secret-passphrase';
|
|
568
|
+
var pems = await generate(null, { passphrase: passphrase },null);
|
|
569
|
+
|
|
570
|
+
assert.ok(!!pems.private, 'has a private key');
|
|
571
|
+
assert.include(pems.private, 'ENCRYPTED', 'private key should be encrypted');
|
|
572
|
+
|
|
573
|
+
// Verify the key can be decrypted with the correct passphrase
|
|
574
|
+
const privateKey = crypto.createPrivateKey({
|
|
575
|
+
key: pems.private,
|
|
576
|
+
passphrase: passphrase
|
|
577
|
+
});
|
|
578
|
+
assert.ok(privateKey, 'should be able to decrypt private key with passphrase');
|
|
579
|
+
|
|
580
|
+
// Verify signing works with decrypted key
|
|
581
|
+
const testData = 'Hello, World!';
|
|
582
|
+
const sign = crypto.createSign('SHA256');
|
|
583
|
+
sign.update(testData);
|
|
584
|
+
sign.end();
|
|
585
|
+
const signature = sign.sign({ key: pems.private, passphrase: passphrase });
|
|
586
|
+
|
|
587
|
+
const verify = crypto.createVerify('SHA256');
|
|
588
|
+
verify.update(testData);
|
|
589
|
+
verify.end();
|
|
590
|
+
const isValid = verify.verify(pems.public, signature);
|
|
591
|
+
assert.isTrue(isValid, 'encrypted key should work for signing');
|
|
592
|
+
});
|
|
593
|
+
|
|
594
|
+
it('should fail to decrypt private key with wrong passphrase', async function () {
|
|
595
|
+
const passphrase = 'correct-passphrase';
|
|
596
|
+
var pems = await generate(null, { passphrase: passphrase },null);
|
|
597
|
+
|
|
598
|
+
assert.throws(() => {
|
|
599
|
+
crypto.createPrivateKey({
|
|
600
|
+
key: pems.private,
|
|
601
|
+
passphrase: 'wrong-passphrase'
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
});
|
|
605
|
+
});
|