5

我想在我的自托管 WCF 服务中有一个 SSL 端点,它可以接受带有 HTTP 基本身份验证凭据或客户端证书凭据的请求。

对于 IIS 托管服务,IIS 区分“接受客户端证书”和“需要客户端证书”。

WCFWebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;似乎类似于 IIS 中的“需要证书”设置。

有没有办法将 WCF 自托管服务配置为接受客户端证书凭据,但不要求每个客户端都提供它们?是否有用于自托管 WCF 服务的 IIS“接受客户端证书”的 WCF 模拟?

4

2 回答 2

5

我找到了一种在 WCF 中选择接受 SSL 客户端证书的方法,但它需要一个肮脏的技巧。如果有人有更好的解决方案(除了“不要使用 WCF”),我很想听听。

在对反编译的 WCF Http 通道类进行了大量研究之后,我学到了一些东西:

  1. WCF Http 是单片的。有无数类飞来飞去,但它们都被标记为“内部”,因此无法访问。如果您试图拦截或扩展核心 HTTP 行为,那么 WCF 通道绑定堆栈不值得一大堆 bean,因为新绑定类想要在 HTTP 堆栈中摆弄的东西都是不可访问的。
  2. WCF 位于 HttpListener / HTTPSYS 之上,就像 IIS 一样。HttpListener 提供对 SSL 客户端证书的访问。但是,WCF HTTP 不提供对底层 HttpListener 的任何访问。

我能找到的最接近的拦截点是HttpChannelListener(内部类)打开一个通道并返回一个IReplyChannel. IReplyChannel具有接收新请求的方法,这些方法返回一个RequestContext.

Http 内部类为此构造和返回的实际对象实例RequestContextListenerHttpContext(内部类)。ListenerHttpContext持有对 a 的引用HttpListenerContext,它来自System.Net.HttpListenerWCF 下的公共层。

HttpListenerContext.Request.GetClientCertificate()是我们需要查看 SSL 握手中是否有可用的客户端证书的方法,如果有则加载它,如果没有则跳过它。

不幸的是,引用HttpListenerContext是 的私有字段ListenerHttpContext,所以为了完成这项工作,我不得不求助于一个肮脏的把戏。我使用反射来读取私有字段的值,以便我可以获取HttpListenerContext当前请求的值。

所以,我是这样做的:

首先,创建一个后代,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;
        }
    }
}

接下来,我们需要将上面传输绑定元素截获的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; }
        }
    }

}

接下来,我们需要它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);
        }
    }
}

在 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);

最后,在 Web 服务身份验证器中,我们使用以下代码查看客户端证书是否被上述拦截器捕获:

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

请注意,要使此功能起作用,HttpsTransportBindingElement.RequireClientCertificate必须将其设置为 False。如果设置为 true,则 WCF 将仅接受带有客户端证书的 SSL 连接。

使用此解决方案,Web 服务完全负责验证客户端证书。未使用 WCF 的自动证书验证。

Constants.X509ClientCertificateMessagePropertyName是您想要的任何字符串值。它需要相当独特以避免与标准消息属性名称冲突,但由于它仅用于在我们自己的服务的不同部分之间进行通信,因此它不需要是一个特殊的众所周知的值。它可能是一个以您的公司或域名开头的 URN,或者如果您真的很懒,只是一个 GUID 值。没有人会在意。

请注意,由于此解决方案依赖于 WCF HTTP 实现中的内部类和私有字段的名称,因此此解决方案可能不适合在某些项目中部署。对于给定的 .NET 版本,它应该是稳定的,但内部结构在未来的 .NET 版本中很容易发生变化,从而导致此代码无效。

同样,如果有人有更好的解决方案,我欢迎提出建议。

于 2013-09-16T17:51:24.343 回答
0

我认为这行不通。

如果您无法影响客户端以创建空证书或接受对证书的未分配引用,请从服务器端验证这种特殊情况并记录到日志文件,那么就没有办法了。您将不得不模仿 IIS 行为,并且必须先检查一下。这是一个猜测。没有专业知识。

您通常做的是 a) 尝试通过遍历提供的证书链来验证证书 b) 如果没有提供证书,请双重和三重检查客户端并记录发生的情况。

我认为“.net”不会给你控制谈判的机会。

伊莫为中间的人打开门。这就是为什么我认为 MS 不允许这样做和 Java 类似,afik。

最后我决定把服务放在 IIS 后面。WCF 无论如何都使用 'IIS' (http.sys) iirc。如果您让 IIS 做更多的事情,这并没有太大的区别。

SBB 是为数不多的允许您以方便的方式执行此操作的库之一。您可以访问谈判的每一步。

一旦我使用了 Delphi 和 ELDOS SecureBlackbox('之前' WCF ... net 3.0),它就以这种方式工作。今天,您必须在服务器端进行广泛的调查,人们转向两侧的方法。

在 Java 中,您必须创建简单信任所有内容的 TrustManager。

我认为 IIS 是剩下的选择。

于 2013-09-15T16:30:52.183 回答