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