Implement client certificate authentication

This commit is contained in:
Master T 2015-11-14 11:00:50 +01:00
parent 7e8a405917
commit bd30f28dfd
12 changed files with 345 additions and 24 deletions

View File

@ -0,0 +1,12 @@
// 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.
namespace Microsoft.AspNet.Server.Kestrel.Https
{
public enum ClientCertificateMode
{
NoCertificate,
AllowCertificate,
RequireCertificate
}
}

View File

@ -0,0 +1,11 @@
// 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.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace Microsoft.AspNet.Server.Kestrel.Https
{
public delegate bool ClientCertificateValidationCallback(
X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors);
}

View File

@ -11,6 +11,11 @@ namespace Microsoft.AspNet.Server.Kestrel.Https
public static class HttpsApplicationBuilderExtensions public static class HttpsApplicationBuilderExtensions
{ {
public static IApplicationBuilder UseKestrelHttps(this IApplicationBuilder app, X509Certificate2 cert) public static IApplicationBuilder UseKestrelHttps(this IApplicationBuilder app, X509Certificate2 cert)
{
return app.UseKestrelHttps(new HttpsConnectionFilterOptions { ServerCertificate = cert});
}
public static IApplicationBuilder UseKestrelHttps(this IApplicationBuilder app, HttpsConnectionFilterOptions options)
{ {
var serverInfo = app.ServerFeatures.Get<IKestrelServerInformation>(); var serverInfo = app.ServerFeatures.Get<IKestrelServerInformation>();
@ -21,7 +26,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Https
var prevFilter = serverInfo.ConnectionFilter ?? new NoOpConnectionFilter(); var prevFilter = serverInfo.ConnectionFilter ?? new NoOpConnectionFilter();
serverInfo.ConnectionFilter = new HttpsConnectionFilter(cert, prevFilter); serverInfo.ConnectionFilter = new HttpsConnectionFilter(options, prevFilter);
return app; return app;
} }

View File

@ -3,29 +3,37 @@
using System; using System;
using System.Net.Security; using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Features.Internal;
using Microsoft.AspNet.Server.Kestrel.Filter; using Microsoft.AspNet.Server.Kestrel.Filter;
namespace Microsoft.AspNet.Server.Kestrel.Https namespace Microsoft.AspNet.Server.Kestrel.Https
{ {
public class HttpsConnectionFilter : IConnectionFilter public class HttpsConnectionFilter : IConnectionFilter
{ {
private readonly X509Certificate2 _cert; private readonly X509Certificate2 _serverCert;
private readonly ClientCertificateMode _clientCertMode;
private readonly ClientCertificateValidationCallback _clientValidationCallback;
private readonly IConnectionFilter _previous; private readonly IConnectionFilter _previous;
private X509Certificate2 _clientCert;
public HttpsConnectionFilter(X509Certificate2 cert, IConnectionFilter previous) public HttpsConnectionFilter(HttpsConnectionFilterOptions options, IConnectionFilter previous)
{ {
if (cert == null) if (options.ServerCertificate == null)
{ {
throw new ArgumentNullException(nameof(cert)); throw new ArgumentNullException(nameof(options.ServerCertificate));
} }
if (previous == null) if (previous == null)
{ {
throw new ArgumentNullException(nameof(previous)); throw new ArgumentNullException(nameof(previous));
} }
_cert = cert; _serverCert = options.ServerCertificate;
_clientCertMode = options.ClientCertificateMode;
_clientValidationCallback = options.ClientCertificateValidation;
_previous = previous; _previous = previous;
} }
@ -35,10 +43,68 @@ namespace Microsoft.AspNet.Server.Kestrel.Https
if (string.Equals(context.Address.Scheme, "https", StringComparison.OrdinalIgnoreCase)) if (string.Equals(context.Address.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{ {
var sslStream = new SslStream(context.Connection); SslStream sslStream;
await sslStream.AuthenticateAsServerAsync(_cert); if (_clientCertMode == ClientCertificateMode.NoCertificate)
{
sslStream = new SslStream(context.Connection);
await sslStream.AuthenticateAsServerAsync(_serverCert);
}
else
{
sslStream = new SslStream(context.Connection, leaveInnerStreamOpen: false,
userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) =>
{
if (sslPolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateNotAvailable))
{
return _clientCertMode != ClientCertificateMode.RequireCertificate;
}
if (_clientValidationCallback == null)
{
if (sslPolicyErrors != SslPolicyErrors.None)
{
return false;
}
}
#if DOTNET5_4
// conversion X509Certificate to X509Certificate2 not supported
// https://github.com/dotnet/corefx/issues/4510
X509Certificate2 certificate2 = null;
return false;
#else
X509Certificate2 certificate2 = certificate as X509Certificate2 ??
new X509Certificate2(certificate);
#endif
if (_clientValidationCallback != null)
{
if (!_clientValidationCallback(certificate2, chain, sslPolicyErrors))
{
return false;
}
}
_clientCert = certificate2;
return true;
});
await sslStream.AuthenticateAsServerAsync(_serverCert, clientCertificateRequired: true,
enabledSslProtocols: SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls,
checkCertificateRevocation: false);
}
context.Connection = sslStream; context.Connection = sslStream;
} }
} }
public void PrepareRequest(IFeatureCollection features)
{
_previous.PrepareRequest(features);
if (_clientCert != null)
{
features.Set<ITlsConnectionFeature>(
new TlsConnectionFeature { ClientCertificate = _clientCert });
}
}
} }
} }

View File

@ -0,0 +1,19 @@
// 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.AspNet.Server.Kestrel.Https
{
public class HttpsConnectionFilterOptions
{
public HttpsConnectionFilterOptions()
{
ClientCertificateMode = ClientCertificateMode.NoCertificate;
}
public X509Certificate2 ServerCertificate { get; set; }
public ClientCertificateMode ClientCertificateMode { get; set; }
public ClientCertificateValidationCallback ClientCertificateValidation { get; set; }
}
}

View File

@ -2,12 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO; using System.IO;
using Microsoft.AspNet.Http.Features;
namespace Microsoft.AspNet.Server.Kestrel.Filter namespace Microsoft.AspNet.Server.Kestrel.Filter
{ {
public class ConnectionFilterContext public class ConnectionFilterContext
{ {
public ServerAddress Address { get; set; } public ServerAddress Address { get; set; }
public Stream Connection { get; set; } public Stream Connection { get; set; }
} }
} }

View File

@ -2,11 +2,13 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
namespace Microsoft.AspNet.Server.Kestrel.Filter namespace Microsoft.AspNet.Server.Kestrel.Filter
{ {
public interface IConnectionFilter public interface IConnectionFilter
{ {
Task OnConnection(ConnectionFilterContext context); Task OnConnection(ConnectionFilterContext context);
void PrepareRequest(IFeatureCollection features);
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Server.Kestrel.Infrastructure; using Microsoft.AspNet.Server.Kestrel.Infrastructure;
namespace Microsoft.AspNet.Server.Kestrel.Filter namespace Microsoft.AspNet.Server.Kestrel.Filter
@ -12,5 +13,8 @@ namespace Microsoft.AspNet.Server.Kestrel.Filter
{ {
return TaskUtilities.CompletedTask; return TaskUtilities.CompletedTask;
} }
public void PrepareRequest(IFeatureCollection features)
{}
} }
} }

View File

@ -177,7 +177,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
private Frame CreateFrame() private Frame CreateFrame()
{ {
return new Frame(this, _remoteEndPoint, _localEndPoint); return new Frame(this, _remoteEndPoint, _localEndPoint, ConnectionFilter);
} }
void IConnectionControl.Pause() void IConnectionControl.Pause()

View File

@ -11,6 +11,7 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Server.Kestrel.Filter;
using Microsoft.AspNet.Server.Kestrel.Infrastructure; using Microsoft.AspNet.Server.Kestrel.Infrastructure;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives; using Microsoft.Extensions.Primitives;
@ -55,19 +56,22 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
private readonly IPEndPoint _localEndPoint; private readonly IPEndPoint _localEndPoint;
private readonly IPEndPoint _remoteEndPoint; private readonly IPEndPoint _remoteEndPoint;
private readonly IConnectionFilter _connectionFilter;
public Frame(ConnectionContext context) public Frame(ConnectionContext context)
: this(context, remoteEndPoint: null, localEndPoint: null) : this(context, remoteEndPoint: null, localEndPoint: null, connectionFilter: null)
{ {
} }
public Frame(ConnectionContext context, public Frame(ConnectionContext context,
IPEndPoint remoteEndPoint, IPEndPoint remoteEndPoint,
IPEndPoint localEndPoint) IPEndPoint localEndPoint,
IConnectionFilter connectionFilter)
: base(context) : base(context)
{ {
_remoteEndPoint = remoteEndPoint; _remoteEndPoint = remoteEndPoint;
_localEndPoint = localEndPoint; _localEndPoint = localEndPoint;
_connectionFilter = connectionFilter;
FrameControl = this; FrameControl = this;
Reset(); Reset();
@ -140,6 +144,8 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
httpConnectionFeature.IsLocal = false; httpConnectionFeature.IsLocal = false;
} }
_connectionFilter?.PrepareRequest(this);
_requestAbortCts?.Dispose(); _requestAbortCts?.Dispose();
} }
@ -272,7 +278,7 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
// If _requestAbort is set, the connection has already been closed. // If _requestAbort is set, the connection has already been closed.
if (!_requestAborted) if (!_requestAborted)
{ {
await ProduceEnd(); await ProduceEnd();
// Finish reading the request body in case the app did not. // Finish reading the request body in case the app did not.
await messageBody.Consume(); await messageBody.Consume();
@ -301,15 +307,15 @@ namespace Microsoft.AspNet.Server.Kestrel.Http
// If _requestAborted is set, the connection has already been closed. // If _requestAborted is set, the connection has already been closed.
if (!_requestAborted) if (!_requestAborted)
{ {
// Inform client no more data will ever arrive // Inform client no more data will ever arrive
ConnectionControl.End(ProduceEndType.SocketShutdownSend); ConnectionControl.End(ProduceEndType.SocketShutdownSend);
// Wait for client to either disconnect or send unexpected data // Wait for client to either disconnect or send unexpected data
await SocketInput; await SocketInput;
// Dispose socket // Dispose socket
ConnectionControl.End(ProduceEndType.SocketDisconnect); ConnectionControl.End(ProduceEndType.SocketDisconnect);
} }
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -95,6 +95,9 @@ namespace Microsoft.AspNet.Server.KestrelTests
return _empty; return _empty;
} }
public void PrepareRequest(IFeatureCollection frame)
{}
public int BytesRead => _rewritingStream.BytesRead; public int BytesRead => _rewritingStream.BytesRead;
} }
@ -110,6 +113,9 @@ namespace Microsoft.AspNet.Server.KestrelTests
context.Connection = new RewritingStream(oldConnection); context.Connection = new RewritingStream(oldConnection);
} }
public void PrepareRequest(IFeatureCollection frame)
{}
} }
private class RewritingStream : Stream private class RewritingStream : Stream

View File

@ -3,12 +3,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Net.Security; using System.Net.Security;
using System.Net.Sockets;
using System.Security.Cryptography.X509Certificates; using System.Security.Cryptography.X509Certificates;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Server.Kestrel.Filter; using Microsoft.AspNet.Server.Kestrel.Filter;
using Microsoft.AspNet.Server.Kestrel.Https; using Microsoft.AspNet.Server.Kestrel.Https;
using Microsoft.AspNet.Testing.xunit; using Microsoft.AspNet.Testing.xunit;
@ -55,19 +59,20 @@ namespace Microsoft.AspNet.Server.KestrelTests
handler.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; handler.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
#endif #endif
var sereverAddress = "https://localhost:54321/"; var serverAddress = "https://localhost:54321/";
var serviceContext = new TestServiceContext() var serviceContext = new TestServiceContext()
{ {
ConnectionFilter = new HttpsConnectionFilter( ConnectionFilter = new HttpsConnectionFilter(
new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"), new HttpsConnectionFilterOptions
{ ServerCertificate = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword")},
new NoOpConnectionFilter()) new NoOpConnectionFilter())
}; };
using (var server = new TestServer(App, serviceContext, sereverAddress)) using (var server = new TestServer(App, serviceContext, serverAddress))
{ {
using (var client = new HttpClient(handler)) using (var client = new HttpClient(handler))
{ {
var result = await client.PostAsync(sereverAddress, new FormUrlEncodedContent(new[] { var result = await client.PostAsync(serverAddress, new FormUrlEncodedContent(new[] {
new KeyValuePair<string, string>("content", "Hello World?") new KeyValuePair<string, string>("content", "Hello World?")
})); }));
@ -82,5 +87,189 @@ namespace Microsoft.AspNet.Server.KestrelTests
#endif #endif
} }
} }
// https://github.com/aspnet/KestrelHttpServer/issues/240
// This test currently fails on mono because of an issue with SslStream.
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task RequireCertificateFailsWhenNoCertificate()
{
RemoteCertificateValidationCallback validationCallback =
(sender, cert, chain, sslPolicyErrors) => true;
try
{
#if DNX451
var handler = new HttpClientHandler();
ServicePointManager.ServerCertificateValidationCallback += validationCallback;
#else
var handler = new WinHttpHandler();
handler.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
#endif
var serverAddress = "https://localhost:54321/";
var serviceContext = new TestServiceContext()
{
ConnectionFilter = new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
{
ServerCertificate = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"),
ClientCertificateMode = ClientCertificateMode.RequireCertificate
},
new NoOpConnectionFilter())
};
using (var server = new TestServer(App, serviceContext, serverAddress))
{
using (var client = new HttpClient())
{
await Assert.ThrowsAnyAsync<Exception>(
() => client.GetAsync(serverAddress));
}
}
}
finally
{
#if DNX451
ServicePointManager.ServerCertificateValidationCallback -= validationCallback;
#endif
}
}
// https://github.com/dotnet/corefx/issues/4512
// WinHttpHandler throws an Exception (ERROR_INTERNET_SECURE_FAILURE)
#if DNX451
// https://github.com/aspnet/KestrelHttpServer/issues/240
// This test currently fails on mono because of an issue with SslStream.
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task AllowCertificateContinuesWhenNoCertificate()
{
RemoteCertificateValidationCallback validationCallback =
(sender, cert, chain, sslPolicyErrors) => true;
try
{
#if DNX451
var handler = new HttpClientHandler();
ServicePointManager.ServerCertificateValidationCallback += validationCallback;
#else
var handler = new WinHttpHandler();
handler.ServerCertificateValidationCallback += (sender, cert, chain, sslPolicyErrors) => true;
#endif
var serverAddress = "https://localhost:54321/";
var serviceContext = new TestServiceContext()
{
ConnectionFilter = new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
{
ServerCertificate = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"),
ClientCertificateMode = ClientCertificateMode.AllowCertificate
},
new NoOpConnectionFilter())
};
RequestDelegate app = context =>
{
Assert.Equal(context.Features.Get<ITlsConnectionFeature>(), null);
return context.Response.WriteAsync("hello world");
};
using (var server = new TestServer(app, serviceContext, serverAddress))
{
using (var client = new HttpClient())
{
var result = await client.GetAsync(serverAddress);
Assert.Equal("hello world", await result.Content.ReadAsStringAsync());
}
}
}
finally
{
#if DNX451
ServicePointManager.ServerCertificateValidationCallback -= validationCallback;
#endif
}
}
#endif
// https://github.com/dotnet/corefx/issues/4510
// Can't convert X509Certificate to X509Certificate2 in HttpsConnectionFilter
#if DNX451
// https://github.com/aspnet/KestrelHttpServer/issues/240
// This test currently fails on mono because of an issue with SslStream.
[ConditionalFact]
[OSSkipCondition(OperatingSystems.Linux)]
[OSSkipCondition(OperatingSystems.MacOSX)]
public async Task CertificatePassedToHttpContext()
{
RemoteCertificateValidationCallback validationCallback =
(sender, cert, chain, sslPolicyErrors) => true;
try
{
#if DNX451
ServicePointManager.ServerCertificateValidationCallback += validationCallback;
#endif
var serverAddress = "https://localhost:54321/";
var serviceContext = new TestServiceContext()
{
ConnectionFilter = new HttpsConnectionFilter(
new HttpsConnectionFilterOptions
{
ServerCertificate = new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"),
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
ClientCertificateValidation = (certificate, chain, sslPolicyErrors) => true
},
new NoOpConnectionFilter())
};
RequestDelegate app = context =>
{
var tlsFeature = context.Features.Get<ITlsConnectionFeature>();
Assert.NotNull(tlsFeature);
Assert.NotNull(tlsFeature.ClientCertificate);
Assert.NotNull(context.Connection.ClientCertificate);
return context.Response.WriteAsync("hello world");
};
using (var server = new TestServer(app, serviceContext, serverAddress))
{
// SslStream is used to ensure the certificate is actually passed to the server
// HttpClient might not send the certificate because it is invalid or it doesn't match any
// of the certificate authorities sent by the server in the SSL handshake.
using (var client = new TcpClient())
{
await client.ConnectAsync("127.0.0.1", 54321);
SslStream stream = new SslStream(client.GetStream(), false, (sender, certificate, chain, errors) => true,
(sender, host, certificates, certificate, issuers) => new X509Certificate2(@"TestResources/testCert.pfx", "testPassword"));
await stream.AuthenticateAsClientAsync("localhost");
var request = Encoding.UTF8.GetBytes("GET / HTTP/1.0\r\n\r\n");
await stream.WriteAsync(request, 0, request.Length);
var reader = new StreamReader(stream);
var line = await reader.ReadLineAsync();
Assert.Equal("HTTP/1.0 200 OK", line);
}
}
}
finally
{
#if DNX451
ServicePointManager.ServerCertificateValidationCallback -= validationCallback;
#endif
}
}
#endif
} }
} }