Missing Certificate Pinning

ID

java.android_certificate_pinning

Severity

high

Resource

Misconfiguration

Language

Java

Tags

CWE:295, CWE:297, MASVS:MSTG-NETWORK-4, NIST.SP.800-53, OWASP:2021:A2, OWASP:2021:A7, PCI-DSS:6.5.4, android

Description

Certificate pinning is a critical security mechanism that validates the server’s certificate against a known, pre-defined certificate or public key. When certificate pinning is missing, Android applications are vulnerable to man-in-the-middle (MITM) attacks, even when using HTTPS.

This detector identifies Android applications that make network requests without implementing certificate pinning, specifically:

  • OkHttp clients built without CertificatePinner configuration

  • Retrofit clients without custom OkHttpClient with certificate pinning

  • Network connections that should be protected by certificate pinning but aren’t

Without certificate pinning, an attacker who has compromised a Certificate Authority or installed a malicious certificate on the device can intercept, read, and modify encrypted HTTPS traffic.

Rationale

HTTPS alone is not sufficient to protect sensitive mobile applications. While TLS/SSL encrypts traffic between client and server, it relies on the Certificate Authority (CA) trust model. This model has several weaknesses:

  • Compromised CAs: If a Certificate Authority is compromised, attackers can issue valid certificates for any domain

  • User-Installed Certificates: Users can install malicious root certificates on their devices

  • Nation-State Attacks: Some countries operate CAs that could be compelled to issue fraudulent certificates

  • Certificate Misissuance: CAs have historically mis-issued certificates due to errors or social engineering

This is particularly dangerous for payment processing, healthcare, or banking applications where: * PCI-DSS compliance requires protection of cardholder data * HIPAA requires protection of health information * Financial regulations require secure transmission of sensitive data

Certificate pinning provides defense-in-depth by ensuring the application only trusts specific certificates or public keys, regardless of whether the CA chain validates correctly.

Remediation

When to Use Certificate Pinning

Certificate pinning is recommended for:

  • Online Banking and Financial Apps

  • Healthcare Applications

  • Payment Processing: Apps processing credit card or payment data (PCI-DSS requirement)

  • Enterprise/Corporate Apps: Internal business apps connecting to known corporate servers

  • Government and Military Applications

  • IoT and Embedded Systems: Devices with limited capability to perform full certificate validation

  • Apps with Fixed Endpoints: Applications that only connect to a small, known set of servers under your control

Certificate pinning provides strong protection when:

  • You control both the client and server infrastructure

  • The set of server endpoints is limited and well-defined

  • Security requirements justify the operational overhead

When NOT to Use Certificate Pinning

Certificate pinning may not be appropriate for:

  • Apps with Dynamic/User-Provided URLs: Browsers, web viewers, or apps where users can enter arbitrary URLs

  • Apps Connecting to Many Third-Party Services: Applications integrating with numerous external APIs you don’t control

  • Content Aggregators: News readers, social media clients, or RSS aggregators connecting to diverse sources.

  • SDK/Libraries for Third-Party Use.

  • Rapid Prototyping/Frequent Backend Changes.

Certificate pinning creates overhead because certificate renewal requires updating the application or its configuration. When a server’s certificate expires and is renewed, pinned apps will fail to connect. Organizations must release an app update with new certificate pins before the old certificate expires. Users who do not update their app will lose connectivity when certificates rotate, which can result in service disruptions if not carefully planned.

To mitigate these risks:

  • Always pin multiple certificates (current + backup)

  • Plan certificate rotations in advance

  • Monitor certificate expiration dates

  • Consider using remote configuration for pins (with secure delivery)

  • Have a rollback/recovery strategy

Which Certificate to Pin?

Choose your certificate pinning strategy based on your app’s update process and security needs:

Leaf Certificate (Server) Pinning

  • Not recommended for most apps.

  • Very secure, but requires updating the app every time the server certificate changes, which can be often (e.g., every 90 days for Let’s Encrypt).

  • Best only if you need maximum security and can ensure reliable, rapid app updates.

Intermediate CA Pinning

  • Recommended for most production apps.

  • Secure and practical: app updates are needed only if you switch CA providers or the intermediate CA changes (which happens rarely, every 5-10 years).

  • Lets you renew server certificates without updating the app, as long as you stick to the same CA.

Root CA Pinning

  • Least recommended.

  • Very flexible: hardly ever needs updating. But weak security: a compromised CA can sign for any domain.

  • Use only if you cannot update your app at all, such as for some IoT devices.

Public Key Pinning (SPKI)

  • Industry best practice.

  • Pin the public key hash, not the certificate, so certs can be replaced or renewed as long as the key pair stays the same.

  • Supports flexible certificate renewals and is recommended by OWASP. However, you’ll need to update your app if you rotate keys (suggested annually).

For most production Android applications, follow this strategy:

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit

class SecureApiClient {
    // Pin the SPKI (public key) of intermediate CA certificates
    // Include both current and backup intermediate CAs
    private val certificatePinner = CertificatePinner.Builder()
        // Primary intermediate CA (e.g., Let's Encrypt R3)
        .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")

        // Backup intermediate CA (e.g., Let's Encrypt R4 or different CA)
        .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")

        // Optional: Pin your own server's public key as additional layer
        .add("api.example.com", "sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=")

        .build()

    // Create OkHttpClient with certificate pinning
    private val client = OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    fun fetchUserData(): User {
        val request = Request.Builder()
            .url("https://api.example.com/user/profile")
            .build()

        // Now protected by certificate pinning
        val response = client.newCall(request).execute()
        return parseUser(response.body?.string())
    }
}

How to extract certificate pins:

# Extract SPKI hash for intermediate CA certificate
# Step 1: Get the certificate chain
openssl s_client -connect api.example.com:443 -showcerts < /dev/null 2>/dev/null | \
    openssl x509 -outform PEM > cert.pem

# Step 2: Extract public key and compute SHA-256 hash
openssl x509 -in cert.pem -pubkey -noout | \
    openssl pkey -pubin -outform der | \
    openssl dgst -sha256 -binary | \
    openssl enc -base64

# Alternative: Let OkHttp tell you the pins
# Add intentionally wrong pin, run app, and check LogCat for actual pins:
# CertificatePinner: Certificate pinning failure!
#   Peer certificate chain:
#     sha256/AAAAAAA...=: CN=api.example.com
#     sha256/BBBBBBB...=: CN=Let's Encrypt Authority X3
#     sha256/CCCCCCC...=: CN=DST Root CA X3

Implementation Examples

1. OkHttp Certificate Pinning

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
import java.util.concurrent.TimeUnit

class SecureApiClient {
    private val certificatePinner = CertificatePinner.Builder()
        .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
        .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
        .build()

    private val client = OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()
}

2. Retrofit with Pinned OkHttpClient

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

class SecureRetrofitClient {
    private val certificatePinner = CertificatePinner.Builder()
        .add("api.example.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
        .add("api.example.com", "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
        .build()

    private val okHttpClient = OkHttpClient.Builder()
        .certificatePinner(certificatePinner)
        .build()

    private val retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com")
        .client(okHttpClient)  // Provide custom client with pinning
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val api: ApiService = retrofit.create(ApiService::class.java)
}

3. Network Security Configuration (Android 7.0+)

For Android 7.0 (API 24) and above, you can use Network Security Configuration:

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>
        <!-- Set expiration well before actual certificate expiry -->
        <pin-set expiration="2026-01-01">
            <!-- Pin intermediate CA public keys -->
            <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
            <!-- Backup pin for rotation -->
            <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

AndroidManifest.xml:

<application
    android:networkSecurityConfig="@xml/network_security_config"
    ...>
</application>

Best Practices

  1. Pin Intermediate CA Public Keys: This is the recommended approach - balance of security and operational flexibility

  2. Always Pin Multiple Certificates: Include at least two pins (current + backup) to handle certificate rotation

  3. Plan Certificate Rotation in Advance:

    • Monitor certificate expiration dates (set alerts 90 days before expiry)

    • Release app updates with new pins at least 30 days before old certificates expire

    • Keep backup pins active during transition period

  4. Set Expiration Dates: In Network Security Config, set expiration dates slightly before actual certificate expiry

  5. Monitor Pinning Failures: Implement analytics to track certificate pinning failures (may indicate attack or misconfiguration)

  6. Test Thoroughly: Test pinning in development, staging, and production environments

  7. Document Your Pins: Maintain documentation of which certificates are pinned and when they expire

  8. Consider Remote Configuration: For critical apps, consider fetching pins from a secure remote config (with initial hardcoded pins as fallback)

// Example with monitoring and error handling
private val client = OkHttpClient.Builder()
    .certificatePinner(certificatePinner)
    .eventListener(object : EventListener() {
        override fun callFailed(call: Call, ioe: IOException) {
            if (ioe is SSLPeerUnverifiedException) {
                // Log to analytics - potential MITM attempt or expired pins
                analytics.logSecurityEvent("certificate_pinning_failure",
                    mapOf(
                        "url" to call.request().url.toString(),
                        "error" to ioe.message
                    )
                )
                // Alert operations team if pinning failures exceed threshold
            }
        }
    })
    .build()

References