|
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. Pete's samples at GotDotNet.com have been downloaded over 41,000 times. You can read Peter's UnBlog Here. --><-- NOTE: Post QUESTIONS on FORUMS! |  |
|