@@ -151,14 +151,22 @@ public class KestrelServerOptions
151151 private Action < HttpsConnectionAdapterOptions > HttpsDefaults { get ; set ; } = _ => { } ;
152152
153153 /// <summary>
154- /// The default server certificate for https endpoints. This is applied lazily after HttpsDefaults and user options.
154+ /// The development server certificate for https endpoints. This is applied lazily after HttpsDefaults and user options.
155155 /// </summary>
156- internal X509Certificate2 ? DefaultCertificate { get ; set ; }
156+ /// <remarks>
157+ /// Getter exposed for testing.
158+ /// </remarks>
159+ internal X509Certificate2 ? DevelopmentCertificate { get ; private set ; }
160+
161+ /// <summary>
162+ /// Allow tests to explicitly set the default certificate.
163+ /// </summary>
164+ internal X509Certificate2 ? TestOverrideDefaultCertificate { get ; set ; }
157165
158166 /// <summary>
159167 /// Has the default dev certificate load been attempted?
160168 /// </summary>
161- internal bool IsDevCertLoaded { get ; set ; }
169+ internal bool IsDevelopmentCertificateLoaded { get ; set ; }
162170
163171 /// <summary>
164172 /// Internal AppContext switch to toggle the WebTransport and HTTP/3 datagrams experiemental features.
@@ -227,16 +235,34 @@ internal void ApplyHttpsDefaults(HttpsConnectionAdapterOptions httpsOptions)
227235 HttpsDefaults ( httpsOptions ) ;
228236 }
229237
230- internal void ApplyDefaultCert ( HttpsConnectionAdapterOptions httpsOptions )
238+ internal void ApplyDefaultCertificate ( HttpsConnectionAdapterOptions httpsOptions )
231239 {
232240 if ( httpsOptions . ServerCertificate != null || httpsOptions . ServerCertificateSelector != null )
233241 {
234242 return ;
235243 }
236244
237- EnsureDefaultCert ( ) ;
245+ if ( TestOverrideDefaultCertificate is X509Certificate2 certificateFromTest )
246+ {
247+ httpsOptions . ServerCertificate = certificateFromTest ;
248+ return ;
249+ }
250+
251+ if ( ConfigurationLoader ? . DefaultCertificate is X509Certificate2 certificateFromLoader )
252+ {
253+ httpsOptions . ServerCertificate = certificateFromLoader ;
254+ return ;
255+ }
238256
239- httpsOptions . ServerCertificate = DefaultCertificate ;
257+ if ( ! IsDevelopmentCertificateLoaded )
258+ {
259+ IsDevelopmentCertificateLoaded = true ;
260+ Debug . Assert ( DevelopmentCertificate is null ) ;
261+ var logger = ApplicationServices ! . GetRequiredService < ILogger < KestrelServer > > ( ) ;
262+ DevelopmentCertificate = GetDevelopmentCertificateFromStore ( logger ) ;
263+ }
264+
265+ httpsOptions . ServerCertificate = DevelopmentCertificate ;
240266 }
241267
242268 internal void Serialize ( Utf8JsonWriter writer )
@@ -253,8 +279,8 @@ internal void Serialize(Utf8JsonWriter writer)
253279 writer . WritePropertyName ( nameof ( AllowResponseHeaderCompression ) ) ;
254280 writer . WriteBooleanValue ( AllowResponseHeaderCompression ) ;
255281
256- writer . WritePropertyName ( nameof ( IsDevCertLoaded ) ) ;
257- writer . WriteBooleanValue ( IsDevCertLoaded ) ;
282+ writer . WritePropertyName ( nameof ( IsDevelopmentCertificateLoaded ) ) ;
283+ writer . WriteBooleanValue ( IsDevelopmentCertificateLoaded ) ;
258284
259285 writer . WriteString ( nameof ( RequestHeaderEncodingSelector ) , RequestHeaderEncodingSelector == DefaultHeaderEncodingSelector ? "default" : "configured" ) ;
260286 writer . WriteString ( nameof ( ResponseHeaderEncodingSelector ) , ResponseHeaderEncodingSelector == DefaultHeaderEncodingSelector ? "default" : "configured" ) ;
@@ -280,46 +306,44 @@ internal void Serialize(Utf8JsonWriter writer)
280306 writer . WriteEndArray ( ) ;
281307 }
282308
283- private void EnsureDefaultCert ( )
309+ private static X509Certificate2 ? GetDevelopmentCertificateFromStore ( ILogger < KestrelServer > logger )
284310 {
285- if ( DefaultCertificate == null && ! IsDevCertLoaded )
311+ try
286312 {
287- IsDevCertLoaded = true ; // Only try once
288- var logger = ApplicationServices ! . GetRequiredService < ILogger < KestrelServer > > ( ) ;
289- try
313+ var cert = CertificateManager . Instance . ListCertificates ( StoreName . My , StoreLocation . CurrentUser , isValid : true , requireExportable : false )
314+ . FirstOrDefault ( ) ;
315+
316+ if ( cert is null )
290317 {
291- DefaultCertificate = CertificateManager . Instance . ListCertificates ( StoreName . My , StoreLocation . CurrentUser , isValid : true , requireExportable : false )
292- . FirstOrDefault ( ) ;
293-
294- if ( DefaultCertificate != null )
295- {
296- var status = CertificateManager . Instance . CheckCertificateState ( DefaultCertificate , interactive : false ) ;
297- if ( ! status . Success )
298- {
299- // Display a warning indicating to the user that a prompt might appear and provide instructions on what to do in that
300- // case. The underlying implementation of this check is specific to Mac OS and is handled within CheckCertificateState.
301- // Kestrel must NEVER cause a UI prompt on a production system. We only attempt this here because Mac OS is not supported
302- // in production.
303- Debug . Assert ( status . FailureMessage != null , "Status with a failure result must have a message." ) ;
304- logger . DeveloperCertificateFirstRun ( status . FailureMessage ) ;
305-
306- // Prevent binding to HTTPS if the certificate is not valid (avoid the prompt)
307- DefaultCertificate = null ;
308- }
309- else if ( ! CertificateManager . Instance . IsTrusted ( DefaultCertificate ) )
310- {
311- logger . DeveloperCertificateNotTrusted ( ) ;
312- }
313- }
314- else
315- {
316- logger . UnableToLocateDevelopmentCertificate ( ) ;
317- }
318+ logger . UnableToLocateDevelopmentCertificate ( ) ;
319+ return null ;
318320 }
319- catch
321+
322+ var status = CertificateManager . Instance . CheckCertificateState ( cert , interactive : false ) ;
323+ if ( ! status . Success )
320324 {
321- logger . UnableToLocateDevelopmentCertificate ( ) ;
325+ // Display a warning indicating to the user that a prompt might appear and provide instructions on what to do in that
326+ // case. The underlying implementation of this check is specific to Mac OS and is handled within CheckCertificateState.
327+ // Kestrel must NEVER cause a UI prompt on a production system. We only attempt this here because Mac OS is not supported
328+ // in production.
329+ Debug . Assert ( status . FailureMessage != null , "Status with a failure result must have a message." ) ;
330+ logger . DeveloperCertificateFirstRun ( status . FailureMessage ) ;
331+
332+ // Prevent binding to HTTPS if the certificate is not valid (avoid the prompt)
333+ return null ;
322334 }
335+
336+ if ( ! CertificateManager . Instance . IsTrusted ( cert ) )
337+ {
338+ logger . DeveloperCertificateNotTrusted ( ) ;
339+ }
340+
341+ return cert ;
342+ }
343+ catch
344+ {
345+ logger . UnableToLocateDevelopmentCertificate ( ) ;
346+ return null ;
323347 }
324348 }
325349
0 commit comments