Wednesday, June 09, 2010

WCF, SSL Certificates and Certificate Validation Errors

When you use secure transport (such as SSL over Http), WCF by default requires that you have a valid certificate for the server hosting your service.

For the certificate to be valid the CN value needs to match the server name and the chain has to be valid (i.e., the root or one of the children of the root need to be in the trusted root authority on the machine from where you are running the WCF client).

But often times (especially in dev), you might be using a certificate that you created and hence does not have a valid root authority. In these cases, WCF will throw the following exception of type “System.ServiceModel.Security.SecurityNegotiationException”:

Could not establish trust relationship for the SSL/TLS secure channel with authority '<name>'.

In these cases, WCF provides you with an easy mechanism to by pass the validation of the certificate by assigning your own delegate to “ServicePointManager.ServerCertificateValidationCallback ”

ServicePointManager.ServerCertificateValidationCallback 
                    += new RemoteCertificateValidationCallback(RemoteCertificateCallBack);

Typically, most examples on the Internet tell you to setup RemoteCertificateCallBack to return true. This will be ok for small simple apps, where you are working against only one web-service. But in scenarios where you might have multiple web-services running on different servers with different certificates, this can be a security issue. The reason for this is that the ServicePointManager.ServerCertificateValidationCallback delegate is invoked for all web-services running over HTTPs running within the same app-domain as your application. In such circumstances, what you need to do is to be as specific as possible when returning true for a certificate on which the ServerCertificateValidationCallback is being called. Here is some sample code that does just that using the CN name.

//call when app starts up
static Setup()
{
    if (!string.IsNullOrEmpty("comma separated list of CN names that should pass - use App.config to store this"))
    {
        ServicePointManager.ServerCertificateValidationCallback 
            += new RemoteCertificateValidationCallback(RemoteCertificateCallBack);
    }
}

//callback method
static bool RemoteCertificateCallBack(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
    bool certIsValid = sslPolicyErrors == SslPolicyErrors.None;
    
    //only check if the cert is not valid.
    if (!certIsValid)
    {
        //make sure the issuer has been set
        if (certificate != null && !string.IsNullOrEmpty(certificate.Issuer))
        {
            string listOfCertNames = "comma separated list of CN names that should pass - use App.config to store this";
            if (!string.IsNullOrEmpty(listOfCertNames))
            {
                string[] certNames = listOfCertNames.Split(',');
                for (int index = 0; index < certNames.Length; index++)
                {
                    certIsValid = ValidateIfKnownCertificateName(certificate, certNames[index]);
                    if (certIsValid)
                        break;//cert has been determined to be valid - break!
                }
            }
        }
    }
    return certIsValid;
}

/// <summary>
/// returns true if the certificate's issuer has a CN defined in the IgnoreCertErrorsList appsetting.
/// </summary>
/// <param name="certificate"></param>
/// <param name="certName"></param>
/// <param name="index"></param>
/// <returns></returns>
private static bool ValidateIfKnownCertificateName(X509Certificate certificate, string certName)
{
    bool certIsValid = false;
    try
    {
        int cnIndex = certificate.Issuer.IndexOf("CN=");
        if (cnIndex >= 0)
        {
            cnIndex = cnIndex + 3;//"CN="
            int cnEndIndex = certificate.Issuer.IndexOf(",", cnIndex);
            if (cnEndIndex > cnIndex)
            {
                //get the CN value
                string CNinIssuer = certificate.Issuer.Substring(cnIndex, cnEndIndex - cnIndex);
                if (string.Compare(CNinIssuer, certName, StringComparison.OrdinalIgnoreCase) == 0)
                {
                    //value was the same as one of the ones we know of.
                    certIsValid = true;
                }
            }
        }
    }
    catch
    {
        certIsValid = false;
    }
    return certIsValid;
}

No comments: