Wed 11 May 2016
| Modified: Fri 10 April 2026
SSL Pinning on Android (Certificate and Public Key Pinning)
Securing communication between an Android app and its backend starts with TLS. TLS protects data in transit by encrypting the connection, authenticating the server, and preventing sensitive traffic from being sent in clear text. In a standard Android setup, the app relies on the platform trust store and normal certificate validation to decide whether a server certificate should be trusted.
Pinning adds an extra trust restriction on top of that baseline. Instead of accepting any certificate chain that validates through the normal trust model, the app only accepts chains that contain specific expected key material. That can make man-in-the-middle attacks harder in some scenarios, but it also introduces operational risk: certificate changes, CA migration, or key rotation can break legitimate connections if pinning is too rigid or poorly managed. Android’s guidance on certificate pinning is careful here, noting that pinning is not generally recommended for most apps because server configuration changes can break connectivity unless the client is updated.
This guide explains what SSL pinning on Android means, when it helps, what it does not protect against, the main implementation options, how to validate it safely, and how to operate it in production without turning certificate rotation into an outage. It also includes HttpsURLConnection, OkHttp, Retrofit, Volley, and Picasso examples for teams working across both modern and legacy Android networking stacks.

Table of Contents
- What SSL pinning on Android means
- SSL pinning vs TLS pinning vs certificate pinning
- What pinning does not protect against
- Threat model: when pinning helps
- Pinning options on Android
- Implementation examples
- How to verify SSL pinning works
- Operating pinning safely in production
- Related reading
- FAQ
What SSL pinning on Android means
In a standard Android setup, an app trusts server certificates that validate through the platform trust store. SSL pinning adds an extra restriction on top of that model: the connection is accepted only if the certificate chain contains specific expected key material for the target domain.
On Android, this is typically implemented by pinning hashes of the certificate’s public key, also known as SPKI pinning. Instead of trusting any valid chain anchored in a generally trusted certificate authority, the app narrows trust to chains that include one of the pinned public keys.
In practice, this means the app is not only checking whether a certificate is valid, but also whether it matches a more specific trust expectation defined by the application itself. OkHttp’s CertificatePinner follows the same idea by validating the server’s certificate chain against pinned public key hashes.
SSL pinning vs TLS pinning vs certificate pinning
“SSL pinning” is still the phrase most people use, but in practice it usually refers to TLS certificate pinning or public key pinning. In Android’s current security guidance, pinning is described in terms of public key hashes. Throughout this guide, the terms SSL pinning, TLS pinning, certificate pinning, and public key pinning are used in the way practitioners commonly search for and discuss the topic, while keeping the technical meaning precise.
What pinning does not protect against
Pinning is not a substitute for proper TLS validation. If an app uses an unsafe HostnameVerifier or an unsafe X509TrustManager, it may still become vulnerable to impersonation and interception. Android documents both of these as explicit security risks because they can cause the app to accept malicious or invalid connections.
Pinning also does not fix server-side vulnerabilities, leaked credentials, broken session handling, insecure APIs, or sensitive data exposure elsewhere in the app. It is one transport-security control, not a complete mobile app security strategy. That is one reason to treat pinning as part of a broader Android security model rather than as a standalone hardening checkbox.
Threat model: when pinning helps
Android’s documentation explains the core scenario clearly: by default, apps trust many pre-installed certificate authorities, and if one of those CAs were to issue a fraudulent certificate, the app could be exposed to an on-path attacker. Pinning reduces that trust by requiring the certificate chain to contain one of the pinned public keys. OkHttp describes the same protection in terms of defending against attacks on certificate authorities and against man-in-the-middle certificate authorities known or unknown to the user.
That makes pinning more relevant when a team wants tighter control over which trust chains are accepted for a specific backend. It is generally easiest to justify when the backend is stable, certificate lifecycle is tightly managed, and the operational burden of rotation has already been planned. Even then, pinning is a high-friction control and should not be introduced casually. Android treats it as an operationally fragile measure, and OkHttp’s own documentation is explicit enough to label it: “Certificate Pinning is Dangerous! because it limits certificate updates and adds operational complexity.
Pinning options on Android
In newer Android codebases, pinning is more commonly handled through Android Network Security Configuration (see example in next section) , which supports certificate pinning, backup pins, pin expiration, custom trust anchors, and debug overrides in a declarative configuration file. This keeps trust decisions centralized rather than spreading TLS handling across multiple libraries. If the app uses OkHttp directly, CertificatePinner remains the main client-level option. Because Retrofit sits on top of an HTTP client, pinning behavior in Retrofit setups depends on the configured underlying client, typically OkHttp.
In older Android codebases, pinning may still rely on custom TLS handling in HttpsURLConnection, Volley, or Picasso integrations. These examples can still be useful as historical references or for maintaining legacy apps, but they should be treated as older implementation patterns rather than the default choice for new projects. For Android pinning, OWASP MASTG describes Network Security Configuration as the preferred and recommended approach, while warning that custom TrustManager-based pinning is a low-level method that is prone to errors if not implemented carefully
| Approach | Supports pinning | Maintenance burden | Main gotcha | Best fit |
|---|---|---|---|---|
| Android Network Security Configuration | Yes | Lower | Requires careful domain and debug setup | Most modern Android apps |
| OkHttp CertificatePinner | Yes | Medium | Rotation and hostname coverage must be planned | Apps already using OkHttp directly |
| Retrofit with configured OkHttp client | Yes | Medium | Retrofit inherits the client behavior; pinning is not separate | Retrofit-based apps |
| HttpsURLConnection custom trust setup | Possible | Higher | Easy to over-customize TLS handling | Legacy maintenance only |
| Volley custom TLS setup | Possible | Higher | Harder to keep centralized | Legacy maintenance only |
| Picasso custom downloader setup | Possible | Higher | Older integration pattern | Legacy maintenance only |
This comparison reflects current Android guidance, OkHttp’s documentation, and the practical difference between declarative platform-level configuration and older library-specific trust customization.
Implementation examples
Pinning can be implemented in different ways depending on the Android networking stack in use. For new work, start with Network Security Configuration or a modern OkHttp setup. The library examples below are best understood as legacy reference patterns for teams that still maintain older stacks.
Modern recommendation before the legacy examples
If you are building a new Android app today, start with Android Network Security Configuration for declarative trust and pinning, or with OkHttp CertificatePinner if your networking stack is already centered on OkHttp. Android supports public-key pinning through pin-set, supports backup pins, allows an optional expiration date, and includes debug-only overrides so teams can test securely without weakening release builds.
Android Network Security Configuration (NSC) pinning example (declarative)
Create res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Apply rules to a specific domain (and optionally its subdomains) -->
<domain-config cleartextTrafficPermitted="false">
<domain includeSubdomains="true">example.com</domain>
<!-- Public-key pins (SHA-256 of SPKI), with an expiration date -->
<pin-set expiration="2027-12-31">
<!-- Primary pin -->
<pin digest="SHA-256">afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=</pin>
<!-- Backup pin (rotate keys/certs safely) -->
<pin digest="SHA-256">BASE64_SHA256_BACKUP_SPKI_PIN_HERE=</pin>
</pin-set>
<!-- Optional: customize which CAs you trust for this domain -->
<trust-anchors>
<certificates src="system" />
<!-- If you also need a private CA bundled with the app: -->
<!-- <certificates src="@raw/my_private_ca" /> -->
</trust-anchors>
</domain-config>
<!-- Optional (debug builds): allow user-added / debug CAs without weakening release -->
<debug-overrides>
<trust-anchors>
<certificates src="user" />
<!-- or a dedicated debug CA -->
<!-- <certificates src="@raw/debug_ca" /> -->
</trust-anchors>
</debug-overrides>
</network-security-config>
Key points: pin-set supports multiple pins (primary + backup) and an expiration date (format yyyy-MM-dd), after which pinning is disabled if you don’t update the config.
Wire it up in AndroidManifest.xml
<application
android:networkSecurityConfig="@xml/network_security_config"
android:usesCleartextTraffic="false"
... >
</application>
Android certificate pinning using HttpsURLConnection
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// Generate the certificate using the certificate file under res/raw/cert.cer
InputStream caInput = new BufferedInputStream(getResources().openRawResource(R.raw.cert));
Certificate ca = cf.generateCertificate(caInput);
caInput.close();
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
// Tell the URLConnection to use a SocketFactory from our SSLContext
URL url = new URL("https://example.com/faq/");
HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in = urlConnection.getInputStream();
theString = readInputStream(in);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
Android certificate pinning using OkHTTP
//okhttp version 3.x
public CertificatePinning()
{
// We add the public key of our trusted server hashed and encoded base64
client = new OkHttpClient.Builder().certificatePinner(new CertificatePinner.Builder()
.add("https://example.com", "sha256/afwiKY3RxoMmLkuRW1l7QsPZTJPwDS2pdDROQjXw8ig=").build()).build();
}
public void run() throws Exception
{
Request request = new Request.Builder().url("https://example.com/faq").build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
/**
* response.handshake() contains the TLS handshake of the connection that carried this response, or null if the response
* was received without TLS.
*/
if(response.handshake())
{
for (Certificate certificate : response.handshake().peerCertificates())
{
//Pin returns SHA-256 hash of the public key
String caPin = CertificatePinner.pin(certificate);
// then you we to check the hash value and apply the corresponding action
}
}
}
Android certificate pinning using Retrofit
// Use the steps above to create OkHttpClient and set the custom client when building adapter
Retrofit retrofit = new Retrofit.Builder()
.baseUrl("https://example.com")
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();
Volley:
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// Generate the certificate using the certificate file under res/raw/cert.cer
InputStream caInput = new BufferedInputStream(getResources().openRawResource(R.raw.cert));
Certificate ca = cf.generateCertificate(caInput);
caInput.close();
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore trusted = KeyStore.getInstance(keyStoreType);
trusted.load(null, null);
trusted.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(trusted);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
SSLSocketFactory sf = context.getSocketFactory();
mRequestQueue = Volley.newRequestQueue(mCtx.getApplicationContext(), new HurlStack(null, sf));
Android certificate pinning using Picasso
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// Certificate file is under res/raw/cert.cer
InputStream caInput = new BufferedInputStream(getResources().openRawResource(R.raw.cert));
Certificate ca = cf.generateCertificate(caInput);
caInput.close();
// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);
// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);
// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);
OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setSslSocketFactory(sslContext.getSocketFactory());
OkHttpDownloader okHttpDownloader = new OkHttpDownloader(okHttpClient);
Picasso.Builder builder = new Picasso.Builder(context);
builder.downloader(okHttpDownloader);
Picasso sPicasso = builder.build();
How to verify SSL pinning works (without breaking users)
To verify SSL pinning on Android and iOS apps, set up a proxy like Burp Suite or mitmproxy and confirm the app blocks traffic interception despite trusting the proxy's CA certificate. This proves pinning prevents man-in-the-middle attacks.
Prerequisites:
- Install mitmproxy: pip install mitmproxy.
- Run mitmdump -p 8080 (or mitmproxy for UI); note your IP (e.g., ifconfig or ip addr).
- Rooted Android or jailbroken iOS recommended for full trust; works on emulators/simulators.
Android Steps
- Set device Wi-Fi proxy to
:8080; test with browser HTTPS (should proxy). - Visit http://mitm.it/?mode=android in browser; download/install user CA.
- For system trust (required for most apps): openssl x509 -inform PEM -subject_hash_old -in ~/.mitmproxy/mitmproxy-ca-cert.pem | head -1 →
.0; adb push .0 /system/etc/security/cacerts/; adb shell chmod 644 /system/etc/security/cacerts/ .0; reboot. - Trigger app network calls—if no flows in mitmproxy/mitmdump and SSL errors in adb logcat | grep -i ssl—pinning is active.
Validation:
Normal app function without proxy confirms issue is pinning, not config. If traffic intercepts, no pinning (or use bypass tools like objection for confirmation).
Tru to do that at the application startup but also after since some application might check only during specific actions and ignore the check afterward.
Operating pinning safely in production (rotation, backup pins, rollout)
The main production risk with pinning is certificate or key rotation. Android explicitly recommends always including a backup key so that if you need to switch keys or change CAs, app connectivity is not lost. It also supports an expiration date for pins, which can reduce the risk of indefinite breakage in stale app versions, though that comes with the tradeoff that expired pins no longer protect the connection.
OkHttp is equally direct about the operational burden: pinning limits the server team’s ability to update certificates and migrate between certificate authorities. That is why pinning should be rolled out like a production feature, not just merged like a code cleanup. A sensible rollout path is internal builds first, then beta, then a limited production percentage, and only then full rollout once certificate behavior, monitoring, and fallback plans are validated. The staged rollout advice here is an operational recommendation based on the constraints Android and OkHttp document.
When pinning fails in production, the app should fail in a way that is understandable and diagnosable. At a minimum, define the user-facing message, logging fields, alerting thresholds, and support triage path. A pin failure is different from a generic timeout or offline condition, so it should not disappear into the same error bucket.
Enterprise TLS inspection and captive portals also deserve an explicit policy decision. Because pinning restricts trust to expected key material, environments that replace server certificates can cause pinning to fail by design. Teams should decide that behavior deliberately before rollout instead of discovering it during deployment. For a technical example of analyzing traffic in pinned environments without disabling pinning, see our article on Universal bypass of SSL Pinning ... from theory to a full working PoC with LLDB.
FAQ
Is SSL pinning on Android worth it?
It can be, but only when the backend, release process, and certificate lifecycle are mature enough to support it. Android’s official guidance explicitly cautions that certificate pinning is not generally recommended for many apps because backend certificate changes can break connectivity unless the client is updated. When teams do choose pinning, Android recommends backup pins and a short expiration window.
What is the difference between certificate pinning and public key pinning?
In Android’s current guidance, pinning is expressed as hashes of the certificate’s public key rather than as a raw full-certificate file comparison. OkHttp’s CertificatePinner also documents SPKI-based pinning. What breaks when certificates rotate? If the new certificate chain no longer contains one of the pinned keys, the app can lose connectivity until the pins are updated or a backup pin is already present. That is why Android recommends backup keys and supports optional pin expiration.
Does Android Network Security Configuration support pinning?
Yes. Android supports certificate pinning through pin-set entries in Network Security Configuration, including multiple pins, backup pins, expiration, custom trust anchors, and debug-only overrides.
Does Retrofit do pinning by itself?
No. Retrofit turns your HTTP API into a Java or Kotlin interface, and pinning behavior comes from the configured underlying client, such as OkHttp.
How do I test pinning without weakening production security?
Use controlled staging tests, validate both success and mismatch paths, and use Android’s debug configuration support instead of weakening release validation logic.
How should Android apps handle enterprise TLS inspection?
Teams should decide that policy explicitly before rollout. Because pinning validates specific key material, TLS-inspecting environments can fail by design, so the expected behavior should be documented rather than discovered by users during deployment.
What is the most modern starting point for new Android apps?
For most new apps, start with Network Security Configuration for centralized declarative pinning, or use OkHttp CertificatePinner when the networking layer is already centered on OkHttp. Keep older library-specific custom TLS setups only when maintaining legacy cod
Table of Contents
- SSL Pinning on Android (Certificate and Public Key Pinning)
- Table of Contents
- What SSL pinning on Android means
- SSL pinning vs TLS pinning vs certificate pinning
- Threat model: when pinning helps
- Pinning options on Android
- Implementation examples
- Modern recommendation before the legacy examples
- Android Network Security Configuration (NSC) pinning example (declarative)
- Android certificate pinning using HttpsURLConnection
- Android certificate pinning using OkHTTP
- Android certificate pinning using Retrofit
- Android certificate pinning using Picasso
- How to verify SSL pinning works (without breaking users)
- Operating pinning safely in production (rotation, backup pins, rollout)
- FAQ
- Is SSL pinning on Android worth it?
- What is the difference between certificate pinning and public key pinning?
- Does Android Network Security Configuration support pinning?
- Does Retrofit do pinning by itself?
- How do I test pinning without weakening production security?
- How should Android apps handle enterprise TLS inspection?
- What is the most modern starting point for new Android apps?