Is there an easy way to tie custom X509 cert validation to BasicHttpBinding (or CustomHttpBinding for the same matter, which will implement transport-only security)?
EDIT1: I added a ServerCertificateValidationCallback to the code for the sake of showing that it doesn't fire up either
Here's what I'm trying to do:
1) wrote custom X509CertificateValidator:
public class MyX509Validator : X509CertificateValidator
{
public override void Validate(X509Certificate2 certificate)
{
Console.WriteLine("Incoming validation: subj={0}, thumb={1}",
certificate.Subject, certificate.Thumbprint);
}
}
2) created host:
var soapBinding = new BasicHttpBinding() { Namespace = "http://test.com" };
soapBinding.Security.Mode = BasicHttpSecurityMode.Transport;
soapBinding.Security.Transport.ClientCredentialType =
HttpClientCredentialType.Certificate;
var sh = new ServiceHost(typeof(Service1), uri);
sh.AddServiceEndpoint(typeof(IService1), soapBinding, "");
sh.Credentials.ServiceCertificate.SetCertificate(StoreLocation.LocalMachine,
StoreName.My, X509FindType.FindBySubjectName, "localhost");
sh.Credentials.ClientCertificate.Authentication.CertificateValidationMode =
System.ServiceModel.Security.X509CertificateValidationMode.Custom;
sh.Credentials.ClientCertificate.Authentication.CustomCertificateValidator =
new MyX509Validator();
System.Net.ServicePointManager.ServerCertificateValidationCallback +=
delegate(object sender, X509Certificate certificate,
X509Chain chain, SslPolicyErrors sslPolicyErrors)
{
Console.WriteLine("Incoming validation: subj={0}, thumb={1}",
certificate.Subject, certificate.GetIssuerName());
return true;
};
sh.Open();
3) Created WCF Client:
var binding = new BasicHttpBinding();
binding.Security.Mode = BasicHttpSecurityMode.Transport;
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
var cli = new ServiceReference2.Service1Client(binding,
new EndpointAddress("https://localhost:801/Service1"));
cli.ClientCredentials.ClientCertificate.SetCertificate(StoreLocation.LocalMachine,
StoreName.My, X509FindType.FindBySubjectName, "localhost");
cli.HelloWorld();
Authentication works fine, but MyX509Validator.Validate() never gets called. I have suspicion that X509CertificateValidator only works on message security, not on transport. Is that right? Is there something I could do to override transport-level cert validation?
I'm assuming that you are talking about the certificate that is tied to HTTPS...
If so, here is the answer: The X509CertificateValidationMode is used within the header of the document that you are exchanging to provide authentication. It does nothing for the underlying transport itself, which in this case is HTTPS with its' associated x.509 cert. So, you are spot on with your assumption.
If you want to provide custom certificate validation of the transport, use System.Net.ServicePointManager.ServerCertificateValidationCallback instead. Be careful though, this is an app wide setting. If you are cycling through multiple endpoints and don't set this to null, it will remain active.
I am doing this using the following settings and my custom validator definitely gets hit:
serviceHost.Credentials.ClientCertificate.Authentication.CertificateValidationMode =
X509CertificateValidationMode.Custom;
serviceHost.Credentials.ClientCertificate.Authentication.CustomCertificateValidator =
new CertificateValidator(ConfigurationManager.AppSettings["Cerficate"]);
My behaviour is:
<behavior name="CustomCertificateBehavior">
<serviceCredentials>
<clientCertificate>
<authentication certificateValidationMode="Custom" />
</clientCertificate>
</serviceCredentials>
</behavior>
</serviceBehaviors>
And my binding:
<basicHttpBinding>
<binding name="SecureBinding" maxReceivedMessageSize="5242880">
<security mode="Transport">
<transport clientCredentialType="Certificate"></transport>
</security>
</binding>
</basicHttpBinding>
Related
I have a WCF servcie set up with custom binding and a custom cert validator.
THe cert validator is defined as follows. It will be expanded later, but is just doing a basic verification currently.
public class MyX509CertificateValidator : X509CertificateValidator
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(MyX509CertificateValidator));
public MyX509CertificateValidator()
{
Logger.Info("certval - Constructor ");
}
public override void Validate(X509Certificate2 certificate)
{
Logger.Info("certval - Validate(). Calling Cert.validate()");
bool verifyResult = certificate.Verify();
Logger.Info("verify result: " + verifyResult);
if (!verifyResult)
{
throw new SecurityTokenValidationException("cert had some bad juju");
}
}
}
My web.config is set up as follows. The goal is to use Transport security and use sessions. I want the cert to be validated once, when the session is being created. However, I can see through logging in the cert validator, that the validation takes place for every service call that a client makes, when using an existing open WCF client proxy.
I've verified that my WCF service instance is created once per session (logging in the constructor is being called once per session). But, the cert validator is being called every single service calls. How can I get the cert validator to be called only at the start of a session?
Given that it appears to be using sessions, I assumed that the cert verification would be sesssion-full, and invoked just once per session. I've perused the WCF configuration documentation on MSDN and do not see a way to further customize reliableSession tag, or anything related to Security to do what I wish.
Here's the web.config and the service definition
[ServiceBehavior(AutomaticSessionShutdown = true,
InstanceContextMode = InstanceContextMode.PerSession, ConcurrencyMode = ConcurrencyMode.Multiple)]
public class WcfBasicService : IWcfBasicService
{
...
<system.serviceModel>
<bindings>
<customBinding>
<binding name="reliableSessionOverHttps">
<reliableSession/>
<security authenticationMode="CertificateOverTransport"/>
<httpsTransport />
</binding>
</customBinding>
</bindings>
<services>
<service name="WcfServiceLibrary1.WcfBasicService">
<endpoint address="" binding="customBinding" contract="WcfServiceLibrary1.IWcfBasicService" name="mainEndpoint"
bindingConfiguration="reliableSessionOverHttps">
<identity>
<dns value="localhost" />
</identity>
</endpoint>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceCredentials>
<clientCertificate>
<authentication certificateValidationMode="Custom" customCertificateValidatorType="WcfServiceLibrary1.MyX509CertificateValidator, WcfServiceLibrary1" />
</clientCertificate>
</serviceCredentials>
<!-- To avoid disclosing metadata information,
set the values below to false before deployment -->
<serviceMetadata httpGetEnabled="True" httpsGetEnabled="True" />
<!-- To receive exception details in faults for debugging purposes,
set the value below to true. Set to false before deployment
to avoid disclosing exception information -->
<serviceDebug includeExceptionDetailInFaults="True" />
</behavior>
</serviceBehaviors>
</behaviors>
</system.serviceModel>
AFAIK there is no any good WCF configuration solution for that, but you may implement some sort of cache inside Validate method, using Thumbprint property of the certificate (Thumbprint is actually hash of the certificate body):
public class MyX509CertificateValidator : X509CertificateValidator
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(MyX509CertificateValidator));
private string lastValidCertTumbprint = null;
public MyX509CertificateValidator()
{
Logger.Info("certval - Constructor ");
}
public override void Validate(X509Certificate2 certificate)
{
if ((lastValidCertTumbprint != null) && (certificate.Tumbprint == lastValidCertTumbprint))
{
return; // Fast track
}
Logger.Info("certval - Validate(). Calling Cert.validate()");
bool verifyResult = certificate.Verify();
Logger.Info("verify result: " + verifyResult);
if (!verifyResult)
{
throw new SecurityTokenValidationException("cert had some bad juju");
}
// The cert valid, save this fact into fast track cache
lastValidCertTumbprint = certificate.Tumbprint;
}
}
I assume that session duration is far less then certificate lifetime and in case certificate(s) revoked, you have other means to terminate the sessions :)
To made things better, you may add some sort of timestamp of the last validation call and re-validate certificate if reasonable timeout (say, 30min) expired:
public class MyX509CertificateValidator : X509CertificateValidator
{
private static readonly ILog Logger = LogManager.GetLogger(typeof(MyX509CertificateValidator));
private string lastValidCertTumbprint = null;
private Stopwatch lastValidCertTimeMarker = new Stopwatch();
private const int VALIDATION_CACHE_LIFETIME = 30*60*1000; // in ms // 30min
public MyX509CertificateValidator()
{
Logger.Info("certval - Constructor ");
}
public override void Validate(X509Certificate2 certificate)
{
if ((lastValidCertTumbprint != null)
&& (certificate.Tumbprint == lastValidCertTumbprint)
&& (lastValidCertTimeMarker.ElapsedMilliseconds < VALIDATION_CACHE_LIFETIME))
{
return; // Fast track
}
lastValidCertTumbprint = null;
Logger.Info("certval - Validate(). Calling Cert.validate()");
bool verifyResult = certificate.Verify();
Logger.Info("verify result: " + verifyResult);
if (!verifyResult)
{
throw new SecurityTokenValidationException("cert had some bad juju");
}
// The cert valid, save this fact into fast track cache and save timestamp
lastValidCertTumbprint = certificate.Tumbprint;
lastValidCertTimeMarker.Reset();
lastValidCertTimeMarker.Start();
}
}
I have a WCF service setup with Messager security using certificates. I am using self-signed certificates for the moment. I do not use the certificate store, but instead load my certificates on the fly.
The service works when I use MessageSecurity=None, but with security turned on, as soon as I make a call to a service endpoint, I get the error:
An unsecured or incorrectly secured fault was received from the other party. See the inner FaultException for the fault code and detail. --> At least one security token of the message could not be validated.
The relevant sections of my service configuration look like this:
<endpoint name="DB_msgAnonymousSecured" address="Secure"
binding="wsHttpBinding" bindingConfiguration="UploadServiceSecure"
contract="DataStoreInterfaces.IDataStoreServices" >
</endpoint>
<wsHttpBinding>
<binding name="UploadServiceSecure" >
<security mode="Message">
<!-- Message security with certificate authentication-->
<message clientCredentialType="Certificate"
negotiateServiceCredential="false" establishSecurityContext="false"/>
</security>
</binding>
</wsHttpBinding>
The certificates are configured in the code like this:
Server:
using (WebServiceHost queryHost = new WebServiceHost(typeof(DataStoreServices.DatabaseServicesImplementation)))
{
try
{
queryHost.Credentials.ServiceCertificate.Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(Application.StartupPath + "\\Server.pfx");
queryHost.Credentials.ClientCertificate.Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(Application.StartupPath + "\\Client.cer");
queryHost.Open();
while (true) //endless loop
{
Thread.Sleep(100);
}
}
catch (Exception ex)
{
}
}
Client:
var binding = new WSHttpBinding();
binding.Security.Mode = SecurityMode.Message;
binding.Security.Message.ClientCredentialType = MessageCredentialType.Certificate;
binding.Security.Message.EstablishSecurityContext = false;
binding.Security.Message.NegotiateServiceCredential = false;
EndpointAddress address = new EndpointAddress(new Uri(serviceEndpointAddress), EndpointIdentity.CreateDnsIdentity("CompanyXYZ Server")); //usually, the DnsIdentity is the domain name. In our case, it's the value that was entered during the creation of the self-signed certificate.
ChannelFactory<IDataStoreServices> cf = new ChannelFactory<IDataStoreServices>(binding, address);
cf.Credentials.ServiceCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.None; //since our certificate is self-signed, we don't want it to be validated
cf.Credentials.ClientCertificate.Certificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(Environment.CurrentDirectory + "\\Client.pfx");
cf.Credentials.ServiceCertificate.DefaultCertificate = new System.Security.Cryptography.X509Certificates.X509Certificate2(Environment.CurrentDirectory + "\\Server.cer");
IDataStoreServices channel = cf.CreateChannel();
response = channel.TestConnection(); //this line causes the exception
I have been struggling with this issue for weeks now and I am out of ideas.
Does anybody have any hints for me why this isn't working?
So, I noticed FaultException is not giving me the proper result when I use the BasicHttpBinding. When I use WSHttpBinding it works file.
The issue is, From WCF Service if I throw the FaultException like below,
var translations = new List<FaultReasonText> { new FaultReasonText("FaultReasonText 1"), new FaultReasonText("FaultReasonText 2") };
throw new FaultException<MessageServiceFault>(MessageServiceFault.Fault1, new FaultReason(translations));
When it reaches to the client the fault.Reason.Translations count is 1. That means the first one (FaultReasonText 1) only is getting back to client.
But when I use WSHttpBinding the count is 2. Where the issue is? Can anyone help me on this.
It gives me different result when I test the below code with BasicHttpBinding & WSHttpBinding bindings.
class Program
{
static void Main(string[] args)
{
try
{
string baseAddress = "http://" + Environment.MachineName + ":8000/Service";
ServiceHost host = new ServiceHost(typeof(MessageService), new Uri(baseAddress));
host.AddServiceEndpoint(typeof(IMessageService), new WSHttpBinding(), "");
host.Open();
Console.WriteLine("Host opened");
ChannelFactory<IMessageService> myChannelFactory = new ChannelFactory<IMessageService>(new WSHttpBinding(), new EndpointAddress(baseAddress));
IMessageService channel = myChannelFactory.CreateChannel();
var response = channel.GetMessage();
}
catch (FaultException fault)
{
fault.Reason.Translations.ToList().ForEach(i => Console.WriteLine(i.Text));
Console.WriteLine(false);
}
}
}
[ServiceContract]
public interface IMessageService
{
[OperationContract]
[FaultContract(typeof(MessageServiceFault))]
string GetMessage();
}
public class MessageService : IMessageService
{
public string GetMessage()
{
var translations = new List<FaultReasonText> { new FaultReasonText("FaultReasonText 1"), new FaultReasonText("FaultReasonText 2") };
throw new FaultException<MessageServiceFault>(MessageServiceFault.Fault1, new FaultReason(translations));
}
}
[DataContract]
public enum MessageServiceFault
{
[EnumMember]
Fault1,
[EnumMember]
Fault2
}
EDIT:
But, this article says, You can supply a number of different text strings that get picked from depending on the user's language settings. The Translations bucket holds all of the different text strings and their associated cultural identifiers (tied together by a FaultReasonText). When no culture is specified for a fault reason or a translation search, the assumed culture is the current thread culture. For example, if you want a translation to "en-UK", we'll first look for "en-UK" and then we'll look for "en". If we still can't find a match, then we'll take the first translation in the list, which could be anything.
If so, Why in case of WsHttpBinding it returns me the 2 FaultReasonText ?
To use FaultException, you need to activate SOAP 1.2 on your web service.
BasicHttpBinding uses SOAP 1.1, WSHttpBinding uses SOAP 1.2. That's why it works with WSHttpBinding and not with BasicHttpBinding.
Instead of using BasicHttpBinding, you should better use customBindings, with textMessageEncoding and httpTransport :
<customBinding>
<binding name="simpleBinding">
<textMessageEncoding messageVersion="Soap12" writeEncoding="utf-8" />
<httpTransport />
</binding>
</customBinding>
If you convert a default basicHttpBinding with this tool : you will obtain :
<!-- generated via Yaron Naveh's http://webservices20.blogspot.com/ -->
<customBinding>
<binding name="NewBinding0">
<textMessageEncoding MessageVersion="Soap11" />
<httpTransport />
</binding>
</customBinding>
<!-- generated via Yaron Naveh's http://webservices20.blogspot.com/ -->
Source binding :
<bindings>
<basicHttpBinding>
<binding name="NewBinding0" />
</basicHttpBinding>
</bindings>
Try to activate SOAP 12 to your service, and it will work
We're developing a Silverlight Client onto a server-based API exposed via WCF.
I'm trying to move my WCF client code (which works fine) from a configuration-based model to a programmatic model. This will enable me to have a single "root" URL which I can apply at start-up and not require installations to have to maintain humongous configuration files.
I'm stuggling converting my configurations to Silverlight-capable code, though.
Where I have the configuration below for one of my services:
<configuration>
<system.serviceModel>
<bindings>
<customBinding>
<binding name="CustomBinding_ISilverlightHelper">
<binaryMessageEncoding />
<httpTransport maxReceivedMessageSize="2147483647" maxBufferSize="2147483647">
<extendedProtectionPolicy policyEnforcement="Never" />
</httpTransport>
</binding>
</customBinding>
</bindings>
<client>
<endpoint address="http://localhost:50072/API/WCF/Silverlight/SilverlightHelper.svc"
binding="customBinding" bindingConfiguration="CustomBinding_ISilverlightHelper"
contract="API.WCF.Silverlight.ISilverlightHelper" name="CustomBinding_ISilverlightHelper" />
</client>
</system.serviceModel>
</configuration>
I can't figure out how to create the equivelant client-config code. At the moment I have:
CustomBinding customBinding = new CustomBinding();
// I see I need to do something with customBinding but the properties don't seem
// logical
// I have used BasicHttpBinding, but it just returns with "Not Found" (the service does resolve to a valid URL)
BasicHttpBinding basicHttpBinding = new BasicHttpBinding() { MaxBufferSize = int.MaxValue, MaxReceivedMessageSize = int.MaxValue };
EndpointAddress endpointAddress = new EndpointAddress("http://localhost:50072/API/WCF/Silverlight/SilverlightHelper.svc");
ISilverlightHelper silverlightHelper= new ChannelFactory<ISilverlightHelper>(basicHttpBinding, endpointAddress).CreateChannel();
AsyncCallback asyncCallback = delegate(IAsyncResult result)
{
ISilverlightHelper asyncSilverlightHelper = (ISilverlightHelper)result.AsyncState;
string[] files=asyncSilverlightHelper.EndGetPlugInXapNames(result).ToArray();
};
silverlightHelper.BeginGetPlugInXapNames(asyncCallback, silverlightHelper);
Any clues would be appreciated. I've spent all morning Googling/Binging/Overflowing but haven't come across this scenario. Or I might be just so far wrong ...
Sorted it.
I created the BinaryMessageEncodingBindingElement and HttpTransportBindingElements, added them to the CustomBinding and it all works.
Here's my annotated code:
// create the binding elements
BinaryMessageEncodingBindingElement binaryMessageEncoding = new BinaryMessageEncodingBindingElement();
HttpTransportBindingElement httpTransport = new HttpTransportBindingElement() { MaxBufferSize = int.MaxValue, MaxReceivedMessageSize = int.MaxValue };
// add the binding elements into a Custom Binding
CustomBinding customBinding = new CustomBinding(binaryMessageEncoding,httpTransport);
// create the Endpoint URL (I'll use a configured URL later - all web services will then move as one)
EndpointAddress endpointAddress = new EndpointAddress("http://localhost:50072/API/WCF/Silverlight/SilverlightHelper.svc");
// create an interface for the WCF service
ISilverlightHelper silverlightHelper= new ChannelFactory<ISilverlightHelper>(customBinding, endpointAddress).CreateChannel();
// set-up the asynchronous callback
AsyncCallback asyncCallback = delegate(IAsyncResult result)
{
ISilverlightHelper asyncSilverlightHelper = (ISilverlightHelper)result.AsyncState;
string[] files=asyncSilverlightHelper.EndGetPlugInXapNames(result).ToArray();
};
// execute the call
silverlightHelper.BeginGetPlugInXapNames(asyncCallback, silverlightHelper);
I'm trying go get WCF server and client mutually authenticate each other using SSL certificates on transport level using BasicHttpBinding. Here's how the server is getting created:
var soapBinding = new BasicHttpBinding() { Namespace = "http://test.com" };
soapBinding.Security.Mode = BasicHttpSecurityMode.Transport;
soapBinding.Security.Transport.ClientCredentialType =
HttpClientCredentialType.Certificate;
var sh = new ServiceHost(typeof(Service1), uri);
sh.AddServiceEndpoint(typeof(IService1), soapBinding, "");
sh.Credentials.ServiceCertificate.SetCertificate(
StoreLocation.LocalMachine, StoreName.My,
X509FindType.FindBySubjectName, "localhost");
sh.Open();
Here's the client:
var binding = new BasicHttpBinding();
binding.Security.Mode = BasicHttpSecurityMode.Transport;
var service = new ServiceReference2.Service1Client(binding,
new EndpointAddress("https://localhost:801/Service1"));
service.ClientCredentials.ClientCertificate.SetCertificate(
StoreLocation.LocalMachine, StoreName.My,
X509FindType.FindBySubjectName, "localhost");
service.ClientCredentials.ServiceCertificate.Authentication.
CertificateValidationMode =
System.ServiceModel.Security.X509CertificateValidationMode.PeerTrust;
service.HelloWorld();
Certificate for localhost is in Personal, Trusted Root and Trusted 3rd Party containers. Internet Explorer can connect to host and see WSDL. Also, SSL calls work fine with ClientCredentialType = HttpClientCredentialType.None
HelloWorld() fails with:
System.ServiceModel.Security.MessageSecurityException occurred<br/>
Message="The HTTP request was forbidden with client authentication
scheme 'Anonymous'."
which is a rethrown exception from: "The remote server returned an error: (403) Forbidden."
how does one go around figuring out wtf is going on?
Try adding this in the client just after setting Security.Mode:
binding.Security.Transport.ClientCredentialType = HttpClientCredentialType.Certificate;
If you use standard generated proxy class you can to set transport client credential type to Certificate in App.Config:
<binding name="SpoDataServiceSoap">
<security mode="Transport">
<transport clientCredentialType="Certificate"></transport>
</security>
</binding>
C#
var client = new MyServiceSoapClient()
X509Certificate2 cert = CertificateHelper.GetClientCertificate();
client.ClientCredentials.ClientCertificate.Certificate = cert;