Skip to content
This repository was archived by the owner on Dec 18, 2018. It is now read-only.

Implement client certificate authentication #385

Merged
merged 8 commits into from
Nov 19, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions src/Microsoft.AspNet.Server.Kestrel.Https/ClientCertificateMode.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ namespace Microsoft.AspNet.Server.Kestrel.Https
public static class HttpsApplicationBuilderExtensions
{
public static IApplicationBuilder UseKestrelHttps(this IApplicationBuilder app, X509Certificate2 cert)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I remove this in favor of the HttpsConnectionFilterOptions overload?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not yet. This will be the most common configuration.

{
return app.UseKestrelHttps(new HttpsConnectionFilterOptions { ServerCertificate = cert});
}

public static IApplicationBuilder UseKestrelHttps(this IApplicationBuilder app, HttpsConnectionFilterOptions options)
{
var serverInfo = app.ServerFeatures.Get<IKestrelServerInformation>();

Expand All @@ -21,7 +26,7 @@ public static IApplicationBuilder UseKestrelHttps(this IApplicationBuilder app,

var prevFilter = serverInfo.ConnectionFilter ?? new NoOpConnectionFilter();

serverInfo.ConnectionFilter = new HttpsConnectionFilter(cert, prevFilter);
serverInfo.ConnectionFilter = new HttpsConnectionFilter(options, prevFilter);

return app;
}
Expand Down
84 changes: 77 additions & 7 deletions src/Microsoft.AspNet.Server.Kestrel.Https/HttpsConnectionFilter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,36 @@

using System;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Features.Internal;
using Microsoft.AspNet.Server.Kestrel.Filter;

namespace Microsoft.AspNet.Server.Kestrel.Https
{
public class HttpsConnectionFilter : IConnectionFilter
{
private readonly X509Certificate2 _cert;
private readonly HttpsConnectionFilterOptions _options;
private readonly IConnectionFilter _previous;

public HttpsConnectionFilter(X509Certificate2 cert, IConnectionFilter previous)
public HttpsConnectionFilter(HttpsConnectionFilterOptions options, IConnectionFilter previous)
{
if (cert == null)
if (options == null)
{
throw new ArgumentNullException(nameof(cert));
throw new ArgumentNullException(nameof(options));
}
if (previous == null)
{
throw new ArgumentNullException(nameof(previous));
}
if (options.ServerCertificate == null)
{
throw new ArgumentException("The server certificate parameter is required.");
}

_cert = cert;
_options = options;
_previous = previous;
}

Expand All @@ -35,8 +42,71 @@ public async Task OnConnection(ConnectionFilterContext context)

if (string.Equals(context.Address.Scheme, "https", StringComparison.OrdinalIgnoreCase))
{
var sslStream = new SslStream(context.Connection);
await sslStream.AuthenticateAsServerAsync(_cert);
X509Certificate2 clientCertificate = null;
SslStream sslStream;
if (_options.ClientCertificateMode == ClientCertificateMode.NoCertificate)
{
sslStream = new SslStream(context.Connection);
await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, clientCertificateRequired: false,
enabledSslProtocols: _options.SslProtocols, checkCertificateRevocation: _options.CheckCertificateRevocation);
}
else
{
sslStream = new SslStream(context.Connection, leaveInnerStreamOpen: false,
userCertificateValidationCallback: (sender, certificate, chain, sslPolicyErrors) =>
{
if (certificate == null)
{
return _options.ClientCertificateMode != ClientCertificateMode.RequireCertificate;
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: extra empty line

if (_options.ClientCertificateValidation == null)
{
if (sslPolicyErrors != SslPolicyErrors.None)
{
return false;
}
}

X509Certificate2 certificate2 = certificate as X509Certificate2;
if (certificate2 == null)
{
#if DOTNET5_4
// conversion X509Certificate to X509Certificate2 not supported
// https://github.com/dotnet/corefx/issues/4510
return false;
#else
certificate2 = new X509Certificate2(certificate);
#endif
}

if (_options.ClientCertificateValidation != null)
{
if (!_options.ClientCertificateValidation(certificate2, chain, sslPolicyErrors))
{
return false;
}
}

clientCertificate = certificate2;
return true;
});
await sslStream.AuthenticateAsServerAsync(_options.ServerCertificate, clientCertificateRequired: true,
enabledSslProtocols: _options.SslProtocols, checkCertificateRevocation: _options.CheckCertificateRevocation);
}

var previousPrepareRequest = context.PrepareRequest;
context.PrepareRequest = features =>
{
previousPrepareRequest?.Invoke(features);

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also set IHttpRequestFeature.Scheme = "https"

if (clientCertificate != null)
{
features.Set<ITlsConnectionFeature>(new TlsConnectionFeature { ClientCertificate = clientCertificate });
}

features.Get<IHttpRequestFeature>().Scheme = "https";
};
context.Connection = sslStream;
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// 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;
using System.Net.Security;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;

namespace Microsoft.AspNet.Server.Kestrel.Https
{
public class HttpsConnectionFilterOptions
{
public HttpsConnectionFilterOptions()
{
ClientCertificateMode = ClientCertificateMode.NoCertificate;
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
}

public X509Certificate2 ServerCertificate { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not required for this PR, but I think this eventually needs to support different certs for different ports. Something like Func<ServerAddress, X509Certificate2>?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also need to think about ALPN if/when SslStream ever supports it.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, I meant SNI, but yes, same dependency.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Tratcher Depending on how much flexibility you want to allow, each option could be based on the ServerAddress.

UseKestrelHttps(this IApplicationBuilder app, Func<ServerAddress, HttpsConnectionFilterOptions>)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wow. At that point it might make more sense to start a separate instance for each port/config.

public ClientCertificateMode ClientCertificateMode { get; set; }
public Func<X509Certificate2, X509Chain, SslPolicyErrors, bool> ClientCertificateValidation { get; set; }
public SslProtocols SslProtocols { get; set; }
public bool CheckCertificateRevocation { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// 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;
using System.IO;
using Microsoft.AspNet.Http.Features;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

?


namespace Microsoft.AspNet.Server.Kestrel.Filter
{
public class ConnectionFilterContext
{
public ServerAddress Address { get; set; }
public Stream Connection { get; set; }
public Stream Connection { get; set; }
public Action<IFeatureCollection> PrepareRequest { get; set; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At first I wasn't sure, but now I think I'm sold on making PrepareRequest an action over adding a state object to the context.

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.

using System.Threading.Tasks;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Server.Kestrel.Infrastructure;

namespace Microsoft.AspNet.Server.Kestrel.Filter
Expand Down
2 changes: 1 addition & 1 deletion src/Microsoft.AspNet.Server.Kestrel/Http/Connection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ private void OnRead(UvStreamHandle handle, int status)

private Frame CreateFrame()
{
return new Frame(this, _remoteEndPoint, _localEndPoint);
return new Frame(this, _remoteEndPoint, _localEndPoint, _filterContext?.PrepareRequest);
}

void IConnectionControl.Pause()
Expand Down
10 changes: 8 additions & 2 deletions src/Microsoft.AspNet.Server.Kestrel/Http/Frame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Threading.Tasks;
using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Server.Kestrel.Filter;
using Microsoft.AspNet.Server.Kestrel.Infrastructure;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Primitives;
Expand Down Expand Up @@ -55,19 +56,22 @@ public partial class Frame : FrameContext, IFrameControl

private readonly IPEndPoint _localEndPoint;
private readonly IPEndPoint _remoteEndPoint;
private readonly Action<IFeatureCollection> _prepareRequest;

public Frame(ConnectionContext context)
: this(context, remoteEndPoint: null, localEndPoint: null)
: this(context, remoteEndPoint: null, localEndPoint: null, prepareRequest: null)
{
}

public Frame(ConnectionContext context,
IPEndPoint remoteEndPoint,
IPEndPoint localEndPoint)
IPEndPoint localEndPoint,
Action<IFeatureCollection> prepareRequest)
: base(context)
{
_remoteEndPoint = remoteEndPoint;
_localEndPoint = localEndPoint;
_prepareRequest = prepareRequest;

FrameControl = this;
Reset();
Expand Down Expand Up @@ -140,6 +144,8 @@ public void Reset()
httpConnectionFeature.IsLocal = false;
}

_prepareRequest?.Invoke(this);

_requestAbortCts?.Dispose();
}

Expand Down
Loading