Certificate Pinning
3 min read
Security
Pin the public-key hash (SPKI), not the full certificate — certs rotate every 90 days with Let's Encrypt, but the public key can stay stable across renewals. Always ship two or more pins (current + planned next) and a remote kill-switch so a bad pin doesn't lock users out.
| Attack | Protection without pinning | With pinning |
|---|---|---|
| MitM with a self-signed cert | TLS rejects | TLS rejects |
| MitM with a cert signed by a compromised CA | TLS accepts (CA is trusted) | Rejected — cert/key not in pin set |
| Corporate proxy installing a root | Accepted | Rejected |
| User installs malicious root cert | Accepted | Rejected |
Pinning is about shrinking the trust surface from "all globally trusted CAs" to "this specific certificate or public key I expect."
| Pin what? | Pros | Cons |
|---|---|---|
| Leaf certificate (whole cert) | Most strict | Breaks on every cert rotation (every 90 days for Let's Encrypt) |
| Public key (SPKI hash) | Recommended — pin survives cert renewal as long as the key is reused | Still breaks if you rotate keys |
| Intermediate CA | Survives leaf rotation | Wider trust = less protection |
Code in action — public-key pinning via Dio
// Hashes of the public keys you trust (base64 SHA-256 of the SPKI)
const _pins = <String>{
'sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=', // current
'sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=', // next (rotation safety)
};
class SecureApiClient {
SecureApiClient() {
_dio = Dio();
(_dio.httpClientAdapter as IOHttpClientAdapter).createHttpClient = () {
final client = HttpClient();
client.badCertificateCallback = (cert, host, port) {
// Only invoked when default validation fails — keep returning false unless pin matches
return _matchesPin(cert);
};
return client;
};
}
late final Dio _dio;
bool _matchesPin(X509Certificate cert) {
final spkiHash = base64Encode(sha256.convert(_spki(cert)).bytes);
return _pins.contains('sha256/$spkiHash');
}
}
// In practice, use the http_certificate_pinning or talsec packages —
// they handle SPKI extraction + cross-platform quirks.
Pinning strategy in production
| Phase | Practice |
|---|---|
| Choose what to pin | Public key (SPKI hash), not full cert |
| Ship redundancy | Pin 2+ keys: current + planned next |
| Plan rotation | Have a backup pin in the binary before you rotate the live one |
| Add a kill switch | Remote config that disables pinning if it goes wrong |
| Don't pin in dev | Make pinning configurable per environment |
| Monitor failures | Log pinning rejections to alert on real problems vs attacks |
| iOS / Android specifics | Use platform-native pinning where you can (App Transport Security, NetworkSecurityConfig) for defence in depth |
Common mistakes to avoid
// ❌ Pinning only ONE cert/key with no backup
// Cert rotates one Sunday at 3am → every user gets locked out
// ✅ Always ship at least 2 pins; rotate before the old one expires
// ❌ Pinning the LEAF certificate when using Let's Encrypt
// LE rotates every ~60-90 days → constant breakage
// ✅ Pin public key (or pin intermediate as fallback)
// ❌ Returning true from badCertificateCallback for development
client.badCertificateCallback = (_, __, ___) => true; // 💥 ships to production
// ✅ Gate behind kDebugMode + explicit dev-only flag
// ❌ Implementing pin checks but ignoring the host
// Pin matches global → accepts MitM cert for any host with same key (rare but real)
// ✅ Verify host as part of validation
// ❌ Not having a remote-disable path
// Server cert breaks on a holiday → users locked out, no fix until next release
// ✅ Remote config flag to disable pinning, with reporting on failures
Interview follow-ups
-
Why is public-key pinning preferred over certificate pinning? Certificates rotate (often every 90 days with Let's Encrypt). Public keys can be reused across rotations, so pinning the SPKI hash lets you renew your cert without pushing a new app build. If you rotate keys too, you still need a backup pin shipped ahead of time.
-
What does the
badCertificateCallbackactually do in Dart? It's invoked when the platform's default chain validation fails. Returningtruemeans "trust it anyway." Pinning code returns true only when the cert matches your pin; for all other certs (including ones the OS trusts but you don't), you return false to reject. -
What's the trade-off of pinning? You gain protection against trusted-CA compromise and rogue proxies. You pay for it with operational risk: pin a bad value, ship it, and users can't connect. The right way is "pin + plan for rotation + remote kill switch + monitoring."
-
Does pinning protect against a compromised endpoint? No. Pinning only ensures you're talking to the real server. If the real server is compromised, pinning won't help. Pair with end-to-end encryption (signed payloads, additional auth) for that threat model.
How helpful was this content?
Please sign in to rate this article.