In the previous article “Certificate Pinning: Why Your App Needs It“, we discussed why certificate pinning is essential for securing client-server communication. This article will focus on pure technical side of certificate pinning implementation.

We will create a simple command line application that fetches source code of my SaaS product web site CertWatch. Why command line? Because, it is good enough example that will work everywhere else and because it is isolated enough that it is easy to follow even for a casual tech follower.

The site uses Let’s Encrypt certificate, that changes quite often, so beware that certificate public key may not be valid at the time you read this article.

First, we need to obtain public key of server certificate. We can do this by either viewing certificate in the browser or use a tool like openssl.

private static string g_ssPublicKey =
		"3082010A0282010100C4FFFFE6F3945AD2BF27D5DD674166130D5D2021CEFF0" +
		"4B06AD48F5F56A6245C590433121C8F08B7A565FBF38F1102917CB7434AFE91" +
		"18E2CB904BA723D57182B680B872CF05578B234F65DB1A39CD77DEBD07D0939" +
		"A0C440A9AE9245D0CAB59480DC3864D744BA6404B0D6DA9BAEE0E85CE0816D9" +
		"D7F43468D2E073CBA2EA10114323B0053F8AE29F86AD846B71FE4D7924494FB" +
		"0D80E3C78875085163B53121EBEBCF1356A4386DFF9E2CB93D0BD9CA3A39D4A" +
		"AC7BB34F2FF4AC70D59DBCD92254D48DE0BC3CCB4A8B4822D64CCE46F1E539B" +
		"116A00420825AD2AFF128F7A761D79186FB747761E47187BD527B1398F603DC" +
		"F7DCABD3535C28B7FB2C3068230203010001";

In the program Main function, we will use HttpClient to connect to the web site. In order to set certificate handling and verification, we must first declare HttpClientHandler. The handler must do at least three things:

  • enable Certificate Revocation List check to check that certificate has not been revoked
  • set client certificate options to Manual, as we are going to manually validate certificate
  • implement server certificate validation callback method to perform validation.

Before we implement the callback method, let’s set a boiler plate for it and set first two items on the list. Validation method will return false and thus fail on every request.

using var handler = new HttpClientHandler(); 
		handler.CheckCertificateRevocationList = true;
		handler.ClientCertificateOptions = ClientCertificateOption.Manual;
		handler.ServerCertificateCustomValidationCallback = (message, certificate, chain, sslPolicyErrors) => { return false; };

Great. With that done, we can implement the body of the callback function. First, we need to check if the server web site certificate actually exists. If it doesn’t we return false and print an error. Here, you would do some logging instead, but this is a demo app.

if (null == certificate)
{
	Console.WriteLine("ERROR: No server certificate found");
	return false;
}

Next, we verify there are no SSL policy errors in certificate chain. If they are, we return false and print all policy errors.

if (sslPolicyErrors != SslPolicyErrors.None)
{
	Console.WriteLine($"SSL Policy Errors: {sslPolicyErrors}");
	return false;
}

Then, we obtain, the public key of server certificate and check if it matches the one, we declared in our app. If there is no match, we again print an error and return false. If public keys match, then the certificate is correct and we successfully end validation.

string sPublicKeyToVerify = certificate.GetPublicKeyString();
if (g_ssPublicKey != sPublicKeyToVerify)
{
	Console.WriteLine("ERROR: Certificate public key mismatch");
	return false;
}

Console.WriteLine("Certificate public key matches");
return true;

Now, all we have to do, is call our website and print it’s source code. The validation procedure will run immediately after SSL handshake is established.

using var httpClient = new HttpClient(handler);
string sResponse = await httpClient.GetStringAsync("https://certwatch.dev");
Console.WriteLine(sResponse);

In less than 50 lines of code, we have implemented a working, robust certificate pinning that ensures our app only communicates with our genuine API server. This simple technique significantly raises the bar against possible man-in-the-middle attacks.

Complete working example is embedded below. If you cannot see it, it is also published as gist on Github.

In the next article, we will check common pitfalls and dangers of certificate pinning in production environments.