Using Web Services Enhancements (WSE) for X.509 Certificate Authentication and Digital Signature
By Peter A. Bromberg, Ph.D.
Printer - Friendly Version
Peter Bromberg

In the first article in this series, we covered how to use the Web Services Enhancements Toolkit to perform SHA1 Digest hashed username/password authentication with the IPasswordProvider interface, using a modified version of the Northwind Database Employees table to do our username/password authentication with the UsernameToken element.



Besides UsernameTokens, the WS-Security specification also defines the BinarySecurityToken element for storing X.509 v3 certificates and Kerberos v5 tickets. WSE currently supports the X.509 flavor, and you will see that the implementation is not much more complicated than what we did with the UsernameToken element. If you are not yet familiar with the architecture and use of the new WSE from Microsoft for .NET, I strongly suggest you read my first article linked above first, review and test the code, and then come back. The process of installing, managing and using Certificates with WSE is a little more involved than username / password authentication, but most of this revolves around the actual certificate management process and not the WSE code. The code to use Certificates to sign and encrypt SOAP messages with WSE is fairly straightforward.

Before we start with Certificates, we should probably take a little time to familiarize ourselves with two important tools, Certmgr and Makecert. Certmgr.exe is the Certificate Manager Tool, and performs the following basic functions:

  • Displays certificates, CTLs, and CRLs to the console.
  • Adds certificates, CTLs, and CRLs to a certificate store.
  • Deletes certificates, CTLs, and CRLs from a certificate store.
  • Saves an X.509 certificate, CTL, or CRL from a certificate store to a file

Certmgr has a number of command - line options, and you can find the full documentation here.

Certmgr.exe works with two types of certificate stores: StoreFile and system store. It is not necessary to specify the type of certificate store; Certmgr.exe can identify the store type and perform the appropriate operations. Running Certmgr.exe without specifying any options launches a GUI that helps with the certificate management tasks that are also available from the command line. The GUI provides an import wizard, which copies certificates, CTLs, and CRLs from your disk to a certificate store. To run Certmgr.exe in GUI mode, simply do Start/Run, enter "C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Bin\certmgr.exe", and hit the enter key:

View Certificates
Certificate Details / Edit Properties

Certificate Creation Tool (Makecert.exe):

The Certificate Creation tool generates X.509 certificates for testing purposes only. It creates a public and private key pair for digital signatures and stores it in a certificate file. This tool also associates the key pair with a specified publisher's name and creates an X.509 certificate that binds a user-specified name to the public part of the key pair.

NOTE: Only the Makecert from the .NET Framework 1.1 (Everett) has the capability to create test certificates that can be used successfully with the WSE! Because this can be difficult to find, I have included a copy of the newest Makecert.exe in the downloadable ZIP file for this solution at the bottom of the article.

Makecert.exe includes basic and extended options. Basic options are those most commonly used to create a certificate. Extended options provide more flexibility.

makecert [options] outputCertificateFile

Argument

Description

outputCertificateFile

The name of the .cer file where the test X.509 certificate will be written.

Basic Options

Option

Description

-n x509name

Specifies the subject's certificate name. This name must conform to the X.500 standard. The simplest method is to specify the name in double quotes, preceded by CN=; for example, "CN=myName".

-sk keyname

Specifies the subject's key container location, which contains the private key. If a key container does not exist, it will be created.

-sr location

Specifies the subject's certificate store location. Location can be either currentuser (the default), or localmachine.

-ss store

Specifies the subject's certificate store name that stores the output certificate.

-# number

Specifies a serial Number from 1 to 2^31-1. The default is a unique value generated by Makecert.exe.

-$ authority

Specifies the signing authority of the certificate, which must be set to either commercial (for certificates used by commercial software publishers) or individual (for certificates used by individual software publishers).

-?

Displays command syntax and a list of basic options for the tool.

-!

Displays command syntax and a list of extended options for the tool.

Extended Options

Option

Description

-a algorithm

Specifies the signature algorithm. Must be either md5 (the default) or sha1.

-b mm/dd/yyyy

Specifies the start of the validity period. Defaults to the certificate's creation date.

-cy certType

Specifies the certificate type. Valid values are end for end-entity, authority for certification authority, or both.

-d name

Displays the subject's name.

-e mm/dd/yyyy

Specifies the end of the validity period. Defaults to 12/31/2039 11:59:59 GMT.

-eku oid[,oid]

Inserts a list of comma-separated, enhanced key usage object identifiers (OIDs) into the certificate.

-h number

Specifies the maximum height of the tree below this certificate.

-ic file

Specifies the issuer's certificate file.

-ik keyName

Specifies the issuer's key container name.

-iky keytype

Specifies the issuer's key type, which must be signature, exchange, or an integer (such as 4).

-in name

Specifies the issuer's certificate common name.

-ip provider

Specifies the issuer's CryptoAPI provider name.

-ir location

Specifies the location of the issuer's certificate store. Location can be either currentuser (the default) or localmachine.

-is store

Specifies the issuer's certificate store name.

-iv pvkFile

Specifies the issuer's .pvk private key file.

-iy pvkFile

Specifies the issuer's CryptoAPI provider type.

-l link

Links to policy information (for example, a URL).

-m number

Specifies the duration, in months, of the certificate validity period.

-nscp

Includes the Netscape client-authorization extension.

-r

Creates a self-signed certificate.

-sc file

Specifies the subject's certificate file.

-sky keytype

Specifies the subject's key type, which must be signature, exchange, or an integer (such as 4).

-sp provider

Specifies the subject's CryptoAPI provider name.

-sv pvkFile

Specifies the subject's .pvk private key file. The file is created if none exists.

-sy type

Specifies the subject's CryptoAPI provider type.

Examples

The following command creates a test certificate and writes it to testCert.cer.

makecert testCert.cer

The following command creates a test certificate and writes it to testPAB.cer, using the subject's key container and the certificate subject's X.500 name, and writes it to the root store:

makecert -sk PAB -n "CN=PeterBromberg" -ss root -sr localmachine testPAB.cer

If you do not specify "localmachine" with the "sr" switch, or do not provide this switch, when this is executed from a Visual Studio.NET command prompt you will see the following dialog upon success:

 

NOTE: to enable a Visual Studio.NET "command prompt here" context menu item, just save the following lines to a text file with a .REG extension and double-click on the saved file:


Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Directory\shell\Command Prompt\Command]
@="Cmd.exe /k \"C:\\Program Files\\Microsoft Visual Studio .NET\\Common7\\Tools\\vsvars32.bat\" "

Voila! Context menu choice for a Command Prompt with VS.NET environment by simply right-clicking on any folder!

OK, so that was our hot-shot crash course in Certmgr.exe and Makecert.exe! Now we can build a BinarySecurityToken. First thing we need in order to include a certificate with a request is to tell .NET where to find it. Certificates are normally kept in a Certificate Store, and each user has a private store of certificates. The certificate itself is not really "private" since it is only a digitally - signed way to give out a public key. What's important is that it matches the private key in the secure key store of the person who is going to use the certificate. In this manner, you can digitally sign a message with your private key, and the receiver would be able to verify your digital signature using the matching public key that you have included along with the message. If you are somewhat fuzzy on how RSA encryption and public / private keys work, you might find my article "RSA Encryption Demystified" enlightening.

WSE provides classes to open Windows Certificate stores and access the certificates. If you would like to iterate some of your certificates programmatically, here is how to access them:

private X509CertificateStore store;
private void button1_Click(object sender, System.EventArgs e)
{
store = X509CertificateStore.CurrentUserStore(
X509CertificateStore.RootStore.ToString());
store.OpenRead();
foreach(X509Certificate cert in store.Certificates)
{
listBox1.Items.Add(cert.GetName());
}
store =X509CertificateStore.LocalMachineStore(X509CertificateStore.RootStore.ToString());
store.OpenRead();
foreach(X509Certificate cert in store.Certificates)
{
listBox1.Items.Add(cert.GetName());
}
}

You can also retrieve certificates by using one of the search methods, like so:

X509CertificateCollection cc=store.FindCertificateBySubjectString(strName);
foreach(X509Certificate cert3 in cc)
listBox1.Items.Add(cert3.GetName());

The above will match on a substring, which can be very useful.

Now that we have seen how to create certificates, manipulate them and access them programmatically, we can access the certificate on the server side in a manner not too different from the way we got our UsernameToken in the previous article. We are almost ready to start with our code, but before we do, let's make sure that ASP.NET has the required permissions to access the certificate store on the server.

Required Permissions forWSE to Sign or Decrypt with an X.509 Certificate

In order for WSE to obtain the X.509 private key from the local computer certificate store, it must have permission to do so. By default, only the owner and the System account can access the private key of a certificate. Also by default, the ASP.NET service runs under the ASPNET account, and that account does not have access to the private key.

To give the ASPNET account access to the private key, give the account under which ASP.NET is running Full Control access to the files containing the keys the WSE will need to retrieve in the following folder:
C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys

The account the ASP.NET worker process runs under is controlled by the <processModel> element in the Machine.config file. Set the userName attribute of the <processModel> element to specify the account ASP.NET runs under. By default, the userName attribute is set to the special machine account, which maps to the low-privileged ASPNET user account created when the .NET Framework SDK is installed.

  • Open Windows Explorer.
  • Navigate to the C:\Documents and Settings\All Users\Application Data\Microsoft\Crypto\RSA\MachineKeys folder.
  • Select the files containing the keys that the WSE will need to retrieve.
  • From the File menu, select Properties.
  • On the Security tab, add the ASPNET account and select the Full Control option.

    Note: Determining which key file in the MachineKeys folder is associated with a certificate can be difficult. One easy method is to note the creation date and time when creating a new certificate. When you view the files in the MachineKeys directory, check the Date Modified field for the corresponding date and time.

Creating the Web Service Methods

At the server, we need to specify where the WSE searches for X.509 certificates when it attempts to retrieve or verify a certificate. Typically, a client application sets the storeLocation attribute to CurrentUser and an XML Web service sets it to LocalMachine. The default is LocalMachine.
This attribute also specifies the certificate store the CA certificate chain is retrieved from during the signature verification process. The signature verification process occurs when a SOAP message that is signed is received to verify the integrity of the signature. If the SOAP message recipient is an XML Web service, then the WSE always retrieves the CA certificate chain from the LocalMachine, unless the process identity for ASP.NET (ASPNET by default) is changed to an account with log-on permissions. The identity of the ASP.NET account is specified in the <processModel> element. Since in this case we are also using a test certificate created by Makecert.exe, we also need to specify that the AllowTestRoot attribute is set to "true". our <x509 ... element goes in the <configuration> <microsoft.web.services> <security> section of web.config, and should look like the following:

<x509 storeLocation="CurrentUser" verifyTrust="true" allowTestRoot="true" />

We also need to add the X509 class to our Webservice (both on the client and the server) with:

using Microsoft.Web.Services.Security.X509;

So now, on the server, our WebMethod will look like the following:

[WebMethod]
public DataSet CustOrderHist(string CustId)
{
// Only accept SOAP formatted requests
SoapContext requestContext = HttpSoapContext.RequestContext;
if(requestContext==null)
{
throw new ApplicationException("Non-SOAP request!");
}
// check all the tokens in Tokens Collection for a X509SecurityToken
bool valid=false;
try
{
foreach(SecurityToken tkn in requestContext.Security.Tokens)
{
if(tkn is X509SecurityToken)
valid=true;
}
}
catch(Exception ex)
{
throw new Exception( ex.Message + ": " + ex.InnerException.Message);
}

if (valid==false)
throw new ApplicationException("Invalid Credentials.");
SqlConnection cn;
SqlDataAdapter da;
DataSet ds ;
cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());
cn.Open();
da = new SqlDataAdapter("custorderHist '" +CustId + "'", cn);
ds = new DataSet();
da.Fill(ds, "CustOrderHist");
return ds;
}

Not very different at all from how we handled UsernameTokens! Now on the client, our method will look like this:

private void Button1_Click(object sender, System.EventArgs e)
{
store = X509CertificateStore.CurrentUserStore(
X509CertificateStore.RootStore.ToString());
store.OpenRead();
localhost.CertificateServiceWse wse=new localhost.CertificateServiceWse();
X509CertificateCollection col=(X509CertificateCollection)store.FindCertificateBySubjectString(txtCertificate.Text);
X509Certificate cert =null;
try
{
cert = col[0];
}
catch(Exception ex)
{
lblMessages.Text="Certificate not Found!";
return;
}
wse.RequestSoapContext.Security.Tokens.Add (new X509SecurityToken(cert));
try
{
DataSet ds=wse.CustOrderHist(txtCustID.Text);
DataGrid1.DataSource=ds;
DataGrid1.DataBind();
}
catch(Exception ex)
{
DataGrid1.Visible=false;
lblMessages.Text=ex.Message;
}

}

Under the hood, our SOAP Header now sports a nice addition, the BinarySecurityToken element:

<soap:Header>
<wsrp:path soap:actor="http://schemas.xmlsoap.org/soap/actor/next" soap:mustUnderstand="1"
xmlns:wsrp="http://schemas.xmlsoap.org/rp">
<wsrp:action>http://tempuri.org/CustOrderHist</wsrp:action>
<wsrp:to>http://localhost/WSECertificateAuth/CertificateService.asmx</wsrp:to>
<wsrp:id>uuid:e4992608-7930-434c-9a54-0453ac189d0d</wsrp:id>
</wsrp:path>
<wsu:Timestamp xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility">
<wsu:Created>2002-12-31T15:36:34Z</wsu:Created>
<wsu:Expires>2002-12-31T15:41:34Z</wsu:Expires>
</wsu:Timestamp>
<wsse:Security soap:mustUnderstand="1" xmlns:wsse="http://schemas.xmlsoap.org/ws/2002/07/secext">
<wsse:BinarySecurityToken ValueType="wsse:X509v3" EncodingType="wsse:Base64Binary"
xmlns:wsu="http://schemas.xmlsoap.org/ws/2002/07/utility"
wsu:Id="SecurityToken-595c0db0-7b1c-4038-9005-d0e4566efb1c">
MIIBcTCCARugAwIBAgIQLIB/4r0Rf4RL7upb3E2lAzANBgkqhkiG9w0BAQQFADA
WMRQwEgYDVQQDEwtSb290IEFnZW5jeTAeFw0wMjEyMzAyMDQyMjVaFw0zOTEyMz
EyMzU5NTlaMBAxDjAMBgNVBAMTBVBldGV5MFwwDQYJKoZIhvcNAQEBBQADSwAwS
AJBAMDRte7rxIpqBT0SYSXpw7773Ex0fiUfzFapAxCh4O2PQctO2UiiM4xzA/UZ
qfo08rUZLltT3XPWOEMxwKxrxmsCAwEAAaNLMEkwRwYDVR0BBEAwPoAQEuQJLQY
dHU8AjWEh3BZkY6EYMBYxFDASBgNVBAMTC1Jvb3QgQWdlbmN5ghAGN2wAqgBkih
HPuNSqXDX0MA0GCSqGSIb3DQEBBAUAA0EAEKQ23JTWrFCUdmck/CUkv8ruAgEyU
BOo14RkWRSiLfT17zf4zKDGuO0jJRZHBNsDhfUeWjy/9e4d8G5czgTpgA==
</wsse:BinarySecurityToken>
</wsse:Security>
</soap:Header>


With this, we are 95 percent of the way to "First Base" -- we've learned how to code the Web Service to look for a valid X509 Certificate in the SOAP headers from the Client, and we've learned how to retrieve and send a specific certificate from the local machine store for the client and add it to the SOAP headers according to the WS-Security specifications with WSE. However, the mere act of sending a certificate in our SOAP messages is not much of a way to authenticate anything. The idea is to sign some entity (SOAP Body, whatever) with the private key corresponding to the public key contained in the certificate that is sent. Then, the recipient can use the public key to determine that the message is intact and has not been altered, and that the sender is who they say they are.

With WSE, creating these digital signatures is easy and fast. We have a SoapContext.Security.Elements collection that allows us to add various WS-Security conformant elements. So by simply adding to the code above where we retrieved and included our certificate, we can now use it to sign the request as well. On the client, this only requires a few additional lines of code:

wse.RequestSoapContext.Security.Tokens.Add (new X509SecurityToken(cert));
X509SecurityToken crtTkn = new X509SecurityToken(cert);
wse.RequestSoapContext.Security.Tokens.Add(crtTkn);
wse.RequestSoapContext.Security.Elements.Add(new Signature(crtTkn));

Now we'll need to verify the digital signature on the server in our Web Service. Our final WebMethod looks like this:

public DataSet CustOrderHist(string CustId)
{
// Only accept SOAP formatted requests
SoapContext requestContext = HttpSoapContext.RequestContext;
if(requestContext==null)
{
throw new ApplicationException("Non-SOAP request!");
}
// check all the tokens in Tokens Collection for a X509SecurityToken
bool valid=false;
try
{
foreach(SecurityToken tkn in requestContext.Security.Tokens)
{
if(tkn is X509SecurityToken)
{
foreach(Object elem in requestContext.Security.Elements)
{
if(elem is Signature)
{
Signature sign=(Signature)elem;
// Verify it signs the body of the request---
if(sign!=null && (sign.SignatureOptions & SignatureOptions.IncludeSoapBody)!=0)
{
if(sign.SecurityToken is X509SecurityToken)
valid=true;
}
}
}
}
}
}
catch(Exception ex)
{
throw new Exception( ex.Message + ": " + ex.InnerException.Message);
}

if (valid==false)
throw new ApplicationException("Invalid Credentials.");
SqlConnection cn;
SqlDataAdapter da;
DataSet ds ;
cn = new SqlConnection(System.Configuration.ConfigurationSettings.AppSettings["SqlConn"].ToString());
cn.Open();
da = new SqlDataAdapter("custorderHist '" +CustId + "'", cn);
ds = new DataSet();
da.Fill(ds, "CustOrderHist");
return ds;
}
}

Voila! We have now retreived an X509 Certificate on the client, included it in the SOAP Headers, and used it to sign the SOAP Body. On the server, we checked to see if a valid X509 Certificate was used and verified that it indeed has been used to sign the message Body using the SignatureOptions property. Since we now know who sent the message and that it arrived at the Web Service unaltered, we send back the requested DataSet. You can download the full solution at the link below. Of course, you will need to either create your own certificate, or use one that is common to both client and server machines, and change the various portions of the code where it is retrieved and referenced. In the next installment of this series, we'll look more deeply at the various SignatureOptions properties, perform partial message signing as well as XML Encryption, and look into the WSE "pipeline", including how to use DIME attachments.

Download the code that accompanies this article


Peter Bromberg is a C# MVP, MCP, and .NET consultant who has worked in the banking and financial industry for 20 years. He has architected and developed web - based corporate distributed application solutions since 1995, and focuses exclusively on the .NET Platform.