package com.miniabs; import java.io.ByteArrayInputStream; import java.io.IOException; import java.security.KeyStore; import java.security.MessageDigest; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.net.ssl.HttpsURLConnection; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import javax.net.ssl.X509TrustManager; import android.content.Context; import android.util.Base64; /** * Decides which HTTPS certificates MiniABS accepts: * 1. anything the phone itself trusts; * 2. certificates chaining to the modern roots in {@link Roots} (Android 2.3's list predates Let's Encrypt); * 3. certificates the user chose to trust after checking the fingerprint ("pins"). */ final class Trust { private Trust() { } /** The last certificate chain that wasn't trusted, so the UI can offer to trust it. */ static final class Rejected { final X509Certificate[] chain; final String reason; final long at; Rejected(X509Certificate[] chain, String reason) { this.chain = chain; this.reason = reason; this.at = System.currentTimeMillis(); } boolean expired() { return reason != null && reason.startsWith("expired"); } } static volatile Rejected lastRejected; private static SSLSocketFactory factory; private static List bundled; // ------------------------------------------------------------------ setup static synchronized SSLSocketFactory factory(Context c) throws IOException { if (factory == null) { try { SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(null, new TrustManager[]{new Manager(c.getApplicationContext())}, null); factory = ctx.getSocketFactory(); } catch (Exception e) { throw new IOException("As conexións seguras non están dispoñibles: " + e.getMessage()); } } return factory; } static void apply(Context c, HttpsURLConnection conn) throws IOException { conn.setSSLSocketFactory(factory(c)); } /** Recently rejected (this attempt), or null. */ static Rejected recentRejection() { Rejected r = lastRejected; return r != null && System.currentTimeMillis() - r.at < 60000 ? r : null; } private static synchronized List bundled() { if (bundled == null) { List list = new ArrayList(); for (String b64 : Roots.DER_BASE64) { try { list.add(parse(Base64.decode(b64, Base64.DEFAULT))); } catch (Exception ignored) { // a root this phone can't parse; skip it } } bundled = list; } return bundled; } static X509Certificate parse(byte[] der) throws CertificateException { return (X509Certificate) CertificateFactory.getInstance("X.509") .generateCertificate(new ByteArrayInputStream(der)); } // ------------------------------------------------------------------ pins static List pinned(Context c) { List out = new ArrayList(); String s = Prefs.get(c).sp.getString("pins", ""); if (s.length() == 0) return out; for (String b64 : s.split(",")) { try { out.add(parse(Base64.decode(b64, Base64.DEFAULT))); } catch (Exception ignored) { } } return out; } /** Trusts the top certificate the server sent (its issuing CA, or the certificate itself if self-signed). */ static synchronized X509Certificate pin(Context c, X509Certificate[] chain) { X509Certificate top = top(chain); try { String b64 = Base64.encodeToString(top.getEncoded(), Base64.NO_WRAP); String s = Prefs.get(c).sp.getString("pins", ""); if (!Arrays.asList(s.split(",")).contains(b64)) { Prefs.get(c).sp.edit().putString("pins", s.length() == 0 ? b64 : s + "," + b64).commit(); } } catch (Exception ignored) { } factory = null; lastRejected = null; return top; } static synchronized void forgetPins(Context c) { Prefs.get(c).sp.edit().remove("pins").commit(); factory = null; } /** Walks up the chain as far as the server's own certificates go. */ static X509Certificate top(X509Certificate[] chain) { X509Certificate cur = chain[0]; for (int step = 0; step < 8; step++) { if (cur.getIssuerX500Principal().equals(cur.getSubjectX500Principal())) return cur; // self-signed X509Certificate next = issuerIn(cur, chain); if (next == null) return cur; cur = next; } return cur; } // ------------------------------------------------------------------ verification private static final class Manager implements X509TrustManager { private final Context c; private X509TrustManager system; Manager(Context c) { this.c = c; try { TrustManagerFactory f = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); f.init((KeyStore) null); for (TrustManager t : f.getTrustManagers()) { if (t instanceof X509TrustManager) system = (X509TrustManager) t; } } catch (Exception ignored) { // no system trust store: rely on bundled roots and pins } } public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException { if (chain == null || chain.length == 0) throw new CertificateException("No certificate"); if (system != null) { try { system.checkServerTrusted(chain, authType); return; } catch (CertificateException ignored) { // fall through to our own roots and pins } catch (RuntimeException ignored) { } } List anchors = new ArrayList(bundled()); anchors.addAll(pinned(c)); String why = verify(chain, anchors); if (why == null) return; lastRejected = new Rejected(chain.clone(), why); throw new CertificateException("Certificate not trusted: " + why); } public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException { throw new CertificateException("Not used"); } public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } /** * Returns null if chain[0] chains up to one of the anchors, or a short reason why not. * Every certificate that signs another must be a CA, so a website's own certificate * can't be used to forge one for a different site. */ static String verify(X509Certificate[] chain, List anchors) { X509Certificate cur = chain[0]; try { cur.checkValidity(); } catch (Exception e) { return "expired or not yet valid (check the phone's date)"; } for (int step = 0; step < 8; step++) { for (X509Certificate a : anchors) { if (Arrays.equals(encoded(a), encoded(cur))) return null; // trusted as-is } for (X509Certificate a : anchors) { if (a.getSubjectX500Principal().equals(cur.getIssuerX500Principal()) && isCa(a) && signedBy(cur, a)) { return null; } } X509Certificate next = issuerIn(cur, chain); if (next == null) return "unknown issuer"; if (!isCa(next)) return "signed by a certificate that isn't a CA"; try { next.checkValidity(); } catch (Exception e) { return "expired intermediate certificate (check the phone's date)"; } cur = next; } return "chain too long"; } private static X509Certificate issuerIn(X509Certificate cert, X509Certificate[] chain) { for (X509Certificate x : chain) { if (x == cert || Arrays.equals(encoded(x), encoded(cert))) continue; if (x.getSubjectX500Principal().equals(cert.getIssuerX500Principal()) && signedBy(cert, x)) return x; } return null; } private static boolean isCa(X509Certificate x) { if (x.getBasicConstraints() < 0) return false; boolean[] ku = x.getKeyUsage(); return ku == null || ku.length <= 5 || ku[5]; // keyCertSign } private static boolean signedBy(X509Certificate cert, X509Certificate issuer) { try { cert.verify(issuer.getPublicKey()); return true; } catch (Exception e) { return false; } } private static byte[] encoded(X509Certificate x) { try { return x.getEncoded(); } catch (Exception e) { return new byte[0]; } } /** Standard host name check: DNS/IP subject alternative names (one-label wildcards), else the CN. */ static boolean hostMatches(X509Certificate cert, String host) { host = host.toLowerCase(Locale.US); boolean hasDns = false; try { Collection> sans = cert.getSubjectAlternativeNames(); if (sans != null) { for (List e : sans) { int type = ((Integer) e.get(0)).intValue(); String v = String.valueOf(e.get(1)).toLowerCase(Locale.US); if (type == 2) { hasDns = true; if (nameMatches(v, host)) return true; } else if (type == 7 && v.equals(host)) { return true; } } } } catch (Exception ignored) { } if (hasDns) return false; return nameMatches(commonName(cert.getSubjectX500Principal()).toLowerCase(Locale.US), host); } private static boolean nameMatches(String pattern, String host) { if (pattern.startsWith("*.")) { String suffix = pattern.substring(1); // ".example.com" if (host.length() <= suffix.length() || !host.endsWith(suffix)) return false; return host.substring(0, host.length() - suffix.length()).indexOf('.') < 0; } return pattern.equals(host); } // ------------------------------------------------------------------ display static String fingerprint(X509Certificate x) { try { byte[] d = MessageDigest.getInstance("SHA-256").digest(x.getEncoded()); StringBuilder sb = new StringBuilder(); for (int i = 0; i < d.length; i++) { if (i > 0) sb.append(i == 16 ? "\n" : ":"); int v = d[i] & 0xff; sb.append("0123456789ABCDEF".charAt(v >> 4)).append("0123456789ABCDEF".charAt(v & 15)); } return sb.toString(); } catch (Exception e) { return "?"; } } static String commonName(javax.security.auth.x500.X500Principal p) { String n = p.getName(); Matcher m = Pattern.compile("(?:^|,)CN=([^,]+)").matcher(n); if (m.find()) return m.group(1); m = Pattern.compile("(?:^|,)O=([^,]+)").matcher(n); return m.find() ? m.group(1) : n; } /** Text for the "trust this server?" dialog. */ static String describe(Rejected r, String host) { X509Certificate leaf = r.chain[0]; X509Certificate top = top(r.chain); StringBuilder sb = new StringBuilder(); sb.append("Este teléfono non recoñece o certificado de ").append(host).append(".\n\n"); sb.append("Emitido para: ").append(commonName(leaf.getSubjectX500Principal())).append('\n'); sb.append("Emitido por: ").append(commonName(leaf.getIssuerX500Principal())).append('\n'); sb.append("Válido ata: ").append(Util.date(leaf.getNotAfter().getTime(), true)).append("\n\n"); sb.append("Pegada dixital SHA-256:\n").append(fingerprint(leaf)).append("\n\n"); sb.append("Continúa só se coincide coa pegada que amosa o navegador do teu ordenador para este sitio."); if (top != leaf) { sb.append("\n\nMiniABS confiará desde agora nos certificados emitidos por «") .append(commonName(top.getSubjectX500Principal())).append("»."); } return sb.toString(); } }