Certificate Pinning

Low PriorityAsked in ~35% of mid-level interviews

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.

AttackProtection without pinningWith pinning
MitM with a self-signed certTLS rejectsTLS rejects
MitM with a cert signed by a compromised CATLS accepts (CA is trusted)Rejected — cert/key not in pin set
Corporate proxy installing a rootAcceptedRejected
User installs malicious root certAcceptedRejected

Pinning is about shrinking the trust surface from "all globally trusted CAs" to "this specific certificate or public key I expect."

Pin what?ProsCons
Leaf certificate (whole cert)Most strictBreaks 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 reusedStill breaks if you rotate keys
Intermediate CASurvives leaf rotationWider 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

PhasePractice
Choose what to pinPublic key (SPKI hash), not full cert
Ship redundancyPin 2+ keys: current + planned next
Plan rotationHave a backup pin in the binary before you rotate the live one
Add a kill switchRemote config that disables pinning if it goes wrong
Don't pin in devMake pinning configurable per environment
Monitor failuresLog pinning rejections to alert on real problems vs attacks
iOS / Android specificsUse 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

  1. 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.

  2. What does the badCertificateCallback actually do in Dart? It's invoked when the platform's default chain validation fails. Returning true means "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.

  3. 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."

  4. 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.