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      * @throw 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      * @throw 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 }