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:
{
public:
GLOBAL_NOTIFICATION_STATUS OnGlobalPreBeginRequest(IPreBeginRequestProvider* pProvider)
{
IHttpRequest * pRequest = pProvider->GetHttpContext()->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;
}
const DWORD FailureReasonsToIgnore = CERT_E_CN_NO_MATCH |
CERT_E_UNTRUSTEDCA |
CERT_E_CN_NO_MATCH;
// Remove SSL policy error flags.
pClientCertificate->CertFlags &= ~FailureReasonsToIgnore;
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.