selfsigned
Advanced tools
+142
| var { assert } = require('chai'); | ||
| var crypto = require('crypto'); | ||
| describe('EC keys', function () { | ||
| var generate = require('../index').generate; | ||
| it('should generate EC certificate with P-256 curve (default)', async function () { | ||
| var pems = await generate(null, { keyType: 'ec' }); | ||
| 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 privateKey = crypto.createPrivateKey(pems.private); | ||
| assert.strictEqual(privateKey.asymmetricKeyType, 'ec', 'should be EC key'); | ||
| assert.strictEqual(privateKey.asymmetricKeyDetails.namedCurve, 'prime256v1', 'should use P-256 curve'); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.ok(cert.subject, 'cert has a subject'); | ||
| }); | ||
| it('should generate EC certificate with P-384 curve', async function () { | ||
| var pems = await generate(null, { keyType: 'ec', curve: 'P-384' }); | ||
| const privateKey = crypto.createPrivateKey(pems.private); | ||
| assert.strictEqual(privateKey.asymmetricKeyType, 'ec', 'should be EC key'); | ||
| assert.strictEqual(privateKey.asymmetricKeyDetails.namedCurve, 'secp384r1', 'should use P-384 curve'); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.ok(cert.publicKey, 'can generate P-384 EC certs'); | ||
| }); | ||
| it('should generate EC certificate with P-521 curve', async function () { | ||
| var pems = await generate(null, { keyType: 'ec', curve: 'P-521' }); | ||
| const privateKey = crypto.createPrivateKey(pems.private); | ||
| assert.strictEqual(privateKey.asymmetricKeyType, 'ec', 'should be EC key'); | ||
| assert.strictEqual(privateKey.asymmetricKeyDetails.namedCurve, 'secp521r1', 'should use P-521 curve'); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.ok(cert.publicKey, 'can generate P-521 EC certs'); | ||
| }); | ||
| it('should generate valid EC key pair that work together', async function () { | ||
| var pems = await generate(null, { keyType: 'ec', curve: 'P-256' }); | ||
| const testData = 'Hello, World!'; | ||
| 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, 'EC public key should verify signature from EC private key'); | ||
| }); | ||
| it('should support EC with sha256 algorithm', async function () { | ||
| var pems = await generate(null, { keyType: 'ec', algorithm: 'sha256' }); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.ok(cert.publicKey, 'can generate EC cert with sha256'); | ||
| }); | ||
| it('should support EC with sha384 algorithm', async function () { | ||
| var pems = await generate(null, { keyType: 'ec', curve: 'P-384', algorithm: 'sha384' }); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.ok(cert.publicKey, 'can generate EC cert with sha384'); | ||
| }); | ||
| it('should support EC with sha512 algorithm', async function () { | ||
| var pems = await generate(null, { keyType: 'ec', curve: 'P-521', algorithm: 'sha512' }); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.ok(cert.publicKey, 'can generate EC cert with sha512'); | ||
| }); | ||
| it('should generate EC client certificate', async function () { | ||
| var pems = await generate(null, { keyType: 'ec', clientCertificate: true }); | ||
| assert.ok(!!pems.clientcert, 'should include a client cert'); | ||
| assert.ok(!!pems.clientprivate, 'should include a client private key'); | ||
| assert.ok(!!pems.clientpublic, 'should include a client public key'); | ||
| const clientPrivateKey = crypto.createPrivateKey(pems.clientprivate); | ||
| assert.strictEqual(clientPrivateKey.asymmetricKeyType, 'ec', 'client key should be EC'); | ||
| }); | ||
| it('should support passphrase with EC keys', async function () { | ||
| const passphrase = 'ec-secret-passphrase'; | ||
| var pems = await generate(null, { keyType: 'ec', passphrase: passphrase }); | ||
| assert.include(pems.private, 'ENCRYPTED', 'EC private key should be encrypted'); | ||
| const privateKey = crypto.createPrivateKey({ | ||
| key: pems.private, | ||
| passphrase: passphrase | ||
| }); | ||
| assert.strictEqual(privateKey.asymmetricKeyType, 'ec', 'decrypted key should be EC'); | ||
| }); | ||
| it('should support using existing EC keyPair', async function () { | ||
| const firstPems = await generate(null, { keyType: 'ec', curve: 'P-256' }); | ||
| const secondPems = await generate(null, { | ||
| keyType: 'ec', | ||
| curve: 'P-256', | ||
| keyPair: { | ||
| privateKey: firstPems.private, | ||
| publicKey: firstPems.public | ||
| } | ||
| }); | ||
| assert.strictEqual(firstPems.private, secondPems.private, 'should use provided EC private key'); | ||
| assert.strictEqual(firstPems.public, secondPems.public, 'should use provided EC public key'); | ||
| }); | ||
| it('should support custom attributes with EC', async function () { | ||
| const attrs = [ | ||
| { name: 'commonName', value: 'ec-test.example.com' }, | ||
| { name: 'countryName', value: 'US' }, | ||
| { name: 'organizationName', value: 'EC Test Corp' } | ||
| ]; | ||
| var pems = await generate(attrs, { keyType: 'ec' }); | ||
| const cert = new crypto.X509Certificate(pems.cert); | ||
| assert.include(cert.subject, 'CN=ec-test.example.com', 'should include custom CN'); | ||
| assert.include(cert.subject, 'O=EC Test Corp', 'should include custom organization'); | ||
| }); | ||
| }); |
@@ -6,3 +6,6 @@ { | ||
| "Bash(npm test)", | ||
| "Bash(gh issue view:*)" | ||
| "Bash(gh issue view:*)", | ||
| "Bash(npx mocha:*)", | ||
| "Bash(gh issue comment:*)", | ||
| "Bash(gh issue close:*)" | ||
| ], | ||
@@ -9,0 +12,0 @@ "deny": [], |
+145
-4
@@ -21,2 +21,117 @@ declare enum ASN1Class { | ||
| /** | ||
| * Subject Alternative Name entry types: | ||
| * - 1: email (rfc822Name) | ||
| * - 2: DNS name | ||
| * - 6: URI | ||
| * - 7: IP address | ||
| */ | ||
| declare interface SubjectAltNameEntry { | ||
| /** | ||
| * Type of the alternative name: | ||
| * - 1: email (rfc822Name) | ||
| * - 2: DNS name | ||
| * - 6: URI | ||
| * - 7: IP address | ||
| */ | ||
| type: 1 | 2 | 6 | 7; | ||
| /** Value for types 1, 2, 6 (email, DNS, URI) */ | ||
| value?: string; | ||
| /** IP address for type 7 (IPv4 or IPv6) */ | ||
| ip?: string; | ||
| } | ||
| declare interface BasicConstraintsExtension { | ||
| name: 'basicConstraints'; | ||
| /** Is this a CA certificate? */ | ||
| cA?: boolean; | ||
| /** Maximum depth of valid certificate chain */ | ||
| pathLenConstraint?: number; | ||
| /** Mark extension as critical */ | ||
| critical?: boolean; | ||
| } | ||
| declare interface KeyUsageExtension { | ||
| name: 'keyUsage'; | ||
| digitalSignature?: boolean; | ||
| nonRepudiation?: boolean; | ||
| /** Also known as contentCommitment */ | ||
| contentCommitment?: boolean; | ||
| keyEncipherment?: boolean; | ||
| dataEncipherment?: boolean; | ||
| keyAgreement?: boolean; | ||
| /** For CA certificates */ | ||
| keyCertSign?: boolean; | ||
| /** For CA certificates */ | ||
| cRLSign?: boolean; | ||
| encipherOnly?: boolean; | ||
| decipherOnly?: boolean; | ||
| /** Mark extension as critical */ | ||
| critical?: boolean; | ||
| } | ||
| declare interface ExtKeyUsageExtension { | ||
| name: 'extKeyUsage'; | ||
| /** TLS server authentication */ | ||
| serverAuth?: boolean; | ||
| /** TLS client authentication */ | ||
| clientAuth?: boolean; | ||
| codeSigning?: boolean; | ||
| emailProtection?: boolean; | ||
| timeStamping?: boolean; | ||
| /** Mark extension as critical */ | ||
| critical?: boolean; | ||
| } | ||
| declare interface SubjectAltNameExtension { | ||
| name: 'subjectAltName'; | ||
| altNames: SubjectAltNameEntry[]; | ||
| /** Mark extension as critical */ | ||
| critical?: boolean; | ||
| } | ||
| declare type CertificateExtension = | ||
| | BasicConstraintsExtension | ||
| | KeyUsageExtension | ||
| | ExtKeyUsageExtension | ||
| | SubjectAltNameExtension; | ||
| declare interface ClientCertificateOptions { | ||
| /** | ||
| * Key size for the client certificate in bits (RSA only) | ||
| * @default 2048 | ||
| */ | ||
| keySize?: number | ||
| /** | ||
| * Key type for client certificate | ||
| * @default inherits from main keyType | ||
| */ | ||
| keyType?: 'rsa' | 'ec' | ||
| /** | ||
| * Elliptic curve for client certificate (EC only) | ||
| * @default "P-256" | ||
| */ | ||
| curve?: 'P-256' | 'P-384' | 'P-521' | ||
| /** | ||
| * Signature algorithm for client certificate | ||
| * @default inherits from main algorithm or "sha1" | ||
| */ | ||
| algorithm?: string | ||
| /** | ||
| * Client certificate's common name | ||
| * @default "John Doe jdoe123" | ||
| */ | ||
| cn?: string | ||
| /** | ||
| * The date before which the client certificate should not be valid | ||
| * @default now | ||
| */ | ||
| notBeforeDate?: Date | ||
| /** | ||
| * The date after which the client certificate should not be valid | ||
| * @default notBeforeDate + 1 year | ||
| */ | ||
| notAfterDate?: Date | ||
| } | ||
| declare interface SelfsignedOptions { | ||
@@ -36,3 +151,8 @@ /** | ||
| /** | ||
| * the size for the private key in bits | ||
| * Key type: "rsa" or "ec" (elliptic curve) | ||
| * @default "rsa" | ||
| */ | ||
| keyType?: 'rsa' | 'ec' | ||
| /** | ||
| * the size for the private key in bits (RSA only) | ||
| * @default 2048 | ||
@@ -42,6 +162,24 @@ */ | ||
| /** | ||
| * additional extensions for the certificate | ||
| * The elliptic curve to use (EC only): "P-256", "P-384", or "P-521" | ||
| * @default "P-256" | ||
| */ | ||
| extensions?: any[]; | ||
| curve?: 'P-256' | 'P-384' | 'P-521' | ||
| /** | ||
| * Certificate extensions. Supports basicConstraints, keyUsage, extKeyUsage, and subjectAltName. | ||
| * If not provided, defaults are used including DNS SAN matching commonName. | ||
| * @example | ||
| * ```typescript | ||
| * extensions: [ | ||
| * { name: 'basicConstraints', cA: false }, | ||
| * { name: 'keyUsage', digitalSignature: true, keyEncipherment: true }, | ||
| * { name: 'subjectAltName', altNames: [ | ||
| * { type: 2, value: 'localhost' }, | ||
| * { type: 7, ip: '127.0.0.1' }, | ||
| * { type: 7, ip: '::1' } | ||
| * ]} | ||
| * ] | ||
| * ``` | ||
| */ | ||
| extensions?: CertificateExtension[]; | ||
| /** | ||
| * The signature algorithm: sha256, sha384, sha512 or sha1 | ||
@@ -58,8 +196,10 @@ * @default "sha1" | ||
| * generate client cert signed by the original key | ||
| * Can be `true` for defaults or an options object | ||
| * @default false | ||
| */ | ||
| clientCertificate?: boolean | ||
| clientCertificate?: boolean | ClientCertificateOptions | ||
| /** | ||
| * client certificate's common name | ||
| * @default "John Doe jdoe123" | ||
| * @deprecated Use clientCertificate.cn instead | ||
| */ | ||
@@ -70,2 +210,3 @@ clientCertificateCN?: string | ||
| * @default 2048 | ||
| * @deprecated Use clientCertificate.keySize instead | ||
| */ | ||
@@ -72,0 +213,0 @@ clientCertificateKeySize?: number |
+225
-75
@@ -34,4 +34,10 @@ const { X509CertificateGenerator, X509Certificate, X509ChainBuilder, BasicConstraintsExtension, KeyUsagesExtension, KeyUsageFlags, ExtendedKeyUsageExtension, ExtendedKeyUsage, SubjectAlternativeNameExtension, GeneralName } = require("@peculiar/x509"); | ||
| function getSigningAlgorithm(key) { | ||
| const hashAlg = getAlgorithmName(key); | ||
| function getSigningAlgorithm(hashKey, keyType) { | ||
| const hashAlg = getAlgorithmName(hashKey); | ||
| if (keyType === 'ec') { | ||
| return { | ||
| name: "ECDSA", | ||
| hash: hashAlg | ||
| }; | ||
| } | ||
| return { | ||
@@ -43,2 +49,114 @@ name: "RSASSA-PKCS1-v1_5", | ||
| function getKeyAlgorithm(options) { | ||
| const keyType = options.keyType || 'rsa'; | ||
| const hashAlg = getAlgorithmName(options.algorithm || 'sha1'); | ||
| if (keyType === 'ec') { | ||
| const curve = options.curve || 'P-256'; | ||
| return { | ||
| name: "ECDSA", | ||
| namedCurve: curve | ||
| }; | ||
| } | ||
| return { | ||
| name: "RSASSA-PKCS1-v1_5", | ||
| modulusLength: options.keySize || 2048, | ||
| publicExponent: new Uint8Array([1, 0, 1]), | ||
| hash: hashAlg | ||
| }; | ||
| } | ||
| // Build extensions array from options or use defaults | ||
| // Supports the old node-forge extension format for backwards compatibility | ||
| function buildExtensions(userExtensions, commonName) { | ||
| if (!userExtensions || userExtensions.length === 0) { | ||
| // Default extensions | ||
| return [ | ||
| 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) | ||
| ]; | ||
| } | ||
| // Convert user extensions from node-forge format to @peculiar/x509 format | ||
| const extensions = []; | ||
| for (const ext of userExtensions) { | ||
| const critical = ext.critical || false; | ||
| switch (ext.name) { | ||
| case 'basicConstraints': | ||
| extensions.push(new BasicConstraintsExtension( | ||
| ext.cA || false, | ||
| ext.pathLenConstraint, | ||
| critical | ||
| )); | ||
| break; | ||
| case 'keyUsage': | ||
| let flags = 0; | ||
| if (ext.digitalSignature) flags |= KeyUsageFlags.digitalSignature; | ||
| if (ext.nonRepudiation || ext.contentCommitment) flags |= KeyUsageFlags.nonRepudiation; | ||
| if (ext.keyEncipherment) flags |= KeyUsageFlags.keyEncipherment; | ||
| if (ext.dataEncipherment) flags |= KeyUsageFlags.dataEncipherment; | ||
| if (ext.keyAgreement) flags |= KeyUsageFlags.keyAgreement; | ||
| if (ext.keyCertSign) flags |= KeyUsageFlags.keyCertSign; | ||
| if (ext.cRLSign) flags |= KeyUsageFlags.cRLSign; | ||
| if (ext.encipherOnly) flags |= KeyUsageFlags.encipherOnly; | ||
| if (ext.decipherOnly) flags |= KeyUsageFlags.decipherOnly; | ||
| extensions.push(new KeyUsagesExtension(flags, critical)); | ||
| break; | ||
| case 'extKeyUsage': | ||
| const usages = []; | ||
| if (ext.serverAuth) usages.push(ExtendedKeyUsage.serverAuth); | ||
| if (ext.clientAuth) usages.push(ExtendedKeyUsage.clientAuth); | ||
| if (ext.codeSigning) usages.push(ExtendedKeyUsage.codeSigning); | ||
| if (ext.emailProtection) usages.push(ExtendedKeyUsage.emailProtection); | ||
| if (ext.timeStamping) usages.push(ExtendedKeyUsage.timeStamping); | ||
| extensions.push(new ExtendedKeyUsageExtension(usages, critical)); | ||
| break; | ||
| case 'subjectAltName': | ||
| const altNames = (ext.altNames || []).map(alt => { | ||
| // node-forge type values: | ||
| // 1 = email (rfc822Name) | ||
| // 2 = DNS | ||
| // 6 = URI | ||
| // 7 = IP | ||
| switch (alt.type) { | ||
| case 1: // email | ||
| return { type: 'email', value: alt.value }; | ||
| case 2: // DNS | ||
| return { type: 'dns', value: alt.value }; | ||
| case 6: // URI | ||
| return { type: 'url', value: alt.value }; | ||
| case 7: // IP | ||
| return { type: 'ip', value: alt.ip || alt.value }; | ||
| default: | ||
| // Try to infer type from properties | ||
| if (alt.ip) return { type: 'ip', value: alt.ip }; | ||
| if (alt.dns) return { type: 'dns', value: alt.dns }; | ||
| if (alt.email) return { type: 'email', value: alt.email }; | ||
| if (alt.uri || alt.url) return { type: 'url', value: alt.uri || alt.url }; | ||
| return { type: 'dns', value: alt.value }; | ||
| } | ||
| }); | ||
| extensions.push(new SubjectAlternativeNameExtension(altNames, critical)); | ||
| break; | ||
| default: | ||
| // Skip unknown extensions with a warning | ||
| console.warn(`Unknown extension "${ext.name}" ignored`); | ||
| } | ||
| } | ||
| return extensions; | ||
| } | ||
| // Convert attributes from node-forge format to X509 name format | ||
@@ -62,46 +180,69 @@ function convertAttributes(attrs) { | ||
| // Detect key type from PEM key using Node.js crypto | ||
| function detectKeyType(pemKey) { | ||
| const keyObject = nodeCrypto.createPrivateKey(pemKey); | ||
| return keyObject.asymmetricKeyType; // 'rsa' or 'ec' | ||
| } | ||
| // Map Node.js curve names to Web Crypto curve names | ||
| function normalizeECCurve(curveName) { | ||
| const curveMap = { | ||
| 'prime256v1': 'P-256', | ||
| 'secp384r1': 'P-384', | ||
| 'secp521r1': 'P-521', | ||
| 'P-256': 'P-256', | ||
| 'P-384': 'P-384', | ||
| 'P-521': 'P-521' | ||
| }; | ||
| return curveMap[curveName] || curveName; | ||
| } | ||
| // Get EC curve from key object | ||
| function getECCurve(keyObject) { | ||
| const details = keyObject.asymmetricKeyDetails; | ||
| if (details && details.namedCurve) { | ||
| return normalizeECCurve(details.namedCurve); | ||
| } | ||
| return 'P-256'; // default | ||
| } | ||
| // 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-----/); | ||
| async function importPrivateKey(pemKey, algorithm, keyType) { | ||
| // Auto-detect key type if not provided | ||
| const keyObject = nodeCrypto.createPrivateKey(pemKey); | ||
| const detectedKeyType = keyObject.asymmetricKeyType; | ||
| const actualKeyType = keyType || detectedKeyType; | ||
| if (pkcs8Match) { | ||
| const pemContents = pkcs8Match[1].replace(/\s/g, ''); | ||
| const binaryDer = Buffer.from(pemContents, 'base64'); | ||
| return await crypto.subtle.importKey( | ||
| 'pkcs8', | ||
| binaryDer, | ||
| { | ||
| 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: 'RSASSA-PKCS1-v1_5', | ||
| hash: getAlgorithmName(algorithm), | ||
| }, | ||
| true, | ||
| ['sign'] | ||
| ); | ||
| // Convert to PKCS#8 format | ||
| 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'); | ||
| let importAlgorithm; | ||
| if (actualKeyType === 'ec') { | ||
| const curve = getECCurve(keyObject); | ||
| importAlgorithm = { | ||
| name: 'ECDSA', | ||
| namedCurve: curve | ||
| }; | ||
| } else { | ||
| throw new Error('Unsupported private key format. Expected PKCS#8 or PKCS#1 RSA key.'); | ||
| importAlgorithm = { | ||
| name: 'RSASSA-PKCS1-v1_5', | ||
| hash: getAlgorithmName(algorithm) | ||
| }; | ||
| } | ||
| return await crypto.subtle.importKey( | ||
| 'pkcs8', | ||
| binaryDer, | ||
| importAlgorithm, | ||
| true, | ||
| ['sign'] | ||
| ); | ||
| } | ||
| async function importPublicKey(pemKey, algorithm) { | ||
| async function importPublicKey(pemKey, algorithm, keyType, curve) { | ||
| const pemContents = pemKey | ||
@@ -114,9 +255,19 @@ .replace(/-----BEGIN PUBLIC KEY-----/, '') | ||
| let importAlgorithm; | ||
| if (keyType === 'ec') { | ||
| importAlgorithm = { | ||
| name: 'ECDSA', | ||
| namedCurve: curve || 'P-256' | ||
| }; | ||
| } else { | ||
| importAlgorithm = { | ||
| name: 'RSASSA-PKCS1-v1_5', | ||
| hash: getAlgorithmName(algorithm) | ||
| }; | ||
| } | ||
| return await crypto.subtle.importKey( | ||
| 'spki', | ||
| binaryDer, | ||
| { | ||
| name: 'RSASSA-PKCS1-v1_5', | ||
| hash: getAlgorithmName(algorithm), | ||
| }, | ||
| importAlgorithm, | ||
| true, | ||
@@ -173,3 +324,4 @@ ['verify'] | ||
| const subjectName = convertAttributes(attrs); | ||
| const signingAlg = getSigningAlgorithm(options.algorithm); | ||
| const keyType = options.keyType || 'rsa'; | ||
| const signingAlg = getSigningAlgorithm(options.algorithm, keyType); | ||
@@ -181,11 +333,3 @@ // Extract common name for SAN extension | ||
| // 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) | ||
| ]; | ||
| const extensions = buildExtensions(options.extensions, commonName); | ||
@@ -197,3 +341,3 @@ let cert; | ||
| const caCert = new X509Certificate(ca.cert); | ||
| const caPrivateKey = await importPrivateKey(ca.key, options.algorithm || "sha256"); | ||
| const caPrivateKey = await importPrivateKey(ca.key, options.algorithm || "sha256", keyType); | ||
@@ -283,10 +427,15 @@ cert = await X509CertificateGenerator.create({ | ||
| const clientCN = clientOpts.cn || options.clientCertificateCN || "John Doe jdoe123"; | ||
| // Client cert uses same key type and curve as main cert by default | ||
| const clientKeyType = clientOpts.keyType || keyType; | ||
| const clientCurve = clientOpts.curve || options.curve || 'P-256'; | ||
| const clientKeyAlg = getKeyAlgorithm({ | ||
| keyType: clientKeyType, | ||
| keySize: clientKeySize, | ||
| algorithm: clientAlgorithm, | ||
| curve: clientCurve | ||
| }); | ||
| const clientKeyPair = await crypto.subtle.generateKey( | ||
| { | ||
| name: "RSASSA-PKCS1-v1_5", | ||
| modulusLength: clientKeySize, | ||
| publicExponent: new Uint8Array([1, 0, 1]), | ||
| hash: getAlgorithmName(clientAlgorithm), | ||
| }, | ||
| clientKeyAlg, | ||
| true, | ||
@@ -322,4 +471,4 @@ ["sign", "verify"] | ||
| // Signing algorithm for client cert (can differ from main cert) | ||
| const clientSigningAlg = getSigningAlgorithm(clientAlgorithm); | ||
| // Signing algorithm for client cert - uses main key type since signed by root | ||
| const clientSigningAlg = getSigningAlgorithm(clientAlgorithm, keyType); | ||
@@ -382,3 +531,5 @@ // Create client cert signed by root key | ||
| * @param {object} options | ||
| * @param {number} [options.keySize=2048] the size for the private key in bits | ||
| * @param {string} [options.keyType="rsa"] Key type: "rsa" or "ec" (elliptic curve) | ||
| * @param {number} [options.keySize=2048] the size for the private key in bits (RSA only) | ||
| * @param {string} [options.curve="P-256"] The elliptic curve to use: "P-256", "P-384", or "P-521" (EC only) | ||
| * @param {object} [options.extensions] additional extensions for the certificate | ||
@@ -389,3 +540,5 @@ * @param {string} [options.algorithm="sha1"] The signature algorithm sha256, sha384, sha512 or sha1 | ||
| * @param {boolean|object} [options.clientCertificate=false] Generate client cert signed by the original key. Can be `true` for defaults or an options object. | ||
| * @param {number} [options.clientCertificate.keySize=2048] Key size for the client certificate in bits | ||
| * @param {number} [options.clientCertificate.keySize=2048] Key size for the client certificate in bits (RSA only) | ||
| * @param {string} [options.clientCertificate.keyType] Key type for client cert (defaults to main keyType) | ||
| * @param {string} [options.clientCertificate.curve] Elliptic curve for client cert (EC only) | ||
| * @param {string} [options.clientCertificate.algorithm] Signature algorithm for client cert (defaults to options.algorithm or "sha1") | ||
@@ -407,3 +560,4 @@ * @param {string} [options.clientCertificate.cn="John Doe jdoe123"] Client certificate's common name | ||
| const keySize = options.keySize || 2048; | ||
| const keyType = options.keyType || 'rsa'; | ||
| const curve = options.curve || 'P-256'; | ||
@@ -415,14 +569,10 @@ let keyPair; | ||
| keyPair = { | ||
| privateKey: await importPrivateKey(options.keyPair.privateKey, options.algorithm || "sha1"), | ||
| publicKey: await importPublicKey(options.keyPair.publicKey, options.algorithm || "sha1") | ||
| privateKey: await importPrivateKey(options.keyPair.privateKey, options.algorithm || "sha1", keyType), | ||
| publicKey: await importPublicKey(options.keyPair.publicKey, options.algorithm || "sha1", keyType, curve) | ||
| }; | ||
| } else { | ||
| // Generate new key pair | ||
| // Generate new key pair using appropriate algorithm | ||
| const keyAlg = getKeyAlgorithm(options); | ||
| keyPair = await crypto.subtle.generateKey( | ||
| { | ||
| name: "RSASSA-PKCS1-v1_5", | ||
| modulusLength: keySize, | ||
| publicExponent: new Uint8Array([1, 0, 1]), | ||
| hash: getAlgorithmName(options.algorithm || "sha1"), | ||
| }, | ||
| keyAlg, | ||
| true, | ||
@@ -429,0 +579,0 @@ ["sign", "verify"] |
+2
-2
| { | ||
| "name": "selfsigned", | ||
| "version": "5.2.0", | ||
| "version": "5.4.0", | ||
| "description": "Generate self signed certificates private and public keys", | ||
@@ -8,3 +8,3 @@ "main": "index.js", | ||
| "scripts": { | ||
| "test": "mocha -t 5000" | ||
| "test": "mocha -t 10000" | ||
| }, | ||
@@ -11,0 +11,0 @@ "repository": { |
+159
-2
@@ -42,3 +42,5 @@ # selfsigned | ||
| const pems = await selfsigned.generate(null, { | ||
| keySize: 2048, // the size for the private key in bits (default: 2048) | ||
| keyType: 'rsa', // key type: 'rsa' or 'ec' (default: 'rsa') | ||
| keySize: 2048, // the size for the private key in bits (default: 2048, RSA only) | ||
| curve: 'P-256', // elliptic curve: 'P-256', 'P-384', or 'P-521' (default: 'P-256', EC only) | ||
| notBeforeDate: new Date(), // start of certificate validity (default: now) | ||
@@ -85,2 +87,155 @@ notAfterDate: new Date('2026-01-01'), // end of certificate validity (default: notBeforeDate + 365 days) | ||
| ### Custom Extensions | ||
| You can customize certificate extensions using the `extensions` option. This is useful for adding Subject Alternative Names (SANs) with IPv6 addresses, custom key usage, and more. | ||
| ```js | ||
| const pems = await selfsigned.generate( | ||
| [{ name: 'commonName', value: 'localhost' }], | ||
| { | ||
| 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 | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
| ); | ||
| ``` | ||
| #### Supported Extensions | ||
| **basicConstraints** | ||
| ```js | ||
| { | ||
| name: 'basicConstraints', | ||
| cA: true, // is this a CA certificate? | ||
| pathLenConstraint: 0, // max depth of valid cert chain (optional) | ||
| critical: true // mark as critical extension | ||
| } | ||
| ``` | ||
| **keyUsage** | ||
| ```js | ||
| { | ||
| name: 'keyUsage', | ||
| digitalSignature: true, | ||
| nonRepudiation: true, | ||
| keyEncipherment: true, | ||
| dataEncipherment: true, | ||
| keyAgreement: true, | ||
| keyCertSign: true, // for CA certificates | ||
| cRLSign: true, // for CA certificates | ||
| encipherOnly: true, | ||
| decipherOnly: true, | ||
| critical: true | ||
| } | ||
| ``` | ||
| **extKeyUsage** (Extended Key Usage) | ||
| ```js | ||
| { | ||
| name: 'extKeyUsage', | ||
| serverAuth: true, // TLS server authentication | ||
| clientAuth: true, // TLS client authentication | ||
| codeSigning: true, | ||
| emailProtection: true, | ||
| timeStamping: true | ||
| } | ||
| ``` | ||
| **subjectAltName** (Subject Alternative Name) | ||
| ```js | ||
| { | ||
| name: 'subjectAltName', | ||
| altNames: [ | ||
| { type: 1, value: 'user@example.com' }, // email (rfc822Name) | ||
| { type: 2, value: 'example.com' }, // DNS name | ||
| { type: 2, value: '*.example.com' }, // wildcard DNS | ||
| { type: 6, value: 'http://example.com/webid' }, // URI | ||
| { type: 7, ip: '127.0.0.1' }, // IPv4 address | ||
| { type: 7, ip: '::1' } // IPv6 address | ||
| ] | ||
| } | ||
| ``` | ||
| #### Default Extensions | ||
| When no `extensions` option is provided (or an empty array), the following defaults are used: | ||
| ```js | ||
| [ | ||
| { name: 'basicConstraints', cA: false, critical: true }, | ||
| { name: 'keyUsage', digitalSignature: true, keyEncipherment: true, critical: true }, | ||
| { name: 'extKeyUsage', serverAuth: true, clientAuth: true }, | ||
| { name: 'subjectAltName', altNames: [ | ||
| { type: 2, value: commonName }, | ||
| // For localhost, also includes: { type: 7, ip: '127.0.0.1' } | ||
| ]} | ||
| ] | ||
| ``` | ||
| ### Elliptic Curve (EC) Keys | ||
| By default, selfsigned generates RSA keys. You can generate certificates using elliptic curve cryptography instead, which provides equivalent security with smaller key sizes and faster operations. | ||
| ```js | ||
| // Generate EC certificate with P-256 curve (default) | ||
| const pems = await selfsigned.generate(null, { keyType: 'ec' }); | ||
| // Generate EC certificate with P-384 curve | ||
| const pems = await selfsigned.generate(null, { keyType: 'ec', curve: 'P-384' }); | ||
| // Generate EC certificate with P-521 curve and SHA-512 | ||
| const pems = await selfsigned.generate(null, { | ||
| keyType: 'ec', | ||
| curve: 'P-521', | ||
| algorithm: 'sha512' | ||
| }); | ||
| ``` | ||
| **Supported curves:** | ||
| - `P-256` (default) - 128-bit security, fastest | ||
| - `P-384` - 192-bit security | ||
| - `P-521` - 256-bit security, strongest | ||
| EC keys work with all other options including `clientCertificate`, `passphrase`, `ca`, and `keyPair`: | ||
| ```js | ||
| // EC certificate with encrypted private key | ||
| const pems = await selfsigned.generate(null, { | ||
| keyType: 'ec', | ||
| passphrase: 'secret' | ||
| }); | ||
| // EC certificate with client certificate | ||
| const pems = await selfsigned.generate(null, { | ||
| keyType: 'ec', | ||
| clientCertificate: true | ||
| }); | ||
| // Reuse existing EC key pair | ||
| const pems = await selfsigned.generate(null, { | ||
| keyType: 'ec', | ||
| curve: 'P-256', | ||
| keyPair: { | ||
| publicKey: existingPublicKey, | ||
| privateKey: existingPrivateKey | ||
| } | ||
| }); | ||
| ``` | ||
| ### Using Your Own Keys | ||
@@ -239,3 +394,5 @@ | ||
| cn: 'jdoe', // common name (default: 'John Doe jdoe123') | ||
| keySize: 4096, // key size in bits (default: 2048) | ||
| keyType: 'rsa', // key type: 'rsa' or 'ec' (default: inherits from parent) | ||
| keySize: 4096, // key size in bits (default: 2048, RSA only) | ||
| curve: 'P-256', // elliptic curve (default: 'P-256', EC only) | ||
| algorithm: 'sha256', // signature algorithm (default: inherits from parent or 'sha1') | ||
@@ -242,0 +399,0 @@ notBeforeDate: new Date(), // validity start (default: now) |
+193
-0
@@ -373,2 +373,195 @@ var { assert } = require('chai'); | ||
| 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 () { | ||
@@ -375,0 +568,0 @@ const passphrase = 'my-secret-passphrase'; |
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.
88146
40.99%15
7.14%1734
46.7%475
49.37%4
33.33%5
25%