001package lucee.runtime.net.http.sni; 002 003import java.net.InetAddress; 004import java.net.UnknownHostException; 005import java.security.cert.Certificate; 006import java.security.cert.CertificateParsingException; 007import java.security.cert.X509Certificate; 008import java.util.ArrayList; 009import java.util.Collection; 010import java.util.List; 011import java.util.Locale; 012import java.util.NoSuchElementException; 013 014import javax.naming.InvalidNameException; 015import javax.naming.NamingException; 016import javax.naming.directory.Attribute; 017import javax.naming.directory.Attributes; 018import javax.naming.ldap.LdapName; 019import javax.naming.ldap.Rdn; 020import javax.net.ssl.HostnameVerifier; 021import javax.net.ssl.SSLException; 022import javax.net.ssl.SSLSession; 023import javax.security.auth.x500.X500Principal; 024 025import org.apache.commons.logging.Log; 026import org.apache.commons.logging.LogFactory; 027import org.apache.http.annotation.Immutable; 028import org.apache.http.conn.util.DomainType; 029import org.apache.http.conn.util.InetAddressUtils; 030import org.apache.http.conn.util.PublicSuffixMatcher; 031 032 033public class AbsDefaultHostnameVerifier implements HostnameVerifier { 034 035 final static int DNS_NAME_TYPE = 2; 036 final static int IP_ADDRESS_TYPE = 7; 037 038 private final Log log = LogFactory.getLog(getClass()); 039 040 private final PublicSuffixMatcher publicSuffixMatcher; 041 042 public AbsDefaultHostnameVerifier(final PublicSuffixMatcher publicSuffixMatcher) { 043 this.publicSuffixMatcher = publicSuffixMatcher; 044 } 045 046 public AbsDefaultHostnameVerifier() { 047 this(null); 048 } 049 050 @Override 051 public boolean verify(final String host, final SSLSession session) { 052 try { 053 final Certificate[] certs = session.getPeerCertificates(); 054 final X509Certificate x509 = (X509Certificate) certs[0]; 055 verify(host, x509); 056 return true; 057 } catch (final SSLException ex) { 058 if (log.isDebugEnabled()) { 059 log.debug(ex.getMessage(), ex); 060 } 061 return false; 062 } 063 } 064 065 public void verify( 066 final String host, final X509Certificate cert) throws SSLException { 067 final boolean ipv4 = InetAddressUtils.isIPv4Address(host); 068 final boolean ipv6 = InetAddressUtils.isIPv6Address(host); 069 final int subjectType = ipv4 || ipv6 ? IP_ADDRESS_TYPE : DNS_NAME_TYPE; 070 final List<String> subjectAlts = extractSubjectAlts(cert, subjectType); 071 if (subjectAlts != null && !subjectAlts.isEmpty()) { 072 if (ipv4) { 073 matchIPAddress(host, subjectAlts); 074 } else if (ipv6) { 075 matchIPv6Address(host, subjectAlts); 076 } else { 077 matchDNSName(host, subjectAlts, this.publicSuffixMatcher); 078 } 079 } else { 080 // CN matching has been deprecated by rfc2818 and can be used 081 // as fallback only when no subjectAlts are available 082 final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 083 final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); 084 if (cn == null) { 085 throw new SSLException("Certificate subject for <" + host + "> doesn't contain " + 086 "a common name and does not have alternative names"); 087 } 088 matchCN(host, cn, this.publicSuffixMatcher); 089 } 090 } 091 092 static void matchIPAddress(final String host, final List<String> subjectAlts) throws SSLException { 093 for (int i = 0; i < subjectAlts.size(); i++) { 094 final String subjectAlt = subjectAlts.get(i); 095 if (host.equals(subjectAlt)) { 096 return; 097 } 098 } 099 throw new SSLException("Certificate for <" + host + "> doesn't match any " + 100 "of the subject alternative names: " + subjectAlts); 101 } 102 103 static void matchIPv6Address(final String host, final List<String> subjectAlts) throws SSLException { 104 final String normalisedHost = normaliseAddress(host); 105 for (int i = 0; i < subjectAlts.size(); i++) { 106 final String subjectAlt = subjectAlts.get(i); 107 final String normalizedSubjectAlt = normaliseAddress(subjectAlt); 108 if (normalisedHost.equals(normalizedSubjectAlt)) { 109 return; 110 } 111 } 112 throw new SSLException("Certificate for <" + host + "> doesn't match any " + 113 "of the subject alternative names: " + subjectAlts); 114 } 115 116 static void matchDNSName(final String host, final List<String> subjectAlts, 117 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException { 118 final String normalizedHost = host.toLowerCase(Locale.ROOT); 119 for (int i = 0; i < subjectAlts.size(); i++) { 120 final String subjectAlt = subjectAlts.get(i); 121 final String normalizedSubjectAlt = subjectAlt.toLowerCase(Locale.ROOT); 122 if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt, publicSuffixMatcher)) { 123 return; 124 } 125 } 126 throw new SSLException("Certificate for <" + host + "> doesn't match any " + 127 "of the subject alternative names: " + subjectAlts); 128 } 129 130 static void matchCN(final String host, final String cn, 131 final PublicSuffixMatcher publicSuffixMatcher) throws SSLException { 132 if (!matchIdentityStrict(host, cn, publicSuffixMatcher)) { 133 throw new SSLException("Certificate for <" + host + "> doesn't match " + 134 "common name of the certificate subject: " + cn); 135 } 136 } 137 138 static boolean matchDomainRoot(final String host, final String domainRoot) { 139 if (domainRoot == null) { 140 return false; 141 } 142 return host.endsWith(domainRoot) && (host.length() == domainRoot.length() 143 || host.charAt(host.length() - domainRoot.length() - 1) == '.'); 144 } 145 146 private static boolean matchIdentity(final String host, final String identity, 147 final PublicSuffixMatcher publicSuffixMatcher, 148 final boolean strict) { 149 if (publicSuffixMatcher != null && host.contains(".")) { 150 if (!matchDomainRoot(host, publicSuffixMatcher.getDomainRoot(identity, DomainType.ICANN))) { 151 return false; 152 } 153 } 154 155 // RFC 2818, 3.1. Server Identity 156 // "...Names may contain the wildcard 157 // character * which is considered to match any single domain name 158 // component or component fragment..." 159 // Based on this statement presuming only singular wildcard is legal 160 final int asteriskIdx = identity.indexOf('*'); 161 if (asteriskIdx != -1) { 162 final String prefix = identity.substring(0, asteriskIdx); 163 final String suffix = identity.substring(asteriskIdx + 1); 164 if (!prefix.isEmpty() && !host.startsWith(prefix)) { 165 return false; 166 } 167 if (!suffix.isEmpty() && !host.endsWith(suffix)) { 168 return false; 169 } 170 // Additional sanity checks on content selected by wildcard can be done here 171 if (strict) { 172 final String remainder = host.substring( 173 prefix.length(), host.length() - suffix.length()); 174 if (remainder.contains(".")) { 175 return false; 176 } 177 } 178 return true; 179 } 180 return host.equalsIgnoreCase(identity); 181 } 182 183 static boolean matchIdentity(final String host, final String identity, 184 final PublicSuffixMatcher publicSuffixMatcher) { 185 return matchIdentity(host, identity, publicSuffixMatcher, false); 186 } 187 188 static boolean matchIdentity(final String host, final String identity) { 189 return matchIdentity(host, identity, null, false); 190 } 191 192 static boolean matchIdentityStrict(final String host, final String identity, 193 final PublicSuffixMatcher publicSuffixMatcher) { 194 return matchIdentity(host, identity, publicSuffixMatcher, true); 195 } 196 197 static boolean matchIdentityStrict(final String host, final String identity) { 198 return matchIdentity(host, identity, null, true); 199 } 200 201 static String extractCN(final String subjectPrincipal) throws SSLException { 202 if (subjectPrincipal == null) { 203 return null; 204 } 205 try { 206 final LdapName subjectDN = new LdapName(subjectPrincipal); 207 final List<Rdn> rdns = subjectDN.getRdns(); 208 for (int i = rdns.size() - 1; i >= 0; i--) { 209 final Rdn rds = rdns.get(i); 210 final Attributes attributes = rds.toAttributes(); 211 final Attribute cn = attributes.get("cn"); 212 if (cn != null) { 213 try { 214 final Object value = cn.get(); 215 if (value != null) { 216 return value.toString(); 217 } 218 } catch (NoSuchElementException ignore) { 219 } catch (NamingException ignore) { 220 } 221 } 222 } 223 return null; 224 } catch (InvalidNameException e) { 225 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name"); 226 } 227 } 228 229 static List<String> extractSubjectAlts(final X509Certificate cert, final int subjectType) { 230 Collection<List<?>> c = null; 231 try { 232 c = cert.getSubjectAlternativeNames(); 233 } catch(final CertificateParsingException ignore) { 234 } 235 List<String> subjectAltList = null; 236 if (c != null) { 237 for (final List<?> aC : c) { 238 final List<?> list = aC; 239 final int type = ((Integer) list.get(0)).intValue(); 240 if (type == subjectType) { 241 final String s = (String) list.get(1); 242 if (subjectAltList == null) { 243 subjectAltList = new ArrayList<String>(); 244 } 245 subjectAltList.add(s); 246 } 247 } 248 } 249 return subjectAltList; 250 } 251 252 /* 253 * Normalize IPv6 or DNS name. 254 */ 255 static String normaliseAddress(final String hostname) { 256 if (hostname == null) { 257 return hostname; 258 } 259 try { 260 final InetAddress inetAddress = InetAddress.getByName(hostname); 261 return inetAddress.getHostAddress(); 262 } catch (final UnknownHostException unexpected) { // Should not happen, because we check for IPv6 address above 263 return hostname; 264 } 265 } 266}