Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 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
{
bool IsTestMock { get; }

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