1 /*
   2  * Copyright (c) 2000, 2011, 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 <tt>extendedOperation()</tt> 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      * <tt>SSLSocketFactory.getSupportedCipherSuites()</tt> 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         this.suites = suites;
 138     }
 139 
 140     /**
 141      * Overrides the default hostname verifier used by <tt>negotiate()</tt>
 142      * after the TLS handshake has completed. If
 143      * <tt>setHostnameVerifier()</tt> has not been called before
 144      * <tt>negotiate()</tt> is invoked, <tt>negotiate()</tt>
 145      * will perform a simple case ignore match. If called after
 146      * <tt>negotiate()</tt>, this method does not do anything.
 147      *
 148      * @param verifier The non-null hostname verifier callback.
 149      * @see #negotiate
 150      */
 151     public void setHostnameVerifier(HostnameVerifier verifier) {
 152         this.verifier = verifier;
 153     }
 154 
 155     /**
 156      * Negotiates a TLS session using the default SSL socket factory.
 157      * <p>
 158      * This method is equivalent to <tt>negotiate(null)</tt>.
 159      *
 160      * @return The negotiated SSL session
 161      * @throw IOException If an IO error was encountered while establishing
 162      * the TLS session.
 163      * @see #setEnabledCipherSuites
 164      * @see #setHostnameVerifier
 165      */
 166     public SSLSession negotiate() throws IOException {
 167 
 168         return negotiate(null);
 169     }
 170 
 171     /**
 172      * Negotiates a TLS session using an SSL socket factory.
 173      * <p>
 174      * Creates an SSL socket using the supplied SSL socket factory and
 175      * attaches it to the existing connection. Performs the TLS handshake
 176      * and returns the negotiated session information.
 177      * <p>
 178      * If cipher suites have been set via <tt>setEnabledCipherSuites</tt>
 179      * then they are enabled before the TLS handshake begins.
 180      * <p>
 181      * Hostname verification is performed after the TLS handshake completes.
 182      * The default check performs a case insensitive match of the server's
 183      * hostname against that in the server's certificate. The server's
 184      * hostname is extracted from the subjectAltName in the server's
 185      * certificate (if present). Otherwise the value of the common name
 186      * attribute of the subject name is used. If a callback has
 187      * been set via <tt>setHostnameVerifier</tt> then that verifier is used if
 188      * the default check fails.
 189      * <p>
 190      * If an error occurs then the SSL socket is closed and an IOException
 191      * is thrown. The underlying connection remains intact.
 192      *
 193      * @param factory The possibly null SSL socket factory to use.
 194      * If null, the default SSL socket factory is used.
 195      * @return The negotiated SSL session
 196      * @throw IOException If an IO error was encountered while establishing
 197      * the TLS session.
 198      * @see #setEnabledCipherSuites
 199      * @see #setHostnameVerifier
 200      */
 201     public SSLSession negotiate(SSLSocketFactory factory) throws IOException {
 202 
 203         if (isClosed && sslSocket != null) {
 204             throw new IOException("TLS connection is closed.");
 205         }
 206 
 207         if (factory == null) {
 208             factory = getDefaultFactory();
 209         }
 210 
 211         if (debug) {
 212             System.out.println("StartTLS: About to start handshake");
 213         }
 214 
 215         SSLSession sslSession = startHandshake(factory).getSession();
 216 
 217         if (debug) {
 218             System.out.println("StartTLS: Completed handshake");
 219         }
 220 
 221         SSLPeerUnverifiedException verifExcep = null;
 222         try {
 223             if (verify(hostname, sslSession)) {
 224                 isClosed = false;
 225                 return sslSession;
 226             }
 227         } catch (SSLPeerUnverifiedException e) {
 228             // Save to return the cause
 229             verifExcep = e;
 230         }
 231         if ((verifier != null) &&
 232                 verifier.verify(hostname, sslSession)) {
 233             isClosed = false;
 234             return sslSession;
 235         }
 236 
 237         // Verification failed
 238         close();
 239         sslSession.invalidate();
 240         if (verifExcep == null) {
 241             verifExcep = new SSLPeerUnverifiedException(
 242                         "hostname of the server '" + hostname +
 243                         "' does not match the hostname in the " +
 244                         "server's certificate.");
 245         }
 246         throw verifExcep;
 247     }
 248 
 249     /**
 250      * Closes the TLS connection gracefully and reverts back to the underlying
 251      * connection.
 252      *
 253      * @throw IOException If an IO error was encountered while closing the
 254      * TLS connection
 255      */
 256     public void close() throws IOException {
 257 
 258         if (isClosed) {
 259             return;
 260         }
 261 
 262         if (debug) {
 263             System.out.println("StartTLS: replacing SSL " +
 264                                 "streams with originals");
 265         }
 266 
 267         // Replace SSL streams with the original streams
 268         ldapConnection.replaceStreams(
 269                         originalInputStream, originalOutputStream);
 270 
 271         if (debug) {
 272             System.out.println("StartTLS: closing SSL Socket");
 273         }
 274         sslSocket.close();
 275 
 276         isClosed = true;
 277     }
 278 
 279     /**
 280      * Sets the connection for TLS to use. The TLS connection will be attached
 281      * to this connection.
 282      *
 283      * @param ldapConnection The non-null connection to use.
 284      * @param hostname The server's hostname. If null, the hostname used to
 285      * open the connection will be used instead.
 286      */
 287     public void setConnection(Connection ldapConnection, String hostname) {
 288         this.ldapConnection = ldapConnection;
 289         this.hostname = (hostname != null) ? hostname : ldapConnection.host;
 290         originalInputStream = ldapConnection.inStream;
 291         originalOutputStream = ldapConnection.outStream;
 292     }
 293 
 294     /*
 295      * Returns the default SSL socket factory.
 296      *
 297      * @return The default SSL socket factory.
 298      * @throw IOException If TLS is not supported.
 299      */
 300     private SSLSocketFactory getDefaultFactory() throws IOException {
 301 
 302         if (defaultFactory != null) {
 303             return defaultFactory;
 304         }
 305 
 306         return (defaultFactory =
 307             (SSLSocketFactory) SSLSocketFactory.getDefault());
 308     }
 309 
 310     /*
 311      * Start the TLS handshake and manipulate the input and output streams.
 312      *
 313      * @param factory The SSL socket factory to use.
 314      * @return The SSL socket.
 315      * @throw IOException If an exception occurred while performing the
 316      * TLS handshake.
 317      */
 318     private SSLSocket startHandshake(SSLSocketFactory factory)
 319         throws IOException {
 320 
 321         if (ldapConnection == null) {
 322             throw new IllegalStateException("LDAP connection has not been set."
 323                 + " TLS requires an existing LDAP connection.");
 324         }
 325 
 326         if (factory != currentFactory) {
 327             // Create SSL socket layered over the existing connection
 328             sslSocket = (SSLSocket) factory.createSocket(ldapConnection.sock,
 329                 ldapConnection.host, ldapConnection.port, false);
 330             currentFactory = factory;
 331 
 332             if (debug) {
 333                 System.out.println("StartTLS: Created socket : " + sslSocket);
 334             }
 335         }
 336 
 337         if (suites != null) {
 338             sslSocket.setEnabledCipherSuites(suites);
 339             if (debug) {
 340                 System.out.println("StartTLS: Enabled cipher suites");
 341             }
 342         }
 343 
 344         // Connection must be quite for handshake to proceed
 345 
 346         try {
 347             if (debug) {
 348                 System.out.println(
 349                         "StartTLS: Calling sslSocket.startHandshake");
 350             }
 351             sslSocket.startHandshake();
 352             if (debug) {
 353                 System.out.println(
 354                         "StartTLS: + Finished sslSocket.startHandshake");
 355             }
 356 
 357             // Replace original streams with the new SSL streams
 358             ldapConnection.replaceStreams(sslSocket.getInputStream(),
 359                 sslSocket.getOutputStream());
 360             if (debug) {
 361                 System.out.println("StartTLS: Replaced IO Streams");
 362             }
 363 
 364         } catch (IOException e) {
 365             if (debug) {
 366                 System.out.println("StartTLS: Got IO error during handshake");
 367                 e.printStackTrace();
 368             }
 369 
 370             sslSocket.close();
 371             isClosed = true;
 372             throw e;   // pass up exception
 373         }
 374 
 375         return sslSocket;
 376     }
 377 
 378     /*
 379      * Verifies that the hostname in the server's certificate matches the
 380      * hostname of the server.
 381      * The server's first certificate is examined. If it has a subjectAltName
 382      * that contains a dNSName then that is used as the server's hostname.
 383      * The server's hostname may contain a wildcard for its left-most name part.
 384      * Otherwise, if the certificate has no subjectAltName then the value of
 385      * the common name attribute of the subject name is used.
 386      *
 387      * @param hostname The hostname of the server.
 388      * @param session the SSLSession used on the connection to host.
 389      * @return true if the hostname is verified, false otherwise.
 390      */
 391 
 392     private boolean verify(String hostname, SSLSession session)
 393         throws SSLPeerUnverifiedException {
 394 
 395         java.security.cert.Certificate[] certs = null;
 396 
 397         // if IPv6 strip off the "[]"
 398         if (hostname != null && hostname.startsWith("[") &&
 399                 hostname.endsWith("]")) {
 400             hostname = hostname.substring(1, hostname.length() - 1);
 401         }
 402         try {
 403             HostnameChecker checker = HostnameChecker.getInstance(
 404                                                 HostnameChecker.TYPE_LDAP);
 405             // Use ciphersuite to determine whether Kerberos is active.
 406             if (session.getCipherSuite().startsWith("TLS_KRB5")) {
 407                 Principal principal = getPeerPrincipal(session);
 408                 if (!HostnameChecker.match(hostname, principal)) {
 409                     throw new SSLPeerUnverifiedException(
 410                         "hostname of the kerberos principal:" + principal +
 411                         " does not match the hostname:" + hostname);
 412                 }
 413             } else { // X.509
 414 
 415                 // get the subject's certificate
 416                 certs = session.getPeerCertificates();
 417                 X509Certificate peerCert;
 418                 if (certs[0] instanceof java.security.cert.X509Certificate) {
 419                     peerCert = (java.security.cert.X509Certificate) certs[0];
 420                 } else {
 421                     throw new SSLPeerUnverifiedException(
 422                             "Received a non X509Certificate from the server");
 423                 }
 424                 checker.match(hostname, peerCert);
 425             }
 426 
 427             // no exception means verification passed
 428             return true;
 429         } catch (SSLPeerUnverifiedException e) {
 430 
 431             /*
 432              * The application may enable an anonymous SSL cipher suite, and
 433              * hostname verification is not done for anonymous ciphers
 434              */
 435             String cipher = session.getCipherSuite();
 436             if (cipher != null && (cipher.indexOf("_anon_") != -1)) {
 437                 return true;
 438             }
 439             throw e;
 440         } catch (CertificateException e) {
 441 
 442             /*
 443              * Pass up the cause of the failure
 444              */
 445             throw(SSLPeerUnverifiedException)
 446                 new SSLPeerUnverifiedException("hostname of the server '" +
 447                                 hostname +
 448                                 "' does not match the hostname in the " +
 449                                 "server's certificate.").initCause(e);
 450         }
 451     }
 452 
 453     /*
 454      * Get the peer principal from the session
 455      */
 456     private static Principal getPeerPrincipal(SSLSession session)
 457             throws SSLPeerUnverifiedException {
 458         Principal principal;
 459         try {
 460             principal = session.getPeerPrincipal();
 461         } catch (AbstractMethodError e) {
 462             // if the JSSE provider does not support it, return null, since
 463             // we need it only for Kerberos.
 464             principal = null;
 465         }
 466         return principal;
 467     }
 468 }