I've spent some time dealing with WCF securing with certificates and came to a solution that I want to share. As you probably know, WCF supports certificate authentication and it's not so hard to set up. However, you will need to install certificates on both the service machine and the caller machine. This can be a problem if you want to host the service on a shared hosting environment for example. Even if the service is hosted on a machine in your network, you will still need some permissions to be given to the service application pool user in order to access the certificate private key.
So with the help of this blog post, I found a way to create Self Signed certificate using some Windows native methods.
using System;
using System.Runtime.InteropServices;
namespace Certificate.Native
{
internal static class NativeMethods
{
[DllImport("kernel32.dll", SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool FileTimeToSystemTime(
[In] ref long fileTime,
out SystemTime systemTime);
[DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptAcquireContextW(
out IntPtr providerContext,
[MarshalAs(UnmanagedType.LPWStr)] string container,
[MarshalAs(UnmanagedType.LPWStr)] string provider,
int providerType,
int flags);
[DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptReleaseContext(
IntPtr providerContext,
int flags);
[DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptGenKey(
IntPtr providerContext,
int algorithmId,
int flags,
out IntPtr cryptKeyHandle);
[DllImport("AdvApi32.dll", SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CryptDestroyKey(
IntPtr cryptKeyHandle);
[DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
public static extern IntPtr CertCreateSelfSignCertificate(
IntPtr providerHandle,
[In] ref CryptoApiBlob subjectIssuerBlob,
int flags,
[In] ref CryptKeyProviderInformation keyProviderInformation,
IntPtr signatureAlgorithm,
[In] ref SystemTime startTime,
[In] ref SystemTime endTime,
IntPtr extensions);
[DllImport("Crypt32.dll", SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool CertFreeCertificateContext(
IntPtr certificateContext);
}
}
I have created CertificateSerializer
to serialize the certificate to base64string
:
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
namespace Certificate
{
public class CertificateSerializer
{
public X509Certificate2 Deserialize(string certificateString)
{
byte[] numArray = Convert.FromBase64String(certificateString);
string tempFileName = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName());
try
{
File.WriteAllBytes(tempFileName, numArray);
X509Certificate2 certificate = new X509Certificate2(tempFileName,
default(string), X509KeyStorageFlags.Exportable);
return certificate;
}
finally
{
File.Delete(tempFileName);
}
}
public string Serialize(X509Certificate2 certificate)
{
byte[] numArray = certificate.Export(X509ContentType.Pfx);
string base64String = Convert.ToBase64String(numArray);
return base64String;
}
}
}
In the configuration section, we can store the serialized certificate and the Thumbprints and Subjects of the trusted certificates, so that we can give the service the information about the trusted parties. Here's an example of the custom section content:
<configSections>
<section name="certificateSection"
type="Certificate.Configuration.CertificatesSection, Certificate"/>
</configSections>
<certificateSection certificate="MIIExgIBAzCCBIYGCSqGSIb3DQEHAaCCBHcEggRzMIIEbzCCApgG
CSqGSIb3DQEHAaCCAokEggKFMIICgTCCAn0GCyqGSIb3DQEMCgECoIIBjjCCAYowHAYKKoZIhvcNAQwB
AzAOBAgbm3IPSqU0BQICB9AEggFojvCBmeSJ6n4IlKxgSv1XgIB5LaD7tb06f/yTLsZRK+4rnqCaesm
YFFHP889JTySdqPPyE6fNrpFXTcvcRC6lQQglLnxbRZQotPHvDv4MEzEdI67zkkfM7RsxxXRUQE+ex
5H+oQxjScvVRWlKa0KXLk7DOa+Ijz/epLFCum2CE2aUE/AOdi8GCYf7D0yMa472/buQRX1qWX5M
YuH+sZI1py/unS8R5R4cytRr8dKJHmn3YtLuhEwQOuXiQ/mUK5PKj+xYp6b8ssVIXQjuLpXZnnT
7i/KdZipxmTCf+OtXbAysBw2VaQ9+NmR8cufy8nUb/KgSNfcE3hTHTxIaBnhddhuHxvfR5oYIAz
PK3NTq/S1qCEqxDJnBFapdnRcKfHEAlDwIB/KZyHgKdVBiu16pB9e+bxl840CW6vI/tILBbpww3
rjvzKKQYZZ6uPu1oNLS2TeX7JsBJE3p0HJE2DPFLfmXLPVPSkHBMYHbMBMGCSqGSIb3DQEJFTEG
BAQBAAAAMFcGCSqGSIb3DQEJFDFKHkgAZgAzADEAMwBlADQAYQA4AC0AMAA2ADMANwAtADQANAA1
ADAALQBiAGMANAA5AC0AMwA2AGEAMABkAGIAOQAxADkAOABhAGQwawYJKwYBBAGCNxEBMV4eXA
BNAGkAYwByAG8AcwBvAGYAdAAgAEUAbgBoAGEAbgBjAGUAZAAgAEMAcgB5AHAAdABvAGcAcgBh
AHAAaABpAGMAIABQAHIAbwB2AGkAZABlAHIAIAB2ADEALgAwMIIBzwYJKoZIhvcNAQcGoIIBw
DCCAbwCAQAwggG1BgkqhkiG9w0BBwEwHAYKKoZIhvcNAQwBBjAOBAjWmEM3BmtPWwICB9CAgg
GIIxS2KaegZ8TDfdq1AP55giyOzgLOvd1LYA5M1QwRmYcM4IiJe5Z6yB6usrnMa/oAJ6suBw72
UTO8lTGc/AXWtbrJg6KM0CuyI7lKdoShn36FRx35djx5plXpDxVrZtR2MbOxgSdUNyUCiu
RWe/FUzpwE93IWQnfIleeziH1YXuZdvxy/vTLKT2VngeZh3BjyG25n7Fj44xgy7CQM/g/q+TgHB
JjnY9qD36kPdaWxkxytadpJH3GgnKLjoQCvHhFN6NEVhErnvzZo63jPZIDWHxr7EYGkbVTzwtPw
locTDgm75gS/IwCMNdAxHP9ofMM4H+2g/UV88R4ABgUoP139Drz5LrfHFsnvPx3/twygMX6lUccnw
yKZTVcphjADHU6FVsm2/xJ/nqxCkiUt7ciz150FqGxJ+vxg5zo533eHjViwdDBHTMIopyypOY69
xNfN1VGPMKxfc/d5z6ayKKpi9lXQMIUumoz5Xqjnq4dyschqoUbGNW1LB+0Y3BNHxeXyGlYsTsr
9nYowNzAfMAcGBSsOAwIaBBRgiHbVQmQbvNqXli2R3sBoa6AirAQUeAWzhwSRejw9yMIGB2GgBY76bbM=">
<trustedCertificates>
<certificateInfo thumbprint="64123DFA95F03AFB818EC61C874241B62E2A4886"
subject="ServiceCertificate"/>
</trustedCertificates>
</certificateSection>
I have created a small Windows application for generating certificates and getting its serialized value and thumbprint:
Now, we will create a custom service behavior extension to take care of the service credentials:
using System;
using System.ServiceModel.Configuration;
namespace Certificate.Extensions
{
public class CertificateExtensionBehavior : BehaviorExtensionElement
{
public override Type BehaviorType
{
get { return typeof(CertificateServiceCredentials); }
}
protected override object CreateBehavior()
{
return new CertificateServiceCredentials();
}
}
}
Here is the CertificateServiceCredentials
class which inherits from ServiceCredentials
:
using System.Configuration;
using System.Configuration;
using System.Linq;
using System.ServiceModel.Description;
using Certificate.Configuration;
namespace Certificate.Extensions
{
public class CertificateServiceCredentials : ServiceCredentials
{
public CertificateServiceCredentials()
{
CertificatesSection certificateSection =
(CertificatesSection)ConfigurationManager.GetSection("certificateSection");
var trustedList =
certificateSection.TrustedCertificates.Cast<TrustedCertificateInfo>().Select
(x => x.Thumbprint);
this.ServiceCertificate.Certificate = certificateSection.Certificate;
this.ClientCertificate.Authentication.CertificateValidationMode =
System.ServiceModel.Security.X509CertificateValidationMode.Custom;
this.ClientCertificate.Authentication.CustomCertificateValidator =
new CertificateValidator(trustedList);
}
protected override ServiceCredentials CloneCore()
{
return new CertificateServiceCredentials();
}
}
}
We use a custom certificate validation mode with Certificate
validator who checks if the certificate in the response is in the current certificate trusted list. We get the certificate and the trusted list from our custom configuration section. And here's the CertificateValidator
:
using System;
using System;
using System.Collections.Generic;
using System.IdentityModel.Selectors;
using System.Linq;
using System.Security.Cryptography.X509Certificates;
using System.Security;
namespace Certificate
{
public class CertificateValidator : X509CertificateValidator
{
private readonly IEnumerable<string> trustedThumbprints;
public CertificateValidator(IEnumerable<string> thumbprints)
{
this.trustedThumbprints = thumbprints;
}
public override void Validate(X509Certificate2 certificate)
{
if (certificate == null)
{
throw new SecurityException("Missing certificate");
}
if (!trustedThumbprints.Any(thumbprint => thumbprint.Equals(certificate.Thumbprint)))
{
throw new SecurityException("The provided certificate is not trusted!");
}
}
}
}
So after that, we need to set the service to use this custom extension, so the web.config of the service should look like this:
="1.0"
<configuration>
<configSections>
<section name="certificateSection"
type="Certificate.Configuration.CertificatesSection, Certificate"/>
</configSections>
<certificateSection certificate="MIIExgIBAzCCBIYGCSqGSIb3DQEHAaCCBHcEggRzMIIEbzCCApgG
CSqGSIb3DQEHAaCCAokEggKFMIICgTCCAn0GCyqGSIb3DQEMCgECoIIBjjCCAYowHAYKKoZIhvcNAQwBAzAOBAi
qwSbxN4Q2IQICB9AEggFoaa40+yIrF2Wb3/L4rYE7UpzrhI5S2O8wx72gj41tudES+QiM8DGxC1YWaHx4+
THXFkQs62A6eDTKYOZUc8oMUFyVbY70Joq8un6keunCz4xP26MB2vygpx1/ASA6CTdeN3r9JEYss7DGaFnv
UJKPH44oyBagaecT3fo0CA+Qa7vfcrlrhcyyovVs5lfJNUm13IF8/bNMCcOdgUnjX/tlay53YDulZSD0kP3a
pd60zzBAtIr2GD/h3NjiIcjSauDUf7bdvEV0LHAC78mRB/6nUaYiwZhAphky8ufR3dMzZGt5bglbEb8WkEw4
bh/qrUxofA5uDmRgjnusAVtOcm0BUvK458bzsyKaRwuw8wSK+Srii5ZYjE5DSTc3msu5jCKZ5pC03w8tde
Xmc5Xqq5TziKpDXW1bCa9D/O8mRnz+IsRa1FirDV/Spp37wyucLJkluKHZTpOeTWXRGy1/8Ys5kDxeXAamJ
xSLMYHbMBMGCSqGSIb3DQEJFTEGBAQBAAAAMFcGCSqGSIb3DQEJFDFKHkgAZAAzADAAMwBkADQAZgA2AC0
AYQBmADAANQAtADQANwBjAGUALQBiAGIAZgBjAC0AMwBhAGMANQBlADgAYQBiAGUAOAA3ADcwawYJKwYBB
AGCNxEBMV4eXABNAGkAYwByAG8AcwBvAGYAdAAgAEUAbgBoAGEAbgBjAGUAZAAgAEMAcgB5AHAAdABvAGc
AcgBhAHAAaABpAGMAIABQAHIAbwB2AGkAZABlAHIAIAB2ADEALgAwMIIBzwYJKoZIhvcNAQcGoIIBwD
CCAbwCAQAwggG1BgkqhkiG9w0BBwEwHAYKKoZIhvcNAQwBBjAOBAgatfFYOA+7qAICB9CAggGIOL+
SmdG5n6oummdHrr7u0LH7+VwF3rICFqQTncXX9iVTND6DXJArJfFsYGs1fwq4mzTxmrpBArsf0pCht3
7x5m7m9k/JL/LeXWlh5re+tZptnEl/l/45AUvN3/fMzoaG4rD5keA1POOoir9fVTiiJjvPfIYvriI8s
iMwx13fyuFYNZlF+T1pkR6WQbRKTYS49nSGxhIgsoUkxXkGm64CRgXRriHqhopDqUmzCgHhE68jjt78
Ff9iYl/1KYBvpJfgBTnvV0dNcXcHhmOkOLqHA6ONBFeARH6ous1i2AUoXfTVoFptTb0eSQTrZkravx2
uJrSSuMtPP2qOkGkVQNE2TsJdyFVEKwhXuVyhpuDFky56Q73RDzQCfFEhZHfmleUCaZSVJlUXY86b6/
Qk4ebzmGyje7+7z29PARHJBKWHJi/759fKmpTMO27gYor+ylFhqz21crjX7uae0jLKg59CjdSgJocpZ5
jOK+B4sWqFuEYUMpcUcN3pZ2jkFMqQcWzOinegbwKeMzgwNzAfMAcGBSsOAwIaBBS9S/16xb6qbDTK5f
Y6EyjfSc/nvgQU1/tVtjNCPOGmYwOUDNupoL7hlpI=">
<trustedCertificates>
<certificateInfo thumbprint="625C675C8C7FF2A4041573116211367DABA71969"
subject="CallerCertificate"/>
</trustedCertificates>
</certificateSection>
<system.web>
<compilation debug="true" targetFramework="4.0" />
</system.web>
<system.serviceModel>
<behaviors>
<serviceBehaviors>
<behavior>
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
<certificateExtension />
</behavior>
</serviceBehaviors>
</behaviors>
<serviceHostingEnvironment multipleSiteBindingsEnabled="true" />
<services>
<service name="Service.Service">
<endpoint name="myService"
address=""
binding="wsDualHttpBinding"
contract="Service.IService"
bindingConfiguration="certificateBinding"/>
</service>
</services>
<bindings>
<wsDualHttpBinding>
<binding name="certificateBinding">
<security mode="Message">
<message clientCredentialType="Certificate" />
</security>
</binding>
</wsDualHttpBinding>
</bindings>
<extensions>
<behaviorExtensions>
<add name="certificateExtension"
type="Certificate.Extensions.CertificateExtensionBehavior, Certificate"/>
</behaviorExtensions>
</extensions>
</system.serviceModel>
<system.webServer>
<modules runAllManagedModulesForAllRequests="true"/>
</system.webServer>
</configuration>
And the app.config of the caller should look like this:
="1.0"="utf-8"
<configuration>
<configSections>
<section name="certificateSection"
type="Certificate.Configuration.CertificatesSection, Certificate"/>
</configSections>
<certificateSection certificate="MIIExgIBAzCCBIYGCSqGSIb3DQEHAaCCBHcEggRzMIIEbzCCApgGCSqGS
Ib3DQEHAaCCAokEggKFMIICgTCCAn0GCyqGSIb3DQEMCgECoIIBjjCCAYowHAYKKoZIhvcNAQwBAzAOBAgbm3IP
SqU0BQICB9AEggFojvCBmeSJ6n4IlKxgSv1XgIB5LaD7tb06f/yTLsZRK+4rnqCaesmYFFHP889JTySdqPPyE6f
NrpFXTcvcRC6lQQglLnxbRZQotPHvDv4MEzEdI67zkkfM7RsxxXRUQE+ex5H+oQxjScvVRWlKa0KXLk7DOa+
Ijz/epLFCum2CE2aUE/AOdi8GCYf7D0yMa472/buQRX1qWX5MYuH+sZI1py/unS8R5R4cytRr8dKJHmn3Y
tLuhEwQOuXiQ/mUK5PKj+xYp6b8ssVIXQjuLpXZnnT7i/KdZipxmTCf+OtXbAysBw2VaQ9+NmR8cufy8
nUb/KgSNfcE3hTHTxIaBnhddhuHxvfR5oYIAzPK3NTq/S1qCEqxDJnBFapdnRcKfHEAlDwIB/KZyHgKdVBiu1
6pB9e+bxl840CW6vI/tILBbpww3rjvzKKQYZZ6uPu1oNLS2TeX7JsBJE3p0HJE2DPFLfmXLPVPSkHBMYH
bMBMGCSqGSIb3DQEJFTEGBAQBAAAAMFcGCSqGSIb3DQEJFDFKHkgAZgAzADEAMwBlADQAYQA4AC0AMAA
2ADMANwAtADQANAA1ADAALQBiAGMANAA5AC0AMwA2AGEAMABkAGIAOQAxADkAOABhAGQwawYJKwYBBAGC
NxEBMV4eXABNAGkAYwByAG8AcwBvAGYAdAAgAEUAbgBoAGEAbgBjAGUAZAAgAEMAcgB5AHAAdABvAGcAc
gBhAHAAaABpAGMAIABQAHIAbwB2AGkAZABlAHIAIAB2ADEALgAwMIIBzwYJKoZIhvcNAQcGoIIBwDCCAbw
CAQAwggG1BgkqhkiG9w0BBwEwHAYKKoZIhvcNAQwBBjAOBAjWmEM3BmtPWwICB9CAggGIIxS2KaegZ8TDf
dq1AP55giyOzgLOvd1LYA5M1QwRmYcM4IiJe5Z6yB6usrnMa/oAJ6suBw72UTO8lTGc/AXWtbrJg6KM0Cuy
I7lKdoShn36FRx35djx5plXpDxVrZtR2MbOxgSdUNyUCiuRWe/FUzpwE93IWQnfIleeziH1YXuZdvxy/vTL
KT2VngeZh3BjyG25n7Fj44xgy7CQM/g/q+TgHBJjnY9qD36kPdaWxkxytadpJH3GgnKLjoQCvHhFN6NEVh
ErnvzZo63jPZIDWHxr7EYGkbVTzwtPwlocTDgm75gS/IwCMNdAxHP9ofMM4H+2g/UV88R4ABgUoP139Dr
z5LrfHFsnvPx3/twygMX6lUccnwyKZTVcphjADHU6FVsm2/xJ/nqxCkiUt7ciz150FqGxJ+vxg5zo533e
HjViwdDBHTMIopyypOY69xNfN1VGPMKxfc/d5z6ayKKpi9lXQMIUumoz5Xqjnq4dyschqoUbGNW1LB+0Y
3BNHxeXyGlYsTsr9nYowNzAfMAcGBSsOAwIaBBRgiHbVQmQbvNqXli2R3sBoa6AirAQUeAWzh
wSRejw9yMIGB2GgBY76bbM=">
<trustedCertificates>
<certificateInfo thumbprint="64123DFA95F03AFB818EC61C874241B62E2A4886"
subject="ServiceCertificate"/>
</trustedCertificates>
</certificateSection>
<system.serviceModel>
<bindings>
<wsDualHttpBinding>
<binding name="certificatesBinfing">
<security mode="Message">
<message clientCredentialType="Certificate"/>
</security>
</binding>
</wsDualHttpBinding>
</bindings>
<client>
<endpoint address="http://localhost:9986/Service.svc" binding="wsDualHttpBinding"
bindingConfiguration="certificatesBinfing" contract="IService"
name="BasicHttpBinding_IService">
</endpoint>
</client>
</system.serviceModel>
</configuration>
I have added a small WCF Extensions helper class from here.
using System;
using System.ServiceModel;
namespace Caller.Proxy
{
public static class WcfExtensions
{
public static void Using<T>(this T client, Action<T> work)
where T : ICommunicationObject
{
try
{
work(client);
client.Close();
}
catch (CommunicationException)
{
client.Abort();
}
catch (TimeoutException)
{
client.Abort();
}
catch (Exception)
{
client.Abort();
throw;
}
}
}
}
Now the only thing we need to do is to call the service from the caller:
using System;
using System.Configuration;
using System.Linq;
using System.ServiceModel;
using Caller.Proxy;
using Certificate;
using Certificate.Configuration;
namespace Caller
{
class Program
{
static void Main(string[] args)
{
new ServiceClient().Using(channel =>
{
CertificatesSection certificateSection =
(CertificatesSection)ConfigurationManager.GetSection("certificateSection");
var trustedList =
certificateSection.TrustedCertificates.Cast<TrustedCertificateInfo>().ToList();
var endpointAddress = channel.Endpoint.Address.Uri;
string trustedSubject = trustedList.FirstOrDefault().Subject;
var identity = EndpointIdentity.CreateDnsIdentity(trustedSubject);
channel.Endpoint.Address = new EndpointAddress(endpointAddress, identity);
channel.ClientCredentials.ClientCertificate.Certificate = certificateSection.Certificate;
channel.ClientCredentials.ServiceCertificate.Authentication.CertificateValidationMode =
System.ServiceModel.Security.X509CertificateValidationMode.Custom;
channel.ClientCredentials.ServiceCertificate.Authentication.CustomCertificateValidator =
new CertificateValidator(trustedList.Select(x => x.Thumbprint));
var data = channel.GetData(1);
Console.WriteLine(data);
});
}
}
}
And that's it. We can create a new certificate with the generator, set the serialized value in the configuration file of one of the sides and add the certificate subject and thumbprint in the other side's trusted certificates and vice versa.
If you host the service and the site on IIS, you need to set IIS Application Pool configuration (Application Pools > Advanced Settings) to load the user profile for the application pool identity user.
Otherwise, the user may not be able to load the certificate.
You can find the example project here.