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

Add SNI Support #2425

Merged
merged 1 commit into from
Apr 4, 2018
Merged

Add SNI Support #2425

merged 1 commit into from
Apr 4, 2018

Conversation

Tratcher
Copy link
Member

#2357 Here's what Kestrel's SNI support might look like.
This is based on the initial CoreFx design in dotnet/corefx#28278.

@Tratcher Tratcher added the blocked Blocked label Mar 23, 2018
@Tratcher Tratcher added this to the 2.1.0-preview2 milestone Mar 23, 2018
@Tratcher Tratcher self-assigned this Mar 23, 2018
@shirhatti
Copy link

What's the behavior for legacy clients that don't support SNI? I'm assuming since we fallback to using the ServerCertificate if specified or close the connection?

@benaadams
Copy link
Contributor

/cc @Drawaes

@Drawaes
Copy link
Contributor

Drawaes commented Mar 24, 2018

To be fair it's not "legacy clients" SNI isn't required and if you go via an IP there is no SNI to compare.

The problem you have of course is if you are multi-homing you can't just use "certificate x" it might be the wrong website. So I think a default should be allowed but not required. It's a MAY include extension.

As an aside I wonder if the possible cipher list could be supplied if you are reading the client hello anyway. That way say a smaller and more secure ECDSA cert could be supplied and an RSA supplied if the client doesn't support it.

@Tratcher
Copy link
Member Author

Tratcher commented Mar 24, 2018

@Drawaes you'll have to ask on dotnet/corefx#28278 about exposing additional parameters. Right now they only give us a name.

@shirhatti The current SslStream PR will invoke the callback with a null name if SNI was not supplied by the client. The developer is free to return a default certificate to continue, or return null or throw to abort. ServerCertificate is always ignored if a selector is provided (SslStream makes them mutually exclusive).

var cert = selector(name);
if (cert != null)
{
EnsureCertificateIsAllowedForServerAuth(cert);
Copy link
Member Author

@Tratcher Tratcher Mar 24, 2018

Choose a reason for hiding this comment

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

@blowdart it's potentially quite expensive to validate EKUs on every new connection. Can we exclude that for the advanced SNI scenario?

Copy link
Member

Choose a reason for hiding this comment

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

Cache the result based on cert thumbprint?

Copy link
Member Author

Choose a reason for hiding this comment

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

@davidfowl what are the odds you'd allow registering and injecting a MemoryCache here? 😁

Copy link
Member

Choose a reason for hiding this comment

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

Kestrel.Core shouldn't depend on the memory cache.

#else
{
get => null;
set => throw new PlatformNotSupportedException("This API requires targeting netcoreapp2.1");
Copy link
Member

Choose a reason for hiding this comment

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

I'm not a huge fan of this throwing. If we made another callback API that let you control the cert per connection what would that look like?

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated. Now a callback will be allowed even for 2.0. The difference is that for 2.0 we'll always invoke it with a null name before the handshake. The developer won't be able to distinguish between the client and server failing to provide SNI.

@krwq
Copy link

krwq commented Mar 26, 2018

If server name could not be extracted or is not supported the callback will pass null string

var serverCert = _serverCertificate;
if (_serverCertificateSelector != null)
{
serverCert = _serverCertificateSelector(null, null);
Copy link
Member

Choose a reason for hiding this comment

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

Do you think we should null check serverCert to get better errors when ServerCertificateSelector is not null but its return value is?

// If a selector is provided then ignore the cert, it may be a default cert.
if (selector != null)
{
// SslStream doesn't allow both.
Copy link
Member

Choose a reason for hiding this comment

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

I'm thinking it should be up to the caller of the HttpsConnectionAdapter ctor to ensure either ServerCertificateSelector or ServerCertificate is set but not both. I think it's better to throw if both are set instead of committing to this behavior of preferring the selector.

Copy link
Member

Choose a reason for hiding this comment

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

In UseHttps, is ServerCertificate already set by the time the config callback is called?

Copy link
Member Author

Choose a reason for hiding this comment

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

The default certs mess that up. See #2422

{
throw new ArgumentException(CoreStrings.ServiceCertificateRequired, nameof(options));
Copy link
Member

Choose a reason for hiding this comment

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

lol the good 'ol service certificate.

/// 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 Func<string, X509Certificate2> ServerCertificateSelector { get; set; }
Copy link
Member Author

Choose a reason for hiding this comment

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

Consider passing through a state object, either a Kestrel specific object or the SslStream itself. That's what SslStream passes to their callback (as an Object).

Copy link
Member

Choose a reason for hiding this comment

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

So more of a "sender" than a state object? I don't think a state object is necessary since any closures would be allocated only once for the lifetime of the server.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, a sender. Is there any connection specific state we think would be interesting to expose to this callback?

Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just send the SslStream any properties are likely to be exposed on that in the future and means you won't have to change the kestrel side for every change.

Copy link
Member Author

Choose a reason for hiding this comment

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

SslStream is largely uninitialized at this stage in the handshake. See dotnet/corefx#28278 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that is the way it is today. But as we have client hello parsing started it's just a backdoor to other parts being parsed and populated ;) (don't tell anyone) and if these features are populated then there will be no lightup required in kestrel.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe the ConnectionContext would be useful.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated to pass through the sender. Trying to pass though per-connection state requires allocating a per-connection closure. We'll follow up in RC to see if that makes sense.

Copy link
Member

Choose a reason for hiding this comment

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

Not that we have to do this now, but a per-connection closure allocation isn't a big deal in the context of initializing an HTTPS connection in Kestrel right now.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I realized that looking at the client cert validation code. I added it back for now.

@Tratcher Tratcher force-pushed the tratcher/sni branch 3 times, most recently from f4ad8ad to 9c8095b Compare April 3, 2018 18:49
@Tratcher Tratcher changed the title [WIP] Outline SNI APIs Outline SNI APIs Apr 3, 2018
@Tratcher Tratcher changed the title Outline SNI APIs Add SNI Support Apr 3, 2018
@Tratcher
Copy link
Member Author

Tratcher commented Apr 3, 2018

Everything appears to be working in manual and automated tests. There are a few unrelated flaky test failures. @halter73 ready for a final review.

The last bit is figuring out what CoreFx build to target. @pakrym is waiting for the next prodcon build before updating universe.

@davidfowl
Copy link
Member

These aren't flaky tests, these look like regressions:

11:53:27 Log Critical[0]: Unexpected exception in HttpConnection.ProcessRequestsAsync. System.ObjectDisposedException: Cannot access a disposed object.
11:53:27 Object name: 'MemoryPoolBlock'.
11:53:27    at System.Buffers.MemoryPoolBlock.Dispose()
11:53:27    at System.IO.Pipelines.BufferSegment.ResetMemory()
11:53:27    at System.IO.Pipelines.Pipe.CompletePipe()
11:53:27    at System.IO.Pipelines.Pipe.CompleteWriter(Exception exception)
11:53:27    at System.IO.Pipelines.Pipe.DefaultPipeWriter.Complete(Exception exception)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal.AdaptedPipeline.<ReadInputAsync>d__16.MoveNext()
11:53:27 --- End of stack trace from previous location where exception was thrown ---
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal.AdaptedPipeline.<RunAsync>d__14.MoveNext()
11:53:27 --- End of stack trace from previous location where exception was thrown ---
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.HttpConnection.<ProcessRequestsAsync>d__47`1.MoveNext()
11:53:27 Log Critical[0]: ConnectionDispatcher.Execute() 0HLCPIGNNCR8B System.Exception: Unexpected critical error. Log Critical[0]: Unexpected exception in HttpConnection.ProcessRequestsAsync. System.ObjectDisposedException: Cannot access a disposed object.
11:53:27 Object name: 'MemoryPoolBlock'.
11:53:27    at System.Buffers.MemoryPoolBlock.Dispose()
11:53:27    at System.IO.Pipelines.BufferSegment.ResetMemory()
11:53:27    at System.IO.Pipelines.Pipe.CompletePipe()
11:53:27    at System.IO.Pipelines.Pipe.CompleteWriter(Exception exception)
11:53:27    at System.IO.Pipelines.Pipe.DefaultPipeWriter.Complete(Exception exception)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal.AdaptedPipeline.<ReadInputAsync>d__16.MoveNext()
11:53:27 --- End of stack trace from previous location where exception was thrown ---
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal.AdaptedPipeline.<RunAsync>d__14.MoveNext()
11:53:27 --- End of stack trace from previous location where exception was thrown ---
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.HttpConnection.<ProcessRequestsAsync>d__47`1.MoveNext() ---> System.ObjectDisposedException: Cannot access a disposed object.
11:53:27 Object name: 'MemoryPoolBlock'.
11:53:27    at System.Buffers.MemoryPoolBlock.Dispose()
11:53:27    at System.IO.Pipelines.BufferSegment.ResetMemory()
11:53:27    at System.IO.Pipelines.Pipe.CompletePipe()
11:53:27    at System.IO.Pipelines.Pipe.CompleteWriter(Exception exception)
11:53:27    at System.IO.Pipelines.Pipe.DefaultPipeWriter.Complete(Exception exception)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal.AdaptedPipeline.<ReadInputAsync>d__16.MoveNext()
11:53:27 --- End of stack trace from previous location where exception was thrown ---
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Adapter.Internal.AdaptedPipeline.<RunAsync>d__14.MoveNext()
11:53:27 --- End of stack trace from previous location where exception was thrown ---
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ValidateEnd(Task task)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.HttpConnection.<ProcessRequestsAsync>d__47`1.MoveNext()
11:53:27    --- End of inner exception stack trace ---
11:53:27    at Microsoft.AspNetCore.Testing.TestApplicationErrorLogger.Log[TState](LogLevel logLevel, EventId eventId, TState state, Exception exception, Func`3 formatter)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.KestrelTrace.Log[TState](LogLevel logLevel, EventId eventId, TState state, Exception exception, Func`3 formatter)
11:53:27    at Microsoft.Extensions.Logging.LoggerExtensions.LogCritical(ILogger logger, EventId eventId, Exception exception, String message, Object[] args)
11:53:27    at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.HttpConnection.<ProcessRequestsAsync>d__47`1.MoveNext()
11:53:27 --- End of stack trace from previous location where exception was thrown ---
11:53:27    at System.Runtime.CompilerServices.TaskAwaiter.ThrowForNonSuccess(Task task)
11:53:27    at 

@Tratcher
Copy link
Member Author

Tratcher commented Apr 3, 2018

@pakrym this is the stuff you're updating right? Is this a matter of mismatched dependencies?

@pakrym
Copy link
Contributor

pakrym commented Apr 3, 2018

The update is finished in release/2.1. These failures might be an actual issue.

var localhostCert = CertificateLoader.LoadFromStoreCert("localhost", "My", StoreLocation.CurrentUser, allowInvalid: true);
httpsOptions.ServerCertificateSelector = (features, name) =>
{
// TODO: Name check, multiple certs, null names.
Copy link
Member

Choose a reason for hiding this comment

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

Is this something we actually want to do in this sample?

Copy link
Member Author

@Tratcher Tratcher Apr 3, 2018

Choose a reason for hiding this comment

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

No plans to do this in the sample, only showing where to do it. I'll remove the TODO.

@@ -29,14 +30,25 @@ public HttpsConnectionAdapterOptions()

/// <summary>
/// <para>
/// Specifies the server certificate used to authenticate HTTPS connections.
/// Specifies the server certificate used to authenticate HTTPS connections. This is ignored if ServerCertificateSelector is set.
Copy link
Member

Choose a reason for hiding this comment

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

I wish we didn't have to set an order of precedence. Are there any alternatives?

Copy link
Member Author

Choose a reason for hiding this comment

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

There are several layers that can assign default certs, so the best option for now is to ignore it if a selector is provided. See #2422

listenOptions.UseHttps(httpsOptions =>
{
var localhostCert = CertificateLoader.LoadFromStoreCert("localhost", "My", StoreLocation.CurrentUser, allowInvalid: true);
httpsOptions.ServerCertificateSelector = (features, name) =>
Copy link
Member

@halter73 halter73 Apr 3, 2018

Choose a reason for hiding this comment

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

I'm not sure I like this just being a settable property. What about adding a RegisterCertificateSelectorCallback() method that takes a state object as well?

Copy link
Member Author

@Tratcher Tratcher Apr 3, 2018

Choose a reason for hiding this comment

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

Offline note: this is per endpoint, not per connection. No state is required to reduce allocations.

@Tratcher Tratcher merged commit 9ea2c50 into release/2.1 Apr 4, 2018
@Tratcher Tratcher deleted the tratcher/sni branch April 4, 2018 20:43
@Tratcher Tratcher removed the blocked Blocked label Apr 5, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

9 participants