Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Unable to prefer TLS 1.1 for BasicHttpsBinding #3442

Closed
Zek99 opened this issue Mar 13, 2019 · 9 comments
Closed

Unable to prefer TLS 1.1 for BasicHttpsBinding #3442

Zek99 opened this issue Mar 13, 2019 · 9 comments
Assignees

Comments

@Zek99
Copy link

Zek99 commented Mar 13, 2019

Maybe the issue should be titled: Overridden BindingElement.BuildChannelFactory not invoked.

I have a requirement to consume some SOAP webservices. We have a client certificate signed using MD5 and therefore it is unusable for TLS 1.2.

I understand that ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11; is not usable anymore but I am unable to find any alternative. I have read through polymorphic inheritance treechain that is BasicHttpsBinding to discover that its a Binding that uses HttpsTransportBindingElement and HttpTransportBindingElement. I found the capability to alter the HttpClientFactory using:

HttpMessageHandler handler = clientHandler;
if(_httpMessageHandlerFactory!= null)
{
handler = _httpMessageHandlerFactory(clientHandler);
}

but I cannot find a way to populate the BindingContext that is passed to HttpChannelFactory with the required Func<> that is pulled out here:

_httpMessageHandlerFactory = context.BindingParameters.Find<Func<HttpClientHandler, HttpMessageHandler>>();

In general it appears that BuildChannelFactory is not used and instead a new HttpChannelFactory is instantiated. If so, what is the point of BindingElement.BuildChannelFactory and how do I go about adding my own properties to the BindingContext and BindingElement used by HttpChannelFactory:

internal HttpChannelFactory(HttpTransportBindingElement bindingElement, BindingContext context)
: base(bindingElement, context, HttpTransportDefaults.GetDefaultMessageEncoderFactory())

Below is my code from latest attempt. I have tried variations of the below.

public class OlderTlsHttpsTransportBindingElement : HttpsTransportBindingElement
{
    public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
    {
        return base.CanBuildChannelFactory<TChannel>(context);
    }

    public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
    {
        context.BindingParameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(x =>
        {
            x.SslProtocols = SslProtocols.Tls11;

            return new CustomMessageHandler(x);
        }));

        return base.BuildChannelFactory<TChannel>(context);
    }
}

public class CustomMessageHandler : DelegatingHandler
{

    public CustomMessageHandler(HttpClientHandler handler)
    {
        InnerHandler = handler;
    }

    protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return base.SendAsync(request, cancellationToken);
    }
}

var binding = new CustomBinding(
    new OlderTlsHttpsTransportBindingElement()
);

var endpointAddress = new EndpointAddress(new Uri("https://SuperAnnoyingWebservice.com.au"));

var client = new GeneratedSvcUtilClient(binding, endpointAddress);

var channel = client.ChannelFactory.CreateChannel();

var result = await channel.InvokeFunctionAsync();
@Zek99
Copy link
Author

Zek99 commented Mar 15, 2019

Okay, I have discovered a fix and another issue. I was testing with a certificate that supports the latest signature hash algorithm at it appears that client.ClientCredentials.ClientCertificate.Certificate = x5092; doesn't work. I thought it wasn't sending the certificate because TLS1.2 doesn't support certificates signed with an MD5 signature. Using Wireshark, it appears that WCF ignores my ClientCredential settings.

var x5092 = new X509Certificate2("settings for SHA1 signed certificate")

var client = new GeneratedSvcUtilClient();

client.ClientCredentials.ClientCertificate.Certificate = x5092;

var result = await client.InvokeMethodAsync();

Results in the certificate not being sent.

My solution was using Endpoint behaviours. Full code:

var client = new GeneratedSvcUtilClient();
client.Endpoint.EndpointBehaviors.Add(new ForceCertificateEndpointBehavior());
var channel = client.ChannelFactory.CreateChannel

public class ForceCertificateEndpointBehavior : IEndpointBehavior
{
    public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
    {
        var x5092 = new X509Certificate2("Settings for SHA1 signed certificate");

        bindingParameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(x =>
        {
            x.SslProtocols = SslProtocols.Tls12;
            x.ClientCertificates.Add(x5092);

            return new CustomMessageHandler(x);
        }));
    }
    public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
    public void Validate(ServiceEndpoint endpoint) { }
}

I believe that I shouldn't have to use EndpointBehaviours and I should be able to configure WCF via the properties provided by the generated ClientBase objects. If I force Tls11 I am able to use the older certificate. To me, this looks like that somewhere within WCF code it disregards settings provided to it.

I'll see if I can manage to build WCF from source and debug the issue.

@mconnew
Copy link
Member

mconnew commented Mar 21, 2019

There's a lot of questions through this issue so I'll start from the top and at the end will sum up the solution to your problem.
BindingElement.BuildChannelFactory is definitely used. Here's a partial call stack for when HttpChannelFactory<TChannel> is constructed when calling ChannelFactory<T>.Open():

  System.ServiceModel.Channels.HttpChannelFactory<System.ServiceModel.Channels.IRequestChannel>.HttpChannelFactory(System.ServiceModel.Channels.HttpTransportBindingElement bindingElement, System.ServiceModel.Channels.BindingContext context) Line 60 C#
  System.ServiceModel.Channels.HttpsChannelFactory<System.ServiceModel.Channels.IRequestChannel>.HttpsChannelFactory(System.ServiceModel.Channels.HttpsTransportBindingElement httpsBindingElement, System.ServiceModel.Channels.BindingContext context) Line 31 C#
* System.ServiceModel.Channels.HttpsTransportBindingElement.BuildChannelFactory<System.ServiceModel.Channels.IRequestChannel>(System.ServiceModel.Channels.BindingContext context) Line 104 C#
  System.ServiceModel.Channels.BindingContext.BuildInnerChannelFactory<System.ServiceModel.Channels.IRequestChannel>() Line 83 C#
  System.ServiceModel.Channels.MessageEncodingBindingElement.InternalBuildChannelFactory<System.ServiceModel.Channels.IRequestChannel>(System.ServiceModel.Channels.BindingContext context) Line 35 C#
  System.ServiceModel.Channels.TextMessageEncodingBindingElement.BuildChannelFactory<System.ServiceModel.Channels.IRequestChannel>(System.ServiceModel.Channels.BindingContext context) Line 141 C#
  System.ServiceModel.Channels.BindingContext.BuildInnerChannelFactory<System.ServiceModel.Channels.IRequestChannel>() Line 83 C#
  System.ServiceModel.Channels.Binding.BuildChannelFactory<System.ServiceModel.Channels.IRequestChannel>(System.ServiceModel.Channels.BindingParameterCollection parameters) Line 174 C#
  System.ServiceModel.Channels.ServiceChannelFactory.BuildChannelFactory(System.ServiceModel.Description.ServiceEndpoint serviceEndpoint, bool useActiveAutoClose) Line 140 C#
  System.ServiceModel.ChannelFactory.CreateFactory() Line 167 C#
  System.ServiceModel.ChannelFactory.OnOpening() Line 361 C#

The class HttpsTransportBindingElement is derived from BindingElement and you can see the call to the virtual method BindingElement.BuildChannelFactory on the third line.

Your next issue is why your client certificate isn't getting used. You need to tell WCF that you want to use the certificate. The way you do this is by specifying the client credential type on the binding or tell the binding element that a client certificate is required:

  var binding = new BasicHttpsBinding();
  binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; 

or

  var binding = new BasicHttpsBinding();
  binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;

Doing this tells WCF to get the client certificate from the ClientCredentials class and set in on the HttpClientHandler.

Exposing HttpClientHandler or the concept of SslProtocols on the client class generated doesn't make a lot of sense. Your binding might not use HTTP or SSL/TLS at all, for example, it could be NetTcpBinding using windows authentication. The purpose of BindingParameterCollection is to enable you to pass extra information to the BindingElement which can't be added in a more abstract way via API's. In theory we could add a property to HttpsTransportBindingElement to control the SslProtocol used, but that wouldn't work on the .NET Framework as the scope of setting which protocol to use is different. The .NET Framework uses HttpWebRequest so we can't expose any HttpWebRequest of HttpClientHandler semantics on the public api surface as it wouldn't make sense on all platforms.

The generated class is generated as a partial class so you can extend it by creating your own GeneratedSvcUtilClient.cs file and extend the behavior. E.g. something like this:

public class SslProtocolCertificateEndpointBehavior : IEndpointBehavior
{
    public SslProtocols SslProtocols { get; set; } = SslProtocols.Tls12;
    public void AddBindingParameters(ServiceEndpoint endpoint, 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(ServiceEndpoint endpoint, ClientRuntime clientRuntime) { }
    public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher) { }
    public void Validate(ServiceEndpoint endpoint) { }
}

public partial class GeneratedSvcUtilClient
{
    public System.Net.SslProtocols SslProtocols
    {
        get
        {
            var behavior = Endpoint.EndpointBehaviors.Find<SslProtocolCertificateEndpointBehavior>();
            if (behavior != null} return behavior.SslProtocols;
            return SslProtocols.Tls12;
        }
        set
        {
            var behavior = client.Endpoint.EndpointBehaviors.Find<SslProtocolCertificateEndpointBehavior>();
            if (behavior == null)
            {
                behavior = new SslProtocolCertificateEndpointBehavior();
                Endpoint.EndpointBehaviors.Add(behavior);
            }
            behavior.SslProtocols = value;
        }
    }
}

@Zek99
Copy link
Author

Zek99 commented Mar 21, 2019

I appreciate the detailed response. I'll try to break my issues up more next time. Embarrassing that I forgot about binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;.

And I agree, having the generated client class aware of the transport layer doesn't make sense; the truck doesn't care what is being transported and the cargo doesn't care how it gets to where its going (that is how I rationalise it). That is why I would have liked to have the SSLProtocols be defined via the BasicHttpsBinding which I cannot do as it is not an accessible property.

So I wish to provide my own "truck" to the client constructor parameters allow, I derive from BasicHttpsBinding and alter the properties available to me.

In this example, OldSSLHttpsBinding.BuildChannelFactory<TChannel> is not invoked in this example.

public class OldSSLHttpsBinding : BasicHttpsBinding
{
    public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingParameterCollection parameters)
    {
        parameters.Add(new Func<HttpClientHandler, HttpMessageHandler>(x =>
        {
            x.SslProtocols = SslProtocols.Tls11;

            return x;
        }));

        return base.BuildChannelFactory<TChannel>(parameters);
    }

    public OldSSLHttpsBinding(BasicHttpsSecurityMode mode) : base(mode)
    {

    }
}

var x5092 = new X509Certificate2(keyDir, password, X509KeyStorageFlags.DefaultKeySet);
var binding = new OldSSLHttpsBinding(BasicHttpsSecurityMode.Transport);
binding.MaxReceivedMessageSize = 100000000;
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
var endpointAddress = new EndpointAddress(new Uri(endpointUri));
var client = new GeneratedServiceUtil(binding, endpointAddress);
client.ChannelFactory.Credentials.ClientCertificate.Certificate = x5092;
var channel = client.ChannelFactory.CreateChannel();

var data = await channel.InvokeMethod(requestData);

Its like the client doesn't generate the channel factory from the binding it is given which is what I would expect it would do. Otherwise why have BuildChannelFactory?

I am trying to debug the issue using the wcf source code, I can compile it but the structure of the project is beyond my current skill set to deal with.

@mconnew
Copy link
Member

mconnew commented Mar 22, 2019

I think I see your problem. That method isn't generally called directly on the Binding with most code paths. What usually happens is Binding.CreateBindingElements() is called and passed to CustomBinding which is then used to call BuildChannelFactory. A Binding isn't designed to modify the BuildChannelFactory behavior, it's there to modify the BindingElements returned when calling CreateBindingElement. The call you need to derive from and modify would be HttpsTransportBindingElement and make the changes in the BuildChannelFactory method on that class.

@Zek99
Copy link
Author

Zek99 commented Mar 22, 2019

I did that with OlderTlsHttpsTransportBindingElement in my initial post. It is not invoked.

@Zek99
Copy link
Author

Zek99 commented Mar 22, 2019

Figured how to debug with source from WCF. I will find why BuildChannelFactory isn't being invoked soon I hope.

@Zek99
Copy link
Author

Zek99 commented Mar 22, 2019

Thanks for reviewing my issue, I'll rely on the EndpointBehavious instead of the Bindings, I am just "yak shaving" at this point.

As far as I can tell the ClientBase instantiates a new ClientChannel rather than building one from the bindings which is confusing, why I am able to override the BuildChannelFactory method if it isn't used? I imagine it is used by internal developers to build the underlying library.

_channelFactory = new ChannelFactory<TChannel>(binding, remoteAddress);

@mconnew
Copy link
Member

mconnew commented Mar 22, 2019

Every usage of the virtual method Binding.BuildChannelFactory that I could find was on the CustomBinding class. So basically a new CustomBinding is created from the passed in Binding by calling Binding.CreateBindingElements and passing it to the CustomBinding constructor. Then BuildChannelFactory is called on CustomBinding. The only usage for the BuildChannelFactory method outside of CustomBinding would be if you want to work with raw Message objects and not use contracts. Then you could call BasicHttpBinding.BuildChannelFactory<IRequestChannel>() which would return you a ChannelFactory<IRequestChannel> and allow you to create an IRequestChannel to send and receive raw Message objects.
I'm closing this issue now as you have your solution.

@mconnew mconnew closed this as completed Mar 22, 2019
@Zek99
Copy link
Author

Zek99 commented Mar 22, 2019

Thanks, I appreciate the time you have spent.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants