2013-09-06 28 views
5

Chciałbym mieć pojedynczy punkt końcowy SSL w mojej samohostowanej usłudze WCF, która może przyjmować żądania z podstawowymi poświadczeniami autoryzacji HTTP lub poświadczeniami certyfikatu klienta.Opcjonalnie akceptuj certyfikaty klienta w samodzielnej usłudze WCF

W przypadku usług IIS hostowanych usługi IIS rozróżniają "Zaakceptuj certyfikaty klienta" i "Wymagają certyfikatów klienta".

WCF WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; wydaje się być analogiem ustawienia "wymaga certyfikatów" w IIS.

Czy istnieje sposób skonfigurowania samodzielnie hostowanej usługi WCF do akceptowania poświadczeń certyfikatu klienta, ale nie wymaga tego od każdego klienta? Czy istnieje analog WCF z IIS "Akceptuje certyfikaty klienta" dla samodzielnie hostowanych usług WCF?

Odpowiedz

0

Myślę, że to nie działa.

Jeśli nie można wpłynąć na klienta w celu utworzenia pustego certyfikatu lub przyjęcia nieprzypisanego odniesienia do certyfikatu, należy sprawdzić ten specjalny przypadek od strony serwera i zalogować się do pliku dziennika, a następnie nie ma możliwości. Będziesz musiał naśladować zachowanie IIS i będziesz musiał to sprawdzić wcześniej. Zgadnij. Brak doświadczenia.

Co robisz zazwyczaj jest a) próbują zweryfikować certyfikat przechodząc przez łańcucha certyfikatów warunkiem b) W przypadku braku świadectwa przewidzianego podwójne i potrójne sprawdzić klienta i zalogować wystąpienie.

Myślę, że ".net" nie daje możliwości kontrolowania negocjacji.

Imo, które otwiera drzwi do mężczyzny pośrodku. Dlatego uważam, że MS nie pozwala na to i Java jest podobna, afik.

Ostatecznie zdecydowałem się umieścić usługę za IIS. WCF używa "IIS" (http.sys) w każdym razie iirc. Nie zrobi to wielkiej różnicy, jeśli pozwolisz IIS zrobić niewiele więcej.

SBB jest jedną z niewielu bibliotek, które umożliwiają to w wygodny sposób. Masz dostęp do każdego etapu negocjacji.

Kiedyś użyłem Delphi i ELDOS SecureBlackbox ("przed" WCF ... net 3.0) i działało to w ten sposób. Dzisiaj musisz przeprowadzić obszerne śledztwo po stronie serwera, a ludzie zmierzają w kierunku dwustronnego podejścia.

W Javie trzeba utworzyć TrustManager, który po prostu ufa wszystkim.

Myślę, że IIS jest opcją.

5

Znalazłem sposób, aby opcjonalnie akceptować certyfikaty klienta SSL w WCF, ale wymaga to brudnej sztuczki. Jeśli ktokolwiek ma lepsze rozwiązanie (inne niż "Nie korzystam z WCF"), bardzo chciałbym to usłyszeć.

Po dużo kopania wokół w decompiled WCF HTTP zajęcia kanałów, nauczyłem się kilku rzeczy:

  1. WCF HTTP jest monolitem. Wokół latają lekcje bezilności, ale wszystkie z nich są oznaczone jako "wewnętrzne", a zatem niedostępne. Stos powiązania kanału WCF nie jest warty wzniesienia ziaren, jeśli próbujesz przechwycić lub rozszerzyć główne zachowania HTTP, ponieważ rzeczy, których nowa klasa wiążąca chciałby poruszyć w stosie HTTP, są niedostępne.
  2. WCF jeździ na HttpListener/HTTPSYS, tak jak robi to IIS.HttpListener zapewnia dostęp do certyfikatu klienta SSL. Protokół WCF HTTP nie zapewnia jednak żadnego dostępu do ukrytej listy HttpListener.

Najbliższy punkt przechwycenie udało mi się znaleźć to, kiedy HttpChannelListener (klasa wewnętrzna) otwiera kanał i zwraca IReplyChannel. IReplyChannel ma metody otrzymywania nowego żądania, a te metody zwracają wartość RequestContext.

Właściwą instancją obiektu skonstruowaną i zwróconą przez wewnętrzne klasy HTTP dla tego RequestContext jest ListenerHttpContext (klasa wewnętrzna). ListenerHttpContext zawiera odniesienie do HttpListenerContext, które pochodzi z publicznej warstwy System.Net.HttpListener pod spodem WCF.

HttpListenerContext.Request.GetClientCertificate() to metoda, którą musimy sprawdzić, czy istnieje certyfikat klienta dostępny w uzgadnianiu SSL, załaduj go, jeśli istnieje, lub pomiń go, jeśli nie jest.

Niestety, odniesienie do HttpListenerContext jest prywatnym polem ListenerHttpContext, więc aby to zadziałało musiałem uciekać się do jednej brudnej sztuczki. Używam odbicia, aby odczytać wartość pola prywatnego, aby uzyskać dostęp do HttpListenerContext bieżącego żądania.

Więc, oto jak to zrobiłem:

Najpierw należy utworzyć potomka HttpsTransportBindingElement tak, że możemy zastąpić BuildChannelListener<TChannel> przechwycić i owinąć słuchacza kanału zwrócony przez klasę bazową:

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

Następny musimy owinąć ChannelListener przechwycone przez wyżej elementu transportowego Oprawa:

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

} 

Następnie musimy to ReplyChannelWrapper wdrożyć IReplyChannel i przechwytywanie połączeń, które przechodzą kontekst żądania więc możemy szkopuł 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); 
     } 
    } 
} 

w Serwisie WWW, możemy ustawić wiązania jak to kanał:

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

i wreszcie, w uwierzytelnienia usług internetowych stosujemy poniższy kod, żeby sprawdzić, czy certyfikat klienta został schwytany przez wyżej przechwytujących:

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

Zauważ, że aby to zadziałało, HttpsTransportBindingElement.RequireClientCertificate musi być ustawione na False. Jeśli ma wartość true, to WCF akceptuje tylko połączenia SSL z certyfikatami klienta.

Dzięki temu rozwiązaniu usługa internetowa ponosi pełną odpowiedzialność za sprawdzenie certyfikatu klienta. Automatyczna walidacja certyfikatu WCF nie jest włączona.

Constants.X509ClientCertificateMessagePropertyName jest dowolną wartością ciągu, który ma być. Musi być dość unikalny, aby uniknąć kolizji ze standardowymi nazwami właściwości komunikatów, ale ponieważ jest używany tylko do komunikacji między różnymi częściami naszej własnej usługi, nie musi być specjalną dobrze znaną wartością. Może to być URN zaczynający się od nazwy Twojej firmy lub domeny, lub jeśli jesteś naprawdę leniwy tylko wartością GUID. Nikogo to nie obchodzi.

Należy zauważyć, że ponieważ to rozwiązanie zależy od nazwy klasy wewnętrznej i pola prywatnego w implementacji HTTP WCF, to rozwiązanie może nie być odpowiednie do wdrożenia w niektórych projektach. Powinien być stabilny dla danego wydania .NET, ale wewnętrzne elementy mogą łatwo zmienić się w przyszłych wydaniach .NET, czyniąc ten kod nieskutecznym.

Jeszcze raz, jeśli ktoś ma lepsze rozwiązanie, przyjmuję z zadowoleniem sugestie.

+0

Dziękuję. Dobrze znać ludzi takich jak ty. To interesujące rozwiązanie. Zajrzałem do moich folderów archiwum. Myliłem się. Myślałem, że możesz po prostu podłączyć kolejne "gniazdo". Wymieszałem to. –

+0

Nie na temat - ale może może ci pomóc w praktyce. Portfusion. http://sourceforge.net/p/portfusion/home/PortFusion/ http://fusion.corsis.eu/ https://github.com/corsis/PortFusion#readme –

+0

Imponujące badania, życzę pracy out of box z X509CertificateValidationMode.Custom, po prostu przekazanie wartości null, jeśli nie ma certyfikatu klienta. – Sergii