TLS Certificate Pinning: Implementation and Bypass Techniques
Mamoun Tarsha-Kurdi
8 min read
Introduction
Certificate pinning enhances TLS security by rejecting certificates from untrusted CAs, even if system-trusted. However, incorrect implementation creates vulnerabilities exploitable during penetration testing and reverse engineering.
Certificate Pinning Mechanisms
Public Key Pinning
// Android: Pin specific public key
public class PinningTrustManager implements X509TrustManager {
private static final String PINNED_PUBLIC_KEY_HASH =
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=";
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
// Get leaf certificate
X509Certificate cert = chain[0];
// Extract public key
PublicKey publicKey = cert.getPublicKey();
// Compute SHA-256 hash
byte[] spkiBytes = publicKey.getEncoded();
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(spkiBytes);
String actualHash = "sha256/" + Base64.encodeToString(hash, Base64.NO_WRAP);
// Compare with pinned hash
if (!actualHash.equals(PINNED_PUBLIC_KEY_HASH)) {
throw new CertificateException("Certificate pin mismatch!");
}
}
}
Certificate Pinning
// iOS: Pin entire certificate
class CertificatePinner: NSObject, URLSessionDelegate {
let pinnedCertificates: [SecCertificate]
init(certificateFiles: [String]) {
var certs: [SecCertificate] = []
for filename in certificateFiles {
guard let path = Bundle.main.path(forResource: filename, ofType: "cer"),
let data = try? Data(contentsOf: URL(fileURLWithPath: path)),
let cert = SecCertificateCreateWithData(nil, data as CFData) else {
continue
}
certs.append(cert)
}
self.pinnedCertificates = certs
}
func urlSession(_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard let serverTrust = challenge.protectionSpace.serverTrust else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Get server certificate
guard let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}
// Check if matches any pinned certificate
let serverCertData = SecCertificateCopyData(serverCert) as Data
for pinnedCert in pinnedCertificates {
let pinnedCertData = SecCertificateCopyData(pinnedCert) as Data
if serverCertData == pinnedCertData {
// Pin match!
let credential = URLCredential(trust: serverTrust)
completionHandler(.useCredential, credential)
return
}
}
// No match - reject connection
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
Bypass Techniques
1. Frida Dynamic Instrumentation
// Frida script to bypass Android SSL pinning
Java.perform(function() {
console.log("[*] Bypassing SSL pinning...");
// Hook TrustManager
var TrustManager = Java.use('javax.net.ssl.X509TrustManager');
var SSLContext = Java.use('javax.net.ssl.SSLContext');
// Create custom TrustManager that accepts all certificates
var TrustManagerImpl = Java.registerClass({
name: 'com.sensepost.test.TrustManagerImpl',
implements: [TrustManager],
methods: {
checkClientTrusted: function(chain, authType) {
console.log("[+] checkClientTrusted called, bypassing...");
},
checkServerTrusted: function(chain, authType) {
console.log("[+] checkServerTrusted called, bypassing...");
},
getAcceptedIssuers: function() {
console.log("[+] getAcceptedIssuers called");
return [];
}
}
});
// Hook SSLContext.init()
SSLContext.init.overload(
'[Ljavax.net.ssl.KeyManager;',
'[Ljavax.net.ssl.TrustManager;',
'java.security.SecureRandom'
).implementation = function(keyManager, trustManager, secureRandom) {
console.log("[+] SSLContext.init() called");
console.log("[+] Replacing TrustManager with custom implementation");
var customTrustManager = TrustManagerImpl.$new();
this.init(keyManager, [customTrustManager], secureRandom);
};
console.log("[*] SSL pinning bypass complete!");
});
2. Objection Framework
# Automated SSL pinning bypass
objection --gadget com.example.app explore
# Inside objection REPL
android sslpinning disable
# Verify bypass
android sslpinning watchfor
3. Custom Trust Store (Root Required)
#!/usr/bin/env python3
"""
Add Burp Suite CA to Android system trust store
Requires root access
"""
import subprocess
import hashlib
def install_burp_ca_system_wide():
"""Install Burp CA as system-trusted certificate"""
# 1. Get Burp Suite CA certificate (DER format)
# Export from Burp: Proxy > Options > Import/Export CA Certificate
burp_cert_path = "burp-ca-cert.der"
# 2. Convert to PEM format
subprocess.run([
'openssl', 'x509',
'-inform', 'DER',
'-in', burp_cert_path,
'-out', 'burp-ca-cert.pem'
])
# 3. Compute hash for Android filename format
with open('burp-ca-cert.pem', 'rb') as f:
cert_data = f.read()
cert_hash = hashlib.md5(cert_data).hexdigest()[:8]
android_cert_name = f"{cert_hash}.0"
# 4. Push to Android system certificate store
subprocess.run(['adb', 'root'])
subprocess.run(['adb', 'remount'])
subprocess.run([
'adb', 'push',
'burp-ca-cert.pem',
f'/system/etc/security/cacerts/{android_cert_name}'
])
# 5. Set correct permissions
subprocess.run([
'adb', 'shell',
'chmod', '644',
f'/system/etc/security/cacerts/{android_cert_name}'
])
# 6. Reboot device
subprocess.run(['adb', 'reboot'])
print(f"[+] Burp CA installed as {android_cert_name}")
print("[+] Device rebooting...")
print("[+] SSL pinning should now be ineffective for most apps")
if __name__ == '__main__':
install_burp_ca_system_wide()
4. Patching APK
#!/bin/bash
# Patch Android APK to disable certificate pinning
APK="target_app.apk"
OUTPUT="target_app_patched.apk"
# 1. Decompile APK
apktool d "$APK" -o app_decompiled
# 2. Modify Network Security Config
cat > app_decompiled/res/xml/network_security_config.xml << 'EOF'
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config>
<trust-anchors>
<!-- Trust system certificates -->
<certificates src="system" />
<!-- Trust user-added certificates (Burp, mitmproxy, etc.) -->
<certificates src="user" />
</trust-anchors>
</base-config>
</network-security-config>
EOF
# 3. Reference in AndroidManifest.xml
sed -i 's/<application /<application android:networkSecurityConfig="@xml\/network_security_config" /' \
app_decompiled/AndroidManifest.xml
# 4. Remove pinning code from smali (if present)
# Find and patch TrustManager implementations
find app_decompiled/smali -name "*.smali" -exec sed -i \
's/invoke-virtual.*checkServerTrusted/# &/' {} \;
# 5. Rebuild APK
apktool b app_decompiled -o "$OUTPUT"
# 6. Sign APK
keytool -genkey -v -keystore my-key.keystore -alias my-key \
-keyalg RSA -keysize 2048 -validity 10000 \
-storepass android -keypass android \
-dname "CN=Test, O=Test, C=US"
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 \
-keystore my-key.keystore -storepass android \
"$OUTPUT" my-key
# 7. Align APK
zipalign -v 4 "$OUTPUT" "${OUTPUT/.apk/_aligned.apk}"
echo "[+] Patched APK: ${OUTPUT/.apk/_aligned.apk}"
echo "[+] Install with: adb install ${OUTPUT/.apk/_aligned.apk}"
Secure Implementation
Android Network Security Config
<!-- res/xml/network_security_config.xml -->
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<domain-config>
<domain includeSubdomains="true">api.example.com</domain>
<pin-set expiration="2025-12-31">
<!-- Pin public key hash (SPKI) -->
<pin digest="SHA-256">base64hash==</pin>
<!-- Backup pin (different key) -->
<pin digest="SHA-256">backuphash==</pin>
</pin-set>
</domain-config>
<!-- Debug-only: Allow user certificates in debug builds -->
<debug-overrides>
<trust-anchors>
<certificates src="user" />
</trust-anchors>
</debug-overrides>
</network-security-config>
iOS App Transport Security
<!-- Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
<key>NSPinnedDomains</key>
<dict>
<key>api.example.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSPinnedLeafIdentities</key>
<array>
<dict>
<key>SPKI-SHA256-BASE64</key>
<string>base64hash==</string>
</dict>
</array>
</dict>
</dict>
</dict>
Certificate Generation for Testing
#!/bin/bash
# Generate self-signed certificate for testing pinning
# 1. Generate private key
openssl genrsa -out server.key 2048
# 2. Generate certificate
openssl req -new -x509 -key server.key -out server.crt -days 365 \
-subj "/C=US/ST=State/L=City/O=Org/CN=api.example.com"
# 3. Extract public key hash for pinning
openssl x509 -in server.crt -pubkey -noout | \
openssl pkey -pubin -outform der | \
openssl dgst -sha256 -binary | \
base64
# Output: base64hash== (use this in pinning configuration)
Detection and Monitoring
Runtime Integrity Checks
public class CertificatePinningDetector {
/**
* Detect if certificate pinning has been bypassed
*/
public static boolean detectBypass() {
try {
// Check if Frida is injected
if (detectFrida()) {
Log.w("Security", "Frida detected - potential bypass!");
return true;
}
// Check if running in emulator/rooted device
if (isRooted() || isEmulator()) {
Log.w("Security", "Rooted/emulator environment detected");
return true;
}
// Check SSL context
SSLContext context = SSLContext.getInstance("TLS");
TrustManager[] trustManagers = getTrustManagers(context);
if (trustManagers == null || trustManagers.length == 0) {
Log.w("Security", "No trust managers - potential bypass!");
return true;
}
// Verify trust manager class is expected
for (TrustManager tm : trustManagers) {
String className = tm.getClass().getName();
// Check for known bypass class names
if (className.contains("TrustManagerImpl") ||
className.contains("NaiveTrustManager") ||
className.contains("AllTrustManager")) {
Log.w("Security", "Suspicious TrustManager: " + className);
return true;
}
}
return false;
} catch (Exception e) {
Log.e("Security", "Error detecting bypass", e);
return false;
}
}
private static boolean detectFrida() {
// Check for Frida artifacts
File[] suspiciousFiles = {
new File("/data/local/tmp/frida-server"),
new File("/data/local/tmp/frida-agent"),
};
for (File file : suspiciousFiles) {
if (file.exists()) {
return true;
}
}
// Check for Frida ports
try {
Socket socket = new Socket("localhost", 27042);
socket.close();
return true; // Frida default port is open
} catch (IOException e) {
// Port not open - good
}
return false;
}
}
Best Practices
Multi-Layer Security
// Kotlin: Implement defense in depth
class SecureAPIClient {
companion object {
// 1. Certificate pinning
private val pinnedPublicKeys = arrayOf(
"sha256/primary_key_hash==",
"sha256/backup_key_hash=="
)
// 2. Root detection
fun isDeviceSecure(): Boolean {
return !RootBeer(context).isRooted &&
!isEmulator() &&
!hasHooks()
}
// 3. Integrity checks
fun verifyAppIntegrity(): Boolean {
val packageInfo = context.packageManager
.getPackageInfo(context.packageName, PackageManager.GET_SIGNATURES)
val signature = packageInfo.signatures[0]
val md = MessageDigest.getInstance("SHA-256")
val digest = md.digest(signature.toByteArray())
// Compare with known good signature
return digest.contentEquals(KNOWN_SIGNATURE_HASH)
}
}
init {
// Perform security checks on initialization
if (!isDeviceSecure()) {
throw SecurityException("Insecure environment detected")
}
if (!verifyAppIntegrity()) {
throw SecurityException("App integrity check failed")
}
}
}
Key Rotation Strategy
"""
Certificate pinning with graceful key rotation
"""
class PinningConfig:
def __init__(self):
self.pins = {
'current': [
'sha256/current_primary_hash==',
'sha256/current_backup_hash=='
],
'next': [
'sha256/next_primary_hash==',
'sha256/next_backup_hash=='
]
}
# Rotation schedule
self.rotation_date = datetime(2025, 6, 1)
def get_active_pins(self):
"""Return pins to enforce based on current date"""
now = datetime.now()
if now < self.rotation_date:
# Accept both current and next pins during transition
return self.pins['current'] + self.pins['next']
else:
# After rotation date, only accept new pins
return self.pins['next']
Conclusion
Certificate pinning significantly enhances security but must be implemented correctly to avoid:
- Operational issues during certificate rotation
- Easy bypass through dynamic instrumentation
- Debugging difficulties during development
Recommendations:
- Use public key pinning (more flexible than certificate pinning)
- Include backup pins for rotation
- Implement multi-layer security (root detection, integrity checks)
- Use Android Network Security Config or iOS ATS for declarative pinning
- Monitor for bypass attempts in production
References
- OWASP Mobile Security Testing Guide - Certificate Pinning
- RFC 7469 - Public Key Pinning Extension for HTTP
- Android Developers - Network Security Configuration
- Apple Developer - App Transport Security