C# – WebRequest Error – Could not create SSL/TLS secure channel

chttpsssl

I am trying to write C# code which makes a web request against a REST service endpoint used for calculating sales tax within a web application. This is a third party service, and it is secured using SSL. There are two environments, UAT and production. The code that runs the webrequest looks like this:

...

var req = WebRequest.Create(url) as HttpWebRequest;
req.Method = "POST";
req.ContentType = "application/json";

...

using (var webresponse = req.GetResponse())
{
    using (var responseStream = new StreamReader(webresponse.GetResponseStream()))
    {
        var respJson = responseStream.ReadToEnd();
        calcResult = BuildResponse(calcRequest, respJson, consoleWriteRawReqResponse);
    }
}

return calcResult;

This works fine against the UAT environment. But when I run the same code against the production environment, I get the error:

"Could not create SSL/TLS secure channel"

I am able to execute both requests from Postman without issue, without any special modifications.

This led me down the path of investigating this error, and I found many helpful SO posts discussing the topic, including:

The request was aborted: Could not create SSL/TLS secure channel

Could not create SSL/TLS secure channel, despite setting ServerCertificateValidationCallback

These helped by pointing me in the right direction, which was to look at setting the ServicePointManager.SecurityProtocol setting to a different value, and using the ServicePointManager.ServerCertificateValidationCallback to investigate errors.

What I found after playing with these is the following:

  • The UAT environment call will work with the default setting of Ssl3 | Tls (default for .NET 4.5.2), while the production environment will not.
  • The production call will work ONLY when I set this setting to explicitly to Ssl3.

That code looks like this:

...

var req = WebRequest.Create(url) as HttpWebRequest;
req.Method = "POST";
req.ContentType = "application/json";

...

ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3;
ServicePointManager.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(CertValidationCallback);
using (var webresponse = req.GetResponse())
{
    using (var responseStream = new StreamReader(webresponse.GetResponseStream()))
    {
        var respJson = responseStream.ReadToEnd();
        calcResult = BuildResponse(calcRequest, respJson, consoleWriteRawReqResponse);
    }
}
ServicePointManager.SecurityProtocol = SecurityProtocolType.Ssl3 | SecurityProtocolType.Tls;

return calcResult;

This is particularly confusing because in looking at the endpoints in a web browser, I can see that they are both secured by the same wildcard certificate and are both using TLS 1.0.

So I would expect that setting ServicePointManager.SecurityProtocol to TLS would work, but it does not.

I really want to avoid setting ServicePointManager.SecurityProtocol explicitly to SSL3 because our application is a web application and has multiple other integration points that communicate over SSL. These are all working fine and I do not want to adversely affect their functionality. Even if I set this setting right before the call, and then change it back right after, I run the risk of hitting concurrency issues since ServicePointManager.SecurityProtocol is static.

I investigated that topic as well, and did not like what I read. There are mentions of using different app domains:

.NET https requests with different security protocols across threads

How to use SSL3 instead of TLS in a particular HttpWebRequest?

But that seems overly complex / hacky to me. Is dealing with creating an app domain really the only solution? Or is this something I simply should not be trying to solve and instead take it up with the owner of the service in question? It is very curious to me that it would work with TLS on one environment / server, but not the other.

EDIT
I did some more playing with this. I changed my client to use the approach outlined quite well in this blog post (using a different app domain to isolate the code that changes the ServicePointManager.SecurityProtocol):

https://bitlush.com/blog/executing-code-in-a-separate-application-domain-using-c-sharp

This actually worked quite well, and could be fall back solution. But I also learned that the provider of the service in question has a different endpoint (same URL, different port) that is secured using TLS 1.2. Thankfully, by expanding my SecurityProtocol setting like so in the global.asax.cs application start event:

ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;

I am able to communicate with the service fine in all environments. It also does not affect my existing integrations with other services (CyberSource, for example).

However – now there is a new but related question. Why is it that this call ONLY works if I expand SecurityProtocolType as above? My other integrations, like CyberSource, did not require this. Yet this one does. And they all appear to be secured using TLS 1.2 from what I saw in the browser.

Best Answer

If you run 4.0 you can use it like this:

ServicePointManager.SecurityProtocol = (SecurityProtocolType)3072; // SecurityProtocolType.Tls12