Validate certificate EKU when it is provided
This commit is contained in:
parent
bd8a2c8a62
commit
a3c157cb61
|
|
@ -1,6 +1,6 @@
|
|||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.26621.2
|
||||
VisualStudioVersion = 15.0.26706.0
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{7972A5D6-3385-4127-9277-428506DD44FF}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
|
|
@ -52,9 +52,20 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server
|
|||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Performance", "test\Microsoft.AspNetCore.Server.Kestrel.Performance\Microsoft.AspNetCore.Server.Kestrel.Performance.csproj", "{EBFE9719-A44B-4978-A71F-D5C254E7F35A}"
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestResources", "TestResources", "{2822C132-BFFB-4D53-AC5B-E7E47DD81A6E}"
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "TestCertificates", "TestCertificates", "{2822C132-BFFB-4D53-AC5B-E7E47DD81A6E}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
test\shared\TestResources\testCert.pfx = test\shared\TestResources\testCert.pfx
|
||||
test\shared\TestCertificates\eku.client.ini = test\shared\TestCertificates\eku.client.ini
|
||||
test\shared\TestCertificates\eku.client.pfx = test\shared\TestCertificates\eku.client.pfx
|
||||
test\shared\TestCertificates\eku.code_signing.ini = test\shared\TestCertificates\eku.code_signing.ini
|
||||
test\shared\TestCertificates\eku.code_signing.pfx = test\shared\TestCertificates\eku.code_signing.pfx
|
||||
test\shared\TestCertificates\eku.multiple_usages.ini = test\shared\TestCertificates\eku.multiple_usages.ini
|
||||
test\shared\TestCertificates\eku.multiple_usages.pfx = test\shared\TestCertificates\eku.multiple_usages.pfx
|
||||
test\shared\TestCertificates\eku.server.ini = test\shared\TestCertificates\eku.server.ini
|
||||
test\shared\TestCertificates\eku.server.pfx = test\shared\TestCertificates\eku.server.pfx
|
||||
test\shared\TestCertificates\make-test-certs.sh = test\shared\TestCertificates\make-test-certs.sh
|
||||
test\shared\TestCertificates\no_extensions.ini = test\shared\TestCertificates\no_extensions.ini
|
||||
test\shared\TestCertificates\no_extensions.pfx = test\shared\TestCertificates\no_extensions.pfx
|
||||
test\shared\TestCertificates\testCert.pfx = test\shared\TestCertificates\testCert.pfx
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv", "src\Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv\Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj", "{A76B8C8C-0DC5-4DD3-9B1F-02E51A0915F4}"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Security;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
using System.Threading.Tasks;
|
||||
|
|
@ -15,9 +16,14 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
{
|
||||
public class HttpsConnectionAdapter : IConnectionAdapter
|
||||
{
|
||||
// See http://oid-info.com/get/1.3.6.1.5.5.7.3.1
|
||||
// Indicates that a certificate can be used as a SSL server certificate
|
||||
private const string ServerAuthenticationOid = "1.3.6.1.5.5.7.3.1";
|
||||
|
||||
private static readonly ClosedAdaptedConnection _closedAdaptedConnection = new ClosedAdaptedConnection();
|
||||
|
||||
private readonly HttpsConnectionAdapterOptions _options;
|
||||
private readonly X509Certificate2 _serverCertificate;
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public HttpsConnectionAdapter(HttpsConnectionAdapterOptions options)
|
||||
|
|
@ -37,6 +43,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
throw new ArgumentException(HttpsStrings.ServiceCertificateRequired, nameof(options));
|
||||
}
|
||||
|
||||
// capture the certificate now so it can be switched after validation
|
||||
_serverCertificate = options.ServerCertificate;
|
||||
|
||||
EnsureCertificateIsAllowedForServerAuth(_serverCertificate);
|
||||
|
||||
_options = options;
|
||||
_logger = loggerFactory?.CreateLogger(nameof(HttpsConnectionAdapter));
|
||||
}
|
||||
|
|
@ -100,7 +111,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
|
||||
try
|
||||
{
|
||||
await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, certificateRequired,
|
||||
await sslStream.AuthenticateAsServerAsync(_serverCertificate, certificateRequired,
|
||||
_options.SslProtocols, _options.CheckCertificateRevocation);
|
||||
}
|
||||
catch (IOException ex)
|
||||
|
|
@ -119,6 +130,43 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
return new HttpsAdaptedConnection(sslStream);
|
||||
}
|
||||
|
||||
private static void EnsureCertificateIsAllowedForServerAuth(X509Certificate2 certificate)
|
||||
{
|
||||
/* If the Extended Key Usage extension is included, then we check that the serverAuth usage is included. (http://oid-info.com/get/1.3.6.1.5.5.7.3.1)
|
||||
* If the Extended Key Usage extension is not included, then we assume the certificate is allowed for all usages.
|
||||
*
|
||||
* See also https://blogs.msdn.microsoft.com/kaushal/2012/02/17/client-certificates-vs-server-certificates/
|
||||
*
|
||||
* From https://tools.ietf.org/html/rfc3280#section-4.2.1.13 "Certificate Extensions: Extended Key Usage"
|
||||
*
|
||||
* If the (Extended Key Usage) extension is present, then the certificate MUST only be used
|
||||
* for one of the purposes indicated. If multiple purposes are
|
||||
* indicated the application need not recognize all purposes indicated,
|
||||
* as long as the intended purpose is present. Certificate using
|
||||
* applications MAY require that a particular purpose be indicated in
|
||||
* order for the certificate to be acceptable to that application.
|
||||
*/
|
||||
|
||||
var hasEkuExtension = false;
|
||||
|
||||
foreach (var extension in certificate.Extensions.OfType<X509EnhancedKeyUsageExtension>())
|
||||
{
|
||||
hasEkuExtension = true;
|
||||
foreach (var oid in extension.EnhancedKeyUsages)
|
||||
{
|
||||
if (oid.Value.Equals(ServerAuthenticationOid, StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hasEkuExtension)
|
||||
{
|
||||
throw new InvalidOperationException(HttpsStrings.FormatInvalidServerCertificateEku(certificate.Thumbprint));
|
||||
}
|
||||
}
|
||||
|
||||
private static X509Certificate2 ConvertToX509Certificate2(X509Certificate certificate)
|
||||
{
|
||||
if (certificate == null)
|
||||
|
|
|
|||
|
|
@ -23,7 +23,12 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
}
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Specifies the server certificate used to authenticate HTTPS connections.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If the server certificate has an Extended Key Usage extension, the usages must include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public X509Certificate2 ServerCertificate { get; set; }
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,9 @@
|
|||
<data name="AuthenticationFailed" xml:space="preserve">
|
||||
<value>Failed to authenticate HTTPS connection.</value>
|
||||
</data>
|
||||
<data name="InvalidServerCertificateEku" xml:space="preserve">
|
||||
<value>Certificate {thumbprint} cannot be used as an SSL server certificate. It has an Extended Key Usage extension but the usages do not include Server Authentication (OID 1.3.6.1.5.5.7.3.1).</value>
|
||||
</data>
|
||||
<data name="ServiceCertificateRequired" xml:space="preserve">
|
||||
<value>The server certificate parameter is required.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
// 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.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Server.Kestrel.FunctionalTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
|
||||
|
|
@ -24,6 +24,20 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Https
|
|||
internal static string FormatAuthenticationFailed()
|
||||
=> GetString("AuthenticationFailed");
|
||||
|
||||
/// <summary>
|
||||
/// Certificate {thumbprint} cannot be used as an SSL server certificate. It has an Extended Key Usage extension but the usages do not include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
|
||||
/// </summary>
|
||||
internal static string InvalidServerCertificateEku
|
||||
{
|
||||
get => GetString("InvalidServerCertificateEku");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Certificate {thumbprint} cannot be used as an SSL server certificate. It has an Extended Key Usage extension but the usages do not include Server Authentication (OID 1.3.6.1.5.5.7.3.1).
|
||||
/// </summary>
|
||||
internal static string FormatInvalidServerCertificateEku(object thumbprint)
|
||||
=> string.Format(CultureInfo.CurrentCulture, GetString("InvalidServerCertificateEku", "thumbprint"), thumbprint);
|
||||
|
||||
/// <summary>
|
||||
/// The server certificate parameter is required.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
<ItemGroup>
|
||||
<None Include="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Compile Include="..\shared\**\*.cs" />
|
||||
<Content Include="..\shared\TestResources\testCert.pfx" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="..\shared\TestCertificates\*.pfx" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Security;
|
||||
|
|
@ -18,12 +19,19 @@ using Microsoft.AspNetCore.Server.Kestrel.Core;
|
|||
using Microsoft.AspNetCore.Server.Kestrel.Https;
|
||||
using Microsoft.AspNetCore.Testing;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
||||
{
|
||||
public class HttpsConnectionAdapterTests
|
||||
{
|
||||
private static X509Certificate2 _x509Certificate2 = new X509Certificate2(TestResources.TestCertificatePath, "testPassword");
|
||||
private readonly ITestOutputHelper _output;
|
||||
|
||||
public HttpsConnectionAdapterTests(ITestOutputHelper output)
|
||||
{
|
||||
_output = output;
|
||||
}
|
||||
|
||||
// https://github.com/aspnet/KestrelHttpServer/issues/240
|
||||
// This test currently fails on mono because of an issue with SslStream.
|
||||
|
|
@ -367,6 +375,60 @@ namespace Microsoft.AspNetCore.Server.Kestrel.FunctionalTests
|
|||
}
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("no_extensions.pfx")]
|
||||
public void AcceptsCertificateWithoutExtensions(string testCertName)
|
||||
{
|
||||
var certPath = TestResources.GetCertPath(testCertName);
|
||||
_output.WriteLine("Loading " + certPath);
|
||||
var cert = new X509Certificate2(certPath, "testPassword");
|
||||
Assert.Empty(cert.Extensions.OfType<X509EnhancedKeyUsageExtension>());
|
||||
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
ServerCertificate = cert,
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("eku.server.pfx")]
|
||||
[InlineData("eku.multiple_usages.pfx")]
|
||||
public void ValidatesEnhancedKeyUsageOnCertificate(string testCertName)
|
||||
{
|
||||
var certPath = TestResources.GetCertPath(testCertName);
|
||||
_output.WriteLine("Loading " + certPath);
|
||||
var cert = new X509Certificate2(certPath, "testPassword");
|
||||
Assert.NotEmpty(cert.Extensions);
|
||||
var eku = Assert.Single(cert.Extensions.OfType<X509EnhancedKeyUsageExtension>());
|
||||
Assert.NotEmpty(eku.EnhancedKeyUsages);
|
||||
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
ServerCertificate = cert,
|
||||
});
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData("eku.code_signing.pfx")]
|
||||
[InlineData("eku.client.pfx")]
|
||||
public void ThrowsForCertificatesMissingServerEku(string testCertName)
|
||||
{
|
||||
var certPath = TestResources.GetCertPath(testCertName);
|
||||
_output.WriteLine("Loading " + certPath);
|
||||
var cert = new X509Certificate2(certPath, "testPassword");
|
||||
Assert.NotEmpty(cert.Extensions);
|
||||
var eku = Assert.Single(cert.Extensions.OfType<X509EnhancedKeyUsageExtension>());
|
||||
Assert.NotEmpty(eku.EnhancedKeyUsages);
|
||||
|
||||
var ex = Assert.Throws<InvalidOperationException>(() =>
|
||||
new HttpsConnectionAdapter(new HttpsConnectionAdapterOptions
|
||||
{
|
||||
ServerCertificate = cert,
|
||||
}));
|
||||
|
||||
Assert.Equal(HttpsStrings.FormatInvalidServerCertificateEku(cert.Thumbprint), ex.Message);
|
||||
}
|
||||
|
||||
private static async Task App(HttpContext httpContext)
|
||||
{
|
||||
var request = httpContext.Request;
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<Compile Include="..\shared\**\*.cs" />
|
||||
<Content Include="..\shared\TestResources\testCert.pfx" CopyToOutputDirectory="PreserveNewest" />
|
||||
<Content Include="..\shared\TestCertificates\*.pfx" CopyToOutputDirectory="PreserveNewest" />
|
||||
<None Update="xunit.runner.json" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,12 @@
|
|||
# See https://www.openssl.org/docs/man1.0.2/apps/req.html for details on file format
|
||||
|
||||
[ req ]
|
||||
prompt = no
|
||||
distinguished_name = testdn
|
||||
|
||||
[ testdn ]
|
||||
commonName = testcertonly
|
||||
|
||||
# see https://www.openssl.org/docs/man1.0.2/apps/x509v3_config.html
|
||||
[ req_extensions ]
|
||||
extendedKeyUsage = clientAuth
|
||||
Binary file not shown.
|
|
@ -0,0 +1,12 @@
|
|||
# See https://www.openssl.org/docs/man1.0.2/apps/req.html for details on file format
|
||||
|
||||
[ req ]
|
||||
prompt = no
|
||||
distinguished_name = testdn
|
||||
|
||||
[ testdn ]
|
||||
commonName = testcertonly
|
||||
|
||||
# see https://www.openssl.org/docs/man1.0.2/apps/x509v3_config.html
|
||||
[ req_extensions ]
|
||||
extendedKeyUsage = codeSigning
|
||||
Binary file not shown.
|
|
@ -0,0 +1,12 @@
|
|||
# See https://www.openssl.org/docs/man1.0.2/apps/req.html for details on file format
|
||||
|
||||
[ req ]
|
||||
prompt = no
|
||||
distinguished_name = req_distinguished_name
|
||||
|
||||
[ req_distinguished_name ]
|
||||
commonName = testcertonly
|
||||
|
||||
# see https://www.openssl.org/docs/man1.0.2/apps/x509v3_config.html
|
||||
[ req_extensions ]
|
||||
extendedKeyUsage = serverAuth,clientAuth
|
||||
Binary file not shown.
|
|
@ -0,0 +1,12 @@
|
|||
# See https://www.openssl.org/docs/man1.0.2/apps/req.html for details on file format
|
||||
|
||||
[ req ]
|
||||
prompt = no
|
||||
distinguished_name = testdn
|
||||
|
||||
[ testdn ]
|
||||
commonName = testcertonly
|
||||
|
||||
# see https://www.openssl.org/docs/man1.0.2/apps/x509v3_config.html
|
||||
[ req_extensions ]
|
||||
extendedKeyUsage = serverAuth
|
||||
Binary file not shown.
|
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
#
|
||||
# Should be obvious, but don't use the certs created here for anything real. This is just meant for our testing.
|
||||
#
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
__machine_has() {
|
||||
hash "$1" > /dev/null 2>&1
|
||||
return $?
|
||||
}
|
||||
|
||||
#
|
||||
# Main
|
||||
#
|
||||
|
||||
if ! __machine_has openssl; then
|
||||
echo 'OpenSSL is required to create the test certificates.' 1>&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# See https://www.openssl.org/docs/man1.0.2/apps/x509.html for more details on the openssl conf file
|
||||
|
||||
if [[ $# == 0 ]]; then
|
||||
echo "Usage: ${BASH_SOURCE[0]} <INI_FILE>..."
|
||||
echo ""
|
||||
echo "Arguments:"
|
||||
echo " <INI_FILE> Multiple allowed. Path to the *.ini file that configures a cert."
|
||||
fi
|
||||
|
||||
# loop over all arguments
|
||||
while [[ $# > 0 ]]; do
|
||||
# bashism for trimming the extension
|
||||
config=$1
|
||||
shift
|
||||
cert_name="${config%.*}"
|
||||
key="$cert_name.pem"
|
||||
cert="$cert_name.crt"
|
||||
pfx="$cert_name.pfx"
|
||||
|
||||
echo "Creating cert $cert_name"
|
||||
|
||||
# see https://www.openssl.org/docs/man1.0.2/apps/req.html
|
||||
openssl req -x509 \
|
||||
-days 1 \
|
||||
-config $config \
|
||||
-nodes \
|
||||
-newkey rsa:2048 \
|
||||
-keyout $key \
|
||||
-extensions req_extensions \
|
||||
-out $cert
|
||||
|
||||
# See https://www.openssl.org/docs/man1.0.2/apps/pkcs12.html
|
||||
openssl pkcs12 -export \
|
||||
-in $cert \
|
||||
-inkey $key \
|
||||
-out $pfx \
|
||||
-password pass:testPassword # so secure ;)
|
||||
|
||||
rm $key $cert
|
||||
done
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
# See https://www.openssl.org/docs/man1.0.2/apps/req.html for details on file format
|
||||
|
||||
[ req ]
|
||||
prompt = no
|
||||
distinguished_name = testdn
|
||||
|
||||
[ testdn ]
|
||||
commonName = testcertonly
|
||||
|
||||
# see https://www.openssl.org/docs/man1.0.2/apps/x509v3_config.html
|
||||
[ req_extensions ]
|
||||
# keyUsages =
|
||||
# extendedKeyUsage =
|
||||
Binary file not shown.
|
|
@ -7,8 +7,9 @@ namespace Microsoft.AspNetCore.Testing
|
|||
{
|
||||
public static class TestResources
|
||||
{
|
||||
private static readonly string _testCertificatePath = Path.Combine(Directory.GetCurrentDirectory(), "testCert.pfx");
|
||||
private static readonly string _baseDir = Directory.GetCurrentDirectory();
|
||||
|
||||
public static string TestCertificatePath => _testCertificatePath;
|
||||
public static string TestCertificatePath { get; } = Path.Combine(_baseDir, "testCert.pfx");
|
||||
public static string GetCertPath(string name) => Path.Combine(_baseDir, name);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue