001: /*
002: * Copyright 2000-2003 Sun Microsystems, Inc. All Rights Reserved.
003: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
004: *
005: * This code is free software; you can redistribute it and/or modify it
006: * under the terms of the GNU General Public License version 2 only, as
007: * published by the Free Software Foundation. Sun designates this
008: * particular file as subject to the "Classpath" exception as provided
009: * by Sun in the LICENSE file that accompanied this code.
010: *
011: * This code is distributed in the hope that it will be useful, but WITHOUT
012: * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
013: * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
014: * version 2 for more details (a copy is included in the LICENSE file that
015: * accompanied this code).
016: *
017: * You should have received a copy of the GNU General Public License version
018: * 2 along with this work; if not, write to the Free Software Foundation,
019: * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
020: *
021: * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
022: * CA 95054 USA or visit www.sun.com if you need additional information or
023: * have any questions.
024: */
025:
026: package com.sun.jndi.ldap.ext;
027:
028: import java.io.InputStream;
029: import java.io.OutputStream;
030: import java.io.BufferedInputStream;
031: import java.io.BufferedOutputStream;
032: import java.io.IOException;
033:
034: import java.net.Socket;
035:
036: import java.util.Collection;
037: import java.util.Iterator;
038: import java.util.List;
039:
040: import java.security.Principal;
041: import java.security.cert.X509Certificate;
042: import java.security.cert.CertificateException;
043: import javax.security.auth.kerberos.KerberosPrincipal;
044:
045: import javax.net.ssl.SSLSession;
046: import javax.net.ssl.SSLSocket;
047: import javax.net.ssl.SSLSocketFactory;
048: import javax.net.ssl.SSLPeerUnverifiedException;
049: import javax.net.ssl.SSLContext;
050: import javax.net.ssl.HostnameVerifier;
051: import sun.security.util.HostnameChecker;
052:
053: import javax.naming.*;
054: import javax.naming.ldap.*;
055: import com.sun.jndi.ldap.Connection;
056:
057: /**
058: * This class implements the LDAPv3 Extended Response for StartTLS as
059: * defined in
060: * <a href="http://www.ietf.org/rfc/rfc2830.txt">Lightweight Directory
061: * Access Protocol (v3): Extension for Transport Layer Security</a>
062: *
063: * The object identifier for StartTLS is 1.3.6.1.4.1.1466.20037
064: * and no extended response value is defined.
065: *
066: *<p>
067: * The Start TLS extended request and response are used to establish
068: * a TLS connection over the existing LDAP connection associated with
069: * the JNDI context on which <tt>extendedOperation()</tt> is invoked.
070: *
071: * @see StartTlsRequest
072: * @author Vincent Ryan
073: */
074: final public class StartTlsResponseImpl extends StartTlsResponse {
075:
076: private static final boolean debug = false;
077:
078: /*
079: * The dNSName type in a subjectAltName extension of an X.509 certificate
080: */
081: private static final int DNSNAME_TYPE = 2;
082:
083: /*
084: * The server's hostname.
085: */
086: private transient String hostname = null;
087:
088: /*
089: * The LDAP socket.
090: */
091: private transient Connection ldapConnection = null;
092:
093: /*
094: * The original input stream.
095: */
096: private transient InputStream originalInputStream = null;
097:
098: /*
099: * The original output stream.
100: */
101: private transient OutputStream originalOutputStream = null;
102:
103: /*
104: * The SSL socket.
105: */
106: private transient SSLSocket sslSocket = null;
107:
108: /*
109: * The SSL socket factories.
110: */
111: private transient SSLSocketFactory defaultFactory = null;
112: private transient SSLSocketFactory currentFactory = null;
113:
114: /*
115: * The list of cipher suites to be enabled.
116: */
117: private transient String[] suites = null;
118:
119: /*
120: * The hostname verifier callback.
121: */
122: private transient HostnameVerifier verifier = null;
123:
124: /*
125: * The flag to indicate that the TLS connection is closed.
126: */
127: private transient boolean isClosed = true;
128:
129: private static final long serialVersionUID = -1126624615143411328L;
130:
131: // public no-arg constructor required by JDK's Service Provider API.
132:
133: public StartTlsResponseImpl() {
134: }
135:
136: /**
137: * Overrides the default list of cipher suites enabled for use on the
138: * TLS connection. The cipher suites must have already been listed by
139: * <tt>SSLSocketFactory.getSupportedCipherSuites()</tt> as being supported.
140: * Even if a suite has been enabled, it still might not be used because
141: * the peer does not support it, or because the requisite certificates
142: * (and private keys) are not available.
143: *
144: * @param suites The non-null list of names of all the cipher suites to
145: * enable.
146: * @see #negotiate
147: */
148: public void setEnabledCipherSuites(String[] suites) {
149: this .suites = suites;
150: }
151:
152: /**
153: * Overrides the default hostname verifier used by <tt>negotiate()</tt>
154: * after the TLS handshake has completed. If
155: * <tt>setHostnameVerifier()</tt> has not been called before
156: * <tt>negotiate()</tt> is invoked, <tt>negotiate()</tt>
157: * will perform a simple case ignore match. If called after
158: * <tt>negotiate()</tt>, this method does not do anything.
159: *
160: * @param verifier The non-null hostname verifier callback.
161: * @see #negotiate
162: */
163: public void setHostnameVerifier(HostnameVerifier verifier) {
164: this .verifier = verifier;
165: }
166:
167: /**
168: * Negotiates a TLS session using the default SSL socket factory.
169: * <p>
170: * This method is equivalent to <tt>negotiate(null)</tt>.
171: *
172: * @return The negotiated SSL session
173: * @throw IOException If an IO error was encountered while establishing
174: * the TLS session.
175: * @see #setEnabledCipherSuites
176: * @see #setHostnameVerifier
177: */
178: public SSLSession negotiate() throws IOException {
179:
180: return negotiate(null);
181: }
182:
183: /**
184: * Negotiates a TLS session using an SSL socket factory.
185: * <p>
186: * Creates an SSL socket using the supplied SSL socket factory and
187: * attaches it to the existing connection. Performs the TLS handshake
188: * and returns the negotiated session information.
189: * <p>
190: * If cipher suites have been set via <tt>setEnabledCipherSuites</tt>
191: * then they are enabled before the TLS handshake begins.
192: * <p>
193: * Hostname verification is performed after the TLS handshake completes.
194: * The default check performs a case insensitive match of the server's
195: * hostname against that in the server's certificate. The server's
196: * hostname is extracted from the subjectAltName in the server's
197: * certificate (if present). Otherwise the value of the common name
198: * attribute of the subject name is used. If a callback has
199: * been set via <tt>setHostnameVerifier</tt> then that verifier is used if
200: * the default check fails.
201: * <p>
202: * If an error occurs then the SSL socket is closed and an IOException
203: * is thrown. The underlying connection remains intact.
204: *
205: * @param factory The possibly null SSL socket factory to use.
206: * If null, the default SSL socket factory is used.
207: * @return The negotiated SSL session
208: * @throw IOException If an IO error was encountered while establishing
209: * the TLS session.
210: * @see #setEnabledCipherSuites
211: * @see #setHostnameVerifier
212: */
213: public SSLSession negotiate(SSLSocketFactory factory)
214: throws IOException {
215:
216: if (isClosed && sslSocket != null) {
217: throw new IOException("TLS connection is closed.");
218: }
219:
220: if (factory == null) {
221: factory = getDefaultFactory();
222: }
223:
224: if (debug) {
225: System.out.println("StartTLS: About to start handshake");
226: }
227:
228: SSLSession sslSession = startHandshake(factory).getSession();
229:
230: if (debug) {
231: System.out.println("StartTLS: Completed handshake");
232: }
233:
234: SSLPeerUnverifiedException verifExcep = null;
235: try {
236: if (verify(hostname, sslSession)) {
237: isClosed = false;
238: return sslSession;
239: }
240: } catch (SSLPeerUnverifiedException e) {
241: // Save to return the cause
242: verifExcep = e;
243: }
244: if ((verifier != null) && verifier.verify(hostname, sslSession)) {
245: isClosed = false;
246: return sslSession;
247: }
248:
249: // Verification failed
250: close();
251: sslSession.invalidate();
252: if (verifExcep == null) {
253: verifExcep = new SSLPeerUnverifiedException(
254: "hostname of the server '" + hostname
255: + "' does not match the hostname in the "
256: + "server's certificate.");
257: }
258: throw verifExcep;
259: }
260:
261: /**
262: * Closes the TLS connection gracefully and reverts back to the underlying
263: * connection.
264: *
265: * @throw IOException If an IO error was encountered while closing the
266: * TLS connection
267: */
268: public void close() throws IOException {
269:
270: if (isClosed) {
271: return;
272: }
273:
274: if (debug) {
275: System.out.println("StartTLS: replacing SSL "
276: + "streams with originals");
277: }
278:
279: // Replace SSL streams with the original streams
280: ldapConnection.replaceStreams(originalInputStream,
281: originalOutputStream);
282:
283: if (debug) {
284: System.out.println("StartTLS: closing SSL Socket");
285: }
286: sslSocket.close();
287:
288: isClosed = true;
289: }
290:
291: /**
292: * Sets the connection for TLS to use. The TLS connection will be attached
293: * to this connection.
294: *
295: * @param ldapConnection The non-null connection to use.
296: * @param hostname The server's hostname. If null, the hostname used to
297: * open the connection will be used instead.
298: */
299: public void setConnection(Connection ldapConnection, String hostname) {
300: this .ldapConnection = ldapConnection;
301: this .hostname = (hostname != null) ? hostname
302: : ldapConnection.host;
303: originalInputStream = ldapConnection.inStream;
304: originalOutputStream = ldapConnection.outStream;
305: }
306:
307: /*
308: * Returns the default SSL socket factory.
309: *
310: * @return The default SSL socket factory.
311: * @throw IOException If TLS is not supported.
312: */
313: private SSLSocketFactory getDefaultFactory() throws IOException {
314:
315: if (defaultFactory != null) {
316: return defaultFactory;
317: }
318:
319: return (defaultFactory = (SSLSocketFactory) SSLSocketFactory
320: .getDefault());
321: }
322:
323: /*
324: * Start the TLS handshake and manipulate the input and output streams.
325: *
326: * @param factory The SSL socket factory to use.
327: * @return The SSL socket.
328: * @throw IOException If an exception occurred while performing the
329: * TLS handshake.
330: */
331: private SSLSocket startHandshake(SSLSocketFactory factory)
332: throws IOException {
333:
334: if (ldapConnection == null) {
335: throw new IllegalStateException(
336: "LDAP connection has not been set."
337: + " TLS requires an existing LDAP connection.");
338: }
339:
340: if (factory != currentFactory) {
341: // Create SSL socket layered over the existing connection
342: sslSocket = (SSLSocket) factory.createSocket(
343: ldapConnection.sock, ldapConnection.host,
344: ldapConnection.port, false);
345: currentFactory = factory;
346:
347: if (debug) {
348: System.out.println("StartTLS: Created socket : "
349: + sslSocket);
350: }
351: }
352:
353: if (suites != null) {
354: sslSocket.setEnabledCipherSuites(suites);
355: if (debug) {
356: System.out.println("StartTLS: Enabled cipher suites");
357: }
358: }
359:
360: // Connection must be quite for handshake to proceed
361:
362: try {
363: if (debug) {
364: System.out
365: .println("StartTLS: Calling sslSocket.startHandshake");
366: }
367: sslSocket.startHandshake();
368: if (debug) {
369: System.out
370: .println("StartTLS: + Finished sslSocket.startHandshake");
371: }
372:
373: // Replace original streams with the new SSL streams
374: ldapConnection.replaceStreams(sslSocket.getInputStream(),
375: sslSocket.getOutputStream());
376: if (debug) {
377: System.out.println("StartTLS: Replaced IO Streams");
378: }
379:
380: } catch (IOException e) {
381: if (debug) {
382: System.out
383: .println("StartTLS: Got IO error during handshake");
384: e.printStackTrace();
385: }
386:
387: sslSocket.close();
388: isClosed = true;
389: throw e; // pass up exception
390: }
391:
392: return sslSocket;
393: }
394:
395: /*
396: * Verifies that the hostname in the server's certificate matches the
397: * hostname of the server.
398: * The server's first certificate is examined. If it has a subjectAltName
399: * that contains a dNSName then that is used as the server's hostname.
400: * The server's hostname may contain a wildcard for its left-most name part.
401: * Otherwise, if the certificate has no subjectAltName then the value of
402: * the common name attribute of the subject name is used.
403: *
404: * @param hostname The hostname of the server.
405: * @param session the SSLSession used on the connection to host.
406: * @return true if the hostname is verified, false otherwise.
407: */
408:
409: private boolean verify(String hostname, SSLSession session)
410: throws SSLPeerUnverifiedException {
411:
412: java.security.cert.Certificate[] certs = null;
413:
414: // if IPv6 strip off the "[]"
415: if (hostname != null && hostname.startsWith("[")
416: && hostname.endsWith("]")) {
417: hostname = hostname.substring(1, hostname.length() - 1);
418: }
419: try {
420: HostnameChecker checker = HostnameChecker
421: .getInstance(HostnameChecker.TYPE_LDAP);
422: Principal principal = getPeerPrincipal(session);
423: if (principal instanceof KerberosPrincipal) {
424: if (!checker.match(hostname,
425: (KerberosPrincipal) principal)) {
426: throw new SSLPeerUnverifiedException(
427: "hostname of the kerberos principal:"
428: + principal
429: + " does not match the hostname:"
430: + hostname);
431: }
432: } else {
433:
434: // get the subject's certificate
435: certs = session.getPeerCertificates();
436: X509Certificate peerCert;
437: if (certs[0] instanceof java.security.cert.X509Certificate) {
438: peerCert = (java.security.cert.X509Certificate) certs[0];
439: } else {
440: throw new SSLPeerUnverifiedException(
441: "Received a non X509Certificate from the server");
442: }
443: checker.match(hostname, peerCert);
444: }
445:
446: // no exception means verification passed
447: return true;
448: } catch (SSLPeerUnverifiedException e) {
449:
450: /*
451: * The application may enable an anonymous SSL cipher suite, and
452: * hostname verification is not done for anonymous ciphers
453: */
454: String cipher = session.getCipherSuite();
455: if (cipher != null && (cipher.indexOf("_anon_") != -1)) {
456: return true;
457: }
458: throw e;
459: } catch (CertificateException e) {
460:
461: /*
462: * Pass up the cause of the failure
463: */
464: throw (SSLPeerUnverifiedException) new SSLPeerUnverifiedException(
465: "hostname of the server '" + hostname
466: + "' does not match the hostname in the "
467: + "server's certificate.").initCause(e);
468: }
469: }
470:
471: /*
472: * Get the peer principal from the session
473: */
474: private static Principal getPeerPrincipal(SSLSession session)
475: throws SSLPeerUnverifiedException {
476: Principal principal;
477: try {
478: principal = session.getPeerPrincipal();
479: } catch (AbstractMethodError e) {
480: // if the JSSE provider does not support it, return null, since
481: // we need it only for Kerberos.
482: principal = null;
483: }
484: return principal;
485: }
486: }
|