Android
DomainCertificatePinningFragment.kt
package dev.jons.example.pinning.android
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Color
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.EditText
import android.widget.ListView
import android.widget.TextView
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.Fragment
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import dev.jons.example.pinning.android.GatherCertificateHashesIntentService.Companion.getCertistApiHashesForDomain
import dev.jons.example.pinning.android.GatherCertificateHashesIntentService.Companion.getLocalSocketHashesForDomain
import java.util.*
class DomainCertificatePinningFragment : Fragment() {
private val localHashes: ArrayList<String?> = ArrayList()
private val certistHashes: ArrayList<String?> = ArrayList()
private var localSocketResultsAdapter: ArrayAdapter<String?>? = null
private var certIstResultsAdapter: ArrayAdapter<String?>? = null
private var rootView: View? = null
private var editText: EditText? = null
private var doHashesMatch: TextView? = null
private val localResultsFilter = IntentFilter(
GatherCertificateHashesIntentService.ACTION_RESULT_LOCAL_SOCKET_HASHES)
private val certIstResultsFilter = IntentFilter(
GatherCertificateHashesIntentService.ACTION_RESULT_CERTIST_HASHES)
override fun onCreateView(inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?): View? {
this.rootView = inflater.inflate(R.layout.fragment_certificate_pinning, container, false)
localSocketResultsAdapter = ArrayAdapter(inflater.context,
R.layout.expected_hash, R.id.list_view_item_hash, localHashes)
certIstResultsAdapter = ArrayAdapter(inflater.context,
R.layout.expected_hash, R.id.list_view_item_hash, certistHashes)
this.editText = this.rootView?.findViewById(R.id.edit_text_first)
this.editText?.setOnKeyListener { _: View?, keyCode: Int, event: KeyEvent ->
if (event.action == KeyEvent.ACTION_DOWN &&
keyCode == KeyEvent.KEYCODE_ENTER) {
gatherCertificates()
return@setOnKeyListener true
}
return@setOnKeyListener false
}
this.editText?.doOnTextChanged { text, start, before, count ->
}
doHashesMatch = this.rootView?.findViewById(R.id.do_certificates_match_label)
(this.rootView?.findViewById<View>(R.id.local_socket_results) as ListView).adapter = localSocketResultsAdapter
(this.rootView?.findViewById<View>(R.id.certist_results) as ListView).adapter = certIstResultsAdapter
this.rootView?.findViewById<View>(R.id.button_first)?.setOnClickListener { _: View? -> gatherCertificates() }
return rootView
}
private fun gatherCertificates() {
val manager = LocalBroadcastManager.getInstance(rootView!!.context)
manager.registerReceiver(getResultsReceiver(manager,
localSocketResultsAdapter, R.id.local_socket_results_label), localResultsFilter)
manager.registerReceiver(getResultsReceiver(manager,
certIstResultsAdapter, R.id.certist_results_label), certIstResultsFilter)
val domain = editText!!.text.toString()
getCertistApiHashesForDomain(rootView!!.context, domain)
getLocalSocketHashesForDomain(rootView!!.context, domain)
}
private fun getResultsReceiver(
manager: LocalBroadcastManager,
adapter: ArrayAdapter<String?>?,
label: Int): BroadcastReceiver {
return object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
rootView!!.findViewById<View>(label).visibility = View.VISIBLE
manager.unregisterReceiver(this)
adapter!!.clear()
adapter.addAll(*intent.extras?.getStringArray(GatherCertificateHashesIntentService.RESULT_SHA256_HASHES))
adapter.notifyDataSetChanged()
allFound()
}
}
}
private fun grabHashesFromAdapter(adapter: ArrayAdapter<String?>?): List<String?> {
val l: MutableList<String?> = ArrayList()
(0 until adapter!!.count).forEach { i -> l += adapter.getItem(i) }
return l
}
private fun allFound() {
doHashesMatch!!.visibility = View.VISIBLE
val certist = grabHashesFromAdapter(certIstResultsAdapter)
val local = grabHashesFromAdapter(localSocketResultsAdapter)
val matchPair = Pair(resources.getColor(android.R.color.holo_green_dark, null),
R.string.certificate_hashes_match_label)
val noMatchPair = Pair(Color.RED, R.string.certificate_hashes_donot_match_label)
doHashesMatch!!.setTextColor(if (certist == local) matchPair.first else noMatchPair.first)
doHashesMatch!!.setText(if (certist == local) matchPair.second else noMatchPair.second)
}
}
GatherCertificateHashesIntentService.kt
package dev.jons.example.pinning.android
import android.app.IntentService
import android.content.Context
import android.content.Intent
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import org.json.JSONException
import org.json.JSONObject
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
import java.net.URL
import java.security.MessageDigest
import java.security.NoSuchAlgorithmException
import java.security.cert.CertificateEncodingException
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLSocketFactory
class GatherCertificateHashesIntentService : IntentService("RetrieveCertIstApiResults") {
override fun onHandleIntent(intent: Intent?) {
if (intent != null) {
val action = intent.action
val hostName = intent.getStringExtra(EXTRAS_HOST_NAME)
if (hostName != null) {
when {
ACTION_GET_CERTIST_HASHES == action -> {
getCertificateHashesFromCertIstApi(hostName)
}
ACTION_GET_LOCAL_SOCKET_HASHES == action -> {
getCertificateHashesFromLocalSocket(hostName)
}
else -> {
throw UnsupportedOperationException(String.format("ERROR UNKNOWN ACTION: %s", action))
}
}
}
}
}
private fun getCertificateHashesFromCertIstApi(hostName: String) {
try {
val format = String.format("https://api.cert.ist/%s", hostName)
val urlConnection = URL(format).openConnection() as HttpsURLConnection
urlConnection.requestMethod = "GET"
urlConnection.readTimeout = 10 * 1000
urlConnection.connectTimeout = 10 * 1000
urlConnection.sslSocketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
urlConnection.connect()
val total = StringBuilder()
urlConnection.inputStream.use { stream ->
BufferedReader(InputStreamReader(stream)).use { reader ->
var line: String?
while (reader.readLine().also { line = it } != null) {
total.append(line).append('\n')
}
}
}
val root = JSONObject(total.toString())
val chain = root.getJSONArray("chain")
val sha256Hashes = arrayOfNulls<String>(chain.length())
(0 until chain.length()).forEach { i ->
val certificateInChain = chain.getJSONObject(i)
val der = certificateInChain.getJSONObject("der")
val hashes = der.getJSONObject("hashes")
sha256Hashes[i] = hashes.getString("sha256")
}
val data = Intent(ACTION_RESULT_CERTIST_HASHES)
data.putExtra(RESULT_SHA256_HASHES, sha256Hashes)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(data)
} catch (e: IOException) {
e.printStackTrace()
} catch (e: JSONException) {
e.printStackTrace()
}
}
private fun getCertificateHashesFromLocalSocket(hostName: String) {
try {
val url = URL(String.format("https://%s", hostName))
val urlConnection = url.openConnection() as HttpsURLConnection
urlConnection.requestMethod = "GET"
urlConnection.readTimeout = 10 * 1000
urlConnection.connectTimeout = 10 * 1000
urlConnection.sslSocketFactory = SSLSocketFactory.getDefault() as SSLSocketFactory
urlConnection.connect()
val hashes = arrayOfNulls<String>(urlConnection.serverCertificates.size)
urlConnection.serverCertificates.indices.forEach { i ->
val certificate = urlConnection.serverCertificates[i]
val encoded = certificate.encoded
val digest = MessageDigest.getInstance("SHA-256")
val hash = digest.digest(encoded)
val hex = StringBuilder(hash.size * 2)
for (b in hash) hex.append(String.format("%02x", b))
hashes[i] = hex.toString()
}
val data = Intent(ACTION_RESULT_LOCAL_SOCKET_HASHES)
data.putExtra(RESULT_SHA256_HASHES, hashes)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(data)
} catch (e: IOException) {
e.printStackTrace()
} catch (e: CertificateEncodingException) {
e.printStackTrace()
} catch (e: NoSuchAlgorithmException) {
e.printStackTrace()
}
}
companion object {
const val ACTION_GET_CERTIST_HASHES = "dev.jons.example.pinning.android.action.GET_CERTIST_HASHES"
const val ACTION_GET_LOCAL_SOCKET_HASHES = "dev.jons.example.pinning.android.action.GET_LOCAL_SOCKET_HASHES"
const val ACTION_RESULT_LOCAL_SOCKET_HASHES = "dev.jons.example.pinning.android.action.RESULT_LOCAL_SOCKET_HASHES"
const val ACTION_RESULT_CERTIST_HASHES = "dev.jons.example.pinning.android.action.RESULT_CERTIST_HASHES"
const val EXTRAS_HOST_NAME = "dev.jons.example.pinning.android.extra.HOST_NAME"
const val RESULT_SHA256_HASHES = "dev.jons.example.pinning.android.result.RESULT_SHA256_HASHES"
@JvmStatic
fun getCertistApiHashesForDomain(context: Context, domain: String?) {
val intent = Intent(context, GatherCertificateHashesIntentService::class.java)
intent.action = ACTION_GET_CERTIST_HASHES
intent.putExtra(EXTRAS_HOST_NAME, domain)
context.startService(intent)
}
@JvmStatic
fun getLocalSocketHashesForDomain(context: Context, domain: String?) {
val intent = Intent(context, GatherCertificateHashesIntentService::class.java)
intent.action = ACTION_GET_LOCAL_SOCKET_HASHES
intent.putExtra(EXTRAS_HOST_NAME, domain)
context.startService(intent)
}
}
}
strings.xml
<resources>
<string name="app_name">Certificate pinning</string>
<string name="next">Validate Certificates</string>
<string name="initial_domain">urip.io</string>
<string name="local_socket_results_label">Hashes as seen from local socket</string>
<string name="certist_results_label">Hashes as cert.ist sees them on the public internet</string>
<string name="certificate_hashes_match_label">Certificates from CertIst api and local sockets matched</string>
<string name="certificate_hashes_donot_match_label">Certificates from CertIst api and local sockets DO NOT matched</string>
<string name="domain_label">Domain:</string>
</resources>
Github