1- import type { CAOptions , Certificate , CertificateOptions , TlsOption } from './types'
1+ import type { CAOptions , Cert , Certificate , CertificateExtension , CertificateOptions , CertPath , RandomSerialNumber , SubjectAltName , TlsOption } from './types'
22import fs from 'node:fs'
33import os from 'node:os'
44import path from 'node:path'
55import { consola as log } from 'consola'
66import forge , { pki , tls } from 'node-forge'
77import { config } from './config'
8- import { debugLog , findFoldersWithFile , makeNumberPositive , runCommand } from './utils'
8+ import { debugLog , findFoldersWithFile , makeNumberPositive , runCommand , getPrimaryDomain } from './utils'
99
10- interface Cert {
11- certificate : string
12- privateKey : string
13- }
10+ /**
11+ * Generates Subject Alternative Names for the certificate
12+ * @param options Certificate generation options
13+ * @returns Array of SubjectAltName objects
14+ */
15+ function generateSubjectAltNames ( options : CertificateOptions ) : SubjectAltName [ ] {
16+ const altNames : SubjectAltName [ ] = [ ]
17+
18+ // Add all domains to SAN
19+ const domains = new Set < string > ( )
20+
21+ // Add primary domain if explicitly set
22+ if ( options . domain ) {
23+ domains . add ( options . domain )
24+ }
25+
26+ // Add all domains from the domains array
27+ if ( options . domains ?. length ) {
28+ options . domains . forEach ( domain => domains . add ( domain ) )
29+ }
30+
31+ // Convert domains to SAN entries
32+ for ( const domain of domains ) {
33+ altNames . push ( { type : 2 , value : domain } )
34+ }
1435
15- type CertPath = string
16- type RandomSerialNumber = string
36+ // Add IP addresses if specified
37+ if ( options . altNameIPs ?. length ) {
38+ for ( const ip of options . altNameIPs ) {
39+ altNames . push ( { type : 7 , value : ip } )
40+ }
41+ }
42+
43+ // Add URIs if specified
44+ if ( options . altNameURIs ?. length ) {
45+ for ( const uri of options . altNameURIs ) {
46+ altNames . push ( { type : 6 , value : uri } )
47+ }
48+ }
49+
50+ // Add any additional subject alt names
51+ if ( options . subjectAltNames ?. length ) {
52+ altNames . push ( ...options . subjectAltNames )
53+ }
54+
55+ debugLog ( 'cert' , `Generated ${ altNames . length } Subject Alternative Names` , options . verbose )
56+ return altNames
57+ }
1758
1859/**
1960 * Generate a random serial number for the Certificate
@@ -49,18 +90,32 @@ export function calculateValidityDates(options: {
4990 return { notBefore, notAfter }
5091}
5192
52- function generateCertificateExtensions ( options : CertificateOptions ) {
53- const extensions = [ ]
93+ /**
94+ * Generates certificate extensions including subject alt names
95+ * @param options Certificate generation options
96+ * @returns Array of certificate extensions
97+ */
98+ function generateCertificateExtensions ( options : CertificateOptions ) : CertificateExtension [ ] {
99+ const extensions : CertificateExtension [ ] = [ ]
54100
55- // Basic Constraints
101+ // Add basic constraints
56102 extensions . push ( {
57103 name : 'basicConstraints' ,
58104 cA : options . isCA ?? false ,
59105 critical : true ,
60106 ...( options . basicConstraints || { } ) ,
61107 } )
62108
63- // Key Usage
109+ // Add subject alt names
110+ const altNames = generateSubjectAltNames ( options )
111+ if ( altNames . length > 0 ) {
112+ extensions . push ( {
113+ name : 'subjectAltName' ,
114+ altNames,
115+ } )
116+ }
117+
118+ // Add key usage if specified
64119 if ( options . keyUsage ) {
65120 extensions . push ( {
66121 name : 'keyUsage' ,
@@ -69,22 +124,14 @@ function generateCertificateExtensions(options: CertificateOptions) {
69124 } )
70125 }
71126
72- // Extended Key Usage
127+ // Add extended key usage if specified
73128 if ( options . extKeyUsage ) {
74129 extensions . push ( {
75130 name : 'extKeyUsage' ,
76131 ...options . extKeyUsage ,
77132 } )
78133 }
79134
80- // Subject Alt Names
81- if ( options . subjectAltNames && options . subjectAltNames . length > 0 ) {
82- extensions . push ( {
83- name : 'subjectAltName' ,
84- altNames : options . subjectAltNames ,
85- } )
86- }
87-
88135 return extensions
89136}
90137
@@ -145,10 +192,20 @@ export async function createRootCA(options: CAOptions = {}): Promise<Certificate
145192 }
146193}
147194
195+ /**
196+ * Generates a certificate for one or multiple domains
197+ * @param options Certificate generation options
198+ * @returns Generated certificate
199+ */
148200export async function generateCertificate ( options : CertificateOptions ) : Promise < Certificate > {
149201 debugLog ( 'cert' , 'Generating new certificate' , options . verbose )
150202 debugLog ( 'cert' , `Options: ${ JSON . stringify ( options ) } ` , options . verbose )
151203
204+ // Validate that at least one domain is specified
205+ if ( ! options . domain && ! options . domains ?. length ) {
206+ throw new Error ( 'Either domain or domains must be specified' )
207+ }
208+
152209 if ( ! options . rootCA ?. certificate || ! options . rootCA ?. privateKey ) {
153210 throw new Error ( 'Root CA certificate and private key are required' )
154211 }
@@ -158,30 +215,34 @@ export async function generateCertificate(options: CertificateOptions): Promise<
158215
159216 debugLog ( 'cert' , 'Generating 2048-bit RSA key pair for host certificate' , options . verbose )
160217 const keySize = 2048
161- // const keySize = options.keySize || 2048
162218 const { privateKey, publicKey } = pki . rsa . generateKeyPair ( keySize )
163219
164- // Allow for custom certificate attributes
220+ // Use the primary domain for the CN if no specific commonName is provided
221+ const commonName = options . commonName || getPrimaryDomain ( options )
222+
165223 const attributes = options . certificateAttributes || [
166224 { shortName : 'C' , value : options . countryName || config . countryName } ,
167225 { shortName : 'ST' , value : options . stateName || config . stateName } ,
168226 { shortName : 'L' , value : options . localityName || config . localityName } ,
169227 { shortName : 'O' , value : options . organizationName || config . organizationName } ,
170- { shortName : 'CN' , value : options . commonName || config . commonName } ,
228+ { shortName : 'CN' , value : commonName } ,
171229 ]
172230
173231 const { notBefore, notAfter } = calculateValidityDates ( {
174232 validityDays : options . validityDays ,
175233 verbose : options . verbose ,
176234 } )
177235
236+ // Generate certificate
178237 const cert = pki . createCertificate ( )
179238 cert . publicKey = publicKey
180239 cert . serialNumber = generateRandomSerial ( options . verbose )
181240 cert . validity . notBefore = notBefore
182241 cert . validity . notAfter = notAfter
183242 cert . setSubject ( attributes )
184243 cert . setIssuer ( caCert . subject . attributes )
244+
245+ // Set extensions with proper typing
185246 cert . setExtensions ( generateCertificateExtensions ( options ) )
186247 cert . sign ( caKey , forge . md . sha256 . create ( ) )
187248
0 commit comments