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

时间:2013-09-06 22:00:06

标签: wcf authentication ssl client-certificates self-hosting

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

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

WCF的WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;似乎是IIS中“需要证书”设置的模拟。

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

2 个答案:

答案 0 :(得分:5)

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

经过反编译的WCF Http频道课程后,我学到了一些东西:

  1. WCF Http是单片的。有许多班级飞来飞去,但所有班级都标有“内部”,因此无法进入。如果你试图拦截或扩展核心HTTP行为,那么WCF通道绑定堆栈就不值得用豆子了,因为新的绑定类在HTTP堆栈中想要操作的东西都是不可访问的。
  2. 就像IIS一样,WCF位于HttpListener / HTTPSYS之上。 HttpListener提供对SSL客户端证书的访问。但是,WCF HTTP不提供对底层HttpListener的任何访问。
  3. 我能找到的最接近的拦截点是HttpChannelListener(内部类)打开频道并返回IReplyChannel时。 IReplyChannel具有接收新请求的方法,这些方法返回RequestContext

    由此RequestContext的Http内部类构造和返回的实际对象实例是ListenerHttpContext(内部类)。 ListenerHttpContext包含对HttpListenerContext的引用,该System.Net.HttpListener来自WCF下方的公共HttpListenerContext.Request.GetClientCertificate()图层。

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

    不幸的是,对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; } } } 来拦截并包装基类返回的通道侦听器:

    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; }
            }
        }
    
    }
    

    接下来,我们需要包装上面的传输绑定元素拦截的ChannelListener:

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

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

    在网络服务中,我们设置了这样的频道绑定:

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

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

    HttpsTransportBindingElement.RequireClientCertificate

    请注意,要使其中的任何一个工作,Constants.X509ClientCertificateMessagePropertyName必须设置为False。如果设置为true,则WCF将仅接受带有客户端证书的SSL连接。

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

    {{1}}是您想要的任何字符串值。它必须是合理的唯一,以避免与标准消息属性名称冲突,但由于它仅用于在我们自己的服务的不同部分之间进行通信,因此它不需要是一个特殊的已知值。它可能是以您的公司或域名开头的URN,或者如果您真的只是一个GUID值。没人会关心。

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

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

答案 1 :(得分:0)

我认为这不起作用。

如果您无法影响客户端以便创建空证书或接受证书的未分配引用,请从服务器端验证此特殊情况并登录到日志文件,则无法进行。您将不得不模仿IIS行为,您必须先检查。这是猜测。没有专业知识。

你通常做的是 a)尝试通过遍历链提供证书来验证证书 b)如果没有提供证书,则双重和三重检查客户端并记录事件。

我认为'.net'不会让你有机会控制谈判。

Imo打开了中间男人的大门。这就是为什么我认为MS不允许和Java类似,afik。

最后我决定把服务放在IIS后面。无论如何,WCF使用'IIS'(http.sys)。如果让IIS再做一点,它就不会产生太大的影响。

SBB是少数能够以方便的方式实现这一目标的库之一。您可以访问协商的每一步。

一旦我使用Delphi和ELDOS SecureBlackbox('之前'WCF ... net 3.0)并且它就是这样工作的。今天你必须在服务器端进行广泛的调查,人们会采取双向的方法。

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

我认为IIS是剩下的选择。