How to allow self-signed client certificates in IIS
IIS allows you to use client certificates but it may give you hard time if your certificate is wacky (e.g. self-signed). In the client-side, if you have a .NET application that wants to be more permissible on accepting server certificates, you can hook the event ServerCertificateValidationCallback and decide weather or not you want to accept the server certificate; the callback provides you the certificate, the chain and the SslPolicyErrors (as flags) found during validation.
IIS don’t have such abstraction for validating client certificates, but it does provide the raw mechanism for performing the validation yourself. By default, IIS will do the validation for you during the BeginRequest pipeline event and will fail if any error is found. To perform the validation yourself you need listen the PreBeginRequest global event, stream the certificate from client and finally ignore the SSL Policy Errors (Flags) that you want.
The PreBeginRequest global event is not available (yet) for .NET code, you have to bring your C++ skills and write an IIS global module, no worries, there no too much code to write.
High level, you need:
- Create a Visual C++ project following the walkthrough Creating a Global-Level HTTP Module By Using Native Code
- Once you have the RegisterModule function that describes the walkthrough above, create Global-Level Module class as described in Designing Native-Code HTTP Modules. Override the virtual method OnGlobalPreBeginRequest and register for GL_PRE_BEGIN_REQUEST.
- In the OnGlboalPreBeginRequest method, get a pointer to the IHttpRequest interface and call NegotiateClientCertificate synchronously. This method will ask the client to present its certificate.
- Once you have the client certificate negotiated, get it by calling GetClientCertificate. The client certificate is represented by the structure HTTP_SSL_CLIENT_CERT_INFO.
- Finally, just remove the SSL Policy error flags that you don’t care about from the field CertFlags. Set it to zero to ignore everything, including expired certificates.
The code that you need (at your own risk) is something like:
class CClientCertificateGlobalModule : public CGlobalModule
{
public:
GLOBAL_NOTIFICATION_STATUS OnGlobalPreBeginRequest(IPreBeginRequestProvider* pProvider)
{
IHttpContext* pContext = pProvider->GetHttpContext();
IHttpRequest* pRequest = pContext->GetRequest();
if (pRequest->GetRawHttpRequest()->pSslInfo == NULL)
{
return GL_NOTIFICATION_CONTINUE;
}
HRESULT hr;
HTTP_SSL_CLIENT_CERT_INFO* pClientCertificate;
BOOL fNegotiated;
hr = pRequest->GetClientCertificate(&pClientCertificate, &fNegotiated);
if (FAILED(hr))
{
pProvider->SetErrorStatus(hr);
return GL_NOTIFICATION_CONTINUE;
}
if (fNegotiated)
{
return GL_NOTIFICATION_CONTINUE;
}
hr = pRequest->NegotiateClientCertificate(FALSE);
if (FAILED(hr))
{
pProvider->SetErrorStatus(hr);
return GL_NOTIFICATION_CONTINUE;
}
hr = pRequest->GetClientCertificate(&pClientCertificate, &fNegotiated);
if (FAILED(hr))
{
pProvider->SetErrorStatus(hr);
return GL_NOTIFICATION_CONTINUE;
}
// No longer a candidate for caching.
pContext->GetResponse()->DisableKernelCache(14 /*SSL*/);
switch (pClientCertificate->CertFlags)
{
case CERT_E_UNTRUSTEDCA:
case CERT_E_CN_NO_MATCH:
pClientCertificate->CertFlags = 0;
break;
}
return GL_NOTIFICATION_CONTINUE;
}
void Terminate()
{
// Don't do anything since it is used as a global variable.
}
};
// Global variable is fine because we assume we don't
// need to do anything on Terminate method.
CClientCertificateGlobalModule g_ClientCertificateGlobalModule;
HRESULT RegisterModule(DWORD, IHttpModuleRegistrationInfo* pModuleInfo, IHttpServer*)
{
return pModuleInfo->SetGlobalNotifications(&g_ClientCertificateGlobalModule,
GL_PRE_BEGIN_REQUEST);
}
A small deployment suggestion, in the Visual C++ project, change the settings to statically link to all libraries, like STL/CRT/MFC so you don’t require to install the Microsoft Visual C++ Redistributable Package.
Comments
Anonymous
July 25, 2012
Great Sometimes i see very nice and easy created blogs but in the most ways they are very usefull like your blog. http://johnnymbombenza.com/Anonymous
January 29, 2013
Tenks for yuo admin http://www.oyunoynatr.org/Anonymous
October 27, 2013
Thanks for your description and provided source code. It really helped me. In step 5 of the description you mention to "just remove the SSL Policy error flags that you don’t care about from the field CertFlags". It's not really clear to me how to do this because CertFlags represents more an error code than a set of flags (msdn.microsoft.com/.../aa377188(v=vs.85).aspx). As an example, a certificate might be expired and has an untrusted root. I want to pass through all client certificates with an untrusted root (CERT_E_UNTRUSTEDROOT, 0x800B0109L), i.e., ignore this specific error/flag, but leave all other flags/error indicators, like expired (CERT_E_EXPIRED, 0x800B0101L) untouched.Anonymous
May 31, 2014
Is there anything for Managed Code as yet to get around this in IIS?Anonymous
October 30, 2014
Curious to know if there are any updates on supporting this in managed code.