diff --git a/src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs b/src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs
index f25f75f41f62..31f097495b1e 100644
--- a/src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs
+++ b/src/Servers/HttpSys/ref/Microsoft.AspNetCore.Server.HttpSys.netcoreapp.cs
@@ -26,6 +26,12 @@ public enum AuthenticationSchemes
Negotiate = 8,
Kerberos = 16,
}
+ public enum ClientCertificateMethod
+ {
+ NoCertificate = 0,
+ AllowCertificate = 1,
+ AllowRenegotation = 2,
+ }
public enum Http503VerbosityLevel : long
{
Basic = (long)0,
@@ -46,6 +52,7 @@ public partial class HttpSysOptions
public HttpSysOptions() { }
public bool AllowSynchronousIO { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Server.HttpSys.AuthenticationManager Authentication { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } }
+ public Microsoft.AspNetCore.Server.HttpSys.ClientCertificateMethod ClientCertificateMethod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public bool EnableResponseCaching { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
public Microsoft.AspNetCore.Server.HttpSys.Http503VerbosityLevel Http503Verbosity { get { throw null; } set { } }
public int MaxAccepts { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } }
diff --git a/src/Servers/HttpSys/src/ClientCertificateMethod.cs b/src/Servers/HttpSys/src/ClientCertificateMethod.cs
new file mode 100644
index 000000000000..6c5265df0898
--- /dev/null
+++ b/src/Servers/HttpSys/src/ClientCertificateMethod.cs
@@ -0,0 +1,26 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Server.HttpSys
+{
+ ///
+ /// Describes the client certificate negotiation method for HTTPS connections.
+ ///
+ public enum ClientCertificateMethod
+ {
+ ///
+ /// A client certificate will not be populated on the request.
+ ///
+ NoCertificate = 0,
+
+ ///
+ /// A client certificate will be populated if already present at the start of a request.
+ ///
+ AllowCertificate,
+
+ ///
+ /// The TLS session can be renegotiated to request a client certificate.
+ ///
+ AllowRenegotation
+ }
+}
diff --git a/src/Servers/HttpSys/src/FeatureContext.cs b/src/Servers/HttpSys/src/FeatureContext.cs
index a689230ab80d..7cce609f3e71 100644
--- a/src/Servers/HttpSys/src/FeatureContext.cs
+++ b/src/Servers/HttpSys/src/FeatureContext.cs
@@ -316,7 +316,17 @@ X509Certificate2 ITlsConnectionFeature.ClientCertificate
{
if (IsNotInitialized(Fields.ClientCertificate))
{
- _clientCert = Request.GetClientCertificateAsync().Result; // TODO: Sync;
+ var method = _requestContext.Server.Options.ClientCertificateMethod;
+ if (method == ClientCertificateMethod.AllowCertificate)
+ {
+ _clientCert = Request.ClientCertificate;
+ }
+ else if (method == ClientCertificateMethod.AllowRenegotation)
+ {
+ _clientCert = Request.GetClientCertificateAsync().Result; // TODO: Sync over async;
+ }
+ // else if (method == ClientCertificateMethod.NoCertificate) // No-op
+
SetInitialized(Fields.ClientCertificate);
}
return _clientCert;
diff --git a/src/Servers/HttpSys/src/HttpSysOptions.cs b/src/Servers/HttpSys/src/HttpSysOptions.cs
index 477a0bda2812..15e83d9fea73 100644
--- a/src/Servers/HttpSys/src/HttpSysOptions.cs
+++ b/src/Servers/HttpSys/src/HttpSysOptions.cs
@@ -54,6 +54,13 @@ public string RequestQueueName
///
public RequestQueueMode RequestQueueMode { get; set; }
+ ///
+ /// Indicates how client certificates should be populated. The default is to allow renegotation.
+ /// This does not change the netsh 'clientcertnegotiation' binding option which will need to be enabled for
+ /// ClientCertificateMethod.AllowCertificate to resolve a certificate.
+ ///
+ public ClientCertificateMethod ClientCertificateMethod { get; set; } = ClientCertificateMethod.AllowRenegotation;
+
///
/// The maximum number of concurrent accepts.
///
diff --git a/src/Servers/HttpSys/src/RequestProcessing/Request.cs b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
index bf663da134ee..049789abea38 100644
--- a/src/Servers/HttpSys/src/RequestProcessing/Request.cs
+++ b/src/Servers/HttpSys/src/RequestProcessing/Request.cs
@@ -6,12 +6,15 @@
using System.Globalization;
using System.IO;
using System.Net;
+using System.Security;
using System.Security.Authentication;
+using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.HttpSys.Internal;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.Server.HttpSys
{
@@ -323,6 +326,30 @@ private void GetTlsHandshakeResults()
KeyExchangeStrength = (int)handshake.KeyExchangeStrength;
}
+ public X509Certificate2 ClientCertificate
+ {
+ get
+ {
+ if (_clientCert == null && SslStatus == SslStatus.ClientCert)
+ {
+ try
+ {
+ _clientCert = _nativeRequestContext.GetClientCertificate();
+ }
+ catch (CryptographicException ce)
+ {
+ RequestContext.Logger.LogDebug(ce, "An error occurred reading the client certificate.");
+ }
+ catch (SecurityException se)
+ {
+ RequestContext.Logger.LogDebug(se, "An error occurred reading the client certificate.");
+ }
+ }
+
+ return _clientCert;
+ }
+ }
+
// Populates the client certificate. The result may be null if there is no client cert.
// TODO: Does it make sense for this to be invoked multiple times (e.g. renegotiate)? Client and server code appear to
// enable this, but it's unclear what Http.Sys would do.
diff --git a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs
index 2c4dcf1ce1fc..4108d901e2b9 100644
--- a/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs
+++ b/src/Shared/HttpSys/RequestProcessing/NativeRequestContext.cs
@@ -8,6 +8,7 @@
using System.Diagnostics;
using System.Net.Sockets;
using System.Runtime.InteropServices;
+using System.Security.Cryptography.X509Certificates;
using System.Security.Principal;
using Microsoft.Extensions.Primitives;
@@ -518,5 +519,49 @@ private IReadOnlyDictionary> GetRequestInfo(IntPtr bas
return new ReadOnlyDictionary>(info);
}
+
+ internal X509Certificate2 GetClientCertificate()
+ {
+ if (_permanentlyPinned)
+ {
+ return GetClientCertificate((IntPtr)_nativeRequest, (HttpApiTypes.HTTP_REQUEST_V2*)_nativeRequest);
+ }
+ else
+ {
+ fixed (byte* pMemoryBlob = _backingBuffer)
+ {
+ var request = (HttpApiTypes.HTTP_REQUEST_V2*)(pMemoryBlob + _bufferAlignment);
+ return GetClientCertificate(_originalBufferAddress, request);
+ }
+ }
+ }
+
+ // Throws CryptographicException
+ private X509Certificate2 GetClientCertificate(IntPtr baseAddress, HttpApiTypes.HTTP_REQUEST_V2* nativeRequest)
+ {
+ var request = nativeRequest->Request;
+ long fixup = (byte*)nativeRequest - (byte*)baseAddress;
+ if (request.pSslInfo == null)
+ {
+ return null;
+ }
+
+ var sslInfo = (HttpApiTypes.HTTP_SSL_INFO*)((byte*)request.pSslInfo + fixup);
+ if (sslInfo->SslClientCertNegotiated == 0 || sslInfo->pClientCertInfo == null)
+ {
+ return null;
+ }
+
+ var clientCertInfo = (HttpApiTypes.HTTP_SSL_CLIENT_CERT_INFO*)((byte*)sslInfo->pClientCertInfo + fixup);
+ if (clientCertInfo->pCertEncoded == null)
+ {
+ return null;
+ }
+
+ var clientCert = clientCertInfo->pCertEncoded + fixup;
+ byte[] certEncoded = new byte[clientCertInfo->CertEncodedSize];
+ Marshal.Copy((IntPtr)clientCert, certEncoded, 0, certEncoded.Length);
+ return new X509Certificate2(certEncoded);
+ }
}
}