From 6815a788a3dc37e9294b944a26fa56e74e4f88d3 Mon Sep 17 00:00:00 2001 From: Christian Schneppe Date: Sun, 18 Dec 2016 21:56:56 +0100 Subject: add support for RFC7711 to MTM --- .../de/duenndns/ssl/MemorizingTrustManager.java | 276 +++++++++++++++++---- 1 file changed, 224 insertions(+), 52 deletions(-) (limited to 'libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java') diff --git a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java index d2fc75db8..e46b6d5fb 100644 --- a/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java +++ b/libs/MemorizingTrustManager/src/de/duenndns/ssl/MemorizingTrustManager.java @@ -28,35 +28,52 @@ package de.duenndns.ssl; import android.app.Activity; import android.app.Application; -import android.app.Notification; import android.app.NotificationManager; -import android.app.PendingIntent; import android.app.Service; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.os.Handler; +import android.preference.PreferenceManager; +import android.util.Base64; +import android.util.Log; import android.util.SparseArray; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.net.URL; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.security.cert.Certificate; +import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateExpiredException; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Collection; import java.util.Enumeration; import java.util.List; import java.util.Locale; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.regex.Pattern; import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLSession; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; @@ -72,7 +89,15 @@ import javax.net.ssl.X509TrustManager; * WARNING: This only works if a dedicated thread is used for * opening sockets! */ -public class MemorizingTrustManager implements X509TrustManager { +public class MemorizingTrustManager { + + + private static final Pattern PATTERN_IPV4 = Pattern.compile("\\A(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEX4DECCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::((?:[0-9A-Fa-f]{1,4}:)*)(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_6HEX4DEC = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}:){6,6})(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\z"); + private static final Pattern PATTERN_IPV6_HEXCOMPRESSED = Pattern.compile("\\A((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)::((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)\\z"); + private static final Pattern PATTERN_IPV6 = Pattern.compile("\\A(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}\\z"); + final static String DECISION_INTENT = "de.duenndns.ssl.DECISION"; final static String DECISION_INTENT_ID = DECISION_INTENT + ".decisionId"; final static String DECISION_INTENT_CERT = DECISION_INTENT + ".cert"; @@ -98,6 +123,7 @@ public class MemorizingTrustManager implements X509TrustManager { private KeyStore appKeyStore; private X509TrustManager defaultTrustManager; private X509TrustManager appTrustManager; + private String poshCacheDir; /** * Creates an instance of the MemorizingTrustManager class that falls back to a custom TrustManager. @@ -156,30 +182,12 @@ public class MemorizingTrustManager implements X509TrustManager { File dir = app.getDir(KEYSTORE_DIR, Context.MODE_PRIVATE); keyStoreFile = new File(dir + File.separator + KEYSTORE_FILE); + poshCacheDir = app.getFilesDir().getAbsolutePath() + "/posh_cache/"; + appKeyStore = loadAppKeyStore(); } - /** - * Returns a X509TrustManager list containing a new instance of - * TrustManagerFactory. - *

- * This function is meant for convenience only. You can use it - * as follows to integrate TrustManagerFactory for HTTPS sockets: - *

- *

-     *     SSLContext sc = SSLContext.getInstance("TLS");
-     *     sc.init(null, MemorizingTrustManager.getInstanceList(this),
-     *         new java.security.SecureRandom());
-     *     HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());
-     * 
- * - * @param c Activity or Service to show the Dialog / Notification - */ - public static X509TrustManager[] getInstanceList(Context c) { - return new X509TrustManager[]{new MemorizingTrustManager(c)}; - } - /** * Binds an Activity to the MTM for displaying the query dialog. *

@@ -395,7 +403,7 @@ public class MemorizingTrustManager implements X509TrustManager { return false; } - public void checkCertTrusted(X509Certificate[] chain, String authType, boolean isServer, boolean interactive) + public void checkCertTrusted(X509Certificate[] chain, String authType, String domain, boolean isServer, boolean interactive) throws CertificateException { LOGGER.log(Level.FINE, "checkCertTrusted(" + chain + ", " + authType + ", " + isServer + ")"); try { @@ -424,6 +432,15 @@ public class MemorizingTrustManager implements X509TrustManager { else defaultTrustManager.checkClientTrusted(chain, authType); } catch (CertificateException e) { + boolean trustSystemCAs = !PreferenceManager.getDefaultSharedPreferences(master).getBoolean("dont_trust_system_cas", false); + if (domain != null && isServer && trustSystemCAs && !isIp(domain)) { + String hash = getBase64Hash(chain[0], "SHA-256"); + List fingerprints = getPoshFingerprints(domain); + if (hash != null && fingerprints.contains(hash)) { + Log.d("mtm", "trusted cert fingerprint of " + domain + " via posh"); + return; + } + } e.printStackTrace(); if (interactive) { interactCert(chain, authType, e); @@ -434,17 +451,147 @@ public class MemorizingTrustManager implements X509TrustManager { } } - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - checkCertTrusted(chain, authType, false, true); + private List getPoshFingerprints(String domain) { + List cached = getPoshFingerprintsFromCache(domain); + if (cached == null) { + return getPoshFingerprintsFromServer(domain); + } else { + return cached; + } } - public void checkServerTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - checkCertTrusted(chain, authType, true, true); + private List getPoshFingerprintsFromServer(String domain) { + return getPoshFingerprintsFromServer(domain, "https://" + domain + "/.well-known/posh/xmpp-client.json", -1, true); + } + + private List getPoshFingerprintsFromServer(String domain, String url, int maxTtl, boolean followUrl) { + Log.d("mtm", "downloading json for " + domain + " from " + url); + try { + List results = new ArrayList<>(); + HttpsURLConnection connection = (HttpsURLConnection) new URL(url).openConnection(); + connection.setConnectTimeout(5000); + connection.setReadTimeout(5000); + BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream())); + String inputLine; + StringBuilder builder = new StringBuilder(); + while ((inputLine = in.readLine()) != null) { + builder.append(inputLine); + } + JSONObject jsonObject = new JSONObject(builder.toString()); + in.close(); + int expires = jsonObject.getInt("expires"); + if (expires <= 0) { + return new ArrayList<>(); + } + if (maxTtl >= 0) { + expires = Math.min(maxTtl, expires); + } + String redirect; + try { + redirect = jsonObject.getString("url"); + } catch (JSONException e) { + redirect = null; + } + if (followUrl && redirect != null && redirect.toLowerCase().startsWith("https")) { + return getPoshFingerprintsFromServer(domain, redirect, expires, false); + } + JSONArray fingerprints = jsonObject.getJSONArray("fingerprints"); + for (int i = 0; i < fingerprints.length(); i++) { + JSONObject fingerprint = fingerprints.getJSONObject(i); + String sha256 = fingerprint.getString("sha-256"); + if (sha256 != null) { + results.add(sha256); + } + } + writeFingerprintsToCache(domain, results, 1000L * expires + System.currentTimeMillis()); + return results; + } catch (Exception e) { + Log.d("mtm", "error fetching posh " + e.getMessage()); + return new ArrayList<>(); + } + } + + private File getPoshCacheFile(String domain) { + return new File(poshCacheDir + domain + ".json"); + } + + private void writeFingerprintsToCache(String domain, List results, long expires) { + File file = getPoshCacheFile(domain); + file.getParentFile().mkdirs(); + try { + file.createNewFile(); + JSONObject jsonObject = new JSONObject(); + jsonObject.put("expires", expires); + jsonObject.put("fingerprints", new JSONArray(results)); + FileOutputStream outputStream = new FileOutputStream(file); + outputStream.write(jsonObject.toString().getBytes()); + outputStream.flush(); + outputStream.close(); + } catch (Exception e) { + e.printStackTrace(); + } } - public X509Certificate[] getAcceptedIssuers() { + private List getPoshFingerprintsFromCache(String domain) { + File file = getPoshCacheFile(domain); + try { + InputStream is = new FileInputStream(file); + BufferedReader buf = new BufferedReader(new InputStreamReader(is)); + + String line = buf.readLine(); + StringBuilder sb = new StringBuilder(); + + while (line != null) { + sb.append(line).append("\n"); + line = buf.readLine(); + } + JSONObject jsonObject = new JSONObject(sb.toString()); + is.close(); + long expires = jsonObject.getLong("expires"); + long expiresIn = expires - System.currentTimeMillis(); + if (expiresIn < 0) { + file.delete(); + return null; + } else { + Log.d("mtm", "posh fingerprints expire in " + (expiresIn / 1000) + "s"); + } + List result = new ArrayList<>(); + JSONArray jsonArray = jsonObject.getJSONArray("fingerprints"); + for (int i = 0; i < jsonArray.length(); ++i) { + result.add(jsonArray.getString(i)); + } + return result; + } catch (FileNotFoundException e) { + return null; + } catch (IOException e) { + return null; + } catch (JSONException e) { + file.delete(); + return null; + } + } + + private static boolean isIp(final String server) { + return server != null && ( + PATTERN_IPV4.matcher(server).matches() + || PATTERN_IPV6.matcher(server).matches() + || PATTERN_IPV6_6HEX4DEC.matcher(server).matches() + || PATTERN_IPV6_HEX4DECCOMPRESSED.matcher(server).matches() + || PATTERN_IPV6_HEXCOMPRESSED.matcher(server).matches()); + } + + private static String getBase64Hash(X509Certificate certificate, String digest) throws CertificateEncodingException { + MessageDigest md; + try { + md = MessageDigest.getInstance(digest); + } catch (NoSuchAlgorithmException e) { + return null; + } + md.update(certificate.getEncoded()); + return Base64.encodeToString(md.digest(), Base64.NO_WRAP); + } + + private X509Certificate[] getAcceptedIssuers() { LOGGER.log(Level.FINE, "getAcceptedIssuers()"); return defaultTrustManager.getAcceptedIssuers(); } @@ -556,21 +703,6 @@ public class MemorizingTrustManager implements X509TrustManager { return si.toString(); } - // We can use Notification.Builder once MTM's minSDK is >= 11 - @SuppressWarnings("deprecation") - void startActivityNotification(Intent intent, int decisionId, String certName) { - Notification n = new Notification(android.R.drawable.ic_lock_lock, - master.getString(R.string.mtm_notification), - System.currentTimeMillis()); - PendingIntent call = PendingIntent.getActivity(master, 0, intent, 0); - n.setLatestEventInfo(master.getApplicationContext(), - master.getString(R.string.mtm_notification), - certName, call); - n.flags |= Notification.FLAG_AUTO_CANCEL; - - notificationManager.notify(NOTIFICATION_ID + decisionId, n); - } - /** * Returns the top-most entry of the activity stack. * @@ -600,7 +732,6 @@ public class MemorizingTrustManager implements X509TrustManager { getUI().startActivity(ni); } catch (Exception e) { LOGGER.log(Level.FINE, "startActivity(MemorizingActivity)", e); - startActivityNotification(ni, myId, message); } } }); @@ -711,22 +842,39 @@ public class MemorizingTrustManager implements X509TrustManager { } + public X509TrustManager getNonInteractive(String domain) { + return new NonInteractiveMemorizingTrustManager(domain); + } + + public X509TrustManager getInteractive(String domain) { + return new InteractiveMemorizingTrustManager(domain); + } + public X509TrustManager getNonInteractive() { - return new NonInteractiveMemorizingTrustManager(); + return new NonInteractiveMemorizingTrustManager(null); + } + + public X509TrustManager getInteractive() { + return new InteractiveMemorizingTrustManager(null); } private class NonInteractiveMemorizingTrustManager implements X509TrustManager { + private final String domain; + + public NonInteractiveMemorizingTrustManager(String domain) { + this.domain = domain; + } + @Override - public void checkClientTrusted(X509Certificate[] chain, String authType) - throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, false, false); + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, false); } @Override public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { - MemorizingTrustManager.this.checkCertTrusted(chain, authType, true, false); + MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, false); } @Override @@ -735,4 +883,28 @@ public class MemorizingTrustManager implements X509TrustManager { } } + + private class InteractiveMemorizingTrustManager implements X509TrustManager { + private final String domain; + + public InteractiveMemorizingTrustManager(String domain) { + this.domain = domain; + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { + MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, false, true); + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + MemorizingTrustManager.this.checkCertTrusted(chain, authType, domain, true, true); + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + return MemorizingTrustManager.this.getAcceptedIssuers(); + } + } } -- cgit v1.2.3