0 votes
by (140 points)

In our application, we do certificate pinning usign custom certificate validation, and would need to replace the ICertificatePolicy.CheckValidationResult with the ValidatingCertificate event handler. Part of our logic needs to have access to the request object (url and headers) to determine if the certificate is valid or not. (At a high level, it needs to make a call to determine if the current client is configured to allow additional certificates, so that call needs a more relaxed certification validation logic).

Is there any way to access the WebRequest instance through the SslCertificateValidationEventArgs, or any other way ?

Thanks.

Subha Tarafdar.

Applies to: Rebex HTTPS

1 Answer

0 votes
by (70.2k points)

Actually, this sounds a little weird for us. The TLS/SSL layer is absolutely independent from HTTP protocol. You can use it for securing other protocols such as FTP. So interlink certificate validation with a WebRequest is little weird.

Also, the TLS/SSL session is created on server name (domain) basis, not URL basis. This means that subsequent requests for same server name (domain) doesn't require creating new TLS/SSL session (and certificate validation). Please see following sample code:

var creator = new HttpRequestCreator();
creator.ValidatingCertificate += (s, e) =>
{
    Console.WriteLine("Validating certificate for server: '{0}', IP: '{1}'.", e.ServerName, e.Socket.RemoteEndPoint);
    e.Accept();
};

string[] urls = 
{ 
    "https://rebex.net/sftp.net", // rebex.net
    "https://rebex.net/zip.net", // rebex.net
    "https://www.rebex.net/sftp.net", // www.rebex.net
    "https://www.rebex.net/zip.net", // www.rebex.net
    "https://rebex.net/sftp.net" // rebex.net
};

foreach (var url in urls)
{
    Console.WriteLine("Requesting: {0}", url);
    var request = creator.Create(url);
    var response = request.GetResponse();
    response.Close();
}

And this is the output:

Requesting: https://rebex.net/sftp.net
Validating certificate for server: 'rebex.net', IP: '195.144.107.196:443'.
Requesting: https://rebex.net/zip.net
Requesting: https://www.rebex.net/sftp.net
Validating certificate for server: 'www.rebex.net', IP: '195.144.107.196:443'.
Requesting: https://www.rebex.net/zip.net
Requesting: https://rebex.net/sftp.net

As you can see, the certificate validation was required only two times. Once for 'rebex.net' server name and once for 'www.rebex.net' server name. All subsequent requests were associated with already established TLS/SSL sessions.

If you want to disable this behavior, you can set creator.Settings.SslSessionCacheEnabled = false. This will cause, the TLS/SSL session will be created and validated for each request again.

Now to your problem. If you really need WebRequest instance at time you are validating a certificate, I suggest you to associate created requests with HttpRequestCreator instance like this:

public class MyCreator : HttpRequestCreator
{
    // holds request based on server name
    public Dictionary<string, WebRequest> Requests = new Dictionary<string, WebRequest>();

    public new HttpRequest Create(string uriString)
    {
        var request = base.Create(uriString);
        if (request.RequestUri.Scheme.Equals("https", StringComparison.OrdinalIgnoreCase))
        {
            lock (Requests)
            {
                // save request for current server name
                Requests[request.RequestUri.Host] = request;
            }
        }
        return request;
    }
}

Then you can get instance of the last created WebRequest in ValidatingCertificate like this:

var creator = new MyCreator();
creator.ValidatingCertificate += (s, e) =>
{
    WebRequest request = creator.Requests[e.ServerName];
    Console.WriteLine("Validating certificate for server: '{0}', IP: '{1}'.", e.ServerName, e.Socket.RemoteEndPoint);
    Console.WriteLine("Validating URI: {0}", request.RequestUri);
    e.Accept();
};

Please note, that this code will work for sequential requests, but can fail for parallel requests.

If parallel requests are necessary, you can either add synchronization or simply create own HttpRequestCreator instance for each WebRequest. In second case, association between creator and request is unique, but TLS/SSL sessions cannot be shared.

by (140 points)
Thanks for the reply. The corresponding .Net method ICertificatePolicy.CheckValidationResult does provide a way to access the WebRequest class. I totally agree that the certificate validation should not be tied to a Http request class. But coupling it to a  WebRequest is probably valid, since it is the base class for HttpWebReqest, FtpWebRequest, etc and is not directly tied to a specific protocol.

I need this specific check just for one request in the application. I can track this by maintaining a state in the application, or using the HttpRequestCreator override that you suggested. Shouldn't be a big problem.

Great point about the SSL session caching. This request that needs the relaxed certificate validation is the first request that the app makes to the server. We have keep-alive disabled for this specific request, which seems to force the certificate to be revalidated again on the next request. Using the SslSessionCacheEnabled setting that you mentioned seems to be a better approach, I just need to set it to false during this request and then set it back to true after that.
by (70.2k points)
Please note that `HttpRequestCreator` class has two caches which are enabled by default:

1. cache for SSL/TLS sessions - this can be DISABLED by setting HttpRequestCreator.Settings.SslSessionCacheEnabled = false

2. cache for keep-alive requests - this can be DISABLED by setting  HttpRequestCreator.Settings.HttpSessionCacheEnabled = false

Please note that SslSessionCache and HttpSessionCache are two independent caches.

Also please note that keep-alive requests don't affects SSL/TLS layer. So using or not using keep-alive doesn't affect certificate validation, which is raised when new SSL/TLS session is created.
...