| File: TlsConfigurationLoader.cs | Web Access |
| Project: src\aspnetcore\src\Servers\Kestrel\Core\src\Microsoft.AspNetCore.Server.Kestrel.Core.csproj (Microsoft.AspNetCore.Server.Kestrel.Core) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.AspNetCore.Certificates.Generation; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates; using Microsoft.AspNetCore.Server.Kestrel.Https; using Microsoft.AspNetCore.Server.Kestrel.Https.Internal; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Server.Kestrel.Core; /// <summary> /// An abstraction over the parts of <see cref="KestrelConfigurationLoader"/> that would prevent us from trimming TLS support /// in `CreateSlimBuilder` scenarios. Managed by <see cref="HttpsConfigurationService"/>. /// </summary> internal sealed class TlsConfigurationLoader { private readonly ICertificateConfigLoader _certificateConfigLoader; private readonly string _applicationName; private readonly ILogger<KestrelServer> _serverLogger; private readonly ILogger<HttpsConnectionMiddleware> _httpsLogger; public TlsConfigurationLoader( IHostEnvironment hostEnvironment, ILogger<KestrelServer> serverLogger, ILogger<HttpsConnectionMiddleware> httpsLogger) { _certificateConfigLoader = new CertificateConfigLoader(hostEnvironment, serverLogger); _applicationName = hostEnvironment.ApplicationName; _serverLogger = serverLogger; _httpsLogger = httpsLogger; } /// <summary> /// Applies various configuration settings to <paramref name="httpsOptions"/> and <paramref name="endpoint"/>. /// </summary> public void ApplyHttpsConfiguration( HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint, KestrelServerOptions serverOptions, CertificateConfig? defaultCertificateConfig, ConfigurationReader configurationReader) { serverOptions.ApplyHttpsDefaults(httpsOptions); if (endpoint.SslProtocols.HasValue) { httpsOptions.SslProtocols = endpoint.SslProtocols.Value; } else { // Ensure endpoint is reloaded if it used the default protocol and the SslProtocols changed. endpoint.SslProtocols = configurationReader.EndpointDefaults.SslProtocols; } if (endpoint.ClientCertificateMode.HasValue) { httpsOptions.ClientCertificateMode = endpoint.ClientCertificateMode.Value; } else { // Ensure endpoint is reloaded if it used the default mode and the ClientCertificateMode changed. endpoint.ClientCertificateMode = configurationReader.EndpointDefaults.ClientCertificateMode; } // A cert specified directly on the endpoint overrides any defaults. var (serverCert, fullChain) = _certificateConfigLoader.LoadCertificate(endpoint.Certificate, endpoint.Name); httpsOptions.ServerCertificate = serverCert ?? httpsOptions.ServerCertificate; httpsOptions.ServerCertificateChain = fullChain ?? httpsOptions.ServerCertificateChain; if (!httpsOptions.HasServerCertificateOrSelector) { // Fallback serverOptions.ApplyDefaultCertificate(httpsOptions); // Ensure endpoint is reloaded if it used the default certificate and the certificate changed. endpoint.Certificate = defaultCertificateConfig; } } /// <summary> /// Calls an appropriate overload of <see cref="Microsoft.AspNetCore.Hosting.ListenOptionsHttpsExtensions.UseHttps(ListenOptions)"/> /// on <paramref name="listenOptions"/>, with or without SNI, according to how <paramref name="endpoint"/> is configured. /// </summary> /// <returns>Updated <see cref="ListenOptions"/> for convenient chaining.</returns> public ListenOptions UseHttpsWithSni( ListenOptions listenOptions, HttpsConnectionAdapterOptions httpsOptions, EndpointConfig endpoint) { if (listenOptions.IsTls) { return listenOptions; } if (endpoint.Sni.Count == 0) { if (!httpsOptions.HasServerCertificateOrSelector) { throw new InvalidOperationException(CoreStrings.NoCertSpecifiedNoDevelopmentCertificateFound); } return listenOptions.UseHttps(httpsOptions); } var sniOptionsSelector = new SniOptionsSelector(endpoint.Name, endpoint.Sni, _certificateConfigLoader, httpsOptions, listenOptions.Protocols, _httpsLogger); var tlsCallbackOptions = new TlsHandshakeCallbackOptions() { OnConnection = SniOptionsSelector.OptionsCallback, HandshakeTimeout = httpsOptions.HandshakeTimeout, OnConnectionState = sniOptionsSelector, }; return listenOptions.UseHttps(tlsCallbackOptions); } /// <summary> /// Retrieves the default or, failing that, developer certificate from <paramref name="configurationReader"/>. /// </summary> public CertificateAndConfig? LoadDefaultCertificate(ConfigurationReader configurationReader) { if (configurationReader.Certificates.TryGetValue("Default", out var defaultCertConfig)) { var (defaultCert, defaultCertChain) = _certificateConfigLoader.LoadCertificate(defaultCertConfig, "Default"); if (defaultCert != null) { if (defaultCertChain != null) { return new CertificateAndConfig(defaultCert, defaultCertConfig, defaultCertChain); } return new CertificateAndConfig(defaultCert, defaultCertConfig); } } else if (FindDeveloperCertificateFile(configurationReader) is CertificateAndConfig pair) { _serverLogger.LocatedDevelopmentCertificate(pair.Certificate); return pair; } return null; } private CertificateAndConfig? FindDeveloperCertificateFile(ConfigurationReader configurationReader) { string? certificatePath = null; if (configurationReader.Certificates.TryGetValue("Development", out var certificateConfig) && certificateConfig.Path == null && certificateConfig.Password != null && TryGetCertificatePath(_applicationName, out certificatePath) && File.Exists(certificatePath)) { try { var certificate = new X509Certificate2(certificatePath, certificateConfig.Password); if (IsDevelopmentCertificate(certificate)) { return new CertificateAndConfig(certificate, certificateConfig); } } catch (CryptographicException) { _serverLogger.FailedToLoadDevelopmentCertificate(certificatePath); } } else if (!string.IsNullOrEmpty(certificatePath)) { _serverLogger.FailedToLocateDevelopmentCertificateFile(certificatePath); } return null; } private static bool IsDevelopmentCertificate(X509Certificate2 certificate) { return string.Equals(certificate.Subject, CertificateManager.LocalhostHttpsDistinguishedName, StringComparison.Ordinal) && CertificateManager.IsHttpsDevelopmentCertificate(certificate); } private static bool TryGetCertificatePath(string applicationName, [NotNullWhen(true)] out string? path) { // See https://github.com/aspnet/Hosting/issues/1294 var appData = Environment.GetEnvironmentVariable("APPDATA"); var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); var basePath = appData != null ? Path.Combine(appData, "ASP.NET", "https") : null; basePath = basePath ?? (home != null ? Path.Combine(home, ".aspnet", "https") : null); path = basePath != null ? Path.Combine(basePath, $"{applicationName}.pfx") : null; return path != null; } }