Skip to content

Commit a3d1956

Browse files
authored
Make HttpSys client certificate behavior consistent #33586 (#34012)
1 parent 3c95550 commit a3d1956

File tree

6 files changed

+73
-35
lines changed

6 files changed

+73
-35
lines changed
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<Project>
2+
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
3+
<PropertyGroup>
4+
<KestrelSharedSourceRoot>$(MSBuildThisFileDirectory)..\Kestrel\shared\</KestrelSharedSourceRoot>
5+
</PropertyGroup>
6+
</Project>

src/Servers/HttpSys/HttpSysServer.slnf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@
88
"src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj",
99
"src\\Http\\Authentication.Abstractions\\src\\Microsoft.AspNetCore.Authentication.Abstractions.csproj",
1010
"src\\Http\\Authentication.Core\\src\\Microsoft.AspNetCore.Authentication.Core.csproj",
11+
"src\\Http\\Features\\src\\Microsoft.Extensions.Features.csproj",
1112
"src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj",
1213
"src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj",
1314
"src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj",
14-
"src\\Http\\Features\\src\\Microsoft.Extensions.Features.csproj",
1515
"src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj",
1616
"src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj",
1717
"src\\Http\\Metadata\\src\\Microsoft.AspNetCore.Metadata.csproj",
@@ -25,8 +25,8 @@
2525
"src\\Servers\\HttpSys\\src\\Microsoft.AspNetCore.Server.HttpSys.csproj",
2626
"src\\Servers\\HttpSys\\test\\FunctionalTests\\Microsoft.AspNetCore.Server.HttpSys.FunctionalTests.csproj",
2727
"src\\Servers\\HttpSys\\test\\Tests\\Microsoft.AspNetCore.Server.HttpSys.Tests.csproj",
28-
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
2928
"src\\Servers\\IIS\\IIS\\src\\Microsoft.AspNetCore.Server.IIS.csproj",
29+
"src\\Servers\\Kestrel\\Transport.Sockets\\src\\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj",
3030
"src\\Testing\\src\\Microsoft.AspNetCore.Testing.csproj"
3131
]
3232
}

src/Servers/HttpSys/src/RequestProcessing/RequestContext.FeatureCollection.cs

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ internal partial class RequestContext :
6262
private string? _connectionId;
6363
private string? _traceIdentitfier;
6464
private X509Certificate2? _clientCert;
65+
private Task<X509Certificate2?>? _clientCertTask;
6566
private ClaimsPrincipal? _user;
6667
private Stream _responseStream = default!;
6768
private PipeWriter? _pipeWriter;
@@ -318,15 +319,10 @@ string IHttpConnectionFeature.ConnectionId
318319
if (IsNotInitialized(Fields.ClientCertificate))
319320
{
320321
var method = Server.Options.ClientCertificateMethod;
321-
if (method == ClientCertificateMethod.AllowCertificate)
322+
if (method != ClientCertificateMethod.NoCertificate)
322323
{
323324
_clientCert = Request.ClientCertificate;
324325
}
325-
else if (method == ClientCertificateMethod.AllowRenegotation)
326-
{
327-
_clientCert = Request.GetClientCertificateAsync().Result; // TODO: Sync over async;
328-
}
329-
// else if (method == ClientCertificateMethod.NoCertificate) // No-op
330326

331327
SetInitialized(Fields.ClientCertificate);
332328
}
@@ -335,29 +331,34 @@ string IHttpConnectionFeature.ConnectionId
335331
set
336332
{
337333
_clientCert = value;
334+
_clientCertTask = Task.FromResult(value);
338335
SetInitialized(Fields.ClientCertificate);
339336
}
340337
}
341338

342-
async Task<X509Certificate2?> ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken)
339+
Task<X509Certificate2?> ITlsConnectionFeature.GetClientCertificateAsync(CancellationToken cancellationToken)
343340
{
344-
if (IsNotInitialized(Fields.ClientCertificate))
341+
if (_clientCertTask != null)
345342
{
346-
var method = Server.Options.ClientCertificateMethod;
347-
if (method != ClientCertificateMethod.NoCertificate)
348-
{
349-
// Check if a cert was already available on the connection.
350-
_clientCert = Request.ClientCertificate;
351-
}
343+
return _clientCertTask;
344+
}
352345

353-
if (_clientCert == null && method == ClientCertificateMethod.AllowRenegotation)
354-
{
355-
_clientCert = await Request.GetClientCertificateAsync(cancellationToken);
356-
}
346+
var tlsFeature = (ITlsConnectionFeature)this;
347+
var clientCert = tlsFeature.ClientCertificate; // Lazy initialized
348+
if (clientCert != null
349+
|| Server.Options.ClientCertificateMethod != ClientCertificateMethod.AllowRenegotation
350+
// Delayed client cert negotiation is not allowed on HTTP/2.
351+
|| Request.ProtocolVersion >= HttpVersion.Version20)
352+
{
353+
return _clientCertTask = Task.FromResult(clientCert);
354+
}
357355

358-
SetInitialized(Fields.ClientCertificate);
356+
return _clientCertTask = GetCertificateAsync(cancellationToken);
357+
358+
async Task<X509Certificate2?> GetCertificateAsync(CancellationToken cancellation)
359+
{
360+
return _clientCert = await Request.GetClientCertificateAsync(cancellation);
359361
}
360-
return _clientCert;
361362
}
362363

363364
internal ITlsConnectionFeature? GetTlsConnectionFeature()

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

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ namespace Microsoft.AspNetCore.Server.HttpSys
2121
{
2222
public class HttpsTests
2323
{
24+
private static readonly X509Certificate2 _x509Certificate2 = TestResources.GetTestCertificate("eku.client.pfx");
25+
2426
[ConditionalFact]
2527
public async Task Https_200OK_Success()
2628
{
@@ -66,38 +68,61 @@ public async Task Https_EchoHelloWorld_Success()
6668
}
6769
}
6870

69-
[ConditionalFact]
70-
public async Task Https_ClientCertNotSent_ClientCertNotPresent()
71+
[ConditionalTheory]
72+
[InlineData(ClientCertificateMethod.NoCertificate)]
73+
[InlineData(ClientCertificateMethod.AllowCertificate)]
74+
[InlineData(ClientCertificateMethod.AllowRenegotation)]
75+
public async Task Https_ClientCertNotSent_ClientCertNotPresent(ClientCertificateMethod clientCertificateMethod)
7176
{
72-
using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
77+
using (Utilities.CreateDynamicHttpsServer("", out var root, out var address, options =>
78+
{
79+
options.ClientCertificateMethod = clientCertificateMethod;
80+
},
81+
async httpContext =>
7382
{
7483
var tls = httpContext.Features.Get<ITlsConnectionFeature>();
7584
Assert.NotNull(tls);
85+
Assert.Null(tls.ClientCertificate);
7686
var cert = await tls.GetClientCertificateAsync(CancellationToken.None);
7787
Assert.Null(cert);
7888
Assert.Null(tls.ClientCertificate);
7989
}))
8090
{
81-
string response = await SendRequestAsync(address);
91+
var response = await SendRequestAsync(address);
8292
Assert.Equal(string.Empty, response);
8393
}
8494
}
8595

86-
[ConditionalFact(Skip = "Manual test only, client certs are not always available.")]
87-
public async Task Https_ClientCertRequested_ClientCertPresent()
96+
[ConditionalTheory]
97+
[InlineData(ClientCertificateMethod.NoCertificate)]
98+
[InlineData(ClientCertificateMethod.AllowCertificate)]
99+
[InlineData(ClientCertificateMethod.AllowRenegotation)]
100+
public async Task Https_ClientCertRequested_ClientCertPresent(ClientCertificateMethod clientCertificateMethod)
88101
{
89-
using (Utilities.CreateDynamicHttpsServer(out var address, async httpContext =>
102+
using (Utilities.CreateDynamicHttpsServer("", out var root, out var address, options =>
103+
{
104+
options.ClientCertificateMethod = clientCertificateMethod;
105+
},
106+
async httpContext =>
90107
{
91108
var tls = httpContext.Features.Get<ITlsConnectionFeature>();
92109
Assert.NotNull(tls);
110+
Assert.Null(tls.ClientCertificate);
93111
var cert = await tls.GetClientCertificateAsync(CancellationToken.None);
94-
Assert.NotNull(cert);
95-
Assert.NotNull(tls.ClientCertificate);
112+
if (clientCertificateMethod == ClientCertificateMethod.AllowRenegotation)
113+
{
114+
Assert.NotNull(cert);
115+
Assert.NotNull(tls.ClientCertificate);
116+
}
117+
else
118+
{
119+
Assert.Null(cert);
120+
Assert.Null(tls.ClientCertificate);
121+
}
96122
}))
97123
{
98-
X509Certificate2 cert = FindClientCert();
99-
Assert.NotNull(cert);
100-
string response = await SendRequestAsync(address, cert);
124+
Assert.NotNull(_x509Certificate2);
125+
var response = await SendRequestAsync(address, _x509Certificate2);
101126
Assert.Equal(string.Empty, response);
102127
}
103128
}

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
<Compile Include="$(SharedSourceRoot)ValueTaskExtensions\**\*.cs" LinkBase="Shared\" />
1616
<Compile Remove="$(SharedSourceRoot)ServerInfrastructure\DuplexPipe.cs" />
1717
<Compile Remove="$(SharedSourceRoot)ServerInfrastructure\StringUtilities.cs" />
18+
<Compile Include="$(KestrelSharedSourceRoot)test\TestResources.cs" LinkBase="shared" />
19+
<Content Include="$(KestrelSharedSourceRoot)test\TestCertificates\*.pfx" LinkBase="shared\TestCertificates" CopyToOutputDirectory="PreserveNewest" />
1820
</ItemGroup>
1921

2022
<ItemGroup>

src/Servers/Kestrel/Core/src/Internal/TlsConnectionFeature.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ public X509Certificate2? ClientCertificate
4646
{
4747
return _clientCert ??= ConvertToX509Certificate2(_sslStream.RemoteCertificate);
4848
}
49-
set => _clientCert = value;
49+
set
50+
{
51+
_clientCert = value;
52+
_clientCertTask = Task.FromResult(value);
53+
}
5054
}
5155

5256
// Used for event source, not part of any of the feature interfaces.

0 commit comments

Comments
 (0)