Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@LeandroSQ
Last active October 21, 2021 12:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save LeandroSQ/1ece9d988efe6dec4883842c14316cbc to your computer and use it in GitHub Desktop.
Save LeandroSQ/1ece9d988efe6dec4883842c14316cbc to your computer and use it in GitHub Desktop.
Kotlin - SSL Pinning by certificate SHA1 fingerprint - Retrofit OkHttp3
package quevedo.soares.leandro.ssl
import okhttp3.Connection
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.security.MessageDigest
import java.security.cert.Certificate
/**
* This class handles the ssl pinning of given certificates before each request made by the application
*
* @important Must be used as NetworkInterceptor instead of a simple Interceptor
**/
class SSLPinningInterceptor : Interceptor {
// For caching SHA1 hashes between requests
private var cache: HashMap<Certificate, String> = hashMapOf()
// Specify which certificates to accept
private var knownCertificates = arrayListOf(
"XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX:XX"
)
/**
* Generate a sha1 hash digest from the certificate DER binary
**/
private fun generateFingerprintFromCertificate(certificate: Certificate): String {
if (cache.containsKey(certificate)) return cache[certificate]!!
// Digest the certificate DER binary to sha1
val fingerprint = MessageDigest.getInstance("SHA-1")
.digest(certificate.encoded)
// Format it into a hex string separated by ":"
.joinToString(separator = ":") { eachByte ->
"%02x".format(eachByte).uppercase()
}
// Updates the cache
cache[certificate] = fingerprint
return fingerprint
}
/**
* Extract the fingerprint of each certificate in the peer chain of the socket connection
**/
private fun getCertificatePeerChainFingerprints(connection: Connection?): List<String> {
return connection?.handshake()?.peerCertificates?.map {
generateFingerprintFromCertificate(it)
} ?: listOf()
}
override fun intercept(chain: Interceptor.Chain): Response {
// Ignores SSL Pinning on HTTP, only for HTTPS
if (!chain.request().isHttps) return chain.proceed(chain.request())
// Only check the certificate peer chain when the connection is available
chain.connection()?.let { connection ->
// Extract the fingerprints from the certificate peer chain
val fingerprints = getCertificatePeerChainFingerprints(connection)
// Check if any of the fingerprints are known to the application
val isKnownCertificate = knownCertificates.any { fingerprints.contains(it) }
if (!isKnownCertificate) {
// Network interceptors are required to call chain.proceed
chain.proceed(chain.request())
// Returns an empty response, interrupting
return Response.Builder()
.body("".toResponseBody("application/json".toMediaTypeOrNull()))
.code(502)
.message("Invalid certificate peer chain")
.protocol(Protocol.HTTP_2)
.request(chain.request())
.build()
}
}
return chain.proceed(chain.request()) // Continue with the request
}
}
@LeandroSQ
Copy link
Author

LeandroSQ commented Oct 21, 2021

Usage:

// At your OkHttpClient building add
client.addNetworkInterceptor(SSLPinningInterceptor())

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment