/* | 
|
 * Copyright (c) 2000, 2020, 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.IOException;  | 
|
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.HostnameVerifier;  | 
|
import sun.security.util.HostnameChecker;  | 
|
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) {  | 
|
// The impl does accept null suites, although the spec requires  | 
|
        // a non-null list. | 
|
this.suites = suites == null ? null : suites.clone();  | 
|
}  | 
|
    /** | 
|
     * 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, false);  | 
|
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.isEmpty())  | 
|
? ldapConnection.host : hostname;  | 
|
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(), true);  | 
|
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 (!HostnameChecker.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;  | 
|
}  | 
|
}  |