// Copyright (c) Microsoft Open Technologies, Inc. // All Rights Reserved // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // THIS CODE IS PROVIDED *AS IS* BASIS, WITHOUT WARRANTIES OR // CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING // WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF // TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR // NON-INFRINGEMENT. // See the Apache 2 License for the specific language governing // permissions and limitations under the License. // ----------------------------------------------------------------------- // // Copyright (c) Microsoft Corporation. All rights reserved. // // ----------------------------------------------------------------------- using System; using System.Diagnostics.CodeAnalysis; using System.Diagnostics.Contracts; using System.Runtime.InteropServices; using System.Security; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; namespace Microsoft.Net.Http.Server { // This class is used to load the client certificate on-demand. Because client certs are optional, all // failures are handled internally and reported via ClientCertException or ClientCertError. internal unsafe sealed class ClientCertLoader : IAsyncResult, IDisposable { private const uint CertBoblSize = 1500; private static readonly IOCompletionCallback IOCallback = new IOCompletionCallback(WaitCallback); private SafeNativeOverlapped _overlapped; private byte[] _backingBuffer; private UnsafeNclNativeMethods.HttpApi.HTTP_SSL_CLIENT_CERT_INFO* _memoryBlob; private uint _size; private TaskCompletionSource _tcs; private RequestContext _requestContext; private int _clientCertError; private X509Certificate2 _clientCert; private Exception _clientCertException; private CancellationTokenRegistration _cancellationRegistration; internal ClientCertLoader(RequestContext requestContext, CancellationToken cancellationToken) { _requestContext = requestContext; _tcs = new TaskCompletionSource(); // we will use this overlapped structure to issue async IO to ul // the event handle will be put in by the BeginHttpApi2.ERROR_SUCCESS() method Reset(CertBoblSize); if (cancellationToken.CanBeCanceled) { _cancellationRegistration = cancellationToken.Register(RequestContext.AbortDelegate, _requestContext); } } internal X509Certificate2 ClientCert { get { Contract.Assert(Task.IsCompleted); return _clientCert; } } internal int ClientCertError { get { Contract.Assert(Task.IsCompleted); return _clientCertError; } } internal Exception ClientCertException { get { Contract.Assert(Task.IsCompleted); return _clientCertException; } } private RequestContext RequestContext { get { return _requestContext; } } private Task Task { get { return _tcs.Task; } } private SafeNativeOverlapped NativeOverlapped { get { return _overlapped; } } private UnsafeNclNativeMethods.HttpApi.HTTP_SSL_CLIENT_CERT_INFO* RequestBlob { get { return _memoryBlob; } } private void Reset(uint size) { if (size == _size) { return; } if (_size != 0) { _overlapped.Dispose(); } _size = size; if (size == 0) { _overlapped = null; _memoryBlob = null; _backingBuffer = null; return; } _backingBuffer = new byte[checked((int)size)]; var boundHandle = RequestContext.Server.BoundHandle; _overlapped = new SafeNativeOverlapped(boundHandle, boundHandle.AllocateNativeOverlapped(IOCallback, this, _backingBuffer)); _memoryBlob = (UnsafeNclNativeMethods.HttpApi.HTTP_SSL_CLIENT_CERT_INFO*)Marshal.UnsafeAddrOfPinnedArrayElement(_backingBuffer, 0); } // When you use netsh to configure HTTP.SYS with clientcertnegotiation = enable // which means negotiate client certificates, when the client makes the // initial SSL connection, the server (HTTP.SYS) requests the client certificate. // // Some apps may not want to negotiate the client cert at the beginning, // perhaps serving the default.htm. In this case the HTTP.SYS is configured // with clientcertnegotiation = disabled, which means that the client certificate is // optional so initially when SSL is established HTTP.SYS won't ask for client // certificate. This works fine for the default.htm in the case above, // however, if the app wants to demand a client certificate at a later time // perhaps showing "YOUR ORDERS" page, then the server wants to negotiate // Client certs. This will in turn makes HTTP.SYS to do the // SEC_I_RENOGOTIATE through which the client cert demand is made // // NOTE: When calling HttpReceiveClientCertificate you can get // ERROR_NOT_FOUND - which means the client did not provide the cert // If this is important, the server should respond with 403 forbidden // HTTP.SYS will not do this for you automatically internal Task LoadClientCertificateAsync() { uint size = CertBoblSize; bool retry; do { retry = false; uint bytesReceived = 0; uint statusCode = UnsafeNclNativeMethods.HttpApi.HttpReceiveClientCertificate( RequestContext.RequestQueueHandle, RequestContext.Request.ConnectionId, (uint)UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE, RequestBlob, size, &bytesReceived, NativeOverlapped); if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA) { UnsafeNclNativeMethods.HttpApi.HTTP_SSL_CLIENT_CERT_INFO* pClientCertInfo = RequestBlob; size = bytesReceived + pClientCertInfo->CertEncodedSize; Reset(size); retry = true; } else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_NOT_FOUND) { // The client did not send a cert. Complete(0, null); } else if (statusCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && WebListener.SkipIOCPCallbackOnSuccess) { IOCompleted(statusCode, bytesReceived); } else if (statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && statusCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING) { // Some other bad error, possible(?) return values are: // ERROR_INVALID_HANDLE, ERROR_INSUFFICIENT_BUFFER, ERROR_OPERATION_ABORTED // Also ERROR_BAD_DATA if we got it twice or it reported smaller size buffer required. Fail(new WebListenerException((int)statusCode)); } } while (retry); return Task; } private void Complete(int certErrors, X509Certificate2 cert) { // May be null _clientCert = cert; _clientCertError = certErrors; Dispose(); _tcs.TrySetResult(null); } private void Fail(Exception ex) { // TODO: Log _clientCertException = ex; Dispose(); _tcs.TrySetResult(null); } private unsafe void IOCompleted(uint errorCode, uint numBytes) { IOCompleted(this, errorCode, numBytes); } [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "Redirected to callback")] private static unsafe void IOCompleted(ClientCertLoader asyncResult, uint errorCode, uint numBytes) { RequestContext requestContext = asyncResult.RequestContext; try { if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_MORE_DATA) { // There is a bug that has existed in http.sys since w2k3. Bytesreceived will only // return the size of the initial cert structure. To get the full size, // we need to add the certificate encoding size as well. UnsafeNclNativeMethods.HttpApi.HTTP_SSL_CLIENT_CERT_INFO* pClientCertInfo = asyncResult.RequestBlob; asyncResult.Reset(numBytes + pClientCertInfo->CertEncodedSize); uint bytesReceived = 0; errorCode = UnsafeNclNativeMethods.HttpApi.HttpReceiveClientCertificate( requestContext.RequestQueueHandle, requestContext.Request.ConnectionId, (uint)UnsafeNclNativeMethods.HttpApi.HTTP_FLAGS.NONE, asyncResult._memoryBlob, asyncResult._size, &bytesReceived, asyncResult._overlapped); if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_IO_PENDING || (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS && !WebListener.SkipIOCPCallbackOnSuccess)) { return; } } if (errorCode == UnsafeNclNativeMethods.ErrorCodes.ERROR_NOT_FOUND) { // The client did not send a cert. asyncResult.Complete(0, null); } else if (errorCode != UnsafeNclNativeMethods.ErrorCodes.ERROR_SUCCESS) { asyncResult.Fail(new WebListenerException((int)errorCode)); } else { UnsafeNclNativeMethods.HttpApi.HTTP_SSL_CLIENT_CERT_INFO* pClientCertInfo = asyncResult._memoryBlob; if (pClientCertInfo == null) { asyncResult.Complete(0, null); } else { if (pClientCertInfo->pCertEncoded != null) { try { byte[] certEncoded = new byte[pClientCertInfo->CertEncodedSize]; Marshal.Copy((IntPtr)pClientCertInfo->pCertEncoded, certEncoded, 0, certEncoded.Length); asyncResult.Complete((int)pClientCertInfo->CertFlags, new X509Certificate2(certEncoded)); } catch (CryptographicException exception) { // TODO: Log asyncResult.Fail(exception); } catch (SecurityException exception) { // TODO: Log asyncResult.Fail(exception); } } } } } catch (Exception exception) { asyncResult.Fail(exception); } } private static unsafe void WaitCallback(uint errorCode, uint numBytes, NativeOverlapped* nativeOverlapped) { var asyncResult = (ClientCertLoader)ThreadPoolBoundHandle.GetNativeOverlappedState(nativeOverlapped); IOCompleted(asyncResult, errorCode, numBytes); } public void Dispose() { Dispose(true); } private void Dispose(bool disposing) { if (disposing) { _cancellationRegistration.Dispose(); if (_overlapped != null) { _memoryBlob = null; _overlapped.Dispose(); } } } public object AsyncState { get { return _tcs.Task.AsyncState; } } public WaitHandle AsyncWaitHandle { get { return ((IAsyncResult)_tcs.Task).AsyncWaitHandle; } } public bool CompletedSynchronously { get { return ((IAsyncResult)_tcs.Task).CompletedSynchronously; } } public bool IsCompleted { get { return _tcs.Task.IsCompleted; } } } }