367 lines
14 KiB
C#
367 lines
14 KiB
C#
// 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 file="ClientCertLoader.cs" company="Microsoft">
|
|
// Copyright (c) Microsoft Corporation. All rights reserved.
|
|
// </copyright>
|
|
// -----------------------------------------------------------------------
|
|
|
|
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<object> _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<object>();
|
|
// 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; }
|
|
}
|
|
}
|
|
}
|