The King of Pain: Secured Asynchronous Web Services
Flash back to 1990. I'm writing a program to compute "substantially equal periodic payments" from annuities used in retirement plans and listening the latest tape from The Police. One song resonates with me. King of Pain. I'm sure there's programming that is a lot more painful than computing SEPPs, but at the time, it felt like Root Canal.
Flash forward to 2004. I'm working with Java and Security guru Matt Payne on a presentation we're giving later this week for Nebraska's CERT conference on Web Services. You know Web Services, right? This stuff the promises to make platform inter-operability painless?
It's not quite Root Canal. In fact, I have been rather surprised by how easy its really been. Granted, we aren't exactly trying to boil the Ocean. I'm writing some .NET clients for his Java Web Services and he's writing some Java clients for my .NET Web Services. Everything works pretty smoothly. Until you start trying to make things a little more secure, or if you push on performance.
I'll talk first about my woes. The first real challenge I ran into was a scenario where we wanted to exchange X.509 certificates with each other. Now, neither of us have what you'd call deep pockets, so setting up any kind of SSL was probably going to have to be done with self-signed certificates. Generating such a certificate on my side wasn't too hard thanks to the SelfSSL tool in the IIS6 Resource Kit. Getting Visual Studio and the .NET runtime to trust it was. I'll give Microsoft kudos -- they've made the process of using a self-signed certificate for Web Services a bit of hassle so that it becomes an unappealing vector for exploit. At the same time, it's a bit ugly to work through for yourself. Basically, if you're calling a Web Service on an SSL channel using a self-signed certificate, you need to override .NET security policy. While there's a few ways to do that, the one I like best is to do it programmatically. Start by adding a class like this to your application:
Public Class OverrideCertificatePolicy
Implements ICertificatePolicy
Public Function CheckValidationResult(ByVal srvPoint As ServicePoint, _
ByVal cert As X509Certificate, ByVal request As WebRequest, _
ByVal certificateProblem As Integer) _
As Boolean Implements ICertificatePolicy.CheckValidationResult
Return True
End Function
End Class
When you've constructed an instance of the proxy class for the Web Service you want to call, you need to add a line of code like this:
System.Net.ServicePointManager.CertificatePolicy = New OverrideCertificatePolicy
There's more discussion of this class, the underlying problem and other solutions in KB823177. Note that you really should inspect the certificate and the request to in this class to make sure it only overrides the request to service you are calling.
The next issue I ran into is when we wanted to force the use of a X.509 certificate for log-on. IIS6 supports the mapping of a certificate to a domain or local machine account -- that's not the real challenge, though. Getting the certificate request from and to a Linux client is. I had set up Certificate Services and exposed its Web interface. However, Certificate Services would always fail to generate certificates for users who submitted their request using FireFox (it complains that the subject name is too long.) So I generated the certificate for Matt using a Windows computer and put the certificate out for download. We then found out that unlike IE, FireFox doesn't support the saving of a certificate file to disk! We've worked around that. My recommendation is that if you find yourself needing to do this, use a USB drive of some kind to transport the certificate.
Eventually I ran into a "Kent being Dumb" problem too. With IIS6, you can require a known X.509 certificate to be presented before IIS6 will authenticate the user. But that's not the same as forcing a log-on. For a few hours, I battled with this. Although the certificate request part was working fine, I was still getting prompted to log-on. I didn't think that should be happening since, in the mapping process, you are providing an account and password for the certificate. Turns out that I had anonymous access turned off and the ASPNET account didn't have an ACE in the DACL for the files in site in question. If you want to force a log-on using on an X.509, anonymous access must be enabled (or IIS still sends the 403.3 response, despite the certificate) and the anonymous account needs at least a read ACE in the target file DACL.
My latest -- and hopefully last -- problem happens whenever I call Matt's services asynchronously. Basically, I'm calling his service once for each record in a query resultset, and that resultset has about 1400 rows. That's a lot of wait time if I'm going to call each row synchronously. So I decided to use the Begin method call supported by the proxy class generated from the WSDL. That lets me spin up a great number of concurrent requests, but the problem I'm running into now is figuring out when all of the requests are done processing so I can update the database on my end. My original code looked something like this:
Private Sub TransmitHandler(ByVal iResult As IAsyncResult)
UpdateStatus(CInt(iResult.AsyncState))
End Sub
Public Sub TransmitTransactions()
Dim enc As New Encryption
Dim whProxy As New WarehouseProxy.WareHouseService
Dim merchantID As String
Dim trackingID As Guid
Dim result As IAsyncResult
Dim callbackTo As New AsyncCallback(AddressOf TransmitHandler)
Try
_DBConn = New SqlConnection( _
enc.SimpleDescrypt( _
ConfigurationSettings.AppSettings("DBConnStr")))
_DBConn.Open()
_DBTrans = _DBConn.BeginTransaction
merchantID = enc.SimpleDescrypt( _
ConfigurationSettings.AppSettings("MerchantID"))
Console.WriteLine("Update {0} transactions.", _
_Transactions.Rows.Count)
For Each row As DataRow In _Transactions.Rows
trackingID = CType(row.Item(TransactionCols.TrackingID), Guid)
result = whProxy.BeginsubmitOrder(merchantID, _
CStr(row.Item(TransactionCols.CreditAccountID)), _
trackingID.ToString, _
CSng(row.Item(TransactionCols.Amount)), _
GetTimeStamp(), _
callbackTo, _
CInt(row.Item(TransactionCols.TransactionID)))
Next
_DBTrans.Commit()
Catch ex As Exception
_DBTrans.Rollback()
Throw ex
Finally
If Not whProxy Is Nothing Then
whProxy.Dispose()
End If
If Not _DBTrans Is Nothing Then
_DBTrans.Dispose()
End If
If Not _DBConn Is Nothing Then
_DBConn.Close()
_DBConn.Dispose()
End If
End Try
End Sub
It works well enough for the first few transactions, but since the FOR-NEXT loop finishes well in advance of all the SubmitOrder calls, my program goes on to commit to commit the handful of transactions posted rather than waiting for all of them to finish before doing so. What I need to test is creating an array of WaitHandles, then issuing AsyncWaitHandle.WaitAll() with that array on the last result object class, before calling the Commit method, I guess.
Again, this whole process hasn't been nearly as painful as I thought it would, but it makes me long for the relative ease of computing SEPPs.