Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix TLS resume with HttpClientHandler on Linux #88214

Merged
merged 6 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
using System.IO;
using System.Net.Security;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Threading;
Expand Down Expand Up @@ -174,6 +175,9 @@ public class GenericLoopbackOptions
SslProtocols.Tls12;

public int ListenBacklog { get; set; } = 1;
#if !NETSTANDARD2_0 && !NETFRAMEWORK
public SslStreamCertificateContext? CertificateContext { get; set; }
#endif
}

public struct HttpHeaderData
Expand Down
27 changes: 22 additions & 5 deletions src/libraries/Common/tests/System/Net/Http/LoopbackServer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ public sealed partial class LoopbackServer : GenericLoopbackServer, IDisposable
public LoopbackServer(Options options = null)
{
_options = options ??= new Options();
#if !NETSTANDARD2_0 && !NETFRAMEWORK
if (_options.UseSsl && _options.CertificateContext == null)
{
_options.CertificateContext = SslStreamCertificateContext.Create(_options.Certificate ?? Configuration.Certificates.GetServerCertificate(), null);
}
#endif
}

#pragma warning disable CS1998 // Async method lacks 'await' operators and will run synchronously
Expand Down Expand Up @@ -471,14 +477,25 @@ public static async Task<Connection> CreateAsync(SocketWrapper socket, Stream st
if (httpOptions.UseSsl)
{
var sslStream = new SslStream(stream, false, delegate { return true; });
#if !NETFRAMEWORK
SslServerAuthenticationOptions sslOptions = new SslServerAuthenticationOptions()
{
EnabledSslProtocols = httpOptions.SslProtocols,
ServerCertificateContext = httpOptions.CertificateContext ?? SslStreamCertificateContext.Create(Configuration.Certificates.GetServerCertificate(), null),
ClientCertificateRequired = true,
};

await sslStream.AuthenticateAsServerAsync(sslOptions, default).ConfigureAwait(false);
#else
using (X509Certificate2 cert = httpOptions.Certificate ?? Configuration.Certificates.GetServerCertificate())
{
await sslStream.AuthenticateAsServerAsync(
cert,
clientCertificateRequired: true, // allowed but not required
enabledSslProtocols: httpOptions.SslProtocols,
checkCertificateRevocation: false).ConfigureAwait(false);
await sslStream.AuthenticateAsServerAsync(
cert,
clientCertificateRequired: true, // allowed but not required
enabledSslProtocols: httpOptions.SslProtocols,
checkCertificateRevocation: false).ConfigureAwait(false);
wfurt marked this conversation as resolved.
Show resolved Hide resolved
}
#endif
stream = sslStream;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ public bool AllowAutoRedirect
}
}

internal ClientCertificateOption ClientCertificateOptions;

public const bool SupportsAutomaticDecompression = false;
public const bool SupportsProxy = false;
public const bool SupportsRedirectConfiguration = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ public partial class HttpClientHandler : HttpMessageHandler
private HttpHandlerType Handler => _underlyingHandler;
#endif

private ClientCertificateOption _clientCertificateOptions;

private volatile bool _disposed;

public HttpClientHandler()
Expand Down Expand Up @@ -207,34 +205,29 @@ public int MaxResponseHeadersLength

public ClientCertificateOption ClientCertificateOptions
{
get => _clientCertificateOptions;
get => _underlyingHandler.ClientCertificateOptions;
set
{
switch (value)
{
case ClientCertificateOption.Manual:
#if TARGET_BROWSER
_clientCertificateOptions = value;
#else
#if !TARGET_BROWSER
ThrowForModifiedManagedSslOptionsIfStarted();
_clientCertificateOptions = value;
_underlyingHandler.SslOptions.LocalCertificateSelectionCallback = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => CertificateHelper.GetEligibleClientCertificate(_underlyingHandler.SslOptions.ClientCertificates)!;
#endif
break;

case ClientCertificateOption.Automatic:
#if TARGET_BROWSER
_clientCertificateOptions = value;
#else
#if !TARGET_BROWSER
ThrowForModifiedManagedSslOptionsIfStarted();
_clientCertificateOptions = value;
_underlyingHandler.SslOptions.LocalCertificateSelectionCallback = (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) => CertificateHelper.GetEligibleClientCertificate()!;
#endif
break;

default:
throw new ArgumentOutOfRangeException(nameof(value));
}
_underlyingHandler.ClientCertificateOptions = value;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,15 @@ private static SslClientAuthenticationOptions ConstructSslOptions(HttpConnection

SslClientAuthenticationOptions sslOptions = poolManager.Settings._sslOptions?.ShallowClone() ?? new SslClientAuthenticationOptions();

// This is only set if we are underlying handler for HttpClientHandler
if (poolManager.Settings._clientCertificateOptions == ClientCertificateOption.Manual && sslOptions.LocalCertificateSelectionCallback != null &&
(sslOptions.ClientCertificates == null || sslOptions.ClientCertificates.Count == 0))
{
// If we have no client certificates do not set callback when internal selection is used.
// It breaks TLS resume on Linux
sslOptions.LocalCertificateSelectionCallback = null;
}

// Set TargetHost for SNI
sslOptions.TargetHost = sslHostName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ internal sealed class HttpConnectionSettings
// Http2 flow control settings:
internal int _initialHttp2StreamWindowSize = HttpHandlerDefaults.DefaultInitialHttp2StreamWindowSize;

internal ClientCertificateOption _clientCertificateOptions;

public HttpConnectionSettings()
{
bool allowHttp2 = GlobalHttpSettings.SocketsHttpHandler.AllowHttp2;
Expand All @@ -71,6 +73,8 @@ public HttpConnectionSettings()
allowHttp3 && allowHttp2 ? HttpVersion.Version30 :
allowHttp2 ? HttpVersion.Version20 :
HttpVersion.Version11;

_clientCertificateOptions = ClientCertificateOption.Automatic;
}

/// <summary>Creates a copy of the settings but with some values normalized to suit the implementation.</summary>
Expand Down Expand Up @@ -117,6 +121,7 @@ public HttpConnectionSettings CloneAndNormalize()
_activityHeadersPropagator = _activityHeadersPropagator,
_defaultCredentialsUsedForProxy = _proxy != null && (_proxy.Credentials == CredentialCache.DefaultCredentials || _defaultProxyCredentials == CredentialCache.DefaultCredentials),
_defaultCredentialsUsedForServer = _credentials == CredentialCache.DefaultCredentials,
_clientCertificateOptions = _clientCertificateOptions,
};

return settings;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,15 @@ public DistributedContextPropagator? ActivityHeadersPropagator
}
}

internal ClientCertificateOption ClientCertificateOptions
{
get => _settings._clientCertificateOptions;
set
{
CheckDisposedOrStarted();
_settings._clientCertificateOptions = value;
}
}
protected override void Dispose(bool disposing)
{
if (disposing && !_disposed)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
using System.Net.Sockets;
using System.Net.Test.Common;
using System.Numerics;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Authentication;
using System.Security.Cryptography.X509Certificates;
Expand Down Expand Up @@ -4288,6 +4289,51 @@ public sealed class SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http11 :
{
public SocketsHttpHandler_SocketsHttpHandler_SecurityTest_Http11(ITestOutputHelper output) : base(output) { }
protected override Version UseVersion => HttpVersion.Version11;

#if DEBUG
[Theory]
[InlineData(true)]
[InlineData(false)]
[PlatformSpecific(TestPlatforms.Windows | TestPlatforms.Linux)]
public async Task Https_MultipleRequests_TlsResumed(bool useSocketHandler)
{
await LoopbackServer.CreateClientAndServerAsync(async uri =>
{
HttpMessageHandler handler = useSocketHandler ? CreateSocketsHttpHandler(allowAllCertificates: true) : CreateHttpClientHandler();
using (HttpClient client = CreateHttpClient(handler))
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get,uri);
request.Headers.Add("Host", "foo.bar");
request.Headers.Add("Connection", "close");

HttpResponseMessage response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);

request = new HttpRequestMessage(HttpMethod.Get,uri);
request.Headers.Add("Host", "foo.bar");
response = await client.SendAsync(request);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
}
},
async server =>
{
await server.AcceptConnectionSendResponseAndCloseAsync();
await server.AcceptConnectionAsync(async connection =>
{
SslStream ssl = (SslStream)connection.Stream;
object connectionInfo = typeof(SslStream).GetField(
"_connectionInfo",
BindingFlags.Instance | BindingFlags.NonPublic).GetValue(ssl);

bool resumed = (bool)connectionInfo.GetType().GetProperty("TlsResumed").GetValue(connectionInfo);
Assert.True(resumed);

await connection.ReadRequestHeaderAndSendResponseAsync();
});
},
new LoopbackServer.Options { UseSsl = true, SslProtocols = SslProtocols.Tls12 });
}
#endif
}

[ConditionalClass(typeof(PlatformDetection), nameof(PlatformDetection.SupportsAlpn))]
Expand Down