001/* 002 * ==================================================================== 003 * Licensed to the Apache Software Foundation (ASF) under one 004 * or more contributor license agreements. See the NOTICE file 005 * distributed with this work for additional information 006 * regarding copyright ownership. The ASF licenses this file 007 * to you under the Apache License, Version 2.0 (the 008 * "License"); you may not use this file except in compliance 009 * with the License. You may obtain a copy of the License at 010 * 011 * http://www.apache.org/licenses/LICENSE-2.0 012 * 013 * Unless required by applicable law or agreed to in writing, 014 * software distributed under the License is distributed on an 015 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 016 * KIND, either express or implied. See the License for the 017 * specific language governing permissions and limitations 018 * under the License. 019 * ==================================================================== 020 * 021 * This software consists of voluntary contributions made by many 022 * individuals on behalf of the Apache Software Foundation. For more 023 * information on the Apache Software Foundation, please see 024 * <http://www.apache.org/>. 025 * 026 */ 027 028package org.apache.http.conn.ssl; 029 030import java.io.IOException; 031import java.io.InputStream; 032import java.security.cert.Certificate; 033import java.security.cert.X509Certificate; 034import java.util.ArrayList; 035import java.util.Arrays; 036import java.util.List; 037import java.util.Locale; 038 039import javax.net.ssl.SSLException; 040import javax.net.ssl.SSLSession; 041import javax.net.ssl.SSLSocket; 042import javax.security.auth.x500.X500Principal; 043 044import org.apache.commons.logging.Log; 045import org.apache.commons.logging.LogFactory; 046import org.apache.http.conn.util.InetAddressUtils; 047import org.apache.http.util.Args; 048 049/** 050 * Abstract base class for all standard {@link X509HostnameVerifier} 051 * implementations. 052 * 053 * @since 4.0 054 * 055 * @deprecated (4.4) use an implementation of {@link javax.net.ssl.HostnameVerifier} or 056 * {@link DefaultHostnameVerifier}. 057 */ 058@Deprecated 059public abstract class AbstractVerifier implements X509HostnameVerifier { 060 061 private final Log log = LogFactory.getLog(getClass()); 062 063 final static String[] BAD_COUNTRY_2LDS = 064 { "ac", "co", "com", "ed", "edu", "go", "gouv", "gov", "info", 065 "lg", "ne", "net", "or", "org" }; 066 067 static { 068 // Just in case developer forgot to manually sort the array. :-) 069 Arrays.sort(BAD_COUNTRY_2LDS); 070 } 071 072 @Override 073 public final void verify(final String host, final SSLSocket ssl) 074 throws IOException { 075 Args.notNull(host, "Host"); 076 SSLSession session = ssl.getSession(); 077 if(session == null) { 078 // In our experience this only happens under IBM 1.4.x when 079 // spurious (unrelated) certificates show up in the server' 080 // chain. Hopefully this will unearth the real problem: 081 final InputStream in = ssl.getInputStream(); 082 in.available(); 083 /* 084 If you're looking at the 2 lines of code above because 085 you're running into a problem, you probably have two 086 options: 087 088 #1. Clean up the certificate chain that your server 089 is presenting (e.g. edit "/etc/apache2/server.crt" 090 or wherever it is your server's certificate chain 091 is defined). 092 093 OR 094 095 #2. Upgrade to an IBM 1.5.x or greater JVM, or switch 096 to a non-IBM JVM. 097 */ 098 099 // If ssl.getInputStream().available() didn't cause an 100 // exception, maybe at least now the session is available? 101 session = ssl.getSession(); 102 if(session == null) { 103 // If it's still null, probably a startHandshake() will 104 // unearth the real problem. 105 ssl.startHandshake(); 106 107 // Okay, if we still haven't managed to cause an exception, 108 // might as well go for the NPE. Or maybe we're okay now? 109 session = ssl.getSession(); 110 } 111 } 112 113 final Certificate[] certs = session.getPeerCertificates(); 114 final X509Certificate x509 = (X509Certificate) certs[0]; 115 verify(host, x509); 116 } 117 118 @Override 119 public final boolean verify(final String host, final SSLSession session) { 120 try { 121 final Certificate[] certs = session.getPeerCertificates(); 122 final X509Certificate x509 = (X509Certificate) certs[0]; 123 verify(host, x509); 124 return true; 125 } catch(final SSLException ex) { 126 if (log.isDebugEnabled()) { 127 log.debug(ex.getMessage(), ex); 128 } 129 return false; 130 } 131 } 132 133 @Override 134 public final void verify( 135 final String host, final X509Certificate cert) throws SSLException { 136 final List<SubjectName> allSubjectAltNames = DefaultHostnameVerifier.getSubjectAltNames(cert); 137 final List<String> subjectAlts = new ArrayList<String>(); 138 if (InetAddressUtils.isIPv4Address(host) || InetAddressUtils.isIPv6Address(host)) { 139 for (SubjectName subjectName: allSubjectAltNames) { 140 if (subjectName.getType() == SubjectName.IP) { 141 subjectAlts.add(subjectName.getValue()); 142 } 143 } 144 } else { 145 for (SubjectName subjectName: allSubjectAltNames) { 146 if (subjectName.getType() == SubjectName.DNS) { 147 subjectAlts.add(subjectName.getValue()); 148 } 149 } 150 } 151 final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 152 final String cn = DefaultHostnameVerifier.extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); 153 verify(host, 154 cn != null ? new String[] {cn} : null, 155 subjectAlts != null && !subjectAlts.isEmpty() ? subjectAlts.toArray(new String[subjectAlts.size()]) : null); 156 } 157 158 public final void verify(final String host, final String[] cns, 159 final String[] subjectAlts, 160 final boolean strictWithSubDomains) 161 throws SSLException { 162 163 final String cn = cns != null && cns.length > 0 ? cns[0] : null; 164 final List<String> subjectAltList = subjectAlts != null && subjectAlts.length > 0 ? Arrays.asList(subjectAlts) : null; 165 166 final String normalizedHost = InetAddressUtils.isIPv6Address(host) ? 167 DefaultHostnameVerifier.normaliseAddress(host.toLowerCase(Locale.ROOT)) : host; 168 169 if (subjectAltList != null) { 170 for (final String subjectAlt: subjectAltList) { 171 final String normalizedAltSubject = InetAddressUtils.isIPv6Address(subjectAlt) ? 172 DefaultHostnameVerifier.normaliseAddress(subjectAlt) : subjectAlt; 173 if (matchIdentity(normalizedHost, normalizedAltSubject, strictWithSubDomains)) { 174 return; 175 } 176 } 177 throw new SSLException("Certificate for <" + host + "> doesn't match any " + 178 "of the subject alternative names: " + subjectAltList); 179 } else if (cn != null) { 180 final String normalizedCN = InetAddressUtils.isIPv6Address(cn) ? 181 DefaultHostnameVerifier.normaliseAddress(cn) : cn; 182 if (matchIdentity(normalizedHost, normalizedCN, strictWithSubDomains)) { 183 return; 184 } 185 throw new SSLException("Certificate for <" + host + "> doesn't match " + 186 "common name of the certificate subject: " + cn); 187 } else { 188 throw new SSLException("Certificate subject for <" + host + "> doesn't contain " + 189 "a common name and does not have alternative names"); 190 } 191 } 192 193 private static boolean matchIdentity(final String host, final String identity, final boolean strict) { 194 if (host == null) { 195 return false; 196 } 197 final String normalizedHost = host.toLowerCase(Locale.ROOT); 198 final String normalizedIdentity = identity.toLowerCase(Locale.ROOT); 199 // The CN better have at least two dots if it wants wildcard 200 // action. It also can't be [*.co.uk] or [*.co.jp] or 201 // [*.org.uk], etc... 202 final String parts[] = normalizedIdentity.split("\\."); 203 final boolean doWildcard = parts.length >= 3 && parts[0].endsWith("*") && 204 (!strict || validCountryWildcard(parts)); 205 if (doWildcard) { 206 boolean match; 207 final String firstpart = parts[0]; 208 if (firstpart.length() > 1) { // e.g. server* 209 final String prefix = firstpart.substring(0, firstpart.length() - 1); // e.g. server 210 final String suffix = normalizedIdentity.substring(firstpart.length()); // skip wildcard part from cn 211 final String hostSuffix = normalizedHost.substring(prefix.length()); // skip wildcard part from normalizedHost 212 match = normalizedHost.startsWith(prefix) && hostSuffix.endsWith(suffix); 213 } else { 214 match = normalizedHost.endsWith(normalizedIdentity.substring(1)); 215 } 216 return match && (!strict || countDots(normalizedHost) == countDots(normalizedIdentity)); 217 } else { 218 return normalizedHost.equals(normalizedIdentity); 219 } 220 } 221 222 private static boolean validCountryWildcard(final String parts[]) { 223 if (parts.length != 3 || parts[2].length() != 2) { 224 return true; // it's not an attempt to wildcard a 2TLD within a country code 225 } 226 return Arrays.binarySearch(BAD_COUNTRY_2LDS, parts[1]) < 0; 227 } 228 229 public static boolean acceptableCountryWildcard(final String cn) { 230 return validCountryWildcard(cn.split("\\.")); 231 } 232 233 public static String[] getCNs(final X509Certificate cert) { 234 final String subjectPrincipal = cert.getSubjectX500Principal().toString(); 235 try { 236 final String cn = DefaultHostnameVerifier.extractCN(subjectPrincipal); 237 return cn != null ? new String[] { cn } : null; 238 } catch (final SSLException ex) { 239 return null; 240 } 241 } 242 243 /** 244 * Extracts the array of SubjectAlt DNS names from an X509Certificate. 245 * Returns null if there aren't any. 246 * <p> 247 * Note: Java doesn't appear able to extract international characters 248 * from the SubjectAlts. It can only extract international characters 249 * from the CN field. 250 * </p> 251 * <p> 252 * (Or maybe the version of OpenSSL I'm using to test isn't storing the 253 * international characters correctly in the SubjectAlts?). 254 * </p> 255 * 256 * @param cert X509Certificate 257 * @return Array of SubjectALT DNS names stored in the certificate. 258 */ 259 public static String[] getDNSSubjectAlts(final X509Certificate cert) { 260 final List<SubjectName> subjectAltNames = DefaultHostnameVerifier.getSubjectAltNames(cert); 261 if (subjectAltNames == null) { 262 return null; 263 } 264 final List<String> dnsAlts = new ArrayList<String>(); 265 for (SubjectName subjectName: subjectAltNames) { 266 if (subjectName.getType() == SubjectName.DNS) { 267 dnsAlts.add(subjectName.getValue()); 268 } 269 } 270 return dnsAlts.isEmpty() ? dnsAlts.toArray(new String[dnsAlts.size()]) : null; 271 } 272 273 /** 274 * Counts the number of dots "." in a string. 275 * @param s string to count dots from 276 * @return number of dots 277 */ 278 public static int countDots(final String s) { 279 int count = 0; 280 for(int i = 0; i < s.length(); i++) { 281 if(s.charAt(i) == '.') { 282 count++; 283 } 284 } 285 return count; 286 } 287 288}