SSL Pinning chroni przed atakami man-in-the-middle przez weryfikację certyfikatu serwera.
Standardowo Android ufa wszystkim certyfikatom z systemu CA. SSL Pinning wymusza, że aplikacja akceptuje tylko konkretny certyfikat lub klucz publiczny serwera. Chroni to przed:
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()
// 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()
# 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
val pinning = KNETSSLPinning.builder()
.add("api.example.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")
.add("cdn.example.com",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=")
.add("*.example.com", // Wildcard
"sha256/CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC=")
.build()
// 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()
Alternatywnie użyj konfiguracji XML (Android 7+):
<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>
<application
android:networkSecurityConfig="@xml/network_security_config">
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}")
}
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()
// Dodatkowa weryfikacja z Certificate Transparency
val pinning = KNETSSLPinning.builder()
.add("api.example.com", "sha256/...")
.requireCertificateTransparency(true)
.build()
// 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
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)
}
}
}