Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kestrel SNI from config #24286

Merged
merged 24 commits into from
Aug 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Http/Routing/src/Matching/HostMatcherPolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
else if (
host.StartsWith(WildcardPrefix) &&

// Note that we only slice of the `*`. We want to match the leading `.` also.
// Note that we only slice off the `*`. We want to match the leading `.` also.
MemoryExtensions.EndsWith(requestHost, host.Slice(WildcardHost.Length), StringComparison.OrdinalIgnoreCase))
{
// Matches a suffix wildcard.
Expand Down
12 changes: 12 additions & 0 deletions src/Servers/Kestrel/Core/src/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -620,4 +620,16 @@ For more information on configuring HTTPS see https://go.microsoft.com/fwlink/?l
<data name="UnrecognizedCertificateKeyOid" xml:space="preserve">
<value>Unknown algorithm for certificate with public key type '{0}'.</value>
</data>
<data name="SniNotConfiguredForServerName" xml:space="preserve">
<value>Connection refused because no SNI configuration section was found for '{serverName}' in '{endpointName}'. To allow all connections, add a wildcard ('*') SNI section.</value>
</data>
<data name="SniNotConfiguredToAllowNoServerName" xml:space="preserve">
<value>Connection refused because the client did not specify a server name, and no wildcard ('*') SNI configuration section was found in '{endpointName}'.</value>
</data>
<data name="SniNameCannotBeEmpty" xml:space="preserve">
<value>The endpoint {endpointName} is invalid because an SNI configuration section has an empty string as its key. Use a wildcard ('*') SNI section to match all server names.</value>
</data>
<data name="EndpointHasUnusedHttpsConfig" xml:space="preserve">
<value>The non-HTTPS endpoint {endpointName} includes HTTPS-only configuration for {keyName}.</value>
</data>
</root>
6 changes: 4 additions & 2 deletions src/Servers/Kestrel/Core/src/HttpsConnectionAdapterOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
/// </summary>
public class HttpsConnectionAdapterOptions
{
internal static TimeSpan DefaultHandshakeTimeout = TimeSpan.FromSeconds(10);

private TimeSpan _handshakeTimeout;

/// <summary>
Expand All @@ -24,7 +26,7 @@ public class HttpsConnectionAdapterOptions
public HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.NoCertificate;
HandshakeTimeout = TimeSpan.FromSeconds(10);
HandshakeTimeout = DefaultHandshakeTimeout;
}

/// <summary>
Expand Down Expand Up @@ -91,7 +93,7 @@ public void AllowAnyClientCertificate()
public Action<ConnectionContext, SslServerAuthenticationOptions> OnAuthenticate { get; set; }

/// <summary>
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite.
/// Specifies the maximum amount of time allowed for the TLS/SSL handshake. This must be positive and finite. Defaults to 10 seconds.
/// </summary>
public TimeSpan HandshakeTimeout
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// Copyright (c) Microsoft. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System;
using System.IO;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using Microsoft.AspNetCore.Server.Kestrel.Https;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates
{
internal class CertificateConfigLoader : ICertificateConfigLoader
{
public CertificateConfigLoader(IHostEnvironment hostEnvironment, ILogger<KestrelServer> logger)
{
HostEnvironment = hostEnvironment;
Logger = logger;
}

public IHostEnvironment HostEnvironment { get; }
public ILogger<KestrelServer> Logger { get; }

public bool IsTestMock => false;

public X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName)
{
if (certInfo is null)
{
return null;
}

if (certInfo.IsFileCert && certInfo.IsStoreCert)
{
throw new InvalidOperationException(CoreStrings.FormatMultipleCertificateSources(endpointName));
}
else if (certInfo.IsFileCert)
{
var certificatePath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path);
if (certInfo.KeyPath != null)
{
var certificateKeyPath = Path.Combine(HostEnvironment.ContentRootPath, certInfo.KeyPath);
var certificate = GetCertificate(certificatePath);

if (certificate != null)
{
certificate = LoadCertificateKey(certificate, certificateKeyPath, certInfo.Password);
}
else
{
Logger.FailedToLoadCertificate(certificateKeyPath);
}

if (certificate != null)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return PersistKey(certificate);
}

return certificate;
}
else
{
Logger.FailedToLoadCertificateKey(certificateKeyPath);
}

throw new InvalidOperationException(CoreStrings.InvalidPemKey);
}

return new X509Certificate2(Path.Combine(HostEnvironment.ContentRootPath, certInfo.Path), certInfo.Password);
}
else if (certInfo.IsStoreCert)
{
return LoadFromStoreCert(certInfo);
}

return null;
}

private static X509Certificate2 PersistKey(X509Certificate2 fullCertificate)
{
// We need to force the key to be persisted.
// See https://github.com/dotnet/runtime/issues/23749
var certificateBytes = fullCertificate.Export(X509ContentType.Pkcs12, "");
return new X509Certificate2(certificateBytes, "", X509KeyStorageFlags.DefaultKeySet);
}

private static X509Certificate2 LoadCertificateKey(X509Certificate2 certificate, string keyPath, string password)
{
// OIDs for the certificate key types.
const string RSAOid = "1.2.840.113549.1.1.1";
const string DSAOid = "1.2.840.10040.4.1";
const string ECDsaOid = "1.2.840.10045.2.1";

var keyText = File.ReadAllText(keyPath);
return certificate.PublicKey.Oid.Value switch
{
RSAOid => AttachPemRSAKey(certificate, keyText, password),
ECDsaOid => AttachPemECDSAKey(certificate, keyText, password),
DSAOid => AttachPemDSAKey(certificate, keyText, password),
_ => throw new InvalidOperationException(string.Format(CoreStrings.UnrecognizedCertificateKeyOid, certificate.PublicKey.Oid.Value))
};
}

private static X509Certificate2 GetCertificate(string certificatePath)
{
if (X509Certificate2.GetCertContentType(certificatePath) == X509ContentType.Cert)
{
return new X509Certificate2(certificatePath);
}

return null;
}

private static X509Certificate2 AttachPemRSAKey(X509Certificate2 certificate, string keyText, string password)
{
using var rsa = RSA.Create();
if (password == null)
{
rsa.ImportFromPem(keyText);
}
else
{
rsa.ImportFromEncryptedPem(keyText, password);
}

return certificate.CopyWithPrivateKey(rsa);
}

private static X509Certificate2 AttachPemDSAKey(X509Certificate2 certificate, string keyText, string password)
{
using var dsa = DSA.Create();
if (password == null)
{
dsa.ImportFromPem(keyText);
}
else
{
dsa.ImportFromEncryptedPem(keyText, password);
}

return certificate.CopyWithPrivateKey(dsa);
}

private static X509Certificate2 AttachPemECDSAKey(X509Certificate2 certificate, string keyText, string password)
{
using var ecdsa = ECDsa.Create();
if (password == null)
{
ecdsa.ImportFromPem(keyText);
}
else
{
ecdsa.ImportFromEncryptedPem(keyText, password);
}

return certificate.CopyWithPrivateKey(ecdsa);
}

private static X509Certificate2 LoadFromStoreCert(CertificateConfig certInfo)
{
var subject = certInfo.Subject;
var storeName = string.IsNullOrEmpty(certInfo.Store) ? StoreName.My.ToString() : certInfo.Store;
var location = certInfo.Location;
var storeLocation = StoreLocation.CurrentUser;
if (!string.IsNullOrEmpty(location))
{
storeLocation = (StoreLocation)Enum.Parse(typeof(StoreLocation), location, ignoreCase: true);
}
var allowInvalid = certInfo.AllowInvalid ?? false;

return CertificateLoader.LoadFromStoreCert(subject, storeName, storeLocation, allowInvalid);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Security.Cryptography.X509Certificates;

namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Certificates
{
internal interface ICertificateConfigLoader
halter73 marked this conversation as resolved.
Show resolved Hide resolved
{
bool IsTestMock { get; }

X509Certificate2 LoadCertificate(CertificateConfig certInfo, string endpointName);
}
}
Loading