001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, software 013 * distributed under the License is distributed on an "AS IS" BASIS, 014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 015 * See the License for the specific language governing permissions and 016 * limitations under the License. 017 */ 018package org.apache.hadoop.hbase.io.crypto.tls; 019 020import java.net.InetAddress; 021import java.security.cert.Certificate; 022import java.security.cert.CertificateParsingException; 023import java.security.cert.X509Certificate; 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.List; 028import java.util.Locale; 029import java.util.NoSuchElementException; 030import java.util.Objects; 031import java.util.Optional; 032import javax.naming.InvalidNameException; 033import javax.naming.NamingException; 034import javax.naming.directory.Attribute; 035import javax.naming.directory.Attributes; 036import javax.naming.ldap.LdapName; 037import javax.naming.ldap.Rdn; 038import javax.net.ssl.HostnameVerifier; 039import javax.net.ssl.SSLException; 040import javax.net.ssl.SSLPeerUnverifiedException; 041import javax.net.ssl.SSLSession; 042import javax.security.auth.x500.X500Principal; 043import org.apache.yetus.audience.InterfaceAudience; 044import org.slf4j.Logger; 045import org.slf4j.LoggerFactory; 046 047import org.apache.hbase.thirdparty.com.google.common.net.InetAddresses; 048 049/** 050 * When enabled in {@link X509Util}, handles verifying that the hostname of a peer matches the 051 * certificate it presents. 052 * <p/> 053 * This file has been copied from the Apache ZooKeeper project. 054 * @see <a href= 055 * "https://github.com/apache/zookeeper/blob/5820d10d9dc58c8e12d2e25386fdf92acb360359/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKHostnameVerifier.java">Base 056 * revision</a> 057 */ 058@InterfaceAudience.Private 059final class HBaseHostnameVerifier implements HostnameVerifier { 060 061 private static final Logger LOG = LoggerFactory.getLogger(HBaseHostnameVerifier.class); 062 063 /** 064 * Note: copied from Apache httpclient with some minor modifications. We want host verification, 065 * but depending on the httpclient jar caused unexplained performance regressions (even when the 066 * code was not used). 067 */ 068 private static final class SubjectName { 069 070 static final int DNS = 2; 071 static final int IP = 7; 072 073 private final String value; 074 private final int type; 075 076 SubjectName(final String value, final int type) { 077 if (type != DNS && type != IP) { 078 throw new IllegalArgumentException("Invalid type: " + type); 079 } 080 this.value = Objects.requireNonNull(value); 081 this.type = type; 082 } 083 084 public int getType() { 085 return type; 086 } 087 088 public String getValue() { 089 return value; 090 } 091 092 @Override 093 public String toString() { 094 return value; 095 } 096 097 } 098 099 @Override 100 public boolean verify(final String host, final SSLSession session) { 101 try { 102 final Certificate[] certs = session.getPeerCertificates(); 103 final X509Certificate x509 = (X509Certificate) certs[0]; 104 verify(host, x509); 105 return true; 106 } catch (final SSLException ex) { 107 LOG.debug("Unexpected exception", ex); 108 return false; 109 } 110 } 111 112 void verify(final String host, final X509Certificate cert) throws SSLException { 113 final List<SubjectName> subjectAlts = getSubjectAltNames(cert); 114 if (subjectAlts != null && !subjectAlts.isEmpty()) { 115 Optional<InetAddress> inetAddress = parseIpAddress(host); 116 if (inetAddress.isPresent()) { 117 matchIPAddress(host, inetAddress.get(), subjectAlts); 118 } else { 119 matchDNSName(host, subjectAlts); 120 } 121 } else { 122 // CN matching has been deprecated by rfc2818 and can be used 123 // as fallback only when no subjectAlts are available 124 final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 125 final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); 126 if (cn == null) { 127 throw new SSLException("Certificate subject for <" + host + "> doesn't contain " 128 + "a common name and does not have alternative names"); 129 } 130 matchCN(host, cn); 131 } 132 } 133 134 private static void matchIPAddress(final String host, final InetAddress inetAddress, 135 final List<SubjectName> subjectAlts) throws SSLException { 136 for (final SubjectName subjectAlt : subjectAlts) { 137 if (subjectAlt.getType() == SubjectName.IP) { 138 Optional<InetAddress> parsed = parseIpAddress(subjectAlt.getValue()); 139 if (parsed.filter(altAddr -> altAddr.equals(inetAddress)).isPresent()) { 140 return; 141 } 142 } 143 } 144 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " 145 + "of the subject alternative names: " + subjectAlts); 146 } 147 148 private static void matchDNSName(final String host, final List<SubjectName> subjectAlts) 149 throws SSLException { 150 final String normalizedHost = host.toLowerCase(Locale.ROOT); 151 for (final SubjectName subjectAlt : subjectAlts) { 152 if (subjectAlt.getType() == SubjectName.DNS) { 153 final String normalizedSubjectAlt = subjectAlt.getValue().toLowerCase(Locale.ROOT); 154 if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt)) { 155 return; 156 } 157 } 158 } 159 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " 160 + "of the subject alternative names: " + subjectAlts); 161 } 162 163 private static void matchCN(final String host, final String cn) throws SSLException { 164 final String normalizedHost = host.toLowerCase(Locale.ROOT); 165 final String normalizedCn = cn.toLowerCase(Locale.ROOT); 166 if (!matchIdentityStrict(normalizedHost, normalizedCn)) { 167 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " 168 + "common name of the certificate subject: " + cn); 169 } 170 } 171 172 private static boolean matchIdentity(final String host, final String identity, 173 final boolean strict) { 174 // RFC 2818, 3.1. Server Identity 175 // "...Names may contain the wildcard 176 // character * which is considered to match any single domain name 177 // component or component fragment..." 178 // Based on this statement presuming only singular wildcard is legal 179 final int asteriskIdx = identity.indexOf('*'); 180 if (asteriskIdx != -1) { 181 final String prefix = identity.substring(0, asteriskIdx); 182 final String suffix = identity.substring(asteriskIdx + 1); 183 if (!prefix.isEmpty() && !host.startsWith(prefix)) { 184 return false; 185 } 186 if (!suffix.isEmpty() && !host.endsWith(suffix)) { 187 return false; 188 } 189 // Additional sanity checks on content selected by wildcard can be done here 190 if (strict) { 191 final String remainder = host.substring(prefix.length(), host.length() - suffix.length()); 192 return !remainder.contains("."); 193 } 194 return true; 195 } 196 return host.equalsIgnoreCase(identity); 197 } 198 199 private static boolean matchIdentityStrict(final String host, final String identity) { 200 return matchIdentity(host, identity, true); 201 } 202 203 private static String extractCN(final String subjectPrincipal) throws SSLException { 204 if (subjectPrincipal == null) { 205 return null; 206 } 207 try { 208 final LdapName subjectDN = new LdapName(subjectPrincipal); 209 final List<Rdn> rdns = subjectDN.getRdns(); 210 for (int i = rdns.size() - 1; i >= 0; i--) { 211 final Rdn rds = rdns.get(i); 212 final Attributes attributes = rds.toAttributes(); 213 final Attribute cn = attributes.get("cn"); 214 if (cn != null) { 215 try { 216 final Object value = cn.get(); 217 if (value != null) { 218 return value.toString(); 219 } 220 } catch (final NoSuchElementException ignore) { 221 // ignore exception 222 } catch (final NamingException ignore) { 223 // ignore exception 224 } 225 } 226 } 227 return null; 228 } catch (final InvalidNameException e) { 229 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name"); 230 } 231 } 232 233 private static Optional<InetAddress> parseIpAddress(String host) { 234 host = host.trim(); 235 // Uri strings only work for ipv6 and are wrapped with brackets 236 // Unfortunately InetAddresses can't handle a mixed input, so we 237 // check here and choose which parse method to use. 238 if (host.startsWith("[") && host.endsWith("]")) { 239 return parseIpAddressUriString(host); 240 } else { 241 return parseIpAddressString(host); 242 } 243 } 244 245 private static Optional<InetAddress> parseIpAddressUriString(String host) { 246 if (InetAddresses.isUriInetAddress(host)) { 247 return Optional.of(InetAddresses.forUriString(host)); 248 } 249 return Optional.empty(); 250 } 251 252 private static Optional<InetAddress> parseIpAddressString(String host) { 253 if (InetAddresses.isInetAddress(host)) { 254 return Optional.of(InetAddresses.forString(host)); 255 } 256 return Optional.empty(); 257 } 258 259 @SuppressWarnings("MixedMutabilityReturnType") 260 private static List<SubjectName> getSubjectAltNames(final X509Certificate cert) { 261 try { 262 final Collection<List<?>> entries = cert.getSubjectAlternativeNames(); 263 if (entries == null) { 264 return Collections.emptyList(); 265 } 266 final List<SubjectName> result = new ArrayList<SubjectName>(); 267 for (List<?> entry : entries) { 268 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null; 269 if (type != null) { 270 if (type == SubjectName.DNS || type == SubjectName.IP) { 271 final Object o = entry.get(1); 272 if (o instanceof String) { 273 result.add(new SubjectName((String) o, type)); 274 } else { 275 LOG.debug("non-string Subject Alt Name type detected, not currently supported: {}", 276 o); 277 } 278 } 279 } 280 } 281 return result; 282 } catch (final CertificateParsingException ignore) { 283 return Collections.emptyList(); 284 } 285 } 286}