1 /* 2 * Copyright (c) 2000, 2013, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.jndi.ldap.ext; 27 28 import java.io.InputStream; 29 import java.io.OutputStream; 30 import java.io.IOException; 31 32 import java.security.Principal; 33 import java.security.cert.X509Certificate; 34 import java.security.cert.CertificateException; 35 36 import javax.net.ssl.SSLSession; 37 import javax.net.ssl.SSLSocket; 38 import javax.net.ssl.SSLSocketFactory; 39 import javax.net.ssl.SSLPeerUnverifiedException; 40 import javax.net.ssl.HostnameVerifier; 41 import sun.security.util.HostnameChecker; 42 43 import javax.naming.ldap.*; 44 import com.sun.jndi.ldap.Connection; 45 46 /** 47 * This class implements the LDAPv3 Extended Response for StartTLS as 48 * defined in 49 * <a href="http://www.ietf.org/rfc/rfc2830.txt">Lightweight Directory 50 * Access Protocol (v3): Extension for Transport Layer Security</a> 51 * 52 * The object identifier for StartTLS is 1.3.6.1.4.1.1466.20037 53 * and no extended response value is defined. 54 * 55 * <p> 56 * The Start TLS extended request and response are used to establish 57 * a TLS connection over the existing LDAP connection associated with 58 * the JNDI context on which {@code extendedOperation()} is invoked. 59 * 60 * @see StartTlsRequest 61 * @author Vincent Ryan 62 */ 63 final public class StartTlsResponseImpl extends StartTlsResponse { 64 65 private static final boolean debug = false; 66 67 /* 68 * The dNSName type in a subjectAltName extension of an X.509 certificate 69 */ 70 private static final int DNSNAME_TYPE = 2; 71 72 /* 73 * The server's hostname. 74 */ 75 private transient String hostname = null; 76 77 /* 78 * The LDAP socket. 79 */ 80 private transient Connection ldapConnection = null; 81 82 /* 83 * The original input stream. 84 */ 85 private transient InputStream originalInputStream = null; 86 87 /* 88 * The original output stream. 89 */ 90 private transient OutputStream originalOutputStream = null; 91 92 /* 93 * The SSL socket. 94 */ 95 private transient SSLSocket sslSocket = null; 96 97 /* 98 * The SSL socket factories. 99 */ 100 private transient SSLSocketFactory defaultFactory = null; 101 private transient SSLSocketFactory currentFactory = null; 102 103 /* 104 * The list of cipher suites to be enabled. 105 */ 106 private transient String[] suites = null; 107 108 /* 109 * The hostname verifier callback. 110 */ 111 private transient HostnameVerifier verifier = null; 112 113 /* 114 * The flag to indicate that the TLS connection is closed. 115 */ 116 private transient boolean isClosed = true; 117 118 private static final long serialVersionUID = -1126624615143411328L; 119 120 // public no-arg constructor required by JDK's Service Provider API. 121 122 public StartTlsResponseImpl() {} 123 124 /** 125 * Overrides the default list of cipher suites enabled for use on the 126 * TLS connection. The cipher suites must have already been listed by 127 * {@code SSLSocketFactory.getSupportedCipherSuites()} as being supported. 128 * Even if a suite has been enabled, it still might not be used because 129 * the peer does not support it, or because the requisite certificates 130 * (and private keys) are not available. 131 * 132 * @param suites The non-null list of names of all the cipher suites to 133 * enable. 134 * @see #negotiate 135 */ 136 public void setEnabledCipherSuites(String[] suites) { 137 // The impl does accept null suites, although the spec requires 138 // a non-null list. 139 this.suites = suites == null ? null : suites.clone(); 140 } 141 142 /** 143 * Overrides the default hostname verifier used by {@code negotiate()} 144 * after the TLS handshake has completed. If 145 * {@code setHostnameVerifier()} has not been called before 146 * {@code negotiate()} is invoked, {@code negotiate()} 147 * will perform a simple case ignore match. If called after 148 * {@code negotiate()}, this method does not do anything. 149 * 150 * @param verifier The non-null hostname verifier callback. 151 * @see #negotiate 152 */ 153 public void setHostnameVerifier(HostnameVerifier verifier) { 154 this.verifier = verifier; 155 } 156 157 /** 158 * Negotiates a TLS session using the default SSL socket factory. 159 * <p> 160 * This method is equivalent to {@code negotiate(null)}. 161 * 162 * @return The negotiated SSL session 163 * @throws IOException If an IO error was encountered while establishing 164 * the TLS session. 165 * @see #setEnabledCipherSuites 166 * @see #setHostnameVerifier 167 */ 168 public SSLSession negotiate() throws IOException { 169 170 return negotiate(null); 171 } 172 173 /** 174 * Negotiates a TLS session using an SSL socket factory. 175 * <p> 176 * Creates an SSL socket using the supplied SSL socket factory and 177 * attaches it to the existing connection. Performs the TLS handshake 178 * and returns the negotiated session information. 179 * <p> 180 * If cipher suites have been set via {@code setEnabledCipherSuites} 181 * then they are enabled before the TLS handshake begins. 182 * <p> 183 * Hostname verification is performed after the TLS handshake completes. 184 * The default check performs a case insensitive match of the server's 185 * hostname against that in the server's certificate. The server's 186 * hostname is extracted from the subjectAltName in the server's 187 * certificate (if present). Otherwise the value of the common name 188 * attribute of the subject name is used. If a callback has 189 * been set via {@code setHostnameVerifier} then that verifier is used if 190 * the default check fails. 191 * <p> 192 * If an error occurs then the SSL socket is closed and an IOException 193 * is thrown. The underlying connection remains intact. 194 * 195 * @param factory The possibly null SSL socket factory to use. 196 * If null, the default SSL socket factory is used. 197 * @return The negotiated SSL session 198 * @throws IOException If an IO error was encountered while establishing 199 * the TLS session. 200 * @see #setEnabledCipherSuites 201 * @see #setHostnameVerifier 202 */ 203 public SSLSession negotiate(SSLSocketFactory factory) throws IOException { 204 205 if (isClosed && sslSocket != null) { 206 throw new IOException("TLS connection is closed."); 207 } 208 209 if (factory == null) { 210 factory = getDefaultFactory(); 211 } 212 213 if (debug) { 214 System.out.println("StartTLS: About to start handshake"); 215 } 216 217 SSLSession sslSession = startHandshake(factory).getSession(); 218 219 if (debug) { 220 System.out.println("StartTLS: Completed handshake"); 221 } 222 223 SSLPeerUnverifiedException verifExcep = null; 224 try { 225 if (verify(hostname, sslSession)) { 226 isClosed = false; 227 return sslSession; 228 } 229 } catch (SSLPeerUnverifiedException e) { 230 // Save to return the cause 231 verifExcep = e; 232 } 233 if ((verifier != null) && 234 verifier.verify(hostname, sslSession)) { 235 isClosed = false; 236 return sslSession; 237 } 238 239 // Verification failed 240 close(); 241 sslSession.invalidate(); 242 if (verifExcep == null) { 243 verifExcep = new SSLPeerUnverifiedException( 244 "hostname of the server '" + hostname + 245 "' does not match the hostname in the " + 246 "server's certificate."); 247 } 248 throw verifExcep; 249 } 250 251 /** 252 * Closes the TLS connection gracefully and reverts back to the underlying 253 * connection. 254 * 255 * @throws IOException If an IO error was encountered while closing the 256 * TLS connection 257 */ 258 public void close() throws IOException { 259 260 if (isClosed) { 261 return; 262 } 263 264 if (debug) { 265 System.out.println("StartTLS: replacing SSL " + 266 "streams with originals"); 267 } 268 269 // Replace SSL streams with the original streams 270 ldapConnection.replaceStreams( 271 originalInputStream, originalOutputStream); 272 273 if (debug) { 274 System.out.println("StartTLS: closing SSL Socket"); 275 } 276 sslSocket.close(); 277 278 isClosed = true; 279 } 280 281 /** 282 * Sets the connection for TLS to use. The TLS connection will be attached 283 * to this connection. 284 * 285 * @param ldapConnection The non-null connection to use. 286 * @param hostname The server's hostname. If null, the hostname used to 287 * open the connection will be used instead. 288 */ 289 public void setConnection(Connection ldapConnection, String hostname) { 290 this.ldapConnection = ldapConnection; 291 this.hostname = (hostname != null) ? hostname : ldapConnection.host; 292 originalInputStream = ldapConnection.inStream; 293 originalOutputStream = ldapConnection.outStream; 294 } 295 296 /* 297 * Returns the default SSL socket factory. 298 * 299 * @return The default SSL socket factory. 300 * @throws IOException If TLS is not supported. 301 */ 302 private SSLSocketFactory getDefaultFactory() throws IOException { 303 304 if (defaultFactory != null) { 305 return defaultFactory; 306 } 307 308 return (defaultFactory = 309 (SSLSocketFactory) SSLSocketFactory.getDefault()); 310 } 311 312 /* 313 * Start the TLS handshake and manipulate the input and output streams. 314 * 315 * @param factory The SSL socket factory to use. 316 * @return The SSL socket. 317 * @throws IOException If an exception occurred while performing the 318 * TLS handshake. 319 */ 320 private SSLSocket startHandshake(SSLSocketFactory factory) 321 throws IOException { 322 323 if (ldapConnection == null) { 324 throw new IllegalStateException("LDAP connection has not been set." 325 + " TLS requires an existing LDAP connection."); 326 } 327 328 if (factory != currentFactory) { 329 // Create SSL socket layered over the existing connection 330 sslSocket = (SSLSocket) factory.createSocket(ldapConnection.sock, 331 ldapConnection.host, ldapConnection.port, false); 332 currentFactory = factory; 333 334 if (debug) { 335 System.out.println("StartTLS: Created socket : " + sslSocket); 336 } 337 } 338 339 if (suites != null) { 340 sslSocket.setEnabledCipherSuites(suites); 341 if (debug) { 342 System.out.println("StartTLS: Enabled cipher suites"); 343 } 344 } 345 346 // Connection must be quite for handshake to proceed 347 348 try { 349 if (debug) { 350 System.out.println( 351 "StartTLS: Calling sslSocket.startHandshake"); 352 } 353 sslSocket.startHandshake(); 354 if (debug) { 355 System.out.println( 356 "StartTLS: + Finished sslSocket.startHandshake"); 357 } 358 359 // Replace original streams with the new SSL streams 360 ldapConnection.replaceStreams(sslSocket.getInputStream(), 361 sslSocket.getOutputStream()); 362 if (debug) { 363 System.out.println("StartTLS: Replaced IO Streams"); 364 } 365 366 } catch (IOException e) { 367 if (debug) { 368 System.out.println("StartTLS: Got IO error during handshake"); 369 e.printStackTrace(); 370 } 371 372 sslSocket.close(); 373 isClosed = true; 374 throw e; // pass up exception 375 } 376 377 return sslSocket; 378 } 379 380 /* 381 * Verifies that the hostname in the server's certificate matches the 382 * hostname of the server. 383 * The server's first certificate is examined. If it has a subjectAltName 384 * that contains a dNSName then that is used as the server's hostname. 385 * The server's hostname may contain a wildcard for its left-most name part. 386 * Otherwise, if the certificate has no subjectAltName then the value of 387 * the common name attribute of the subject name is used. 388 * 389 * @param hostname The hostname of the server. 390 * @param session the SSLSession used on the connection to host. 391 * @return true if the hostname is verified, false otherwise. 392 */ 393 394 private boolean verify(String hostname, SSLSession session) 395 throws SSLPeerUnverifiedException { 396 397 java.security.cert.Certificate[] certs = null; 398 399 // if IPv6 strip off the "[]" 400 if (hostname != null && hostname.startsWith("[") && 401 hostname.endsWith("]")) { 402 hostname = hostname.substring(1, hostname.length() - 1); 403 } 404 try { 405 HostnameChecker checker = HostnameChecker.getInstance( 406 HostnameChecker.TYPE_LDAP); 407 // Use ciphersuite to determine whether Kerberos is active. 408 if (session.getCipherSuite().startsWith("TLS_KRB5")) { 409 Principal principal = getPeerPrincipal(session); 410 if (!HostnameChecker.match(hostname, principal)) { 411 throw new SSLPeerUnverifiedException( 412 "hostname of the kerberos principal:" + principal + 413 " does not match the hostname:" + hostname); 414 } 415 } else { // X.509 416 417 // get the subject's certificate 418 certs = session.getPeerCertificates(); 419 X509Certificate peerCert; 420 if (certs[0] instanceof java.security.cert.X509Certificate) { 421 peerCert = (java.security.cert.X509Certificate) certs[0]; 422 } else { 423 throw new SSLPeerUnverifiedException( 424 "Received a non X509Certificate from the server"); 425 } 426 checker.match(hostname, peerCert); 427 } 428 429 // no exception means verification passed 430 return true; 431 } catch (SSLPeerUnverifiedException e) { 432 433 /* 434 * The application may enable an anonymous SSL cipher suite, and 435 * hostname verification is not done for anonymous ciphers 436 */ 437 String cipher = session.getCipherSuite(); 438 if (cipher != null && (cipher.indexOf("_anon_") != -1)) { 439 return true; 440 } 441 throw e; 442 } catch (CertificateException e) { 443 444 /* 445 * Pass up the cause of the failure 446 */ 447 throw(SSLPeerUnverifiedException) 448 new SSLPeerUnverifiedException("hostname of the server '" + 449 hostname + 450 "' does not match the hostname in the " + 451 "server's certificate.").initCause(e); 452 } 453 } 454 455 /* 456 * Get the peer principal from the session 457 */ 458 private static Principal getPeerPrincipal(SSLSession session) 459 throws SSLPeerUnverifiedException { 460 Principal principal; 461 try { 462 principal = session.getPeerPrincipal(); 463 } catch (AbstractMethodError e) { 464 // if the JSSE provider does not support it, return null, since 465 // we need it only for Kerberos. 466 principal = null; 467 } 468 return principal; 469 } 470 }