Introduction

Certificate pinning (HPKP's successor) involves hardcoding the expected public key hash or certificate fingerprint in an application. When the server rotates its SSL certificate, the new certificate's public key hash differs from the pinned value, causing all pinned clients to reject the connection. This is especially problematic for mobile apps that cannot be quickly updated, as the pin is compiled into the app binary.

Symptoms

  • Mobile app fails to connect after server certificate rotation
  • curl with --pinnedpubkey fails with SSL: public key does not match pinned public key
  • Application logs show Certificate pinning failure or Public key pin validation failed
  • SSL connection works in browser but fails in pinned application
  • Error is immediate and consistent across all pinned clients

Common Causes

  • Certificate rotated without updating the pinned public key in the application
  • New certificate generated with a new key pair instead of reusing the existing key
  • CA change requiring a new certificate with a different key
  • Certificate renewal process generates a fresh key instead of keeping the same CSR
  • Backup pin not configured, leaving no fallback during rotation

Step-by-Step Fix

  1. 1.Extract the public key hash from the new certificate:
  2. 2.```bash
  3. 3.# SHA-256 hash of the public key (SPKI format)
  4. 4.openssl x509 -in new-cert.pem -pubkey -noout | \
  5. 5.openssl pkey -pubin -outform der | \
  6. 6.openssl dgst -sha256 -binary | \
  7. 7.openssl enc -base64
  8. 8.# Output: sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
  9. 9.`
  10. 10.Update the pin in the application (method depends on the platform):
  11. 11.```bash
  12. 12.# For curl:
  13. 13.curl --pinnedpubkey "sha256//NEW_BASE64_HASH==" https://server

# For Android (Network Security Configuration): # res/xml/network_security_config.xml # Update the pin-set digest value ```

  1. 1.For server-side HPKP header (deprecated but still in use):
  2. 2.```nginx
  3. 3.# Generate both current and backup key hashes
  4. 4.openssl x509 -in current.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
  5. 5.openssl x509 -in backup.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
  6. 6.`
  7. 7.Add to Nginx:
  8. 8.```nginx
  9. 9.add_header Public-Key-Pins 'pin-sha256="CURRENT_HASH="; pin-sha256="BACKUP_HASH="; max-age=5184000; includeSubDomains';
  10. 10.`
  11. 11.Implement safe certificate rotation with key reuse:
  12. 12.```bash
  13. 13.# Generate a CSR from the existing private key
  14. 14.openssl req -new -key existing-private.key -out new.csr
  15. 15.# Submit the CSR to the CA for a new certificate
  16. 16.# The new certificate will have the same public key hash
  17. 17.`
  18. 18.Test pinning before deploying the new certificate:
  19. 19.```bash
  20. 20.# Test with the expected pin
  21. 21.curl --pinnedpubkey "sha256//EXPECTED_HASH==" https://staging-server
  22. 22.`

Prevention

  • Always generate certificate renewals from the existing private key (new CSR, same key)
  • Configure backup pins in all pinned applications
  • Use a short pinning max-age to allow faster recovery from rotation issues
  • Document the pin extraction and update process in certificate rotation runbooks
  • Consider using Certificate Transparency (CT) logs as an alternative to pinning for monitoring