WCF: FindBySubjectKeyIdentifier x509FindType search for client certificate

Problem Description
WCF service is configured with basicHttpBinding, security mode Transport and client credential type certificate. This service is hosted on IIS (Windows server 2008 R2 or any other. In order to communicate with the service, consumer presents client certificate as a credential type. However, client application fails while creating the channel factory with message "Cannot find the X.509 certificate" (even though certificate detail is provided in configuration file).

The problem is observed for all the .NET framework versions, and tested with the latest 4.6.2 version – having the same challenge.

How consumer tried
This is one of the traditional approaches in WCF to read subjectKeyIdentifier value, and set FindBySubjectKeyIdentifier by configuration.

app.config

 
< system.serviceModel>
< bindings>
< basicHttpBinding>
< binding name="BasicHttpBinding_IService1">
< security mode="Transport">
< transport clientCredentialType="Certificate" />
< /security>
< /binding>
< /basicHttpBinding>
< /bindings>

< client>
< endpoint address="https://pupanda-server.fareast.corp.microsoft.com:2092/ClientCertAuthService/Service1.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1" behaviorConfiguration="epbehave1"
contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" />
< /client>

< behaviors>
< endpointBehaviors>
< behavior name="epbehave1">
<clientCredentials>
< clientCertificate storeLocation="CurrentUser" storeName="My"
x509FindType="FindBySubjectKeyIdentifier" findValue="7a c9 62 5a 6a a1 20 fc 0f 4d 67 a9 93 cb 5e 1c d8 24 9d f2" />
< /clientCredentials>
< /behavior>
< /endpointBehaviors>
< /behaviors>

< /system.serviceModel>

Program.cs

 
static void Main(string[] args)
{
    using (var p = new ServiceReference1.Service1Client())
    {
         // WCF service method call
         Console.WriteLine(p.GetData(123));
    }
}

Also - if client searches for client certificate by issuer or serial number or subject name or thumbprint provided in configuration file, it works. However, not for SubjectKeyIdentifier option.

More failure information

 
0:001> !pe
Exception object: 02b5dbb0
Exception type:   System.InvalidOperationException
Message:          Cannot find the X.509 certificate using the following search criteria: StoreName 'My', StoreLocation 'CurrentUser', FindType 'FindBySubjectKeyIdentifier', FindValue '7a c9 62 5a 6a a1 20 fc 0f 4d 67 a9 93 cb 5e 1c d8 24 9d f2'.
 
0:001> kL
# ChildEBP RetAddr
00 00afe964 7377a701 kernelbase!RaiseException
01 00afea00 7377aeec clr!RaiseTheExceptionInternalOnly+0x27c
02 00afeac8 6ebfa98a clr!IL_Throw+0x138
03 00afeafc 6ebfa864 system_servicemodel_ni!System.ServiceModel.Security.SecurityUtils.GetCertificateFromStoreCore(System.Security.Cryptography.X509Certificates.StoreName, System.Security.Cryptography.X509Certificates.StoreLocation, System.Security.Cryptography.X509Certificates.X509FindType, System.Object, System.ServiceModel.EndpointAddress, Boolean)+0x112
04 00afeb24 6ebfdb2c system_servicemodel_ni!System.ServiceModel.Security.SecurityUtils.GetCertificateFromStore(System.Security.Cryptography.X509Certificates.StoreName, System.Security.Cryptography.X509Certificates.StoreLocation, System.Security.Cryptography.X509Certificates.X509FindType, System.Object, System.ServiceModel.EndpointAddress)+0x20
05 00afeb44 6f519138 system_servicemodel_ni!System.ServiceModel.Security.X509CertificateInitiatorClientCredential.SetCertificate(System.Security.Cryptography.X509Certificates.StoreLocation, System.Security.Cryptography.X509Certificates.StoreName, System.Security.Cryptography.X509Certificates.X509FindType, System.Object)+0x2c
06 00afeb68 6f518793 system_servicemodel_ni!System.ServiceModel.Configuration.X509InitiatorCertificateClientElement.ApplyConfiguration(System.ServiceModel.Security.X509CertificateInitiatorClientCredential)+0xe4
07 00afeb98 6f5186c1 system_servicemodel_ni!System.ServiceModel.Configuration.ClientCredentialsElement.ApplyConfiguration(System.ServiceModel.Description.ClientCredentials)+0xc7
08 00afebb0 6ec2b329 system_servicemodel_ni!System.ServiceModel.Configuration.ClientCredentialsElement.CreateBehavior()+0xad
09 00afebe4 6f84cd70 system_servicemodel_ni!System.ServiceModel.Description.ConfigLoader.LoadBehaviors[[System.__Canon, mscorlib]](System.ServiceModel.Configuration.ServiceModelExtensionCollectionElement`1<System.ServiceModel.Configuration.BehaviorExtensionElement>, System.Collections.Generic.KeyedByTypeCollection`1<System.__Canon>, Boolean)+0xc5
0a 00afec14 6ec02b0b system_servicemodel_ni!System.ServiceModel.Description.ConfigLoader.LoadChannelBehaviors(System.ServiceModel.Description.ServiceEndpoint, System.String)+0xc4ece4
0b 00afec30 6ec02a8c system_servicemodel_ni!System.ServiceModel.ChannelFactory.ApplyConfiguration(System.String, System.Configuration.Configuration)+0x7b
0c 00afec50 6ebfe939 system_servicemodel_ni!System.ServiceModel.ChannelFactory.ApplyConfiguration(System.String)+0xc
0d 00afec50 6ec274e2 system_servicemodel_ni!System.ServiceModel.ChannelFactory.InitializeEndpoint(System.String, System.ServiceModel.EndpointAddress)+0x8d
0e 00afec84 6f85a57c system_servicemodel_ni!System.ServiceModel.ChannelFactory`1[[System.__Canon, mscorlib]]..ctor(System.String, System.ServiceModel.EndpointAddress)+0x82
0f 00afeca0 6ec1bc00 system_servicemodel_ni!System.ServiceModel.ConfigurationEndpointTrait`1[[System.__Canon, mscorlib]].CreateSimplexFactory()+0xc3e978
10 00afeca8 6ec1b8c7 system_servicemodel_ni!System.ServiceModel.ConfigurationEndpointTrait`1[[System.__Canon, mscorlib]].CreateChannelFactory()+0x14
11 00afecbc 6ec1b886 system_servicemodel_ni!System.ServiceModel.ClientBase`1[[System.__Canon, mscorlib]].CreateChannelFactoryRef(System.ServiceModel.EndpointTrait`1<System.__Canon>)+0x13
12 00afecf0 6f437bfc system_servicemodel_ni!System.ServiceModel.ClientBase`1[[System.__Canon, mscorlib]].InitializeChannelFactoryRef()+0x166
13 00afed00 0288053d system_servicemodel_ni!System.ServiceModel.ClientBase`1[[System.__Canon, mscorlib]]..ctor()+0x128
14 00afed0c 028804a2 consoleapplication1!ConsoleApplication1.ServiceReference1.Service1Client..ctor()+0x1d
15 00afed48 73671376 consoleapplication1!ConsoleApplication1.Program.Main(System.String[])+0x5a
16 00afed54 7367366f clr!CallDescrWorkerInternal+0x34
17 00afeda8 7367d376 clr!CallDescrWorkerWithHandler+0x6b
18 00afee18 7378a204 clr!MethodDescCallSite::CallTargetWorker+0x158
19 (Inline) -------- clr!MethodDescCallSite::Call+0xf
1a 00afef3c 7378a40b clr!RunMain+0x1aa
1b 00aff1b0 7378aa33 clr!Assembly::ExecuteMainMethod+0x124
1c 00aff6b8 7378aad9 clr!SystemDomain::ExecuteMainMethod+0x651
1d 00aff710 7378a2da clr!ExecuteEXE+0x4c
1e 00aff750 737a599f clr!_CorExeMainInternal+0xdc
1f 00aff78c 73e9cccb clr!_CorExeMain+0x4d
20 00aff7c8 7430dd05 mscoreei!_CorExeMain+0x10a
21 (Inline) -------- mscoree!ShellShim__CorExeMain+0x94
22 00aff7d8 772638f4 mscoree!_CorExeMain_Exported+0xa5
23 00aff7ec 776a5de3 kernel32!BaseThreadInitThunk+0x24
24 00aff834 776a5dae ntdll!__RtlUserThreadStart+0x2f
25 00aff844 00000000 ntdll!_RtlUserThreadStart+0x1b

Source code

 
static X509Certificate2 GetCertificateFromStoreCore(StoreName storeName, StoreLocation storeLocation, 
X509FindType findType, object findValue, EndpointAddress target, bool throwIfMultipleOrNoMatch)
{
   if (findValue == null)
   {
       throw DiagnosticUtility.ExceptionUtility.ThrowHelperArgumentNull("findValue");
   }

   X509CertificateStore store = new X509CertificateStore(storeName, storeLocation);
   X509Certificate2Collection certs = null;

   try
   {
       store.Open(OpenFlags.ReadOnly);
       certs = store.Find(findType, findValue, false);   < ---------------
       if (certs.Count == 1)
       {
           return new X509Certificate2(certs[0]);
       }

       if (throwIfMultipleOrNoMatch)
       {
           throw DiagnosticUtility.ExceptionUtility.ThrowHelperError(CreateCertificateLoadException(
storeName, storeLocation, findType, findValue, target, certs.Count));
       }
       else
       {
           return null;
       }
   }

   finally
   {
       SecurityUtils.ResetAllCertificates(certs);
       store.Close();
   }
}

Work around 1
Explicitly read the certificate from certificate store, and populate the clientCertificate property.

App.config

 
< system.serviceModel>
< bindings>
< basicHttpBinding>
< binding name="BasicHttpBinding_IService1">
< security mode="Transport">
< transport clientCredentialType="Certificate" />
< /security>
< /binding>
< /basicHttpBinding>
< /bindings>

< client>
< endpoint address="https://pupanda-server.fareast.corp.microsoft.com:2092/ClientCertAuthService/Service1.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1"
contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" />
< /client>
< /system.serviceModel>

Program.cs

 
static void Main(string[] args)
{
   using (var p = new ServiceReference1.Service1Client())
   {
       StoreLocation location = StoreLocation.CurrentUser;
       X509Store store = new X509Store(StoreName.My, location);
       store.Open(OpenFlags.ReadOnly);
       X509Certificate2Collection certificates = store.Certificates.Find(X509FindType.FindBySubjectKeyIdentifier, "7a c9 62 5a 6a a1 20 fc 0f 4d 67 a9 93 cb 5e 1c d8 24 9d f2", true);
       p.ClientCredentials.ClientCertificate.Certificate = certificates[0];

       // WCF service method call
       Console.WriteLine(p.GetData(123));
    }
}

Work around 2
This is more of a WCF extensibility based work around. It needs to provide the subjectKeyIdentifier value as an appSetting value, read the certificate explicitly from certificate store, and associate back as a client certificate. Also – it is more of a configurable approach.

Create a custom behavior like the following:

 
namespace ConsoleApplication1
{
     public class CustomBehavior : IEndpointBehavior
     {
          public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
          {
               var clientCredentials = bindingParameters.Find<ClientCredentials>();
               if (clientCredentials == null)
               {
                     throw new Exception("Ensure to have blank clientCredentials provided in the EndpointBehaviors section of configuration file.");
               }
               clientCredentials.ClientCertificate.Certificate = GetClientCertificate();
          }
          
          private X509Certificate2 GetClientCertificate()
          {
               StoreLocation location = StoreLocation.CurrentUser;
               var store = new X509Store(StoreName.My, location);
               store.Open(OpenFlags.ReadOnly);
               var subjectKeyIdentifier = ConfigurationManager.AppSettings["subjectKeyIdentifier"];
               var certificates = store.Certificates.Find(X509FindType.FindBySubjectKeyIdentifier, subjectKeyIdentifier, true);
               return certificates[0];
          }

          public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
          {
          }

          public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
          {
          }

          public void Validate(ServiceEndpoint endpoint)
          {
          }
     }

     public class CustomBehaviorExtension : BehaviorExtensionElement
     {
          public override Type BehaviorType
          {
                get { return typeof(CustomBehavior); }
          }

          protected override object CreateBehavior()
          {
                return new CustomBehavior();
          }
     }
}

Configuration file:

 
<configuration>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.6.1" />
</startup>
<appSettings>
<add key="subjectKeyIdentifier" value="7a c9 62 5a 6a a1 20 fc 0f 4d 67 a9 93 cb 5e 1c d8 24 9d f2"/>
</appSettings>
<system.serviceModel>
<bindings>
<basicHttpBinding>
<binding name="BasicHttpBinding_IService1">
<security mode="Transport">
<transport clientCredentialType="Certificate" />
</security>
</binding>
</basicHttpBinding>
</bindings>

<client>
<endpoint address="https://pupanda-server.fareast.corp.microsoft.com:2092/ClientCertAuthService/Service1.svc"
binding="basicHttpBinding" bindingConfiguration="BasicHttpBinding_IService1" behaviorConfiguration="epBehavior"
contract="ServiceReference1.IService1" name="BasicHttpBinding_IService1" />
</client>

<behaviors>
<endpointBehaviors>
<behavior name="epBehavior">
<clientCredentials />
<certificateIdentifier />
</behavior>
</endpointBehaviors>
</behaviors>
<extensions>
<behaviorExtensions>
<add name="certificateIdentifier" type="ConsoleApplication1.CustomBehaviorExtension, ConsoleApplication1"/>
</behaviorExtensions>
</extensions>

</system.serviceModel>
</configuration>

 

Proxy initialization and service call:

 
class Program
{
      static void Main(string[] args)
      {
           using (var p = new ServiceReference1.Service1Client())
           {
                Console.WriteLine(p.GetData(123));
           }
           
            Console.ReadLine();
       }
}

 

I hope the above recommendations help!