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.net.InetAddress; 031import java.net.UnknownHostException; 032import java.security.cert.Certificate; 033import java.security.cert.CertificateParsingException; 034import java.security.cert.X509Certificate; 035import java.util.ArrayList; 036import java.util.Collection; 037import java.util.Collections; 038import java.util.List; 039import java.util.Locale; 040import java.util.NoSuchElementException; 041 042import javax.naming.InvalidNameException; 043import javax.naming.NamingException; 044import javax.naming.directory.Attribute; 045import javax.naming.directory.Attributes; 046import javax.naming.ldap.LdapName; 047import javax.naming.ldap.Rdn; 048import javax.net.ssl.HostnameVerifier; 049import javax.net.ssl.SSLException; 050import javax.net.ssl.SSLPeerUnverifiedException; 051import javax.net.ssl.SSLSession; 052import javax.security.auth.x500.X500Principal; 053 054import org.apache.commons.logging.Log; 055import org.apache.commons.logging.LogFactory; 056import org.apache.http.annotation.Contract; 057import org.apache.http.annotation.ThreadingBehavior; 058import org.apache.http.conn.util.DomainType; 059import org.apache.http.conn.util.InetAddressUtils; 060import org.apache.http.conn.util.PublicSuffixMatcher; 061 062/** 063 * Default {@link javax.net.ssl.HostnameVerifier} implementation. 064 * 065 * @since 4.4 066 */ 067@Contract(threading = ThreadingBehavior.IMMUTABLE_CONDITIONAL) 068public final class DefaultHostnameVerifier implements HostnameVerifier { 069 070 enum HostNameType { 071 072 IPv4(7), IPv6(7), DNS(2); 073 074 final int subjectType; 075 076 HostNameType(final int subjectType) { 077 this.subjectType = subjectType; 078 } 079 080 } 081 082 private final Log log = LogFactory.getLog(getClass()); 083 084 private final PublicSuffixMatcher publicSuffixMatcher; 085 086 public DefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) { 087 this.publicSuffixMatcher = publicSuffixMatcher; 088 } 089 090 public DefaultHostnameVerifier() { 091 this(null); 092 } 093 094 @Override 095 public boolean verify(final String host, final SSLSession session) { 096 try { 097 final Certificate[] certs = session.getPeerCertificates(); 098 final X509Certificate x509 = (X509Certificate) certs[0]; 099 verify(host, x509); 100 return true; 101 } catch (final SSLException ex) { 102 if (log.isDebugEnabled()) { 103 log.debug(ex.getMessage(), ex); 104 } 105 return false; 106 } 107 } 108 109 public void verify( 110 final String host, final X509Certificate cert) throws SSLException { 111 final HostNameType hostType = determineHostFormat(host); 112 final List<SubjectName> subjectAlts = getSubjectAltNames(cert); 113 if (subjectAlts != null && !subjectAlts.isEmpty()) { 114 switch (hostType) { 115 case IPv4: 116 matchIPAddress(host, subjectAlts); 117 break; 118 case IPv6: 119 matchIPv6Address(host, subjectAlts); 120 break; 121 default: 122 matchDNSName(host, subjectAlts, this.publicSuffixMatcher); 123 } 124 } else { 125 // CN matching has been deprecated by rfc2818 and can be used 126 // as fallback only when no subjectAlts are available 127 final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 128 final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); 129 if (cn == null) { 130 throw new SSLException("Certificate subject for <" + host + "> doesn't contain " + 131 "a common name and does not have alternative names"); 132 } 133 matchCN(host, cn, this.publicSuffixMatcher); 134 } 135 } 136 137 static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) throws SSLException { 138 for (int i = 0; i < subjectAlts.size(); i++) { 139 final SubjectName subjectAlt = subjectAlts.get(i); 140 if (subjectAlt.getType() == SubjectName.IP) { 141 if (host.equals(subjectAlt.getValue())) { 142 return; 143 } 144 } 145 } 146 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " + 147 "of the subject alternative names: " + subjectAlts); 148 } 149 150 static void matchIPv6Address(final String host, final List<SubjectName> subjectAlts) throws SSLException { 151 final String normalisedHost = normaliseAddress(host); 152 for (int i = 0; i < subjectAlts.size(); i++) { 153 final SubjectName subjectAlt = subjectAlts.get(i); 154 if (subjectAlt.getType() == SubjectName.IP) { 155 final String normalizedSubjectAlt = normaliseAddress(subjectAlt.getValue()); 156 if (normalisedHost.equals(normalizedSubjectAlt)) { 157 return; 158 } 159 } 160 } 161 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " + 162 "of the subject alternative names: " + subjectAlts); 163 } 164 165 static void matchDNSName(final String host, final List<SubjectName> subjectAlts, 166 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException { 167 final String normalizedHost = host.toLowerCase(Locale.ROOT); 168 for (int i = 0; i < subjectAlts.size(); i++) { 169 final SubjectName subjectAlt = subjectAlts.get(i); 170 if (subjectAlt.getType() == SubjectName.DNS) { 171 final String normalizedSubjectAlt = subjectAlt.getValue().toLowerCase(Locale.ROOT); 172 if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) { 173 return; 174 } 175 } 176 } 177 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " + 178 "of the subject alternative names: " + subjectAlts); 179 } 180 181 static void matchCN(final String host, final String cn, 182 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException { 183 final String normalizedHost = host.toLowerCase(Locale.ROOT); 184 final String normalizedCn = cn.toLowerCase(Locale.ROOT); 185 if (!matchIdentityStrict(normalizedHost, normalizedCn, publicSuffixMatcher)) { 186 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " + 187 "common name of the certificate subject: " + cn); 188 } 189 } 190 191 static boolean matchDomainRoot(final String host, final String domainRoot) { 192 if (domainRoot == null) { 193 return false; 194 } 195 return host.endsWith(domainRoot) && (host.length() == domainRoot.length() 196 || host.charAt(host.length() - domainRoot.length() - 1) == '.'); 197 } 198 199 private static boolean matchIdentity(final String host, final String identity, 200 final PublicSuffixMatcher publicSuffixMatcher, 201 final boolean strict) { 202 if (publicSuffixMatcher != null && host.contains(".")) { 203 if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, DomainType.ICANN))) { 204 return false; 205 } 206 } 207 208 // RFC 2818, 3.1. Server Identity 209 // "...Names may contain the wildcard 210 // character * which is considered to match any single domain name 211 // component or component fragment..." 212 // Based on this statement presuming only singular wildcard is legal 213 final int asteriskIdx = identity.indexOf('*'); 214 if (asteriskIdx != -1) { 215 final String prefix = identity.substring(0, asteriskIdx); 216 final String suffix = identity.substring(asteriskIdx + 1); 217 if (!prefix.isEmpty() && !host.startsWith(prefix)) { 218 return false; 219 } 220 if (!suffix.isEmpty() && !host.endsWith(suffix)) { 221 return false; 222 } 223 // Additional sanity checks on content selected by wildcard can be done here 224 if (strict) { 225 final String remainder = host.substring( 226 prefix.length(), host.length() - suffix.length()); 227 if (remainder.contains(".")) { 228 return false; 229 } 230 } 231 return true; 232 } 233 return host.equalsIgnoreCase(identity); 234 } 235 236 static boolean matchIdentity(final String host, final String identity, 237 final PublicSuffixMatcher publicSuffixMatcher) { 238 return matchIdentity(host, identity, publicSuffixMatcher, false); 239 } 240 241 static boolean matchIdentity(final String host, final String identity) { 242 return matchIdentity(host, identity, null, false); 243 } 244 245 static boolean matchIdentityStrict(final String host, final String identity, 246 final PublicSuffixMatcher publicSuffixMatcher) { 247 return matchIdentity(host, identity, publicSuffixMatcher, true); 248 } 249 250 static boolean matchIdentityStrict(final String host, final String identity) { 251 return matchIdentity(host, identity, null, true); 252 } 253 254 static String extractCN(final String subjectPrincipal) throws SSLException { 255 if (subjectPrincipal == null) { 256 return null; 257 } 258 try { 259 final LdapName subjectDN = new LdapName(subjectPrincipal); 260 final List<Rdn> rdns = subjectDN.getRdns(); 261 for (int i = rdns.size() - 1; i >= 0; i--) { 262 final Rdn rds = rdns.get(i); 263 final Attributes attributes = rds.toAttributes(); 264 final Attribute cn = attributes.get("cn"); 265 if (cn != null) { 266 try { 267 final Object value = cn.get(); 268 if (value != null) { 269 return value.toString(); 270 } 271 } catch (final NoSuchElementException ignore) { 272 // ignore exception 273 } catch (final NamingException ignore) { 274 // ignore exception 275 } 276 } 277 } 278 return null; 279 } catch (final InvalidNameException e) { 280 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name"); 281 } 282 } 283 284 static HostNameType determineHostFormat(final String host) { 285 if (InetAddressUtils.isIPv4Address(host)) { 286 return HostNameType.IPv4; 287 } else { 288 String s = host; 289 if (s.startsWith("[") && s.endsWith("]")) { 290 s = host.substring(1, host.length() - 1); 291 } 292 if (InetAddressUtils.isIPv6Address(s)) { 293 return HostNameType.IPv6; 294 } 295 } 296 return HostNameType.DNS; 297 } 298 299 static List<SubjectName> getSubjectAltNames(final X509Certificate cert) { 300 try { 301 final Collection<List<?>> entries = cert.getSubjectAlternativeNames(); 302 if (entries == null) { 303 return Collections.emptyList(); 304 } 305 final List<SubjectName> result = new ArrayList<SubjectName>(); 306 for (List<?> entry: entries) { 307 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null; 308 if (type != null) { 309 final String s = (String) entry.get(1); 310 result.add(new SubjectName(s, type)); 311 } 312 } 313 return result; 314 } catch (final CertificateParsingException ignore) { 315 return Collections.emptyList(); 316 } 317 } 318 319 /* 320 * Normalize IPv6 or DNS name. 321 */ 322 static String normaliseAddress(final String hostname) { 323 if (hostname == null) { 324 return hostname; 325 } 326 try { 327 final InetAddress inetAddress = InetAddress.getByName(hostname); 328 return inetAddress.getHostAddress(); 329 } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above 330 return hostname; 331 } 332 } 333}