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.security.cert.Certificate; 021import java.security.cert.CertificateParsingException; 022import java.security.cert.X509Certificate; 023import java.util.ArrayList; 024import java.util.Collection; 025import java.util.Collections; 026import java.util.List; 027import java.util.Locale; 028import java.util.NoSuchElementException; 029import java.util.Objects; 030import javax.naming.InvalidNameException; 031import javax.naming.NamingException; 032import javax.naming.directory.Attribute; 033import javax.naming.directory.Attributes; 034import javax.naming.ldap.LdapName; 035import javax.naming.ldap.Rdn; 036import javax.net.ssl.HostnameVerifier; 037import javax.net.ssl.SSLException; 038import javax.net.ssl.SSLPeerUnverifiedException; 039import javax.net.ssl.SSLSession; 040import javax.security.auth.x500.X500Principal; 041import org.apache.hadoop.hbase.util.Strings; 042import org.apache.yetus.audience.InterfaceAudience; 043import org.slf4j.Logger; 044import org.slf4j.LoggerFactory; 045 046/** 047 * When enabled in {@link X509Util}, handles verifying that the hostname of a peer matches the 048 * certificate it presents. 049 * <p/> 050 * This file has been copied from the Apache ZooKeeper project. 051 * @see <a href= 052 * "https://github.com/apache/zookeeper/blob/5820d10d9dc58c8e12d2e25386fdf92acb360359/zookeeper-server/src/main/java/org/apache/zookeeper/common/ZKHostnameVerifier.java">Base 053 * revision</a> 054 */ 055@InterfaceAudience.Private 056final class HBaseHostnameVerifier implements HostnameVerifier { 057 058 private static final Logger LOG = LoggerFactory.getLogger(HBaseHostnameVerifier.class); 059 060 /** 061 * Note: copied from Apache httpclient with some minor modifications. We want host verification, 062 * but depending on the httpclient jar caused unexplained performance regressions (even when the 063 * code was not used). 064 */ 065 private static final class SubjectName { 066 067 static final int DNS = 2; 068 static final int IP = 7; 069 070 private final String value; 071 private final int type; 072 073 SubjectName(final String value, final int type) { 074 if (type != DNS && type != IP) { 075 throw new IllegalArgumentException("Invalid type: " + type); 076 } 077 this.value = Objects.requireNonNull(value); 078 this.type = type; 079 } 080 081 public int getType() { 082 return type; 083 } 084 085 public String getValue() { 086 return value; 087 } 088 089 @Override 090 public String toString() { 091 return value; 092 } 093 094 } 095 096 @Override 097 public boolean verify(final String host, final SSLSession session) { 098 try { 099 final Certificate[] certs = session.getPeerCertificates(); 100 final X509Certificate x509 = (X509Certificate) certs[0]; 101 verify(host, x509); 102 return true; 103 } catch (final SSLException ex) { 104 LOG.debug("Unexpected exception", ex); 105 return false; 106 } 107 } 108 109 void verify(final String host, final X509Certificate cert) throws SSLException { 110 final List<SubjectName> subjectAlts = getSubjectAltNames(cert); 111 if (subjectAlts != null && !subjectAlts.isEmpty()) { 112 if (Strings.isInetAddress(host)) { 113 matchIPAddress(host, subjectAlts); 114 } else { 115 matchDNSName(host, subjectAlts); 116 } 117 } else { 118 // CN matching has been deprecated by rfc2818 and can be used 119 // as fallback only when no subjectAlts are available 120 final X500Principal subjectPrincipal = cert.getSubjectX500Principal(); 121 final String cn = extractCN(subjectPrincipal.getName(X500Principal.RFC2253)); 122 if (cn == null) { 123 throw new SSLException("Certificate subject for <" + host + "> doesn't contain " 124 + "a common name and does not have alternative names"); 125 } 126 matchCN(host, cn); 127 } 128 } 129 130 private static void matchIPAddress(final String host, final List<SubjectName> subjectAlts) 131 throws SSLException { 132 for (final SubjectName subjectAlt : subjectAlts) { 133 if ( 134 subjectAlt.getType() == SubjectName.IP 135 && Strings.hostnamesEqual(host, subjectAlt.getValue()) 136 ) { 137 return; 138 } 139 } 140 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " 141 + "of the subject alternative names: " + subjectAlts); 142 } 143 144 private static void matchDNSName(final String host, final List<SubjectName> subjectAlts) 145 throws SSLException { 146 final String normalizedHost = host.toLowerCase(Locale.ROOT); 147 for (final SubjectName subjectAlt : subjectAlts) { 148 if (subjectAlt.getType() == SubjectName.DNS) { 149 final String normalizedSubjectAlt = subjectAlt.getValue().toLowerCase(Locale.ROOT); 150 if (matchIdentityStrict(normalizedHost, normalizedSubjectAlt)) { 151 return; 152 } 153 } 154 } 155 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match any " 156 + "of the subject alternative names: " + subjectAlts); 157 } 158 159 private static void matchCN(final String host, final String cn) throws SSLException { 160 final String normalizedHost = host.toLowerCase(Locale.ROOT); 161 final String normalizedCn = cn.toLowerCase(Locale.ROOT); 162 if (!matchIdentityStrict(normalizedHost, normalizedCn)) { 163 throw new SSLPeerUnverifiedException("Certificate for <" + host + "> doesn't match " 164 + "common name of the certificate subject: " + cn); 165 } 166 } 167 168 private static boolean matchIdentity(final String host, final String identity, 169 final boolean strict) { 170 // RFC 2818, 3.1. Server Identity 171 // "...Names may contain the wildcard 172 // character * which is considered to match any single domain name 173 // component or component fragment..." 174 // Based on this statement presuming only singular wildcard is legal 175 final int asteriskIdx = identity.indexOf('*'); 176 if (asteriskIdx != -1) { 177 final String prefix = identity.substring(0, asteriskIdx); 178 final String suffix = identity.substring(asteriskIdx + 1); 179 if (!prefix.isEmpty() && !host.startsWith(prefix)) { 180 return false; 181 } 182 if (!suffix.isEmpty() && !host.endsWith(suffix)) { 183 return false; 184 } 185 // Additional sanity checks on content selected by wildcard can be done here 186 if (strict) { 187 final String remainder = host.substring(prefix.length(), host.length() - suffix.length()); 188 return !remainder.contains("."); 189 } 190 return true; 191 } 192 return host.equalsIgnoreCase(identity); 193 } 194 195 private static boolean matchIdentityStrict(final String host, final String identity) { 196 return matchIdentity(host, identity, true); 197 } 198 199 private static String extractCN(final String subjectPrincipal) throws SSLException { 200 if (subjectPrincipal == null) { 201 return null; 202 } 203 try { 204 final LdapName subjectDN = new LdapName(subjectPrincipal); 205 final List<Rdn> rdns = subjectDN.getRdns(); 206 for (int i = rdns.size() - 1; i >= 0; i--) { 207 final Rdn rds = rdns.get(i); 208 final Attributes attributes = rds.toAttributes(); 209 final Attribute cn = attributes.get("cn"); 210 if (cn != null) { 211 try { 212 final Object value = cn.get(); 213 if (value != null) { 214 return value.toString(); 215 } 216 } catch (final NoSuchElementException ignore) { 217 // ignore exception 218 } catch (final NamingException ignore) { 219 // ignore exception 220 } 221 } 222 } 223 return null; 224 } catch (final InvalidNameException e) { 225 throw new SSLException(subjectPrincipal + " is not a valid X500 distinguished name"); 226 } 227 } 228 229 @SuppressWarnings("MixedMutabilityReturnType") 230 private static List<SubjectName> getSubjectAltNames(final X509Certificate cert) { 231 try { 232 final Collection<List<?>> entries = cert.getSubjectAlternativeNames(); 233 if (entries == null) { 234 return Collections.emptyList(); 235 } 236 final List<SubjectName> result = new ArrayList<SubjectName>(); 237 for (List<?> entry : entries) { 238 final Integer type = entry.size() >= 2 ? (Integer) entry.get(0) : null; 239 if (type != null) { 240 if (type == SubjectName.DNS || type == SubjectName.IP) { 241 final Object o = entry.get(1); 242 if (o instanceof String) { 243 result.add(new SubjectName((String) o, type)); 244 } else { 245 LOG.debug("non-string Subject Alt Name type detected, not currently supported: {}", 246 o); 247 } 248 } 249 } 250 } 251 return result; 252 } catch (final CertificateParsingException ignore) { 253 return Collections.emptyList(); 254 } 255 } 256}