2013-09-06 6 views
5

HTTP 기본 인증 또는 클라이언트 인증서 자격 증명으로 요청을 수락 할 수있는 자체 호스팅 WCF 서비스에서 단일 SSL 끝점을 갖고 싶습니다.자체 호스팅 WCF 서비스에서 클라이언트 인증서를 선택적으로 허용합니다.

IIS에서 호스팅하는 서비스의 경우 IIS는 "클라이언트 인증서 수락"과 "클라이언트 인증서 필요"를 구별합니다.

WCF의 WebHttpBinding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;은 IIS의 "인증서 필요"설정과 유사합니다.

WCF 자체 호스팅 서비스를 구성하여 클라이언트 인증서 자격증을 허용하지만 모든 클라이언트에서 요구하지 않는 방법이 있습니까? 자체 호스팅 WCF 서비스에 대해 "클라이언트 인증서 수락"IIS의 WCF 아날로그가 있습니까?

답변

0

나는 그것이 작동하지 않는다고 생각합니다.

빈 인증서가 만들어 지거나 인증서에 대한 할당되지 않은 참조가 허용되도록 클라이언트에 영향을 줄 수없는 경우 서버 측에서이 특별한 경우의 유효성을 검사하고 로그 파일에 로그하면 아무런 변화가 없습니다. IIS 동작을 모방해야하며 이전에 확인해야합니다. 그것은 추측입니다. 전문 지식이 없습니다.

은 당신이 보통 일은 에) 인증서 체인을 통해 도보로 인증서의 유효성을 검사하려고 더블, 트리플 클라이언트를 확인하고 발생을 기록 제공되지 인증서의 경우) B 제공을합니다.

'.net'은 협상을 제어 할 기회를주지 않는다고 생각합니다.

이모는 중간에있는 사람에게 문을 열어줍니다. 그것이 MS가 Java와 afik를 비슷하게 허용하지 않는 이유입니다.

마지막으로 IIS 뒤에 서비스를 배치하기로 결정했습니다. WCF는 어쨌든 iirc 'IIS'(http.sys)를 사용합니다. IIS가 좀 더 많은 작업을하도록한다면 큰 차이가 없습니다.

SBB는 편리한 방식으로이를 수행 할 수있는 몇 가지 라이브러리 중 하나입니다. 협상의 모든 단계에 액세스 할 수 있습니다.

일단 내가 Delphi와 ELDOS SecureBlackbox ('before'WCF ... net 3.0)를 사용하면 그런 식으로 작동합니다. 오늘날 서버 측에 대한 광범위한 조사가 필요하며 사람들은 양면 접근 방식으로 이동합니다.

자바에서는 모든 것을 신뢰하는 TrustManager를 만들어야합니다.

IIS가 왼쪽 옵션이라고 생각합니다.

5

WCF에서 SSL 클라이언트 인증서를 선택적으로 허용하는 방법을 찾았지만 더러운 트릭이 필요합니다. 누군가 "WCF를 사용하지 마십시오"이외의 더 나은 솔루션을 가지고 있다면 나는 그것을 듣고 싶습니다. 많은에 디 컴파일 WCF HTTP를 채널 클래스, 내가 배운 주위에 몇 가지 파고 후

:

  1. WCF HTTP는 모 놀리 식이다. bezillion 클래스가 주위를 날고 있지만, 모두 "내부"라고 표시되어 액세스 할 수 없습니다. 새로운 바인딩 클래스가 HTTP 스택에서 피델을 사용하기를 원할 것이기 때문에 핵심 HTTP 동작을 가로 채거나 확장하려는 경우 WCF 채널 바인딩 스택은 가치가 없습니다.
  2. IIS와 마찬가지로 WCF가 HttpListener/HTTPSYS 위에 놓입니다.HttpListener는 SSL 클라이언트 인증서에 대한 액세스를 제공합니다. WCF HTTP는 기본 HttpListener에 대한 액세스를 제공하지 않습니다.

가장 가까운 차단 점은 HttpChannelListener (내부 클래스)이 채널을 열고 IReplyChannel을 반환하는 것입니다. IReplyChannel에는 새 요청을 수신하는 메소드가 있으며 해당 메소드는 RequestContext을 리턴합니다.

RequestContext에 대한 HTTP 내부 클래스에 의해 생성되고 반환 된 실제 객체 인스턴스는 ListenerHttpContext (내부 클래스)입니다. ListenerHttpContext에는 HttpListenerContext에 대한 참조가 있으며, 여기에는 공개 System.Net.HttpListener WCF 아래의 계층이 있습니다.

HttpListenerContext.Request.GetClientCertificate()은 SSL 핸드 셰이크에서 사용할 수있는 클라이언트 인증서가 있는지 확인하고 필요하면로드하거나없는 경우 건너 뜁니다.

불행히도 HttpListenerContext에 대한 참조는 ListenerHttpContext의 비공개 필드이므로이 작업을 수행하려면 더러운 트릭을 사용해야합니다. 리플렉션을 사용하여 비공개 필드의 값을 읽으면 현재 요청의 HttpListenerContext에 도착할 수 있습니다.

그래서, 여기에 어떻게 내가 해냈어 :

첫째, 그래서 우리는 기본 클래스에 의해 반환 된 채널 수신기를 차단하고 포장 BuildChannelListener<TChannel>를 오버라이드 (override) 할 수 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; } 
     } 
    } 

} 

다음으로 우리는,그래서 우리는 HttpListenerContext 암초에 걸릴 수 있습니다 요청 컨텍스트를 통과 IReplyChannel와 절편 통화 구현 :

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

그리고 마지막으로 : 웹 서비스에서

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

을, 우리는 다음과 같이 채널 바인딩을 설정 웹 서비스 인증 자에서 다음 코드를 사용하여 클라이언트 인증서가 위의 인터셉터에 의해 캡처되었는지 확인합니다.

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

이 작업을 수행하려면 HttpsTransportBindingElement.RequireClientCertificate을 False로 설정해야합니다. true로 설정하면 WCF는 클라이언트 인증서가있는 SSL 연결 만 수락합니다.

이 솔루션을 사용하면 웹 서비스가 클라이언트 인증서의 유효성을 완전히 확인해야합니다. 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