Recently, I needed to incorporate a java web service (not developed in-house) into my client’s existing ASP.Net website. The web service calls need to be secured by utilizing X509 certificate over SSL.
Surprisingly, I found very little information on this topic (there are some, but they are scattered here and there and the information was not as complete as I hoped). And although the code to implement web service call using certificate is trivial (a few lines), debugging can be quite challenging. Here I will share some my experience. This discussion applies to calling all web services (not just limited to jws) from ASP.Net using certificates. Do not confuse what we are trying to achieve here with WSE (Web Service Enhancement), as communication with web services using X509 certificate can be done with or without WSE, it is an orthogonal concept. Here we are just using the plain web service, no WSE.
- Importing certificates.
The import process differs slightly for Windows XP and Windows Server 2003. For either operating system, you will need to import the certificate to the local machine store instead of personal store. To do this, run mmc (from start->run), and choose Add Snap-in (Ctrl+M), select Add to add “Certificates” to the certificate manager. When prompted for location, select Computer Account, and then Local Computer (the default is user account). We assume that the certificate is imported to one of the root certificate authorities (e.g. Third-Party Root Certificate Authorities).
ASP.Net typically runs under ASPNET_WP user under Windows XP and IIS_WPG under Windows 2003 server by default. On a Windows XP machine, the above steps are sufficient for ASP.Net to utilize the certificate imported. But for Windows 2003 server, you will need to grant the rights to the IIS_WPG user so that ASP.Net can access the certificate. This can be achieved by using Microsoft’s winhttpcertcfg.exe utility (download here) as shown below:
winhttpcertcfg.exe -g -c LOCAL_MACHINE\Root -s "{certificate name}" -a "IIS_WPG"
Note that ASP.Net worker process might be running under an account other then the ones mentioned above (i.e. customized to run under a particular user account), and in this case, you will need to grant permission to whatever process the work process is running under.
- Access certificate in code
To use the certificate in code, only a few lines are needed (C#):
X509Store store = new X509Store(StoreName.Root, StoreLocation.LocalMachine);
store.Open(OpenFlags.ReadOnly); X509Certificate2Collection col =
store.Certificates.Find(X509FindType.FindBySerialNumber, "{serial number no space}", true);
//service is the webservice that need to //be authenticated using X509 certificate
TestWebService service = new TestWebService();
//Note, we should find the certificate from the the
//certificate is imported correctly and the serial
service.Test();
}
The code above assumes that you are using .Net 2.0, since quite a few earlier methods in framework 1.0 have been deprecated. The serial number of a certificate can be found in the certificate store by clicking on the properties of the certificate:
Note that you will need to remove all the spaces in the serial number. Alternatively, you may also use any other X509FindType’s (e.g. FindByIssuerName, FindBySubjectKey, etc.) to retrieve the certificate in the certificate store.
- Debugging
The first step in debugging is to make sure that you actually can access the certificate in code. Set a breakpoint at col.Count and make sure that it is 1. If col.Count is 0, you will need to go back and inspect whether step 1 and 2 were performed correctly.
So far we have verified that the certificate was imported correctly. Making sure that the certificate actually works would take a bit more patience if you are stuck with mysterious errors.
One of the most common problems when you try to run the program after utilizing thee certificate is the error
The request was aborted: Could not create SSL/TLS secure channel.
This is typically caused by incorrect certificate (e.g. key length, certificate type mismatch) settings. To help debugging the exact cause of this problem, you may want to include some trace listeners in your web.config (or app.config) file (see Durgaprasad Gorti’s Blog).
<system.diagnostics>
<trace autoflush="true"/>
<sources>
<source name="System.Net" maxdatasize="1024">
<listeners>
<add name="TraceFile"/>
</listeners>
</source>
<source name="System.Net.Sockets" maxdatasize="1024">
<listeners>
<add name="TraceFile"/>
</listeners>
</source>
</sources>
<sharedListeners>
<add name="TraceFile" type="System.Diagnostics.TextWriterTraceListener" initializeData="trace.log"/>
</sharedListeners>
<switches>
<add name="System.Net" value="Verbose" />
<add name="System.Net.Sockets" value="Verbose" />
</switches>
</system.diagnostics>
When you run your ASP.Net project with this configuration section, you will see a trace file (trace.log) generated in your project folder when you run your web project. When your code errors out with the exception
The request was aborted: Could not create SSL/TLS secure channel.
Take a look at trace file, you should see that the service found the certificate on your computer (assume that the certificate is imported correctly, if not go to step 1 and 2), and you might also find something similar to the following (just an example to give you an idea what to look for):
System.Net Information: 0 : [1916] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = 6019cc0:1749d0, targetName = {IP Address}, inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation)
System.Net Information: 0 : [1916] InitializeSecurityContext(In-Buffer length=7, Out-Buffer length=0, returned code=IllegalMessage).
System.Net Error: 0 : [1916] Exception in the HttpWebRequest#37256635::EndGetResponse – The request was aborted: Could not create SSL/TLS secure channel.
The above trace information generally indicates that the certificate used is not correct. You might want to double check the RSA key length and certificate type to ensure that they are correct before investigating further. There is a lot of information in the trace file, so you might need to comb through it a couple of times before you find the problem. If your certificate is a test certificate you might want to change the last argument passed to store.Certificates.Find is set to false because the test certificate might not be valid.
Another common problem for this error is that the account under which ASP.Net is running does not have proper permissions to access the provided certificate. This situation typically happens on Windows 2003 server where ASP.Net is running under IIS_WPG. This situation can be confirmed from the following trace messages (occurs right before InitializeSecurityContext):
System.Net Information: 0 : [4608] AcquireCredentialsHandle(package = Microsoft Unified Security Protocol Provider, intent = Outbound, scc = System.Net.SecureCredential)
System.Net Error: 0 : [4608] AcquireCredentialsHandle() failed with error 0X8009030D.
System.Net Information: 0 : [4608] AcquireCredentialsHandle(package = Microsoft Unified Security Protocol Provider, intent = Outbound, scc = System.Net.SecureCredential)
System.Net Error: 0 : [4608] Exception in the HttpWebRequest#34090260::EndGetResponse – The request was aborted: Could not create SSL/TLS secure channel.
If you get the AcquireCredentialsHandle() failed message, you might want to double check step 1 to see if you have granted the ASP.Net worker process the correct permission (using winhttpcertcfg.exe).
A successful hand shake looks like this (only a small portion is shown):
System.Net Information: 0 : [3048] SecureChannel#56552832 – Certificate is of type X509Certificate2 and contains the private key.
System.Net Information: 0 : [3048] AcquireCredentialsHandle(package = Microsoft Unified Security Protocol Provider, intent = Outbound, scc = System.Net.SecureCredential)
System.Net Information: 0 : [3048] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = (null), targetName = {IP Address}, inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation)
System.Net Information: 0 : [3048] InitializeSecurityContext(In-Buffer length=0, Out-Buffer length=70, returned code=ContinueNeeded).
System.Net.Sockets Verbose: 0 : [3048] Socket#49783578::Send()
System.Net.Sockets Verbose: 0 : [3048] Data from Socket#49783578::Send
System.Net.Sockets Verbose: 0 : [3048] 00000000 : 16 03 01 00 41 01 00 00-3D 03 01 45 70 5F AF FD : ….A…=..Ep_..
System.Net.Sockets Verbose: 0 : [3048] 00000010 : BA F6 F1 25 72 36 E5 48-A6 A6 C0 8D C7 67 C9 C8 : …%r6.H…..g..
System.Net.Sockets Verbose: 0 : [3048] 00000020 : DE 6F C1 D2 23 C3 37 66-9B 0F 7A 00 00 16 00 04 : .o..#.7f..z…..
System.Net.Sockets Verbose: 0 : [3048] 00000030 : 00 05 00 0A 00 09 00 64-00 62 00 03 00 06 00 13 : …….d.b……
System.Net.Sockets Verbose: 0 : [3048] 00000040 : 00 12 00 63 01 00 : …c..
System.Net.Sockets Verbose: 0 : [3048] Exiting Socket#49783578::Send() -> 70#70
System.Net.Sockets Verbose: 0 : [3048] Socket#49783578::Receive()
System.Net.Sockets Verbose: 0 : [3048] Data from Socket#49783578::Receive
System.Net.Sockets Verbose: 0 : [3048] 00000000 : 16 03 01 00 2A : ….*
System.Net.Sockets Verbose: 0 : [3048] Exiting Socket#49783578::Receive() -> 5#5
System.Net.Sockets Verbose: 0 : [3048] Socket#49783578::Receive()
System.Net.Sockets Verbose: 0 : [3048] Data from Socket#49783578::Receive
System.Net.Sockets Verbose: 0 : [3048] 00000005 : 02 00 00 26 03 01 61 66-83 6C 31 45 99 CE EC 91 : …&..af.l1E….
System.Net.Sockets Verbose: 0 : [3048] 00000015 : 32 C8 7D 7B 23 41 36 8B-04 C9 99 33 97 6E B3 9D : 2.}{#A6….3.n..
System.Net.Sockets Verbose: 0 : [3048] 00000025 : 32 79 2C F7 93 94 00 00-04 00 : 2y,…….
System.Net.Sockets Verbose: 0 : [3048] Exiting Socket#49783578::Receive() -> 42#42
System.Net Information: 0 : [3048] InitializeSecurityContext(credential = System.Net.SafeFreeCredential_SECURITY, context = 6008b48:f6028, targetName = {IP Address}, inFlags = ReplayDetect, SequenceDetect, Confidentiality, AllocateMemory, InitManualCredValidation)
System.Net Information: 0 : [3048] InitializeSecurityContext(In-Buffer length=47, Out-Buffer length=0, returned code=ContinueNeeded).
Also, consuming web services via SSL using X509 certificate does not require the web services being accessed via port 443 (standard SSL port), it can be any port as long as the URL is prefixed with https. Nor does it require the whole website being secured by SSL. It is perfectly fine for a non secure website to access a secure web service. The certificate in our scenario is used to authenticate the caller of a web service and the service being called does not care whether or not the call is originated from a secure website as long as the certificate presented is valid.
Even though calling web services can be done securely using X509 certificates without using WSE (see link at the end), it is highly recommended that WSE be used whenever it is possible because WSE implements more security features then just the initial handshake.
Here are some related links you might find worth reading:
Windows HTTP Services Certificate Configuration Tool (WinHttpCertCfg.exe)
Certificate Revocation and Status Checking
Securing XML Web Services Created Using ASP.NET