View Javadoc
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 <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         // 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 <tt>negotiate()</tt>
144      * after the TLS handshake has completed. If
145      * <tt>setHostnameVerifier()</tt> has not been called before
146      * <tt>negotiate()</tt> is invoked, <tt>negotiate()</tt>
147      * will perform a simple case ignore match. If called after
148      * <tt>negotiate()</tt>, 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 <tt>negotiate(null)</tt>.
161      *
162      * @return The negotiated SSL session
163      * @throw 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 <tt>setEnabledCipherSuites</tt>
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 <tt>setHostnameVerifier</tt> 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      * @throw 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      * @throw 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 }