X.509 Certificate Validator

This sample demonstrates how to implement a custom X.509 Certificate Validator. This is useful in cases where none of the built-in X.509 Certificate Validation modes is appropriate for the requirements of the application. This sample shows a service that has a custom validator that accepts self-issued certificates. The client uses such a certificate to authenticate to the service.

Note: As anyone can construct a self-issued certificate the custom validator used by the service is less secure than the default behavior provided by the ChainTrust X509CertificateValidationMode. The security implications of this should be carefully considered before using this validation logic in production code.

In summary this sample demonstrates how:

  • The client can be authenticated using an X.509 certificate.

  • The server validates the client credentials against a custom X509CertificateValidator.

  • The server is authenticated using the server's X.509 certificate.

The service exposes a single endpoint for communicating with the service, defined using the configuration file App.config. The endpoint consists of an address, a binding, and a contract. The binding is configured with a standard wsHttpBinding that defaults to using WSSecurity and client certificate authentication. The service behavior specifies the Custom mode for validating client X.509 certificates along with the type of the validator class. The behavior also specifies the server certificate using the serviceCertificate element. The server certificate has to contain the same value for the SubjectName as the findValue in the <serviceCertificate>.

  <system.serviceModel>
    <services>
      <service name="Microsoft.ServiceModel.Samples.CalculatorService"
               behaviorConfiguration="CalculatorServiceBehavior">
        <!-- use host/baseAddresses to configure base address -->
        <!-- provided by host -->
        <host>
          <baseAddresses>
            <add baseAddress =
                "http://localhost:8001/servicemodelsamples/service" />
          </baseAddresses>
        </host>
        <!-- use base address specified above, provide one endpoint -->
        <endpoint address="certificate"
               binding="wsHttpBinding"
               bindingConfiguration="Binding"
               contract="Microsoft.ServiceModel.Samples.ICalculator" />
      </service>
    </services>
    <bindings>
      <wsHttpBinding>
        <!-- X509 certificate binding -->
        <binding name="Binding">
          <security mode="Message">
            <message clientCredentialType="Certificate" />
          </security>
        </binding>
      </wsHttpBinding>
    </bindings>
    <behaviors>
      <serviceBehaviors>
        <behavior name="CalculatorServiceBehavior">
          <serviceDebug includeExceptionDetailInFaults ="true"/>
          <serviceCredentials>
            <!-- The serviceCredentials behavior allows one -->
            <!-- to specify authentication constraints on -->
            <!-- client certificates. -->
            <clientCertificate>
              <!-- Setting the certificateValidationMode to -->
              <!-- Custom means that if the custom -->
              <!-- X509CertificateValidator does NOT throw -->
              <!-- an exception, then the provided certificate -->
              <!-- will be trusted without performing any -->
              <!-- validation beyond that performed by the custom -->
              <!-- validator. The security implications of this -->
              <!-- setting should be carefully considered before -->
              <!-- using Custom in production code. -->
              <authentication
                 certificateValidationMode="Custom"
                 customCertificateValidatorType =
"Microsoft.ServiceModel.Samples.CustomX509CertificateValidator, service" />
            </clientCertificate>
            <!-- The serviceCredentials behavior allows one to -->
            <!-- define a service certificate. -->
            <!-- A service certificate is used by a client to -->
            <!-- authenticate the service and provide message -->
            <!-- protection. This configuration references the -->
            <!-- "localhost" certificate installed during the setup -->
            <!-- instructions. -->
            <serviceCertificate findValue="localhost"
                 storeLocation="LocalMachine"
                 storeName="My" x509FindType="FindBySubjectName" />
          </serviceCredentials>
        </behavior>
      </serviceBehaviors>
    </behaviors>
      </system.serviceModel>

The client endpoint configuration consists of a configuration name, an absolute address for the service endpoint, the binding, and the contract. The client binding is configured with the appropriate mode and message clientCredentialType.

<system.serviceModel>
    <client>
      <!-- X509 certificate based endpoint -->
      <endpoint name="Certificate"
        address=
        "http://localhost:8001/servicemodelsamples/service/certificate"
                binding="wsHttpBinding"
                bindingConfiguration="Binding"
                behaviorConfiguration="ClientCertificateBehavior"
                contract="Microsoft.ServiceModel.Samples.ICalculator">
      </endpoint>
    </client>
    <bindings>
        <wsHttpBinding>
            <!-- X509 certificate binding -->
            <binding name="Binding">
                <security mode="Message">
                    <message clientCredentialType="Certificate" />
               </security>
            </binding>
       </wsHttpBinding>
    </bindings>
    <behaviors>
      <endpointBehaviors>
        <behavior name="ClientCertificateBehavior">
          <clientCredentials>
            <serviceCertificate>
              <!-- Setting the certificateValidationMode to -->
              <!-- PeerOrChainTrust means that if the certificate -->
              <!-- is in the user's Trusted People store, then it -->
              <!-- is trusted without performing a validation of -->
              <!-- the certificate's issuer chain. -->
              <!-- This setting is used here for convenience so -->
              <!-- that the sample can be run without having to -->
              <!-- have certificates issued by a certification -->
              <!-- authority (CA). This setting is less secure -->
              <!-- than the default, ChainTrust. The security -->
              <!-- implications of this setting should be -->
              <!-- carefully considered before using -->
              <!-- PeerOrChainTrust in production code.-->
              <authentication
                  certificateValidationMode="PeerOrChainTrust" />
            </serviceCertificate>
          </clientCredentials>
        </behavior>
      </endpointBehaviors>
    </behaviors>
  </system.serviceModel>

The client implementation sets the client certificate to use.

// Create a client with Certificate endpoint configuration
CalculatorClient client = new CalculatorClient("Certificate");
try
{
    client.ClientCredentials.ClientCertificate.SetCertificate(StoreLocation.CurrentUser, StoreName.My, X509FindType.FindBySubjectName, "test1");

    // Call the Add service operation.
    double value1 = 100.00D;
    double value2 = 15.99D;
    double result = client.Add(value1, value2);
    Console.WriteLine("Add({0},{1}) = {2}", value1, value2, result);

    // Call the Subtract service operation.
    value1 = 145.00D;
    value2 = 76.54D;
    result = client.Subtract(value1, value2);
    Console.WriteLine("Subtract({0},{1}) = {2}", value1, value2, result);

    // Call the Multiply service operation.
    value1 = 9.00D;
    value2 = 81.25D;
    result = client.Multiply(value1, value2);
    Console.WriteLine("Multiply({0},{1}) = {2}", value1, value2, result);

    // Call the Divide service operation.
    value1 = 22.00D;
    value2 = 7.00D;
    result = client.Divide(value1, value2);
    Console.WriteLine("Divide({0},{1}) = {2}", value1, value2, result);
    client.Close();
}
catch (TimeoutException e)
{
    Console.WriteLine("Call timed out : {0}", e.Message);
    client.Abort();
}
catch (CommunicationException e)
{
    Console.WriteLine("Call failed : {0}", e.Message);
    client.Abort();
}
catch (Exception e)
{
    Console.WriteLine("Call failed : {0}", e.Message);
    client.Abort();
}

This sample uses a custom X509CertificateValidator to validate certificates. The sample implements CustomX509CertificateValidator, derived from X509CertificateValidator. See documentation about X509CertificateValidator for more information. This particular custom validator sample implements the Validate method to accept any X.509 certificate that is self-issued as shown in the following code.

public class CustomX509CertificateValidator : X509CertificateValidator
{
  public override void Validate ( X509Certificate2 certificate )
  {
   // Only accept self-issued certificates
   if (certificate.Subject != certificate.Issuer)
     throw new Exception("Certificate is not self-issued");
   }
}

Once the validator is implemented in service code, the service host must be informed about the validator instance to use. This is done using the following code.

serviceHost.Credentials.ClientCertificate.Authentication.CertificateValidationMode = X509CertificateValidationMode.Custom;
serviceHost.Credentials.ClientCertificate.Authentication.CustomCertificateValidator = new CustomX509CertificateValidator();

Or you can do the same thing in configuration as follows.

<behaviors>
    <serviceBehaviors>
     <behavior name="CalculatorServiceBehavior">
       ...
   <serviceCredentials>
    <!--The serviceCredentials behavior allows one to specify -->
    <!--authentication constraints on client certificates.-->
    <clientCertificate>
    <!-- Setting the certificateValidationMode to Custom means -->
    <!--that if the custom X509CertificateValidator does NOT -->
    <!--throw an exception, then the provided certificate will -->
    <!--be trusted without performing any validation beyond that -->
    <!--performed by the custom validator. The security -->
    <!--implications of this setting should be carefully -->
    <!--considered before using Custom in production code. -->
    <authentication certificateValidationMode="Custom"
       customCertificateValidatorType =
"Microsoft.ServiceModel.Samples. CustomX509CertificateValidator, service" />
   </clientCertificate>
   </serviceCredentials>
   ...
  </behavior>
 </serviceBehaviors>
</behaviors>

When you run the sample, the operation requests and responses are displayed in the client console window. The client should successfully call all the methods. Press ENTER in the client window to shut down the client.

Setup Batch File

The Setup.bat batch file included with this sample allows you to configure the server with relevant certificates to run a self-hosted application that requires server certificate-based security. This batch file must be modified to work across computers or to work in a non-hosted case.

The following provides a brief overview of the different sections of the batch files so that they can be modified to run in the appropriate configuration:

  • Creating the server certificate:

    The following lines from the Setup.bat batch file create the server certificate to be used. The %SERVER_NAME% variable specifies the server name. Change this variable to specify your own server name. The default value is localhost.

    echo ************
    echo Server cert setup starting
    echo %SERVER_NAME%
    echo ************
    echo making server cert
    echo ************
    makecert.exe -sr LocalMachine -ss MY -a sha1 -n CN=%SERVER_NAME% -sky exchange -pe
    
  • Installing the server certificate into client's trusted certificate store:

    The following lines in the Setup.bat batch file copy the server certificate into the client trusted people store. This step is required since certificates generated by Makecert.exe are not implicitly trusted by the client system. If you already have a certificate that is rooted in a client trusted root certificate—for example, a Microsoft issued certificate—this step of populating the client certificate store with the server certificate is not required.

    certmgr.exe -add -r LocalMachine -s My -c -n %SERVER_NAME% -r CurrentUser -s TrustedPeople
    
  • Creating the client certificate:

    The following lines from the Setup.bat batch file create the client certificate to be used. The %USER_NAME% variable specifies the client name. This value is set to "test1" because this is the name the client code looks for. If you change the value of %USER_NAME% you must change the corresponding value in the Client.cs source file and rebuild the client.

    The certificate is stored in My (Personal) store under the CurrentUser store location.

    echo ************
    echo Client cert setup starting
    echo %USER_NAME%
    echo ************
    echo making client cert
    echo ************
    makecert.exe -sr CurrentUser -ss MY -a sha1 -n CN=%USER_NAME% -sky exchange -pe
    
  • Installing the client certificate into server's trusted certificate store:

    The following lines in the Setup.bat batch file copy the client certificate into the trusted people store. This step is required because certificates generated by Makecert.exe are not implicitly trusted by the server system. If you already have a certificate that is rooted in a trusted root certificate—for example, a Microsoft issued certificate—this step of populating the server certificate store with the client certificate is not required.

    certmgr.exe -add -r CurrentUser -s My -c -n %USER_NAME% -r LocalMachine -s TrustedPeople
    

To set up and build the sample

  1. To build the solution, follow the instructions in Building the Windows Communication Foundation Samples.

  2. To run the sample in a single- or cross-computer configuration, use the following instructions.

To run the sample on the same computer

  1. Run Setup.bat from the sample install folder inside a Visual Studio command prompt opened with administrator privileges. This installs all the certificates required for running the sample.

    Important

    The Setup.bat batch file is designed to be run from a Visual Studio Command Prompt. The PATH environment variable set within the Visual Studio Command Prompt points to the directory that contains executables required by the Setup.bat script.

  2. Launch Service.exe from service\bin.

  3. Launch Client.exe from \client\bin. Client activity is displayed on the client console application.

  4. If the client and service are not able to communicate, see Troubleshooting Tips for WCF Samples.

To run the sample across computers

  1. Create a directory on the service computer.

  2. Copy the service program files from \service\bin to the virtual directory on the service computer. Also copy the Setup.bat, Cleanup.bat, GetComputerName.vbs and ImportClientCert.bat files to the service computer.

  3. Create a directory on the client computer for the client binaries.

  4. Copy the client program files to the client directory on the client computer. Also copy the Setup.bat, Cleanup.bat, and ImportServiceCert.bat files to the client.

  5. On the server, run setup.bat service in a Developer Command Prompt for Visual Studio opened with administrator privileges. Running setup.bat with the service argument creates a service certificate with the fully-qualified domain name of the computer and exports the service certificate to a file named Service.cer.

  6. Edit Service.exe.config to reflect the new certificate name (in the findValue attribute in the <serviceCertificate>) which is the same as the fully-qualified domain name of the computer. Also change the computer name in the <service>/<baseAddresses> element from localhost to fully qualified name of your service computer.

  7. Copy the Service.cer file from the service directory to the client directory on the client computer.

  8. On the client, run setup.bat client in a Developer Command Prompt for Visual Studio opened with administrator privileges. Running setup.bat with the client argument creates a client certificate named client.com and exports the client certificate to a file named Client.cer.

  9. In the Client.exe.config file on the client computer, change the address value of the endpoint to match the new address of your service. Do this by replacing localhost with the fully-qualified domain name of the server.

  10. Copy the Client.cer file from the client directory to the service directory on the server.

  11. On the client, run ImportServiceCert.bat in a Developer Command Prompt for Visual Studio opened with administrator privileges. This imports the service certificate from the Service.cer file into the CurrentUser - TrustedPeople store.

  12. On the server, run ImportClientCert.bat in a Developer Command Prompt for Visual Studio opened with administrator privileges. This imports the client certificate from the Client.cer file into the LocalMachine - TrustedPeople store.

  13. On the server computer, launch Service.exe from the command prompt window.

  14. On the client computer, launch Client.exe from a command prompt window. If the client and service are not able to communicate, see Troubleshooting Tips for WCF Samples.

To clean up after the sample

  1. Run Cleanup.bat in the samples folder once you have finished running the sample. This removes the server and client certificates from the certificate store.

Note

This script does not remove service certificates on a client when running this sample across computers. If you have run Windows Communication Foundation (WCF) samples that use certificates across computers, be sure to clear the service certificates that have been installed in the CurrentUser - TrustedPeople store. To do this, use the following command: certmgr -del -r CurrentUser -s TrustedPeople -c -n <Fully Qualified Server Machine Name> For example: certmgr -del -r CurrentUser -s TrustedPeople -c -n server1.contoso.com.