@@ -7,72 +7,228 @@ import forge, { pki, tls } from 'node-forge'
7
7
import { resolveConfig } from './config'
8
8
import type { GenerateCertOptions } from './types'
9
9
10
- export async function generateCert ( options ?: GenerateCertOptions ) {
11
- log . debug ( 'generateCert' , options )
10
+ const makeNumberPositive = ( hexString : string ) => {
11
+ let mostSignificativeHexDigitAsInt = Number . parseInt ( hexString [ 0 ] , 16 )
12
12
13
- const opts = await resolveConfig ( options )
14
- const keys = pki . rsa . generateKeyPair ( 2048 )
15
- const cert = pki . createCertificate ( )
16
- cert . publicKey = keys . publicKey
13
+ if ( mostSignificativeHexDigitAsInt < 8 ) return hexString
14
+
15
+ mostSignificativeHexDigitAsInt -= 8
16
+ return mostSignificativeHexDigitAsInt . toString ( ) + hexString . substring ( 1 )
17
+ }
18
+
19
+ // Generate a random serial number for the Certificate
20
+ const randomSerialNumber = ( ) => {
21
+ return makeNumberPositive ( forge . util . bytesToHex ( forge . random . getBytesSync ( 20 ) ) )
22
+ }
17
23
18
- // NOTE: serialNumber is the hex encoded value of an ASN.1 INTEGER.
19
- // Conforming CAs should ensure serialNumber is:
20
- // - no more than 20 octets
21
- // - non-negative (prefix a '00' if your value starts with a '1' bit)
22
- cert . serialNumber = `01${ crypto . randomBytes ( 19 ) . toString ( 'hex' ) } ` // 1 octet = 8 bits = 1 byte = 2 hex chars
23
- cert . validity . notBefore = new Date ( )
24
- cert . validity . notAfter = new Date ( new Date ( ) . getTime ( ) + 1000 * 60 * 60 * 24 * ( opts . validityDays ?? 1 ) )
24
+ // Get the Not Before Date for a Certificate (will be valid from 2 days ago)
25
+ const getCertNotBefore = ( ) => {
26
+ const twoDaysAgo = new Date ( Date . now ( ) - 60 * 60 * 24 * 2 * 1000 )
27
+ const year = twoDaysAgo . getFullYear ( )
28
+ const month = ( twoDaysAgo . getMonth ( ) + 1 ) . toString ( ) . padStart ( 2 , '0' )
29
+ const day = twoDaysAgo . getDate ( )
30
+ return new Date ( `${ year } -${ month } -${ day } 00:00:00Z` )
31
+ }
25
32
26
- const attrs = [
33
+ // Get Certificate Expiration Date (Valid for 90 Days)
34
+ const getCertNotAfter = ( notBefore : any ) => {
35
+ const ninetyDaysLater = new Date ( notBefore . getTime ( ) + 60 * 60 * 24 * 90 * 1000 )
36
+ const year = ninetyDaysLater . getFullYear ( )
37
+ const month = ( ninetyDaysLater . getMonth ( ) + 1 ) . toString ( ) . padStart ( 2 , '0' )
38
+ const day = ninetyDaysLater . getDate ( )
39
+ return new Date ( `${ year } -${ month } -${ day } 23:59:59Z` )
40
+ }
41
+
42
+ // Get CA Expiration Date (Valid for 100 Years)
43
+ const getCANotAfter = ( notBefore : any ) => {
44
+ const year = notBefore . getFullYear ( ) + 100
45
+ const month = ( notBefore . getMonth ( ) + 1 ) . toString ( ) . padStart ( 2 , '0' )
46
+ const day = notBefore . getDate ( )
47
+ return new Date ( `${ year } -${ month } -${ day } 23:59:59Z` )
48
+ }
49
+
50
+ const DEFAULT_C = 'US'
51
+ const DEFAULT_ST = 'California'
52
+ const DEFAULT_L = 'Melbourne'
53
+ const DEFAULT_O = 'Tlsx Stacks RootCA'
54
+
55
+ // Generate a new Root CA Certificate
56
+ export async function CreateRootCA ( ) {
57
+ // Create a new Keypair for the Root CA
58
+ const { privateKey, publicKey } = forge . pki . rsa . generateKeyPair ( 2048 )
59
+
60
+ // Define the attributes for the new Root CA
61
+ const attributes = [
27
62
{
28
- name : 'countryName ' ,
29
- value : opts . countryName ?? 'US' ,
63
+ shortName : 'C ' ,
64
+ value : DEFAULT_C ,
30
65
} ,
31
66
{
32
67
shortName : 'ST' ,
33
- value : opts . stateName ?? 'California' ,
68
+ value : DEFAULT_ST ,
69
+ } ,
70
+ {
71
+ shortName : 'L' ,
72
+ value : DEFAULT_L ,
34
73
} ,
35
74
{
36
- name : 'organizationName ' ,
37
- value : opts . organizationName ?? 'tlsx stacks.localhost' , // simply for a recognizable name
75
+ shortName : 'CN ' ,
76
+ value : DEFAULT_O ,
38
77
} ,
39
78
]
40
79
41
- cert . setSubject ( attrs )
42
- cert . setIssuer ( attrs )
80
+ const extensions = [
81
+ {
82
+ name : 'basicConstraints' ,
83
+ cA : true ,
84
+ } ,
85
+ {
86
+ name : 'keyUsage' ,
87
+ keyCertSign : true ,
88
+ cRLSign : true ,
89
+ } ,
90
+ ]
91
+
92
+ // Create an empty Certificate
93
+ const cert = forge . pki . createCertificate ( )
94
+
95
+ // Set the Certificate attributes for the new Root CA
96
+ cert . publicKey = publicKey
97
+ cert . privateKey = privateKey
98
+ cert . serialNumber = randomSerialNumber ( )
99
+ cert . validity . notBefore = getCertNotBefore ( )
100
+ cert . validity . notAfter = getCANotAfter ( cert . validity . notBefore )
101
+ cert . setSubject ( attributes )
102
+ cert . setIssuer ( attributes )
103
+ cert . setExtensions ( extensions )
104
+
105
+ // Self-sign the Certificate
106
+ cert . sign ( privateKey , forge . md . sha512 . create ( ) )
107
+
108
+ // Convert to PEM format
109
+ const pemCert = forge . pki . certificateToPem ( cert )
110
+ const pemKey = forge . pki . privateKeyToPem ( privateKey )
111
+
112
+ // Return the PEM encoded cert and private key
113
+ return {
114
+ certificate : pemCert ,
115
+ privateKey : pemKey ,
116
+ notBefore : cert . validity . notBefore ,
117
+ notAfter : cert . validity . notAfter ,
118
+ }
119
+ }
43
120
44
- // add alt names so that the browser won't complain
45
- cert . setExtensions ( [
121
+ export async function generateCert (
122
+ hostCertCN : string ,
123
+ domain : string ,
124
+ rootCAObject : { certificate : string ; privateKey : string } ,
125
+ options ?: GenerateCertOptions ,
126
+ ) {
127
+ log . debug ( 'generateCert' , options )
128
+
129
+ if ( ! hostCertCN . toString ( ) . trim ( ) ) throw new Error ( '"hostCertCN" must be a String' )
130
+ if ( ! domain . toString ( ) . trim ( ) ) throw new Error ( '"validDomain" must be a String' )
131
+
132
+ if ( ! rootCAObject || ! rootCAObject . certificate || ! rootCAObject . privateKey )
133
+ throw new Error ( '"rootCAObject" must be an Object with the properties "certificate" & "privateKey"' )
134
+
135
+ const opts = await resolveConfig ( options )
136
+ // Convert the Root CA PEM details, to a forge Object
137
+ const caCert = pki . certificateFromPem ( rootCAObject . certificate )
138
+ const caKey = pki . privateKeyFromPem ( rootCAObject . privateKey )
139
+
140
+ // Create a new Keypair for the Host Certificate
141
+ const hostKeys = pki . rsa . generateKeyPair ( 2048 )
142
+ // Define the attributes/properties for the Host Certificate
143
+ const attributes = [
46
144
{
47
- name : 'subjectAltName' ,
48
- altNames : [
49
- ...( opts . altNameURIs !== undefined ? opts . altNameURIs . map ( ( uri ) => ( { type : 6 , value : uri } ) ) : [ ] ) ,
145
+ shortName : 'C' ,
146
+ value : DEFAULT_C ,
147
+ } ,
148
+ {
149
+ shortName : 'ST' ,
150
+ value : DEFAULT_ST ,
151
+ } ,
152
+ {
153
+ shortName : 'L' ,
154
+ value : DEFAULT_L ,
155
+ } ,
156
+ {
157
+ shortName : 'CN' ,
158
+ value : hostCertCN ,
159
+ } ,
160
+ ]
50
161
51
- ...( opts . altNameIPs !== undefined ? opts . altNameIPs . map ( ( uri ) => ( { type : 7 , ip : uri } ) ) : [ ] ) ,
52
- ] ,
162
+ const extensions = [
163
+ // {
164
+ // name: 'basicConstraints',
165
+ // cA: true
166
+ // },
167
+ {
168
+ name : 'nsCertType' ,
169
+ server : true ,
170
+ } ,
171
+ {
172
+ name : 'subjectKeyIdentifier' ,
173
+ } ,
174
+ {
175
+ name : 'authorityKeyIdentifier' ,
176
+ authorityCertIssuer : true ,
177
+ serialNumber : caCert . serialNumber ,
178
+ } ,
179
+ {
180
+ name : 'keyUsage' ,
181
+ digitalSignature : true ,
182
+ nonRepudiation : true ,
183
+ keyEncipherment : true ,
184
+ } ,
185
+ {
186
+ name : 'extKeyUsage' ,
187
+ serverAuth : true ,
188
+ } ,
189
+ {
190
+ name : 'subjectAltName' ,
191
+ altNames : { type : 2 , value : domain } ,
53
192
} ,
54
- ] )
193
+ ]
194
+
195
+ // Create an empty Certificate
196
+ const newHostCert = forge . pki . createCertificate ( )
197
+ newHostCert . publicKey = hostKeys . publicKey
55
198
56
- // self-sign certificate
57
- cert . sign ( keys . privateKey )
199
+ // Set the attributes for the new Host Certificate
200
+ newHostCert . publicKey = hostKeys . publicKey
201
+ newHostCert . serialNumber = randomSerialNumber ( )
202
+ newHostCert . validity . notBefore = getCertNotBefore ( )
203
+ newHostCert . validity . notAfter = getCertNotAfter ( newHostCert . validity . notBefore )
204
+ newHostCert . setSubject ( attributes )
205
+ newHostCert . setIssuer ( caCert . subject . attributes )
206
+ newHostCert . setExtensions ( extensions )
58
207
59
- // convert a Forge certificate and private key to PEM
60
- const pem = pki . certificateToPem ( cert )
61
- const privateKey = pki . privateKeyToPem ( keys . privateKey )
208
+ // Sign the new Host Certificate using the CA
209
+ newHostCert . sign ( caKey , forge . md . sha512 . create ( ) )
210
+
211
+ // Convert to PEM format
212
+ const pemHostCert = pki . certificateToPem ( newHostCert )
213
+ const pemHostKey = pki . privateKeyToPem ( hostKeys . privateKey )
62
214
63
215
return {
64
- cert : pem ,
65
- privateKey,
216
+ certificate : pemHostCert ,
217
+ privateKey : pemHostKey ,
66
218
}
67
219
}
68
220
69
221
export interface AddCertOptions {
70
222
customCertPath ?: string
71
223
}
72
224
73
- export async function addCertToSystemTrustStore ( cert : string , options ?: AddCertOptions ) {
225
+ export async function addCertToSystemTrustStoreAndSaveCerts ( cert : string , CAcert : string , options ?: AddCertOptions ) {
226
+
74
227
const certPath = storeCert ( cert , options )
228
+ const CAcertPath = storeCACert ( CAcert , options )
229
+
75
230
const platform = os . platform ( )
231
+ const args = 'TC, C, C'
76
232
77
233
if ( platform === 'darwin' )
78
234
// macOS
@@ -84,7 +240,14 @@ export async function addCertToSystemTrustStore(cert: string, options?: AddCertO
84
240
else if ( platform === 'linux' )
85
241
// Linux (This might vary based on the distro)
86
242
// for Ubuntu/Debian based systems
87
- await runCommands ( [ `sudo cp ${ certPath } /usr/local/share/ca-certificates/` , `sudo update-ca-certificates` ] )
243
+
244
+ await runCommands ( [
245
+ `sudo cp ${ certPath } /usr/local/share/ca-certificates/` ,
246
+
247
+ `certutil -d sql:${ os . homedir ( ) } /.pki/nssdb -A -t ${ args } -n ${ DEFAULT_O } -i ${ CAcertPath } ` ,
248
+
249
+ `sudo update-ca-certificates` ,
250
+ ] )
88
251
else throw new Error ( `Unsupported platform: ${ platform } ` )
89
252
return certPath
90
253
}
@@ -102,4 +265,17 @@ export function storeCert(cert: string, options?: AddCertOptions) {
102
265
return certPath
103
266
}
104
267
268
+ export function storeCACert ( CAcert : string , options ?: AddCertOptions ) {
269
+ // Construct the path using os.homedir() and path.join()
270
+ const certPath = options ?. customCertPath || path . join ( os . homedir ( ) , '.stacks' , 'ssl' , `stacks.localhost.ca.crt` )
271
+
272
+ // Ensure the directory exists before writing the file
273
+ const certDir = path . dirname ( certPath )
274
+ if ( ! fs . existsSync ( certDir ) ) fs . mkdirSync ( certDir , { recursive : true } )
275
+
276
+ fs . writeFileSync ( certPath , CAcert )
277
+
278
+ return certPath
279
+ }
280
+
105
281
export { tls , pki , forge }
0 commit comments