2013-09-06 17 views
5

自己基本WCFサービスで、HTTP基本認証資格情報またはクライアント証明書資格情報で要求を受け入れることができる単一のSSLエンドポイントが必要です。自己ホスト型WCFサービスでクライアント証明書をオプションで受け入れる

IISでホストされているサービスの場合、IISは「クライアント証明書を受け入れる」と「クライアント証明書が必要です」を区別します。

WCFのWebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;は、IISの「証明書を必要とする」設定と類似しているようです。

クライアント証明書の資格情報を受け入れるようにWCF自己ホストサービスを設定する方法はありますか?自己ホスト型WCFサービスのIISの「クライアント証明書を受け入れる」というWCFのアナログはありますか?

答えて

0

私は動作しないと思います。

空の証明書が作成されるようにクライアントに影響を与えたり、証明書への未割り当ての参照を受け入れることができない場合は、この特殊なケースをサーバー側から検証し、ログファイルにログします。 IISの動作を模倣する必要があり、前もってチェックする必要があります。それは推測です。専門知識はありません。

あなたが通常行うことは にあるa)の証明書チェーンを歩いて証明書を検証しよう Bを提供)がない証明書の場合は、クライアントをチェックし、発生をログダブル、トリプル提供。

「.net」は、あなたに交渉をコントロールする機会を与えるものではないと思います。

今は、真ん中の人の扉を開きます。だからこそ私はMSがそれとJavaのようなafikを許さないと思うのです。

最後に、IISの背後にサービスを置くことに決めました。 WCFはとにかくiircの 'IIS'(http.sys)を使用します。 IISにもう少しやらせるなら、大きな違いはありません。

SBBは、便利な方法でこれを実行できる数少ないライブラリの1つです。交渉のすべてのステップにアクセスできます。

一度私はDelphiとELDOS SecureBlackbox( 'before' WCF ... net 3.0)を使い、それはそのように機能しました。今日では、サーバー側について広範な調査を行う必要があり、人々は2つの側面のアプローチに移行します。

Javaでは、単にすべてを信頼するTrustManagerを作成する必要があります。

私はIISが残っているオプションだと思います。

5

WCFでオプションでSSLクライアント証明書を受け入れる方法が見つかりましたが、汚いトリックが必要です。もし誰かが(WCFを使わないでください)より良い解決策を持っているなら、私はそれを聞いてみたいと思います。

ずっと逆コンパイルWCF HTTPチャンネルのクラスで周り掘り後、私はいくつかのことを学びました:

  1. WCF HTTPはモノリシックです。ベジリオンクラスが飛行していますが、それらのすべてが「内部」とマークされているためアクセスできません。新しいバインディングクラスがHTTPスタックで手伝ってくれるものはすべてアクセスできないため、コアHTTPビヘイビアをインターセプトまたは拡張しようとすると、WCFチャネルバインディングスタックはビッグヒルの価値がありません。
  2. IISと同様に、HttpListener/HTTPSYSの上にWCFが乗っています。HttpListenerは、SSLクライアント証明書へのアクセスを提供します。ただし、WCF HTTPは基になるHttpListenerへのアクセスを提供しません。

HttpChannelListener(内部クラス)がチャネルを開き、IReplyChannelを返したときに最も近いインターセプトポイントが見つかりました。 IReplyChannelには新しいリクエストを受信するメソッドがあり、これらのメソッドはRequestContextを返します。

このRequestContextのHttp内部クラスによって構築され返された実際のオブジェクトインスタンスはListenerHttpContext(内部クラス)です。 ListenerHttpContextは、HttpListenerContextへの参照を保持します。これは、公開されているWCFの下にあるSystem.Net.HttpListenerのレイヤーに由来します。

HttpListenerContext.Request.GetClientCertificate()は、SSLハンドシェイクで利用可能なクライアント証明書があるかどうかを確認し、存在する場合はロードし、存在しない場合はスキップする方法です。

残念なことに、HttpListenerContextへの参照はListenerHttpContextのプライベートフィールドです。この作業を行うために、私は1つの汚いトリックに頼らざるを得ませんでした。私はリフレクションを使ってプライベートフィールドの値を読み取って、現在のリクエストのHttpListenerContextにアクセスできるようにします。

だから、ここで私はそれをやった方法は次のとおりです。

まず、我々は、基本クラスによって返されたチャネルリスナーを傍受してラップするBuildChannelListener<TChannel>をオーバーライドすることができるようにHttpsTransportBindingElementの子孫を作成します。

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は、私たちがHttpListenerContextを暗礁ことができるように、要求コンテキストを渡しIReplyChannelと切片の呼び出しを実装する:Webサービスでは

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

そして最後に、 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リリースで容易に変更され、このコードは効果がありません。

もう一度、誰かより良い解決策があれば、私は提案を歓迎します。

+0

ありがとうございます。あなたのような人を知ることは良いことです。それは興味深い解決策です。私はアーカイブフォルダを調べました。私は間違っていた。私はあなたが単に別の 'ソケット'を差し込むことができると思った。私はそれを混ぜた。 –

+0

トピックをオフに - 実際にあなたを助けるかもしれません。 Portfusion。 http://sourceforge.net/p/portfusion/home/PortFusion/ http://fusion.corsis.eu/ https://github.com/corsis/PortFusion#readme –

+0

印象的な研究、私はそれがうまくいくX509CertificateValidationMode.Customをそのまま使用して、クライアント証明書がない場合はnullを渡します。 – Sergii

関連する問題