2
0
Эх сурвалжийг харах

Load configuration in UseHttps (#48138)

So that configuration-based certs will be considered, if necessary.

Largely salvaged from #48056
Builds on #48137
Fixes #45801
Andrew Casey 2 жил өмнө
parent
commit
ede4e0b17a

+ 0 - 6
src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs

@@ -77,12 +77,6 @@ public class HttpsConnectionAdapterOptions
     /// </summary>
     public SslProtocols SslProtocols { get; set; }
 
-    /// <summary>
-    /// The protocols enabled on this endpoint.
-    /// </summary>
-    /// <remarks>Defaults to HTTP/1.x only.</remarks>
-    internal HttpProtocols HttpProtocols { get; set; }
-
     /// <summary>
     /// Specifies whether the certificate revocation list is checked during authentication.
     /// </summary>

+ 4 - 3
src/Servers/Kestrel/Core/src/ListenOptionsHttpsExtensions.cs

@@ -166,6 +166,9 @@ public static class ListenOptionsHttpsExtensions
         // We consider calls to `UseHttps` to be a clear expression of user intent to pull in HTTPS configuration support
         listenOptions.KestrelServerOptions.EnableHttpsConfiguration();
 
+        // If there's a configuration, load it so that the results will be available to ApplyDefaultCertificate
+        listenOptions.KestrelServerOptions.ConfigurationLoader?.LoadInternal();
+
         var options = new HttpsConnectionAdapterOptions();
         listenOptions.KestrelServerOptions.ApplyHttpsDefaults(options);
         configureOptions(options);
@@ -196,9 +199,7 @@ public static class ListenOptionsHttpsExtensions
 
         listenOptions.Use(next =>
         {
-            // Set the list of protocols from listen options
-            httpsOptions.HttpProtocols = listenOptions.Protocols;
-            var middleware = new HttpsConnectionMiddleware(next, httpsOptions, loggerFactory, metrics);
+            var middleware = new HttpsConnectionMiddleware(next, httpsOptions, listenOptions.Protocols, loggerFactory, metrics);
             return middleware.OnConnectionAsync;
         });
 

+ 8 - 6
src/Servers/Kestrel/Core/src/Middleware/HttpsConnectionMiddleware.cs

@@ -43,17 +43,19 @@ internal sealed class HttpsConnectionMiddleware
     // The following fields are only set by TlsHandshakeCallbackOptions ctor.
     private readonly Func<TlsHandshakeCallbackContext, ValueTask<SslServerAuthenticationOptions>>? _tlsCallbackOptions;
     private readonly object? _tlsCallbackOptionsState;
-    private readonly HttpProtocols _httpProtocols;
+
+    // Internal for testing
+    internal readonly HttpProtocols _httpProtocols;
 
     // Pool for cancellation tokens that cancel the handshake
     private readonly CancellationTokenSourcePool _ctsPool = new();
 
-    public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, KestrelMetrics metrics)
-      : this(next, options, loggerFactory: NullLoggerFactory.Instance, metrics: metrics)
+    public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, KestrelMetrics metrics)
+      : this(next, options, httpProtocols, loggerFactory: NullLoggerFactory.Instance, metrics: metrics)
     {
     }
 
-    public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, ILoggerFactory loggerFactory, KestrelMetrics metrics)
+    public HttpsConnectionMiddleware(ConnectionDelegate next, HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, ILoggerFactory loggerFactory, KestrelMetrics metrics)
     {
         ArgumentNullException.ThrowIfNull(options);
 
@@ -74,7 +76,7 @@ internal sealed class HttpsConnectionMiddleware
         //_sslStreamFactory = s => new SslStream(s);
 
         _options = options;
-        _options.HttpProtocols = ValidateAndNormalizeHttpProtocols(_options.HttpProtocols, _logger);
+        _httpProtocols = ValidateAndNormalizeHttpProtocols(httpProtocols, _logger);
 
         // capture the certificate now so it can't be switched after validation
         _serverCertificate = options.ServerCertificate;
@@ -331,7 +333,7 @@ internal sealed class HttpsConnectionMiddleware
             CertificateRevocationCheckMode = _options.CheckCertificateRevocation ? X509RevocationMode.Online : X509RevocationMode.NoCheck,
         };
 
-        ConfigureAlpn(sslOptions, _options.HttpProtocols);
+        ConfigureAlpn(sslOptions, _httpProtocols);
 
         _options.OnAuthenticate?.Invoke(context, sslOptions);
 

+ 150 - 1
src/Servers/Kestrel/Kestrel/test/KestrelConfigurationLoaderTests.cs

@@ -402,7 +402,156 @@ public class KestrelConfigurationLoaderTests
         void CheckListenOptions(X509Certificate2 expectedCert)
         {
             var listenOptions = Assert.Single(serverOptions.ConfigurationBackedListenOptions);
-            Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions.ServerCertificate.SerialNumber);
+            Assert.Equal(expectedCert.SerialNumber, listenOptions.HttpsOptions!.ServerCertificate.SerialNumber);
+        }
+    }
+
+    [Fact]
+    public void LoadDevelopmentCertificate_LoadBeforeUseHttps()
+    {
+        try
+        {
+            var serverOptions = CreateServerOptions();
+            var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
+            var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
+            var path = GetCertificatePath();
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+            File.WriteAllBytes(path, bytes);
+
+            var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
+            {
+                new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
+            }).Build();
+
+            serverOptions.Configure(config);
+
+            Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
+
+            serverOptions.ConfigurationLoader.Load();
+
+            Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
+            Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);
+
+            var ran1 = false;
+            serverOptions.ListenAnyIP(4545, listenOptions =>
+            {
+                ran1 = true;
+                listenOptions.UseHttps();
+            });
+            Assert.True(ran1);
+
+            var listenOptions = serverOptions.CodeBackedListenOptions.Single();
+            listenOptions.Build();
+            Assert.Equal(listenOptions.HttpsOptions.ServerCertificate?.SerialNumber, certificate.SerialNumber);
+        }
+        finally
+        {
+            if (File.Exists(GetCertificatePath()))
+            {
+                File.Delete(GetCertificatePath());
+            }
+        }
+    }
+
+    [Fact]
+    public void LoadDevelopmentCertificate_UseHttpsBeforeLoad()
+    {
+        try
+        {
+            var serverOptions = CreateServerOptions();
+            var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
+            var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
+            var path = GetCertificatePath();
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+            File.WriteAllBytes(path, bytes);
+
+            var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
+            {
+                new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
+            }).Build();
+
+            serverOptions.Configure(config);
+
+            Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
+
+            var ran1 = false;
+            serverOptions.ListenAnyIP(4545, listenOptions =>
+            {
+                ran1 = true;
+                listenOptions.UseHttps();
+            });
+            Assert.True(ran1);
+
+            // Use Https triggers a load, so the default cert is already set
+            Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
+            Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);
+
+            // This Load is a no-op (tested elsewhere)
+            serverOptions.ConfigurationLoader.Load();
+
+            var listenOptions = serverOptions.CodeBackedListenOptions.Single();
+            listenOptions.Build();
+            Assert.Equal(listenOptions.HttpsOptions.ServerCertificate?.SerialNumber, certificate.SerialNumber);
+        }
+        finally
+        {
+            if (File.Exists(GetCertificatePath()))
+            {
+                File.Delete(GetCertificatePath());
+            }
+        }
+    }
+
+    [Fact]
+    public void LoadDevelopmentCertificate_UseHttpsBeforeConfigure()
+    {
+        try
+        {
+            var serverOptions = CreateServerOptions();
+            var certificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
+            var bytes = certificate.Export(X509ContentType.Pkcs12, "1234");
+            var path = GetCertificatePath();
+            Directory.CreateDirectory(Path.GetDirectoryName(path));
+            File.WriteAllBytes(path, bytes);
+
+            var defaultCertificate = TestResources.GetTestCertificate();
+            Assert.NotEqual(certificate.SerialNumber, defaultCertificate.SerialNumber);
+            serverOptions.TestOverrideDefaultCertificate = defaultCertificate;
+
+            var ran1 = false;
+            serverOptions.ListenAnyIP(4545, listenOptions =>
+            {
+                ran1 = true;
+                listenOptions.UseHttps();
+            });
+            Assert.True(ran1);
+
+            var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
+            {
+                new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
+            }).Build();
+
+            serverOptions.Configure(config);
+
+            Assert.Null(serverOptions.ConfigurationLoader.DefaultCertificate);
+
+            serverOptions.ConfigurationLoader.Load();
+
+            Assert.NotNull(serverOptions.ConfigurationLoader.DefaultCertificate);
+            Assert.Equal(serverOptions.ConfigurationLoader.DefaultCertificate.SerialNumber, certificate.SerialNumber);
+
+            var listenOptions = serverOptions.CodeBackedListenOptions.Single();
+            listenOptions.Build();
+            // In a perfect world, it would match certificate.SerialNumber, but there's no way for an eager UseHttps
+            // to do that before Configure is called.
+            Assert.Equal(listenOptions.HttpsOptions.ServerCertificate?.SerialNumber, defaultCertificate.SerialNumber);
+        }
+        finally
+        {
+            if (File.Exists(GetCertificatePath()))
+            {
+                File.Delete(GetCertificatePath());
+            }
         }
     }
 

+ 18 - 18
src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsConnectionMiddlewareTests.cs

@@ -264,7 +264,7 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
     [Fact]
     public void ThrowsWhenNoServerCertificateIsProvided()
     {
-        Assert.Throws<ArgumentException>(() => CreateMiddleware(new HttpsConnectionAdapterOptions()));
+        Assert.Throws<ArgumentException>(() => CreateMiddleware(new HttpsConnectionAdapterOptions(), ListenOptions.DefaultHttpProtocols));
     }
 
     [Fact]
@@ -1318,7 +1318,8 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
         CreateMiddleware(new HttpsConnectionAdapterOptions
         {
             ServerCertificate = cert,
-        });
+        },
+        ListenOptions.DefaultHttpProtocols);
     }
 
     [Theory]
@@ -1337,7 +1338,8 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
             CreateMiddleware(new HttpsConnectionAdapterOptions
             {
                 ServerCertificate = cert,
-            }));
+            },
+            ListenOptions.DefaultHttpProtocols));
 
         Assert.Equal(CoreStrings.FormatInvalidServerCertificateEku(cert.Thumbprint), ex.Message);
     }
@@ -1357,6 +1359,7 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
             {
                 ServerCertificate = cert,
             },
+            ListenOptions.DefaultHttpProtocols,
             testLogger);
 
         Assert.Single(testLogger.Messages.Where(log => log.EventId == 9));
@@ -1404,11 +1407,10 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
         var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
         {
             ServerCertificate = _x509Certificate2,
-            HttpProtocols = HttpProtocols.Http1AndHttp2
         };
-        CreateMiddleware(httpConnectionAdapterOptions);
+        var middleware = CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2);
 
-        Assert.Equal(HttpProtocols.Http1, httpConnectionAdapterOptions.HttpProtocols);
+        Assert.Equal(HttpProtocols.Http1, middleware._httpProtocols);
     }
 
     [ConditionalFact]
@@ -1419,11 +1421,10 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
         var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
         {
             ServerCertificate = _x509Certificate2,
-            HttpProtocols = HttpProtocols.Http1AndHttp2
         };
-        CreateMiddleware(httpConnectionAdapterOptions);
+        var middleware = CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http1AndHttp2);
 
-        Assert.Equal(HttpProtocols.Http1AndHttp2, httpConnectionAdapterOptions.HttpProtocols);
+        Assert.Equal(HttpProtocols.Http1AndHttp2, middleware._httpProtocols);
     }
 
     [ConditionalFact]
@@ -1434,10 +1435,9 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
         var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
         {
             ServerCertificate = _x509Certificate2,
-            HttpProtocols = HttpProtocols.Http2
         };
 
-        Assert.Throws<NotSupportedException>(() => CreateMiddleware(httpConnectionAdapterOptions));
+        Assert.Throws<NotSupportedException>(() => CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http2));
     }
 
     [ConditionalFact]
@@ -1448,11 +1448,10 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
         var httpConnectionAdapterOptions = new HttpsConnectionAdapterOptions
         {
             ServerCertificate = _x509Certificate2,
-            HttpProtocols = HttpProtocols.Http2
         };
 
         // Does not throw
-        CreateMiddleware(httpConnectionAdapterOptions);
+        CreateMiddleware(httpConnectionAdapterOptions, HttpProtocols.Http2);
     }
 
     private static HttpsConnectionMiddleware CreateMiddleware(X509Certificate2 serverCertificate)
@@ -1460,18 +1459,19 @@ public class HttpsConnectionMiddlewareTests : LoggedTest
         return CreateMiddleware(new HttpsConnectionAdapterOptions
         {
             ServerCertificate = serverCertificate,
-        });
+        },
+        ListenOptions.DefaultHttpProtocols);
     }
 
-    private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, TestApplicationErrorLogger testLogger = null)
+    private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols, TestApplicationErrorLogger testLogger = null)
     {
         var loggerFactory = testLogger is null ? (ILoggerFactory)NullLoggerFactory.Instance : new LoggerFactory(new[] { new KestrelTestLoggerProvider(testLogger) });
-        return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, loggerFactory, new KestrelMetrics(new TestMeterFactory()));
+        return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, httpProtocols, loggerFactory, new KestrelMetrics(new TestMeterFactory()));
     }
 
-    private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options)
+    private static HttpsConnectionMiddleware CreateMiddleware(HttpsConnectionAdapterOptions options, HttpProtocols httpProtocols)
     {
-        return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, new KestrelMetrics(new TestMeterFactory()));
+        return new HttpsConnectionMiddleware(context => Task.CompletedTask, options, httpProtocols, new KestrelMetrics(new TestMeterFactory()));
     }
 
     private static async Task App(HttpContext httpContext)

+ 14 - 0
src/Servers/Kestrel/test/InMemory.FunctionalTests/HttpsTests.cs

@@ -64,14 +64,18 @@ public class HttpsTests : LoggedTest
 
         Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
 
+        var ranUseHttpsAction = false;
         serverOptions.ListenLocalhost(5001, options =>
         {
             options.UseHttps(opt =>
             {
                 // The default cert is applied after UseHttps.
                 Assert.Null(opt.ServerCertificate);
+                ranUseHttpsAction = true;
             });
         });
+
+        Assert.True(ranUseHttpsAction);
         Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
     }
 
@@ -117,14 +121,19 @@ public class HttpsTests : LoggedTest
             options.ServerCertificate = _x509Certificate2;
             options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
         });
+        var ranUseHttpsAction = false;
         serverOptions.ListenLocalhost(5000, options =>
         {
             options.UseHttps(opt =>
             {
                 Assert.Equal(_x509Certificate2, opt.ServerCertificate);
                 Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
+                ranUseHttpsAction = true;
             });
         });
+
+        Assert.True(ranUseHttpsAction);
+
         // Never lazy loaded
         Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
         Assert.Null(serverOptions.DevelopmentCertificate);
@@ -144,6 +153,7 @@ public class HttpsTests : LoggedTest
             };
             options.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
         });
+        var ranUseHttpsAction = false;
         serverOptions.ListenLocalhost(5000, options =>
         {
             options.UseHttps(opt =>
@@ -151,8 +161,12 @@ public class HttpsTests : LoggedTest
                 Assert.Null(opt.ServerCertificate);
                 Assert.NotNull(opt.ServerCertificateSelector);
                 Assert.Equal(ClientCertificateMode.RequireCertificate, opt.ClientCertificateMode);
+                ranUseHttpsAction = true;
             });
         });
+
+        Assert.True(ranUseHttpsAction);
+
         // Never lazy loaded
         Assert.False(serverOptions.IsDevelopmentCertificateLoaded);
         Assert.Null(serverOptions.DevelopmentCertificate);

+ 92 - 0
src/Servers/Kestrel/test/Interop.FunctionalTests/Http3/Http3TlsTests.cs

@@ -5,6 +5,7 @@ using System.Net;
 using System.Net.Http;
 using System.Net.Quic;
 using System.Net.Security;
+using System.Security.Cryptography.X509Certificates;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.Http;
 using Microsoft.AspNetCore.Server.Kestrel.Core;
@@ -25,6 +26,7 @@ public class Http3TlsTests : LoggedTest
     [MsQuicSupported]
     public async Task ServerCertificateSelector_Invoked()
     {
+        var serverCertificateSelectorActionCalled = false;
         var builder = CreateHostBuilder(async context =>
         {
             await context.Response.WriteAsync("Hello World");
@@ -37,6 +39,7 @@ public class Http3TlsTests : LoggedTest
                 {
                     httpsOptions.ServerCertificateSelector = (context, host) =>
                     {
+                        serverCertificateSelectorActionCalled = true;
                         Assert.Null(context); // The context isn't available durring the quic handshake.
                         Assert.Equal("testhost", host);
                         return TestResources.GetTestCertificate();
@@ -61,6 +64,8 @@ public class Http3TlsTests : LoggedTest
         Assert.Equal(HttpVersion.Version30, response.Version);
         Assert.Equal("Hello World", result);
 
+        Assert.True(serverCertificateSelectorActionCalled);
+
         await host.StopAsync().DefaultTimeout();
     }
 
@@ -422,6 +427,93 @@ public class Http3TlsTests : LoggedTest
         Assert.Throws<InvalidOperationException>(host.Run);
     }
 
+    [ConditionalFact]
+    [MsQuicSupported]
+    public async Task LoadDevelopmentCertificateViaConfiguration()
+    {
+        var expectedCertificate = new X509Certificate2(TestResources.GetCertPath("aspnetdevcert.pfx"), "testPassword", X509KeyStorageFlags.Exportable);
+        var bytes = expectedCertificate.Export(X509ContentType.Pkcs12, "1234");
+        var path = GetCertificatePath();
+        Directory.CreateDirectory(Path.GetDirectoryName(path));
+        File.WriteAllBytes(path, bytes);
+
+        var config = new ConfigurationBuilder().AddInMemoryCollection(new[]
+        {
+            new KeyValuePair<string, string>("Certificates:Development:Password", "1234"),
+        }).Build();
+
+        var ranConfigureKestrelAction = false;
+        var ranUseHttpsAction = false;
+        var hostBuilder = CreateHostBuilder(async context =>
+        {
+            await context.Response.WriteAsync("Hello World");
+        }, configureKestrel: kestrelOptions =>
+        {
+            ranConfigureKestrelAction = true;
+            kestrelOptions.Configure(config);
+
+            kestrelOptions.ListenAnyIP(0, listenOptions =>
+            {
+                listenOptions.Protocols = HttpProtocols.Http3;
+                listenOptions.UseHttps(_ =>
+                {
+                    ranUseHttpsAction = true;
+                });
+            });
+        });
+
+        Assert.False(ranConfigureKestrelAction);
+        Assert.False(ranUseHttpsAction);
+
+        using var host = hostBuilder.Build();
+        await host.StartAsync().DefaultTimeout();
+
+        Assert.True(ranConfigureKestrelAction);
+        Assert.True(ranUseHttpsAction);
+
+        var request = new HttpRequestMessage(HttpMethod.Get, $"https://127.0.0.1:{host.GetPort()}/");
+        request.Version = HttpVersion.Version30;
+        request.VersionPolicy = HttpVersionPolicy.RequestVersionExact;
+        request.Headers.Host = "testhost";
+
+        var ranCertificateValidation = false;
+        var httpHandler = new SocketsHttpHandler();
+        httpHandler.SslOptions = new SslClientAuthenticationOptions
+        {
+            RemoteCertificateValidationCallback = (object _sender, X509Certificate actualCertificate, X509Chain _chain, SslPolicyErrors _sslPolicyErrors) =>
+            {
+                ranCertificateValidation = true;
+                Assert.Equal(expectedCertificate.GetSerialNumberString(), actualCertificate.GetSerialNumberString());
+                return true;
+            },
+            TargetHost = "targethost",
+        };
+        using var client = new HttpMessageInvoker(httpHandler);
+
+        var response = await client.SendAsync(request, CancellationToken.None).DefaultTimeout();
+        response.EnsureSuccessStatusCode();
+        var result = await response.Content.ReadAsStringAsync();
+        Assert.Equal(HttpVersion.Version30, response.Version);
+        Assert.Equal("Hello World", result);
+
+        Assert.True(ranCertificateValidation);
+
+        await host.StopAsync().DefaultTimeout();
+    }
+
+    ///<remarks>
+    /// This is something of a hack - we should actually be calling
+    /// <see cref="Microsoft.AspNetCore.Server.Kestrel.KestrelConfigurationLoader.TryGetCertificatePath"/>.
+    /// </remarks>
+    private static string GetCertificatePath()
+    {
+        var appData = Environment.GetEnvironmentVariable("APPDATA");
+        var home = Environment.GetEnvironmentVariable("HOME");
+        var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null;
+        basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null);
+        return Path.Combine(basePath, $"{typeof(Http3TlsTests).Assembly.GetName().Name}.pfx");
+    }
+
     private IHostBuilder CreateHostBuilder(RequestDelegate requestDelegate, HttpProtocols? protocol = null, Action<KestrelServerOptions> configureKestrel = null)
     {
         return HttpHelpers.CreateHostBuilder(AddTestLogging, requestDelegate, protocol, configureKestrel);