🔒 SSL Pinning

SSL Pinning chroni przed atakami man-in-the-middle przez weryfikację certyfikatu serwera.

Czym jest SSL Pinning?

📝 Wyjaśnienie

Standardowo Android ufa wszystkim certyfikatom z systemu CA. SSL Pinning wymusza, że aplikacja akceptuje tylko konkretny certyfikat lub klucz publiczny serwera. Chroni to przed:

  • Atakami MITM
  • Fałszywymi certyfikatami CA
  • Proxy debugującymi (np. Charles)

Certificate Pinning

import rip.nerd.kitsunenet.security.KNETSSLPinning

val pinning = KNETSSLPinning.certificate(
    hostname = "api.example.com",
    certificatePem = """
        -----BEGIN CERTIFICATE-----
        MIIDXTCCAkWgAwIBAgIJAJC1HiIAZAiU...
        -----END CERTIFICATE-----
    """.trimIndent()
)

val client = KNETClient.builder()
    .sslPinning(pinning)
    .build()

Public Key Pinning (zalecane)

// SHA-256 hash klucza publicznego
val pinning = KNETSSLPinning.publicKey(
    hostname = "api.example.com",
    pins = listOf(
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="  // Backup
    )
)

val client = KNETClient.builder()
    .sslPinning(pinning)
    .build()

Jak uzyskać pin?

# Za pomocą openssl
openssl s_client -connect api.example.com:443 | \
    openssl x509 -pubkey -noout | \
    openssl pkey -pubin -outform der | \
    openssl dgst -sha256 -binary | \
    base64

Wiele hostów

val pinning = KNETSSLPinning.builder()
    .add("api.example.com",
        "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
    .add("cdn.example.com",
        "sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
    .add("*.example.com",  // Wildcard
        "sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=")
    .build()

Z certyfikatem z raw resources

// res/raw/api_cert.cer
val pinning = KNETSSLPinning.fromResource(
    context = context,
    hostname = "api.example.com",
    resourceId = R.raw.api_cert
)

val client = KNETClient.builder()
    .sslPinning(pinning)
    .build()

Network Security Config (Android)

Alternatywnie użyj konfiguracji XML (Android 7+):

res/xml/network_security_config.xml

<network-security-config>
    <domain-config cleartextTrafficPermitted="false">
        <domain includeSubdomains="true">api.example.com</domain>
        <pin-set expiration="2025-01-01">
            <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
            <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
        </pin-set>
    </domain-config>
</network-security-config>

AndroidManifest.xml

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

Obsługa błędów pinning

try {
    val response = client.get("https://api.example.com/data")
} catch (e: SSLPeerUnverifiedException) {
    // Certyfikat nie pasuje do pinu
    Log.e("SSL", "Certificate pinning failed!")
    showSecurityWarning()
} catch (e: SSLHandshakeException) {
    // Ogólny błąd SSL
    Log.e("SSL", "SSL handshake failed: ${e.message}")
}

Debug mode

⚠️ Tylko dla developmentu!

Wyłączenie pinning w debug pozwala używać proxy jak Charles.

val pinning = if (BuildConfig.DEBUG) {
    KNETSSLPinning.disabled()  // Brak pinning w debug
} else {
    KNETSSLPinning.publicKey(
        hostname = "api.example.com",
        pins = listOf("sha256/...")
    )
}

val client = KNETClient.builder()
    .sslPinning(pinning)
    .build()

Certificate Transparency

// Dodatkowa weryfikacja z Certificate Transparency
val pinning = KNETSSLPinning.builder()
    .add("api.example.com", "sha256/...")
    .requireCertificateTransparency(true)
    .build()

Rotacja certyfikatów

// Zawsze dodawaj backup pin dla rotacji
val pinning = KNETSSLPinning.publicKey(
    hostname = "api.example.com",
    pins = listOf(
        "sha256/CurrentCertPinHere...",  // Aktualny
        "sha256/NextCertPinHere...",     // Następny (backup)
        "sha256/RootCAPinHere..."        // Root CA (emergency)
    )
)

// Pin root CA jako ostateczny backup
// zapobiega lockout przy rotacji

Przykład: Pełna konfiguracja

class SecureApiClient(context: Context) {

    private val sslPinning = if (BuildConfig.DEBUG) {
        KNETSSLPinning.disabled()
    } else {
        KNETSSLPinning.builder()
            .add("api.example.com",
                "sha256/CurrentPin...",
                "sha256/BackupPin...")
            .add("cdn.example.com",
                "sha256/CDNPin...")
            .requireCertificateTransparency(true)
            .build()
    }

    val client = KNETClient.builder()
        .sslPinning(sslPinning)
        .addInterceptor(KNETLoggingInterceptor())
        .build()

    suspend fun secureRequest(path: String): KNETResponse {
        return try {
            client.get("https://api.example.com$path")
        } catch (e: SSLException) {
            // Log security incident
            SecurityLogger.logPinningFailure(e)
            throw SecurityException("Connection not secure", e)
        }
    }
}

📚 Zobacz też