Certificate pinning is a security mechanism that allows a client to specify which server certificates or public keys it will trust for a given domain, rather than relying solely on the client’s pre-installed root certificate store.
Here’s how it looks in practice. Imagine you’re building a mobile app that needs to communicate securely with your API. Without pinning, your app trusts any certificate issued by a Certificate Authority (CA) that your operating system trusts. This is generally safe, but a sophisticated attacker could compromise a CA and issue a fraudulent certificate for your domain, which your app would then blindly trust.
Certificate pinning prevents this. You can configure your app to only accept a connection if the server presents a certificate containing a specific public key (or a certificate signed by a specific intermediate CA).
Let’s see it with HPKP (HTTP Public Key Pinning). This was a way to implement pinning via HTTP headers.
Strict-Transport-Security: max-age=31536000; includeSubDomains
Public-Key-Pins: max-age=31536000; pin-sha256="E95hUfK9f5S+C/jD5q5oM7Yv95mC1hT/T0Yg9G3oVnQ="; pin-sha256="F8qM1w74y+s3vB9u3t7oN5yV9n7gB3t7u2oM7Yv95mC1hT/T0Yg9G3oVnQ="
In this HPKP header:
max-age=31536000: Tells the browser to enforce this pinning policy for one year (31,536,000 seconds).pin-sha256="...": This is the actual pin. It’s a base64 encoded SHA-256 hash of the public key within the certificate. You’d typically provide at least two pins: one for your current certificate’s public key and one for a backup certificate’s public key. This is crucial for avoiding lockouts if your primary certificate expires or is compromised.
When a browser receives this header from yourdomain.com, it stores the public key hashes. For the next year, every time it tries to connect to yourdomain.com, it will fetch the server’s certificate, extract its public key, calculate the SHA-256 hash, and check if that hash matches any of the pinned hashes. If there’s no match, the connection is blocked, even if a valid certificate from a trusted CA is presented.
The problem HPKP tried to solve was the trust model of the Public Key Infrastructure (PKI). In the traditional model, clients trust a hierarchical chain of certificates up to a root CA. If a CA is compromised, an attacker can issue fraudulent certificates for any domain. This is like giving a master key to many people and hoping none of them misuse it. HPKP, and other pinning mechanisms, shifts trust from the issuing CA to the specific server’s public key. It’s like saying, "I don’t care who vouches for you; I will only accept this specific individual (public key) presenting themselves."
The Public-Key-Pins header has been deprecated. The primary reason for its deprecation was the significant risk of accidental lockouts. If you misconfigured the pins (e.g., a typo, incorrect hash, or didn’t have a valid backup pin) and deployed it to a large number of users, you could effectively prevent all those users from accessing your site until the max-age expired. It was a powerful tool, but a dangerous one to wield without extreme caution and robust operational procedures.
Public Key Pinning is now primarily implemented via Certificate Transparency (CT) logs and, in some application contexts (like mobile apps), through direct pinning within the application’s code. Certificate Transparency provides a public, auditable log of all certificates issued by CAs, making it harder for a CA to issue fraudulent certificates without detection. For applications where you have full control, like mobile apps, you can embed the expected public key or certificate directly into the app. This is often referred to as "certificate pinning" or "public key pinning" within the app’s network stack. Libraries like OkHttp in Android or AFNetworking in iOS provide mechanisms for this.
For example, in an Android app using OkHttp, you might configure pinning like this:
OkHttpClient client = new OkHttpClient.Builder()
.sslSocketFactory(sslSocketFactory, trustManager)
.hostnameVerifier(hostnameVerifier)
.build();
// ... where sslSocketFactory, trustManager, and hostnameVerifier are custom implementations
// that check the presented certificate against a known public key or certificate hash.
The core idea remains the same: instead of blindly trusting the CA hierarchy, the client verifies that the server’s presented certificate contains a specific, pre-authorized public key. This significantly hardens the application against man-in-the-middle attacks, especially those stemming from compromised Certificate Authorities.
The most critical point most developers miss when considering pinning is the lifecycle management of the pinned keys. If you pin a public key directly and that key’s certificate expires, or if you need to rotate your server’s certificate for any reason (e.g., security best practices, a new key generation), you must update the pinned keys on the client before you deploy the new server certificate. Failure to do so will result in clients being unable to connect to your server, causing an outage. This requires a coordinated deployment strategy that often involves deploying a new certificate signed by an intermediate CA that is also pinned, or carefully managing multiple pins for current and upcoming certificates.
The next step in securing application-to-application communication involves exploring mutual TLS (mTLS), where both the client and server authenticate each other using certificates.