Skip to content

Commit f3b0fbe

Browse files
authored
Allow opt out of HttpSys client cert negotiation #14806 (#14839)
1 parent d0de736 commit f3b0fbe

File tree

6 files changed

+123
-1
lines changed

6 files changed

+123
-1
lines changed

src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public enum AuthenticationSchemes
2626
Negotiate = 8,
2727
Kerberos = 16,
2828
}
29+
public enum ClientCertificateMethod
30+
{
31+
NoCertificate = 0,
32+
AllowCertificate = 1,
33+
AllowRenegotation = 2,
34+
}
2935
public enum Http503VerbosityLevel : long
3036
{
3137
Basic = (long)0,
@@ -46,6 +52,7 @@ public partial class HttpSysOptions
4652
public HttpSysOptions() { }
4753
public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
4854
public Microsoft.AspNetCore.Server.HttpSys.AuthenticationManager Authentication { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
55+
public Microsoft.AspNetCore.Server.HttpSys.ClientCertificateMethod ClientCertificateMethod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
4956
public bool EnableResponseCaching { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
5057
public Microsoft.AspNetCore.Server.HttpSys.Http503VerbosityLevel Http503Verbosity { get { throw null; } set { } }
5158
public int MaxAccepts { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
namespace Microsoft.AspNetCore.Server.HttpSys
5+
{
6+
/// <summary>
7+
/// Describes the client certificate negotiation method for HTTPS connections.
8+
/// </summary>
9+
public enum ClientCertificateMethod
10+
{
11+
/// <summary>
12+
/// A client certificate will not be populated on the request.
13+
/// </summary>
14+
NoCertificate = 0,
15+
16+
/// <summary>
17+
/// A client certificate will be populated if already present at the start of a request.
18+
/// </summary>
19+
AllowCertificate,
20+
21+
/// <summary>
22+
/// The TLS session can be renegotiated to request a client certificate.
23+
/// </summary>
24+
AllowRenegotation
25+
}
26+
}

src/Servers/HttpSys/src/FeatureContext.cs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,17 @@ X509Certificate2 ITlsConnectionFeature.ClientCertificate
316316
{
317317
if (IsNotInitialized(Fields.ClientCertificate))
318318
{
319-
_clientCert = Request.GetClientCertificateAsync().Result; // TODO: Sync;
319+
var method = _requestContext.Server.Options.ClientCertificateMethod;
320+
if (method == ClientCertificateMethod.AllowCertificate)
321+
{
322+
_clientCert = Request.ClientCertificate;
323+
}
324+
else if (method == ClientCertificateMethod.AllowRenegotation)
325+
{
326+
_clientCert = Request.GetClientCertificateAsync().Result; // TODO: Sync over async;
327+
}
328+
// else if (method == ClientCertificateMethod.NoCertificate) // No-op
329+
320330
SetInitialized(Fields.ClientCertificate);
321331
}
322332
return _clientCert;

src/Servers/HttpSys/src/HttpSysOptions.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,13 @@ public string RequestQueueName
5454
/// </summary>
5555
public RequestQueueMode RequestQueueMode { get; set; }
5656

57+
/// <summary>
58+
/// Indicates how client certificates should be populated. The default is to allow renegotation.
59+
/// This does not change the netsh 'clientcertnegotiation' binding option which will need to be enabled for
60+
/// ClientCertificateMethod.AllowCertificate to resolve a certificate.
61+
/// </summary>
62+
public ClientCertificateMethod ClientCertificateMethod { get; set; } = ClientCertificateMethod.AllowRenegotation;
63+
5764
/// <summary>
5865
/// The maximum number of concurrent accepts.
5966
/// </summary>

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,15 @@
66
using System.Globalization;
77
using System.IO;
88
using System.Net;
9+
using System.Security;
910
using System.Security.Authentication;
11+
using System.Security.Cryptography;
1012
using System.Security.Cryptography.X509Certificates;
1113
using System.Security.Principal;
1214
using System.Threading;
1315
using System.Threading.Tasks;
1416
using Microsoft.AspNetCore.HttpSys.Internal;
17+
using Microsoft.Extensions.Logging;
1518

1619
namespace Microsoft.AspNetCore.Server.HttpSys
1720
{
@@ -323,6 +326,30 @@ private void GetTlsHandshakeResults()
323326
KeyExchangeStrength = (int)handshake.KeyExchangeStrength;
324327
}
325328

329+
public X509Certificate2 ClientCertificate
330+
{
331+
get
332+
{
333+
if (_clientCert == null && SslStatus == SslStatus.ClientCert)
334+
{
335+
try
336+
{
337+
_clientCert = _nativeRequestContext.GetClientCertificate();
338+
}
339+
catch (CryptographicException ce)
340+
{
341+
RequestContext.Logger.LogDebug(ce, "An error occurred reading the client certificate.");
342+
}
343+
catch (SecurityException se)
344+
{
345+
RequestContext.Logger.LogDebug(se, "An error occurred reading the client certificate.");
346+
}
347+
}
348+
349+
return _clientCert;
350+
}
351+
}
352+
326353
// Populates the client certificate. The result may be null if there is no client cert.
327354
// TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to
328355
// enable this, but it's unclear what Http.Sys would do.

src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
using System.Diagnostics;
99
using System.Net.Sockets;
1010
using System.Runtime.InteropServices;
11+
using System.Security.Cryptography.X509Certificates;
1112
using System.Security.Principal;
1213
using Microsoft.Extensions.Primitives;
1314

@@ -518,5 +519,49 @@ private IReadOnlyDictionary<int, ReadOnlyMemory<byte>> GetRequestInfo(IntPtr bas
518519

519520
return new ReadOnlyDictionary<int, ReadOnlyMemory<byte>>(info);
520521
}
522+
523+
internal X509Certificate2 GetClientCertificate()
524+
{
525+
if (_permanentlyPinned)
526+
{
527+
return GetClientCertificate((IntPtr)_nativeRequest, (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest);
528+
}
529+
else
530+
{
531+
fixed (byte* pMemoryBlob = _backingBuffer)
532+
{
533+
var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment);
534+
return GetClientCertificate(_originalBufferAddress, request);
535+
}
536+
}
537+
}
538+
539+
// Throws CryptographicException
540+
private X509Certificate2 GetClientCertificate(IntPtr baseAddress, HttpApiTypes.HTTP_REQUEST_V2* nativeRequest)
541+
{
542+
var request = nativeRequest->Request;
543+
long fixup = (byte*)nativeRequest - (byte*)baseAddress;
544+
if (request.pSslInfo == null)
545+
{
546+
return null;
547+
}
548+
549+
var sslInfo = (HttpApiTypes.HTTP_SSL_INFO*)((byte*)request.pSslInfo + fixup);
550+
if (sslInfo->SslClientCertNegotiated == 0 || sslInfo->pClientCertInfo == null)
551+
{
552+
return null;
553+
}
554+
555+
var clientCertInfo = (HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO*)((byte*)sslInfo->pClientCertInfo + fixup);
556+
if (clientCertInfo->pCertEncoded == null)
557+
{
558+
return null;
559+
}
560+
561+
var clientCert = clientCertInfo->pCertEncoded + fixup;
562+
byte[] certEncoded = new byte[clientCertInfo->CertEncodedSize];
563+
Marshal.Copy((IntPtr)clientCert, certEncoded, 0, certEncoded.Length);
564+
return new X509Certificate2(certEncoded);
565+
}
521566
}
522567
}

0 commit comments

Comments
 (0)