简体   繁体   English

(可选)在自托管WCF服务中接受客户端证书

[英]Optionally accept client certificates in a self-hosted WCF service

I'd like to have a single SSL endpoint in my self-hosted WCF service that can accept requests with HTTP basic auth credentials or client certificate credentials. 我想在我的自托管WCF服务中有一个SSL端点,该端点可以接受带有HTTP基本身份验证凭据或客户端证书凭据的请求。

For IIS hosted services, IIS differentiates between "Accepts client certificates" and "Requires client certificates". 对于IIS托管服务,IIS区分“接受客户端证书”和“需要客户端证书”。

WCF's WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; WCF的WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; appears to be the analog of the "requires certificates" setting in IIS. 似乎与IIS中“需要证书”设置类似。

Is there a way to configure a WCF self-hosted service to accept client certificate credentials but not require them from every client? 有没有一种方法可以配置WCF自托管服务以接受客户端证书凭据,但不要求每个客户端都提供它们? Is there a WCF analog of IIS "Accepts client certificates" for self-hosted WCF services? 是否存在用于自托管WCF服务的IIS“接受客户端证书”的WCF类似物?

I found a way to optionally accept SSL client certificates in WCF, but it requires a dirty trick. 我找到了一种方法,可以选择在WCF中接受SSL客户端证书,但这需要一个肮脏的把戏。 If anyone has a better solution (other than "Don't use WCF") I would love to hear it. 如果有人有更好的解决方案(除了“不要使用WCF”),我希望听到它。

After much digging around in decompiled WCF Http channel classes, I've learned a few things: 在反编译的WCF Http通道类中进行了大量研究之后,我学到了一些东西:

  1. WCF Http is monolithic. WCF Http是单片的。 There are a bezillion classes flying around, but all of them are marked "internal" and therefore inaccessible. 有无数的班级飞来飞去,但是所有班级都被标记为“内部”,因此无法访问。 The WCF channel binding stack isn't worth a hill of beans if you're trying to intercept or extend core HTTP behaviors because the things a new binding class would want to fiddle with in the HTTP stack are all inaccessible. 如果您尝试拦截或扩展核心HTTP行为,则WCF通道绑定堆栈不值一提,因为新绑定类想要在HTTP堆栈中摆弄的东西都是无法访问的。
  2. WCF rides on top of HttpListener / HTTPSYS, just like IIS does. WCF就像IIS一样,位于HttpListener / HTTPSYS之上。 HttpListener provides access to the SSL client certificate. HttpListener提供对SSL客户端证书的访问。 WCF HTTP does not provide any access to the underlying HttpListener, though. 但是,WCF HTTP不提供对基础HttpListener的任何访问。

The closest interception point I could find is when HttpChannelListener (internal class) opens a channel and returns an IReplyChannel . 我可以找到的最接近的拦截点是当HttpChannelListener (内部类)打开一个通道并返回IReplyChannel IReplyChannel has methods for receiving a new request, and those methods return a RequestContext . IReplyChannel具有用于接收新请求的方法,并且这些方法返回RequestContext

The actual object instance constructed and returned by the Http internal classes for this RequestContext is ListenerHttpContext (internal class). 由Http内部类为此RequestContext构造并返回的实际对象实例是ListenerHttpContext (内部类)。 ListenerHttpContext holds a reference to a HttpListenerContext , which comes from the public System.Net.HttpListener layer underneath WCF. ListenerHttpContext拥有对HttpListenerContext的引用,该引用来自WCF下的公共System.Net.HttpListener层。

HttpListenerContext.Request.GetClientCertificate() is the method we need to see if there is a client certificate available in the SSL handshake, load it if there is, or skip it if there is not. HttpListenerContext.Request.GetClientCertificate()是我们需要查看SSL握手中是否有客户端证书的方法,如果有,请加载它,否则请跳过它。

Unfortunately, the reference to HttpListenerContext is a private field of ListenerHttpContext , so to make this work I had to resort to one dirty trick. 不幸的是,对HttpListenerContext的引用是ListenerHttpContext的私有字段,因此,为了完成这项工作,我不得不求助于一个肮脏的把戏。 I use reflection to read the value of the private field so that I can get at the HttpListenerContext of the current request. 我使用反射来读取私有字段的值,以便可以获取当前请求的HttpListenerContext

So, here's how I did it: 因此,这是我的操作方式:

First, create a descendant of HttpsTransportBindingElement so that we can override BuildChannelListener<TChannel> to intercept and wrap the channel listener returned by the base class: 首先,创建HttpsTransportBindingElement的后代,以便我们可以重写BuildChannelListener<TChannel>来拦截和包装基类返回的通道侦听器:

using System;
using System.Collections.Generic;
using System.IdentityModel.Claims;
using System.Linq;
using System.Security.Claims;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel;
using System.ServiceModel.Channels;
using System.Text;
using System.Threading.Tasks;

namespace MyNamespace.AcceptSslClientCertificate
{
    public class HttpsTransportBindingElementWrapper: HttpsTransportBindingElement
    {
        public HttpsTransportBindingElementWrapper()
            : base()
        {
        }

        public HttpsTransportBindingElementWrapper(HttpsTransportBindingElementWrapper elementToBeCloned)
            : base(elementToBeCloned)
        {
        }

        // Important! HTTP stack calls Clone() a lot, and without this override the base
        // class will return its own type and we lose our interceptor.
        public override BindingElement Clone()
        {
            return new HttpsTransportBindingElementWrapper(this);
        }

        public override IChannelFactory<TChannel> BuildChannelFactory<TChannel>(BindingContext context)
        {
            var result = base.BuildChannelFactory<TChannel>(context);
            return result;
        }

        // Intercept and wrap the channel listener constructed by the HTTP stack.
        public override IChannelListener<TChannel> BuildChannelListener<TChannel>(BindingContext context)
        {
            var result = new ChannelListenerWrapper<TChannel>( base.BuildChannelListener<TChannel>(context) );
            return result;
        }

        public override bool CanBuildChannelFactory<TChannel>(BindingContext context)
        {
            var result = base.CanBuildChannelFactory<TChannel>(context);
            return result;
        }

        public override bool CanBuildChannelListener<TChannel>(BindingContext context)
        {
            var result = base.CanBuildChannelListener<TChannel>(context);
            return result;
        }

        public override T GetProperty<T>(BindingContext context)
        {
            var result = base.GetProperty<T>(context);
            return result;
        }
    }
}

Next, we need to wrap the ChannelListener intercepted by the above transport binding element: 接下来,我们需要包装上述传输绑定元素截获的ChannelListener:

using System;
using System.Collections.Generic;
using System.Linq;
using System.ServiceModel.Channels;
using System.Text;
using System.Threading.Tasks;

namespace MyNamespace.AcceptSslClientCertificate
{
    public class ChannelListenerWrapper<TChannel> : IChannelListener<TChannel>
        where TChannel : class, IChannel
    {
        private IChannelListener<TChannel> httpsListener;

        public ChannelListenerWrapper(IChannelListener<TChannel> listener)
        {
            httpsListener = listener;

            // When an event is fired on the httpsListener, 
            // fire our corresponding event with the same params.
            httpsListener.Opening += (s, e) =>
            {
                if (Opening != null)
                    Opening(s, e);
            };
            httpsListener.Opened += (s, e) =>
            {
                if (Opened != null)
                    Opened(s, e);
            };
            httpsListener.Closing += (s, e) =>
            {
                if (Closing != null)
                    Closing(s, e);
            };
            httpsListener.Closed += (s, e) =>
            {
                if (Closed != null)
                    Closed(s, e);
            };
            httpsListener.Faulted += (s, e) =>
            {
                if (Faulted != null)
                    Faulted(s, e);
            };
        }

        private TChannel InterceptChannel(TChannel channel)
        {
            if (channel != null && channel is IReplyChannel)
            {
                channel = new ReplyChannelWrapper((IReplyChannel)channel) as TChannel;
            }
            return channel;
        }

        public TChannel AcceptChannel(TimeSpan timeout)
        {
            return InterceptChannel(httpsListener.AcceptChannel(timeout));
        }

        public TChannel AcceptChannel()
        {
            return InterceptChannel(httpsListener.AcceptChannel());
        }

        public IAsyncResult BeginAcceptChannel(TimeSpan timeout, AsyncCallback callback, object state)
        {
            return httpsListener.BeginAcceptChannel(timeout, callback, state);
        }

        public IAsyncResult BeginAcceptChannel(AsyncCallback callback, object state)
        {
            return httpsListener.BeginAcceptChannel(callback, state);
        }

        public TChannel EndAcceptChannel(IAsyncResult result)
        {
            return InterceptChannel(httpsListener.EndAcceptChannel(result));
        }

        public IAsyncResult BeginWaitForChannel(TimeSpan timeout, AsyncCallback callback, object state)
        {
            var result = httpsListener.BeginWaitForChannel(timeout, callback, state);
            return result;
        }

        public bool EndWaitForChannel(IAsyncResult result)
        {
            var r = httpsListener.EndWaitForChannel(result);
            return r;
        }

        public T GetProperty<T>() where T : class
        {
            var result = httpsListener.GetProperty<T>();
            return result;
        }

        public Uri Uri
        {
            get { return httpsListener.Uri; }
        }

        public bool WaitForChannel(TimeSpan timeout)
        {
            var result = httpsListener.WaitForChannel(timeout);
            return result;
        }

        public void Abort()
        {
            httpsListener.Abort();
        }

        public IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback callback, object state)
        {
            var result = httpsListener.BeginClose(timeout, callback, state);
            return result;
        }

        public IAsyncResult BeginClose(AsyncCallback callback, object state)
        {
            var result = httpsListener.BeginClose(callback, state);
            return result;
        }

        public IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
        {
            var result = httpsListener.BeginOpen(timeout, callback, state);
            return result;
        }

        public IAsyncResult BeginOpen(AsyncCallback callback, object state)
        {
            var result = httpsListener.BeginOpen(callback, state);
            return result;
        }

        public void Close(TimeSpan timeout)
        {
            httpsListener.Close(timeout);
        }

        public void Close()
        {
            httpsListener.Close();
        }

        public event EventHandler Closed;

        public event EventHandler Closing;

        public void EndClose(IAsyncResult result)
        {
            httpsListener.EndClose(result);
        }

        public void EndOpen(IAsyncResult result)
        {
            httpsListener.EndOpen(result);
        }

        public event EventHandler Faulted;

        public void Open(TimeSpan timeout)
        {
            httpsListener.Open(timeout);
        }

        public void Open()
        {
            httpsListener.Open();
        }

        public event EventHandler Opened;

        public event EventHandler Opening;

        public System.ServiceModel.CommunicationState State
        {
            get { return httpsListener.State; }
        }
    }

}

Next, we need that ReplyChannelWrapper to implement IReplyChannel and intercept calls that pass a request context so we can snag the HttpListenerContext : 接下来,我们需要ReplyChannelWrapper来实现IReplyChannel并拦截传递请求上下文的调用,以便我们可以截获HttpListenerContext

using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Channels;
using System.Text;
using System.Threading.Tasks;

namespace MyNamespace.AcceptSslClientCertificate
{
    public class ReplyChannelWrapper: IChannel, IReplyChannel
    {
        IReplyChannel channel;

        public ReplyChannelWrapper(IReplyChannel channel)
        {
            this.channel = channel;

            // When an event is fired on the target channel, 
            // fire our corresponding event with the same params.
            channel.Opening += (s, e) =>
            {
                if (Opening != null)
                    Opening(s, e);
            };
            channel.Opened += (s, e) =>
            {
                if (Opened != null)
                    Opened(s, e);
            };
            channel.Closing += (s, e) =>
            {
                if (Closing != null)
                    Closing(s, e);
            };
            channel.Closed += (s, e) =>
            {
                if (Closed != null)
                    Closed(s, e);
            };
            channel.Faulted += (s, e) =>
            {
                if (Faulted != null)
                    Faulted(s, e);
            };
        }

        public T GetProperty<T>() where T : class
        {
            return channel.GetProperty<T>();
        }

        public void Abort()
        {
            channel.Abort();
        }

        public IAsyncResult BeginClose(TimeSpan timeout, AsyncCallback callback, object state)
        {
            return channel.BeginClose(timeout, callback, state);
        }

        public IAsyncResult BeginClose(AsyncCallback callback, object state)
        {
            return channel.BeginClose(callback, state);
        }

        public IAsyncResult BeginOpen(TimeSpan timeout, AsyncCallback callback, object state)
        {
            return channel.BeginOpen(timeout, callback, state);
        }

        public IAsyncResult BeginOpen(AsyncCallback callback, object state)
        {
            return channel.BeginOpen(callback, state);
        }

        public void Close(TimeSpan timeout)
        {
            channel.Close(timeout);
        }

        public void Close()
        {
            channel.Close();
        }

        public event EventHandler Closed;

        public event EventHandler Closing;

        public void EndClose(IAsyncResult result)
        {
            channel.EndClose(result);
        }

        public void EndOpen(IAsyncResult result)
        {
            channel.EndOpen(result);
        }

        public event EventHandler Faulted;

        public void Open(TimeSpan timeout)
        {
            channel.Open(timeout);
        }

        public void Open()
        {
            channel.Open();
        }

        public event EventHandler Opened;

        public event EventHandler Opening;

        public System.ServiceModel.CommunicationState State
        {
            get { return channel.State; }
        }

        public IAsyncResult BeginReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state)
        {
            var r = channel.BeginReceiveRequest(timeout, callback, state);
            return r;
        }

        public IAsyncResult BeginReceiveRequest(AsyncCallback callback, object state)
        {
            var r = channel.BeginReceiveRequest(callback, state);
            return r;
        }

        public IAsyncResult BeginTryReceiveRequest(TimeSpan timeout, AsyncCallback callback, object state)
        {
            var r = channel.BeginTryReceiveRequest(timeout, callback, state);
            return r;
        }

        public IAsyncResult BeginWaitForRequest(TimeSpan timeout, AsyncCallback callback, object state)
        {
            var r = channel.BeginWaitForRequest(timeout, callback, state);
            return r;
        }

        private RequestContext CaptureClientCertificate(RequestContext context)
        {
            try
            {
                if (context != null
                    && context.RequestMessage != null  // Will be null when service is shutting down
                    && context.GetType().FullName == "System.ServiceModel.Channels.HttpRequestContext+ListenerHttpContext")
                {
                    // Defer retrieval of the certificate until it is actually needed. 
                    // This is because some (many) requests may not need the client certificate. 
                    // Why make all requests incur the connection overhead of asking for a client certificate when only some need it?
                    // We use a Lazy<X509Certificate2> here to defer the retrieval of the client certificate
                    // AND guarantee that the client cert is only fetched once regardless of how many times
                    // the message property value is retrieved.
                    context.RequestMessage.Properties.Add(Constants.X509ClientCertificateMessagePropertyName,
                        new Lazy<X509Certificate2>(() =>
                        {
                            // The HttpListenerContext we need is in a private field of an internal WCF class.
                            // Use reflection to get the value of the field. This is our one and only dirty trick.
                            var fieldInfo = context.GetType().GetField("listenerContext", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
                            var listenerContext = (System.Net.HttpListenerContext)fieldInfo.GetValue(context);
                            return listenerContext.Request.GetClientCertificate();
                        }));
                }
            }
            catch (Exception e)
            {
                Logging.Error("ReplyChannel.CaptureClientCertificate exception {0}: {1}", e.GetType().Name, e.Message);
            }
            return context;
        }

        public RequestContext EndReceiveRequest(IAsyncResult result)
        {
            return CaptureClientCertificate(channel.EndReceiveRequest(result));
        }

        public bool EndTryReceiveRequest(IAsyncResult result, out RequestContext context)
        {
            var r = channel.EndTryReceiveRequest(result, out context);
            CaptureClientCertificate(context);
            return r;
        }

        public bool EndWaitForRequest(IAsyncResult result)
        {
            return channel.EndWaitForRequest(result);
        }

        public System.ServiceModel.EndpointAddress LocalAddress
        {
            get { return channel.LocalAddress; }
        }

        public RequestContext ReceiveRequest(TimeSpan timeout)
        {
            return CaptureClientCertificate(channel.ReceiveRequest(timeout));
        }

        public RequestContext ReceiveRequest()
        {
            return CaptureClientCertificate(channel.ReceiveRequest());
        }

        public bool TryReceiveRequest(TimeSpan timeout, out RequestContext context)
        {
            var r = TryReceiveRequest(timeout, out context);
            CaptureClientCertificate(context);
            return r;
        }

        public bool WaitForRequest(TimeSpan timeout)
        {
            return channel.WaitForRequest(timeout);
        }
    }
}

In the web service, we set up the channel binding like this: 在Web服务中,我们像这样设置频道绑定:

    var myUri = new Uri("myuri");
    var host = new WebServiceHost(typeof(MyService), myUri);
    var contractDescription = ContractDescription.GetContract(typeof(MyService));

    if (myUri.Scheme == "https")
    {
        // Construct a custom binding instead of WebHttpBinding
        // Construct an HttpsTransportBindingElementWrapper so that we can intercept HTTPS
        // connection startup activity so that we can capture a client certificate from the
        // SSL link if one is available.
        // This enables us to accept a client certificate if one is offered, but not require
        // a client certificate on every request.
        var binding = new CustomBinding(
            new WebMessageEncodingBindingElement(),
            new HttpsTransportBindingElementWrapper() 
            { 
                RequireClientCertificate = false, 
                ManualAddressing = true 
            });

        var endpoint = new WebHttpEndpoint(contractDescription, new EndpointAddress(myuri));
        endpoint.Binding = binding;

        host.AddServiceEndpoint(endpoint);

And finally, in the web service authenticator we use the following code to see if a client certificate was captured by the above interceptors: 最后,在Web服务身份验证器中,我们使用以下代码来查看上述拦截器是否捕获了客户端证书:

            object lazyCert = null;
            if (OperationContext.Current.IncomingMessageProperties.TryGetValue(Constants.X509ClientCertificateMessagePropertyName, out lazyCert))
            {
                certificate = ((Lazy<X509Certificate2>)lazyCert).Value;
            }

Note that for any of this this to work, HttpsTransportBindingElement.RequireClientCertificate must be set to False. 请注意, HttpsTransportBindingElement.RequireClientCertificate此方法起作用,必须将HttpsTransportBindingElement.RequireClientCertificate设置为False。 If it is set to true, then WCF will only accept SSL connections bearing client certificates. 如果将其设置为true,则WCF将仅接受带有客户端证书的SSL连接。

With this solution, the web service is completely responsible for validating the client certificate. 使用此解决方案,Web服务完全负责验证客户端证书。 WCF's automatic certificate validation is not engaged. WCF的自动证书验证未启用。

Constants.X509ClientCertificateMessagePropertyName is whatever string value you want it to be. Constants.X509ClientCertificateMessagePropertyName是您想要的任何字符串值。 It needs to be reasonably unique to avoid colliding with standard message property names, but since it is only used to communicate between different parts of our own service it doesn't need to be a special well-known value. 它必须具有合理的唯一性,以避免与标准消息属性名称发生冲突,但是由于它仅用于在我们自己服务的不同部分之间进行通信,因此不需要是一个特殊的众所周知的值。 It could be a URN beginning with your company or domain name, or if you're really lazy just a GUID value. 它可能是一个以您的公司或域名开头的URN,或者如果您真的很懒,只是一个GUID值。 No one will care. 没有人会在乎。

Note that because this solution is dependent upon the name of an internal class and a private field in the WCF HTTP implementation, this solution may not be suitable for deployment in some projects. 请注意,由于此解决方案取决于WCF HTTP实现中内部类的名称和私有字段,因此该解决方案可能不适用于某些项目中的部署。 It should be stable for a given .NET release, but the internals could easily change in future .NET releases, rendering this code ineffective. 对于给定的.NET版本,它应该是稳定的,但是在将来的.NET版本中,内部结构很容易更改,从而使此代码无效。

Again, if anyone has any better solution I welcome suggestions. 同样,如果有人有更好的解决方案,我欢迎提出建议。

I think that does not work. 我认为这行不通。

If you cannot influence the client so that an empty certificate is created or an unassigned reference to a certificate is accepted, validate the this special case from the server side and log to a log file then there is no way. 如果您不能影响客户端,从而创建一个空证书或接受对证书的未分配引用,请从服务器端验证此特殊情况并登录到日志文件,那么将无法进行。 You will have to mimic IIS behavior and you will have to check before. 您将必须模仿IIS行为,并且必须先进行检查。 That's a guess. 那是个猜测。 No expertise. 没有专业知识。

What you do usually is to a) try to validate the certificate by walking through the chain for certificates provided b) In case of no certificate provided double and triple check the client and log the occurrence. 您通常要做的是a)尝试通过浏览提供的证书的链来验证证书b)如果没有提供证书,请仔细检查客户端并记录发生情况。

I think '.net' does not give you the opportunity to control the negotiation. 我认为“ .net”并没有给您控制谈判的机会。

Imo that opens the door to the man in the middle. 伊莫打开中间那人的门。 That's why I think MS don't allow that and Java similar, afik. 这就是为什么我认为MS不允许这样做与Java类似,afik。

Finally I decided put the service behind an IIS. 最后,我决定将服务放在IIS之后。 WCF uses the 'IIS' (http.sys) anyway iirc. 无论如何,WCF都会使用IIS(http.sys)。 It does not make a huge difference if you let the IIS do little more. 如果让IIS做更多事情,那不会有太大的区别。

SBB is one of the few libraries that allow you to do that in a convenient way. SBB是少数几个使您可以方便地完成此操作的库之一。 You have access to every step of the negotiation. 您可以访问谈判的每个步骤。

Once I used Delphi and ELDOS SecureBlackbox ('before' WCF ... net 3.0) and it worked that way. 一旦我使用了Delphi和ELDOS SecureBlackbox(“ WCF之前的版本... net 3.0”),它就可以正常工作。 Today you have to do extensive investigation on the server side and people move towards two sided approaches. 今天,您必须在服务器端进行广泛的调查,并且人们朝着两种方法发展。

In Java you have to create TrustManager that simply trusts everything. 在Java中,您必须创建仅信任所有内容的TrustManager。

I think IIS is the option left. 我认为IIS是剩下的选项。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM