-
Notifications
You must be signed in to change notification settings - Fork 1
WCF outside of IIS with REST JSON, SSL, and Client Certificates
For one of the projects that uses Alien Force, we wanted to make a Windows Service that hosted a WCF component (i.e. this is meant to run on machines that may not have IIS or a proper IIS) and also used REST rather than a more formal protocol like SOAP just to make it much easier to call and evolve over time.
So our simple interface will be as follows:
[ServiceContract]
public interface IEchoServer
{
/// <summary>
/// Just respond with message to verify connectivity
/// </summary>
/// <param name="message"></param>
/// <returns></returns>
[OperationContract]
[WebGet(UriTemplate = "Echo?message={message}", RequestFormat = WebMessageFormat.Json, ResponseFormat = WebMessageFormat.Json)]
string Echo(string message);
}
public class EchoServer : IEchoServer
{
public string Echo(string message)
{
return message;
}
}
Regardless of whether you put this in a console app or a service, these simple lines are what hosts the service:
mServerWCF = new ServiceHost(typeof(EchoServer));
mServerWCF.Credentials.ClientCertificate.Authentication.CertificateValidationMode = System.ServiceModel.Security.X509CertificateValidationMode.Custom;
mServerWCF.Credentials.ClientCertificate.Authentication.CustomCertificateValidator = new Auth();
mServerWCF.Open();
I chose to do the certificate validation stuff in code rather than config because it just felt simpler. My config is as follows:
<?xml version="1.0"?> <configuration> <system.serviceModel> <services> <service name="MyServiceNamespace.EchoServer" behaviorConfiguration="EchoServiceBehavior"> <host> <baseAddresses> <add baseAddress="https://someservername/EchoServer"/> </baseAddresses> </host> <!-- this endpoint is exposed at the base address provided by the host--> <endpoint address="" binding="webHttpBinding" behaviorConfiguration="EchoServiceEPBehavior" bindingConfiguration="EchoBinding" contract="MyServiceNamespace.IEchoServer"/> </service> </services> <bindings> <webHttpBinding> <binding name="EchoBinding"> <security mode="Transport"> <transport clientCredentialType="Certificate"/> </security> </binding> </webHttpBinding> </bindings> <!--For debugging purposes set the includeExceptionDetailInFaults attribute to true--> <behaviors> <endpointBehaviors> <behavior name="EchoServiceEPBehavior"> <webHttp helpEnabled="false"/> </behavior> </endpointBehaviors> <serviceBehaviors> <behavior name="EchoServiceBehavior"> <serviceDebug includeExceptionDetailInFaults="True"/> </behavior> </serviceBehaviors> </behaviors> </system.serviceModel> <startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.0"/></startup></configuration>
You will need to assign a certificate to the port you host the service on, see this article
Lastly, you’ll need to implement the Auth class to verify the certificate is one you want to allow access. Your class will look like this, and you’d fill in whatever authorization logic is appropriate.
public class Auth : X509CertificateValidator
{
static string[] Fingerprints = (ConfigurationManager.AppSettings["EchoServer.ClientCertificates"] ?? "").Split(',');
public override void Validate(System.Security.Cryptography.X509Certificates.X509Certificate2 certificate)
{
var hash = Convert.ToBase64String(certificate.GetCertHash());
if (Fingerprints.Any((x) => x == hash))
{
return;
}
throw new SecurityTokenException("Validation Failed!");
}
}
I tried many ways to get this to work with “half-arsed” certificate infrastructure. Don’t do this, it’s just painful and I never did get it to work. Instead, get something like Simple Authority and setup a Certificate Authority and make a certificate for the server and client. On the server, install the CA certificate (using just the .cer file) into trusted certificates for the account your hosted service will run as and also install the full certificate (p12) for the server. The place where half-arsing it really fell down was that your server will send a list of CAs whose client certs it wants, and the client will likely not send a cert that isn’t signed by one of those. So I spent plenty of time wondering why adding to ClientCertificates in HttpWebRequest was getting a 403 Forbidden and complaining that a client certificate wasn’t sent. Basically HttpWebRequest just silently doesn’t send certs that weren’t requested. Turning on tracing for System.Net is what helped me figure this out and give up on not implementing a proper key trust structure.
To call it I used the ClientCertificateWebRequest in Alien Force. It’s just a little utility class to lookup a certificate by subject name and add it to the HttpWebRequest used by WebClient. If you use HttpWebRequest directly, you probably don’t need it, but maybe you’d want to copy the code from the constructor that looks up the certificate. Calling our service is a one liner:
var response = new ClientCertificateWebClient("CN=something, OU=whatever, C=US").DownloadString("https://someservername/EchoServer/Echo?message=foo");
So now hopefully you have it working from the command line. We needed to call this from inside IIS, which presented one last challenge. You would likely import the certificate and trusted root into the local machine store to make life easier, but IIS probably doesn’t have permissions to use the private key for the client cert. You’ll probably get “Could not create SSL/TLS secure channel” or error 0×8009030D. To solve this, go into the certificate manager MMC snap-in, right click “Manage Private Keys” and add the user your IIS application is running as to the permissions for the key. If you’re not sure, try granting Everyone rights first, see if that fixes it, and then hone down on the right user (there may be an event log telling you which one it was trying).