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
curlwith--pinnedpubkeyfails withSSL: public key does not match pinned public key- Application logs show
Certificate pinning failureorPublic 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.Extract the public key hash from the new certificate:
- 2.```bash
- 3.# SHA-256 hash of the public key (SPKI format)
- 4.openssl x509 -in new-cert.pem -pubkey -noout | \
- 5.openssl pkey -pubin -outform der | \
- 6.openssl dgst -sha256 -binary | \
- 7.openssl enc -base64
- 8.# Output: sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
- 9.
` - 10.Update the pin in the application (method depends on the platform):
- 11.```bash
- 12.# For curl:
- 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.For server-side HPKP header (deprecated but still in use):
- 2.```nginx
- 3.# Generate both current and backup key hashes
- 4.openssl x509 -in current.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
- 5.openssl x509 -in backup.pem -pubkey -noout | openssl pkey -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
- 6.
` - 7.Add to Nginx:
- 8.```nginx
- 9.add_header Public-Key-Pins 'pin-sha256="CURRENT_HASH="; pin-sha256="BACKUP_HASH="; max-age=5184000; includeSubDomains';
- 10.
` - 11.Implement safe certificate rotation with key reuse:
- 12.```bash
- 13.# Generate a CSR from the existing private key
- 14.openssl req -new -key existing-private.key -out new.csr
- 15.# Submit the CSR to the CA for a new certificate
- 16.# The new certificate will have the same public key hash
- 17.
` - 18.Test pinning before deploying the new certificate:
- 19.```bash
- 20.# Test with the expected pin
- 21.curl --pinnedpubkey "sha256//EXPECTED_HASH==" https://staging-server
- 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-ageto 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