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); + } } }