mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AbstractVerifier.java

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/mobile/android/thirdparty/ch/boye/httpclientandroidlib/conn/ssl/AbstractVerifier.java	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,366 @@
     1.4 +/*
     1.5 + * ====================================================================
     1.6 + * Licensed to the Apache Software Foundation (ASF) under one
     1.7 + * or more contributor license agreements.  See the NOTICE file
     1.8 + * distributed with this work for additional information
     1.9 + * regarding copyright ownership.  The ASF licenses this file
    1.10 + * to you under the Apache License, Version 2.0 (the
    1.11 + * "License"); you may not use this file except in compliance
    1.12 + * with the License.  You may obtain a copy of the License at
    1.13 + *
    1.14 + *   http://www.apache.org/licenses/LICENSE-2.0
    1.15 + *
    1.16 + * Unless required by applicable law or agreed to in writing,
    1.17 + * software distributed under the License is distributed on an
    1.18 + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    1.19 + * KIND, either express or implied.  See the License for the
    1.20 + * specific language governing permissions and limitations
    1.21 + * under the License.
    1.22 + * ====================================================================
    1.23 + *
    1.24 + * This software consists of voluntary contributions made by many
    1.25 + * individuals on behalf of the Apache Software Foundation.  For more
    1.26 + * information on the Apache Software Foundation, please see
    1.27 + * <http://www.apache.org/>.
    1.28 + *
    1.29 + */
    1.30 +
    1.31 +package ch.boye.httpclientandroidlib.conn.ssl;
    1.32 +
    1.33 +import ch.boye.httpclientandroidlib.annotation.Immutable;
    1.34 +
    1.35 +import ch.boye.httpclientandroidlib.conn.util.InetAddressUtils;
    1.36 +
    1.37 +import java.io.IOException;
    1.38 +import java.io.InputStream;
    1.39 +import java.security.cert.Certificate;
    1.40 +import java.security.cert.CertificateParsingException;
    1.41 +import java.security.cert.X509Certificate;
    1.42 +import java.util.Arrays;
    1.43 +import java.util.Collection;
    1.44 +import java.util.Iterator;
    1.45 +import java.util.LinkedList;
    1.46 +import java.util.List;
    1.47 +import java.util.Locale;
    1.48 +import java.util.StringTokenizer;
    1.49 +import java.util.logging.Logger;
    1.50 +import java.util.logging.Level;
    1.51 +
    1.52 +import javax.net.ssl.SSLException;
    1.53 +import javax.net.ssl.SSLSession;
    1.54 +import javax.net.ssl.SSLSocket;
    1.55 +
    1.56 +/**
    1.57 + * Abstract base class for all standard {@link X509HostnameVerifier}
    1.58 + * implementations.
    1.59 + *
    1.60 + * @since 4.0
    1.61 + */
    1.62 +@Immutable
    1.63 +public abstract class AbstractVerifier implements X509HostnameVerifier {
    1.64 +
    1.65 +    /**
    1.66 +     * This contains a list of 2nd-level domains that aren't allowed to
    1.67 +     * have wildcards when combined with country-codes.
    1.68 +     * For example: [*.co.uk].
    1.69 +     * <p/>
    1.70 +     * The [*.co.uk] problem is an interesting one.  Should we just hope
    1.71 +     * that CA's would never foolishly allow such a certificate to happen?
    1.72 +     * Looks like we're the only implementation guarding against this.
    1.73 +     * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check.
    1.74 +     */
    1.75 +    private final static String[] BAD_COUNTRY_2LDS =
    1.76 +          { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info",
    1.77 +            "lg", "ne", "net", "or", "org" };
    1.78 +
    1.79 +    static {
    1.80 +        // Just in case developer forgot to manually sort the array.  :-)
    1.81 +        Arrays.sort(BAD_COUNTRY_2LDS);
    1.82 +    }
    1.83 +
    1.84 +    public AbstractVerifier() {
    1.85 +        super();
    1.86 +    }
    1.87 +
    1.88 +    public final void verify(String host, SSLSocket ssl)
    1.89 +          throws IOException {
    1.90 +        if(host == null) {
    1.91 +            throw new NullPointerException("host to verify is null");
    1.92 +        }
    1.93 +
    1.94 +        SSLSession session = ssl.getSession();
    1.95 +        if(session == null) {
    1.96 +            // In our experience this only happens under IBM 1.4.x when
    1.97 +            // spurious (unrelated) certificates show up in the server'
    1.98 +            // chain.  Hopefully this will unearth the real problem:
    1.99 +            InputStream in = ssl.getInputStream();
   1.100 +            in.available();
   1.101 +            /*
   1.102 +              If you're looking at the 2 lines of code above because
   1.103 +              you're running into a problem, you probably have two
   1.104 +              options:
   1.105 +
   1.106 +                #1.  Clean up the certificate chain that your server
   1.107 +                     is presenting (e.g. edit "/etc/apache2/server.crt"
   1.108 +                     or wherever it is your server's certificate chain
   1.109 +                     is defined).
   1.110 +
   1.111 +                                           OR
   1.112 +
   1.113 +                #2.   Upgrade to an IBM 1.5.x or greater JVM, or switch
   1.114 +                      to a non-IBM JVM.
   1.115 +            */
   1.116 +
   1.117 +            // If ssl.getInputStream().available() didn't cause an
   1.118 +            // exception, maybe at least now the session is available?
   1.119 +            session = ssl.getSession();
   1.120 +            if(session == null) {
   1.121 +                // If it's still null, probably a startHandshake() will
   1.122 +                // unearth the real problem.
   1.123 +                ssl.startHandshake();
   1.124 +
   1.125 +                // Okay, if we still haven't managed to cause an exception,
   1.126 +                // might as well go for the NPE.  Or maybe we're okay now?
   1.127 +                session = ssl.getSession();
   1.128 +            }
   1.129 +        }
   1.130 +
   1.131 +        Certificate[] certs = session.getPeerCertificates();
   1.132 +        X509Certificate x509 = (X509Certificate) certs[0];
   1.133 +        verify(host, x509);
   1.134 +    }
   1.135 +
   1.136 +    public final boolean verify(String host, SSLSession session) {
   1.137 +        try {
   1.138 +            Certificate[] certs = session.getPeerCertificates();
   1.139 +            X509Certificate x509 = (X509Certificate) certs[0];
   1.140 +            verify(host, x509);
   1.141 +            return true;
   1.142 +        }
   1.143 +        catch(SSLException e) {
   1.144 +            return false;
   1.145 +        }
   1.146 +    }
   1.147 +
   1.148 +    public final void verify(String host, X509Certificate cert)
   1.149 +          throws SSLException {
   1.150 +        String[] cns = getCNs(cert);
   1.151 +        String[] subjectAlts = getSubjectAlts(cert, host);
   1.152 +        verify(host, cns, subjectAlts);
   1.153 +    }
   1.154 +
   1.155 +    public final void verify(final String host, final String[] cns,
   1.156 +                             final String[] subjectAlts,
   1.157 +                             final boolean strictWithSubDomains)
   1.158 +          throws SSLException {
   1.159 +
   1.160 +        // Build the list of names we're going to check.  Our DEFAULT and
   1.161 +        // STRICT implementations of the HostnameVerifier only use the
   1.162 +        // first CN provided.  All other CNs are ignored.
   1.163 +        // (Firefox, wget, curl, Sun Java 1.4, 5, 6 all work this way).
   1.164 +        LinkedList<String> names = new LinkedList<String>();
   1.165 +        if(cns != null && cns.length > 0 && cns[0] != null) {
   1.166 +            names.add(cns[0]);
   1.167 +        }
   1.168 +        if(subjectAlts != null) {
   1.169 +            for (String subjectAlt : subjectAlts) {
   1.170 +                if (subjectAlt != null) {
   1.171 +                    names.add(subjectAlt);
   1.172 +                }
   1.173 +            }
   1.174 +        }
   1.175 +
   1.176 +        if(names.isEmpty()) {
   1.177 +            String msg = "Certificate for <" + host + "> doesn't contain CN or DNS subjectAlt";
   1.178 +            throw new SSLException(msg);
   1.179 +        }
   1.180 +
   1.181 +        // StringBuilder for building the error message.
   1.182 +        StringBuilder buf = new StringBuilder();
   1.183 +
   1.184 +        // We're can be case-insensitive when comparing the host we used to
   1.185 +        // establish the socket to the hostname in the certificate.
   1.186 +        String hostName = host.trim().toLowerCase(Locale.ENGLISH);
   1.187 +        boolean match = false;
   1.188 +        for(Iterator<String> it = names.iterator(); it.hasNext();) {
   1.189 +            // Don't trim the CN, though!
   1.190 +            String cn = it.next();
   1.191 +            cn = cn.toLowerCase(Locale.ENGLISH);
   1.192 +            // Store CN in StringBuilder in case we need to report an error.
   1.193 +            buf.append(" <");
   1.194 +            buf.append(cn);
   1.195 +            buf.append('>');
   1.196 +            if(it.hasNext()) {
   1.197 +                buf.append(" OR");
   1.198 +            }
   1.199 +
   1.200 +            // The CN better have at least two dots if it wants wildcard
   1.201 +            // action.  It also can't be [*.co.uk] or [*.co.jp] or
   1.202 +            // [*.org.uk], etc...
   1.203 +            String parts[] = cn.split("\\.");
   1.204 +            boolean doWildcard = parts.length >= 3 &&
   1.205 +                                 parts[0].endsWith("*") &&
   1.206 +                                 acceptableCountryWildcard(cn) &&
   1.207 +                                 !isIPAddress(host);
   1.208 +
   1.209 +            if(doWildcard) {
   1.210 +                if (parts[0].length() > 1) { // e.g. server*
   1.211 +                    String prefix = parts[0].substring(0, parts.length-2); // e.g. server
   1.212 +                    String suffix = cn.substring(parts[0].length()); // skip wildcard part from cn
   1.213 +                    String hostSuffix = hostName.substring(prefix.length()); // skip wildcard part from host
   1.214 +                    match = hostName.startsWith(prefix) && hostSuffix.endsWith(suffix);
   1.215 +                } else {
   1.216 +                    match = hostName.endsWith(cn.substring(1));                    
   1.217 +                }
   1.218 +                if(match && strictWithSubDomains) {
   1.219 +                    // If we're in strict mode, then [*.foo.com] is not
   1.220 +                    // allowed to match [a.b.foo.com]
   1.221 +                    match = countDots(hostName) == countDots(cn);
   1.222 +                }
   1.223 +            } else {
   1.224 +                match = hostName.equals(cn);
   1.225 +            }
   1.226 +            if(match) {
   1.227 +                break;
   1.228 +            }
   1.229 +        }
   1.230 +        if(!match) {
   1.231 +            throw new SSLException("hostname in certificate didn't match: <" + host + "> !=" + buf);
   1.232 +        }
   1.233 +    }
   1.234 +
   1.235 +    public static boolean acceptableCountryWildcard(String cn) {
   1.236 +        String parts[] = cn.split("\\.");
   1.237 +        if (parts.length != 3 || parts[2].length() != 2) {
   1.238 +            return true; // it's not an attempt to wildcard a 2TLD within a country code
   1.239 +        }
   1.240 +        return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0;
   1.241 +    }
   1.242 +
   1.243 +    public static String[] getCNs(X509Certificate cert) {
   1.244 +        LinkedList<String> cnList = new LinkedList<String>();
   1.245 +        /*
   1.246 +          Sebastian Hauer's original StrictSSLProtocolSocketFactory used
   1.247 +          getName() and had the following comment:
   1.248 +
   1.249 +                Parses a X.500 distinguished name for the value of the
   1.250 +                "Common Name" field.  This is done a bit sloppy right
   1.251 +                 now and should probably be done a bit more according to
   1.252 +                <code>RFC 2253</code>.
   1.253 +
   1.254 +           I've noticed that toString() seems to do a better job than
   1.255 +           getName() on these X500Principal objects, so I'm hoping that
   1.256 +           addresses Sebastian's concern.
   1.257 +
   1.258 +           For example, getName() gives me this:
   1.259 +           1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d
   1.260 +
   1.261 +           whereas toString() gives me this:
   1.262 +           EMAILADDRESS=juliusdavies@cucbc.com
   1.263 +
   1.264 +           Looks like toString() even works with non-ascii domain names!
   1.265 +           I tested it with "&#x82b1;&#x5b50;.co.jp" and it worked fine.
   1.266 +        */
   1.267 +        String subjectPrincipal = cert.getSubjectX500Principal().toString();
   1.268 +        StringTokenizer st = new StringTokenizer(subjectPrincipal, ",");
   1.269 +        while(st.hasMoreTokens()) {
   1.270 +            String tok = st.nextToken();
   1.271 +            int x = tok.indexOf("CN=");
   1.272 +            if(x >= 0) {
   1.273 +                cnList.add(tok.substring(x + 3));
   1.274 +            }
   1.275 +        }
   1.276 +        if(!cnList.isEmpty()) {
   1.277 +            String[] cns = new String[cnList.size()];
   1.278 +            cnList.toArray(cns);
   1.279 +            return cns;
   1.280 +        } else {
   1.281 +            return null;
   1.282 +        }
   1.283 +    }
   1.284 +
   1.285 +    /**
   1.286 +     * Extracts the array of SubjectAlt DNS or IP names from an X509Certificate.
   1.287 +     * Returns null if there aren't any.
   1.288 +     *
   1.289 +     * @param cert X509Certificate
   1.290 +     * @param hostname
   1.291 +     * @return Array of SubjectALT DNS or IP names stored in the certificate.
   1.292 +     */
   1.293 +    private static String[] getSubjectAlts(
   1.294 +            final X509Certificate cert, final String hostname) {
   1.295 +        int subjectType;
   1.296 +        if (isIPAddress(hostname)) {
   1.297 +            subjectType = 7;
   1.298 +        } else {
   1.299 +            subjectType = 2;
   1.300 +        }
   1.301 +
   1.302 +        LinkedList<String> subjectAltList = new LinkedList<String>();
   1.303 +        Collection<List<?>> c = null;
   1.304 +        try {
   1.305 +            c = cert.getSubjectAlternativeNames();
   1.306 +        }
   1.307 +        catch(CertificateParsingException cpe) {
   1.308 +            Logger.getLogger(AbstractVerifier.class.getName())
   1.309 +                    .log(Level.FINE, "Error parsing certificate.", cpe);
   1.310 +        }
   1.311 +        if(c != null) {
   1.312 +            for (List<?> aC : c) {
   1.313 +                List<?> list = aC;
   1.314 +                int type = ((Integer) list.get(0)).intValue();
   1.315 +                if (type == subjectType) {
   1.316 +                    String s = (String) list.get(1);
   1.317 +                    subjectAltList.add(s);
   1.318 +                }
   1.319 +            }
   1.320 +        }
   1.321 +        if(!subjectAltList.isEmpty()) {
   1.322 +            String[] subjectAlts = new String[subjectAltList.size()];
   1.323 +            subjectAltList.toArray(subjectAlts);
   1.324 +            return subjectAlts;
   1.325 +        } else {
   1.326 +            return null;
   1.327 +        }
   1.328 +    }
   1.329 +
   1.330 +    /**
   1.331 +     * Extracts the array of SubjectAlt DNS names from an X509Certificate.
   1.332 +     * Returns null if there aren't any.
   1.333 +     * <p/>
   1.334 +     * Note:  Java doesn't appear able to extract international characters
   1.335 +     * from the SubjectAlts.  It can only extract international characters
   1.336 +     * from the CN field.
   1.337 +     * <p/>
   1.338 +     * (Or maybe the version of OpenSSL I'm using to test isn't storing the
   1.339 +     * international characters correctly in the SubjectAlts?).
   1.340 +     *
   1.341 +     * @param cert X509Certificate
   1.342 +     * @return Array of SubjectALT DNS names stored in the certificate.
   1.343 +     */
   1.344 +    public static String[] getDNSSubjectAlts(X509Certificate cert) {
   1.345 +        return getSubjectAlts(cert, null);
   1.346 +    }
   1.347 +
   1.348 +    /**
   1.349 +     * Counts the number of dots "." in a string.
   1.350 +     * @param s  string to count dots from
   1.351 +     * @return  number of dots
   1.352 +     */
   1.353 +    public static int countDots(final String s) {
   1.354 +        int count = 0;
   1.355 +        for(int i = 0; i < s.length(); i++) {
   1.356 +            if(s.charAt(i) == '.') {
   1.357 +                count++;
   1.358 +            }
   1.359 +        }
   1.360 +        return count;
   1.361 +    }
   1.362 +
   1.363 +    private static boolean isIPAddress(final String hostname) {
   1.364 +        return hostname != null &&
   1.365 +            (InetAddressUtils.isIPv4Address(hostname) ||
   1.366 +                    InetAddressUtils.isIPv6Address(hostname));
   1.367 +    }
   1.368 +
   1.369 +}

mercurial