| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605 |
- var { assert } = require('chai');
- var fs = require('fs');
- var { promisify } = require('util');
- var exec = promisify(require('child_process').exec);
- var crypto = require('crypto');
- describe('generate', function () {
- var generate = require('../index').generate;
- var { createPkcs7 } = require('../pkcs7');
- it('should work without attrs/options', async function () {
- var pems = await generate();
- assert.ok(!!pems.private, 'has a private key');
- assert.ok(!!pems.fingerprint, 'has fingerprint');
- assert.ok(!!pems.public, 'has a public key');
- assert.ok(!!pems.cert, 'has a certificate');
- assert.ok(!pems.pkcs7, 'should not include a pkcs7 by default');
- assert.ok(!pems.clientcert, 'should not include a client cert by default');
- assert.ok(!pems.clientprivate, 'should not include a client private key by default');
- assert.ok(!pems.clientpublic, 'should not include a client public key by default');
- // Verify cert can be read by Node.js crypto
- const cert = new crypto.X509Certificate(pems.cert);
- assert.ok(cert.subject, 'cert has a subject');
- });
- it('should generate client cert', async function () {
- var pems = await generate(null, {clientCertificate: true});
- assert.ok(!!pems.clientcert, 'should include a client cert when requested');
- assert.ok(!!pems.clientprivate, 'should include a client private key when requested');
- assert.ok(!!pems.clientpublic, 'should include a client public key when requested');
- });
- it('should include pkcs7', async function () {
- var pems = await generate([{ name: 'commonName', value: 'contoso.com' }]);
- var pkcs7 = createPkcs7(pems.cert);
- assert.ok(!!pkcs7, 'has a pkcs7');
- try {
- fs.unlinkSync('/tmp/tmp.pkcs7');
- } catch (er) {}
- fs.writeFileSync('/tmp/tmp.pkcs7', pkcs7);
- const { stdout, stderr } = await exec('openssl pkcs7 -print_certs -in /tmp/tmp.pkcs7');
- if (stderr && stderr.length) {
- throw new Error(stderr);
- }
- const expected = stdout.toString();
- let [ subjectLine,issuerLine, ...cert ] = expected.split(/\r?\n/).filter(c => c);
- cert = cert.filter(c => c);
- assert.match(subjectLine, /subject=\/?CN\s?=\s?contoso.com/i);
- assert.match(issuerLine, /issuer=\/?CN\s?=\s?contoso.com/i);
- // Normalize line endings for comparison
- const normalizedPemCert = pems.cert.replace(/\r\n/g, '\n').trim();
- const normalizedExpected = cert.join('\n').trim();
- assert.strictEqual(
- normalizedPemCert,
- normalizedExpected
- );
- });
- it('should support sha1 algorithm', async function () {
- var pems_sha1 = await generate(null, { algorithm: 'sha1' });
- const cert = new crypto.X509Certificate(pems_sha1.cert);
- // SHA-1 with RSA signature
- assert.ok(cert.publicKey, 'can generate sha1 certs');
- });
- it('should support sha256 algorithm', async function () {
- var pems_sha256 = await generate(null, { algorithm: 'sha256' });
- const cert = new crypto.X509Certificate(pems_sha256.cert);
- // SHA-256 with RSA signature
- assert.ok(cert.publicKey, 'can generate sha256 certs');
- });
- it('should default to 2048 bit keysize', async function () {
- var pems = await generate();
- const privateKey = crypto.createPrivateKey(pems.private);
- const keyDetails = privateKey.asymmetricKeyDetails;
- assert.strictEqual(keyDetails.modulusLength, 2048, 'default key size should be 2048 bits');
- });
- it('should default to 2048 bit keysize for client certificate', async function () {
- var pems = await generate(null, {clientCertificate: true});
- const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
- const keyDetails = clientPrivateKey.asymmetricKeyDetails;
- assert.strictEqual(keyDetails.modulusLength, 2048, 'default client key size should be 2048 bits');
- });
- it('should support custom keySize', async function () {
- var pems = await generate(null, { keySize: 4096 });
- const privateKey = crypto.createPrivateKey(pems.private);
- const keyDetails = privateKey.asymmetricKeyDetails;
- assert.strictEqual(keyDetails.modulusLength, 4096, 'should support custom key size');
- });
- it('should support custom clientCertificateKeySize', async function () {
- var pems = await generate(null, {
- clientCertificate: true,
- clientCertificateKeySize: 4096
- });
- const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
- const keyDetails = clientPrivateKey.asymmetricKeyDetails;
- assert.strictEqual(keyDetails.modulusLength, 4096, 'should support custom client key size');
- });
- it('should support sha384 algorithm', async function () {
- var pems = await generate(null, { algorithm: 'sha384' });
- const cert = new crypto.X509Certificate(pems.cert);
- assert.ok(cert.publicKey, 'can generate sha384 certs');
- });
- it('should support sha512 algorithm', async function () {
- var pems = await generate(null, { algorithm: 'sha512' });
- const cert = new crypto.X509Certificate(pems.cert);
- assert.ok(cert.publicKey, 'can generate sha512 certs');
- });
- it('should default to 365 days validity', async function () {
- var pems = await generate();
- const cert = new crypto.X509Certificate(pems.cert);
- const validFrom = new Date(cert.validFrom);
- const validTo = new Date(cert.validTo);
- const diffTime = Math.abs(validTo - validFrom);
- const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
- assert.approximately(diffDays, 365, 1, 'certificate should default to 365 days validity');
- });
- it('should respect notBeforeDate option', async function () {
- const customDate = new Date('2025-01-01T00:00:00Z');
- var pems = await generate(null, { notBeforeDate: customDate });
- const cert = new crypto.X509Certificate(pems.cert);
- const validFrom = new Date(cert.validFrom);
- // Allow small difference for processing time
- assert.approximately(validFrom.getTime(), customDate.getTime(), 5000, 'should use custom notBeforeDate');
- });
- it('should respect notAfterDate option', async function () {
- const notBefore = new Date('2025-01-01T00:00:00Z');
- const notAfter = new Date('2025-02-15T00:00:00Z');
- var pems = await generate(null, { notBeforeDate: notBefore, notAfterDate: notAfter });
- const cert = new crypto.X509Certificate(pems.cert);
- const validFrom = new Date(cert.validFrom);
- const validTo = new Date(cert.validTo);
- assert.approximately(validFrom.getTime(), notBefore.getTime(), 5000, 'should use custom notBeforeDate');
- assert.approximately(validTo.getTime(), notAfter.getTime(), 5000, 'should use custom notAfterDate');
- });
- it('should generate valid fingerprint format', async function () {
- var pems = await generate();
- assert.match(pems.fingerprint, /^[0-9a-f]{2}(:[0-9a-f]{2}){19}$/i, 'fingerprint should be valid SHA-1 format');
- });
- it('should support custom attributes', async function () {
- const attrs = [
- { name: 'commonName', value: 'test.example.com' },
- { name: 'countryName', value: 'GB' },
- { shortName: 'ST', value: 'London' },
- { name: 'localityName', value: 'Westminster' },
- { name: 'organizationName', value: 'Test Corp' },
- { shortName: 'OU', value: 'Engineering' }
- ];
- var pems = await generate(attrs);
- const cert = new crypto.X509Certificate(pems.cert);
- assert.include(cert.subject, 'CN=test.example.com', 'should include custom CN');
- assert.include(cert.subject, 'C=GB', 'should include custom country');
- assert.include(cert.subject, 'O=Test Corp', 'should include custom organization');
- });
- it('should support custom clientCertificateCN (deprecated)', async function () {
- var pems = await generate(null, {
- clientCertificate: true,
- clientCertificateCN: 'Custom User CN'
- });
- const clientCert = new crypto.X509Certificate(pems.clientcert);
- assert.include(clientCert.subject, 'CN=Custom User CN', 'should use custom client CN');
- });
- it('should support clientCertificate as options object with cn', async function () {
- var pems = await generate(null, {
- clientCertificate: {
- cn: 'Client Options CN'
- }
- });
- const clientCert = new crypto.X509Certificate(pems.clientcert);
- assert.include(clientCert.subject, 'CN=Client Options CN', 'should use cn from clientCertificate options');
- });
- it('should support clientCertificate.keySize', async function () {
- var pems = await generate(null, {
- clientCertificate: {
- keySize: 4096
- }
- });
- const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
- const keyDetails = clientPrivateKey.asymmetricKeyDetails;
- assert.strictEqual(keyDetails.modulusLength, 4096, 'should use keySize from clientCertificate options');
- });
- it('should support clientCertificate.notBeforeDate and notAfterDate', async function () {
- const notBefore = new Date('2025-06-01T00:00:00Z');
- const notAfter = new Date('2025-06-30T00:00:00Z');
- var pems = await generate(null, {
- clientCertificate: {
- notBeforeDate: notBefore,
- notAfterDate: notAfter
- }
- });
- const clientCert = new crypto.X509Certificate(pems.clientcert);
- const validFrom = new Date(clientCert.validFrom);
- const validTo = new Date(clientCert.validTo);
- assert.approximately(validFrom.getTime(), notBefore.getTime(), 5000, 'should use notBeforeDate from clientCertificate options');
- assert.approximately(validTo.getTime(), notAfter.getTime(), 5000, 'should use notAfterDate from clientCertificate options');
- });
- it('should support clientCertificate.algorithm', async function () {
- var pems = await generate(null, {
- algorithm: 'sha1', // main cert uses sha1
- clientCertificate: {
- algorithm: 'sha256' // client cert uses sha256
- }
- });
- // Both certs should be valid
- const serverCert = new crypto.X509Certificate(pems.cert);
- const clientCert = new crypto.X509Certificate(pems.clientcert);
- assert.ok(serverCert.publicKey, 'server cert should be valid');
- assert.ok(clientCert.publicKey, 'client cert should be valid');
- });
- it('clientCertificate options should take precedence over deprecated options', async function () {
- var pems = await generate(null, {
- clientCertificateCN: 'Deprecated CN',
- clientCertificateKeySize: 2048,
- clientCertificate: {
- cn: 'New Options CN',
- keySize: 4096
- }
- });
- const clientCert = new crypto.X509Certificate(pems.clientcert);
- assert.include(clientCert.subject, 'CN=New Options CN', 'clientCertificate.cn should take precedence');
- const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate);
- const keyDetails = clientPrivateKey.asymmetricKeyDetails;
- assert.strictEqual(keyDetails.modulusLength, 4096, 'clientCertificate.keySize should take precedence');
- });
- it('should generate valid key pair that work together', async function () {
- var pems = await generate();
- // Test data
- const testData = 'Hello, World!';
- // Create sign and verify objects
- const privateKey = crypto.createPrivateKey(pems.private);
- const publicKey = crypto.createPublicKey(pems.public);
- // Sign with private key
- const sign = crypto.createSign('SHA256');
- sign.update(testData);
- sign.end();
- const signature = sign.sign(privateKey);
- // Verify with public key
- const verify = crypto.createVerify('SHA256');
- verify.update(testData);
- verify.end();
- const isValid = verify.verify(publicKey, signature);
- assert.isTrue(isValid, 'public key should verify signature from private key');
- });
- it('should create client cert signed by server cert', async function () {
- var pems = await generate(null, { clientCertificate: true });
- const serverCert = new crypto.X509Certificate(pems.cert);
- const clientCert = new crypto.X509Certificate(pems.clientcert);
- // Client cert should have different subject than server
- assert.notEqual(clientCert.subject, serverCert.subject, 'client and server should have different subjects');
- // Both certs should be valid
- assert.ok(serverCert.publicKey, 'server cert should be valid');
- assert.ok(clientCert.publicKey, 'client cert should be valid');
- });
- it('should support using existing keyPair', async function () {
- // First generate a key pair
- const firstPems = await generate();
- // Reuse the key pair
- const secondPems = await generate(null, {
- keyPair: {
- privateKey: firstPems.private,
- publicKey: firstPems.public
- }
- });
- // Keys should be identical
- assert.strictEqual(firstPems.private, secondPems.private, 'should use provided private key');
- assert.strictEqual(firstPems.public, secondPems.public, 'should use provided public key');
- // Certificates will be different (different serial, dates) but keys are same
- const firstCert = new crypto.X509Certificate(firstPems.cert);
- const secondCert = new crypto.X509Certificate(secondPems.cert);
- assert.strictEqual(firstCert.publicKey.export({ format: 'pem', type: 'spki' }),
- secondCert.publicKey.export({ format: 'pem', type: 'spki' }),
- 'certificates should contain the same public key');
- });
- it('should create PKCS#7 for client certificate', async function () {
- var pems = await generate([{ name: 'commonName', value: 'server.example.com' }], {
- clientCertificate: true
- });
- var clientPkcs7 = createPkcs7(pems.clientcert);
- assert.ok(!!clientPkcs7, 'should create PKCS#7 for client cert');
- assert.include(clientPkcs7, 'BEGIN PKCS7', 'should be valid PKCS#7 format');
- // Verify with openssl
- try {
- fs.unlinkSync('/tmp/tmp-client.pkcs7');
- } catch (er) {}
- fs.writeFileSync('/tmp/tmp-client.pkcs7', clientPkcs7);
- const { stdout, stderr } = await exec('openssl pkcs7 -print_certs -in /tmp/tmp-client.pkcs7');
- if (stderr && stderr.length) {
- throw new Error(stderr);
- }
- assert.ok(stdout.toString().length > 0, 'openssl should be able to read client PKCS#7');
- });
- it('should generate unique serial numbers', async function () {
- const pems1 = await generate();
- const pems2 = await generate();
- const cert1 = new crypto.X509Certificate(pems1.cert);
- const cert2 = new crypto.X509Certificate(pems2.cert);
- assert.notEqual(cert1.serialNumber, cert2.serialNumber, 'serial numbers should be unique');
- });
- it('should handle minimal attributes', async function () {
- const attrs = [{ name: 'commonName', value: 'minimal.test' }];
- var pems = await generate(attrs);
- const cert = new crypto.X509Certificate(pems.cert);
- assert.include(cert.subject, 'CN=minimal.test', 'should work with minimal attributes');
- });
- describe('extensions', function () {
- it('should support custom subjectAltName with IPv6 (issue #79)', async function () {
- var pems = await generate(
- [{ name: 'commonName', value: 'localhost' }],
- {
- algorithm: 'sha256',
- extensions: [
- {
- name: 'basicConstraints',
- cA: false
- },
- {
- name: 'keyUsage',
- digitalSignature: true,
- keyEncipherment: true
- },
- {
- name: 'subjectAltName',
- altNames: [
- { type: 2, value: 'localhost' }, // DNS
- { type: 7, ip: '127.0.0.1' }, // IPv4
- { type: 7, ip: '::1' } // IPv6
- ]
- }
- ]
- }
- );
- const cert = new crypto.X509Certificate(pems.cert);
- assert.ok(cert.subjectAltName, 'should have subjectAltName');
- assert.include(cert.subjectAltName, 'localhost', 'should include DNS name');
- assert.include(cert.subjectAltName, '127.0.0.1', 'should include IPv4');
- // IPv6 ::1 may be expanded to full form 0:0:0:0:0:0:0:1
- const hasIPv6 = cert.subjectAltName.includes('::1') || cert.subjectAltName.includes('0:0:0:0:0:0:0:1');
- assert.ok(hasIPv6, 'should include IPv6');
- });
- it('should support basicConstraints with cA=true', async function () {
- var pems = await generate(
- [{ name: 'commonName', value: 'Test CA' }],
- {
- extensions: [
- {
- name: 'basicConstraints',
- cA: true,
- critical: true
- },
- {
- name: 'keyUsage',
- keyCertSign: true,
- cRLSign: true,
- critical: true
- }
- ]
- }
- );
- const cert = new crypto.X509Certificate(pems.cert);
- assert.ok(cert.ca, 'certificate should be a CA');
- });
- it('should support keyUsage extension', async function () {
- var pems = await generate(
- [{ name: 'commonName', value: 'test.example.com' }],
- {
- extensions: [
- {
- name: 'basicConstraints',
- cA: false
- },
- {
- name: 'keyUsage',
- digitalSignature: true,
- keyEncipherment: true,
- dataEncipherment: true
- }
- ]
- }
- );
- const cert = new crypto.X509Certificate(pems.cert);
- // Node.js X509Certificate doesn't expose keyUsage directly,
- // but we can verify the cert is valid and can be used
- assert.ok(cert.publicKey, 'should generate valid cert with keyUsage');
- // Verify by using openssl to check extensions
- const fs = require('fs');
- fs.writeFileSync('/tmp/test-keyusage.crt', pems.cert);
- const { execSync } = require('child_process');
- const output = execSync('openssl x509 -in /tmp/test-keyusage.crt -text -noout').toString();
- assert.include(output, 'Digital Signature', 'should have digitalSignature');
- assert.include(output, 'Key Encipherment', 'should have keyEncipherment');
- });
- it('should support extKeyUsage extension', async function () {
- var pems = await generate(
- [{ name: 'commonName', value: 'test.example.com' }],
- {
- extensions: [
- {
- name: 'basicConstraints',
- cA: false
- },
- {
- name: 'extKeyUsage',
- serverAuth: true,
- clientAuth: true,
- codeSigning: true
- }
- ]
- }
- );
- const cert = new crypto.X509Certificate(pems.cert);
- // Node.js crypto doesn't expose extended key usage directly, but cert should be valid
- assert.ok(cert.publicKey, 'should generate valid cert with extKeyUsage');
- });
- it('should support subjectAltName with DNS names', async function () {
- var pems = await generate(
- [{ name: 'commonName', value: 'example.com' }],
- {
- extensions: [
- {
- name: 'basicConstraints',
- cA: false
- },
- {
- name: 'subjectAltName',
- altNames: [
- { type: 2, value: 'example.com' },
- { type: 2, value: 'www.example.com' },
- { type: 2, value: '*.example.com' }
- ]
- }
- ]
- }
- );
- const cert = new crypto.X509Certificate(pems.cert);
- assert.include(cert.subjectAltName, 'example.com', 'should include example.com');
- assert.include(cert.subjectAltName, 'www.example.com', 'should include www.example.com');
- assert.include(cert.subjectAltName, '*.example.com', 'should include wildcard');
- });
- it('should support subjectAltName with email and URI', async function () {
- var pems = await generate(
- [{ name: 'commonName', value: 'test.example.com' }],
- {
- extensions: [
- {
- name: 'basicConstraints',
- cA: false
- },
- {
- name: 'subjectAltName',
- altNames: [
- { type: 2, value: 'test.example.com' },
- { type: 1, value: 'admin@example.com' }, // email
- { type: 6, value: 'http://example.com/webid#me' } // URI
- ]
- }
- ]
- }
- );
- const cert = new crypto.X509Certificate(pems.cert);
- assert.include(cert.subjectAltName, 'test.example.com', 'should include DNS');
- assert.include(cert.subjectAltName, 'admin@example.com', 'should include email');
- assert.include(cert.subjectAltName, 'http://example.com/webid#me', 'should include URI');
- });
- it('should use default extensions when extensions option is empty array', async function () {
- var pems = await generate(
- [{ name: 'commonName', value: 'localhost' }],
- {
- extensions: []
- }
- );
- const cert = new crypto.X509Certificate(pems.cert);
- // Default behavior includes localhost and 127.0.0.1
- assert.include(cert.subjectAltName, 'localhost', 'should use default SAN');
- assert.include(cert.subjectAltName, '127.0.0.1', 'should include default IP for localhost');
- });
- it('should use default extensions when extensions option is not provided', async function () {
- var pems = await generate([{ name: 'commonName', value: 'myhost.local' }]);
- const cert = new crypto.X509Certificate(pems.cert);
- assert.include(cert.subjectAltName, 'myhost.local', 'should use commonName in default SAN');
- });
- });
- it('should support passphrase for private key encryption', async function () {
- const passphrase = 'my-secret-passphrase';
- var pems = await generate(null, { passphrase: passphrase });
- assert.ok(!!pems.private, 'has a private key');
- assert.include(pems.private, 'ENCRYPTED', 'private key should be encrypted');
- // Verify the key can be decrypted with the correct passphrase
- const privateKey = crypto.createPrivateKey({
- key: pems.private,
- passphrase: passphrase
- });
- assert.ok(privateKey, 'should be able to decrypt private key with passphrase');
- // Verify signing works with decrypted key
- const testData = 'Hello, World!';
- const sign = crypto.createSign('SHA256');
- sign.update(testData);
- sign.end();
- const signature = sign.sign({ key: pems.private, passphrase: passphrase });
- const verify = crypto.createVerify('SHA256');
- verify.update(testData);
- verify.end();
- const isValid = verify.verify(pems.public, signature);
- assert.isTrue(isValid, 'encrypted key should work for signing');
- });
- it('should fail to decrypt private key with wrong passphrase', async function () {
- const passphrase = 'correct-passphrase';
- var pems = await generate(null, { passphrase: passphrase });
- assert.throws(() => {
- crypto.createPrivateKey({
- key: pems.private,
- passphrase: 'wrong-passphrase'
- });
- });
- });
- });
|