Network security: TLS, certificate pinning
HTTPS/TLS encrypts traffic, but whom you trust is defined by the system CA store. The problem: a malicious CA can be installed on the user's device (or a CA itself gets compromised) → a MITM (man-in-the-middle) attack.
Certificate pinning — the app trusts only its own server's certificate (usually the SHA-256 hash of the public key); even if the chain looks "trusted", a pin mismatch kills the connection.
Two implementation routes:
- OkHttp CertificatePinner — pins in code
- Network Security Config — declarative in
res/xml/network_security_config.xml: pin-sets + expiration, cleartext bans, debug-overrides
The critical rule — a backup pin: pin only the current certificate and a rotation locks out every user, rescued only by an app update. Always pin a standby key too.
Also: cleartextTrafficPermitted="false" (default since API 28) — HTTP fully banned; debug CAs allowed only inside debug-overrides.
// 1) OkHttp CertificatePinner — backup pin MÜTLƏQ
val pinner = CertificatePinner.Builder()
.add("api.bank.example", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") // cari
.add("api.bank.example", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=") // ehtiyat
.build()
val client = OkHttpClient.Builder()
.certificatePinner(pinner)
.build()
// 2) network_security_config.xml — deklarativ
// <network-security-config>
// <domain-config cleartextTrafficPermitted="false">
// <domain includeSubdomains="true">api.bank.example</domain>
// <pin-set expiration="2027-01-01">
// <pin digest="SHA-256">AAAA...=</pin>
// <pin digest="SHA-256">BBBB...=</pin>
// </pin-set>
// </domain-config>
// </network-security-config>Pinning both ways
What the pin binds to — a nuance you must know: usually the public key (SPKI hash), not the leaf certificate itself — renewing the certificate while keeping the key leaves the pin valid. Pinning an intermediate CA is the softer policy, pinning the leaf the strictest. Pin expiration is the security-availability trade-off: past the date, pinning disables so a forgotten rotation cannot brick the app forever.
🛠 Practice task
Run a MITM in your own lab:
- Install Charles or mitmproxy, add its certificate to the device and read the app's traffic without pinning.
- Then add a
CertificatePinner(or a network security config pin-set) — watch the request die withSSLPeerUnverifiedException. - Set up the debug/release split:
debug-overridesin debug builds only.
Done when: you added a backup pin and can explain the rotation risk it covers.
📚 Sources and documentation
- Network security configurationofficialdeveloper.android.com
- Security with HTTPS and SSLofficialdeveloper.android.com
- OkHttp CertificatePinnerofficiallysine.dev