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