@@ -175,7 +175,8 @@ function calculateCacheKey(domain: string, options: CertOptions) {
175175 return `${ domain } +${ ( [
176176 'expired' ,
177177 'revoked' ,
178- 'selfSigned'
178+ 'selfSigned' ,
179+ 'incompleteChain'
179180 ] as const ) . filter ( ( k : keyof CertOptions ) => options [ k ] ) . join ( '+' ) } `
180181}
181182
@@ -199,15 +200,24 @@ const KEY_PAIR_ALGO = {
199200interface CertGenerationOptions {
200201 selfSigned ?: boolean ;
201202 expired ?: boolean ;
203+ incompleteChain ?: boolean ;
202204}
203205
206+ type IntermediateCA = {
207+ cert : x509 . X509Certificate ,
208+ key : CryptoKey ,
209+ pem : string
210+ } ;
211+
204212export class LocalCA {
205213 private caCert : x509 . X509Certificate ;
206214 private caKey : CryptoKey ;
207215 private options : CAOptions ;
208216
209217 private certInMemoryCache : { [ domain : string ] : LocallyGeneratedCertificate | undefined } = { } ;
210218
219+ private intermediateCA ?: Promise < IntermediateCA > ;
220+
211221 private constructor (
212222 private caCertPem : string ,
213223 caCert : x509 . X509Certificate ,
@@ -243,6 +253,60 @@ export class LocalCA {
243253 return new LocalCA ( caOptions . cert . toString ( ) , caCert , caKey , caOptions ) ;
244254 }
245255
256+ private getIntermediateCA ( ) : Promise < IntermediateCA > {
257+ if ( ! this . intermediateCA ) {
258+ this . intermediateCA = this . createIntermediateCA ( ) ;
259+ }
260+ return this . intermediateCA ;
261+ }
262+
263+ private async createIntermediateCA ( ) : Promise < IntermediateCA > {
264+ const keyPair = await crypto . subtle . generateKey (
265+ { ...KEY_PAIR_ALGO , modulusLength : this . options . keyLength || 2048 } ,
266+ true ,
267+ [ "sign" , "verify" ]
268+ ) as CryptoKeyPair ;
269+
270+ const subjectDistinguishedName = new x509 . Name ( [
271+ { C : [ this . options . countryName ?? 'XX' ] } ,
272+ { O : [ 'Testserver' ] } ,
273+ { CN : [ 'Test Intermediate CA' ] }
274+ ] ) . toString ( ) ;
275+
276+ const notBefore = new Date ( ) ;
277+ notBefore . setDate ( notBefore . getDate ( ) - 1 ) ;
278+ const notAfter = new Date ( ) ;
279+ notAfter . setFullYear ( notAfter . getFullYear ( ) + 1 ) ;
280+
281+ const cert = await x509 . X509CertificateGenerator . create ( {
282+ serialNumber : generateSerialNumber ( ) ,
283+ subject : subjectDistinguishedName ,
284+ issuer : this . caCert . subject ,
285+ notBefore,
286+ notAfter,
287+ signingAlgorithm : KEY_PAIR_ALGO ,
288+ publicKey : keyPair . publicKey as CryptoKey ,
289+ signingKey : this . caKey ,
290+ extensions : [
291+ new x509 . BasicConstraintsExtension ( true , 0 , true ) ,
292+ new x509 . KeyUsagesExtension (
293+ x509 . KeyUsageFlags . keyCertSign |
294+ x509 . KeyUsageFlags . digitalSignature |
295+ x509 . KeyUsageFlags . cRLSign ,
296+ true
297+ ) ,
298+ await x509 . SubjectKeyIdentifierExtension . create ( keyPair . publicKey as CryptoKey , false ) ,
299+ await x509 . AuthorityKeyIdentifierExtension . create ( this . caCert , false )
300+ ]
301+ } ) ;
302+
303+ return {
304+ cert,
305+ key : keyPair . privateKey as CryptoKey ,
306+ pem : cert . toString ( 'pem' )
307+ } ;
308+ }
309+
246310 async generateCertificate (
247311 domain : string ,
248312 options : CertGenerationOptions
@@ -282,9 +346,16 @@ export class LocalCA {
282346 }
283347
284348 const subjectDistinguishedName = new x509 . Name ( subjectJsonNameParams ) . toString ( ) ;
349+
350+ const intermediate = options . incompleteChain && ! options . selfSigned
351+ ? await this . getIntermediateCA ( )
352+ : undefined ;
353+
285354 const issuerDistinguishedName = options . selfSigned
286355 ? subjectDistinguishedName
287- : this . caCert . subject ;
356+ : intermediate
357+ ? intermediate . cert . subject
358+ : this . caCert . subject ;
288359
289360 const notBefore = new Date ( ) ;
290361 const notAfter = new Date ( ) ;
@@ -325,7 +396,10 @@ export class LocalCA {
325396
326397 // We don't include SubjectKeyIdentifierExtension as that's no longer recommended
327398 if ( ! options . selfSigned ) {
328- extensions . push ( await x509 . AuthorityKeyIdentifierExtension . create ( this . caCert , false ) ) ;
399+ extensions . push ( await x509 . AuthorityKeyIdentifierExtension . create (
400+ intermediate ? intermediate . cert : this . caCert ,
401+ false
402+ ) ) ;
329403 }
330404
331405 const certificate = await x509 . X509CertificateGenerator . create ( {
@@ -336,7 +410,11 @@ export class LocalCA {
336410 notAfter,
337411 signingAlgorithm : KEY_PAIR_ALGO ,
338412 publicKey : leafKeyPair . publicKey ,
339- signingKey : options . selfSigned ? leafKeyPair . privateKey as CryptoKey : this . caKey ,
413+ signingKey : options . selfSigned
414+ ? leafKeyPair . privateKey as CryptoKey
415+ : intermediate
416+ ? intermediate . key
417+ : this . caKey ,
340418 extensions
341419 } ) ;
342420
@@ -346,7 +424,9 @@ export class LocalCA {
346424 await crypto . subtle . exportKey ( "pkcs8" , leafKeyPair . privateKey as CryptoKey ) ,
347425 "PRIVATE KEY"
348426 ) ,
349- cert : certPem ,
427+ cert : intermediate
428+ ? `${ certPem . trimEnd ( ) } \n${ intermediate . pem . trimEnd ( ) } \n`
429+ : certPem ,
350430 ca : options . selfSigned ? certPem : this . caCertPem // Use cached CA cert PEM
351431 } ;
352432
0 commit comments