2013-09-06 13 views
5

Mi piacerebbe avere un singolo endpoint SSL nel servizio WCF auto-ospitato che possa accettare richieste con credenziali di autenticazione di base HTTP o credenziali del certificato client.È possibile accettare i certificati client in un servizio WCF auto-ospitato

Per i servizi ospitati IIS, IIS distingue tra "Accetta certificati client" e "Richiede certificati client".

WCF WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate; sembra essere l'analogo dell'impostazione "richiede certificati" in IIS.

C'è un modo per configurare un servizio self-hosted di WCF per accettare le credenziali del certificato client ma non per richiederle da ogni client? Esiste un analogo di WCF di IIS "Accetta certificati client" per i servizi WCF auto-ospitati?

risposta

0

Penso che non funzioni.

Se non è possibile influenzare il client in modo che venga creato un certificato vuoto o un riferimento non assegnato a un certificato, convalidare questo caso speciale dal lato server e accedere a un file di registro, quindi non c'è modo. Dovrai imitare il comportamento di IIS e dovrai controllarlo prima. Questa è una supposizione. Nessuna esperienza

Quello che fai di solito è di a) cercare di convalidare il certificato a piedi attraverso la catena di certificati forniti b) In caso di mancato certificato di doppio o triplo controllo il cliente e registrare l'occorrenza.

Penso che ".net" non ti dia l'opportunità di controllare la negoziazione.

Imo che apre la porta all'uomo nel mezzo. Questo è il motivo per cui penso che la SM non permetta questo e Java simile, afik.

Infine ho deciso di mettere il servizio dietro un IIS. WCF usa comunque "IIS" (http.sys) iirc. Non fa una grande differenza se si lascia che l'IIS faccia ancora un po 'di più.

SBB è una delle poche librerie che consentono di farlo in modo conveniente. Hai accesso a ogni fase della negoziazione.

Una volta ho usato Delphi ed ELDOS SecureBlackbox ('before' WCF ... net 3.0) e ha funzionato in questo modo. Oggi devi fare indagini approfondite sul lato server e le persone si muovono verso approcci bilaterali.

In Java è necessario creare TrustManager che si fida semplicemente di tutto.

Penso che IIS sia l'opzione rimasta.

5

Ho trovato un modo per accettare facoltativamente certificati client SSL in WCF, ma richiede un trucco sporco. Se qualcuno ha una soluzione migliore (diversa da "Non usare WCF") mi piacerebbe sentirla.

Dopo molte scavare intorno a decompiled WCF Http classi di canale, ho imparato un paio di cose:

  1. WCF HTTP è monolitica. Ci sono classi di bezillion che volano in giro, ma tutte sono contrassegnate come "interne" e quindi inaccessibili. Lo stack di binding del canale WCF non vale la pena se si sta tentando di intercettare o estendere i comportamenti core di HTTP perché le cose che una nuova classe di binding vorrebbe maneggiare nello stack HTTP sono tutte inaccessibili.
  2. WCF si sposta su HttpListener/HTTPSYS, proprio come fa IIS.HttpListener fornisce l'accesso al certificato client SSL. Tuttavia, HTTP WCF non fornisce alcun accesso all'HttpListener sottostante.

Il punto di intercettazione più vicino ho potuto trovare è quando HttpChannelListener (classe interna) apre un canale e restituisce un IReplyChannel. IReplyChannel dispone di metodi per ricevere una nuova richiesta e tali metodi restituiscono uno RequestContext.

L'istanza dell'oggetto reale costruita e restituita dalle classi interne Http per questo RequestContext è ListenerHttpContext (classe interna). ListenerHttpContext contiene un riferimento a HttpListenerContext, che proviene dal livello pubblico System.Net.HttpListener sotto WCF.

HttpListenerContext.Request.GetClientCertificate() è il metodo necessario per verificare se è disponibile un certificato client nell'handshake SSL, caricarlo se presente o saltare se non è presente.

Sfortunatamente, il riferimento a HttpListenerContext è un campo privato di ListenerHttpContext, quindi per fare questo lavoro ho dovuto ricorrere a uno sporco trucco. Uso la riflessione per leggere il valore del campo privato in modo da poter ottenere il HttpListenerContext della richiesta corrente.

Quindi, ecco come ho fatto:

In primo luogo, creare un discendente di HttpsTransportBindingElement in modo che possiamo ignorare BuildChannelListener<TChannel> di intercettare e avvolgere l'ascoltatore di canale restituito dalla classe base:

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

Successivo , abbiamo bisogno di avvolgere il ChannelListener intercettato da quanto sopra trasporti elemento vincolante:

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

} 

Quindi, abbiamo bisogno che 01.239.da implementare IReplyChannel e intercettare le chiamate che passano un contesto di richiesta in modo che possiamo agganciare il 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); 
     } 
    } 
} 

Nel servizio web, abbiamo istituito il canale vincolante in questo modo:

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

E, infine, nel autenticatore servizio web usiamo il seguente codice per vedere se un certificato client è stato catturato dai intercettori di cui sopra:

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

Si noti che per tutto ciò funziona, HttpsTransportBindingElement.RequireClientCertificate deve essere impostato su False. Se è impostato su true, WCF accetterà solo connessioni SSL che supportano i certificati client.

Con questa soluzione, il servizio Web è completamente responsabile della convalida del certificato client. La convalida automatica del certificato di WCF non è attiva.

Constants.X509ClientCertificateMessagePropertyName è qualsiasi valore stringa che si desidera che sia. Deve essere ragionevolmente unico per evitare di scontrarsi con i nomi delle proprietà dei messaggi standard, ma dal momento che è usato solo per comunicare tra diverse parti del nostro servizio, non è necessario che sia un valore ben noto. Potrebbe essere un URN che inizia con la tua azienda o nome di dominio, o se sei veramente pigro solo un valore GUID. A nessuno importa.

Si noti che poiché questa soluzione dipende dal nome di una classe interna e di un campo privato nell'implementazione HTTP WCF, questa soluzione potrebbe non essere adatta alla distribuzione in alcuni progetti. Dovrebbe essere stabile per una determinata versione .NET, ma gli interni potrebbero facilmente cambiare nelle future versioni di .NET, rendendo questo codice inefficace.

Ancora una volta, se qualcuno ha una soluzione migliore, accolgo con favore suggerimenti.

+0

Grazie. Buono a conoscere persone come te. Questa è una soluzione interessante. Ho dato un'occhiata alle mie cartelle di archivio. Mi sbagliavo. Pensavo che potessi semplicemente inserire un'altra 'presa'. L'ho mescolato –

+0

Off topic - ma forse può aiutarti nella pratica. Portfusion. http://sourceforge.net/p/portfusion/home/PortFusion/ http://fusion.corsis.eu/ https://github.com/corsis/PortFusion#readme –

+0

Impressionante ricerca, vorrei che funzionasse fuori dalla scatola con X509CertificateValidationMode.Custom, basta passare null se non c'è alcun certificato client. – Sergii

Problemi correlati