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