2013-09-06 16 views
5

Ich möchte einen einzelnen SSL-Endpunkt in meinem selbst gehosteten WCF-Dienst haben, der Anforderungen mit HTTP-Basisauthentifizierungsdaten oder Clientzertifikatsanmeldeinformationen annehmen kann.Akzeptieren Sie optional Clientzertifikate in einem selbst gehosteten WCF-Dienst

Für IIS-gehostete Dienste unterscheidet IIS zwischen "Akzeptiert Client-Zertifikate" und "Benötigt Client-Zertifikate".

WCF WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; scheint das Analog der Einstellung "erfordert Zertifikate" in IIS zu sein.

Gibt es eine Möglichkeit, einen selbst gehosteten WCF-Dienst so zu konfigurieren, dass er Anmeldeinformationen für Clientzertifikate akzeptiert, sie jedoch nicht von jedem Client erfordert? Gibt es ein WCF-Analog von IIS "Akzeptiert Client-Zertifikate" für selbst gehostete WCF-Dienste?

Antwort

0

Ich denke, das funktioniert nicht.

Wenn Sie den Client nicht beeinflussen können, so dass ein leeres Zertifikat erstellt wird oder ein nicht zugewiesener Verweis auf ein Zertifikat akzeptiert wird, validieren Sie diesen speziellen Fall von der Serverseite und protokollieren Sie ihn in einer Protokolldatei. Sie müssen das Verhalten von IIS nachahmen, und Sie müssen es vorher prüfen. Das ist eine Vermutung. Keine Erfahrung.

Was Sie normalerweise tun, ist a) versuchen Sie, das Zertifikat zu validieren, indem Sie durch die Kette für Zertifikate b) Wenn kein Zertifikat zur Verfügung gestellt Doppel-und Triple überprüfen Sie den Client und protokollieren das Auftreten.

Ich denke ".net" gibt Ihnen nicht die Möglichkeit, die Verhandlung zu steuern.

Imo, die die Tür zu dem Mann in der Mitte öffnet. Deshalb glaube ich, dass MS das nicht erlaubt und Java ähnlich, afik.

Schließlich entschied ich, den Dienst hinter einem IIS zu setzen. WCF verwendet den 'IIS' (http.sys) sowieso iirc. Es macht keinen großen Unterschied, wenn Sie den IIS etwas mehr tun lassen.

SBB ist eine der wenigen Bibliotheken, mit denen Sie das bequem machen können. Sie haben Zugang zu jedem Verhandlungsschritt.

Sobald ich Delphi und ELDOS SecureBlackbox ('vor' WCF ... net 3.0) verwendet und es funktioniert so. Heute müssen Sie umfangreiche Untersuchungen auf der Server-Seite durchführen, und die Leute bewegen sich auf zweiseitige Ansätze zu.

In Java müssen Sie TrustManager erstellen, der einfach alles vertraut.

Ich denke, IIS ist die Option übrig.

5

Ich habe einen Weg gefunden, SSL-Client-Zertifikate in WCF optional zu akzeptieren, aber es erfordert einen schmutzigen Trick. Wenn jemand eine bessere Lösung hat (außer WCF nicht verwenden), würde ich es gerne hören.

Nach viel Graben um in dekompiliert WCF Http Kanalklassen, ich habe ein paar Dinge gelernt:

  1. WCF Http ist monolithisch. Es gibt eine Vielzahl von Klassen, die herumfliegen, aber alle sind als "intern" gekennzeichnet und daher unzugänglich. Der WCF-Kanalbindungsstapel ist keine Anhäufung von Beans wert, wenn Sie das HTTP-Hauptverhalten abfangen oder erweitern möchten, da alle Dinge, auf die eine neue Bindungsklasse im HTTP-Stapel zugreifen soll, nicht zugänglich sind.
  2. WCF fährt auf HttpListener/HTTPSYS, genau wie IIS.HttpListener bietet Zugriff auf das SSL-Clientzertifikat. WCF HTTP bietet jedoch keinen Zugriff auf den zugrunde liegenden HttpListener.

Der nächstgelegene Schnittpunkt den ich finden konnte, wenn HttpChannelListener (interne Klasse) einen Kanal öffnet und gibt ein IReplyChannel. IReplyChannel verfügt über Methoden zum Empfangen einer neuen Anforderung, und diese Methoden geben eine RequestContext zurück.

Die tatsächliche Objektinstanz, die von den internen Http-Klassen für dieses RequestContext erstellt und zurückgegeben wird, ist ListenerHttpContext (interne Klasse). ListenerHttpContext enthält einen Verweis auf eine HttpListenerContext, die von der öffentlichen System.Net.HttpListener Schicht unter WCF kommt.

ist die Methode, die wir benötigen, um zu sehen, ob ein Client-Zertifikat im SSL-Handshake verfügbar ist, laden Sie es, falls vorhanden, oder überspringen Sie es, falls nicht.

Leider ist der Verweis auf HttpListenerContext ein privates Feld von ListenerHttpContext, also musste ich zu diesem schmutzigen Trick greifen. Ich benutze Reflektion, um den Wert des privaten Feldes zu lesen, so dass ich unter der HttpListenerContext der aktuellen Anfrage erhalten kann.

So, hier ist, wie ich es tat:

Zuerst ein Nachkomme HttpsTransportBindingElement so erstellen, dass wir BuildChannelListener<TChannel> außer Kraft setzen können abzufangen und wickeln Sie den Kanal Hörer von der Basisklasse zurückgegeben:

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

Weiter , müssen wir die ChannelListener durch das obige Transportbindungselement abgefangen wickeln:

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

} 

Als nächstes müssen wir, dass 01.239.IReplyChannel und Intercept-Anrufe zu implementieren, die einen Anforderungskontext übergeben, so können wir die HttpListenerContext verhaken:

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

Im Web-Service setzen wir den Kanal verbindlich wie auf den Punkt:

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

Und schließlich, in dem Web-Service-Authentifikator verwenden wir den folgenden Code ein, um zu sehen, ob ein Client-Zertifikat durch die oben Abfangjäger gefangen genommen wurde:

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

Beachten Sie, dass für diesen Fall HttpsTransportBindingElement.RequireClientCertificate auf "False" gesetzt sein muss. Wenn es auf "True" festgelegt ist, akzeptiert WCF nur SSL-Verbindungen, die Clientzertifikate enthalten.

Mit dieser Lösung ist der Webdienst vollständig für die Validierung des Clientzertifikats verantwortlich. WCFs automatische Zertifikatsprüfung ist nicht aktiviert.

Constants.X509ClientCertificateMessagePropertyName ist was für String-Wert Sie wollen es sein. Es muss einigermaßen eindeutig sein, um eine Kollision mit standardmäßigen Nachrichteneigenschaftsnamen zu vermeiden. Da es jedoch nur für die Kommunikation zwischen verschiedenen Teilen unseres eigenen Dienstes verwendet wird, muss es kein besonders bekannter Wert sein. Es könnte eine URN sein, die mit Ihrem Firmen- oder Domain-Namen beginnt, oder wenn Sie wirklich faul sind, nur ein GUID-Wert. Niemand wird sich darum kümmern.

Beachten Sie, dass diese Lösung für die Bereitstellung in einigen Projekten möglicherweise nicht geeignet ist, da diese Lösung von dem Name einer internen Klasse und einem privaten Feld in der WCF-HTTP-Implementierung abhängt. Es sollte für eine bestimmte .NET-Version stabil sein, aber die Interna könnten sich in zukünftigen .NET-Versionen leicht ändern, wodurch dieser Code unwirksam wird.

Noch einmal, wenn jemand eine bessere Lösung hat, begrüße ich Vorschläge.

+0

Vielen Dank. Gut, Leute wie dich zu kennen. Das ist eine interessante Lösung. Ich habe in meine Archivordner geschaut. Ich habe mich geirrt. Ich dachte, du könntest einfach eine andere "Steckdose" anschließen. Ich habe das gemischt. –

+0

Off Topic - aber vielleicht kann es Ihnen in der Praxis helfen. Portfusion. http://sourceforge.net/p/portfusion/home/PortFusion/ http://fusion.corsis.eu/ https://github.com/corsis/PortFusion#readme –

+0

Beeindruckende Forschung, ich wünschte, es funktioniert out of box mit X509CertificateValidationMode.Custom, übergibt nur Null, wenn kein Clientzertifikat vorhanden ist. – Sergii

Verwandte Themen