Skip to content

Commit

Permalink
fix TLS resume with HttpClientHandler on Linux (#88214)
Browse files Browse the repository at this point in the history
* fix TLS resume with HttpHandler

* update

* cleanup

* update

* net48

* fix spacing
  • Loading branch information
wfurt authored Jul 6, 2023
1 parent 0518ab1 commit 0224f86
Show file tree
Hide file tree
Showing 8 changed files with 96 additions and 11 deletions.
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
17 changes: 17 additions & 0 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,6 +477,16 @@ 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(
Expand All @@ -479,6 +495,7 @@ await sslStream.AuthenticateAsServerAsync(
enabledSslProtocols: httpOptions.SslProtocols,
checkCertificateRevocation: false).ConfigureAwait(false);
}
#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

0 comments on commit 0224f86

Please sign in to comment.