Skip to content

Commit 06d7fe7

Browse files
authored
Implement ITlsHandshakeFeature for HttpSys (#7284)
1 parent 3fd8a97 commit 06d7fe7

File tree

10 files changed

+221
-36
lines changed

10 files changed

+221
-36
lines changed

src/Servers/HttpSys/src/FeatureContext.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
using System.Globalization;
77
using System.IO;
88
using System.Net;
9+
using System.Security.Authentication;
910
using System.Security.Claims;
1011
using System.Security.Cryptography.X509Certificates;
1112
using System.Threading;
1213
using System.Threading.Tasks;
14+
using Microsoft.AspNetCore.Connections.Features;
1315
using Microsoft.AspNetCore.Http;
1416
using Microsoft.AspNetCore.Http.Features;
1517
using Microsoft.AspNetCore.Http.Features.Authentication;
@@ -24,6 +26,7 @@ internal class FeatureContext :
2426
IHttpResponseFeature,
2527
IHttpSendFileFeature,
2628
ITlsConnectionFeature,
29+
ITlsHandshakeFeature,
2730
// ITlsTokenBindingFeature, TODO: https://github.com/aspnet/HttpSysServer/issues/231
2831
IHttpBufferingFeature,
2932
IHttpRequestLifetimeFeature,
@@ -336,6 +339,12 @@ internal ITlsConnectionFeature GetTlsConnectionFeature()
336339
{
337340
return Request.IsHttps ? this : null;
338341
}
342+
343+
internal ITlsHandshakeFeature GetTlsHandshakeFeature()
344+
{
345+
return Request.IsHttps ? this : null;
346+
}
347+
339348
/* TODO: https://github.com/aspnet/HttpSysServer/issues/231
340349
byte[] ITlsTokenBindingFeature.GetProvidedTokenBindingId() => Request.GetProvidedTokenBindingId();
341350
@@ -482,6 +491,20 @@ bool IHttpBodyControlFeature.AllowSynchronousIO
482491
set => Request.MaxRequestBodySize = value;
483492
}
484493

494+
SslProtocols ITlsHandshakeFeature.Protocol => Request.Protocol;
495+
496+
CipherAlgorithmType ITlsHandshakeFeature.CipherAlgorithm => Request.CipherAlgorithm;
497+
498+
int ITlsHandshakeFeature.CipherStrength => Request.CipherStrength;
499+
500+
HashAlgorithmType ITlsHandshakeFeature.HashAlgorithm => Request.HashAlgorithm;
501+
502+
int ITlsHandshakeFeature.HashStrength => Request.HashStrength;
503+
504+
ExchangeAlgorithmType ITlsHandshakeFeature.KeyExchangeAlgorithm => Request.KeyExchangeAlgorithm;
505+
506+
int ITlsHandshakeFeature.KeyExchangeStrength => Request.KeyExchangeStrength;
507+
485508
internal async Task OnResponseStart()
486509
{
487510
if (_responseStarted)

src/Servers/HttpSys/src/Microsoft.AspNetCore.Server.HttpSys.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<ItemGroup>
1818
<Reference Include="Microsoft.AspNetCore.Authentication.Core" />
19+
<Reference Include="Microsoft.AspNetCore.Connections.Abstractions" />
1920
<Reference Include="Microsoft.AspNetCore.Hosting" />
2021
<Reference Include="Microsoft.Net.Http.Headers" />
2122
<Reference Include="Microsoft.Win32.Registry" />

src/Servers/HttpSys/src/RequestProcessing/Request.cs

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Globalization;
66
using System.IO;
77
using System.Net;
8+
using System.Security.Authentication;
89
using System.Security.Cryptography.X509Certificates;
910
using System.Security.Principal;
1011
using System.Threading;
@@ -83,6 +84,11 @@ internal Request(RequestContext requestContext, NativeRequestContext nativeReque
8384

8485
User = _nativeRequestContext.GetUser();
8586

87+
if (IsHttps)
88+
{
89+
GetTlsHandshakeResults();
90+
}
91+
8692
// GetTlsTokenBindingInfo(); TODO: https://github.com/aspnet/HttpSysServer/issues/231
8793

8894
// Finished directly accessing the HTTP_REQUEST structure.
@@ -232,6 +238,60 @@ private AspNetCore.HttpSys.Internal.SocketAddress LocalEndPoint
232238

233239
internal WindowsPrincipal User { get; }
234240

241+
public SslProtocols Protocol { get; private set; }
242+
243+
public CipherAlgorithmType CipherAlgorithm { get; private set; }
244+
245+
public int CipherStrength { get; private set; }
246+
247+
public HashAlgorithmType HashAlgorithm { get; private set; }
248+
249+
public int HashStrength { get; private set; }
250+
251+
public ExchangeAlgorithmType KeyExchangeAlgorithm { get; private set; }
252+
253+
public int KeyExchangeStrength { get; private set; }
254+
255+
private void GetTlsHandshakeResults()
256+
{
257+
var handshake = _nativeRequestContext.GetTlsHandshake();
258+
259+
Protocol = handshake.Protocol;
260+
// The OS considers client and server TLS as different enum values. SslProtocols choose to combine those for some reason.
261+
// We need to fill in the client bits so the enum shows the expected protocol.
262+
// https://docs.microsoft.com/en-us/windows/desktop/api/schannel/ns-schannel-_secpkgcontext_connectioninfo
263+
// Compare to https://referencesource.microsoft.com/#System/net/System/Net/SecureProtocols/_SslState.cs,8905d1bf17729de3
264+
#pragma warning disable CS0618 // Type or member is obsolete
265+
if ((Protocol & SslProtocols.Ssl2) != 0)
266+
{
267+
Protocol |= SslProtocols.Ssl2;
268+
}
269+
if ((Protocol & SslProtocols.Ssl3) != 0)
270+
{
271+
Protocol |= SslProtocols.Ssl3;
272+
}
273+
#pragma warning restore CS0618 // Type or member is obsolete
274+
if ((Protocol & SslProtocols.Tls) != 0)
275+
{
276+
Protocol |= SslProtocols.Tls;
277+
}
278+
if ((Protocol & SslProtocols.Tls11) != 0)
279+
{
280+
Protocol |= SslProtocols.Tls11;
281+
}
282+
if ((Protocol & SslProtocols.Tls12) != 0)
283+
{
284+
Protocol |= SslProtocols.Tls12;
285+
}
286+
287+
CipherAlgorithm = handshake.CipherType;
288+
CipherStrength = (int)handshake.CipherStrength;
289+
HashAlgorithm = handshake.HashType;
290+
HashStrength = (int)handshake.HashStrength;
291+
KeyExchangeAlgorithm = handshake.KeyExchangeType;
292+
KeyExchangeStrength = (int)handshake.KeyExchangeStrength;
293+
}
294+
235295
// Populates the client certificate. The result may be null if there is no client cert.
236296
// TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to
237297
// enable this, but it's unclear what Http.Sys would do.

src/Servers/HttpSys/src/StandardFeatureCollection.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System;
55
using System.Collections;
66
using System.Collections.Generic;
7+
using Microsoft.AspNetCore.Connections.Features;
78
using Microsoft.AspNetCore.Http.Features;
89
using Microsoft.AspNetCore.Http.Features.Authentication;
910

@@ -19,6 +20,7 @@ internal sealed class StandardFeatureCollection : IFeatureCollection
1920
{ typeof(IHttpResponseFeature), _identityFunc },
2021
{ typeof(IHttpSendFileFeature), _identityFunc },
2122
{ typeof(ITlsConnectionFeature), ctx => ctx.GetTlsConnectionFeature() },
23+
{ typeof(ITlsHandshakeFeature), ctx => ctx.GetTlsHandshakeFeature() },
2224
// { typeof(ITlsTokenBindingFeature), ctx => ctx.GetTlsTokenBindingFeature() }, TODO: https://github.com/aspnet/HttpSysServer/issues/231
2325
{ typeof(IHttpBufferingFeature), _identityFunc },
2426
{ typeof(IHttpRequestLifetimeFeature), _identityFunc },

src/Servers/HttpSys/test/FunctionalTests/HttpsTests.cs

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

4+
using System;
45
using System.IO;
56
using System.Net.Http;
7+
using System.Security.Authentication;
68
using System.Security.Cryptography.X509Certificates;
79
using System.Text;
810
using System.Threading;
911
using System.Threading.Tasks;
12+
using Microsoft.AspNetCore.Connections.Features;
13+
using Microsoft.AspNetCore.Http;
1014
using Microsoft.AspNetCore.Http.Features;
1115
using Microsoft.AspNetCore.Testing.xunit;
1216
using Xunit;
@@ -15,40 +19,38 @@ namespace Microsoft.AspNetCore.Server.HttpSys
1519
{
1620
public class HttpsTests
1721
{
18-
private const string Address = "https://localhost:9090/";
19-
20-
[ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
22+
[ConditionalFact]
2123
public async Task Https_200OK_Success()
2224
{
23-
using (Utilities.CreateHttpsServer(httpContext =>
25+
using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
2426
{
2527
return Task.FromResult(0);
2628
}))
2729
{
28-
string response = await SendRequestAsync(Address);
30+
string response = await SendRequestAsync(address);
2931
Assert.Equal(string.Empty, response);
3032
}
3133
}
3234

33-
[ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
35+
[ConditionalFact]
3436
public async Task Https_SendHelloWorld_Success()
3537
{
36-
using (Utilities.CreateHttpsServer(httpContext =>
38+
using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
3739
{
3840
byte[] body = Encoding.UTF8.GetBytes("Hello World");
3941
httpContext.Response.ContentLength = body.Length;
4042
return httpContext.Response.Body.WriteAsync(body, 0, body.Length);
4143
}))
4244
{
43-
string response = await SendRequestAsync(Address);
45+
string response = await SendRequestAsync(address);
4446
Assert.Equal("Hello World", response);
4547
}
4648
}
4749

48-
[ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
50+
[ConditionalFact]
4951
public async Task Https_EchoHelloWorld_Success()
5052
{
51-
using (Utilities.CreateHttpsServer(httpContext =>
53+
using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
5254
{
5355
string input = new StreamReader(httpContext.Request.Body).ReadToEnd();
5456
Assert.Equal("Hello World", input);
@@ -58,15 +60,15 @@ public async Task Https_EchoHelloWorld_Success()
5860
return Task.FromResult(0);
5961
}))
6062
{
61-
string response = await SendRequestAsync(Address, "Hello World");
63+
string response = await SendRequestAsync(address, "Hello World");
6264
Assert.Equal("Hello World", response);
6365
}
6466
}
6567

66-
[ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
68+
[ConditionalFact]
6769
public async Task Https_ClientCertNotSent_ClientCertNotPresent()
6870
{
69-
using (Utilities.CreateHttpsServer(async httpContext =>
71+
using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
7072
{
7173
var tls = httpContext.Features.Get<ITlsConnectionFeature>();
7274
Assert.NotNull(tls);
@@ -75,15 +77,15 @@ public async Task Https_ClientCertNotSent_ClientCertNotPresent()
7577
Assert.Null(tls.ClientCertificate);
7678
}))
7779
{
78-
string response = await SendRequestAsync(Address);
80+
string response = await SendRequestAsync(address);
7981
Assert.Equal(string.Empty, response);
8082
}
8183
}
8284

83-
[ConditionalFact(Skip = "TODO: Add trait filtering support so these SSL tests don't get run on teamcity or the command line."), Trait("scheme", "https")]
85+
[ConditionalFact(Skip = "Manual test only, client certs are not always available.")]
8486
public async Task Https_ClientCertRequested_ClientCertPresent()
8587
{
86-
using (Utilities.CreateHttpsServer(async httpContext =>
88+
using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
8789
{
8890
var tls = httpContext.Features.Get<ITlsConnectionFeature>();
8991
Assert.NotNull(tls);
@@ -94,7 +96,37 @@ public async Task Https_ClientCertRequested_ClientCertPresent()
9496
{
9597
X509Certificate2 cert = FindClientCert();
9698
Assert.NotNull(cert);
97-
string response = await SendRequestAsync(Address, cert);
99+
string response = await SendRequestAsync(address, cert);
100+
Assert.Equal(string.Empty, response);
101+
}
102+
}
103+
104+
[ConditionalFact]
105+
public async Task Https_SetsITlsHandshakeFeature()
106+
{
107+
using (Utilities.CreateDynamicHttpsServer(out var address, httpContext =>
108+
{
109+
try
110+
{
111+
var tlsFeature = httpContext.Features.Get<ITlsHandshakeFeature>();
112+
Assert.NotNull(tlsFeature);
113+
Assert.True(tlsFeature.Protocol > SslProtocols.None, "Protocol");
114+
Assert.True(Enum.IsDefined(typeof(SslProtocols), tlsFeature.Protocol), "Defined"); // Mapping is required, make sure it's current
115+
Assert.True(tlsFeature.CipherAlgorithm > CipherAlgorithmType.Null, "Cipher");
116+
Assert.True(tlsFeature.CipherStrength > 0, "CipherStrength");
117+
Assert.True(tlsFeature.HashAlgorithm > HashAlgorithmType.None, "HashAlgorithm");
118+
Assert.True(tlsFeature.HashStrength >= 0, "HashStrength"); // May be 0 for some algorithms
119+
Assert.True(tlsFeature.KeyExchangeAlgorithm > ExchangeAlgorithmType.None, "KeyExchangeAlgorithm");
120+
Assert.True(tlsFeature.KeyExchangeStrength > 0, "KeyExchangeStrength");
121+
}
122+
catch (Exception ex)
123+
{
124+
return httpContext.Response.WriteAsync(ex.ToString());
125+
}
126+
return Task.FromResult(0);
127+
}))
128+
{
129+
string response = await SendRequestAsync(address);
98130
Assert.Equal(string.Empty, response);
99131
}
100132
}

src/Servers/HttpSys/test/FunctionalTests/Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,9 @@
1010
<Reference Include="System.Net.Http.WinHttpHandler" />
1111
</ItemGroup>
1212

13+
<PropertyGroup>
14+
<!--Imitate IIS Express so we can use it's cert bindings-->
15+
<PackageTags>214124cd-d05b-4309-9af9-9caa44b2b74a</PackageTags>
16+
</PropertyGroup>
17+
1318
</Project>

src/Servers/HttpSys/test/FunctionalTests/Utilities.cs

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,10 @@ internal static class Utilities
2323
// ports during dynamic port allocation.
2424
private const int BasePort = 5001;
2525
private const int MaxPort = 8000;
26+
private const int BaseHttpsPort = 44300;
27+
private const int MaxHttpsPort = 44399;
2628
private static int NextPort = BasePort;
29+
private static int NextHttpsPort = BaseHttpsPort;
2730
private static object PortLock = new object();
2831
internal static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(15);
2932
internal static readonly int WriteRetryLimit = 1000;
@@ -148,17 +151,37 @@ internal static IServer CreateDynamicHttpServer(string basePath, out string root
148151
throw new Exception("Failed to locate a free port.");
149152
}
150153

151-
internal static IServer CreateHttpsServer(RequestDelegate app)
154+
internal static IServer CreateDynamicHttpsServer(out string baseAddress, RequestDelegate app)
152155
{
153-
return CreateServer("https", "localhost", 9090, string.Empty, app);
156+
return CreateDynamicHttpsServer("/", out var root, out baseAddress, options => { }, app);
154157
}
155158

156-
internal static IServer CreateServer(string scheme, string host, int port, string path, RequestDelegate app)
159+
internal static IServer CreateDynamicHttpsServer(string basePath, out string root, out string baseAddress, Action<HttpSysOptions> configureOptions, RequestDelegate app)
157160
{
158-
var server = CreatePump();
159-
server.Features.Get<IServerAddressesFeature>().Addresses.Add(UrlPrefix.Create(scheme, host, port, path).ToString());
160-
server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
161-
return server;
161+
lock (PortLock)
162+
{
163+
while (NextHttpsPort < MaxHttpsPort)
164+
{
165+
var port = NextHttpsPort++;
166+
var prefix = UrlPrefix.Create("https", "localhost", port, basePath);
167+
root = prefix.Scheme + "://" + prefix.Host + ":" + prefix.Port;
168+
baseAddress = prefix.ToString();
169+
170+
var server = CreatePump();
171+
server.Features.Get<IServerAddressesFeature>().Addresses.Add(baseAddress);
172+
configureOptions(server.Listener.Options);
173+
try
174+
{
175+
server.StartAsync(new DummyApplication(app), CancellationToken.None).Wait();
176+
return server;
177+
}
178+
catch (HttpSysException)
179+
{
180+
}
181+
}
182+
NextHttpsPort = BaseHttpsPort;
183+
}
184+
throw new Exception("Failed to locate a free port.");
162185
}
163186

164187
internal static Task WithTimeout(this Task task) => task.TimeoutAfter(DefaultTimeout);

0 commit comments

Comments
 (0)