diff --git a/Security/src/AuthApi/LocalMutualTlsSupport.cs b/Security/src/AuthApi/LocalMutualTlsSupport.cs new file mode 100644 index 00000000..7280b687 --- /dev/null +++ b/Security/src/AuthApi/LocalMutualTlsSupport.cs @@ -0,0 +1,94 @@ +using System.Net; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Server.Kestrel.Core; +using Microsoft.AspNetCore.Server.Kestrel.Https; + +namespace Steeltoe.Samples.AuthApi; + +internal static class LocalMutualTlsSupport +{ + private const string ForwardedClientCertHeaderName = "X-Forwarded-Client-Cert"; + + // Mirrors LocalCertificateWriter's layout in Steeltoe.Common.Certificates: CA materials live one level + // up from the app directory, under GeneratedCertificates/trust, shared across samples in the solution. + private static readonly string TrustStorePath = ResolveTrustStorePath(); + private static readonly X509Certificate2 RootCaCertificate = LoadTrustedCertificate("SteeltoeCA.crt"); + private static readonly X509Certificate2 IntermediateCertificate = LoadTrustedCertificate("SteeltoeIntermediate.crt"); + + public static IServiceCollection AddLocalMutualTlsSupport(this IServiceCollection services) + { + services.PostConfigure(options => options.ConfigureHttpsDefaults(httpsOptions => + { + httpsOptions.ClientCertificateMode = ClientCertificateMode.AllowCertificate; + + // Kestrel's default validation rejects Steeltoe certs because the Steeltoe-generated CA isn't OS-trusted. + // Validate against the Steeltoe CA/intermediate chain directly instead of trusting the OS store. + httpsOptions.ClientCertificateValidation = ValidateAgainstSteeltoeTrustChain; + })); + + return services; + } + + public static IApplicationBuilder UseLocalMutualTlsSupport(this IApplicationBuilder builder) + { + builder.Use(async (context, next) => + { + context.Request.Headers.Remove(ForwardedClientCertHeaderName); + + if (IsLoopbackAddress(context.Connection.RemoteIpAddress) && context.Connection.ClientCertificate != null) + { + context.Request.Headers[ForwardedClientCertHeaderName] = Convert.ToBase64String(context.Connection.ClientCertificate.RawData); + } + + await next(context); + }); + + return builder; + } + + private static bool IsLoopbackAddress(IPAddress? address) + { + if (address == null) + { + return false; + } + + if (address.IsIPv4MappedToIPv6) + { + address = address.MapToIPv4(); + } + + return IPAddress.IsLoopback(address); + } + + private static bool ValidateAgainstSteeltoeTrustChain(X509Certificate2? certificate, X509Chain? remoteChain, SslPolicyErrors sslPolicyErrors) + { + if (certificate == null) + { + return false; + } + + using var customChain = new X509Chain(); + customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + customChain.ChainPolicy.CustomTrustStore.Add(RootCaCertificate); + customChain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; + + return customChain.Build(certificate); + } + + private static string ResolveTrustStorePath() + { + string appBasePath = AppContext.BaseDirectory.Contains($"{Path.DirectorySeparatorChar}bin{Path.DirectorySeparatorChar}", StringComparison.Ordinal) + ? AppContext.BaseDirectory[..AppContext.BaseDirectory.LastIndexOf($"{Path.DirectorySeparatorChar}bin", StringComparison.Ordinal)] + : AppContext.BaseDirectory[..^1]; + + string parentPath = Directory.GetParent(appBasePath)?.ToString() ?? string.Empty; + return Path.Combine(parentPath, "GeneratedCertificates", "trust"); + } + + private static X509Certificate2 LoadTrustedCertificate(string fileName) + { + return X509Certificate2.CreateFromPem(File.ReadAllText(Path.Combine(TrustStorePath, fileName))); + } +} diff --git a/Security/src/AuthApi/Program.cs b/Security/src/AuthApi/Program.cs index bef41923..2930fbb7 100644 --- a/Security/src/AuthApi/Program.cs +++ b/Security/src/AuthApi/Program.cs @@ -24,6 +24,12 @@ // Steeltoe: Add instance identity certificate to configuration. builder.Configuration.AddAppInstanceIdentityCertificate(new Guid(orgId), new Guid(spaceId)); +if (builder.Environment.IsDevelopment()) +{ + // Steeltoe: Simulate Gorouter's mTLS termination locally. + builder.Services.AddLocalMutualTlsSupport(); +} + // Steeltoe: Register Microsoft's JWT Bearer and Certificate libraries for authentication, configure JWT to work with UAA/Cloud Foundry. builder.Services.AddAuthentication().AddJwtBearer().ConfigureJwtBearerForCloudFoundry().AddCertificate(); @@ -35,13 +41,19 @@ policy.RequireClaim("scope", Globals.RequiredJwtScope); }) // Steeltoe: Register policies requiring space or org to match between client and server certificates. - .AddOrgAndSpacePolicies(); + .AddOrgAndSpacePoliciesForMutualTls(); // Steeltoe: Add actuator endpoints. builder.Services.AddAllActuators(); WebApplication app = builder.Build(); +if (app.Environment.IsDevelopment()) +{ + // Steeltoe: Simulate Gorouter's mTLS termination locally. + app.UseLocalMutualTlsSupport(); +} + // Steeltoe: Use certificate and header forwarding along with ASP.NET Core Authentication and Authorization middleware. app.UseCertificateAuthorization(); diff --git a/Security/src/AuthConsole/Program.cs b/Security/src/AuthConsole/Program.cs index 209fbc88..e7d875e8 100644 --- a/Security/src/AuthConsole/Program.cs +++ b/Security/src/AuthConsole/Program.cs @@ -20,7 +20,7 @@ builder.Configuration.AddAppInstanceIdentityCertificate(new Guid(orgId), new Guid(spaceId)); // Steeltoe: register a typed HttpClient that includes the application instance identity certificate. -builder.Services.AddHttpClient(SetBaseAddress).AddAppInstanceIdentityCertificate().ConfigureLogging(); +builder.Services.AddHttpClient(SetBaseAddress).AddAppInstanceIdentityCertificateForMutualTls().ConfigureLogging(); IHost host = builder.Build(); host.Run(); diff --git a/Security/src/AuthWeb/Program.cs b/Security/src/AuthWeb/Program.cs index 0c0a9b14..49bf577e 100644 --- a/Security/src/AuthWeb/Program.cs +++ b/Security/src/AuthWeb/Program.cs @@ -42,7 +42,7 @@ // Steeltoe: Register HttpClients for communicating with a backend service, including an application instance certificate for authorization. builder.Services.AddHttpClient(SetBaseAddress).ConfigureLogging(); -builder.Services.AddHttpClient(SetBaseAddress).AddAppInstanceIdentityCertificate().ConfigureLogging(); +builder.Services.AddHttpClient(SetBaseAddress).AddAppInstanceIdentityCertificateForMutualTls().ConfigureLogging(); // Steeltoe: Add actuator endpoints. builder.Services.AddAllActuators();