Certificate Pinning - Zertifikat-Pinning in Apps
Certificate pinning is a security technique in which an application accepts only specific TLS certificates or public keys instead of relying on the general CA trust system. It prevents man-in-the-middle attacks even if an attacker possesses a CA-signed certificate. Primarily used in mobile apps. Bypass methods: Frida hooking, SSL kill switch, custom root certificate. Risk: Certificate pinning can block legitimate traffic analysis tools.
Certificate Pinning is a technique used to prevent man-in-the-middle attacks that could be carried out despite the presence of a valid CA-signed certificate. Standard TLS validation trusts any certificate signed by an installed root CA—and there are hundreds of such root CAs worldwide. Certificate pinning reduces this trust to a single certificate or a single public key.
How it works
Why standard TLS isn’t enough:
Standard TLS validation:
App → Server: "Here is my certificate (signed by DigiCert)"
App checks: Is DigiCert a trusted CA? YES → Connection OK!
Problem: Attacker has a compromised CA or their own CA:
→ If a MITM CA is installed on the device (e.g., Burp Suite):
App → Proxy: "Here is my certificate (signed by PortSwigger CA)"
App checks: Is PortSwigger trustworthy? IF YES → MITM successful!
With Certificate Pinning:
App has stored the genuine server public key
App → Proxy: "Here is my certificate..."
App checks: Does the public key match the stored pin?
→ Proxy certificate has a different key → CONNECTION REJECTED!
Pinning Variants:
1. Certificate Pinning:
→ Full certificate stored
→ Very strict: Even when the certificate is renewed, the new certificate must be pinned!
→ Risk: certificate expiration → App crashes → Update required
2. Public Key Pinning (recommended):
→ Only the public key of the leaf or intermediate CA is stored
→ More flexible: Certificate can be renewed as long as the key remains the same
→ HPKP (HTTP Public Key Pinning): obsolete, deprecated in browsers
→ Mobile apps: still widely used
3. CA Pinning:
→ Only the specific CA is pinned (not the certificate)
→ Flexible for certificate renewals
→ Weaker: all certificates from this CA are accepted
Hash format for pinning:
# SHA-256 hash of SubjectPublicKeyInfo:
openssl s_client -connect example.com:443 2>/dev/null | \
openssl x509 -pubkey -noout | \
openssl pkey -pubin -outform DER | \
openssl dgst -sha256 -binary | \
base64
→ Result: "Lkcd0...=" → this hash is stored in the app
Implementation
iOS (App Transport Security):
Info.plist:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>api.example.com</key>
<dict>
<key>NSIncludesSubdomains</key><false/>
<key>NSPinnedCAIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>ABC123...</string>
</dict>
</array>
</dict>
</dict>
</dict>
iOS (URLSession manually):
extension MyDelegate: URLSessionDelegate {
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: ...) {
let serverTrust = challenge.protectionSpace.serverTrust!
let cert = SecTrustGetCertificateAtIndex(serverTrust, 0)!
let pubKey = SecCertificateCopyKey(cert)!
let pubKeyData = SecKeyCopyExternalRepresentation(pubKey, nil)!
let hash = SHA256.hash(data: pubKeyData as Data)
let base64 = Data(hash).base64EncodedString()
if base64 == PINNED_HASH {
completionHandler(.useCredential, ...)
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}
Android (Network Security Config):
res/xml/network_security_config.xml:
<network-security-config>
<domain-config>
<domain includeSubdomains="false">api.example.com</domain>
<pin-set expiration="2027-01-01">
<pin digest="SHA-256">PRIMARY_PIN_HASH==</pin>
<pin digest="SHA-256">BACKUP_PIN_HASH==</pin>
</pin-set>
</domain-config>
</network-security-config>
AndroidManifest.xml:
<application android:networksecurityconfig="@xml/network_security_config">
OkHttp (Java/Kotlin):
val client = OkHttpClient.Builder()
.certificatePinner(
CertificatePinner.Builder()
.add("api.example.com", "sha256/PRIMARY_PIN==")
.add("api.example.com", "sha256/BACKUP_PIN==")
.build()
)
.build()
Pinning Bypass in Penetration Testing
Certificate Pinning Bypass Techniques:
Prerequisite: Root access or debugging mode on the device
Method 1 - Frida (Dynamic Instrumentation):
→ Injects JavaScript into a running app
→ Overrides TLS validation functions at runtime
frida-ios-dump / objection:
objection -g com.example.app explore
ios sslpinning disable
→ Disables pinning in the app process!
Android:
objection -g com.example.app explore
android sslpinning disable
Universal Frida Script (ssl-kill-switch2-equivalent):
→ Hooks into TrustManager.checkServerTrusted()
→ Always returns true → all certificates accepted!
Method 2 - SSL Kill Switch (iOS):
→ Tweak for jailbroken iOS devices
→ Disables pinning system-wide or per app
→ Installable via Cydia/Sileo
Method 3 - Binary Manipulation:
→ Decompile APK/IPA (jadx, apktool)
→ Analyze pinning validation code
→ Remove hash comparison or replace it with your own hash
→ Re-sign app + install
Method 4 - Debugging with Xposed Framework (Android):
→ JustTrustMe module: overrides Java SSL classes
→ TrustMeAlready: similar, more compatibility
Method 5 - Emulator with custom root CA:
→ Android emulator: adb root → certificate in system store
→ iOS Simulator: macOS trusts simulated connections
Detection measures against bypass:
□ Root/Jailbreak Detection (SafetyNet/Play Integrity, jailbreak-detect)
□ Debugger Detection: IsDebuggerPresent, anti-Frida checks
□ Frida Detection: check known Frida file paths/ports
□ Integrity check: Verify app signature at startup
→ But: These measures can also be bypassed!
→ Pinning cannot withstand a determined attacker
→ Goal: Increase the effort required, not make it impossible
Best Practices
Implement certificate pinning correctly:
1. Always include backup pins:
→ At least 2 pins: current key + backup key
→ Backup key: next key to be used during rotation
→ Without backup: rotation → app does not work!
2. Set expiration date:
<pin-set expiration="2027-06-01">
→ Pin set expires → app falls back to normal validation
→ Emergency fallback if key rotation is missed
3. Enable reporting:
→ Send pinning errors to logging endpoint
→ Enables detection of MITM attempts in production
→ Indicator: many pinning errors = MITM attack or incorrect configuration
4. Not for all connections:
→ Only for critical API endpoints (authentication, payments)
→ Analytics, CDN, advertising: no pinning (too frequent rotation)
5. Define rotation process:
□ Generate new key
□ Include new hash in app code
□ Deploy app update (all users must update!)
□ Then: deploy real certificate with new key
□ Remove old pin (after transition period)
When to avoid pinning:
□ If controlled deployment is not possible (long update cycles)
□ If backend is operated by CDN/cloud provider (frequent rotation)
□ For public web apps (browser access: pinning not possible)
→ Alternative: Certificate Transparency Monitoring + HSTS Preloading
```</pin-set></application>