selfsigned
Advanced tools
+97
| # Changelog | ||
| All notable changes to this project will be documented in this file. | ||
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
| ## [5.0.0] - 2025-11-26 | ||
| ### 🚀 Major Rewrite | ||
| Complete rewrite replacing `node-forge` with modern `@peculiar/x509` and `pkijs` libraries. | ||
| ### ✨ Added | ||
| - Native WebCrypto API support for better performance and security | ||
| - TypeScript examples in documentation | ||
| - Async/await support as the primary API | ||
| - Support for `keyPair` option to use existing keys | ||
| - Updated to use Node.js native crypto for all operations | ||
| - Separate `selfsigned/pkcs7` module for tree-shakeable PKCS#7 support | ||
| ### 💥 BREAKING CHANGES | ||
| 1. **Async-only API**: The `generate()` function now returns a Promise. Synchronous generation has been removed. | ||
| ```js | ||
| // Old (v4.x) | ||
| const pems = selfsigned.generate(attrs, options); | ||
| // New (v5.x) | ||
| const pems = await selfsigned.generate(attrs, options); | ||
| ``` | ||
| 2. **No callback support**: Callbacks have been completely removed in favor of Promises. | ||
| ```js | ||
| // Old (v4.x) | ||
| selfsigned.generate(attrs, options, function(err, pems) { ... }); | ||
| // New (v5.x) | ||
| const pems = await selfsigned.generate(attrs, options); | ||
| ``` | ||
| 3. **Minimum Node.js version**: Now requires Node.js >= 15.6.0 (was >= 10) | ||
| - Required for native WebCrypto support | ||
| 4. **Dependencies changed**: | ||
| - ❌ Removed: `node-forge` (1.64 MB) | ||
| - ✅ Added: `@peculiar/x509` (551 KB) - 66% smaller! | ||
| - ✅ Added: `pkijs` (1.94 MB, only for PKCS#7 support) | ||
| - Bundle size reduced by 66% when not using PKCS#7 | ||
| 5. **PKCS#7 API changed**: | ||
| - Old: `const pems = await generate(attrs, { pkcs7: true }); pems.pkcs7` | ||
| - New: `const { createPkcs7 } = require('selfsigned/pkcs7'); const pkcs7 = createPkcs7(pems.cert);` | ||
| - PKCS#7 is now a separate module for better tree-shaking | ||
| ### 🔧 Changed | ||
| - Default key size remains 2048 bits (was incorrectly documented as 1024) | ||
| - PEM output uses `\n` line endings (was `\r\n`) | ||
| - Private keys now use PKCS#8 format (`BEGIN PRIVATE KEY` instead of `BEGIN RSA PRIVATE KEY`) | ||
| - Certificate generation is now fully async using native WebCrypto | ||
| - **PKCS#7 is now tree-shakeable**: Moved to separate `selfsigned/pkcs7` module so bundlers can exclude it when not used | ||
| ### 🐛 Fixed | ||
| - Default key size documentation corrected from 1024 to 2048 bits | ||
| - Improved error handling for certificate generation failures | ||
| ### 📦 Dependencies | ||
| **Removed:** | ||
| - `node-forge@^1.3.1` | ||
| - `@types/node-forge@^1.3.0` | ||
| **Added:** | ||
| - `@peculiar/x509@^1.14.2` (required) | ||
| - `pkijs@^3.3.3` (required, but tree-shakeable via separate `selfsigned/pkcs7` module) | ||
| ### 🔒 Security | ||
| - Now uses Node.js native WebCrypto API instead of JavaScript implementation | ||
| - Better integration with platform security features | ||
| - More secure random number generation | ||
| ### 📚 Documentation | ||
| - Complete README rewrite with async/await examples | ||
| - Added migration guide from v4.x to v5.x | ||
| - Updated all code examples to use async/await | ||
| - Added requirements section highlighting Node.js version requirement | ||
| --- | ||
| ## [4.0.0] - Previous Release | ||
| See git history for changes in 4.x and earlier versions. |
| const https = require('https'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { execSync } = require('child_process'); | ||
| const selfsigned = require('../'); | ||
| async function main() { | ||
| // Get mkcert's CAROOT path | ||
| let caroot; | ||
| try { | ||
| caroot = execSync('mkcert -CAROOT', { encoding: 'utf8' }).trim(); | ||
| } catch (err) { | ||
| console.error('Error: mkcert is not installed or not in PATH'); | ||
| console.error('Install mkcert: https://github.com/FiloSottile/mkcert'); | ||
| process.exit(1); | ||
| } | ||
| const caKeyPath = path.join(caroot, 'rootCA-key.pem'); | ||
| const caCertPath = path.join(caroot, 'rootCA.pem'); | ||
| // Check if CA files exist | ||
| if (!fs.existsSync(caKeyPath) || !fs.existsSync(caCertPath)) { | ||
| console.error('Error: mkcert CA files not found'); | ||
| console.error('Run "mkcert -install" first to create the local CA'); | ||
| process.exit(1); | ||
| } | ||
| console.log('Using mkcert CA from:', caroot); | ||
| // Read CA certificate and key | ||
| const caKey = fs.readFileSync(caKeyPath, 'utf8'); | ||
| const caCert = fs.readFileSync(caCertPath, 'utf8'); | ||
| // Generate a certificate signed by mkcert's CA | ||
| const pems = await selfsigned.generate([ | ||
| { name: 'commonName', value: 'localhost' } | ||
| ], { | ||
| days: 365, | ||
| keySize: 2048, | ||
| algorithm: 'sha256', | ||
| ca: { | ||
| key: caKey, | ||
| cert: caCert | ||
| } | ||
| }); | ||
| // Create HTTPS server with the generated certificate | ||
| const server = https.createServer({ | ||
| key: pems.private, | ||
| cert: pems.cert | ||
| }, (req, res) => { | ||
| res.writeHead(200, { 'Content-Type': 'text/plain' }); | ||
| res.end('Hello from HTTPS server with mkcert CA!\n'); | ||
| }); | ||
| const port = 3443; | ||
| server.listen(port, () => { | ||
| console.log(`HTTPS server running at https://localhost:${port}/`); | ||
| console.log('Certificate fingerprint:', pems.fingerprint); | ||
| console.log('\nSince this certificate is signed by mkcert\'s CA,'); | ||
| console.log('your browser should trust it automatically (if mkcert -install was run).'); | ||
| console.log('\nTest with: curl https://localhost:' + port); | ||
| }); | ||
| } | ||
| main().catch(console.error); |
| const https = require('https'); | ||
| const selfsigned = require('../'); | ||
| async function main() { | ||
| // Generate a self-signed certificate | ||
| const pems = await selfsigned.generate([ | ||
| { name: 'commonName', value: 'localhost' } | ||
| ], { | ||
| days: 365, | ||
| keySize: 2048, | ||
| algorithm: 'sha256' | ||
| }); | ||
| // Create HTTPS server with the generated certificate | ||
| const server = https.createServer({ | ||
| key: pems.private, | ||
| cert: pems.cert | ||
| }, (req, res) => { | ||
| res.writeHead(200, { 'Content-Type': 'text/plain' }); | ||
| res.end('Hello from self-signed HTTPS server!\n'); | ||
| }); | ||
| const port = 3443; | ||
| server.listen(port, () => { | ||
| console.log(`HTTPS server running at https://localhost:${port}/`); | ||
| console.log('Certificate fingerprint:', pems.fingerprint); | ||
| console.log('\nNote: Your browser will warn about the self-signed certificate.'); | ||
| console.log('Test with: curl -k https://localhost:' + port); | ||
| }); | ||
| } | ||
| main().catch(console.error); |
+70
| const pkijs = require("pkijs"); | ||
| const nodeCrypto = require("crypto"); | ||
| // Use Node.js native webcrypto | ||
| const crypto = nodeCrypto.webcrypto; | ||
| // Set up pkijs to use native crypto | ||
| // Note: This modifies global pkijs state. If the consumer also uses pkijs, | ||
| // they should set their own engine or use a version that supports per-instance engines. | ||
| let pkijsInitialized = false; | ||
| function ensurePkijsInitialized() { | ||
| if (!pkijsInitialized) { | ||
| pkijs.setEngine("nodeEngine", crypto, new pkijs.CryptoEngine({ | ||
| name: "", | ||
| crypto: crypto, | ||
| subtle: crypto.subtle | ||
| })); | ||
| pkijsInitialized = true; | ||
| } | ||
| } | ||
| /** | ||
| * Create PKCS#7 formatted certificate from PEM certificate | ||
| * | ||
| * @param {string} certPem - PEM formatted certificate | ||
| * @returns {string} PKCS#7 PEM formatted certificate | ||
| */ | ||
| function createPkcs7(certPem) { | ||
| ensurePkijsInitialized(); | ||
| // Parse the PEM certificate to get raw data | ||
| const certLines = certPem.split('\n').filter(line => | ||
| !line.includes('BEGIN CERTIFICATE') && | ||
| !line.includes('END CERTIFICATE') && | ||
| line.trim() | ||
| ); | ||
| const certBase64 = certLines.join(''); | ||
| const certBuffer = Buffer.from(certBase64, 'base64'); | ||
| // Parse certificate using pkijs | ||
| const asn1Cert = pkijs.Certificate.fromBER(certBuffer); | ||
| // Create PKCS#7 SignedData structure | ||
| const cmsSigned = new pkijs.SignedData({ | ||
| version: 1, | ||
| encapContentInfo: new pkijs.EncapsulatedContentInfo({ | ||
| eContentType: "1.2.840.113549.1.7.1" // data | ||
| }), | ||
| certificates: [asn1Cert] | ||
| }); | ||
| // Wrap in ContentInfo | ||
| const cmsSignedSchema = cmsSigned.toSchema(); | ||
| const cmsContentInfo = new pkijs.ContentInfo({ | ||
| contentType: "1.2.840.113549.1.7.2", // signedData | ||
| content: cmsSignedSchema | ||
| }); | ||
| // Convert to DER and then PEM | ||
| const cmsSignedDer = cmsContentInfo.toSchema().toBER(false); | ||
| const pkcs7Pem = | ||
| '-----BEGIN PKCS7-----\n' + | ||
| Buffer.from(cmsSignedDer).toString('base64').match(/.{1,64}/g).join('\n') + | ||
| '\n-----END PKCS7-----\n'; | ||
| return pkcs7Pem; | ||
| } | ||
| module.exports = { createPkcs7 }; |
| var { assert } = require('chai'); | ||
| var crypto = require('crypto'); | ||
| describe('CA signing', function () { | ||
| var generate = require('../index').generate; | ||
| it('should generate certificate signed by provided CA', async function () { | ||
| // First generate a self-signed CA certificate | ||
| const ca = await generate([ | ||
| { name: 'commonName', value: 'Test CA' }, | ||
| { name: 'organizationName', value: 'Test Organization' } | ||
| ], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| // Generate a certificate signed by the CA | ||
| const pems = await generate([ | ||
| { name: 'commonName', value: 'localhost' } | ||
| ], { | ||
| algorithm: 'sha256', | ||
| ca: { | ||
| key: ca.private, | ||
| cert: ca.cert | ||
| } | ||
| }); | ||
| assert.ok(!!pems.private, 'has a private key'); | ||
| assert.ok(!!pems.public, 'has a public key'); | ||
| assert.ok(!!pems.cert, 'has a certificate'); | ||
| assert.ok(!!pems.fingerprint, 'has fingerprint'); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| const caCert = new crypto.X509Certificate(ca.cert); | ||
| // Verify issuer is the CA, not self-signed | ||
| assert.include(cert.issuer, 'CN=Test CA', 'issuer should be the CA'); | ||
| assert.include(cert.subject, 'CN=localhost', 'subject should be localhost'); | ||
| assert.notEqual(cert.issuer, cert.subject, 'should not be self-signed'); | ||
| // Verify the certificate is signed by the CA | ||
| assert.isTrue(cert.verify(caCert.publicKey), 'certificate should be verified by CA public key'); | ||
| }); | ||
| it('should include Subject Alternative Name extension', async function () { | ||
| const ca = await generate([{ name: 'commonName', value: 'Test CA' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| const pems = await generate([ | ||
| { name: 'commonName', value: 'example.com' } | ||
| ], { | ||
| algorithm: 'sha256', | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.include(cert.subjectAltName, 'DNS:example.com', 'should have DNS SAN matching CN'); | ||
| }); | ||
| it('should include IP SAN for localhost', async function () { | ||
| const ca = await generate([{ name: 'commonName', value: 'Test CA' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| const pems = await generate([ | ||
| { name: 'commonName', value: 'localhost' } | ||
| ], { | ||
| algorithm: 'sha256', | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.include(cert.subjectAltName, 'DNS:localhost', 'should have DNS SAN'); | ||
| assert.include(cert.subjectAltName, 'IP Address:127.0.0.1', 'should have IP SAN for localhost'); | ||
| }); | ||
| it('should support different hash algorithms with CA signing', async function () { | ||
| const ca = await generate([{ name: 'commonName', value: 'Test CA' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| // Test sha384 | ||
| const pems384 = await generate([{ name: 'commonName', value: 'test384.local' }], { | ||
| algorithm: 'sha384', | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| const cert384 = new crypto.X509Certificate(pems384.cert); | ||
| assert.ok(cert384.publicKey, 'should generate sha384 CA-signed cert'); | ||
| // Test sha512 | ||
| const pems512 = await generate([{ name: 'commonName', value: 'test512.local' }], { | ||
| algorithm: 'sha512', | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| const cert512 = new crypto.X509Certificate(pems512.cert); | ||
| assert.ok(cert512.publicKey, 'should generate sha512 CA-signed cert'); | ||
| }); | ||
| it('should respect notAfterDate option with CA signing', async function () { | ||
| const ca = await generate([{ name: 'commonName', value: 'Test CA' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| const notBefore = new Date('2025-01-01T00:00:00Z'); | ||
| const notAfter = new Date('2025-01-31T00:00:00Z'); // 30 days validity | ||
| const pems = await generate([{ name: 'commonName', value: 'short-lived.local' }], { | ||
| algorithm: 'sha256', | ||
| notBeforeDate: notBefore, | ||
| notAfterDate: notAfter, | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| 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 unique certificates with same CA', async function () { | ||
| const ca = await generate([{ name: 'commonName', value: 'Test CA' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| const pems1 = await generate([{ name: 'commonName', value: 'test1.local' }], { | ||
| algorithm: 'sha256', | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| const pems2 = await generate([{ name: 'commonName', value: 'test2.local' }], { | ||
| algorithm: 'sha256', | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| 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'); | ||
| assert.notEqual(pems1.private, pems2.private, 'private keys should be different'); | ||
| }); | ||
| it('should work with custom keySize and CA signing', async function () { | ||
| const ca = await generate([{ name: 'commonName', value: 'Test CA' }], { | ||
| algorithm: 'sha256', | ||
| keySize: 4096 | ||
| }); | ||
| const pems = await generate([{ name: 'commonName', value: 'bigkey.local' }], { | ||
| algorithm: 'sha256', | ||
| keySize: 4096, | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| const privateKey = crypto.createPrivateKey(pems.private); | ||
| assert.strictEqual(privateKey.asymmetricKeyDetails.modulusLength, 4096, 'should use custom key size'); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| const caCert = new crypto.X509Certificate(ca.cert); | ||
| assert.isTrue(cert.verify(caCert.publicKey), 'certificate should verify with CA'); | ||
| }); | ||
| it('should support existing keyPair with CA signing', async function () { | ||
| const ca = await generate([{ name: 'commonName', value: 'Test CA' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| // Generate a key pair first | ||
| const keyPair = await generate([{ name: 'commonName', value: 'keypair.local' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| // Use existing key pair with CA signing | ||
| const pems = await generate([{ name: 'commonName', value: 'reused.local' }], { | ||
| algorithm: 'sha256', | ||
| keyPair: { | ||
| privateKey: keyPair.private, | ||
| publicKey: keyPair.public | ||
| }, | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| assert.strictEqual(pems.private, keyPair.private, 'should use provided private key'); | ||
| assert.strictEqual(pems.public, keyPair.public, 'should use provided public key'); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| const caCert = new crypto.X509Certificate(ca.cert); | ||
| assert.isTrue(cert.verify(caCert.publicKey), 'certificate should verify with CA'); | ||
| }); | ||
| it('should include proper extended key usage extensions', async function () { | ||
| const ca = await generate([{ name: 'commonName', value: 'Test CA' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| const pems = await generate([{ name: 'commonName', value: 'server.local' }], { | ||
| algorithm: 'sha256', | ||
| ca: { key: ca.private, cert: ca.cert } | ||
| }); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| // Check extended key usage (OIDs) | ||
| // 1.3.6.1.5.5.7.3.1 = serverAuth | ||
| // 1.3.6.1.5.5.7.3.2 = clientAuth | ||
| assert.include(cert.keyUsage, '1.3.6.1.5.5.7.3.1', 'should have serverAuth extended key usage'); | ||
| assert.include(cert.keyUsage, '1.3.6.1.5.5.7.3.2', 'should have clientAuth extended key usage'); | ||
| }); | ||
| it('should work with PKCS#1 RSA key format', async function () { | ||
| // Generate a CA with PKCS#1 format key (like mkcert uses) | ||
| const { privateKey, publicKey } = crypto.generateKeyPairSync('rsa', { | ||
| modulusLength: 2048, | ||
| privateKeyEncoding: { type: 'pkcs1', format: 'pem' }, | ||
| publicKeyEncoding: { type: 'spki', format: 'pem' } | ||
| }); | ||
| // Create a self-signed CA cert using the PKCS#1 key | ||
| const ca = await generate([{ name: 'commonName', value: 'PKCS1 CA' }], { | ||
| algorithm: 'sha256' | ||
| }); | ||
| // Now test that we can use a PKCS#1 formatted key as CA | ||
| // Convert our generated key to PKCS#1 for testing | ||
| const caKeyObject = crypto.createPrivateKey(ca.private); | ||
| const pkcs1Key = caKeyObject.export({ type: 'pkcs1', format: 'pem' }); | ||
| const pems = await generate([{ name: 'commonName', value: 'pkcs1-test.local' }], { | ||
| algorithm: 'sha256', | ||
| ca: { | ||
| key: pkcs1Key, | ||
| cert: ca.cert | ||
| } | ||
| }); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| const caCert = new crypto.X509Certificate(ca.cert); | ||
| assert.isTrue(cert.verify(caCert.publicKey), 'should work with PKCS#1 key format'); | ||
| }); | ||
| }); |
| { | ||
| "permissions": { | ||
| "allow": [ | ||
| "WebSearch", | ||
| "WebFetch(domain:github.com)", | ||
| "WebFetch(domain:www.npmjs.com)", | ||
| "Bash(npm view:*)", | ||
| "Bash(npm install:*)", | ||
| "Bash(node:*)", | ||
| "Bash(openssl pkcs7:*)", | ||
| "Bash(npm test:*)" | ||
| "Bash(ls:*)" | ||
| ], | ||
@@ -13,0 +6,0 @@ "deny": [], |
+60
-17
@@ -23,8 +23,2 @@ declare enum ASN1Class { | ||
| /** | ||
| * The number of days before expiration | ||
| * | ||
| * @default 365 */ | ||
| days?: number | ||
| /** | ||
| * The date before which the certificate should not be valid | ||
@@ -36,4 +30,10 @@ * | ||
| /** | ||
| * The date after which the certificate should not be valid | ||
| * | ||
| * @default notBeforeDate + 365 days */ | ||
| notAfterDate?: Date | ||
| /** | ||
| * the size for the private key in bits | ||
| * @default 1024 | ||
| * @default 2048 | ||
| */ | ||
@@ -46,3 +46,3 @@ keySize?: number | ||
| /** | ||
| * The signature algorithm sha256 or sha1 | ||
| * The signature algorithm: sha256, sha384, sha512 or sha1 | ||
| * @default "sha1" | ||
@@ -68,5 +68,21 @@ */ | ||
| * the size for the client private key in bits | ||
| * @default 1024 | ||
| * @default 2048 | ||
| */ | ||
| clientCertificateKeySize?: number | ||
| /** | ||
| * existing key pair to use instead of generating new keys | ||
| */ | ||
| keyPair?: { | ||
| privateKey: string | ||
| publicKey: string | ||
| } | ||
| /** | ||
| * CA certificate and key for signing (if not provided, generates self-signed) | ||
| */ | ||
| ca?: { | ||
| /** CA private key in PEM format */ | ||
| key: string | ||
| /** CA certificate in PEM format */ | ||
| cert: string | ||
| } | ||
| } | ||
@@ -79,14 +95,41 @@ | ||
| fingerprint: string | ||
| pkcs7?: string | ||
| clientprivate?: string | ||
| clientpublic?: string | ||
| clientcert?: string | ||
| clientpkcs7?: string | ||
| } | ||
| /** | ||
| * Generate a certificate (async only) | ||
| * | ||
| * @param attrs Certificate attributes | ||
| * @param opts Generation options | ||
| * @returns Promise that resolves with certificate data | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * // Self-signed certificate | ||
| * const pems = await generate(); | ||
| * | ||
| * const pems = await generate([{ name: 'commonName', value: 'example.com' }]); | ||
| * | ||
| * const pems = await generate(null, { | ||
| * keySize: 2048, | ||
| * algorithm: 'sha256' | ||
| * }); | ||
| * | ||
| * // CA-signed certificate | ||
| * const pems = await generate([{ name: 'commonName', value: 'localhost' }], { | ||
| * algorithm: 'sha256', | ||
| * ca: { | ||
| * key: fs.readFileSync('/path/to/ca.key', 'utf8'), | ||
| * cert: fs.readFileSync('/path/to/ca.crt', 'utf8') | ||
| * } | ||
| * }); | ||
| * ``` | ||
| */ | ||
| export declare function generate( | ||
| attrs?: CertificateField[], | ||
| opts?: SelfsignedOptions | ||
| ): GenerateResult | ||
| export declare function generate( | ||
| attrs?: CertificateField[], | ||
| opts?: SelfsignedOptions, | ||
| /** Optional callback, if not provided the generation is synchronous */ | ||
| done?: (err: undefined | Error, result: GenerateResult) => any | ||
| ): void | ||
| ): Promise<GenerateResult> |
+322
-188
@@ -1,3 +0,7 @@ | ||
| var forge = require("node-forge"); | ||
| const { X509CertificateGenerator, X509Certificate, X509ChainBuilder, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags, ExtendedKeyUsageExtension, ExtendedKeyUsage, SubjectAlternativeNameExtension, GeneralName } = require("@peculiar/x509"); | ||
| const nodeCrypto = require("crypto"); | ||
| // Use Node.js native webcrypto | ||
| const crypto = nodeCrypto.webcrypto; | ||
| // a hexString is considered negative if it's most significant bit is 1 | ||
@@ -17,232 +21,362 @@ // because serial numbers use ones' complement notation | ||
| function getAlgorithm(key) { | ||
| function getAlgorithmName(key) { | ||
| switch (key) { | ||
| case "sha256": | ||
| return forge.md.sha256.create(); | ||
| return "SHA-256"; | ||
| case 'sha384': | ||
| return forge.md.sha384.create(); | ||
| return "SHA-384"; | ||
| case 'sha512': | ||
| return forge.md.sha512.create(); | ||
| return "SHA-512"; | ||
| default: | ||
| return forge.md.sha1.create(); | ||
| return "SHA-1"; | ||
| } | ||
| } | ||
| /** | ||
| * | ||
| * @param {CertificateField[]} attrs Attributes used for subject and issuer. | ||
| * @param {object} options | ||
| * @param {number} [options.days=365] the number of days before expiration | ||
| * @param {number} [options.keySize=2048] the size for the private key in bits | ||
| * @param {object} [options.extensions] additional extensions for the certificate | ||
| * @param {string} [options.algorithm="sha1"] The signature algorithm sha256 or sha1 | ||
| * @param {boolean} [options.pkcs7=false] include PKCS#7 as part of the output | ||
| * @param {boolean} [options.clientCertificate=false] generate client cert signed by the original key | ||
| * @param {string} [options.clientCertificateCN="John Doe jdoe123"] client certificate's common name | ||
| * @param {function} [done] Optional callback, if not provided the generation is synchronous | ||
| * @returns | ||
| */ | ||
| exports.generate = function generate(attrs, options, done) { | ||
| if (typeof attrs === "function") { | ||
| done = attrs; | ||
| attrs = undefined; | ||
| } else if (typeof options === "function") { | ||
| done = options; | ||
| options = {}; | ||
| } | ||
| function getSigningAlgorithm(key) { | ||
| const hashAlg = getAlgorithmName(key); | ||
| return { | ||
| name: "RSASSA-PKCS1-v1_5", | ||
| hash: hashAlg | ||
| }; | ||
| } | ||
| options = options || {}; | ||
| // Convert attributes from node-forge format to X509 name format | ||
| function convertAttributes(attrs) { | ||
| const nameMap = { | ||
| 'commonName': 'CN', | ||
| 'countryName': 'C', | ||
| 'ST': 'ST', | ||
| 'localityName': 'L', | ||
| 'organizationName': 'O', | ||
| 'OU': 'OU' | ||
| }; | ||
| var generatePem = function (keyPair) { | ||
| var cert = forge.pki.createCertificate(); | ||
| return attrs.map(attr => { | ||
| const key = attr.name || attr.shortName; | ||
| const oid = nameMap[key] || key; | ||
| return `${oid}=${attr.value}`; | ||
| }).join(', '); | ||
| } | ||
| cert.serialNumber = toPositiveHex( | ||
| forge.util.bytesToHex(forge.random.getBytesSync(9)) | ||
| ); // the serial number can be decimal or hex (if preceded by 0x) | ||
| // Convert PEM key to CryptoKey | ||
| async function importPrivateKey(pemKey, algorithm) { | ||
| // Support both PKCS#8 and PKCS#1 (RSA) formats | ||
| const pkcs8Match = pemKey.match(/-----BEGIN PRIVATE KEY-----([\s\S]*?)-----END PRIVATE KEY-----/); | ||
| const rsaMatch = pemKey.match(/-----BEGIN RSA PRIVATE KEY-----([\s\S]*?)-----END RSA PRIVATE KEY-----/); | ||
| cert.validity.notBefore = options.notBeforeDate || new Date(); | ||
| var notAfter = new Date(); | ||
| cert.validity.notAfter = notAfter; | ||
| cert.validity.notAfter.setDate(notAfter.getDate() + (options.days || 365)); | ||
| attrs = attrs || [ | ||
| if (pkcs8Match) { | ||
| const pemContents = pkcs8Match[1].replace(/\s/g, ''); | ||
| const binaryDer = Buffer.from(pemContents, 'base64'); | ||
| return await crypto.subtle.importKey( | ||
| 'pkcs8', | ||
| binaryDer, | ||
| { | ||
| name: "commonName", | ||
| value: "example.org", | ||
| name: 'RSASSA-PKCS1-v1_5', | ||
| hash: getAlgorithmName(algorithm), | ||
| }, | ||
| true, | ||
| ['sign'] | ||
| ); | ||
| } else if (rsaMatch) { | ||
| // PKCS#1 RSA key - need to convert using Node.js crypto | ||
| const keyObject = nodeCrypto.createPrivateKey(pemKey); | ||
| const pkcs8Pem = keyObject.export({ type: 'pkcs8', format: 'pem' }); | ||
| const pemContents = pkcs8Pem | ||
| .replace(/-----BEGIN PRIVATE KEY-----/, '') | ||
| .replace(/-----END PRIVATE KEY-----/, '') | ||
| .replace(/\s/g, ''); | ||
| const binaryDer = Buffer.from(pemContents, 'base64'); | ||
| return await crypto.subtle.importKey( | ||
| 'pkcs8', | ||
| binaryDer, | ||
| { | ||
| name: "countryName", | ||
| value: "US", | ||
| name: 'RSASSA-PKCS1-v1_5', | ||
| hash: getAlgorithmName(algorithm), | ||
| }, | ||
| { | ||
| shortName: "ST", | ||
| value: "Virginia", | ||
| }, | ||
| { | ||
| name: "localityName", | ||
| value: "Blacksburg", | ||
| }, | ||
| { | ||
| name: "organizationName", | ||
| value: "Test", | ||
| }, | ||
| { | ||
| shortName: "OU", | ||
| value: "Test", | ||
| }, | ||
| ]; | ||
| true, | ||
| ['sign'] | ||
| ); | ||
| } else { | ||
| throw new Error('Unsupported private key format. Expected PKCS#8 or PKCS#1 RSA key.'); | ||
| } | ||
| } | ||
| cert.setSubject(attrs); | ||
| cert.setIssuer(attrs); | ||
| async function importPublicKey(pemKey, algorithm) { | ||
| const pemContents = pemKey | ||
| .replace(/-----BEGIN PUBLIC KEY-----/, '') | ||
| .replace(/-----END PUBLIC KEY-----/, '') | ||
| .replace(/\s/g, ''); | ||
| cert.publicKey = keyPair.publicKey; | ||
| const binaryDer = Buffer.from(pemContents, 'base64'); | ||
| cert.setExtensions( | ||
| options.extensions || [ | ||
| { | ||
| name: "basicConstraints", | ||
| cA: true, | ||
| }, | ||
| { | ||
| name: "keyUsage", | ||
| keyCertSign: true, | ||
| digitalSignature: true, | ||
| nonRepudiation: true, | ||
| keyEncipherment: true, | ||
| dataEncipherment: true, | ||
| }, | ||
| { | ||
| name: "subjectAltName", | ||
| altNames: [ | ||
| { | ||
| type: 6, // URI | ||
| value: "http://example.org/webid#me", | ||
| }, | ||
| ], | ||
| }, | ||
| ] | ||
| ); | ||
| return await crypto.subtle.importKey( | ||
| 'spki', | ||
| binaryDer, | ||
| { | ||
| name: 'RSASSA-PKCS1-v1_5', | ||
| hash: getAlgorithmName(algorithm), | ||
| }, | ||
| true, | ||
| ['verify'] | ||
| ); | ||
| } | ||
| cert.sign(keyPair.privateKey, getAlgorithm(options && options.algorithm)); | ||
| async function generatePemAsync(keyPair, attrs, options, ca) { | ||
| const { privateKey, publicKey } = keyPair; | ||
| const fingerprint = forge.md.sha1 | ||
| .create() | ||
| .update(forge.asn1.toDer(forge.pki.certificateToAsn1(cert)).getBytes()) | ||
| .digest() | ||
| .toHex() | ||
| .match(/.{2}/g) | ||
| .join(":"); | ||
| // Generate serial number | ||
| const serialBytes = crypto.getRandomValues(new Uint8Array(9)); | ||
| const serialHex = toPositiveHex(Buffer.from(serialBytes).toString('hex')); | ||
| var pem = { | ||
| private: forge.pki.privateKeyToPem(keyPair.privateKey), | ||
| public: forge.pki.publicKeyToPem(keyPair.publicKey), | ||
| cert: forge.pki.certificateToPem(cert), | ||
| fingerprint: fingerprint, | ||
| }; | ||
| // Set up dates | ||
| const notBefore = options.notBeforeDate || new Date(); | ||
| let notAfter; | ||
| if (options.notAfterDate) { | ||
| notAfter = options.notAfterDate; | ||
| } else { | ||
| notAfter = new Date(notBefore); | ||
| notAfter.setDate(notAfter.getDate() + 365); | ||
| } | ||
| if (options && options.pkcs7) { | ||
| var p7 = forge.pkcs7.createSignedData(); | ||
| p7.addCertificate(cert); | ||
| pem.pkcs7 = forge.pkcs7.messageToPem(p7); | ||
| } | ||
| // Default attributes | ||
| attrs = attrs || [ | ||
| { | ||
| name: "commonName", | ||
| value: "example.org", | ||
| }, | ||
| { | ||
| name: "countryName", | ||
| value: "US", | ||
| }, | ||
| { | ||
| shortName: "ST", | ||
| value: "Virginia", | ||
| }, | ||
| { | ||
| name: "localityName", | ||
| value: "Blacksburg", | ||
| }, | ||
| { | ||
| name: "organizationName", | ||
| value: "Test", | ||
| }, | ||
| { | ||
| shortName: "OU", | ||
| value: "Test", | ||
| }, | ||
| ]; | ||
| if (options && options.clientCertificate) { | ||
| var clientkeys = forge.pki.rsa.generateKeyPair( | ||
| options.clientCertificateKeySize || 2048 | ||
| ); | ||
| var clientcert = forge.pki.createCertificate(); | ||
| clientcert.serialNumber = toPositiveHex( | ||
| forge.util.bytesToHex(forge.random.getBytesSync(9)) | ||
| ); | ||
| clientcert.validity.notBefore = new Date(); | ||
| clientcert.validity.notAfter = new Date(); | ||
| clientcert.validity.notAfter.setFullYear( | ||
| clientcert.validity.notBefore.getFullYear() + 1 | ||
| ); | ||
| const subjectName = convertAttributes(attrs); | ||
| const signingAlg = getSigningAlgorithm(options.algorithm); | ||
| var clientAttrs = JSON.parse(JSON.stringify(attrs)); | ||
| // Extract common name for SAN extension | ||
| const commonNameAttr = attrs.find(attr => attr.name === 'commonName' || attr.shortName === 'CN'); | ||
| const commonName = commonNameAttr ? commonNameAttr.value : 'localhost'; | ||
| for (var i = 0; i < clientAttrs.length; i++) { | ||
| if (clientAttrs[i].name === "commonName") { | ||
| if (options.clientCertificateCN) | ||
| clientAttrs[i] = { | ||
| name: "commonName", | ||
| value: options.clientCertificateCN, | ||
| }; | ||
| else | ||
| clientAttrs[i] = { name: "commonName", value: "John Doe jdoe123" }; | ||
| } | ||
| } | ||
| // Build extensions array | ||
| const extensions = [ | ||
| new BasicConstraintsExtension(false, undefined, true), | ||
| new KeyUsagesExtension(KeyUsageFlags.digitalSignature | KeyUsageFlags.keyEncipherment, true), | ||
| new ExtendedKeyUsageExtension([ExtendedKeyUsage.serverAuth, ExtendedKeyUsage.clientAuth], false), | ||
| new SubjectAlternativeNameExtension([ | ||
| { type: 'dns', value: commonName }, | ||
| ...(commonName === 'localhost' ? [{ type: 'ip', value: '127.0.0.1' }] : []) | ||
| ], false) | ||
| ]; | ||
| clientcert.setSubject(clientAttrs); | ||
| let cert; | ||
| // Set the issuer to the parent key | ||
| clientcert.setIssuer(attrs); | ||
| if (ca) { | ||
| // Generate certificate signed by CA | ||
| const caCert = new X509Certificate(ca.cert); | ||
| const caPrivateKey = await importPrivateKey(ca.key, options.algorithm || "sha256"); | ||
| clientcert.publicKey = clientkeys.publicKey; | ||
| cert = await X509CertificateGenerator.create({ | ||
| serialNumber: serialHex, | ||
| subject: subjectName, | ||
| issuer: caCert.subject, | ||
| notBefore: notBefore, | ||
| notAfter: notAfter, | ||
| signingAlgorithm: signingAlg, | ||
| publicKey: publicKey, | ||
| signingKey: caPrivateKey, | ||
| extensions: extensions | ||
| }); | ||
| } else { | ||
| // Generate self-signed certificate | ||
| cert = await X509CertificateGenerator.createSelfSigned({ | ||
| serialNumber: serialHex, | ||
| name: subjectName, | ||
| notBefore: notBefore, | ||
| notAfter: notAfter, | ||
| signingAlgorithm: signingAlg, | ||
| keys: { | ||
| privateKey: privateKey, | ||
| publicKey: publicKey | ||
| }, | ||
| extensions: extensions | ||
| }); | ||
| } | ||
| // Sign client cert with root cert | ||
| clientcert.sign(keyPair.privateKey, getAlgorithm(options && options.algorithm)); | ||
| // Calculate fingerprint (SHA-1 hash of the certificate) | ||
| const certRaw = cert.rawData; | ||
| const fingerprintBuffer = await crypto.subtle.digest('SHA-1', certRaw); | ||
| const fingerprint = Buffer.from(fingerprintBuffer) | ||
| .toString('hex') | ||
| .match(/.{2}/g) | ||
| .join(':'); | ||
| pem.clientprivate = forge.pki.privateKeyToPem(clientkeys.privateKey); | ||
| pem.clientpublic = forge.pki.publicKeyToPem(clientkeys.publicKey); | ||
| pem.clientcert = forge.pki.certificateToPem(clientcert); | ||
| // Export keys to PEM | ||
| const privateKeyDer = await crypto.subtle.exportKey('pkcs8', privateKey); | ||
| const publicKeyDer = await crypto.subtle.exportKey('spki', publicKey); | ||
| if (options.pkcs7) { | ||
| var clientp7 = forge.pkcs7.createSignedData(); | ||
| clientp7.addCertificate(clientcert); | ||
| pem.clientpkcs7 = forge.pkcs7.messageToPem(clientp7); | ||
| const privatePem = | ||
| '-----BEGIN PRIVATE KEY-----\n' + | ||
| Buffer.from(privateKeyDer).toString('base64').match(/.{1,64}/g).join('\n') + | ||
| '\n-----END PRIVATE KEY-----\n'; | ||
| const publicPem = | ||
| '-----BEGIN PUBLIC KEY-----\n' + | ||
| Buffer.from(publicKeyDer).toString('base64').match(/.{1,64}/g).join('\n') + | ||
| '\n-----END PUBLIC KEY-----\n'; | ||
| const certPem = cert.toString('pem'); | ||
| const pem = { | ||
| private: privatePem, | ||
| public: publicPem, | ||
| cert: certPem, | ||
| fingerprint: fingerprint, | ||
| }; | ||
| // Client certificate support | ||
| if (options && options.clientCertificate) { | ||
| const clientKeyPair = await crypto.subtle.generateKey( | ||
| { | ||
| name: "RSASSA-PKCS1-v1_5", | ||
| modulusLength: options.clientCertificateKeySize || 2048, | ||
| publicExponent: new Uint8Array([1, 0, 1]), | ||
| hash: getAlgorithmName(options.algorithm || "sha1"), | ||
| }, | ||
| true, | ||
| ["sign", "verify"] | ||
| ); | ||
| const clientSerialBytes = crypto.getRandomValues(new Uint8Array(9)); | ||
| const clientSerialHex = toPositiveHex(Buffer.from(clientSerialBytes).toString('hex')); | ||
| const clientNotBefore = new Date(); | ||
| const clientNotAfter = new Date(); | ||
| clientNotAfter.setFullYear(clientNotBefore.getFullYear() + 1); | ||
| const clientAttrs = JSON.parse(JSON.stringify(attrs)); | ||
| for (let i = 0; i < clientAttrs.length; i++) { | ||
| if (clientAttrs[i].name === "commonName") { | ||
| clientAttrs[i] = { | ||
| name: "commonName", | ||
| value: options.clientCertificateCN || "John Doe jdoe123" | ||
| }; | ||
| } | ||
| } | ||
| var caStore = forge.pki.createCaStore(); | ||
| caStore.addCertificate(cert); | ||
| const clientSubjectName = convertAttributes(clientAttrs); | ||
| const issuerName = convertAttributes(attrs); | ||
| try { | ||
| forge.pki.verifyCertificateChain( | ||
| caStore, | ||
| [cert], | ||
| function (vfd, depth, chain) { | ||
| if (vfd !== true) { | ||
| throw new Error("Certificate could not be verified."); | ||
| } | ||
| return true; | ||
| } | ||
| ); | ||
| } catch (ex) { | ||
| throw new Error(ex); | ||
| } | ||
| // Create client cert signed by root key | ||
| const clientCertRaw = await X509CertificateGenerator.create({ | ||
| serialNumber: clientSerialHex, | ||
| subject: clientSubjectName, | ||
| issuer: issuerName, | ||
| notBefore: clientNotBefore, | ||
| notAfter: clientNotAfter, | ||
| signingAlgorithm: signingAlg, | ||
| publicKey: clientKeyPair.publicKey, | ||
| signingKey: privateKey // Sign with root private key | ||
| }); | ||
| return pem; | ||
| }; | ||
| // Export client keys | ||
| const clientPrivateKeyDer = await crypto.subtle.exportKey('pkcs8', clientKeyPair.privateKey); | ||
| const clientPublicKeyDer = await crypto.subtle.exportKey('spki', clientKeyPair.publicKey); | ||
| var keySize = options.keySize || 2048; | ||
| pem.clientprivate = | ||
| '-----BEGIN PRIVATE KEY-----\n' + | ||
| Buffer.from(clientPrivateKeyDer).toString('base64').match(/.{1,64}/g).join('\n') + | ||
| '\n-----END PRIVATE KEY-----\n'; | ||
| if (done) { | ||
| // async scenario | ||
| return forge.pki.rsa.generateKeyPair( | ||
| { bits: keySize }, | ||
| function (err, keyPair) { | ||
| if (err) { | ||
| return done(err); | ||
| } | ||
| pem.clientpublic = | ||
| '-----BEGIN PUBLIC KEY-----\n' + | ||
| Buffer.from(clientPublicKeyDer).toString('base64').match(/.{1,64}/g).join('\n') + | ||
| '\n-----END PUBLIC KEY-----\n'; | ||
| try { | ||
| return done(null, generatePem(keyPair)); | ||
| } catch (ex) { | ||
| return done(ex); | ||
| } | ||
| } | ||
| pem.clientcert = clientCertRaw.toString('pem'); | ||
| } | ||
| // Verify certificate chain | ||
| const x509Cert = new X509Certificate(cert.rawData); | ||
| const certificates = [x509Cert]; | ||
| // If CA-signed, include CA cert in the chain for verification | ||
| if (ca) { | ||
| const caCert = new X509Certificate(ca.cert); | ||
| certificates.push(caCert); | ||
| } | ||
| const chainBuilder = new X509ChainBuilder({ | ||
| certificates: certificates | ||
| }); | ||
| const chain = await chainBuilder.build(x509Cert); | ||
| if (chain.length === 0) { | ||
| throw new Error("Certificate could not be verified."); | ||
| } | ||
| return pem; | ||
| } | ||
| /** | ||
| * Generate a certificate (async) | ||
| * | ||
| * @param {CertificateField[]} attrs Attributes used for subject. | ||
| * @param {object} options | ||
| * @param {number} [options.keySize=2048] the size for the private key in bits | ||
| * @param {object} [options.extensions] additional extensions for the certificate | ||
| * @param {string} [options.algorithm="sha1"] The signature algorithm sha256, sha384, sha512 or sha1 | ||
| * @param {Date} [options.notBeforeDate=new Date()] The date before which the certificate should not be valid | ||
| * @param {Date} [options.notAfterDate] The date after which the certificate should not be valid (default: notBeforeDate + 365 days) | ||
| * @param {boolean} [options.clientCertificate=false] generate client cert signed by the original key | ||
| * @param {string} [options.clientCertificateCN="John Doe jdoe123"] client certificate's common name | ||
| * @param {object} [options.ca] CA certificate and key for signing (if not provided, generates self-signed) | ||
| * @param {string} [options.ca.key] CA private key in PEM format | ||
| * @param {string} [options.ca.cert] CA certificate in PEM format | ||
| * @returns {Promise<object>} Promise that resolves with certificate data | ||
| */ | ||
| exports.generate = async function generate(attrs, options) { | ||
| attrs = attrs || undefined; | ||
| options = options || {}; | ||
| const keySize = options.keySize || 2048; | ||
| let keyPair; | ||
| if (options.keyPair) { | ||
| // Import existing key pair | ||
| keyPair = { | ||
| privateKey: await importPrivateKey(options.keyPair.privateKey, options.algorithm || "sha1"), | ||
| publicKey: await importPublicKey(options.keyPair.publicKey, options.algorithm || "sha1") | ||
| }; | ||
| } else { | ||
| // Generate new key pair | ||
| keyPair = await crypto.subtle.generateKey( | ||
| { | ||
| name: "RSASSA-PKCS1-v1_5", | ||
| modulusLength: keySize, | ||
| publicExponent: new Uint8Array([1, 0, 1]), | ||
| hash: getAlgorithmName(options.algorithm || "sha1"), | ||
| }, | ||
| true, | ||
| ["sign", "verify"] | ||
| ); | ||
| } | ||
| var keyPair = options.keyPair | ||
| ? { | ||
| privateKey: forge.pki.privateKeyFromPem(options.keyPair.privateKey), | ||
| publicKey: forge.pki.publicKeyFromPem(options.keyPair.publicKey), | ||
| } | ||
| : forge.pki.rsa.generateKeyPair(keySize); | ||
| return generatePem(keyPair); | ||
| return await generatePemAsync(keyPair, attrs, options, options.ca); | ||
| }; |
+7
-5
| { | ||
| "name": "selfsigned", | ||
| "version": "4.0.1", | ||
| "version": "5.0.0", | ||
| "description": "Generate self signed certificates private and public keys", | ||
@@ -18,3 +18,5 @@ "main": "index.js", | ||
| "signed", | ||
| "certificates" | ||
| "certificates", | ||
| "x509", | ||
| "webcrypto" | ||
| ], | ||
@@ -36,6 +38,6 @@ "author": "José F. Romaniello <jfromaniello@gmail.com> (http://joseoncode.com)", | ||
| "dependencies": { | ||
| "node-forge": "^1" | ||
| "@peculiar/x509": "^1.14.2", | ||
| "pkijs": "^3.3.3" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/node-forge": "^1.3.0", | ||
| "chai": "^4.3.4", | ||
@@ -45,4 +47,4 @@ "mocha": "^9.1.1" | ||
| "engines": { | ||
| "node": ">=10" | ||
| "node": ">=15.6.0" | ||
| } | ||
| } |
+225
-41
@@ -1,84 +0,268 @@ | ||
| Generate a self signed x509 certificate from node.js. | ||
| # selfsigned | ||
| Generate self-signed X.509 certificates using Node.js native crypto. | ||
| ## Install | ||
| ```bash | ||
| npm install selfsigned | ||
| npm install selfsigned | ||
| ``` | ||
| ## Requirements | ||
| - **Node.js >= 15.6.0** (for native WebCrypto support) | ||
| ## Usage | ||
| **Version 5.0 is async-only.** The `generate()` function now returns a Promise. | ||
| ```js | ||
| var selfsigned = require('selfsigned'); | ||
| var attrs = [{ name: 'commonName', value: 'contoso.com' }]; | ||
| var pems = selfsigned.generate(attrs, { days: 365 }); | ||
| console.log(pems) | ||
| ``` | ||
| const selfsigned = require('selfsigned'); | ||
| #### Async | ||
| ```js | ||
| selfsigned.generate(attrs, { days: 365 }, function (err, pems) { | ||
| console.log(pems) | ||
| }); | ||
| const attrs = [{ name: 'commonName', value: 'contoso.com' }]; | ||
| const pems = await selfsigned.generate(attrs); | ||
| console.log(pems); | ||
| ``` | ||
| Will return the following like this: | ||
| ### Output | ||
| ```js | ||
| { | ||
| private: '-----BEGIN RSA PRIVATE KEY-----\r\nMIICXAIBAAKBgQCBFMXMYS/+RZz6+qzv+xeqXPdjw4YKZC4y3dPhSwgEwkecrCTX\r\nsR6boue+1MjIqPqWggXZnotIGldfEN0kn0Jbh2vMTrTx6YwqQ8tceBPoyuuqcYBO\r\nOONAcKOB3MLnZbyOgVtbyT3j68JE5V/lx6LhpIKAgY0m5WIuaKrW6mvLXQIDAQAB\r\nAoGAU6ODGxAqSecPdayyG/ml9vSwNAuAMgGB0eHcpZG5i2PbhRAh+0TAIXaoFQXJ\r\naAPeA2ISqlTJyRmQXYAO2uj61FzeyDzYCf0z3+yZEVz3cO7jB5Pl6iBvzbxWuuuA\r\ncbJtWLhWtW5/jioc8F0EAzZ+lkC/XuVJdwKHDmwt2qvJO+ECQQD+dvo1g3Sz9xGw\r\n21n+fDG5i4128+Qh+JPgh5AeLuXSofc1HMHaOXcC6Wu/Cloh7QAD934b7W0A7VoD\r\ndLd/JLyFAkEAgdwjryyvdhy69e516IrPB3b+m4rggtntBlZREMrk9tOzeIucVO3W\r\ntKI3FHm6JebN2gVcG+rZ+FaDPo+ifJkW+QJBAPojrMwEACmUevB2f9246gxx0UsY\r\nbq6yM3No71OsWEEY8/Bi53CEQqg7Gq5+F6H33qcHmBEN8LQTngN9rY+vZh0CQBg0\r\nqJImii5B/LeK03+dICoMDDmCEYdSh9P+ku3GZBd+Lp3xqBpMmxDgi9PNPN2DwCs7\r\nhIfPpwGbXqtyqp7/CkECQB4OdY+2FbCciI473eQkTu310RMf8jElU63iwnx4R/XN\r\n/mgqN589OfF4SS0U/MoRzYk9jF9IAJN1Mi/571T+nw4=\r\n-----END RSA PRIVATE KEY-----\r\n', | ||
| public: '-----BEGIN PUBLIC KEY-----\r\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCBFMXMYS/+RZz6+qzv+xeqXPdj\r\nw4YKZC4y3dPhSwgEwkecrCTXsR6boue+1MjIqPqWggXZnotIGldfEN0kn0Jbh2vM\r\nTrTx6YwqQ8tceBPoyuuqcYBOOONAcKOB3MLnZbyOgVtbyT3j68JE5V/lx6LhpIKA\r\ngY0m5WIuaKrW6mvLXQIDAQAB\r\n-----END PUBLIC KEY-----\r\n', | ||
| cert: '-----BEGIN CERTIFICATE-----\r\nMIICjTCCAfagAwIBAgIBATANBgkqhkiG9w0BAQUFADBpMRQwEgYDVQQDEwtleGFt\r\ncGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYDVQQH\r\nEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MB4XDTEz\r\nMDgxMzA1NDAyN1oXDTE0MDgxMzA1NDAyN1owaTEUMBIGA1UEAxMLZXhhbXBsZS5v\r\ncmcxCzAJBgNVBAYTAlVTMREwDwYDVQQIEwhWaXJnaW5pYTETMBEGA1UEBxMKQmxh\r\nY2tzYnVyZzENMAsGA1UEChMEVGVzdDENMAsGA1UECxMEVGVzdDCBnzANBgkqhkiG\r\n9w0BAQEFAAOBjQAwgYkCgYEAgRTFzGEv/kWc+vqs7/sXqlz3Y8OGCmQuMt3T4UsI\r\nBMJHnKwk17Eem6LnvtTIyKj6loIF2Z6LSBpXXxDdJJ9CW4drzE608emMKkPLXHgT\r\n6MrrqnGATjjjQHCjgdzC52W8joFbW8k94+vCROVf5cei4aSCgIGNJuViLmiq1upr\r\ny10CAwEAAaNFMEMwDAYDVR0TBAUwAwEB/zALBgNVHQ8EBAMCAvQwJgYDVR0RBB8w\r\nHYYbaHR0cDovL2V4YW1wbGUub3JnL3dlYmlkI21lMA0GCSqGSIb3DQEBBQUAA4GB\r\nAC9hGQlDh8anNo1YDJdG2mYqOQ5uybJV++kixblGaOkoDROPsWepUpL6kMDUtbAM\r\n4uXTyFkvlUQSaQkhNgOY5w/BRIAkCIu6u4D4XcjlCdwFq6vcKMEuWTHMAlBWFla3\r\nXJZAPO10PHuDen7JeMOUf1Re7lRFtwfRGAvVYmrvYFKv\r\n-----END CERTIFICATE-----\r\n' | ||
| private: '-----BEGIN PRIVATE KEY-----\n...', | ||
| public: '-----BEGIN PUBLIC KEY-----\n...', | ||
| cert: '-----BEGIN CERTIFICATE-----\n...', | ||
| fingerprint: 'XX:XX:XX:...' | ||
| } | ||
| ``` | ||
| ## Attributes | ||
| for attributes, please refer to: https://github.com/digitalbazaar/forge/blob/master/lib/x509.js | ||
| ## Options | ||
| ```js | ||
| var pems = selfsigned.generate(null, { | ||
| keySize: 2048, // the size for the private key in bits (default: 1024) | ||
| days: 30, // how long till expiry of the signed certificate (default: 365) | ||
| notBeforeDate: new Date(), // The date before which the certificate should not be valid (default: now) | ||
| const pems = await selfsigned.generate(null, { | ||
| keySize: 2048, // the size for the private key in bits (default: 2048) | ||
| notBeforeDate: new Date(), // start of certificate validity (default: now) | ||
| notAfterDate: new Date('2026-01-01'), // end of certificate validity (default: notBeforeDate + 365 days) | ||
| algorithm: 'sha256', // sign the certificate with specified algorithm (default: 'sha1') | ||
| extensions: [{ name: 'basicConstraints', cA: true }], // certificate extensions array | ||
| pkcs7: true, // include PKCS#7 as part of the output (default: false) | ||
| clientCertificate: true, // generate client cert signed by the original key (default: false) | ||
| clientCertificateCN: 'jdoe' // client certificate's common name (default: 'John Doe jdoe123') | ||
| clientCertificateCN: 'jdoe', // client certificate's common name (default: 'John Doe jdoe123') | ||
| clientCertificateKeySize: 2048, // the size for the client private key in bits (default: 2048) | ||
| ca: { key: '...', cert: '...' } // CA key and cert for signing (default: self-signed) | ||
| }); | ||
| ``` | ||
| > You can avoid key pair generation specifying your own keys (`{ keyPair: { publicKey: '-----BEGIN PUBLIC KEY-----...', privateKey: '-----BEGIN RSA PRIVATE KEY-----...' }`) | ||
| ### Setting Custom Validity Period | ||
| ### Generate Client Certificates | ||
| Use `notBeforeDate` and `notAfterDate` to control certificate validity: | ||
| If you are in an environment where servers require client certificates, you can generate client keys signed by the original (server) key. | ||
| ```js | ||
| // Using date-fns | ||
| const { addDays, addYears } = require('date-fns'); | ||
| const pems = await selfsigned.generate(null, { | ||
| notBeforeDate: new Date(), | ||
| notAfterDate: addDays(new Date(), 30) // Valid for 30 days | ||
| }); | ||
| // Or with vanilla JS | ||
| const notBefore = new Date(); | ||
| const notAfter = new Date(notBefore); | ||
| notAfter.setFullYear(notAfter.getFullYear() + 2); // Valid for 2 years | ||
| const pems = await selfsigned.generate(null, { | ||
| notBeforeDate: notBefore, | ||
| notAfterDate: notAfter | ||
| }); | ||
| ``` | ||
| ### Supported Algorithms | ||
| - `sha1` (default) | ||
| - `sha256` | ||
| - `sha384` | ||
| - `sha512` | ||
| ### Using Your Own Keys | ||
| You can avoid key pair generation by specifying your own keys: | ||
| ```js | ||
| var pems = selfsigned.generate(null, { clientCertificate: true }); | ||
| console.log(pems) | ||
| const pems = await selfsigned.generate(null, { | ||
| keyPair: { | ||
| publicKey: '-----BEGIN PUBLIC KEY-----...', | ||
| privateKey: '-----BEGIN PRIVATE KEY-----...' | ||
| } | ||
| }); | ||
| ``` | ||
| Will return the following like this: | ||
| ### Signing with a CA | ||
| You can generate certificates signed by an existing Certificate Authority instead of self-signed certificates. This is useful for development environments where you want browsers to trust your certificates. | ||
| ```js | ||
| { private: '-----BEGIN RSA PRIVATE KEY-----\r\nMIICXQIBAAKBgQDLg/kS4dCPVu96sbK6MQuUPmhqnF8SeBXVHH18h+0BTj7HqnrA\r\nA75hNVIiSLTChvpzQ0qi2Ju7O2ESUOdx7cvGiftGuZLiI8uL2HVlYuX+wQTIoRHx\r\n9nxv56TIiqnPg5d05vSTLXoiJg5uac3a6+4vnhhTo0XRRXVVboZsfNpuGQIDAQAB\r\nAoGAfhCd9QhUPLZJWeNBJvzCg221GHUMn1Arlfsz8DPyp+BkGyKLLu4iu+xfmEUZ\r\nU3ZxJX0FeqJatTwvAT2EYJpAovx+F37PWFTLAS6T57WI1O5Lj1pTIKVkLrasNQgF\r\nl6qFD3cvEtCZve4LiwDoJ52FO2OtcDcMJ0r2oqbCXSDIlAECQQDnkkxKcTejBZGH\r\nyYEXG9hAznnEZ63LLzlHHF2cIPfxT+9826Wm0IzBxn8Wr4hcAbNx3bVKgsU9p7xA\r\nfKnSqObhAkEA4PwCjPQqxFpiYUmNt7htb8nCEvUDD/QSDyxAH/uJzfr6gOJOD5nT\r\n5gZYblC+CCMDkgDUpro6oATNyeRNoU3GOQJBANdaW26DWZ1WqV9hCpcGAxdJrT30\r\nuVASq66w93Ehy9LzZqFz1tqKacwvH7NmLGZ8AngrGdSgRnOvEMfb50aMYqECQDcG\r\nzCTnbzJZHOjIkaXWsMV/pjz2ugoD2wrk+sYXwoujj/NH5mnAaOhAsw5AJ0pcLfpe\r\nw6QHtmD+68ouUaJbIFkCQQDeu0AXAp6Kbk6570i2DpGUSnkRdGCGS+3ekqqJUpE7\r\nfVUSx1nCF1sPD0p+pO8Rj3i87iI4MlblQRm/wVkrkjiR\r\n-----END RSA PRIVATE KEY-----\r\n', | ||
| public: '-----BEGIN PUBLIC KEY-----\r\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLg/kS4dCPVu96sbK6MQuUPmhq\r\nnF8SeBXVHH18h+0BTj7HqnrAA75hNVIiSLTChvpzQ0qi2Ju7O2ESUOdx7cvGiftG\r\nuZLiI8uL2HVlYuX+wQTIoRHx9nxv56TIiqnPg5d05vSTLXoiJg5uac3a6+4vnhhT\r\no0XRRXVVboZsfNpuGQIDAQAB\r\n-----END PUBLIC KEY-----\r\n', | ||
| cert: '-----BEGIN CERTIFICATE-----\r\nMIIClTCCAf6gAwIBAgIJdMZqoEeGMVYKMA0GCSqGSIb3DQEBBQUAMGkxFDASBgNV\r\nBAMTC2V4YW1wbGUub3JnMQswCQYDVQQGEwJVUzERMA8GA1UECBMIVmlyZ2luaWEx\r\nEzARBgNVBAcTCkJsYWNrc2J1cmcxDTALBgNVBAoTBFRlc3QxDTALBgNVBAsTBFRl\r\nc3QwHhcNMTUxMDI5MTMwNjA1WhcNMTYxMDI4MTMwNjA1WjBpMRQwEgYDVQQDEwtl\r\neGFtcGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYD\r\nVQQHEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MIGf\r\nMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDLg/kS4dCPVu96sbK6MQuUPmhqnF8S\r\neBXVHH18h+0BTj7HqnrAA75hNVIiSLTChvpzQ0qi2Ju7O2ESUOdx7cvGiftGuZLi\r\nI8uL2HVlYuX+wQTIoRHx9nxv56TIiqnPg5d05vSTLXoiJg5uac3a6+4vnhhTo0XR\r\nRXVVboZsfNpuGQIDAQABo0UwQzAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIC9DAm\r\nBgNVHREEHzAdhhtodHRwOi8vZXhhbXBsZS5vcmcvd2ViaWQjbWUwDQYJKoZIhvcN\r\nAQEFBQADgYEAj1Yyyb0R9KRFjIWNFi6RErB/riWylW4CdOK1hOyJZ+VRBWeYLKfX\r\ni///V+tqRvLlYY5x5DnrjXbDjBy0CZuN/J772/Srgp7Nl5cn92zynMJK1q4MEEs3\r\nAE/FO85R0HbGEp+IrwUwDOLR6omBFVdh1EUOTcQU2jLZNbWvLDiWbDo=\r\n-----END CERTIFICATE-----\r\n', | ||
| clientprivate: '-----BEGIN RSA PRIVATE KEY-----\r\nMIICWwIBAAKBgQDjR5FrrdZ1jirqkx3KMPnGjrcObj/vmztWTEZ1kX6gTskQugJU\r\noxktzwDZza4jYODC6Ud2jouFLWeAi5BDSAeLwAQb951qVD9zVsmQ+63V/mvSJUoj\r\nigwj7YjcxyReJ17F0YgjceqrkZaPM8YRo8h1fj1JdPc4ZOUgA5ASZ0h2ewIDAQAB\r\nAoGAfB5DbjibG8ut6Di7VgX1AdhCY+EVjXaKqxAwklgIfOdJqpbKWwpO39NiNY+7\r\nf5qSZB8dZcNmsi4fjfWprPSTGVkk1Qp2uibtFS4MhbLEeyy4cgZfMIBQY+HD0Asf\r\n1NU7WTY5QfzgH3HAKuWpUEWdar/jE+hDPA+wnsMg+TgGARECQQDzlc+5WA9JsG9f\r\nwNRzhMGRxDP4QLmL0iLWupF4BMP/k4OLMjDtzWl725WJ4FjCzML7mSmkWWe/P8f5\r\nwrbR+e8lAkEA7t0CEsiIw8BE55YMuGIz5xI0QDnuwNWmCEmq6+ZziW3L+EuAr1S4\r\nDORqBYm5DuRvBWkWE9Sld0a8vNqWh58tHwJAP1ZYEhicuQuAmkRYucTuVEnRPZ8O\r\n4BV+65jNlIigskcYMEyXvm3oHMWnJ5fHXLfDh4p28n4w5ODfzcjcotK7ZQJAE7bX\r\n8fbtGsLmrPp8aEdqozqkZ1ygsPexMWPrIHcvt/sA56hLoazrV90ORxC73lfKNfcb\r\nZF2bnoGPGEMuQ1lG3wJAPnHysm3DgbSHZQiXWMjF4YDRRV2AeOqX1fmlSeMErwdj\r\ncwIs+ikIBnOwUOh6liJ7yK1YnckDTZTOfUDyG+vdFQ==\r\n-----END RSA PRIVATE KEY-----\r\n', | ||
| clientpublic: '-----BEGIN PUBLIC KEY-----\r\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjR5FrrdZ1jirqkx3KMPnGjrcO\r\nbj/vmztWTEZ1kX6gTskQugJUoxktzwDZza4jYODC6Ud2jouFLWeAi5BDSAeLwAQb\r\n951qVD9zVsmQ+63V/mvSJUojigwj7YjcxyReJ17F0YgjceqrkZaPM8YRo8h1fj1J\r\ndPc4ZOUgA5ASZ0h2ewIDAQAB\r\n-----END PUBLIC KEY-----\r\n', | ||
| clientcert: '-----BEGIN CERTIFICATE-----\r\nMIICSzCCAbSgAwIBAgIBAjANBgkqhkiG9w0BAQUFADBpMRQwEgYDVQQDEwtleGFt\r\ncGxlLm9yZzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYDVQQH\r\nEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MB4XDTE1\r\nMTAyOTEzMDYwNVoXDTE2MTAyOTEzMDYwNVowbjEZMBcGA1UEAxMQSm9obiBEb2Ug\r\namRvZTEyMzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMRMwEQYDVQQH\r\nEwpCbGFja3NidXJnMQ0wCwYDVQQKEwRUZXN0MQ0wCwYDVQQLEwRUZXN0MIGfMA0G\r\nCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDjR5FrrdZ1jirqkx3KMPnGjrcObj/vmztW\r\nTEZ1kX6gTskQugJUoxktzwDZza4jYODC6Ud2jouFLWeAi5BDSAeLwAQb951qVD9z\r\nVsmQ+63V/mvSJUojigwj7YjcxyReJ17F0YgjceqrkZaPM8YRo8h1fj1JdPc4ZOUg\r\nA5ASZ0h2ewIDAQABMA0GCSqGSIb3DQEBBQUAA4GBACOUglBxJ80jzR3DSSMrgRav\r\n7deKUPShEPC3tbVrc3LHPGpCEJUC309aK2mbMwz2jX78tr/ezePELKbyRggUvVgN\r\nB0XdIQkpR9X4mPdtFYkMiWKNVYKd79r0kolprgFPryhT3jsICIOnwE1Ur23Q+Fk2\r\nnizRS0HY4Q25JLCmsWWy\r\n-----END CERTIFICATE-----\r\n' } | ||
| const fs = require('fs'); | ||
| const selfsigned = require('selfsigned'); | ||
| const pems = await selfsigned.generate([ | ||
| { name: 'commonName', value: 'localhost' } | ||
| ], { | ||
| algorithm: 'sha256', | ||
| ca: { | ||
| key: fs.readFileSync('/path/to/ca.key', 'utf8'), | ||
| cert: fs.readFileSync('/path/to/ca.crt', 'utf8') | ||
| } | ||
| }); | ||
| ``` | ||
| To override the default client CN of `john doe jdoe123`, add another option for `clientCertificateCN`: | ||
| The generated certificate will be signed by the provided CA and will include: | ||
| - Subject Alternative Name (SAN) extension with DNS name matching the commonName | ||
| - For `localhost`, an additional IP SAN for `127.0.0.1` | ||
| - Key Usage: digitalSignature, keyEncipherment | ||
| - Extended Key Usage: serverAuth, clientAuth | ||
| #### Using with mkcert | ||
| [mkcert](https://github.com/FiloSottile/mkcert) is a simple tool for making locally-trusted development certificates. Combining it with `selfsigned` provides an excellent developer experience: | ||
| - **No certificate files to manage** - generate trusted certificates on-the-fly at server startup | ||
| - **No git-ignored cert files** - nothing to store, share, or accidentally commit | ||
| - **Browsers trust the certificates automatically** - no security warnings during development | ||
| ```js | ||
| var pems = selfsigned.generate(null, { clientCertificate: true, clientCertificateCN: 'FooBar' }); | ||
| const https = require('https'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
| const { execSync } = require('child_process'); | ||
| const selfsigned = require('selfsigned'); | ||
| // Get mkcert's CA (requires: brew install mkcert && mkcert -install) | ||
| const caroot = execSync('mkcert -CAROOT', { encoding: 'utf8' }).trim(); | ||
| const pems = await selfsigned.generate([ | ||
| { name: 'commonName', value: 'localhost' } | ||
| ], { | ||
| algorithm: 'sha256', | ||
| ca: { | ||
| key: fs.readFileSync(path.join(caroot, 'rootCA-key.pem'), 'utf8'), | ||
| cert: fs.readFileSync(path.join(caroot, 'rootCA.pem'), 'utf8') | ||
| } | ||
| }); | ||
| // Start server with browser-trusted certificate - no files written to disk | ||
| https.createServer({ key: pems.private, cert: pems.cert }, app).listen(443); | ||
| ``` | ||
| See [examples/https-server-mkcert.js](examples/https-server-mkcert.js) for a complete working example. | ||
| ## Attributes | ||
| Attributes follow the X.509 standard: | ||
| ```js | ||
| const attrs = [ | ||
| { name: 'commonName', value: 'example.org' }, | ||
| { name: 'countryName', value: 'US' }, | ||
| { shortName: 'ST', value: 'Virginia' }, | ||
| { name: 'localityName', value: 'Blacksburg' }, | ||
| { name: 'organizationName', value: 'Test' }, | ||
| { shortName: 'OU', value: 'Test' } | ||
| ]; | ||
| ``` | ||
| ## Generate Client Certificates | ||
| For environments where servers require client certificates, you can generate client keys signed by the original (server) key: | ||
| ```js | ||
| const pems = await selfsigned.generate(null, { clientCertificate: true }); | ||
| console.log(pems); | ||
| ``` | ||
| Output includes additional client certificate fields: | ||
| ```js | ||
| { | ||
| private: '-----BEGIN PRIVATE KEY-----\n...', | ||
| public: '-----BEGIN PUBLIC KEY-----\n...', | ||
| cert: '-----BEGIN CERTIFICATE-----\n...', | ||
| fingerprint: 'XX:XX:XX:...', | ||
| clientprivate: '-----BEGIN PRIVATE KEY-----\n...', | ||
| clientpublic: '-----BEGIN PUBLIC KEY-----\n...', | ||
| clientcert: '-----BEGIN CERTIFICATE-----\n...' | ||
| } | ||
| ``` | ||
| To override the default client CN of `John Doe jdoe123`: | ||
| ```js | ||
| const pems = await selfsigned.generate(null, { | ||
| clientCertificate: true, | ||
| clientCertificateCN: 'FooBar' | ||
| }); | ||
| ``` | ||
| ## PKCS#7 Support | ||
| PKCS#7 formatting is available through a separate module for better tree-shaking: | ||
| ```js | ||
| const selfsigned = require('selfsigned'); | ||
| const { createPkcs7 } = require('selfsigned/pkcs7'); | ||
| const pems = await selfsigned.generate(attrs); | ||
| const pkcs7 = createPkcs7(pems.cert); | ||
| console.log(pkcs7); // PKCS#7 formatted certificate | ||
| ``` | ||
| You can also create PKCS#7 for client certificates: | ||
| ```js | ||
| const pems = await selfsigned.generate(null, { clientCertificate: true }); | ||
| const clientPkcs7 = createPkcs7(pems.clientcert); | ||
| ``` | ||
| ## Migration from v4.x | ||
| Version 5.0 introduces breaking changes: | ||
| ### Breaking Changes | ||
| 1. **Async-only API**: The `generate()` function is now async and returns a Promise. Synchronous generation is no longer supported. | ||
| 2. **No callback support**: Callbacks have been removed. Use `async`/`await` or `.then()`. | ||
| 3. **Minimum Node.js version**: Now requires Node.js >= 15.6.0 (was >= 10). | ||
| 4. **Dependencies**: Replaced `node-forge` with `@peculiar/x509` and `pkijs` (66% smaller bundle size). | ||
| 5. **`days` option removed**: Use `notAfterDate` instead. Default validity is 365 days from `notBeforeDate`. | ||
| ### Migration Examples | ||
| **Old (v4.x):** | ||
| ```js | ||
| // Sync | ||
| const pems = selfsigned.generate(attrs, { days: 365 }); | ||
| // Callback | ||
| selfsigned.generate(attrs, { days: 365 }, function(err, pems) { | ||
| if (err) throw err; | ||
| console.log(pems); | ||
| }); | ||
| ``` | ||
| **New (v5.x):** | ||
| ```js | ||
| // Async/await (default 365 days validity) | ||
| const pems = await selfsigned.generate(attrs); | ||
| // Custom validity with notAfterDate | ||
| const notAfter = new Date(); | ||
| notAfter.setDate(notAfter.getDate() + 30); // 30 days | ||
| const pems = await selfsigned.generate(attrs, { notAfterDate: notAfter }); | ||
| // Or with .then() | ||
| selfsigned.generate(attrs) | ||
| .then(pems => console.log(pems)) | ||
| .catch(err => console.error(err)); | ||
| ``` | ||
| ## License | ||
| MIT |
+237
-128
| var { assert } = require('chai'); | ||
| var forge = require('node-forge'); | ||
| var fs = require('fs'); | ||
| var exec = require('child_process').exec; | ||
| var { promisify } = require('util'); | ||
| var exec = promisify(require('child_process').exec); | ||
| var crypto = require('crypto'); | ||
@@ -9,5 +10,6 @@ describe('generate', function () { | ||
| var generate = require('../index').generate; | ||
| var { createPkcs7 } = require('../pkcs7'); | ||
| it('should work without attrs/options', function (done) { | ||
| var pems = generate(); | ||
| it('should work without attrs/options', async function () { | ||
| var pems = await generate(); | ||
| assert.ok(!!pems.private, 'has a private key'); | ||
@@ -22,9 +24,9 @@ assert.ok(!!pems.fingerprint, 'has fingerprint'); | ||
| var caStore = forge.pki.createCaStore(); | ||
| caStore.addCertificate(pems.cert); | ||
| done(); | ||
| // 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', function (done) { | ||
| var pems = generate(null, {clientCertificate: true}); | ||
| it('should generate client cert', async function () { | ||
| var pems = await generate(null, {clientCertificate: true}); | ||
@@ -34,9 +36,9 @@ assert.ok(!!pems.clientcert, 'should include a client cert when requested'); | ||
| assert.ok(!!pems.clientpublic, 'should include a client public key when requested'); | ||
| done(); | ||
| }); | ||
| it('should include pkcs7', function (done) { | ||
| var pems = generate([{ name: 'commonName', value: 'contoso.com' }], {pkcs7: true}); | ||
| it('should include pkcs7', async function () { | ||
| var pems = await generate([{ name: 'commonName', value: 'contoso.com' }]); | ||
| var pkcs7 = createPkcs7(pems.cert); | ||
| assert.ok(!!pems.pkcs7, 'has a pkcs7'); | ||
| assert.ok(!!pkcs7, 'has a pkcs7'); | ||
@@ -47,146 +49,253 @@ try { | ||
| fs.writeFileSync('/tmp/tmp.pkcs7', pems.pkcs7); | ||
| exec('openssl pkcs7 -print_certs -in /tmp/tmp.pkcs7', function (err, stdout, stderr) { | ||
| if (err) { | ||
| return done(err); | ||
| } | ||
| fs.writeFileSync('/tmp/tmp.pkcs7', pkcs7); | ||
| const errorMessage = stderr.toString(); | ||
| if (errorMessage.length) { | ||
| return done(new Error(errorMessage)); | ||
| } | ||
| const { stdout, stderr } = await exec('openssl pkcs7 -print_certs -in /tmp/tmp.pkcs7'); | ||
| 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); | ||
| assert.strictEqual( | ||
| pems.cert, | ||
| cert.join('\r\n') + '\r\n' | ||
| ); | ||
| if (stderr && stderr.length) { | ||
| throw new Error(stderr); | ||
| } | ||
| done(); | ||
| }); | ||
| 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', function (done) { | ||
| var pems_sha1 = generate(null, { algorithm: 'sha1' }); | ||
| assert.ok(forge.pki.certificateFromPem(pems_sha1.cert).siginfo.algorithmOid === forge.pki.oids['sha1WithRSAEncryption'], 'can generate sha1 certs'); | ||
| done(); | ||
| 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', function (done) { | ||
| var pems_sha256 = generate(null, { algorithm: 'sha256' }); | ||
| assert.ok(forge.pki.certificateFromPem(pems_sha256.cert).siginfo.algorithmOid === forge.pki.oids['sha256WithRSAEncryption'], 'can generate sha256 certs'); | ||
| done(); | ||
| 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', function (done) { | ||
| var pems = generate(); | ||
| var privateKey = forge.pki.privateKeyFromPem(pems.private); | ||
| assert.strictEqual(privateKey.n.bitLength(), 2048, 'default key size should be 2048 bits'); | ||
| done(); | ||
| 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', function (done) { | ||
| var pems = generate(null, {clientCertificate: true}); | ||
| var clientPrivateKey = forge.pki.privateKeyFromPem(pems.clientprivate); | ||
| assert.strictEqual(clientPrivateKey.n.bitLength(), 2048, 'default client key size should be 2048 bits'); | ||
| done(); | ||
| 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'); | ||
| }); | ||
| describe('with callback', function () { | ||
| it('should work without attrs/options', function (done) { | ||
| generate(function (err, pems) { | ||
| if (err) done(err); | ||
| assert.ok(!!pems.private, 'has a private key'); | ||
| 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'); | ||
| done(); | ||
| }); | ||
| }); | ||
| 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 generate client cert', function (done) { | ||
| generate(null, {clientCertificate: true}, function (err, pems) { | ||
| if (err) done(err); | ||
| 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'); | ||
| done(); | ||
| }); | ||
| 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 include pkcs7', function (done) { | ||
| generate([{ name: 'commonName', value: 'contoso.com' }], {pkcs7: true}, function (err, pems) { | ||
| if (err) done(err); | ||
| assert.ok(!!pems.pkcs7, 'has a pkcs7'); | ||
| 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'); | ||
| }); | ||
| try { | ||
| fs.unlinkSync('/tmp/tmp.pkcs7'); | ||
| } catch (er) {} | ||
| 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'); | ||
| }); | ||
| fs.writeFileSync('/tmp/tmp.pkcs7', pems.pkcs7); | ||
| exec('openssl pkcs7 -print_certs -in /tmp/tmp.pkcs7', function (err, stdout, stderr) { | ||
| if (err) { | ||
| return done(err); | ||
| } | ||
| it('should default to 365 days validity', async function () { | ||
| var pems = await generate(); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| const errorMessage = stderr.toString(); | ||
| if (errorMessage.length) { | ||
| return done(new Error(errorMessage)); | ||
| } | ||
| 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)); | ||
| const expected = stdout.toString(); | ||
| let [ subjectLine,issuerLine, ...cert ] = expected.split(/\r?\n/).filter(c => c); | ||
| assert.match(subjectLine, /subject=\/?CN\s?=\s?contoso.com/i); | ||
| assert.match(issuerLine, /issuer=\/?CN\s?=\s?contoso.com/i); | ||
| assert.strictEqual( | ||
| pems.cert, | ||
| cert.join('\r\n') + '\r\n' | ||
| ); | ||
| assert.approximately(diffDays, 365, 1, 'certificate should default to 365 days validity'); | ||
| }); | ||
| done(); | ||
| }); | ||
| }); | ||
| }); | ||
| 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); | ||
| it('should support sha1 algorithm', function (done) { | ||
| generate(null, { algorithm: 'sha1' }, function (err, pems_sha1) { | ||
| if (err) done(err); | ||
| assert.ok(forge.pki.certificateFromPem(pems_sha1.cert).siginfo.algorithmOid === forge.pki.oids['sha1WithRSAEncryption'], 'can generate sha1 certs'); | ||
| done(); | ||
| }); | ||
| 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', async function () { | ||
| var pems = await generate(null, { | ||
| clientCertificate: true, | ||
| clientCertificateCN: 'Custom User CN' | ||
| }); | ||
| it('should support sha256 algorithm', function (done) { | ||
| generate(null, { algorithm: 'sha256' }, function (err, pems_sha256) { | ||
| if (err) done(err); | ||
| assert.ok(forge.pki.certificateFromPem(pems_sha256.cert).siginfo.algorithmOid === forge.pki.oids['sha256WithRSAEncryption'], 'can generate sha256 certs'); | ||
| done(); | ||
| }); | ||
| const clientCert = new crypto.X509Certificate(pems.clientcert); | ||
| assert.include(clientCert.subject, 'CN=Custom User CN', 'should use custom client CN'); | ||
| }); | ||
| 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 | ||
| } | ||
| }); | ||
| it('should default to 2048 bit keysize', function (done) { | ||
| generate(function (err, pems) { | ||
| if (err) done(err); | ||
| var privateKey = forge.pki.privateKeyFromPem(pems.private); | ||
| assert.strictEqual(privateKey.n.bitLength(), 2048, 'default key size should be 2048 bits'); | ||
| done(); | ||
| }); | ||
| // 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 | ||
| }); | ||
| it('should default to 2048 bit keysize for client certificate', function (done) { | ||
| generate(null, {clientCertificate: true}, function (err, pems) { | ||
| if (err) done(err); | ||
| var clientPrivateKey = forge.pki.privateKeyFromPem(pems.clientprivate); | ||
| assert.strictEqual(clientPrivateKey.n.bitLength(), 2048, 'default client key size should be 2048 bits'); | ||
| done(); | ||
| }); | ||
| }); | ||
| 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'); | ||
| }); | ||
| }); |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Network access
Supply chain riskThis module accesses the network.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
54516
95.23%2
-33.33%14
55.56%1043
123.34%269
216.47%2
100%3
50%5
400%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed