feat(gateway): supply generated cert as Kestrel HTTPS default
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using ZB.MOM.WW.MxGateway.Contracts;
|
||||
using ZB.MOM.WW.MxGateway.Server.Alarms;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
@@ -55,6 +57,8 @@ public static class GatewayApplication
|
||||
});
|
||||
StaticWebAssetsLoader.UseStaticWebAssets(builder.Environment, builder.Configuration);
|
||||
|
||||
ConfigureSelfSignedTls(builder);
|
||||
|
||||
builder.Services.AddGatewayConfiguration();
|
||||
builder.Services.AddSqliteAuthStore();
|
||||
builder.Services.AddGatewayGrpcAuthorization();
|
||||
@@ -72,6 +76,28 @@ public static class GatewayApplication
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void ConfigureSelfSignedTls(WebApplicationBuilder builder)
|
||||
{
|
||||
if (!Security.Tls.KestrelTlsInspector.RequiresGeneratedCertificate(builder.Configuration))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Configuration.TlsOptions tlsOptions =
|
||||
builder.Configuration.GetSection("MxGateway:Tls").Get<Configuration.TlsOptions>()
|
||||
?? new Configuration.TlsOptions();
|
||||
|
||||
using ILoggerFactory loggerFactory = LoggerFactory.Create(logging => logging.AddConsole());
|
||||
Security.Tls.SelfSignedCertificateProvider provider = new(
|
||||
tlsOptions,
|
||||
loggerFactory.CreateLogger<Security.Tls.SelfSignedCertificateProvider>(),
|
||||
TimeProvider.System);
|
||||
|
||||
X509Certificate2 certificate = provider.LoadOrCreate();
|
||||
builder.WebHost.ConfigureKestrel(options =>
|
||||
options.ConfigureHttpsDefaults(https => https.ServerCertificate = certificate));
|
||||
}
|
||||
|
||||
private static string ResolveContentRootPath()
|
||||
{
|
||||
string? configuredContentRootPath = Environment.GetEnvironmentVariable("ASPNETCORE_CONTENTROOT");
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
using System.Net.Security;
|
||||
using System.Net.Sockets;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting.Server;
|
||||
using Microsoft.AspNetCore.Hosting.Server.Features;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using ZB.MOM.WW.MxGateway.Server;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Gateway;
|
||||
|
||||
public sealed class GatewayTlsBootstrapTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Verifies that when a Kestrel HTTPS endpoint is configured without its own certificate,
|
||||
/// the gateway supplies the generated self-signed certificate as the Kestrel HTTPS default.
|
||||
/// The host must start and bind, and the certificate served on the TLS handshake must be the
|
||||
/// gateway's generated cert (subject <c>CN=MxAccessGateway Self-Signed</c>) — not an ambient
|
||||
/// ASP.NET Core development certificate. On a host with no dev cert installed, starting a
|
||||
/// cert-less HTTPS endpoint throws "No server certificate was specified"; on a host that has a
|
||||
/// trusted dev cert, Kestrel would otherwise serve that dev cert (<c>CN=localhost</c>), so the
|
||||
/// subject assertion is what makes this test fail without the wiring on either kind of host.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task Host_ServesGeneratedCertificate_WhenHttpsEndpointHasNoCertificate()
|
||||
{
|
||||
string certDir = Directory.CreateTempSubdirectory().FullName;
|
||||
try
|
||||
{
|
||||
Environment.SetEnvironmentVariable("Kestrel__Endpoints__Test__Url", "https://127.0.0.1:0");
|
||||
Environment.SetEnvironmentVariable(
|
||||
"MxGateway__Tls__SelfSignedCertPath", Path.Combine(certDir, "gw.pfx"));
|
||||
|
||||
WebApplication app = GatewayApplication.Build([]);
|
||||
await app.StartAsync();
|
||||
try
|
||||
{
|
||||
string servedSubject = await ReadServedCertificateSubjectAsync(app);
|
||||
Assert.Contains("MxAccessGateway Self-Signed", servedSubject, StringComparison.Ordinal);
|
||||
}
|
||||
finally
|
||||
{
|
||||
await app.StopAsync();
|
||||
await app.DisposeAsync();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable("Kestrel__Endpoints__Test__Url", null);
|
||||
Environment.SetEnvironmentVariable("MxGateway__Tls__SelfSignedCertPath", null);
|
||||
Directory.Delete(certDir, recursive: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task<string> ReadServedCertificateSubjectAsync(WebApplication app)
|
||||
{
|
||||
IServerAddressesFeature addresses =
|
||||
app.Services.GetRequiredService<IServer>().Features.Get<IServerAddressesFeature>()
|
||||
?? throw new InvalidOperationException("Server addresses feature was not available.");
|
||||
Uri endpoint = new(addresses.Addresses.First());
|
||||
|
||||
using TcpClient client = new();
|
||||
await client.ConnectAsync(endpoint.Host, endpoint.Port);
|
||||
using SslStream ssl = new(
|
||||
client.GetStream(),
|
||||
leaveInnerStreamOpen: false,
|
||||
userCertificateValidationCallback: (_, _, _, _) => true);
|
||||
await ssl.AuthenticateAsClientAsync("127.0.0.1");
|
||||
|
||||
return ssl.RemoteCertificate?.Subject ?? "(none)";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user