Web Services and HTTPS
"The time has come", the Walrus said, "to speak of secure communications".
There's something I've been meaning to getting around to sorting out for a while now, but have been deliberately ignoring. My excuses have been that I've been too busy, and also that we haven't really needed it - yet.
Pretty soon now, however, we're going to need have a local web service call a web service that is sitting behind a https site. In general, making connections from one site to another over https is something that's never worked extremely well for me, and it's time I stopped putting off my research (we need to deploy this week :)
Generally speaking, there really isn't a problem with communicating over https. So long as the certificate is valid, then everything works nicely. The problem is if for some reason there's a slight problem. In our case at work, our certificates were self signed, and therefore not from a trusted source. Any scenario which would cause IE to display a security dialog when you hit the site will cause you code to throw a System.Net.WebException with a Status of 'WebExceptionStatus.TrustFailure'. And this is what was biting us here.
So first I set up a test web service and put it on a development server that was SSL enabled. While I'd generated my web reference for the test client on a non-https connection, I then at run time changed the URL to point to the https version, to see what error I get. It was, of course, the TrustFailure error, with a message of 'Could not establish trust relationship with remote server'.
A quick Google later and I come up with two pages of use: Could not establish trust relationship with remote server, and via it's comments, Hosting Remote Objects in IIS an the MSDN website (specifically the section Using SSL Certificates with .NET Remoting).
It ends up that you can setup your own Certificate Policy handler, and determine for yourself, if a certificate is a problem, whether to allow it anyway. You just need to provide an object that supports the System.Net.ICertificatePolicy interface.
What's interesting is that in both the example on the first link I gave (which was in C#), and in the MSDN Doco page for the ICertificatePolicy interface (which was in VB) were setting up an Enum to represent all the different error codes when it wanted to reject a certificate. In both cases, they defined the Enum as Long (since it contained only Long values) and then cast the incoming error code (which was an Int) to the enum to find the match - and in all my tests it then failed.
So I had to update the enum to use the correct values as I judged them, and now it's fine.
To get your own custom CertificatePolicy called, you do this:
System.Net.ServicePointManager.CertificatePolicy = New MyCertificatePolicy
or words to that effect...
Here's the class I wrote for my test, allowing only UntrustedRoot (my specific problem) and code 0 (no actual error occurred) through, and denying all others:
Private Class CertificatePolicy
Implements System.Net.ICertificatePolicy
Private Enum CertificateProblems
NoError = 0
Expired = &H800B0101
ValidityPeriodNesting = &H800B0102
Role = &H800B0103
PathLenConst = &H800B0104
Critical = &H800B0105
Purpose = &H800B0106
IssuerChaining = &H800B0107
Malformed = &H800B0108
UntrustedRoot = &H800B0109
Chaining = &H800B010A
Revoked = &H800B010C
UntrustedTestRoot = &H800B010D
RevocationFailure = &H800B010E
CnNoMatch = &H800B010F
WrongUsage = &H800B0110
UntrustedCA = &H800B0112
End Enum
Public Function CheckValidationResult(ByVal srvPoint As System.Net.ServicePoint, ByVal certificate As System.Security.Cryptography.X509Certificates.X509Certificate, ByVal request As System.Net.WebRequest, ByVal certificateProblem As Integer) As Boolean Implements System.Net.ICertificatePolicy.CheckValidationResult
Dim eProblem As CertificateProblems = CType(certificateProblem, CertificateProblems)
Dim bRet As Boolean = False
Select Case eProblem
Case CertificateProblems.NoError
bRet = True
Case CertificateProblems.UntrustedRoot
bRet = True
'Case Else
' Debug.WriteLine(eProblem)
End Select
Return bRet
End Function
End Class
Also note that this is not the most secure way of doing things. In the real code that I implemented, when the problem was UntrustedRoot, I matched certificate.GetIssuerName() to the issuer name I'm expecting in our certificates. If the IssuerName didn't match, I still denied it.
Listening to: cure - metallica - (4:54)