My team is developing a number of WPF plug-ins for a 3rd party thick client application. The WPF plug-ins use WCF to consume web services published by a number of TIBCO serv
I'm the guy who got Kane (our SO lackey!) to ask the original question. I thought I'd finally create an account and post our findings / results / experiences in regards to the answer posted by Aaronaught (so any credit to him above).
We tried adding a custom behaviour as suggested above and setting the behaviourConfiguration on the endpoint configuration element to use it. We couldn't get the code to fire at all so ended up going with a programmatic approach.
As we had a wrapper class set up to build a ClientBase object we used our existing creation functions to add the behaviour after building all the other parts of the ClientBase.
We ran into a few issues doing this also, namely that a ClientCredentials behaviour was already being defined for our ClientBase authenticating with a Username and Password rather than our Certificate + Username and Password. So we removed the existing behaviour programmatically before adding our new certificate based behaviour (with the Username and Password injected) as a temporary measure for testing. Still no dice, our behaviour was being constructed and ApplyClientBehavior was being fired but the service was still falling over when Invoke was called (we never got the real Exception due to a bunch of using statements that were difficult to refactor out).
We then decided instead of removing the existing ClientCredentials behaviour that we would just inject our certificate into it before letting the whole lot procede as normal. Third times a charm and it's all up and working now.
I'd like to thank Aaronaught (and I would vote up if I could!) for putting us on the right trail and providing a well thought out and useful answer.
Heres a small code snippet of it up and running (using a test .CRT file).
protected override ClientBase<TChannel> CreateClientBase(string endpointConfigurationName)
{
ClientBase<TChannel> clientBase = new ClientBase<TChannel>(endpointConfigurationName); // Construct yours however you want here
// ...
ClientCredentials credentials = clientBase.Endpoint.Behaviors.Find<ClientCredentials>();
X509Certificate2 certificate = new X509Certificate2();
byte[] rawCertificateData = File.ReadAllBytes(@"C:\Path\To\YourCert.crt");
certificate.Import(rawCertificateData);
credentials.ClientCertificate.Certificate = certificate;
return clientBase;
}
As another side note, as part of testing we removed all our certificates from the local machine store, this actually caused a problem using Fiddler. Fiddler didn't detect our client certificate because it was purely in memory and not in the the trusted store. If we added it back in to the trusted store then Fiddler started to play nice again.
Thanks again.
Aaronaught had the right idea, but I had to make a few modification to get it to work. The implementation I used follows. I added onto it a bit more with the ability to get a certificate from an embedded resource.
using System.IO;
using System.Linq;
using System.Reflection;
using System.Security.Cryptography.X509Certificates;
using System.ServiceModel.Configuration;
using System.Configuration;
using System.ServiceModel.Description;
namespace System.ServiceModel.Description
{
/// <summary>
/// Uses a X509 certificate from disk as credentials for the client.
/// </summary>
public class ClientCertificateCredentialsFromFile : ClientCredentials
{
public ClientCertificateCredentialsFromFile(CertificateSource certificateSource, string certificateLocation)
{
if (!Enum.IsDefined(typeof(CertificateSource), certificateSource)) { throw new ArgumentOutOfRangeException(nameof(certificateSource), $"{nameof(certificateSource)} contained an unexpected value."); }
if (string.IsNullOrWhiteSpace(certificateLocation)) { throw new ArgumentNullException(nameof(certificateLocation)); }
_certificateSource = certificateSource;
_certificateLocation = certificateLocation;
ClientCertificate.Certificate = certificateSource == CertificateSource.EmbeddedResource ?
GetCertificateFromEmbeddedResource(certificateLocation)
: GetCertificateFromDisk(certificateLocation);
}
/// <summary>
/// Retrieves a certificate from an embedded resource.
/// </summary>
/// <param name="certificateLocation">The certificate location and assembly information. Example: The.Namespace.certificate.cer, Assembly.Name</param>
/// <returns>A new instance of the embedded certificate.</returns>
private static X509Certificate2 GetCertificateFromEmbeddedResource(string certificateLocation)
{
X509Certificate2 result = null;
string[] parts = certificateLocation.Split(new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries);
if (parts.Length < 2) { throw new ArgumentException($"{certificateLocation} was expected to have a format of namespace.resource.extension, assemblyName"); }
string assemblyName = string.Join(",", parts.Skip(1));
var assembly = Assembly.Load(assemblyName);
using (var stream = assembly.GetManifestResourceStream(parts[0]))
{
var bytes = new byte[stream.Length];
stream.Read(bytes, 0, bytes.Length);
result = new X509Certificate2(bytes);
}
return result;
}
/// <summary>
/// Retrieves a certificate from disk.
/// </summary>
/// <param name="certificateLocation">The file path to the certificate.</param>
/// <returns>A new instance of the certificate from disk</returns>
private static X509Certificate2 GetCertificateFromDisk(string certificateLocation)
{
if (!File.Exists(certificateLocation)) { throw new ArgumentException($"File {certificateLocation} not found."); }
return new X509Certificate2(certificateLocation);
}
/// <summary>
/// Used to keep track of the source of the certificate. This is needed when this object is cloned.
/// </summary>
private readonly CertificateSource _certificateSource;
/// <summary>
/// Used to keep track of the location of the certificate. This is needed when this object is cloned.
/// </summary>
private readonly string _certificateLocation;
/// <summary>
/// Creates a duplicate instance of this object.
/// </summary>
/// <remarks>
/// A new instance of the certificate is created.</remarks>
/// <returns>A new instance of <see cref="ClientCertificateCredentialsFromFile"/></returns>
protected override ClientCredentials CloneCore()
{
return new ClientCertificateCredentialsFromFile(_certificateSource, _certificateLocation);
}
}
}
namespace System.ServiceModel.Configuration
{
/// <summary>
/// Configuration element for <see cref="ClientCertificateCredentialsFromFile"/>
/// </summary>
/// <remarks>
/// When configuring the behavior an extension has to be registered first.
/// <code>
/// <![CDATA[
/// <extensions>
/// <behaviorExtensions>
/// <add name = "clientCertificateCredentialsFromFile"
/// type="System.ServiceModel.Configuration.ClientCertificateCredentialsFromFileElement, Assembly.Name" />
/// </behaviorExtensions>
/// </extensions>
/// ]]>
/// </code>
/// Once the behavior is registered it can be used as follows.
/// <code>
/// <![CDATA[
/// <behaviors>
/// <endpointBehaviors>
/// <behavior name = "BehaviorConfigurationName" >
/// <clientCertificateCredentialsFromFile fileLocation="C:\certificates\paypal_cert.cer" />
/// </behavior>
/// </endpointBehaviors>
/// </behaviors>
/// <client>
/// <endpoint address="https://endpoint.domain.com/path/" behaviorConfiguration="BehaviorConfigurationName" ... />
/// </client>
/// ]]>
/// </code>
/// </remarks>
public class ClientCertificateCredentialsFromFileElement : BehaviorExtensionElement
{
/// <summary>
/// Creates a new <see cref="ClientCertificateCredentialsFromFile"/> from this configuration element.
/// </summary>
/// <returns>The newly configured <see cref="ClientCertificateCredentialsFromFile"/></returns>
protected override object CreateBehavior()
{
return new ClientCertificateCredentialsFromFile(Source, Location);
}
/// <summary>
/// Returns <code>typeof(<see cref="ClientCertificateCredentialsFromFile"/>);</code>
/// </summary>
public override Type BehaviorType
{
get
{
return typeof(ClientCertificateCredentialsFromFile);
}
}
/// <summary>
/// An attribute used to configure the file location of the certificate to use for the client's credentials.
/// </summary>
[ConfigurationProperty("location", IsRequired = true)]
public string Location
{
get
{
return this["location"] as string;
}
set
{
this["location"] = value;
}
}
/// <summary>
/// An attribute used to configure where the certificate should should be loaded from.
/// </summary>
[ConfigurationProperty("source", IsRequired = true)]
public CertificateSource Source
{
get
{
return (CertificateSource)this["source"];
}
set
{
this["source"] = value;
}
}
}
/// <summary>
/// Used to declare the source of a certificate.
/// </summary>
public enum CertificateSource
{
FileOnDisk,
EmbeddedResource
}
}
using the above code I was able to configure my client as follows
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="clientCertificateCredentialsFromFile"
type="System.ServiceModel.Configuration.ClientCertificateCredentialsFromFileElement, My.Project.PayPal" />
</behaviorExtensions>
</extensions>
<bindings>
<basicHttpBinding>
<binding name="PayPalAPISoapBinding">
<security mode="Transport">
<transport clientCredentialType="Certificate" />
</security>
</binding>
<binding name="PayPalAPIAASoapBinding">
<security mode="Transport">
<transport clientCredentialType="Certificate" />
</security>
</binding>
</basicHttpBinding>
</bindings>
<behaviors>
<endpointBehaviors>
<behavior name="PayPalAPICredentialBehavior">
<clientCertificateCredentialsFromFile source="EmbeddedResource" location="My.Project.PayPal.Test.Integration.paypal_cert.cer, My.Project.PayPal.Test.Integration" />
</behavior>
<behavior name="PayPalAPIAACredentialBehavior">
<clientCertificateCredentialsFromFile source="EmbeddedResource" location="My.Project.PayPal.Test.Integration.paypal_cert.cer, My.Project.PayPal.Test.Integration" />
</behavior>
</endpointBehaviors>
</behaviors>
<client>
<endpoint
address="https://api.sandbox.paypal.com/2.0/"
behaviorConfiguration="PayPalAPICredentialBehavior"
binding="basicHttpBinding"
bindingConfiguration="PayPalAPISoapBinding"
contract="My.Project.PayPal.Proxy.PayPalAPIInterface"
name="PayPalAPI" />
<endpoint
address="https://api-aa.sandbox.paypal.com/2.0/"
behaviorConfiguration="PayPalAPIAACredentialBehavior"
binding="basicHttpBinding"
bindingConfiguration="PayPalAPIAASoapBinding"
contract="My.Project.PayPal.Proxy.PayPalAPIAAInterface"
name="PayPalAPIAA" />
</client>
</system.serviceModel>
</configuration>
It is possible. We do something similar with Mutual Certificate Auth - the service certificate and in some cases the client certificate are picked up from a central authority as part of an auto-discovery/single-sign-on mechanism.
It's not entirely clear in what context the certificate will be used, but in all cases what you need to do is define your own behavior and behavior element deriving from the particular behavior/element in the System.ServiceModel.Description
namespace that takes the certificate. I'll assume for the time being that it's a client credential. First you have to write the behaviour, which goes something like this:
public class MyCredentials : ClientCredentials
{
public override void ApplyClientBehavior(ServiceEndpoint endpoint,
ClientRuntime behavior)
{
// Assuming GetCertificateFromNetwork retrieves from CDS
ClientCertificate.Certificate = GetCertificateFromNetwork();
}
protected override ClientCredentials CloneCore()
{
// ...
}
}
Now you need to create an element that can go in the XML configuration:
public class MyCredentialsExtensionElement : ClientCredentialsElement
{
protected override object CreateBehavior()
{
return new MyCredentials();
}
public override Type BehaviorType
{
get { return typeof(MyCredentials); }
}
// Snip other overrides like Properties
}
After this you can add the policy to your WCF config:
<behaviors>
<endpointBehaviors>
<behavior name="MyEndpointBehavior">
<myCredentials/>
</behavior>
</endpointBehaviors>
</behaviors>
Edit: Almost forgot to mention, you need to register the extension:
<system.serviceModel>
<extensions>
<behaviorExtensions>
<add name="myCredentials"
type="MyAssembly.MyCredentialsExtensionElement, MyAssembly,
Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" />
</behaviorExtensions>
</extensions>
</system.serviceModel>
Hope that helps. If you need more details on the arrangement of all of these classes and what's going on behind the scenes, try reading Extending WCF with Custom Behaviors.