Skip to content

Commit 188243a

Browse files
authored
Enable HTTP/2 client cert authentication in WinHttpHandler (#33158)
Pre-release WinHTTP's version supports client cert authentication over HTTP/2, but the feature must be explicitly opted-in. PR sets WINHTTP_OPTION_ENABLE_HTTP2_PLUS_CLIENT_CERT to TRUE before invoking WinHttpConnect if the request's protocol is HTTP/2 and scheme is HTTPS. This PR also enables all HTTP 1.1 tests for WinHttpHandler on .Net Core and Framework and the most of HTTP/2 tests on .Net Core.
1 parent e47e7be commit 188243a

33 files changed

+900
-164
lines changed

src/libraries/Common/src/Interop/Windows/WinHttp/Interop.winhttp_types.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,11 @@ internal partial class WinHttp
149149

150150
public const uint WINHTTP_OPTION_ASSURED_NON_BLOCKING_CALLBACKS = 111;
151151

152+
public const uint WINHTTP_OPTION_ENABLE_HTTP2_PLUS_CLIENT_CERT = 161;
152153
public const uint WINHTTP_OPTION_ENABLE_HTTP_PROTOCOL = 133;
153154
public const uint WINHTTP_OPTION_HTTP_PROTOCOL_USED = 134;
154155
public const uint WINHTTP_PROTOCOL_FLAG_HTTP2 = 0x1;
156+
public const uint WINHTTP_HTTP2_PLUS_CLIENT_CERT_FLAG = 0x1;
155157

156158
public const uint WINHTTP_OPTION_UPGRADE_TO_WEB_SOCKET = 114;
157159
public const uint WINHTTP_OPTION_WEB_SOCKET_CLOSE_TIMEOUT = 115;

src/libraries/Common/tests/System/Net/Http/DefaultCredentialsTest.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -335,7 +335,8 @@ private async Task ProcessRequests()
335335

336336
// Send a response in the JSON format that the client expects
337337
string username = context.User.Identity.Name;
338-
await context.Response.OutputStream.WriteAsync(System.Text.Encoding.UTF8.GetBytes($"{{\"authenticated\": \"true\", \"user\": \"{username}\" }}"));
338+
byte[] bytes = System.Text.Encoding.UTF8.GetBytes($"{{\"authenticated\": \"true\", \"user\": \"{username}\" }}");
339+
await context.Response.OutputStream.WriteAsync(bytes);
339340

340341
context.Response.Close();
341342
}

src/libraries/Common/tests/System/Net/Http/GenericLoopbackServer.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ public class GenericLoopbackOptions
8686
public IPAddress Address { get; set; } = IPAddress.Loopback;
8787
public bool UseSsl { get; set; } = PlatformDetection.SupportsAlpn && !Capability.Http2ForceUnencryptedLoopback();
8888
public SslProtocols SslProtocols { get; set; } =
89-
#if !NETSTANDARD2_0
89+
#if !NETSTANDARD2_0 && !NETFRAMEWORK
9090
SslProtocols.Tls13 |
9191
#endif
9292
SslProtocols.Tls12;

src/libraries/Common/tests/System/Net/Http/Http2Frames.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -548,10 +548,12 @@ public override void WriteTo(Span<byte> buffer)
548548
BinaryPrimitives.WriteUInt16BigEndian(buffer, checked((ushort)Origin.Length));
549549
buffer = buffer.Slice(2);
550550

551-
Encoding.ASCII.GetBytes(Origin, buffer);
551+
var tmpBuffer = Encoding.ASCII.GetBytes(Origin);
552+
tmpBuffer.CopyTo(buffer);
552553
buffer = buffer.Slice(Origin.Length);
553554

554-
Encoding.ASCII.GetBytes(AltSvc, buffer);
555+
tmpBuffer = Encoding.ASCII.GetBytes(AltSvc);
556+
tmpBuffer.CopyTo(buffer);
555557
}
556558

557559
public override string ToString() => $"{base.ToString()}\n{nameof(Origin)}: {Origin}\n{nameof(AltSvc)}: {AltSvc}";

src/libraries/Common/tests/System/Net/Http/Http2LoopbackConnection.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
// See the LICENSE file in the project root for more information.
44

55
using System.Collections.Generic;
6-
using System.Diagnostics;
76
using System.IO;
7+
using System.Linq;
88
using System.Net.Http.Functional.Tests;
99
using System.Net.Security;
1010
using System.Net.Sockets;
@@ -28,6 +28,7 @@ public class Http2LoopbackConnection : GenericLoopbackConnection
2828
private readonly byte[] _prefix;
2929
public string PrefixString => Encoding.UTF8.GetString(_prefix, 0, _prefix.Length);
3030
public bool IsInvalid => _connectionSocket == null;
31+
public Stream Stream => _connectionStream;
3132

3233
public Http2LoopbackConnection(Socket socket, Http2Options httpOptions)
3334
{
@@ -40,6 +41,7 @@ public Http2LoopbackConnection(Socket socket, Http2Options httpOptions)
4041

4142
using (var cert = Configuration.Certificates.GetServerCertificate())
4243
{
44+
#if !NETFRAMEWORK
4345
SslServerAuthenticationOptions options = new SslServerAuthenticationOptions();
4446

4547
options.EnabledSslProtocols = httpOptions.SslProtocols;
@@ -51,9 +53,12 @@ public Http2LoopbackConnection(Socket socket, Http2Options httpOptions)
5153

5254
options.ServerCertificate = cert;
5355

54-
options.ClientCertificateRequired = false;
56+
options.ClientCertificateRequired = httpOptions.ClientCertificateRequired;
5557

5658
sslStream.AuthenticateAsServerAsync(options, CancellationToken.None).Wait();
59+
#else
60+
sslStream.AuthenticateAsServerAsync(cert, httpOptions.ClientCertificateRequired, httpOptions.SslProtocols, checkCertificateRevocation: false).Wait();
61+
#endif
5762
}
5863

5964
_connectionStream = sslStream;
@@ -64,6 +69,10 @@ public Http2LoopbackConnection(Socket socket, Http2Options httpOptions)
6469
{
6570
throw new Exception("Connection stream closed while attempting to read connection preface.");
6671
}
72+
else if (Text.Encoding.ASCII.GetString(_prefix).Contains("HTTP/1.1"))
73+
{
74+
throw new Exception("HTTP 1.1 request received.");
75+
}
6776
}
6877

6978
public async Task SendConnectionPrefaceAsync()
@@ -331,7 +340,7 @@ private static (int bytesConsumed, string value) DecodeString(ReadOnlySpan<byte>
331340
}
332341
else
333342
{
334-
string value = Encoding.ASCII.GetString(headerBlock.Slice(bytesConsumed, stringLength));
343+
string value = Encoding.ASCII.GetString(headerBlock.Slice(bytesConsumed, stringLength).ToArray());
335344
return (bytesConsumed + stringLength, value);
336345
}
337346
}

src/libraries/Common/tests/System/Net/Http/Http2LoopbackServer.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,14 @@ public override async Task AcceptConnectionAsync(Func<GenericLoopbackConnection,
181181
}
182182
}
183183

184-
public static async Task CreateClientAndServerAsync(Func<Uri, Task> clientFunc, Func<Http2LoopbackServer, Task> serverFunc, int timeout = 60_000)
184+
public static Task CreateClientAndServerAsync(Func<Uri, Task> clientFunc, Func<Http2LoopbackServer, Task> serverFunc, int timeout = 60_000)
185185
{
186-
using (var server = Http2LoopbackServer.CreateServer())
186+
return CreateClientAndServerAsync(clientFunc, serverFunc, null, timeout);
187+
}
188+
189+
public static async Task CreateClientAndServerAsync(Func<Uri, Task> clientFunc, Func<Http2LoopbackServer, Task> serverFunc, Http2Options http2Options, int timeout = 60_000)
190+
{
191+
using (var server = Http2LoopbackServer.CreateServer(http2Options ?? new Http2Options()))
187192
{
188193
Task clientTask = clientFunc(server.Address);
189194
Task serverTask = serverFunc(server);
@@ -197,6 +202,8 @@ public class Http2Options : GenericLoopbackOptions
197202
{
198203
public int ListenBacklog { get; set; } = 1;
199204

205+
public bool ClientCertificateRequired { get; set; }
206+
200207
public Http2Options()
201208
{
202209
UseSsl = PlatformDetection.SupportsAlpn && !Capability.Http2ForceUnencryptedLoopback();
@@ -237,7 +244,7 @@ public override async Task CreateServerAsync(Func<GenericLoopbackServer, Uri, Ta
237244
}
238245
}
239246

240-
public override Version Version => HttpVersion.Version20;
247+
public override Version Version => HttpVersion20.Value;
241248
}
242249

243250
public enum ProtocolErrors
@@ -257,4 +264,9 @@ public enum ProtocolErrors
257264
INADEQUATE_SECURITY = 0xc,
258265
HTTP_1_1_REQUIRED = 0xd
259266
}
267+
268+
public static class HttpVersion20
269+
{
270+
public static readonly Version Value = new Version(2, 0);
271+
}
260272
}

src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.AcceptAllCerts.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ public void SingletonReturnsTrue()
3939
[InlineData(SslProtocols.Tls, true)]
4040
[InlineData(SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, false)]
4141
[InlineData(SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, true)]
42+
#if !NETFRAMEWORK
4243
[InlineData(SslProtocols.Tls13 | SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, false)]
4344
[InlineData(SslProtocols.Tls13 | SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls, true)]
45+
#endif
4446
[InlineData(SslProtocols.None, false)]
4547
[InlineData(SslProtocols.None, true)]
4648
public async Task SetDelegate_ConnectionSucceeds(SslProtocols acceptedProtocol, bool requestOnlyThisProtocol)
@@ -64,7 +66,11 @@ public async Task SetDelegate_ConnectionSucceeds(SslProtocols acceptedProtocol,
6466
// restrictions on minimum TLS/SSL version
6567
// We currently know that some platforms like Debian 10 OpenSSL
6668
// will by default block < TLS 1.2
69+
#if !NETFRAMEWORK
6770
handler.SslProtocols = SslProtocols.Tls13 | SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
71+
#else
72+
handler.SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls11 | SslProtocols.Tls;
73+
#endif
6874
}
6975

7076
var options = new LoopbackServer.Options { UseSsl = true, SslProtocols = acceptedProtocol };

src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Authentication.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,12 @@ public async Task Credentials_ServerUsesWindowsAuthentication_Success(string ser
602602
[InlineData("Negotiate")]
603603
public async Task Credentials_ServerChallengesWithWindowsAuth_ClientSendsWindowsAuthHeader(string authScheme)
604604
{
605+
#if WINHTTPHANDLER_TEST
606+
if (UseVersion > HttpVersion.Version11)
607+
{
608+
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
609+
}
610+
#endif
605611
await LoopbackServerFactory.CreateClientAndServerAsync(
606612
async uri =>
607613
{

src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.Cancellation.cs

Lines changed: 45 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,17 +28,24 @@ public abstract class HttpClientHandler_Cancellation_Test : HttpClientHandlerTes
2828
{
2929
public HttpClientHandler_Cancellation_Test(ITestOutputHelper output) : base(output) { }
3030

31-
[Theory]
31+
[ConditionalTheory]
3232
[InlineData(false, CancellationMode.Token)]
3333
[InlineData(true, CancellationMode.Token)]
3434
public async Task PostAsync_CancelDuringRequestContentSend_TaskCanceledQuickly(bool chunkedTransfer, CancellationMode mode)
3535
{
36-
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && chunkedTransfer)
36+
if (LoopbackServerFactory.Version >= HttpVersion20.Value && chunkedTransfer)
3737
{
3838
// There is no chunked encoding in HTTP/2 and later
3939
return;
4040
}
4141

42+
#if WINHTTPHANDLER_TEST
43+
if (UseVersion >= HttpVersion20.Value)
44+
{
45+
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
46+
}
47+
#endif
48+
4249
var serverRelease = new TaskCompletionSource<bool>();
4350
await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
4451
{
@@ -76,16 +83,23 @@ await LoopbackServerFactory.CreateClientAndServerAsync(async uri =>
7683
});
7784
}
7885

79-
[Theory]
86+
[ConditionalTheory]
8087
[MemberData(nameof(OneBoolAndCancellationMode))]
8188
public async Task GetAsync_CancelDuringResponseHeadersReceived_TaskCanceledQuickly(bool connectionClose, CancellationMode mode)
8289
{
83-
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && connectionClose)
90+
if (LoopbackServerFactory.Version >= HttpVersion20.Value && connectionClose)
8491
{
8592
// There is no Connection header in HTTP/2 and later
8693
return;
8794
}
8895

96+
#if WINHTTPHANDLER_TEST
97+
if (UseVersion >= HttpVersion20.Value)
98+
{
99+
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
100+
}
101+
#endif
102+
89103
using (HttpClient client = CreateHttpClient())
90104
{
91105
client.Timeout = Timeout.InfiniteTimeSpan;
@@ -130,7 +144,7 @@ await ValidateClientCancellationAsync(async () =>
130144
[MemberData(nameof(TwoBoolsAndCancellationMode))]
131145
public async Task GetAsync_CancelDuringResponseBodyReceived_Buffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, CancellationMode mode)
132146
{
133-
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && (chunkedTransfer || connectionClose))
147+
if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose))
134148
{
135149
// There is no chunked encoding or connection header in HTTP/2 and later
136150
return;
@@ -182,16 +196,23 @@ await ValidateClientCancellationAsync(async () =>
182196
}
183197
}
184198

185-
[Theory]
199+
[ConditionalTheory]
186200
[MemberData(nameof(ThreeBools))]
187201
public async Task GetAsync_CancelDuringResponseBodyReceived_Unbuffered_TaskCanceledQuickly(bool chunkedTransfer, bool connectionClose, bool readOrCopyToAsync)
188202
{
189-
if (LoopbackServerFactory.Version >= HttpVersion.Version20 && (chunkedTransfer || connectionClose))
203+
if (LoopbackServerFactory.Version >= HttpVersion20.Value && (chunkedTransfer || connectionClose))
190204
{
191205
// There is no chunked encoding or connection header in HTTP/2 and later
192206
return;
193207
}
194208

209+
#if WINHTTPHANDLER_TEST
210+
if (UseVersion >= HttpVersion20.Value)
211+
{
212+
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
213+
}
214+
#endif
215+
195216
using (HttpClient client = CreateHttpClient())
196217
{
197218
client.Timeout = Timeout.InfiniteTimeSpan;
@@ -237,14 +258,19 @@ await ValidateClientCancellationAsync(async () =>
237258
});
238259
}
239260
}
240-
241-
[Theory]
261+
[ConditionalTheory]
242262
[InlineData(CancellationMode.CancelPendingRequests, false)]
243263
[InlineData(CancellationMode.DisposeHttpClient, false)]
244264
[InlineData(CancellationMode.CancelPendingRequests, true)]
245265
[InlineData(CancellationMode.DisposeHttpClient, true)]
246266
public async Task GetAsync_CancelPendingRequests_DoesntCancelReadAsyncOnResponseStream(CancellationMode mode, bool copyToAsync)
247267
{
268+
#if WINHTTPHANDLER_TEST
269+
if (UseVersion >= HttpVersion20.Value)
270+
{
271+
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
272+
}
273+
#endif
248274
using (HttpClient client = CreateHttpClient())
249275
{
250276
client.Timeout = Timeout.InfiniteTimeSpan;
@@ -312,7 +338,7 @@ await LoopbackServerFactory.CreateServerAsync(async (server, url) =>
312338
[ConditionalFact]
313339
public async Task MaxConnectionsPerServer_WaitingConnectionsAreCancelable()
314340
{
315-
if (LoopbackServerFactory.Version >= HttpVersion.Version20)
341+
if (LoopbackServerFactory.Version >= HttpVersion20.Value)
316342
{
317343
// HTTP/2 does not use connection limits.
318344
throw new SkipTestException("Not supported on HTTP/2 and later");
@@ -490,11 +516,18 @@ public static IEnumerable<object[]> PostAsync_Cancel_CancellationTokenPassedToCo
490516
}
491517
}
492518

519+
#if !NETFRAMEWORK
493520
[OuterLoop("Uses Task.Delay")]
494-
[Theory]
521+
[ConditionalTheory]
495522
[MemberData(nameof(PostAsync_Cancel_CancellationTokenPassedToContent_MemberData))]
496523
public async Task PostAsync_Cancel_CancellationTokenPassedToContent(HttpContent content, CancellationTokenSource cancellationTokenSource)
497524
{
525+
#if WINHTTPHANDLER_TEST
526+
if (UseVersion > HttpVersion.Version11)
527+
{
528+
throw new SkipTestException($"Test doesn't support {UseVersion} protocol.");
529+
}
530+
#endif
498531
await LoopbackServerFactory.CreateClientAndServerAsync(
499532
async uri =>
500533
{
@@ -518,6 +551,7 @@ await LoopbackServerFactory.CreateClientAndServerAsync(
518551
catch (Exception) { }
519552
});
520553
}
554+
#endif
521555

522556
private async Task ValidateClientCancellationAsync(Func<Task> clientBodyAsync)
523557
{

src/libraries/Common/tests/System/Net/Http/HttpClientHandlerTest.ClientCertificates.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ await TestHelper.WhenAllCompletedOrAnyFailed(
113113
{
114114
_output.WriteLine(
115115
"Client cert: {0}",
116-
((X509Certificate2)sslStream.RemoteCertificate).GetNameInfo(X509NameType.SimpleName, false));
116+
new X509Certificate2(sslStream.RemoteCertificate.Export(X509ContentType.Cert)).GetNameInfo(X509NameType.SimpleName, false));
117117
Assert.Equal(cert, sslStream.RemoteCertificate);
118118
}
119119
else

0 commit comments

Comments
 (0)