🌏 閱讀中文版本
Why Do You Need SSL Pinning?
Mobile Application Security Threats:
- Man-in-the-Middle Attacks (MITM): Attackers intercept communication between app and server, stealing sensitive data
- Certificate Forgery: Malicious software may install forged root certificates on devices
- Public Wi-Fi Risks: Insecure network environments prone to eavesdropping
- Enterprise Internal Risks: Some corporate firewalls decrypt HTTPS traffic for inspection
Value of SSL Pinning:
- Additional Security Layer: Even if device trust chain is compromised, app can still verify server identity
- Prevent Certificate Forgery: Only trust specified certificates or public keys, reject others
- Compliance Requirements: Security regulations for finance, healthcare industries
- Protect Sensitive Data: Ensure user privacy and transaction security
What is SSL Pinning?
SSL Pinning is a security technique that embeds a server’s certificate or public key into an application, allowing the app to trust only specified server certificates. Even if attackers forge certificates, they cannot perform man-in-the-middle attacks (MITM). Pinning can be divided into two methods:
- Certificate Pinning: Embeds the entire server certificate in the app for verification
- Public Key Pinning: Extracts the public key from the server certificate and only verifies if the public key matches
What is a Custom Trust Chain?
Custom Trust Chain is typically used to replace Android’s default certificate verification mechanism. This method allows you to specify root certificates or intermediate certificates that your app trusts.
Common Use Cases:
- Self-Signed Certificates: Server uses self-signed certificates (development or test environments)
- Enterprise Internal CA: Using internal enterprise Certificate Authority (CA)
- Special Intermediate Certificates: Server’s certificate trust chain includes specific intermediate certificates
- Test Environment Isolation: Development environments use different certificate systems
Preparation
Required Tools and Resources:
- Server Certificate: Obtain server certificate in
.ceror.crtformat - OpenSSL: Used to inspect and extract certificate information
- macOS/Linux: Usually pre-installed
- Windows: Download from OpenSSL official site
- Android Project Environment:
- Latest version of Android Studio
- OkHttp library (version 4.x or above)
- Kotlin or Java development environment
Implementation Steps
Step 1: Inspect Server Certificate
First, inspect the server certificate to ensure domain, validity period, and issuer meet requirements.
Use the following OpenSSL command:
openssl x509 -in <your_certificate.cer> -text -noout
Key Points to Verify:
- Subject: Check domain (CN=your.domain.com) is correct
- Issuer: Confirm certificate issuer (e.g., Let’s Encrypt, DigiCert)
- Validity: Check certificate validity period (Not Before / Not After)
- Subject Alternative Name: Confirm all supported domains
Step 2: Extract Server Public Key
If you choose to use Public Key Pinning (recommended), extract the public key from the server certificate:
openssl x509 -in <your_certificate.cer> -pubkey -noout > public_key.pem
Step 3: Generate SHA-256 Fingerprint of Public Key
Generate the SHA-256 fingerprint of the server public key for SSL Pinning:
openssl x509 -in <your_certificate.cer> -pubkey -noout | \
openssl rsa -pubin -outform DER | \
openssl dgst -sha256 -binary | \
base64
Example fingerprint output:
sha256/AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWxYz=
Record this fingerprint for later configuration in Android code.
Step 4: Configure Certificate in Android Project
1. Create resource directory:
mkdir -p app/src/main/res/raw
2. Copy certificate file:
cp <your_certificate.cer> app/src/main/res/raw/server_cert.cer
Step 5: Implement Custom Trust Chain (Kotlin)
import java.security.KeyStore
import java.security.cert.CertificateFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManagerFactory
import javax.net.ssl.X509TrustManager
import okhttp3.OkHttpClient
fun createCustomTrustManager(context: Context): X509TrustManager {
val trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
)
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType())
keyStore.load(null, null)
val certificateFactory = CertificateFactory.getInstance("X.509")
val inputStream = context.resources.openRawResource(R.raw.server_cert)
try {
val certificate = certificateFactory.generateCertificate(inputStream)
keyStore.setCertificateEntry("server", certificate)
} finally {
inputStream.close()
}
trustManagerFactory.init(keyStore)
val trustManagers = trustManagerFactory.trustManagers
return trustManagers[0] as X509TrustManager
}
fun createSecureClient(context: Context): OkHttpClient {
val trustManager = createCustomTrustManager(context)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(trustManager), null)
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.build()
}
Step 6: Implement SSL Pinning (Kotlin)
Configure SSL Pinning using OkHttp:
import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
fun createPinnedClient(): OkHttpClient {
val certificatePinner = CertificatePinner.Builder()
// Primary certificate
.add(
"api.example.com",
"sha256/AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWxYz="
)
// Backup certificate (recommended)
.add(
"api.example.com",
"sha256/BackupCertificateHashGoesHere1234567890AbCdEfGhIjKlMnOpQrStUvWxYz="
)
.build()
return OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.build()
}
Step 7: Combine Custom Trust Chain with SSL Pinning
fun createFullySecureClient(context: Context): OkHttpClient {
val trustManager = createCustomTrustManager(context)
val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, arrayOf(trustManager), null)
val certificatePinner = CertificatePinner.Builder()
.add(
"api.example.com",
"sha256/AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWxYz="
)
.build()
return OkHttpClient.Builder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.certificatePinner(certificatePinner)
.build()
}
Java Implementation Example
import java.security.KeyStore;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import okhttp3.CertificatePinner;
import okhttp3.OkHttpClient;
public class SecureClientBuilder {
public static OkHttpClient createSecureClient(Context context) {
try {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
InputStream certInputStream = context.getResources()
.openRawResource(R.raw.server_cert);
Certificate ca = cf.generateCertificate(certInputStream);
certInputStream.close();
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
keyStore.load(null, null);
keyStore.setCertificateEntry("server", ca);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm()
);
tmf.init(keyStore);
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, tmf.getTrustManagers(), null);
CertificatePinner certificatePinner = new CertificatePinner.Builder()
.add("api.example.com",
"sha256/AbCdEfGhIjKlMnOpQrStUvWxYz1234567890AbCdEfGhIjKlMnOpQrStUvWxYz=")
.build();
return new OkHttpClient.Builder()
.sslSocketFactory(sslContext.getSocketFactory(),
(X509TrustManager) tmf.getTrustManagers()[0])
.certificatePinner(certificatePinner)
.build();
} catch (Exception e) {
throw new RuntimeException("Failed to create secure client", e);
}
}
}
Testing and Verification
Test if SSL Pinning is working:
- Normal connection test:
val client = createPinnedClient() val request = Request.Builder() .url("https://api.example.com/test") .build() client.newCall(request).execute().use { response -> println("Response: ${response.body?.string()}") } - Wrong certificate test:
- Modify the SHA-256 fingerprint in Pinning to an incorrect value
- Expected result: Should throw
SSLPeerUnverifiedException
- Test with Charles Proxy:
- Install Charles’ root certificate on device
- Attempt to intercept app traffic
- Expected result: Connection fails, cannot intercept
Common Issues and Solutions
Issue 1: Why do I need a specific certificate (like eCA1-to-HRCA1.crt)?
Reason:
- This certificate might be part of the server’s certificate trust chain (e.g., intermediate certificate)
- Without this certificate, the app cannot successfully establish a trust chain
- Android’s default trust store may not include certain enterprise or special CAs
Solution:
- Request the complete certificate chain from the server administrator
- Add all intermediate certificates to the KeyStore
- Use
openssl s_clientcommand to check the server’s complete certificate chain
Issue 2: What’s the difference between SSL Pinning and Custom Trust Chain?
| Feature | SSL Pinning | Custom Trust Chain |
|---|---|---|
| Verification Object | Certificate or public key fingerprint | Complete certificate chain |
| Flexibility | Lower, requires app update | Higher, can dynamically manage certificates |
| Security | Very high, exact match | High, depends on trust chain |
| Use Case | Prevent MITM attacks | Self-signed or enterprise CA |
Recommendation: You can use both together for double protection.
Issue 3: Choose Public Key Pinning or Certificate Pinning?
Public Key Pinning (Recommended):
- ✅ More flexible, public key can remain unchanged when certificate is updated
- ✅ Reduces app update frequency
- ✅ Supports certificate rotation
Certificate Pinning:
- ✅ More strict, exact certificate match
- ❌ Must update app after certificate renewal
- ❌ Higher maintenance cost
Issue 4: What to do when certificate expires or needs renewal?
Solutions:
- Prepare multiple Pins:
val certificatePinner = CertificatePinner.Builder() .add("api.example.com", "sha256/current_cert_hash") .add("api.example.com", "sha256/backup_cert_hash") .build() - Implement Pin update mechanism:
- Provide Pin configuration API on server side
- Dynamically update Pin list on app startup
- Use Firebase Remote Config or similar services
- Monitoring and alerts:
- Set certificate expiration reminders (30-60 days in advance)
- Monitor SSL Pinning failure rate
- Prepare emergency update process
Issue 5: How to test in development environment?
Solution:
- Use Build Variants to differentiate development and production
- Disable Pinning in development or use development certificates
- Use conditional compilation:
val client = if (BuildConfig.DEBUG) { // Development: Pinning disabled OkHttpClient.Builder().build() } else { // Production: Pinning enabled createPinnedClient() }
Security Best Practices
- Use Public Key Pinning: Provides better flexibility
- Prepare backup Pins: Configure at least 2-3 valid Pins
- Regular certificate checks: Set up automated monitoring
- Implement Pin update mechanism: Dynamically update via remote configuration
- Error handling and fallback: Provide appropriate error messages when Pin validation fails
- Logging: Record Pinning failure events for security analysis
- Multi-layer protection: Use SSL Pinning together with application-layer encryption
Real-World Application Cases
Financial Applications:
- Use SSL Pinning to protect transaction APIs
- Combine with device fingerprinting and biometric authentication
- Regular certificate and Pin rotation
Enterprise Applications:
- Use custom trust chain to trust enterprise CA
- Configure self-signed certificates for internal networks
- Implement Certificate Transparency
Conclusion
Implementing SSL Pinning and custom trust chains is an important step in ensuring Android app communication security. Through this guide, you can:
- Understand the principles and importance of SSL Pinning
- Correctly configure custom trust chains
- Implement public key Pinning (recommended approach)
- Handle certificate renewal and maintenance issues
- Follow security best practices
Next Steps:
- Verify Pinning configuration in test environment
- Implement certificate monitoring and alert mechanisms
- Prepare certificate rotation plan
- Regular security audits
Related Articles
- iOS SSL Pinning and Certificate Validation Guide
- Secure Guest Mode Data Recording in iOS and Android Applications
- Biometric Change Detection: Complete Implementation Guide for Android and iOS
- iOS vs Android Deep Link Complete Comparison Guide: In-Depth Analysis of Universal Links & App Links
- iOS vs Android Deep Link 完整比較指南:Universal Links 與 App Links 深度解析