Android
DomainCertificatePinningFragment.java
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.fragment.app.Fragment;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import java.util.ArrayList;
import java.util.List;
public class DomainCertificatePinningFragment extends Fragment {
private final ArrayList<String> localHashes;
private final ArrayList<String> certistHashes;
private ArrayAdapter<String> localSocketResultsAdapter;
private ArrayAdapter<String> certIstResultsAdapter;
private View rootView;
private EditText editText;
private TextView doHashesMatch;
private IntentFilter localResultsFilter = new IntentFilter(
GatherCertificateHashesIntentService.ACTION_RESULT_LOCAL_SOCKET_HASHES);
private IntentFilter certIstResultsFilter = new IntentFilter(
GatherCertificateHashesIntentService.ACTION_RESULT_CERTIST_HASHES);
public DomainCertificatePinningFragment() {
this.localHashes = new ArrayList<>();
this.certistHashes = new ArrayList<>();
}
@Override
public View onCreateView(LayoutInflater inflater,
ViewGroup container,
Bundle savedInstanceState) {
rootView = inflater.inflate(R.layout.fragment_certificate_pinning, container, false);
localSocketResultsAdapter = new ArrayAdapter<>(inflater.getContext(),
R.layout.expected_hash, R.id.list_view_item_hash, localHashes);
certIstResultsAdapter = new ArrayAdapter<>(inflater.getContext(),
R.layout.expected_hash, R.id.list_view_item_hash, certistHashes);
editText = rootView.findViewById(R.id.edit_text_first);
editText.setOnKeyListener((View v, int keyCode, KeyEvent event) -> {
if ((event.getAction() == KeyEvent.ACTION_DOWN) &&
(keyCode == KeyEvent.KEYCODE_ENTER)) {
gatherCertificates();
return true;
}
return false;
});
doHashesMatch = rootView.findViewById(R.id.do_certificates_match_label);
((ListView) rootView.findViewById(R.id.local_socket_results)).setAdapter(localSocketResultsAdapter);
((ListView) rootView.findViewById(R.id.certist_results)).setAdapter(certIstResultsAdapter);
rootView.findViewById(R.id.button_first).setOnClickListener((v) -> gatherCertificates());
return rootView;
}
private void gatherCertificates() {
LocalBroadcastManager manager = LocalBroadcastManager.getInstance(rootView.getContext());
manager.registerReceiver(getResultsReceiver(manager,
localSocketResultsAdapter, R.id.local_socket_results_label), localResultsFilter);
manager.registerReceiver(getResultsReceiver(manager,
certIstResultsAdapter, R.id.certist_results_label), certIstResultsFilter);
String domain = editText.getText().toString();
GatherCertificateHashesIntentService.getCertistApiHashesForDomain(rootView.getContext(), domain);
GatherCertificateHashesIntentService.getLocalSocketHashesForDomain(rootView.getContext(), domain);
}
private BroadcastReceiver getResultsReceiver(
final LocalBroadcastManager manager,
final ArrayAdapter<String> adapter,
final int label) {
return new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
manager.unregisterReceiver(this);
adapter.clear();
Bundle extras = intent.getExtras();
if (extras != null) {
adapter.addAll(extras.getStringArray(GatherCertificateHashesIntentService.RESULT_SHA256_HASHES));
}
adapter.notifyDataSetChanged();
allFound();
rootView.findViewById(label).setVisibility(View.VISIBLE);
}
};
}
private List<String> grabHashesFromAdapter(ArrayAdapter<String> adapter) {
List<String> l = new ArrayList<>();
for (int i = 0; i < adapter.getCount(); i++) {
l.add(adapter.getItem(i));
}
return l;
}
private void allFound() {
doHashesMatch.setVisibility(View.VISIBLE);
List<String> certist = grabHashesFromAdapter(certIstResultsAdapter);
List<String> local = grabHashesFromAdapter(localSocketResultsAdapter);
if (certist.equals(local)) {
doHashesMatch.setTextColor(getResources().getColor(android.R.color.holo_green_dark, null));
doHashesMatch.setText(R.string.certificate_hashes_match_label);
} else {
doHashesMatch.setTextColor(Color.RED);
doHashesMatch.setText(R.string.certificate_hashes_donot_match_label);
}
}
}
GatherCertificateHashesIntentService.java
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.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.URL;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateEncodingException;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLSocketFactory;
public class GatherCertificateHashesIntentService extends IntentService {
public static final String TAG = GatherCertificateHashesIntentService.class.getCanonicalName();
public static final String ACTION_GET_CERTIST_HASHES = "dev.jons.example.pinning.android.action.GET_CERTIST_HASHES";
public static final String ACTION_GET_LOCAL_SOCKET_HASHES = "dev.jons.example.pinning.android.action.GET_LOCAL_SOCKET_HASHES";
public static final String ACTION_RESULT_LOCAL_SOCKET_HASHES = "dev.jons.example.pinning.android.action.RESULT_LOCAL_SOCKET_HASHES";
public static final String ACTION_RESULT_CERTIST_HASHES = "dev.jons.example.pinning.android.action.RESULT_CERTIST_HASHES";
public static final String EXTRAS_HOST_NAME = "dev.jons.example.pinning.android.extra.HOST_NAME";
public static final String RESULT_SHA256_HASHES = "dev.jons.example.pinning.android.result.RESULT_SHA256_HASHES";
public GatherCertificateHashesIntentService() {
super("RetrieveCertIstApiResults");
}
public static void getCertistApiHashesForDomain(Context context, String domain) {
Intent intent = new Intent(context, GatherCertificateHashesIntentService.class);
intent.setAction(ACTION_GET_CERTIST_HASHES);
intent.putExtra(EXTRAS_HOST_NAME, domain);
context.startService(intent);
}
public static void getLocalSocketHashesForDomain(Context context, String domain) {
Intent intent = new Intent(context, GatherCertificateHashesIntentService.class);
intent.setAction(ACTION_GET_LOCAL_SOCKET_HASHES);
intent.putExtra(EXTRAS_HOST_NAME, domain);
context.startService(intent);
}
@Override
protected void onHandleIntent(Intent intent) {
if (intent != null) {
final String action = intent.getAction();
final String hostName = intent.getStringExtra(EXTRAS_HOST_NAME);
if (hostName != null) {
if (ACTION_GET_CERTIST_HASHES.equals(action)) {
getCertificateHashesFromCertIstApi(hostName);
} else if (ACTION_GET_LOCAL_SOCKET_HASHES.equals(action)) {
getCertificateHashesFromLocalSocket(hostName);
} else {
throw new UnsupportedOperationException(String.format("ERROR UNKNOWN ACTION: %s", action));
}
}
}
}
private void getCertificateHashesFromCertIstApi(String hostName) {
try {
String format = String.format("https://api.cert.ist/%s", hostName);
HttpsURLConnection urlConnection = (HttpsURLConnection) new URL(format).openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.setReadTimeout(10 * 1000);
urlConnection.setConnectTimeout(10 * 1000);
urlConnection.setSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault());
urlConnection.connect();
StringBuilder total = new StringBuilder();
try (InputStream stream = urlConnection.getInputStream();
BufferedReader reader = new BufferedReader(new InputStreamReader(stream))) {
for (String line; (line = reader.readLine()) != null; ) {
total.append(line).append('\n');
}
}
JSONObject root = new JSONObject(total.toString());
JSONArray chain = root.getJSONArray("chain");
String[] sha256Hashes = new String[chain.length()];
for (int i = 0; i < chain.length(); i++) {
JSONObject certificateInChain = chain.getJSONObject(i);
JSONObject der = certificateInChain.getJSONObject("der");
JSONObject hashes = der.getJSONObject("hashes");
sha256Hashes[i] = hashes.getString("sha256");
}
Intent data = new Intent(ACTION_RESULT_CERTIST_HASHES);
data.putExtra(RESULT_SHA256_HASHES, sha256Hashes);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(data);
} catch (IOException | JSONException e) {
e.printStackTrace();
}
}
private void getCertificateHashesFromLocalSocket(String hostName) {
try {
final URL url = new URL(String.format("https://%s", hostName));
final HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
urlConnection.setRequestMethod("GET");
urlConnection.setReadTimeout(10 * 1000);
urlConnection.setConnectTimeout(10 * 1000);
urlConnection.setSSLSocketFactory((SSLSocketFactory) SSLSocketFactory.getDefault());
urlConnection.connect();
final Certificate[] serverCertificates = urlConnection.getServerCertificates();
final String[] hashes = new String[serverCertificates.length];
for (int i = 0; i < serverCertificates.length; i++) {
Certificate certificate = serverCertificates[i];
final byte[] encoded = certificate.getEncoded();
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(encoded);
StringBuilder hex = new StringBuilder(hash.length * 2);
for (byte b : hash)
hex.append(String.format("%02x", b & 0xFF));
hashes[i] = hex.toString();
}
Intent data = new Intent(ACTION_RESULT_LOCAL_SOCKET_HASHES);
data.putExtra(RESULT_SHA256_HASHES, hashes);
LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(data);
} catch (IOException | CertificateEncodingException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
}
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