Skip to content

Services, Wiki-Artikel, Blog-Beiträge und Glossar-Einträge durchsuchen

↑↓NavigierenEnterÖffnenESCSchließen
Kryptographie & Protokolle Glossary

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(&quot;api.example.com&quot;, &quot;sha256/PRIMARY_PIN==&quot;)
        .add(&quot;api.example.com&quot;, &quot;sha256/BACKUP_PIN==&quot;)
        .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>