Monday, November 12, 2012

HTTPS Communication – HttpListener based Hosting and Client Certification

KEYWORDS: HTTPS, SSL, HttpListener, X509Chain, X509Certificate2, makecert, OpenSSL, client certificate

(Just found, a wonderful tool set that could help you to host easily, http://katanaproject.codeplex.com/. The site referred my post, and I didn't realize till now  :-) thanks for refering.  [2016-2-6])

HttpListener is the easiest way for you to host an HTTP/HTTPS server. This article provides you step-by-step instructions to create your own server and authenticate clients based on client certificate from ground up in C#.

Download the sample code

STEP 1

Firstly, you should create your .net application and add these four lines.

var server = new HttpListener();
server.Prefixes.Add("https://+:90/");
server.Start();
HttpListenerContext context = server.GetContext();

These fourlines will make your server started and listening on the port. Be aware of the exceptions (HttpListenerException) thrown from the invocation server.Start(), and see step 2 to solve it.


STEP 2

Step 1 shows you it’s so easy to start a server. But wait, Start() throws an exception (HttpListenerException: Access Denied, native error code 5, HRESULT 80004005), if you run your app under non-privilege account. If you want a non-privilege account to run the server, you have to add ACL (Access Control Lists) to the system. In command line:

netsh http add urlacl url=https://+:80/MyUri user=DOMAIN\user

Pay attention to the parameter ‘user’. Put whatever user you want to assign the start server right to here. If set the parameter user=users, it will grant all user account (non-privileged) to start the app and listen on the specific ip and port. The ip part ‘+’ stands for all IPs of your machine. For the server you want to handle urls from root (e.g. http://localhost/), you don’t need ‘MyUri’ part, and your command is like this:

netsh http add urlacl url=https://+:80/ user=DOMAIN\user


STEP 3

And then your app won’t throw any exception. Your app would be blocked at server.GetContext() and waiting for incoming connections. Try the url https://localhost:90/ in your browser, there is still an error page with HTTP 101 ERR_CONNECTION_RESET. This because you haven’t assign a certificate to the server and the browser can’t verify the validity of the server. Remember we are visiting an HTTPS site. The server certificate is a must.

So, let’s create the certificates. You can either create your certificates by makecert or by OpenSSL. And this How to Setup a CA gives you an easy tutorial of creating certificates hierachy by OpenSSL. First is the root CA certificate. For experimental cases, makecert is enough. But for product, you may want to use OpenSSL or apply a certificate from CA like VeriSign.

makecert -n "CN=TestCA" -r -sv TestCA.pvk TestCA.cer

And import the root certificate to the system certificate storage of Rusted Root Certification Authority. See this article.

Then create the certificate for your HTTPS web site.

makecert -iv TestCA.pvk -n "CN=TestSite" -sv TestSite.pvk -ic TestCA.cer TestSite.cer -sr LocalMachine -ss My -sky exchange -pe

If you will test your client app on a machine other than the server machine, you have to import the TestCA.cer to the client machine as well. So that the client machine trust TestCA (the root cert), it will also trust the server certificate (TestSite).

Hosting an HTTPS site, you must have a certificate with private key. But the last makecert command creates the private key in TestCA.pvk which can’t be imported to the system storage directly. We have to convert it to .pfx format:

pvk2pfx -pvk "TestSite.pvk" -spc "TestSite.cer" -pfx "TestSite.pfx"

Then you will see the certificate for your site:


STEP 4

How to use the server certificate? At this point, the when client connect to the server, the client will throw an exception (WebException The underlying connection was closed: An unexpected error occurred on a send), simply because the server doesn’t use the certificate yet. To resolve the exception,just binding the certifiate to the server’s ip and port by netsh.

netsh http add sslcert ipport=0.0.0.0:90 appid={61047666-992C-4137-9303-7C01781B054E} certhash=75d0fed71881f2141b5b6cb24801dfa554439b1c clientcertnegotiation=enable

‘0.0.0.0’ in the ipport parameter means every ip of this machine would be assigned with the certificate. The parameter appid is your application id. You can see it in the project property, the ‘Application’ page, and the dialog poped up by clicking ‘Assembly Information’ button. The parameter ‘clientcertnegotiation=enable’ will allows C/S mutually authentication based on certificates, i.e. server side could verfiy the certificate validation of the client side as well as the client side verifying the server side. If you don’t want verification for client side, just omit the parameter.


STEP 5

Visit https://localhost:90/ again, your browser will warning you that the site is not the owner of the certificate. It’s because we don’t have a domain for our experimental site and no domain name was set into the certificate. So just click continue to view the page and the browser will show you a blank page.

Let’s add responding code to the server side, so that we can see something on the page.

string message = "Hello World!";
var buffer = System.Text.Encoding.UTF8.GetBytes(message);
context.Response.OutputStream.Write(buffer, 0, buffer.Length);
context.Response.OutputStream.Close();

Now the page displays “Hello World!”.


STEP 6

We have done the work of constructing server side. The server can show its identity by providing its certificate and client can verify it. Client still shows no certificate to the server. In some cases, the server need to verify the client’s identity, and only when the client is valid (e.g. a valid member of some organization) the server would start data communication. In this case, a client app (other than web browser) is a must. So let’s create a client app.

Here is the basic client code without client certificate.

ServicePointManager.ServerCertificateValidationCallback =
       new RemoteCertificateValidationCallback(CheckValidationResult);

string url = "https://localhost:90/";
Console.WriteLine("Visiting " + url);
HttpWebRequest objRequest = System.Net.HttpWebRequest.Create(url) as HttpWebRequest;
objRequest.ProtocolVersion = HttpVersion.Version10;

var response = objRequest.GetResponse();
var responseReader = new StreamReader(response.GetResponseStream());
var responseContent = responseReader.ReadToEnd();
Console.WriteLine("Server replied: " + responseContent);

CheckValidationResult is a callback function which allows you to perform customized validation against server certificate, returns true to accept the certificate. As expected, the client gets the server reply: “Hello World!”.


STEP 7

Here we add client certification code. Basically you have two ways of creating a X509Certificate2 which could contain public/private key pair. Other ways like manipulating public/private key pair raw data directly, may be tricky and complex.
  1. Load .pfx from file;
  2. Load certificate with private key from the system’s certificate store.
Here is the first one, load from file:

HttpWebRequest objRequest = System.Net.HttpWebRequest.Create(url) as HttpWebRequest;
X509Certificate2 clientCertificate = new X509Certificate2("TestClient.pfx", "the key password");
objRequest.ClientCertificates.Add(clientCertificate);

You have to add certificate to the https request right after you created the request, because GetResponse() will use the certificate immediately. Here is the second way of creating X509Certificate2 - loading the certificate from the system store:

static X509Certificate2 LoadClientCertificate()
{
    // Note: Change "My" and StoreLocation.CurrentUser to where your certificate stored.
    var store = new X509Store("My", StoreLocation.CurrentUser);
    var certificates = store.Certificates.Find(X509FindType.FindBySubjectName, "TestClient", true);
    if (certificates.Count != 0)
    {
       return null;
    }

    return certificates[0];
}

Before running it, you have to import the certificate (with private key) to the store just like you did with the server certificate. Loading a certificate (without private key) can be done by a non-privileged account, while accessing private key of a certificate from the system store requires administrator privilege. So when you run above code by a non-privileged account, you will get the certificate although, but only public key is in it. While the server side needs the client to sign something to verify the client’s identity, so the client must have the private key. So when carrying out further steps of HTTPS communication
  1. When the client certificate loaded from system store, the client code will get an WebException;
  2. When the client cerfiticate loaded from file,  the server will get no client cert (GetClientCertificate() returns null).

Loading from store and loading from file both has pros and cons.

STEP 8

Server side still doesn’t verify the client certificate. So let’s add the code logic.

HttpListenerContext context = server.GetContext();

var clientCertificate = context.Request.GetClientCertificate();
X509Chain chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.Build(clientCertificate);
if (chain.ChainStatus.Length != 0)
{
    // Invalid certificate
    context.Response.OutputStream.Close();
}

X509Chain is a tool which builds the chain of trust of the certificate. If the certificate is invalid, then you can find error information in chain.ChainStatus. You can implement detailed logic upon X509Chain rather than only checking chain.ChainStatus.Length. Set RevocationMode to NoCheck because we don’t have a certificate server to tell you whether a certificate is revoked.


Post a Comment