Last week's exercise began teaching you about the security and policy enhancements for Web Services 2.0. This article, covering the second exercise in the lab, picks up where last week's left off. It was written by MSDN Virtual Labs.
Exercise 2 Securing Web Services with X.509 Certificates
Scenario
In the last exercise you were able to secure SecureInvoiceServiceA by requiring UsernameToken authentication, a message signature, and encryption.
Using UsernameToken for signing and encrypting messages is not the most secure option. Using a binary security token, such as an X.509 certificate, offers a higher level of security. In this exercise, you'll walk through the process of installing some sample certificates and configuring your application to use them for signature and encryption purposes.
You'll be working in the Exercises\B\before directory.
TasksDetailed steps
Installing the Sample Certificates
WSE 2.0 provides two sample certificates for you to use while testing your WSE 2.0 applications. These two certificates were generated by the makecert tool provided in the Microsoft Platform Software Development Kit. In order to begin using these certificates in your applications, you must first install them on your computer.
Note: you should not use these sample certificates in a production environment. You must contact a certificate authority, request your own certificate, and follow the procedures shown here to use it.
Open an MMC console by pressing Start, press Run, type mmc, and then click OK.
On the File menu, click Add/Remove Snap-in
Click Add, under Snap-in, double-click Certificates.
Click My user account to add the certificates for the current user. Click Finish.
Click Add, under Snap-in, double-click Certificates.
Click Computer account for the local machines certificates.
Click Next.
Click Finish.
Click Close.
Click OK.
Your MMC window should now look something like this:
In the console tree, click Certificates - Current User |Personal.
Open the Certificate Import wizard by clicking Action | All Tasks |Import….
Click Next.
In the File Name field, type C:\Program Files\Microsoft WSE\v2.0\Samples\Sample Test Certificates\Client Private.pfx.
Click Next.
In the Password field, type wse2qs.
Click Next.
Click Next.
Click Finish.
Click OK.
Note: this certificate will be used by our client application to sign messages sent to the service. It could also be used to identify the client for authentication purposes.
In the console tree, click Certificates(Local Computer) | Personal.
Open the Certificate Import wizard by clicking Action | All Tasks | Import….
Click Next.
In the File Name field, type C:\Program Files\Microsoft WSE\v2.0\Samples\Sample Test Certificates\Server Private.pfx.
Click Next.
In the Password field, type wse2qs.
Click Next.
Click Next.
Click Finish.
Click OK.
Note: this certificate will be used to encrypt messages between the applications. The client application will use the public key to encrypt the message and the service will use the private key to decrypt the message. The client needs to have the public portion of the certificate available in the Current User store.
In the console tree, click Certificates - Current User | Other People.
Note: if you don't have an Other People store under Current User, open Internet Explorer, click Tools |Internet Options | Content, and click the Certificates button. Click the Other People tab in the certificates dialog. You can import the certificate by clicking Import….and then following steps hh-mm.
If importing in the mmc, open the Certificate Import wizard by clicking Action | All Tasks | Import….
Click Next.
In the File Name field, type C:\Program Files\Microsoft WSE\v2.0\Samples\Sample Test Certificates\Server Public.cer.
Click Next.
Click Next.
Click Finish.
Click OK.
If importing through Internet Explorer, click Close, click OK, close Internet Explorer, and return to the mmc.
Close the mmc.
If prompted to save settings, click No.
Note: this certificate only contains the public portion of Server Private.pfx. The client will use this to encrypt messages and the server will use the private key installed in the Local Machine store to decrypt the messages.
Once you have the certificates installed on your computer, you can begin using them to sign and encrypt messages. In this step, you're going to sign messages sent from the client application using the client certificate (found in the Current User store).
Return to Visual Studio .NET 2003.
Click File | Open | Project.
Navigate to C:\Microsoft Hands-on-Lab\DEV-HOL34\VB \ Exercises\B\before.
Select SecureInvoiceB.sln and click Open.
Note: this solution is equivalent to the one you implemented in the last exercise. It uses a UsernameToken to authenticate, sign, and encrypt message.
Open the code view of InvoiceManagerForm.vb in the SecureInvoiceClient project.
Add an Imports statement for Microsoft.Web.Services2. Security.X509:
Add a new method to the InvoiceManagerForm class named GetX509Token. The method should take two strings (one to represent certificate's key identifier and another to represent the certificate store name that you're going to retrieve the certificate from), and return an X509SecurityToken as illustrated here:
... Private Function GetX509Token(ByVal keyId As String, ByVal storeId As String) As X509SecurityToken ... End Function 'GetX509Token ...
Within the GetX509Token method, you need to retrieve an X509CertificateStore object for the specified store, open the store for reading, and find the certificate based on its key identifier. Then, return the identified certificate as a new X509SecurityToken object. Here's one way to write this code:
... Private Function GetX509Token(ByVal keyId As String, ByVal storeId As String) As X509SecurityToken Dim store As X509CertificateStore = X509CertificateStore.Current UserStore(storeId) store.OpenRead() Dim certs As X509CertificateCollection = store.FindCertificate ByKeyIdentifier( Convert.FromBase64 String(keyId)) store.Close() Return New X509SecurityToken(CType(certs(0), X509Certificate)) End Function 'GetX509Token ...
Go to the ConfigureProxy method in InvoiceManagerForm. This is where we need to modify the code to use the client certificate (found in the Current User store) for signing the message.
Within ConfigureProxy, remove all code within the function. You'll be replacing this code with certificate signing and encryption. You'll be replacing this code with certificate signing and encryption.
Within ConfigureProxy, call your new GetX509Token method. For the key identifier, specify "gBfo0147lM6cKnTbbMSuMVvm FY4=", and for the store name, specify X509CertificateStore.MyStore as illustrated here:
... ' Retreive client certificate for signing Dim clientToken As X509SecurityToken = GetX509Token( "gBfo0147lM6cKnTbbMSuMVvm FY4=", X509CertificateStore.MyStore) ...
Note: You can use the X509 Certificate Tool (called WseCertificate2.exe) to determine the key identifier for a given certificate. This tool ships with WSE 2.0. You can find it in the following directory: C:\Program Files\Microsoft WSE\v2.0\Tools\Certificates. Here's what it looks like:
Add the returned X509SecurityToken to the proxy's RequestSoapContext.Security.Tokens collection as illustrated here:
... ' Retreive client certificate for signing Dim clientToken As X509SecurityToken = GetX509Token( _ "gBfo0147lM6cKnTbbMSuMVvm FY4=", X509CertificateStore.MyStore) ' Add UsernameToken for authentication purposes proxy.RequestSoapContext. Security.Tokens.Add(login. Token) ' Must add client token to message for signature processing proxy.RequestSoapContext. Security.Tokens.Add(client Token) ...
Note: You should continue to send the UsernameToken as well. The UsernameToken will still be used for authentication and authorization purposes. Now you're going to use the certificate for signing the message.
Instantiate a new MessageSignature based on the X509SecurityToken and add it to the proxy's RequestSoapContext.Security. Elements collection as illustrated here:
... ' Retreive client certificate for signing Dim clientToken As X509SecurityToken = GetX509Token( _ "gBfo0147lM6cKnTbbMSuMVvm FY4=", X509CertificateStore.MyStore) ' Add UsernameToken for authentication purposes proxy.RequestSoapContext. Security.Tokens.Add(login. Token) ' Must add client token to message for signature processing proxy.RequestSoapContext. Security.Tokens.Add(client Token) proxy.RequestSoapContext. Security.Elements.Add( _ new MessageSignature(clientToken)) ...
Open WseSecurityHelpers.vb in the SecureInvoiceServiceB project and comment out the call to CheckForEncryption in GetUsernameToken. This makes it so the service doesn't require encryption for the time being.
Note: you'll add X.509-based encryption in the next step and add this line of code back in.
Right-click on the SecureInvoiceServiceB project in Solution Explorer and click WSE Settings 2.0. Navigate to the Security tab and check Allow TestRoots in the X.509 CertificateSettings section.
Click Yes to confirm the enabling of test roots.
Press OK to close the tool.
Build the solution and run the client application. Verify that if everything works as before (except for requiring encryption).
Open OutputTrace.webinfo in the SecureInvoiceClient output directory (bin).
Notice that the message now contains a BinarySecurityToken element in addition to the UsernameToken from before.
Now you're going to use a certificate to encrypt the data sent in the body of the message. You'll use the public certificate found in Current User\Other People to encrypt the message and WSE 2.0 will use the private certificate found on Local Machine\Personal to decrypt the message.
You'll continue working in SecureInvoiceB.sln for this step.
Open InvoiceManagerForm.vb in the SecureInvoiceClient project.
Return to the ConfigureProxy method in InvoiceManagerForm.vb. You're going to add code to this method to encrypt the body of the message. The first thing you need to do is call GetX509Token to retrieve the server token from the Current User\Other People certificate store as illustrated here:
... ' Retreive client certificate for signing Dim clientToken As X509SecurityToken = GetX509Token( _ "gBfo0147lM6cKnTbbMSuMVvm FY4=", X509CertificateStore.MyStore) Dim serverToken As X509SecurityToken = GetX509Token( "bBwPfItvKp3b6TNDq+14qs58 VJQ=", X509CertificateStore.Other People) ...
Then, towards the end of the method, instantiate an EncryptedData object based on the serverToken you retrieved from the certificate store and add it to the proxy's RequestSoapContext.Security. Elements collection as illustrated here:
... ' ConfigureProxy ' Retreive client certificate for signing Dim clientToken As X509SecurityToken = GetX509Token( _ "gBfo0147lM6cKnTbbMSuMVvm FY4=", X509CertificateStore.MyStore) Dim serverToken As X509SecurityToken = GetX509Token( _ "bBwPfItvKp3b6TNDq+14qs58VJQ =", _ X509CertificateStore. OtherPeople) ' Add UsernameToken for authentication purposes proxy.RequestSoapContext. Security.Tokens.Add(login. Token) ' Must add client token to message for signature processing proxy.RequestSoapContext. Security.Tokens.Add(client Token) proxy.RequestSoapContext. Security.Elements.Add( _ new MessageSignature(clientToken)) ' Encrypt the body proxy.RequestSoapContext. Security.Elements.Add( new EncryptedData(serverToken)) ...
When the Web service receives the encrypted message, it needs to know which certificate store to look for the certificate in. Open web.config in the SecureInvoiceB project and add a storeLocation="LocalMachine" attribute to the x509 element in configuration/microsoft.web. services2/security.
Open WseSecurityHelpers.vb in the SecureInvoiceServiceB project and uncomment the call to CheckForEncryption in the GetUsernameToken method. This makes it so the service requires encryption again.
Before you can run the application, you have to give the ASPNET account read access to the private key of the server certificate. Otherwise it won't be able to read it during the decryption process.
Press Start | Run, and enter WseCertificate2.exe to launch the WSE X.509 Certificate Tool.
Change the Certificate Location to Local Computer and Store Name to Personal and press the Open Certificate button.
Select the WSE2QuickStartServer certificate and press OK. Then, press the View Private Key FileProperties… button.
Navigate to the Security tab and give the local machine's ASPNET account read access to the private key using the Add… button.
Note: If the Security tab is not present, click Start | Control Panel| Folder Options. In the View tab, click to deselect the Use Simple FileSharing (Recommended) option at the bottom of the AdvancedSettings list and click Apply followed by Ok Then, click Cancel and repeat from step i.
In the Enter the object names to select box, type ASPNET and click Check Names.
Click OK to close the dialog.
Click Apply.
Click OK.
Close the WSE X.509 Certificate Tool.
Return to Visual Studio .NET 2003.
Build the solution and run the client application. Verify that everything works.
Close the Invoice Manager application.
Refresh and view the OutputTrace.webinfo pane in Visual Studio .NET 2003.
Notice that the body of the SOAP message is now encrypted, and as a result, you should no longer be able to read it. It should look something like this:
In the last step, you wrote code to encrypt the body of the SOAP message. You may also wish to encrypt headers on an individual basis. For example, one header that you may wish to encrypt is the UsernameToken header, especially when including the plain text password in the header. Doing this would allow you to leverage Windows authentication without having to write a custom UsernameToken manager or send the messages over HTTPS.
You'll continue working in SecureInvoiceB.sln for this step.
Open InvoiceManagerForm.vb in the SecureInvoiceClient project.
Return to the ConfigureProxy method in InvoiceManagerForm.vb. You're going to add code to this method to encrypt the UsernameToken header in the message.
At the end of the method, instantiate another EncryptedData element based on the serverToken (like in the last step) but this time you need to specify the reference id of the UsernameToken header as the second parameter to the constructor. Then, add this new EncryptedData element to the proxy's RequestSoapContext.Security. Elements collection as illustrated here:
... ' ConfigureProxy ' Encrypt the body proxy.RequestSoapContext. Security.Elements.Add( _ new EncryptedData(serverToken))
proxy.RequestSoapContext. Security.Elements.Add( new EncryptedData(serverToken, string.Format("#{0}", login.Token.Id)))
Open login.vb in the SecureInvoiceClient project, navigate to the button1_Click method, and change the password option to PasswordOption.SendPlainText when instantiating the UsernameToken.
Open web.config in the SecureInvoiceServiceB project and comment out the <securityTokenManager> element in configuration/microsoft.web.services2 /security. Doing this will disable the custom UsernameToken manager that you've been using.
Note: Now that you've disabled the custom UsernameToken manager, you'll have to provide the passwords that you setup for the local user accounts when logging in. The password “password” will no longer work.
Build the solution and run the client application. Make sure you specify the credentials for the local user accounts (instead of those required by the custom UsernameToken manager) when logging in. Verify that it everything works.
Close the application.
Refresh and view the OutputTrace.webinfo file. Notice that the body of the SOAP message is encrypted as well as the UsernameToken header. You should not be able to read the password, which was originally sent in plain text.