18
votes

I need to trust some self-signed certificates in the application, so I override validation callback like this:

ServicePointManager.ServerCertificateValidationCallback = MyRemoteCertificateValidationCallback;
...

public static bool MyRemoteCertificateValidationCallback(
            Object sender,
            X509Certificate certificate,
            X509Chain chain,
            SslPolicyErrors sslPolicyErrors)
{

    if (sslPolicyErrors == SslPolicyErrors.None)
        return true;

    if (IsAprrovedByMyApplication(sender, certificate))  // <-- no matter what the check here is
       return true;
    else 
       return false;  // <-- here I'd like to call the default Windows handler rather than returning 'false'
}

But when there're some policy errors, and the site I am connecting to is not approved by application, the Exception is thrown. The problem here is that it differs from standard Windows behavior.

Consider this site: https://www.dscoduc.com/

It's certificate has an unknown issuer, and therefore untrusted. I have added it with MMC to the Local Computer's Trusted People (it's Windows 7).

If I run this code without overriding certificate validation callback:

HttpWebRequest http = (HttpWebRequest)HttpWebRequest.Create("https://www.dscoduc.com/");
using (WebResponse resp = http.GetResponse())
{
    using (StreamReader sr = new StreamReader(resp.GetResponseStream()))
    {
        string htmlpage = sr.ReadToEnd();
    }
}

it connects successfully. It means that Windows default validator decided to trust this certificate.

But once I override the ServerCertificateValidationCallback, my callback is called with SslPolicyErrors.RemoteCertificateChainErrors and the chain contains one element with status X509ChainStatusFlags.PartialChain (in fact I would expect to receive no errors here, because current cert is supposed to be trusted)

This site is not included in my trusted list, and do not want to return 'true' from my callback. But I don't want to return 'false' neither, or I'll get an Exception: "The remote certificate is invalid according to the validation procedure", which is obviously not expected for https://www.dscoduc.com/, because it's added to Trusted People store, and is approved by Windows when certificate callback is not overridden. So I want Windows to take the default validation procedure for this site. I don't want to look into Windows Trusted stores myself and go through all the chain elements, because it's already (and hopefully correctly) implemented in Windows.

In other words, I need to explicitly trust to sites approved by the user (which are stored somewhere in his settings), and call the default certification check for all others.

The default value for ServicePointManager.ServerCertificateValidationCallback is null, so there's no 'default' callback for me to call later. How should I call this 'default' certificate handler?

3

3 Answers

5
votes

Something like this might work. Note the X509CertificateValidator allows you to choose whether to include the Trusted People store in the validation.

private static bool CertificateValidationCallBack(
    object sender,
    X509Certificate certificate,
    X509Chain chain,
    SslPolicyErrors sslPolicyErrors)
{
    // Your custom check here...
    if (isYourSpecialCase)
    {
        return true;
    }

    // If it is not your special case then revert to default checks...

    // Convert the certificate to a X509Certificate2
    var certificate2 = certificate as X509Certificate2 ?? new X509Certificate2(certificate);

    try
    {
        // Choose the type of certificate validation you want
        X509CertificateValidator.PeerOrChainTrust.Validate(certificate2);
        //X509CertificateValidator.ChainTrust.Validate(certificate2);
    }
    catch
    {
        return false;
    }

    // Sender is always either a WebReqest or a hostname string
    var request = sender as WebRequest;
    string requestHostname = request != null ? request.RequestUri.Host : (string)sender;

    // Get the hostname from the certificate
    string certHostname = certificate2.GetNameInfo(X509NameType.DnsName, false);

    return requestHostname.Equals(certHostname, StringComparison.InvariantCultureIgnoreCase);
}
6
votes

It's less difficult than you think to walk the chain from within your callback.

Have a look at http://msdn.microsoft.com/en-us/library/dd633677(v=exchg.80).aspx

The code in that sample examines the certificate chain to work out if the certificate is self-signed and if so, trust it. You could adapt that to accept a PartialChain instead or as well. You'd be looking to do something like this:

if (status.Status == X509ChainStatusFlags.PartialChain ||
    (certificate.Subject == certificate.Issuer &&
     status.Status == X509ChainStatusFlags.UntrustedRoot)
{
    // Certificates with a broken chain and
    // self-signed certificates with an untrusted root are valid. 
    continue;
}
else if (status.Status != X509ChainStatusFlags.NoError)
{
    // If there are any other errors in the certificate chain,
    // the certificate is invalid, so the method returns false.
    return false;
}

Alternatively, inspect the Subject property:

private static bool CertificateValidationCallBack(
    object sender,
    System.Security.Cryptography.X509Certificates.X509Certificate certificate,
    System.Security.Cryptography.X509Certificates.X509Chain chain,
    System.Net.Security.SslPolicyErrors sslPolicyErrors)
{
    return certificate.Subject.Contains(".dsoduc.com");
}
0
votes

The @pete.c's solution seems to work correctly (checked different cases)

However, if still unsure that X509CertificateValidator validates the same way, the default callback can be run through reflection:

private static object s_defaultCallback;
private static MethodInfo s_defaultCallbackInvoker;

...
// Get the original callback using reflection 
PropertyInfo[] pis = typeof (ServicePointManager).GetProperties(BindingFlags.Static | BindingFlags.NonPublic);

foreach (var pi in pis)
{
    if (pi.Name == "CertPolicyValidationCallback")
    {
        s_defaultCallback = pi.GetValue(null, null);
        s_defaultCallbackInvoker = s_defaultCallback.GetType().GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
        break;
    }
}
...

private static bool CertificateValidationCallBack(
        object sender,
        X509Certificate certificate,
        X509Chain chain,
        SslPolicyErrors sslPolicyErrors)
{
    // Your custom check here...
    if (isYourSpecialCase)
    {
        return true;
    }

    // Default Windows behavior
    WebRequest req = sender as WebRequest;
    if (req == null)
        return false;

    ServicePoint sp = ServicePointManager.FindServicePoint(req.RequestUri);
    string host = req.RequestUri.Host;
    object [] parameters = new object[]
                               {
                                   host,
                                   sp,
                                   certificate,
                                   req,
                                   chain,
                                   sslPolicyErrors
                               };

    return (bool)s_defaultCallbackInvoker.Invoke(s_defaultCallback, parameters);
}