| /* |
| * Copyright (c) 2000, 2003, Oracle and/or its affiliates. All rights reserved. |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. |
| * |
| * This code is free software; you can redistribute it and/or modify it |
| * under the terms of the GNU General Public License version 2 only, as |
| * published by the Free Software Foundation. Oracle designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Oracle in the LICENSE file that accompanied this code. |
| * |
| * This code is distributed in the hope that it will be useful, but WITHOUT |
| * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
| * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License |
| * version 2 for more details (a copy is included in the LICENSE file that |
| * accompanied this code). |
| * |
| * You should have received a copy of the GNU General Public License version |
| * 2 along with this work; if not, write to the Free Software Foundation, |
| * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. |
| * |
| * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA |
| * or visit www.oracle.com if you need additional information or have any |
| * questions. |
| */ |
| |
| package com.sun.jndi.ldap.ext; |
| |
| import java.io.InputStream; |
| import java.io.OutputStream; |
| import java.io.BufferedInputStream; |
| import java.io.BufferedOutputStream; |
| import java.io.IOException; |
| |
| import java.net.Socket; |
| |
| import java.util.Collection; |
| import java.util.Iterator; |
| import java.util.List; |
| |
| import java.security.Principal; |
| import java.security.cert.X509Certificate; |
| import java.security.cert.CertificateException; |
| |
| import javax.net.ssl.SSLSession; |
| import javax.net.ssl.SSLSocket; |
| import javax.net.ssl.SSLSocketFactory; |
| import javax.net.ssl.SSLPeerUnverifiedException; |
| import javax.net.ssl.SSLContext; |
| import javax.net.ssl.HostnameVerifier; |
| import sun.security.util.HostnameChecker; |
| |
| import javax.naming.*; |
| import javax.naming.ldap.*; |
| import com.sun.jndi.ldap.Connection; |
| |
| /** |
| * This class implements the LDAPv3 Extended Response for StartTLS as |
| * defined in |
| * <a href="http://www.ietf.org/rfc/rfc2830.txt">Lightweight Directory |
| * Access Protocol (v3): Extension for Transport Layer Security</a> |
| * |
| * The object identifier for StartTLS is 1.3.6.1.4.1.1466.20037 |
| * and no extended response value is defined. |
| * |
| *<p> |
| * The Start TLS extended request and response are used to establish |
| * a TLS connection over the existing LDAP connection associated with |
| * the JNDI context on which <tt>extendedOperation()</tt> is invoked. |
| * |
| * @see StartTlsRequest |
| * @author Vincent Ryan |
| */ |
| final public class StartTlsResponseImpl extends StartTlsResponse { |
| |
| private static final boolean debug = false; |
| |
| /* |
| * The dNSName type in a subjectAltName extension of an X.509 certificate |
| */ |
| private static final int DNSNAME_TYPE = 2; |
| |
| /* |
| * The server's hostname. |
| */ |
| private transient String hostname = null; |
| |
| /* |
| * The LDAP socket. |
| */ |
| private transient Connection ldapConnection = null; |
| |
| /* |
| * The original input stream. |
| */ |
| private transient InputStream originalInputStream = null; |
| |
| /* |
| * The original output stream. |
| */ |
| private transient OutputStream originalOutputStream = null; |
| |
| /* |
| * The SSL socket. |
| */ |
| private transient SSLSocket sslSocket = null; |
| |
| /* |
| * The SSL socket factories. |
| */ |
| private transient SSLSocketFactory defaultFactory = null; |
| private transient SSLSocketFactory currentFactory = null; |
| |
| /* |
| * The list of cipher suites to be enabled. |
| */ |
| private transient String[] suites = null; |
| |
| /* |
| * The hostname verifier callback. |
| */ |
| private transient HostnameVerifier verifier = null; |
| |
| /* |
| * The flag to indicate that the TLS connection is closed. |
| */ |
| private transient boolean isClosed = true; |
| |
| private static final long serialVersionUID = -1126624615143411328L; |
| |
| // public no-arg constructor required by JDK's Service Provider API. |
| |
| public StartTlsResponseImpl() {} |
| |
| /** |
| * Overrides the default list of cipher suites enabled for use on the |
| * TLS connection. The cipher suites must have already been listed by |
| * <tt>SSLSocketFactory.getSupportedCipherSuites()</tt> as being supported. |
| * Even if a suite has been enabled, it still might not be used because |
| * the peer does not support it, or because the requisite certificates |
| * (and private keys) are not available. |
| * |
| * @param suites The non-null list of names of all the cipher suites to |
| * enable. |
| * @see #negotiate |
| */ |
| public void setEnabledCipherSuites(String[] suites) { |
| this.suites = suites; |
| } |
| |
| /** |
| * Overrides the default hostname verifier used by <tt>negotiate()</tt> |
| * after the TLS handshake has completed. If |
| * <tt>setHostnameVerifier()</tt> has not been called before |
| * <tt>negotiate()</tt> is invoked, <tt>negotiate()</tt> |
| * will perform a simple case ignore match. If called after |
| * <tt>negotiate()</tt>, this method does not do anything. |
| * |
| * @param verifier The non-null hostname verifier callback. |
| * @see #negotiate |
| */ |
| public void setHostnameVerifier(HostnameVerifier verifier) { |
| this.verifier = verifier; |
| } |
| |
| /** |
| * Negotiates a TLS session using the default SSL socket factory. |
| * <p> |
| * This method is equivalent to <tt>negotiate(null)</tt>. |
| * |
| * @return The negotiated SSL session |
| * @throw IOException If an IO error was encountered while establishing |
| * the TLS session. |
| * @see #setEnabledCipherSuites |
| * @see #setHostnameVerifier |
| */ |
| public SSLSession negotiate() throws IOException { |
| |
| return negotiate(null); |
| } |
| |
| /** |
| * Negotiates a TLS session using an SSL socket factory. |
| * <p> |
| * Creates an SSL socket using the supplied SSL socket factory and |
| * attaches it to the existing connection. Performs the TLS handshake |
| * and returns the negotiated session information. |
| * <p> |
| * If cipher suites have been set via <tt>setEnabledCipherSuites</tt> |
| * then they are enabled before the TLS handshake begins. |
| * <p> |
| * Hostname verification is performed after the TLS handshake completes. |
| * The default check performs a case insensitive match of the server's |
| * hostname against that in the server's certificate. The server's |
| * hostname is extracted from the subjectAltName in the server's |
| * certificate (if present). Otherwise the value of the common name |
| * attribute of the subject name is used. If a callback has |
| * been set via <tt>setHostnameVerifier</tt> then that verifier is used if |
| * the default check fails. |
| * <p> |
| * If an error occurs then the SSL socket is closed and an IOException |
| * is thrown. The underlying connection remains intact. |
| * |
| * @param factory The possibly null SSL socket factory to use. |
| * If null, the default SSL socket factory is used. |
| * @return The negotiated SSL session |
| * @throw IOException If an IO error was encountered while establishing |
| * the TLS session. |
| * @see #setEnabledCipherSuites |
| * @see #setHostnameVerifier |
| */ |
| public SSLSession negotiate(SSLSocketFactory factory) throws IOException { |
| |
| if (isClosed && sslSocket != null) { |
| throw new IOException("TLS connection is closed."); |
| } |
| |
| if (factory == null) { |
| factory = getDefaultFactory(); |
| } |
| |
| if (debug) { |
| System.out.println("StartTLS: About to start handshake"); |
| } |
| |
| SSLSession sslSession = startHandshake(factory).getSession(); |
| |
| if (debug) { |
| System.out.println("StartTLS: Completed handshake"); |
| } |
| |
| SSLPeerUnverifiedException verifExcep = null; |
| try { |
| if (verify(hostname, sslSession)) { |
| isClosed = false; |
| return sslSession; |
| } |
| } catch (SSLPeerUnverifiedException e) { |
| // Save to return the cause |
| verifExcep = e; |
| } |
| if ((verifier != null) && |
| verifier.verify(hostname, sslSession)) { |
| isClosed = false; |
| return sslSession; |
| } |
| |
| // Verification failed |
| close(); |
| sslSession.invalidate(); |
| if (verifExcep == null) { |
| verifExcep = new SSLPeerUnverifiedException( |
| "hostname of the server '" + hostname + |
| "' does not match the hostname in the " + |
| "server's certificate."); |
| } |
| throw verifExcep; |
| } |
| |
| /** |
| * Closes the TLS connection gracefully and reverts back to the underlying |
| * connection. |
| * |
| * @throw IOException If an IO error was encountered while closing the |
| * TLS connection |
| */ |
| public void close() throws IOException { |
| |
| if (isClosed) { |
| return; |
| } |
| |
| if (debug) { |
| System.out.println("StartTLS: replacing SSL " + |
| "streams with originals"); |
| } |
| |
| // Replace SSL streams with the original streams |
| ldapConnection.replaceStreams( |
| originalInputStream, originalOutputStream); |
| |
| if (debug) { |
| System.out.println("StartTLS: closing SSL Socket"); |
| } |
| sslSocket.close(); |
| |
| isClosed = true; |
| } |
| |
| /** |
| * Sets the connection for TLS to use. The TLS connection will be attached |
| * to this connection. |
| * |
| * @param ldapConnection The non-null connection to use. |
| * @param hostname The server's hostname. If null, the hostname used to |
| * open the connection will be used instead. |
| */ |
| public void setConnection(Connection ldapConnection, String hostname) { |
| this.ldapConnection = ldapConnection; |
| this.hostname = (hostname != null) ? hostname : ldapConnection.host; |
| originalInputStream = ldapConnection.inStream; |
| originalOutputStream = ldapConnection.outStream; |
| } |
| |
| /* |
| * Returns the default SSL socket factory. |
| * |
| * @return The default SSL socket factory. |
| * @throw IOException If TLS is not supported. |
| */ |
| private SSLSocketFactory getDefaultFactory() throws IOException { |
| |
| if (defaultFactory != null) { |
| return defaultFactory; |
| } |
| |
| return (defaultFactory = |
| (SSLSocketFactory) SSLSocketFactory.getDefault()); |
| } |
| |
| /* |
| * Start the TLS handshake and manipulate the input and output streams. |
| * |
| * @param factory The SSL socket factory to use. |
| * @return The SSL socket. |
| * @throw IOException If an exception occurred while performing the |
| * TLS handshake. |
| */ |
| private SSLSocket startHandshake(SSLSocketFactory factory) |
| throws IOException { |
| |
| if (ldapConnection == null) { |
| throw new IllegalStateException("LDAP connection has not been set." |
| + " TLS requires an existing LDAP connection."); |
| } |
| |
| if (factory != currentFactory) { |
| // Create SSL socket layered over the existing connection |
| sslSocket = (SSLSocket) factory.createSocket(ldapConnection.sock, |
| ldapConnection.host, ldapConnection.port, false); |
| currentFactory = factory; |
| |
| if (debug) { |
| System.out.println("StartTLS: Created socket : " + sslSocket); |
| } |
| } |
| |
| if (suites != null) { |
| sslSocket.setEnabledCipherSuites(suites); |
| if (debug) { |
| System.out.println("StartTLS: Enabled cipher suites"); |
| } |
| } |
| |
| // Connection must be quite for handshake to proceed |
| |
| try { |
| if (debug) { |
| System.out.println( |
| "StartTLS: Calling sslSocket.startHandshake"); |
| } |
| sslSocket.startHandshake(); |
| if (debug) { |
| System.out.println( |
| "StartTLS: + Finished sslSocket.startHandshake"); |
| } |
| |
| // Replace original streams with the new SSL streams |
| ldapConnection.replaceStreams(sslSocket.getInputStream(), |
| sslSocket.getOutputStream()); |
| if (debug) { |
| System.out.println("StartTLS: Replaced IO Streams"); |
| } |
| |
| } catch (IOException e) { |
| if (debug) { |
| System.out.println("StartTLS: Got IO error during handshake"); |
| e.printStackTrace(); |
| } |
| |
| sslSocket.close(); |
| isClosed = true; |
| throw e; // pass up exception |
| } |
| |
| return sslSocket; |
| } |
| |
| /* |
| * Verifies that the hostname in the server's certificate matches the |
| * hostname of the server. |
| * The server's first certificate is examined. If it has a subjectAltName |
| * that contains a dNSName then that is used as the server's hostname. |
| * The server's hostname may contain a wildcard for its left-most name part. |
| * Otherwise, if the certificate has no subjectAltName then the value of |
| * the common name attribute of the subject name is used. |
| * |
| * @param hostname The hostname of the server. |
| * @param session the SSLSession used on the connection to host. |
| * @return true if the hostname is verified, false otherwise. |
| */ |
| |
| private boolean verify(String hostname, SSLSession session) |
| throws SSLPeerUnverifiedException { |
| |
| java.security.cert.Certificate[] certs = null; |
| |
| // if IPv6 strip off the "[]" |
| if (hostname != null && hostname.startsWith("[") && |
| hostname.endsWith("]")) { |
| hostname = hostname.substring(1, hostname.length() - 1); |
| } |
| try { |
| HostnameChecker checker = HostnameChecker.getInstance( |
| HostnameChecker.TYPE_LDAP); |
| // Use ciphersuite to determine whether Kerberos is active. |
| if (session.getCipherSuite().startsWith("TLS_KRB5")) { |
| Principal principal = getPeerPrincipal(session); |
| if (!checker.match(hostname, principal)) { |
| throw new SSLPeerUnverifiedException( |
| "hostname of the kerberos principal:" + principal + |
| " does not match the hostname:" + hostname); |
| } |
| } else { // X.509 |
| |
| // get the subject's certificate |
| certs = session.getPeerCertificates(); |
| X509Certificate peerCert; |
| if (certs[0] instanceof java.security.cert.X509Certificate) { |
| peerCert = (java.security.cert.X509Certificate) certs[0]; |
| } else { |
| throw new SSLPeerUnverifiedException( |
| "Received a non X509Certificate from the server"); |
| } |
| checker.match(hostname, peerCert); |
| } |
| |
| // no exception means verification passed |
| return true; |
| } catch (SSLPeerUnverifiedException e) { |
| |
| /* |
| * The application may enable an anonymous SSL cipher suite, and |
| * hostname verification is not done for anonymous ciphers |
| */ |
| String cipher = session.getCipherSuite(); |
| if (cipher != null && (cipher.indexOf("_anon_") != -1)) { |
| return true; |
| } |
| throw e; |
| } catch (CertificateException e) { |
| |
| /* |
| * Pass up the cause of the failure |
| */ |
| throw(SSLPeerUnverifiedException) |
| new SSLPeerUnverifiedException("hostname of the server '" + |
| hostname + |
| "' does not match the hostname in the " + |
| "server's certificate.").initCause(e); |
| } |
| } |
| |
| /* |
| * Get the peer principal from the session |
| */ |
| private static Principal getPeerPrincipal(SSLSession session) |
| throws SSLPeerUnverifiedException { |
| Principal principal; |
| try { |
| principal = session.getPeerPrincipal(); |
| } catch (AbstractMethodError e) { |
| // if the JSSE provider does not support it, return null, since |
| // we need it only for Kerberos. |
| principal = null; |
| } |
| return principal; |
| } |
| } |