@@ -54,6 +54,8 @@ public void FirstRun_CreatesValidRoot_AndPersistsToDisk()
5454 Assert . Equal ( "CN=Dev Proxy CA" , root . Subject ) ;
5555 Assert . True ( root . HasPrivateKey ) ;
5656 Assert . True ( IsCertificateAuthority ( root ) ) ;
57+ Assert . True ( HasSubjectKeyIdentifier ( root ) ) ;
58+ Assert . True ( HasKeyCertSignUsage ( root ) ) ;
5759 Assert . True ( root . NotAfter > DateTime . Now ) ;
5860 Assert . True ( File . Exists ( _rootPath ) ) ;
5961 }
@@ -105,6 +107,36 @@ public void NonCaRoot_Regenerates()
105107 Assert . True ( IsCertificateAuthority ( ca . RootCertificate ) ) ;
106108 }
107109
110+ [ Fact ]
111+ public void CaRootWithoutSki_Regenerates ( )
112+ {
113+ File . WriteAllBytes ( _rootPath , CreateCaWithoutSkiPfx ( ) ) ;
114+
115+ using var ca = new CertificateAuthority ( _rootPath , _leafDir ) ;
116+
117+ Assert . True ( HasSubjectKeyIdentifier ( ca . RootCertificate ) ) ;
118+ }
119+
120+ [ Fact ]
121+ public void CaRootWithoutKeyCertSignUsage_Regenerates ( )
122+ {
123+ File . WriteAllBytes ( _rootPath , CreateCaWithoutKeyCertSignUsagePfx ( ) ) ;
124+
125+ using var ca = new CertificateAuthority ( _rootPath , _leafDir ) ;
126+
127+ Assert . True ( HasKeyCertSignUsage ( ca . RootCertificate ) ) ;
128+ }
129+
130+ [ Fact ]
131+ public void CaRootWithoutKeyUsageExtension_Regenerates ( )
132+ {
133+ File . WriteAllBytes ( _rootPath , CreateCaWithoutKeyUsageExtensionPfx ( ) ) ;
134+
135+ using var ca = new CertificateAuthority ( _rootPath , _leafDir ) ;
136+
137+ Assert . True ( HasKeyCertSignUsage ( ca . RootCertificate ) ) ;
138+ }
139+
108140 [ Fact ]
109141 public void GetCertificateForHost_MintsLeafSignedByRoot_AndPersists ( )
110142 {
@@ -116,6 +148,7 @@ public void GetCertificateForHost_MintsLeafSignedByRoot_AndPersists()
116148 Assert . Equal ( "CN=example.com" , leaf . Subject ) ;
117149 Assert . Equal ( ca . RootCertificate . Subject , leaf . Issuer ) ;
118150 Assert . Contains ( "example.com" , GetSanText ( leaf ) , StringComparison . Ordinal ) ;
151+ Assert . True ( DoesAuthorityKeyIdentifierMatchCA ( leaf , ca . RootCertificate ) ) ;
119152 Assert . True ( File . Exists ( Path . Combine ( _leafDir , "example.com.pfx" ) ) ) ;
120153 }
121154
@@ -195,22 +228,111 @@ public void GetCertificateForHost_LeafDoesNotOutliveRoot()
195228 Assert . True ( leaf . NotAfter <= ca . RootCertificate . NotAfter ) ;
196229 }
197230
231+ [ Fact ]
232+ public void GetCertificateForHost_LeafWithoutAki_Regenerates ( )
233+ {
234+ using var ca = new CertificateAuthority ( _rootPath , _leafDir ) ;
235+
236+ _ = Directory . CreateDirectory ( _leafDir ) ;
237+ File . WriteAllBytes ( Path . Combine ( _leafDir , "example.com.pfx" ) , CreateLeafPfx ( ca . RootCertificate , null ) ) ;
238+
239+ var leaf = ca . GetCertificateForHost ( "example.com" ) ;
240+
241+ Assert . True ( DoesAuthorityKeyIdentifierMatchCA ( leaf , ca . RootCertificate ) ) ;
242+ }
243+
244+ [ Fact ]
245+ public void GetCertificateForHost_LeafWithMismatchedAki_Regenerates ( )
246+ {
247+ var rootNotAfter = DateTimeOffset . UtcNow . AddDays ( 10 ) ;
248+ var other_ca = CreateRootCertificate ( rootNotAfter ) ;
249+ using var ca = new CertificateAuthority ( _rootPath , _leafDir ) ;
250+
251+ _ = Directory . CreateDirectory ( _leafDir ) ;
252+ File . WriteAllBytes ( Path . Combine ( _leafDir , "example.com.pfx" ) , CreateLeafPfx ( ca . RootCertificate , other_ca ) ) ;
253+
254+ var leaf = ca . GetCertificateForHost ( "example.com" ) ;
255+
256+ Assert . True ( DoesAuthorityKeyIdentifierMatchCA ( leaf , ca . RootCertificate ) ) ;
257+ }
258+
198259 private static bool IsCertificateAuthority ( X509Certificate2 cert ) =>
199260 cert . Extensions . OfType < X509BasicConstraintsExtension > ( ) . FirstOrDefault ( ) ? . CertificateAuthority == true ;
200261
262+ private static bool HasSubjectKeyIdentifier ( X509Certificate2 cert ) =>
263+ cert . Extensions . OfType < X509SubjectKeyIdentifierExtension > ( ) . Any ( ) ;
264+
265+ private static bool HasKeyCertSignUsage ( X509Certificate2 cert ) =>
266+ ( cert . Extensions
267+ . OfType < X509KeyUsageExtension > ( )
268+ . FirstOrDefault ( ) ? . KeyUsages & X509KeyUsageFlags . KeyCertSign ) == X509KeyUsageFlags . KeyCertSign ;
269+
270+ private static bool DoesAuthorityKeyIdentifierMatchCA ( X509Certificate2 cert , X509Certificate2 ca )
271+ {
272+ var ski = ca . Extensions
273+ . OfType < X509SubjectKeyIdentifierExtension > ( )
274+ . FirstOrDefault ( ) ? . SubjectKeyIdentifierBytes ;
275+
276+ var aki = cert . Extensions
277+ . OfType < X509AuthorityKeyIdentifierExtension > ( )
278+ . FirstOrDefault ( ) ? . KeyIdentifier ;
279+
280+ return aki . HasValue
281+ && ski . HasValue
282+ && aki . Value . Span . SequenceEqual ( ski . Value . Span ) ;
283+ }
284+
201285 private static string GetSanText ( X509Certificate2 cert )
202286 {
203287 var san = cert . Extensions . FirstOrDefault ( e => e . Oid ? . Value == "2.5.29.17" ) ;
204288 return san ? . Format ( false ) ?? string . Empty ;
205289 }
206290
207- private static byte [ ] CreateRootPfx ( DateTimeOffset NotAfter )
291+ private static X509Certificate2 CreateRootCertificate ( DateTimeOffset NotAfter )
208292 {
209293 using var rsa = RSA . Create ( 2048 ) ;
210294 var req = new CertificateRequest ( "CN=Old Root" , rsa , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
211295 req . CertificateExtensions . Add ( new X509BasicConstraintsExtension ( true , false , 0 , true ) ) ;
212296 req . CertificateExtensions . Add ( new X509SubjectKeyIdentifierExtension ( req . PublicKey , false ) ) ;
213- using var cert = req . CreateSelfSigned ( NotAfter . AddDays ( - 10 ) , NotAfter ) ;
297+ req . CertificateExtensions . Add ( new X509KeyUsageExtension (
298+ X509KeyUsageFlags . KeyCertSign | X509KeyUsageFlags . CrlSign , true ) ) ;
299+ return req . CreateSelfSigned ( NotAfter . AddDays ( - 10 ) , NotAfter ) ;
300+ }
301+
302+ private static byte [ ] CreateRootPfx ( DateTimeOffset NotAfter )
303+ {
304+ using var cert = CreateRootCertificate ( NotAfter ) ;
305+ return cert . Export ( X509ContentType . Pkcs12 , string . Empty ) ;
306+ }
307+
308+ private static byte [ ] CreateCaWithoutSkiPfx ( )
309+ {
310+ using var rsa = RSA . Create ( 2048 ) ;
311+ var req = new CertificateRequest ( "CN=CA Root Without SKI" , rsa , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
312+ req . CertificateExtensions . Add ( new X509BasicConstraintsExtension ( true , false , 0 , true ) ) ;
313+ req . CertificateExtensions . Add ( new X509KeyUsageExtension (
314+ X509KeyUsageFlags . KeyCertSign | X509KeyUsageFlags . CrlSign , true ) ) ;
315+ using var cert = req . CreateSelfSigned ( DateTimeOffset . UtcNow . AddDays ( - 1 ) , DateTimeOffset . UtcNow . AddYears ( 1 ) ) ;
316+ return cert . Export ( X509ContentType . Pkcs12 , string . Empty ) ;
317+ }
318+
319+ private static byte [ ] CreateCaWithoutKeyCertSignUsagePfx ( )
320+ {
321+ using var rsa = RSA . Create ( 2048 ) ;
322+ var req = new CertificateRequest ( "CN=CA Root Without KeyCertSign" , rsa , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
323+ req . CertificateExtensions . Add ( new X509BasicConstraintsExtension ( true , false , 0 , true ) ) ;
324+ req . CertificateExtensions . Add ( new X509KeyUsageExtension (
325+ X509KeyUsageFlags . CrlSign , true ) ) ;
326+ using var cert = req . CreateSelfSigned ( DateTimeOffset . UtcNow . AddDays ( - 1 ) , DateTimeOffset . UtcNow . AddYears ( 1 ) ) ;
327+ return cert . Export ( X509ContentType . Pkcs12 , string . Empty ) ;
328+ }
329+
330+ private static byte [ ] CreateCaWithoutKeyUsageExtensionPfx ( )
331+ {
332+ using var rsa = RSA . Create ( 2048 ) ;
333+ var req = new CertificateRequest ( "CN=CA Root Without KeyUsageExtension" , rsa , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
334+ req . CertificateExtensions . Add ( new X509BasicConstraintsExtension ( true , false , 0 , true ) ) ;
335+ using var cert = req . CreateSelfSigned ( DateTimeOffset . UtcNow . AddDays ( - 1 ) , DateTimeOffset . UtcNow . AddYears ( 1 ) ) ;
214336 return cert . Export ( X509ContentType . Pkcs12 , string . Empty ) ;
215337 }
216338
@@ -222,4 +344,21 @@ private static byte[] CreateNonCaPfx()
222344 using var cert = req . CreateSelfSigned ( DateTimeOffset . UtcNow . AddDays ( - 1 ) , DateTimeOffset . UtcNow . AddYears ( 1 ) ) ;
223345 return cert . Export ( X509ContentType . Pkcs12 , string . Empty ) ;
224346 }
347+
348+ private static byte [ ] CreateLeafPfx ( X509Certificate2 ca , X509Certificate2 ? aki_source )
349+ {
350+ using var rsa = RSA . Create ( 2048 ) ;
351+ var req = new CertificateRequest ( "CN=Leaf" , rsa , HashAlgorithmName . SHA256 , RSASignaturePadding . Pkcs1 ) ;
352+ req . CertificateExtensions . Add ( new X509BasicConstraintsExtension ( false , false , 0 , true ) ) ;
353+ if ( aki_source is not null ) {
354+ req . CertificateExtensions . Add (
355+ X509AuthorityKeyIdentifierExtension . CreateFromCertificate (
356+ ca , includeKeyIdentifier : true , includeIssuerAndSerial : false ) ) ;
357+ }
358+ var serialNumber = new byte [ 8 ] ;
359+ RandomNumberGenerator . Fill ( serialNumber ) ;
360+ using var cert = req . Create ( ca , DateTimeOffset . UtcNow . AddDays ( - 1 ) , DateTimeOffset . UtcNow . AddDays ( 10 ) , serialNumber ) ;
361+ return cert . Export ( X509ContentType . Pkcs12 , string . Empty ) ;
362+ }
363+
225364}
0 commit comments