Set the SecurityProtocol (Ssl3 or TLS) on the .net HttpWebRequest per request

Viewed 55125

My application (.net 3.5 sp1) uses the HttpWebRequest to communicate with different endpoints, sometimes its over HTTPS where each hosting server may have a different security protocol requirement say TLS or SSL3 or either.

Generally the servers play nice and happily negotiate/fallback on what SecurityProtocol to use TLS or SSL3, but some don't and when .net is set up as TLS or SSL3 (the default I think) those servers that only support SSL3 cause .net to throw a send error.

From what I can tell .net provides the ServicePointManager object with a property SecurityProtocol which can be set to TLS, SSL3 or both. Hence ideally when set to both the idea is the client and server should negotiate as to what to use, but as previously stated that don't seem to work.

Supposedly you could set the ServicePointManager.SecurityProtocol = Ssl3 but what about the endpoints that want to use TLS?

The problem I see with the ServicePointManager and the SecurityProtocol is that its static and therefore application domain wide.

So to the question..

how would I go about using the HttpWebRequest with a different SecurityProtocol e.g.

1) url 1 set to use TLS | Ssl3 (negotiate)

2) url 2 set to Ssl3 (Ssl3 only)

11 Answers

I know this question is old, but the issue remains even with .Net 4.7.2. In my case, I have a multi-threaded application that is talking to two endpoints. One endpoint only works with TLS 1.2, and the other endpoint only works with TLS 1.0 (the team responsible for that one is working on fixing their endpoint so it will also support TLS 1.2).

To work around this, I moved the service calls for the the endpoint that only works with TLS 1.0 to a separate class in the same assembly, and then loaded the assembly into a separate AppDomain. By doing this, I can use this global variable:

System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls; 

just for the calls to the broken endpoint, while also having calls to the TLS 1.2 endpoint (which don't require setting ServicePointManager.SecurityProtocol to anything specific) continue to work. This also ensures that when the good endpoint is upgraded to TLS 1.3 I don't need to re-release my application. My application is multi-threaded and high capacity, so locking or try/finally are not adequate solutions.

Here is the code I used to load the assembly into a separate domain. Note that I load the assembly from it's current location (Aspnet Tempfiles) so that it doesn't lock the assembly in the bin directory and block future deployments.

Also, note that the proxy class inherits MarshalByRefObject so that it is used as a transparent proxy which preserves System.Net.ServicePointManager with it's own value in it's own AppDomain.

This seems like a silly limitation on the part of the .Net framework, I wish we could just specify the Protocol directly on the web request instead of jumping through hoops, especially after years of this. :(

This code does work, hope it helps you out! :)

private static AppDomain _proxyDomain = null;
private static Object _syncObject = new Object();

public void MakeACallToTls10Endpoint(string tls10Endpoint, string jsonRequest)
{
   if (_proxyDomain == null)
   {
      lock(_syncObject) // Only allow one thread to spin up the app domain.
      {
         if (_proxyDomain == null)
         {
            _proxyDomain = AppDomain.CreateDomain("CommunicationProxyDomain");
         }
      }
   }

   Type communicationProxyType = typeof(CommunicationProxy);
   string assemblyPath = communicationProxyType.Assembly.Location;

   // Always loading from the current assembly, sometimes this moves around in ASPNet Tempfiles causing type not found errors if you make it static.
   ObjectHandle objectHandle = _proxyDomain.CreateInstanceFrom(assemblyPath, communicationProxyType.FullName.Split(',')[0]);
   CommunicationProxy communicationProxy = (CommunicationProxy)objectHandle.Unwrap();

   return communicationProxy.ExecuteHttpPost(tls10Endpoint, jsonRequest);
}

Then, in a separate class:

[Serializable]
public class CommunicationProxy : MarshalByRefObject
{
   public string ExecuteHttpPost(string tls10Endpoint, string jsonRequest)
   {
      ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls;

      // << Bunch of code to do the request >>
   }
}

With Net 4.6 there is HttpClient and WinHttpHandler nuget package available for windows (from microsoft) to set SslProtocols parameters. With Net core you can use HttpClientHandler class for the same.

using (var hc = new HttpClient(new WinHttpHandler() // should have it as a static member
{
    AutomaticDecompression = DecompressionMethods.Deflate | DecompressionMethods.GZip,
    SslProtocols = SslProtocols.Tls | 
                   SslProtocols.Tls11 | 
                   SslProtocols.Tls12
}))
{
    var r = hc.SendAsync(new HttpRequestMessage(HttpMethod.Get, "https://..."));
    r.Wait();
    Console.WriteLine(r.Result.StatusCode);
} // using

Set all of this. In my application it is work for different security protocols.

System.Net.ServicePointManager.SecurityProtocol = 
System.Net.SecurityProtocolType.Ssl3 
| System.Net.SecurityProtocolType.Tls12 
| SecurityProtocolType.Tls11 
| SecurityProtocolType.Tls;

I have encountered same problem and came up with a solution using reflection. In source code for the HttpWebRequest you can find internal property SslProtocols which is set in constructor during creation with the current value from ServicePointManager.SecurityProtocol. So, after creation of the HttpWebRequest and before executing it, set this property to appropriate security protocol:

var request = (HttpWebRequest)WebRequest.Create(endpoint);
typeof(HttpWebRequest)
.GetProperty("SslProtocols", BindingFlags.NonPublic | BindingFlags.Instance)
.SetValue(request, System.Security.Authentication.SslProtocols.Tls12);

If you use WCF System.ServiceModel is not possible to do in .net4.8 but .netCore version have option for this.

You can't do fully replace as is missing many components but for only doing simple calls to some api it could work.

You can load both versions to same project but you need use extern alias solve name conflicts.


Instaling

First you need install NuGet System.Private.ServiceModel to project.

Edit properties of System.Private.ServiceModel in project References and set Aliases to e.g. ServiceModelHack.

Updating web service

In Reference.cs of given web service in project Service References

For this example web servic is named TestService, then file should be in

Service References\TestService\Reference.cs

Add at top of file a line

extern alias ServiceModelHack;

And run replace all text in file:

System.ServiceModel. -> ServiceModelHack::System.ServiceModel.

Calling web service

As System.Private.ServiceModel drop some functionality you can't use App.config directly, you need manually set bindings like:

var binding = new ServiceModelHack::System.ServiceModel.BasicHttpsBinding
{
    Security = new ServiceModelHack::System.ServiceModel.BasicHttpsSecurity
    {
        Transport = new ServiceModelHack::System.ServiceModel.HttpTransportSecurity
        {

        }
    }
};
var endpoint = new ServiceModelHack::System.ServiceModel.EndpointAddress("https://example.com/ws");
using (var service = new TestService(binding, endpoint))
{
    Helper.setTls(service, SslProtocols.Tls13); //describe in next part
    service.Ping();
}
Changing version of TLS

based on: https://github.com/dotnet/wcf/issues/3442#issuecomment-475356182


public static void setTls<T>(ServiceModelHack::System.ServiceModel.ClientBase<T> client, SslProtocols ssl) where T : class
{
    client.Endpoint.EndpointBehaviors.Add(
        new SslProtocolEndpointBehavior
        {
            SslProtocols = ssl,
        }
    );
}
public class SslProtocolEndpointBehavior : ServiceModelHack::System.ServiceModel.Description.IEndpointBehavior
{
    public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls12;
    public void AddBindingParameters(ServiceModelHack::System.ServiceModel.Description.ServiceEndpoint endpoint, ServiceModelHack::System.ServiceModel.Channels.BindingParameterCollection bindingParameters)
    {
        bindingParameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(x =>
        {
            x.SslProtocols = this.SslProtocols;
            return x; // You can just return the modified HttpClientHandler
        }));
    }

    public void ApplyClientBehavior(ServiceModelHack::System.ServiceModel.Description.ServiceEndpoint endpoint, ServiceModelHack::System.ServiceModel.Dispatcher.ClientRuntime clientRuntime) { }
    public void ApplyDispatchBehavior(ServiceModelHack::System.ServiceModel.Description.ServiceEndpoint endpoint, ServiceModelHack::System.ServiceModel.Dispatcher.EndpointDispatcher endpointDispatcher) { }
    public void Validate(ServiceModelHack::System.ServiceModel.Description.ServiceEndpoint endpoint) { }
}
Related