Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

Add support for non-verifiable SSL certificates

This includes expired certificates, self-signed certificates, and pretty
much everything else. White-listing is done based on the SHA1 hash.
  • Loading branch information...
commit aab4ba093dc6117cda3f7e81f097afaa736cd4cc 1 parent 24c8008
@mhagander authored
View
64 res/layout/sslcertdialogprefview.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical" >
+
+ <TextView
+ android:id="@+id/textView1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="SHA-1 fingerprint of SSL certificate to unconditionally accept" />
+
+ <EditText
+ android:id="@+id/editCertFingerprint"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:inputType="textCapCharacters"
+ >
+
+ <requestFocus />
+ </EditText>
+ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="horizontal" >
+ <Button
+ android:id="@+id/buttonGetFromServer"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:enabled="false"
+ android:text="Copy from info below" />
+ <Button
+ android:id="@+id/buttonClear"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:text="Clear" />
+ </LinearLayout>
+
+ <ProgressBar
+ android:id="@+id/progressBar1"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" />
+ <TextView
+ android:id="@+id/textViewCertError"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:visibility="gone"
+ android:text="" />
+
+ <HorizontalScrollView
+ android:id="@+id/scrollView1"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:scrollbars="horizontal"
+ android:fadeScrollbars="false"
+ >
+ <TableLayout
+ android:id="@+id/tableLayoutCertDetails"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content" >
+ </TableLayout>
+ </HorizontalScrollView>
+
+</LinearLayout>
View
16 src/net/hagander/mailinglistmoderator/ServerEditor.java
@@ -20,6 +20,7 @@
import org.xmlpull.v1.XmlSerializer;
import net.hagander.mailinglistmoderator.backend.ListServer;
+import net.hagander.mailinglistmoderator.preferences.SSLCertDialogPreference;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.SharedPreferences;
@@ -90,6 +91,7 @@ public boolean onContextItemSelected(MenuItem item) {
editor.remove(name + "_baseurl");
editor.remove(name + "_password");
editor.remove(name + "_overridecertname");
+ editor.remove(name + "_whitelistedcert");
editor.commit();
for (ListServer s: MailinglistModerator.servers){
@@ -116,6 +118,7 @@ public void onClick(DialogInterface dialog, int which) {
editor.putString(newname + "_baseurl", prefs.getString(name + "_baseurl", ""));
editor.putString(newname + "_password", prefs.getString(name + "_password", ""));
editor.putString(newname + "_overridecertname", prefs.getString(name + "_overridecertname", ""));
+ editor.putString(newname + "_whitelistedcert", prefs.getString(name + "_whitelistedcert", ""));
editor.commit();
MailinglistModerator.servers.add(ListServer.CreateFromPreference(prefs, newname));
@@ -176,7 +179,17 @@ private PreferenceScreen getOneServerSet(String name) {
e_certnameoverride.getEditText().setInputType(InputType.TYPE_TEXT_VARIATION_NORMAL);
e_certnameoverride.setTitle("Non-standard SSL hostname");
e_certnameoverride.setDialogTitle("Accept non-standard SSL certificate hostname");
+ e_certnameoverride.setSummary(prefs.getString(name+"_overridecertname",""));
screen.addPreference(e_certnameoverride);
+
+ /* Create specific preference to deal with whitelisted certs */
+ SSLCertDialogPreference e_whitelistcert = new SSLCertDialogPreference(this);
+ e_whitelistcert.setKey(name + "_whitelistedcert");
+ e_whitelistcert.setTitle("Accept invalid certificate");
+ e_whitelistcert.setDialogTitle("Accept non-validating SSL certificate");
+ e_whitelistcert.setSummary(prefs.getString(name+"_whitelistedcert",""));
+ screen.addPreference(e_whitelistcert);
+
return screen;
}
@@ -212,6 +225,7 @@ public void onClick(DialogInterface dialog, int which) {
editor.putString(name + "_baseurl", "");
editor.putString(name + "_password", "");
editor.putString(name + "_overridecertname", "");
+ editor.putString(name + "_whitelistedcert", "");
editor.commit();
MailinglistModerator.servers.add(ListServer.CreateFromPreference(prefs, name));
@@ -310,6 +324,7 @@ public void onClick(DialogInterface dialog, int which) {
String baseurl = node.getAttribute("url");
String password = node.getAttribute("password");
String overridecertname = node.getAttribute("overridecertname");
+ String whitelistedcert = node.getAttribute("whitelistedcert");
/* Find out if this node already exists */
boolean doesexist = false;
@@ -341,6 +356,7 @@ public void onClick(DialogInterface dialog, int which) {
editor.putString(name + "_baseurl", baseurl);
editor.putString(name + "_password", password);
editor.putString(name + "_overridecertname", overridecertname);
+ editor.putString(name + "_whitelistedcert", whitelistedcert);
editor.commit();
if (!doesexist)
View
72 src/net/hagander/mailinglistmoderator/backend/ListServer.java
@@ -11,9 +11,14 @@
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringWriter;
+import java.math.BigInteger;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
+import java.security.KeyManagementException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Vector;
import java.util.regex.Matcher;
@@ -21,8 +26,10 @@
import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
+import javax.net.ssl.X509TrustManager;
import org.xmlpull.v1.XmlSerializer;
@@ -44,6 +51,7 @@
protected String rooturl;
protected String password;
protected String override_certname;
+ protected String whitelisted_cert;
protected boolean populated;
protected boolean exceptioned;
@@ -60,11 +68,12 @@
* @param password
* Password to access the server (both read and write)
*/
- public ListServer(String name, String rooturl, String password, String override_certname) {
+ public ListServer(String name, String rooturl, String password, String override_certname, String whitelisted_cert) {
this.listname = name;
this.rooturl = rooturl;
this.password = password;
this.override_certname = override_certname;
+ this.whitelisted_cert = whitelisted_cert;
this.populated = false;
this.exceptioned = false;
@@ -83,15 +92,17 @@ public ListServer(String name, String rooturl, String password, String override_
* Password to access the server
* @param override_certname
* SSL certificate name to accept
+ * @param whitelisted_cert
+ * SSL certificate fingerprint to whitelist
* @return A ListServer instance representing this server.
*/
- public static ListServer Create(String name, String rooturl, String password, String override_certname) {
+ public static ListServer Create(String name, String rooturl, String password, String override_certname, String whitelisted_cert) {
if (rooturl.startsWith("dummy:"))
return new Dummy(name, rooturl, password);
if (rooturl.contains("/admindb"))
- return new Mailman(name, rooturl, password, override_certname);
+ return new Mailman(name, rooturl, password, override_certname, whitelisted_cert);
if (rooturl.contains("mj_wwwadm"))
- return new Majordomo2(name, rooturl, password, override_certname);
+ return new Majordomo2(name, rooturl, password, override_certname, whitelisted_cert);
return new Unconfigured(name, rooturl, password);
}
@@ -110,8 +121,9 @@ public static ListServer CreateFromPreference(SharedPreferences pref,
String baseurl = pref.getString(name + "_baseurl", "");
String password = pref.getString(name + "_password", "");
String override_certname = pref.getString(name+"_overridecertname", "");
+ String whitelisted_cert = pref.getString(name+"_whitelistedcert", "");
- return Create(name, baseurl, password, override_certname);
+ return Create(name, baseurl, password, override_certname, whitelisted_cert);
}
/*
@@ -288,6 +300,54 @@ public boolean verify(String hostname, SSLSession session) {
}
});
}
+
+ /* Let's see if we should also check the actual certificate */
+ if (whitelisted_cert != null && !whitelisted_cert.equals("")) {
+ SSLContext context;
+ try {
+ context = SSLContext.getInstance("TLS");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Could not find TLS context!");
+ }
+ try {
+ context.init(null,
+ new X509TrustManager[] { new X509TrustManager() {
+ public void checkClientTrusted(
+ X509Certificate[] chain, String authType)
+ throws CertificateException {
+ }
+
+ public void checkServerTrusted(
+ X509Certificate[] chain, String authType)
+ throws CertificateException {
+
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ throw new RuntimeException("Could not find SHA-1 digest");
+ }
+ md.update(chain[0].getEncoded());
+ byte[] digest = md.digest();
+ BigInteger bi = new BigInteger(1, digest);
+ String fingerprint = String.format("%0" + (digest.length << 1) + "X", bi);
+
+ if (!fingerprint.equals(whitelisted_cert)) {
+ throw new CertificateException("Certificate fingerprint does not match the configured one!");
+ }
+
+ /* Otherwise, if it matches, we said override, so trust everything */
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ } }, null);
+ } catch (KeyManagementException e) {
+ throw new RuntimeException(String.format("Unable to set up key management: %s", e.toString()));
+ }
+ sslconn.setSSLSocketFactory(context.getSocketFactory());
+ }
}
InputStreamReader isr = new InputStreamReader(c.getInputStream());
BufferedReader r = new BufferedReader(isr);
@@ -314,6 +374,8 @@ public void writeXmlElement(XmlSerializer xml) throws IOException {
xml.attribute(null, "password", password);
if (override_certname != null && !override_certname.equals(""))
xml.attribute(null, "overridecertname", override_certname);
+ if (whitelisted_cert != null && !whitelisted_cert.equals(""))
+ xml.attribute(null, "whitelistedcert", whitelisted_cert);
xml.endTag(null, "list");
}
}
View
2  src/net/hagander/mailinglistmoderator/backend/providers/Dummy.java
@@ -20,7 +20,7 @@
*/
public class Dummy extends ListServer {
public Dummy(String name, String rooturl, String password) {
- super(name, rooturl, password, null);
+ super(name, rooturl, password, null, null);
}
/**
View
4 src/net/hagander/mailinglistmoderator/backend/providers/Mailman.java
@@ -22,8 +22,8 @@
*
*/
public class Mailman extends ListServer {
- public Mailman(String name, String rooturl, String password, String override_certname) {
- super(name, rooturl, password, override_certname);
+ public Mailman(String name, String rooturl, String password, String override_certname, String whitelisted_cert) {
+ super(name, rooturl, password, override_certname, whitelisted_cert);
}
/*
View
4 src/net/hagander/mailinglistmoderator/backend/providers/Majordomo2.java
@@ -21,8 +21,8 @@
*
*/
public class Majordomo2 extends ListServer {
- public Majordomo2(String name, String rooturl, String password, String override_certname) {
- super(name, rooturl, password, override_certname);
+ public Majordomo2(String name, String rooturl, String password, String override_certname, String whitelisted_cert) {
+ super(name, rooturl, password, override_certname, whitelisted_cert);
}
/*
View
2  src/net/hagander/mailinglistmoderator/backend/providers/Unconfigured.java
@@ -22,7 +22,7 @@
public class Unconfigured extends ListServer {
public Unconfigured(String name, String rooturl, String password) {
- super(name, rooturl, password, null);
+ super(name, rooturl, password, null, null);
}
@Override
View
234 src/net/hagander/mailinglistmoderator/preferences/SSLCertDialogPreference.java
@@ -0,0 +1,234 @@
+/*
+ * SSLCertDialogPreference.java - This class holds a dialog preference for polling SSL certs
+ *
+ * Copyright (C) 2010 Magnus Hagander <magnus@hagander.net>
+ *
+ * This software is released under the BSD license.
+ */
+package net.hagander.mailinglistmoderator.preferences;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.net.URL;
+import java.net.URLConnection;
+import java.security.KeyManagementException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+
+import javax.net.ssl.HostnameVerifier;
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSession;
+import javax.net.ssl.X509TrustManager;
+
+import net.hagander.mailinglistmoderator.R;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.DialogPreference;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.widget.Button;
+import android.widget.EditText;
+import android.widget.ProgressBar;
+import android.widget.TableLayout;
+import android.widget.TableRow;
+import android.widget.TextView;
+
+public class SSLCertDialogPreference extends DialogPreference implements OnClickListener {
+ private class StringTuple {
+ String description;
+ String value;
+ public StringTuple(String description, String value) {
+ this.description = description;
+ this.value = value;
+ }
+ }
+
+ private String sslCertError = null;
+ private String sslCertFingerprint = null;
+ private ArrayList<StringTuple> sslCertDetails = null;
+
+ private EditText fingerprintField;
+ private ProgressBar progressSpinner;
+ private TableLayout certDetailsTableLayout;
+ private TextView certErrorText;
+ private Button copyButton;
+
+ public SSLCertDialogPreference(Context context) {
+ super(context, null);
+
+ setDialogLayoutResource(R.layout.sslcertdialogprefview);
+ }
+
+ @Override
+ protected void onBindDialogView(final View view) {
+ fingerprintField = (EditText) view.findViewById(R.id.editCertFingerprint);
+ progressSpinner = (ProgressBar) view.findViewById(R.id.progressBar1);
+ certErrorText = (TextView) view.findViewById(R.id.textViewCertError);
+ certDetailsTableLayout = (TableLayout) view.findViewById(R.id.tableLayoutCertDetails);
+ copyButton = (Button) view.findViewById(R.id.buttonGetFromServer);
+ copyButton.setOnClickListener(this);
+ ((Button) view.findViewById(R.id.buttonClear)).setOnClickListener(this);
+
+
+ SharedPreferences pref = getSharedPreferences();
+ fingerprintField.setText(pref.getString(getKey(), ""));
+
+ super.onBindDialogView(view);
+
+ new Thread(new Runnable() {
+ public void run() {
+ FetchSSLCertDescription();
+ view.post(new Runnable() {
+ public void run() {
+ progressSpinner.setVisibility(View.GONE);
+ if (sslCertError != null) {
+ certErrorText.setText(sslCertError);
+ certErrorText.setVisibility(View.VISIBLE);
+ return;
+ }
+ for (StringTuple r : sslCertDetails) {
+ TableRow tr = new TableRow(certDetailsTableLayout.getContext());
+ tr.setLayoutParams(new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ TextView tv_d = new TextView(certDetailsTableLayout.getContext());
+ tv_d.setText(r.description + ":");
+ TextView tv_v = new TextView(certDetailsTableLayout.getContext());
+ tv_v.setText(r.value);
+ tr.addView(tv_d);
+ tr.addView(tv_v);
+ certDetailsTableLayout.addView(tr, new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT));
+ }
+ if (sslCertFingerprint != null) {
+ copyButton.setEnabled(true);
+ }
+ }
+ });
+ }
+ }).start();
+ }
+
+ @Override
+ protected void onDialogClosed(boolean positiveResult) {
+ if(!positiveResult)
+ return;
+
+ SharedPreferences.Editor editor = getEditor();
+ editor.putString(getKey(), fingerprintField.getText().toString());
+ editor.commit();
+ setSummary(fingerprintField.getText());
+ }
+
+ public void onClick(View view) {
+ String btn = ((Button)view).getText().toString();
+ if (btn.equals("Copy from info below")) {
+ if (sslCertFingerprint != null)
+ fingerprintField.setText(sslCertFingerprint);
+ }
+ else if (btn.equals("Clear")) {
+ fingerprintField.setText("");
+ }
+ }
+
+ private void FetchSSLCertDescription() {
+ /*
+ * Fetch the description and the fingerprint for the currently configured
+ * BASE URL.
+ */
+
+ /*
+ * We need to read the other preference, by stripping it out from our own key.
+ * Do that by removing the "
+ */
+ SharedPreferences pref = getSharedPreferences();
+ String baseurl = pref.getString(getKey().replace("_whitelistedcert", "_baseurl"), "FAIL");
+ if (baseurl.equals("")) {
+ sslCertError = "Base URL must be set before this preference can be fetched.";
+ return;
+ }
+
+ URLConnection c;
+ try {
+ final URL u = new URL(baseurl);
+ c = u.openConnection();
+ }
+ catch (Exception e) {
+ sslCertError = "Failed to parse or open URL!";
+ return;
+ }
+
+ SSLContext context;
+ try {
+ context = SSLContext.getInstance("TLS");
+ } catch (NoSuchAlgorithmException e) {
+ sslCertError = "Could not find TLS context!";
+ return;
+ }
+ HttpsURLConnection sslconn = (HttpsURLConnection) c;
+ try {
+ context.init(null,
+ new X509TrustManager[] { new X509TrustManager() {
+ public void checkClientTrusted(
+ X509Certificate[] chain, String authType)
+ throws CertificateException {
+ }
+
+ public void checkServerTrusted(
+ X509Certificate[] chain, String authType)
+ throws CertificateException {
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance("SHA-1");
+ } catch (NoSuchAlgorithmException e) {
+ sslCertError = "Could not find SHA-1 digest";
+ throw new CertificateException();
+ }
+ md.update(chain[0].getEncoded());
+ byte[] digest = md.digest();
+ BigInteger bi = new BigInteger(1, digest);
+ String fingerprint = String.format("%0" + (digest.length << 1) + "X", bi);
+
+ ArrayList<StringTuple> r = new ArrayList<StringTuple>();
+ r.add(new StringTuple("Subject", chain[0].getSubjectDN().toString()));
+ r.add(new StringTuple("Valid from", chain[0].getNotBefore().toLocaleString()));
+ r.add(new StringTuple("Valid to", chain[0].getNotAfter().toLocaleString()));
+ r.add(new StringTuple("Fingerprint", fingerprint));
+ r.add(new StringTuple("Chain length", String.format("%d",chain.length)));
+
+ sslCertDetails = r;
+ sslCertFingerprint = fingerprint;
+ }
+
+ public X509Certificate[] getAcceptedIssuers() {
+ return new X509Certificate[0];
+ }
+ } }, null);
+ } catch (KeyManagementException e) {
+ sslCertError = String.format("Unable to set up key management: %s", e.toString());
+ return;
+ }
+ sslconn.setSSLSocketFactory(context.getSocketFactory());
+
+ /*
+ * We must also turn off hostname verification so we don't get an exception
+ * when connecting.
+ */
+ sslconn.setHostnameVerifier(new HostnameVerifier() {
+ public boolean verify(String hostname, SSLSession session) {
+ return true;
+ }
+ });
+
+ try {
+ sslconn.connect(); /* This should trigger our callback */
+ sslconn.disconnect();
+ }
+ catch (IOException e) {
+ sslCertError = String.format("Unable to connect to server: %s", e.toString());
+ }
+ }
+}
Please sign in to comment.
Something went wrong with that request. Please try again.