/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package javax.net.ssl; import java.net.InetAddress; import java.security.cert.Certificate; import java.security.cert.CertificateParsingException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; import java.util.Locale; /** * A HostnameVerifier that works the same way as Curl and Firefox. * * <p>The hostname must match either the first CN, or any of the subject-alts. * A wildcard can occur in the CN, and in any of the subject-alts. * * @author Julius Davies */ class DefaultHostnameVerifier implements HostnameVerifier { /** * This contains a list of 2nd-level domains that aren't allowed to * have wildcards when combined with country-codes. * For example: [*.co.uk]. * * <p>The [*.co.uk] problem is an interesting one. Should we just hope * that CA's would never foolishly allow such a certificate to happen? * Looks like we're the only implementation guarding against this. * Firefox, Curl, Sun Java 1.4, 5, 6 don't bother with this check. */ private static final String[] BAD_COUNTRY_2LDS = { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", "lg", "ne", "net", "or", "org" }; static { // Just in case developer forgot to manually sort the array. :-) Arrays.sort(BAD_COUNTRY_2LDS); } public final boolean verify(String host, SSLSession session) { Certificate[] certs; try { certs = session.getPeerCertificates(); } catch (SSLException e) { return false; } X509Certificate x509 = (X509Certificate) certs[0]; // We can be case-insensitive when comparing the host we used to // establish the socket to the hostname in the certificate. String hostName = host.trim().toLowerCase(Locale.ENGLISH); // Verify the first CN provided. Other CNs are ignored. Firefox, wget, // curl, and Sun Java work this way. String firstCn = getFirstCn(x509); if (matches(hostName, firstCn)) { return true; } for (String cn : getDNSSubjectAlts(x509)) { if (matches(hostName, cn)) { return true; } } return false; } /** * Returns true if {@code hostname} matches {@code cn}. * * @param hostName a trimmed, lowercase hostname to verify * @param cn a certificate CN or DNS subject alt. Either a literal name or * a wildcard of the form "*.google.com". */ private boolean matches(String hostName, String cn) { if (cn == null) { return false; } // Don't trim the CN, though! cn = cn.toLowerCase(Locale.ENGLISH); if (cn.startsWith("*.")) { // When a wildcard matches, also check that the wildcard is legit // - Wildcards must contain at least two dots: "*.google.com" // - Wildcards must be for private domains. No "*.co.uk" etc. // - Wildcards must not match IP addresses: "*.8.8" int matchLength = cn.length() - 1; return hostName.regionMatches(hostName.length() - matchLength, cn, 1, matchLength) && cn.indexOf('.', 2) != -1 && acceptableCountryWildcard(cn) && !InetAddress.isNumeric(hostName); } else { return hostName.equals(cn); } } private boolean acceptableCountryWildcard(String cn) { int cnLen = cn.length(); if (cnLen >= 7 && cnLen <= 9) { // Look for the '.' in the 3rd-last position: if (cn.charAt(cnLen - 3) == '.') { // Trim off the [*.] and the [.XX]. String s = cn.substring(2, cnLen - 3); // And test against the sorted array of bad 2lds: int x = Arrays.binarySearch(BAD_COUNTRY_2LDS, s); return x < 0; } } return true; } private String getFirstCn(X509Certificate cert) { /* * Sebastian Hauer's original StrictSSLProtocolSocketFactory used * getName() and had the following comment: * * Parses a X.500 distinguished name for the value of the * "Common Name" field. This is done a bit sloppy right * now and should probably be done a bit more according to * <code>RFC 2253</code>. * * I've noticed that toString() seems to do a better job than * getName() on these X500Principal objects, so I'm hoping that * addresses Sebastian's concern. * * For example, getName() gives me this: * 1.2.840.113549.1.9.1=#16166a756c6975736461766965734063756362632e636f6d * * whereas toString() gives me this: * EMAILADDRESS=juliusdavies@cucbc.com * * Looks like toString() even works with non-ascii domain names! * I tested it with "花子.co.jp" and it worked fine. */ String subjectPrincipal = cert.getSubjectX500Principal().toString(); for (String token : subjectPrincipal.split(",")) { int x = token.indexOf("CN="); if (x >= 0) { return token.substring(x + 3); } } return null; } /** * Returns all SubjectAlt DNS names from an X509Certificate. * * <p>Note: Java doesn't appear able to extract international characters * from the SubjectAlts. It can only extract international characters * from the CN field. * * <p>(Or maybe the version of OpenSSL I'm using to test isn't storing the * international characters correctly in the SubjectAlts?). */ private List<String> getDNSSubjectAlts(X509Certificate cert) { Collection<List<?>> subjectAlternativeNames; try { subjectAlternativeNames = cert.getSubjectAlternativeNames(); } catch (CertificateParsingException cpe) { System.logI("Error parsing certificate", cpe); return Collections.emptyList(); } if (subjectAlternativeNames == null) { return Collections.emptyList(); } List<String> subjectAltList = new ArrayList<String>(); for (List<?> pair : subjectAlternativeNames) { int type = (Integer) pair.get(0); // If type is 2, then we've got a dNSName if (type == 2) { subjectAltList.add((String) pair.get(1)); } } return subjectAltList; } }