1
- import type { CAOptions , Certificate , CertificateOptions , TlsOption } from './types'
1
+ import type { CAOptions , Cert , Certificate , CertificateExtension , CertificateOptions , CertPath , RandomSerialNumber , SubjectAltName , TlsOption } from './types'
2
2
import fs from 'node:fs'
3
3
import os from 'node:os'
4
4
import path from 'node:path'
5
5
import { consola as log } from 'consola'
6
6
import forge , { pki , tls } from 'node-forge'
7
7
import { config } from './config'
8
- import { debugLog , findFoldersWithFile , makeNumberPositive , runCommand } from './utils'
8
+ import { debugLog , findFoldersWithFile , makeNumberPositive , runCommand , getPrimaryDomain } from './utils'
9
9
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
+ }
14
35
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
+ }
17
58
18
59
/**
19
60
* Generate a random serial number for the Certificate
@@ -49,18 +90,32 @@ export function calculateValidityDates(options: {
49
90
return { notBefore, notAfter }
50
91
}
51
92
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 [ ] = [ ]
54
100
55
- // Basic Constraints
101
+ // Add basic constraints
56
102
extensions . push ( {
57
103
name : 'basicConstraints' ,
58
104
cA : options . isCA ?? false ,
59
105
critical : true ,
60
106
...( options . basicConstraints || { } ) ,
61
107
} )
62
108
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
64
119
if ( options . keyUsage ) {
65
120
extensions . push ( {
66
121
name : 'keyUsage' ,
@@ -69,22 +124,14 @@ function generateCertificateExtensions(options: CertificateOptions) {
69
124
} )
70
125
}
71
126
72
- // Extended Key Usage
127
+ // Add extended key usage if specified
73
128
if ( options . extKeyUsage ) {
74
129
extensions . push ( {
75
130
name : 'extKeyUsage' ,
76
131
...options . extKeyUsage ,
77
132
} )
78
133
}
79
134
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
-
88
135
return extensions
89
136
}
90
137
@@ -145,10 +192,20 @@ export async function createRootCA(options: CAOptions = {}): Promise<Certificate
145
192
}
146
193
}
147
194
195
+ /**
196
+ * Generates a certificate for one or multiple domains
197
+ * @param options Certificate generation options
198
+ * @returns Generated certificate
199
+ */
148
200
export async function generateCertificate ( options : CertificateOptions ) : Promise < Certificate > {
149
201
debugLog ( 'cert' , 'Generating new certificate' , options . verbose )
150
202
debugLog ( 'cert' , `Options: ${ JSON . stringify ( options ) } ` , options . verbose )
151
203
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
+
152
209
if ( ! options . rootCA ?. certificate || ! options . rootCA ?. privateKey ) {
153
210
throw new Error ( 'Root CA certificate and private key are required' )
154
211
}
@@ -158,30 +215,34 @@ export async function generateCertificate(options: CertificateOptions): Promise<
158
215
159
216
debugLog ( 'cert' , 'Generating 2048-bit RSA key pair for host certificate' , options . verbose )
160
217
const keySize = 2048
161
- // const keySize = options.keySize || 2048
162
218
const { privateKey, publicKey } = pki . rsa . generateKeyPair ( keySize )
163
219
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
+
165
223
const attributes = options . certificateAttributes || [
166
224
{ shortName : 'C' , value : options . countryName || config . countryName } ,
167
225
{ shortName : 'ST' , value : options . stateName || config . stateName } ,
168
226
{ shortName : 'L' , value : options . localityName || config . localityName } ,
169
227
{ shortName : 'O' , value : options . organizationName || config . organizationName } ,
170
- { shortName : 'CN' , value : options . commonName || config . commonName } ,
228
+ { shortName : 'CN' , value : commonName } ,
171
229
]
172
230
173
231
const { notBefore, notAfter } = calculateValidityDates ( {
174
232
validityDays : options . validityDays ,
175
233
verbose : options . verbose ,
176
234
} )
177
235
236
+ // Generate certificate
178
237
const cert = pki . createCertificate ( )
179
238
cert . publicKey = publicKey
180
239
cert . serialNumber = generateRandomSerial ( options . verbose )
181
240
cert . validity . notBefore = notBefore
182
241
cert . validity . notAfter = notAfter
183
242
cert . setSubject ( attributes )
184
243
cert . setIssuer ( caCert . subject . attributes )
244
+
245
+ // Set extensions with proper typing
185
246
cert . setExtensions ( generateCertificateExtensions ( options ) )
186
247
cert . sign ( caKey , forge . md . sha256 . create ( ) )
187
248
0 commit comments