001/* 002 * Copyright 2014-2017 UnboundID Corp. 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright (C) 2014-2017 UnboundID Corp. 007 * 008 * This program is free software; you can redistribute it and/or modify 009 * it under the terms of the GNU General Public License (GPLv2 only) 010 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 011 * as published by the Free Software Foundation. 012 * 013 * This program is distributed in the hope that it will be useful, 014 * but WITHOUT ANY WARRANTY; without even the implied warranty of 015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 016 * GNU General Public License for more details. 017 * 018 * You should have received a copy of the GNU General Public License 019 * along with this program; if not, see <http://www.gnu.org/licenses>. 020 */ 021package com.unboundid.util.ssl; 022 023 024 025import java.net.InetAddress; 026import java.net.URI; 027import java.util.Collection; 028import java.util.List; 029import java.security.cert.Certificate; 030import java.security.cert.X509Certificate; 031import javax.net.ssl.SSLSession; 032import javax.net.ssl.SSLSocket; 033import javax.security.auth.x500.X500Principal; 034 035import com.unboundid.ldap.sdk.DN; 036import com.unboundid.ldap.sdk.LDAPException; 037import com.unboundid.ldap.sdk.RDN; 038import com.unboundid.ldap.sdk.ResultCode; 039import com.unboundid.util.Debug; 040import com.unboundid.util.NotMutable; 041import com.unboundid.util.StaticUtils; 042import com.unboundid.util.ThreadSafety; 043import com.unboundid.util.ThreadSafetyLevel; 044 045import static com.unboundid.util.ssl.SSLMessages.*; 046 047 048 049/** 050 * This class provides an implementation of an {@code SSLSocket} verifier that 051 * will verify that the presented server certificate includes the address to 052 * which the client intended to establish a connection. It will check the CN 053 * attribute of the certificate subject, as well as certain subjectAltName 054 * extensions, including dNSName, uniformResourceIdentifier, and iPAddress. 055 */ 056@NotMutable() 057@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE) 058public final class HostNameSSLSocketVerifier 059 extends SSLSocketVerifier 060{ 061 // Indicates whether to allow wildcard certificates which contain an asterisk 062 // as the first component of a CN subject attribute or dNSName subjectAltName 063 // extension. 064 private final boolean allowWildcards; 065 066 067 068 /** 069 * Creates a new instance of this {@code SSLSocket} verifier. 070 * 071 * @param allowWildcards Indicates whether to allow wildcard certificates 072 * which contain an asterisk as the first component of 073 * a CN subject attribute or dNSName subjectAltName 074 * extension. 075 */ 076 public HostNameSSLSocketVerifier(final boolean allowWildcards) 077 { 078 this.allowWildcards = allowWildcards; 079 } 080 081 082 083 /** 084 * Verifies that the provided {@code SSLSocket} is acceptable and the 085 * connection should be allowed to remain established. 086 * 087 * @param host The address to which the client intended the connection 088 * to be established. 089 * @param port The port to which the client intended the connection to 090 * be established. 091 * @param sslSocket The {@code SSLSocket} that should be verified. 092 * 093 * @throws LDAPException If a problem is identified that should prevent the 094 * provided {@code SSLSocket} from remaining 095 * established. 096 */ 097 @Override() 098 public void verifySSLSocket(final String host, final int port, 099 final SSLSocket sslSocket) 100 throws LDAPException 101 { 102 try 103 { 104 // Get the certificates presented during negotiation. The certificates 105 // will be ordered so that the server certificate comes first. 106 final SSLSession sslSession = sslSocket.getSession(); 107 if (sslSession == null) 108 { 109 throw new LDAPException(ResultCode.CONNECT_ERROR, 110 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_SESSION.get(host, port)); 111 } 112 113 final Certificate[] peerCertificates = sslSession.getPeerCertificates(); 114 if ((peerCertificates == null) || (peerCertificates.length == 0)) 115 { 116 throw new LDAPException(ResultCode.CONNECT_ERROR, 117 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_NO_PEER_CERTS.get(host, port)); 118 } 119 120 if (peerCertificates[0] instanceof X509Certificate) 121 { 122 final StringBuilder certInfo = new StringBuilder(); 123 if (! certificateIncludesHostname(host, 124 (X509Certificate) peerCertificates[0], allowWildcards, certInfo)) 125 { 126 throw new LDAPException(ResultCode.CONNECT_ERROR, 127 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_HOSTNAME_NOT_FOUND.get(host, 128 certInfo.toString())); 129 } 130 } 131 else 132 { 133 throw new LDAPException(ResultCode.CONNECT_ERROR, 134 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_PEER_NOT_X509.get(host, port, 135 peerCertificates[0].getType())); 136 } 137 } 138 catch (final LDAPException le) 139 { 140 Debug.debugException(le); 141 throw le; 142 } 143 catch (final Exception e) 144 { 145 Debug.debugException(e); 146 throw new LDAPException(ResultCode.CONNECT_ERROR, 147 ERR_HOST_NAME_SSL_SOCKET_VERIFIER_EXCEPTION.get(host, port, 148 StaticUtils.getExceptionMessage(e)), 149 e); 150 } 151 } 152 153 154 155 /** 156 * Determines whether the provided certificate contains the specified 157 * hostname. 158 * 159 * @param host The address expected to be found in the provided 160 * certificate. 161 * @param certificate The peer certificate to be validated. 162 * @param allowWildcards Indicates whether to allow wildcard certificates 163 * which contain an asterisk as the first component of 164 * a CN subject attribute or dNSName subjectAltName 165 * extension. 166 * @param certInfo A buffer into which information will be provided 167 * about the provided certificate. 168 * 169 * @return {@code true} if the expected hostname was found in the 170 * certificate, or {@code false} if not. 171 */ 172 static boolean certificateIncludesHostname(final String host, 173 final X509Certificate certificate, 174 final boolean allowWildcards, 175 final StringBuilder certInfo) 176 { 177 final String lowerHost = StaticUtils.toLowerCase(host); 178 179 // First, check the CN from the certificate subject. 180 final String subjectDN = 181 certificate.getSubjectX500Principal().getName(X500Principal.RFC2253); 182 certInfo.append("subject='"); 183 certInfo.append(subjectDN); 184 certInfo.append('\''); 185 186 try 187 { 188 final DN dn = new DN(subjectDN); 189 for (final RDN rdn : dn.getRDNs()) 190 { 191 final String[] names = rdn.getAttributeNames(); 192 final String[] values = rdn.getAttributeValues(); 193 for (int i=0; i < names.length; i++) 194 { 195 final String lowerName = StaticUtils.toLowerCase(names[i]); 196 if (lowerName.equals("cn") || lowerName.equals("commonname") || 197 lowerName.equals("2.5.4.3")) 198 { 199 final String lowerValue = StaticUtils.toLowerCase(values[i]); 200 if (lowerHost.equals(lowerValue)) 201 { 202 return true; 203 } 204 205 if (allowWildcards && lowerValue.startsWith("*.")) 206 { 207 final String withoutWildcard = lowerValue.substring(1); 208 if (lowerHost.endsWith(withoutWildcard)) 209 { 210 return true; 211 } 212 } 213 } 214 } 215 } 216 } 217 catch (final Exception e) 218 { 219 // This shouldn't happen for a well-formed certificate subject, but we 220 // have to handle it anyway. 221 Debug.debugException(e); 222 } 223 224 225 // Next, check any supported subjectAltName extension values. 226 final Collection<List<?>> subjectAltNames; 227 try 228 { 229 subjectAltNames = certificate.getSubjectAlternativeNames(); 230 } 231 catch (final Exception e) 232 { 233 Debug.debugException(e); 234 return false; 235 } 236 237 if (subjectAltNames != null) 238 { 239 for (final List<?> l : subjectAltNames) 240 { 241 try 242 { 243 final Integer type = (Integer) l.get(0); 244 switch (type) 245 { 246 case 2: // dNSName 247 final String dnsName = (String) l.get(1); 248 certInfo.append(" dNSName='"); 249 certInfo.append(dnsName); 250 certInfo.append('\''); 251 252 final String lowerDNSName = StaticUtils.toLowerCase(dnsName); 253 if (lowerHost.equals(lowerDNSName)) 254 { 255 return true; 256 } 257 258 // If the given DNS name starts with a "*.", then it's a wildcard 259 // certificate. See if that's allowed, and if so whether it 260 // matches any acceptable name. 261 if (allowWildcards && lowerDNSName.startsWith("*.")) 262 { 263 final String withoutWildcard = lowerDNSName.substring(1); 264 if (lowerHost.endsWith(withoutWildcard)) 265 { 266 return true; 267 } 268 } 269 break; 270 271 case 6: // uniformResourceIdentifier 272 final String uriString = (String) l.get(1); 273 certInfo.append(" uniformResourceIdentifier='"); 274 certInfo.append(uriString); 275 certInfo.append('\''); 276 277 final URI uri = new URI(uriString); 278 if (lowerHost.equals(StaticUtils.toLowerCase(uri.getHost()))) 279 { 280 return true; 281 } 282 break; 283 284 case 7: // iPAddress 285 final String ipAddressString = (String) l.get(1); 286 certInfo.append(" iPAddress='"); 287 certInfo.append(ipAddressString); 288 certInfo.append('\''); 289 290 final InetAddress inetAddress = 291 InetAddress.getByName(ipAddressString); 292 if (Character.isDigit(host.charAt(0)) || (host.indexOf(':') >= 0)) 293 { 294 final InetAddress a = InetAddress.getByName(host); 295 if (inetAddress.equals(a)) 296 { 297 return true; 298 } 299 } 300 break; 301 302 case 0: // otherName 303 case 1: // rfc822Name 304 case 3: // x400Address 305 case 4: // directoryName 306 case 5: // ediPartyName 307 case 8: // registeredID 308 default: 309 // We won't do any checking for any of these formats. 310 break; 311 } 312 } 313 catch (final Exception e) 314 { 315 Debug.debugException(e); 316 } 317 } 318 } 319 320 return false; 321 } 322}