TLS certificate pinning, often misunderstood as simply "hardcoding certificates," is actually about anchoring your trust in specific public keys rather than just the Certificate Authority (CA) that signed them.
Imagine you’re building a mobile app that talks to your backend API. You want to ensure that your app is only talking to your legitimate API server and not some imposter trying to eavesdrop or tamper with the communication. Normally, when your app establishes a TLS connection, it trusts the server’s certificate because it was issued by a CA that your device already trusts. This chain of trust is generally secure, but what if a CA’s private key is compromised, or a rogue CA is added to your device’s trust store? Suddenly, attackers can issue fraudulent certificates that your app will blindly accept.
Certificate pinning lets you say, "I don’t just trust any certificate signed by a known CA; I specifically trust the certificate (or more precisely, the public key within it) that belongs to my API server."
Let’s see this in action. Suppose your backend API has a certificate with a specific public key. You can extract that public key’s SHA-256 hash. In iOS, you’d typically configure this within your app’s Info.plist file.
<key>NSAppTransportSecurity</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>api.yourdomain.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>exc_pkpinning</key> <!-- This is the key for pinning -->
<true/>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<false/>
<key>SPKI-sha256</key> <!-- The SHA-256 hash of the public key -->
<array>
<string>YOUR_PUBLIC_KEY_SHA256_HASH_HERE</string>
</array>
</dict>
</dict>
</dict>
On Android, you’d use a Network Security Configuration file (e.g., res/xml/network_security_config.xml) and reference it in your AndroidManifest.xml.
res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.yourdomain.com</domain>
<pin-set expiration="2024-12-31"> <!-- Optional expiration date -->
<pin algorithm="SHA-256">YOUR_PUBLIC_KEY_SHA256_HASH_HERE</pin>
<!-- You can pin multiple keys for key rotation -->
<pin algorithm="SHA-256">ANOTHER_PUBLIC_KEY_SHA256_HASH_HERE</pin>
</pin-set>
</domain-config>
</network-security-config>
AndroidManifest.xml:
<application
...
android:networkSecurityConfig="@xml/network_security_config">
...
</application>
The YOUR_PUBLIC_KEY_SHA256_HASH_HERE would be a string like abcdef123456.... You can obtain this hash by taking your server’s public key (often found in the certificate), converting it to DER format, and then computing its SHA-256 hash. For example, using OpenSSL:
openssl x509 -in server.crt -pubkey -noout | openssl pkey -pubin -outform DER | openssl dgst -sha256 -binary | openssl enc -base64
Or, if you have the public key in PEM format:
openssl pkey -in server.key.pem -pubout -outform DER | openssl dgst -sha256 -binary | openssl enc -base64
When your app attempts to connect to api.yourdomain.com, the TLS handshake will proceed. The server presents its certificate. Instead of just checking if the certificate is signed by a trusted CA, the mobile OS (following your pinning configuration) will extract the server’s public key from the certificate. It then computes the SHA-256 hash of this public key and compares it against the hashes you’ve provided in your configuration. If there’s a match, the connection is allowed. If not, the connection is immediately terminated, preventing a potential man-in-the-middle attack.
This mechanism provides a robust layer of security. It’s not just about trusting a signature; it’s about trusting the specific identity (the public key) that the signature is supposed to represent. This is particularly crucial for financial applications, health services, or any app handling sensitive user data.
A common pitfall is pinning the entire certificate instead of just the public key. Certificates expire, and if you pin an expired certificate, your app will stop working. Pinning the public key allows you to rotate your server’s certificate before it expires, as long as the new certificate contains the same public key. You can also pin multiple public key hashes to facilitate a smooth transition during certificate renewal or server infrastructure changes. The expiration attribute in Android’s network_security_config.xml is a crucial safety net; without it, a pinned key that’s no longer valid could permanently break your app.
The true power of pinning lies in its ability to mitigate attacks that bypass traditional CA trust mechanisms, like compromised CAs or compromised intermediate certificates. By directly asserting trust in your server’s public key, you create a much stronger guarantee about the identity of the endpoint your app is communicating with.
The next step after implementing certificate pinning is to consider how you will manage key rotation and updates without breaking your app.