Java tutorial
/* * TLSProfileJSSE.java $Revision: 1.13 $ $Date: 2003/11/18 14:03:10 $ * * Copyright (c) 2001 Invisible Worlds, Inc. All rights reserved. * Copyright (c) 2003 Huston Franklin. All rights reserved. * * The contents of this file are subject to the Blocks Public License (the * "License"); You may not use this file except in compliance with the License. * * You may obtain a copy of the License at http://www.beepcore.org/ * * Software distributed under the License is distributed on an "AS IS" basis, * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License * for the specific language governing rights and limitations under the * License. * */ package org.beepcore.beep.profile.tls.jsse; import java.io.BufferedReader; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStreamReader; import java.net.Socket; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.NoSuchAlgorithmException; import java.util.Collections; import java.util.Hashtable; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import javax.net.ssl.HandshakeCompletedEvent; import javax.net.ssl.HandshakeCompletedListener; import javax.net.ssl.KeyManager; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.beepcore.beep.core.BEEPError; import org.beepcore.beep.core.BEEPException; import org.beepcore.beep.core.Channel; import org.beepcore.beep.core.CloseChannelException; import org.beepcore.beep.core.InputDataStreamAdapter; import org.beepcore.beep.core.MessageMSG; import org.beepcore.beep.core.ProfileRegistry; import org.beepcore.beep.core.RequestHandler; import org.beepcore.beep.core.Session; import org.beepcore.beep.core.SessionCredential; import org.beepcore.beep.core.SessionTuningProperties; import org.beepcore.beep.core.StartChannelException; import org.beepcore.beep.core.StartChannelListener; import org.beepcore.beep.core.StringOutputDataStream; import org.beepcore.beep.profile.Profile; import org.beepcore.beep.profile.ProfileConfiguration; import org.beepcore.beep.profile.tls.TLSProfile; import org.beepcore.beep.transport.tcp.TCPSession; /** * TLS provides encrypted, and optionally authenticated, communication over * a session. TLS is a tuning profile, a special set of profiles that affect * an entire session. As a result, only one channel with the profile of TLS * may be open per session. As with all tuning profiles, TLS may be configured * using properties passed into the init method. * * @see #init * @see org.beepcore.beep.profile.Profile * @see org.beepcore.beep.core.Channel * @see org.beepcore.beep.profile.tls.jsse.TLSProfileJSSEHandshakeCompletedListener */ public class TLSProfileJSSE extends TLSProfile implements Profile, StartChannelListener, RequestHandler { // Constants public static final String PROCEED1 = "<proceed/>"; public static final String PROCEED2 = "<proceed />"; public static final String READY1 = "<ready/>"; public static final String READY2 = "<ready />"; // error messages thrown in exceptions static final String ERR_SERVER_MUST_HAVE_KEY = "Listener must be anonymous if no keys are specified."; static final String ERR_EXPECTED_PROCEED = "Error receiving <proceed />"; static final String ERR_ILLEGAL_KEY_STORE = "Illegal Key Store Type property value"; static final String ERR_ILLEGAL_TRUST_STORE = "Illegal Trust Store Type property value"; static final String ERR_TLS_NOT_SUPPORTED_BY_SESSION = "TLS not supported by this session"; static final String ERR_TLS_SOCKET = "TLS not supported by this session"; static final String ERR_TLS_HANDSHAKE_WAIT = "Error waiting for TLS handshake to complete"; static final String ERR_TLS_NO_AUTHENTICATION = "Authentication failed for this TLS negotiation"; // property names static final String PROPERTY_KEY_MANAGER_ALGORITHM = "Key Algorithm"; static final String PROPERTY_KEY_MANAGER_PROVIDER = "Key Provider"; static final String PROPERTY_TRUST_MANAGER_ALGORITHM = "Trust Algorithm"; static final String PROPERTY_TRUST_MANAGER_PROVIDER = "Trust Provider"; static final String PROPERTY_KEYSTORE_PASSPHRASE = "Key Store Passphrase"; static final String PROPERTY_KEYSTORE_TYPE = "Key Store Data Type"; static final String PROPERTY_KEYSTORE_NAME = "Key Store"; static final String PROPERTY_KEYSTORE_FORMAT = "Key Store Format"; static final String PROPERTY_KEYSTORE_PROVIDER = "Key Store Provider"; static final String PROPERTY_TRUSTSTORE_PASSPHRASE = "Trust Store Passphrase"; static final String PROPERTY_TRUSTSTORE_TYPE = "Trust Store Data Type"; static final String PROPERTY_TRUSTSTORE_NAME = "Trust Store"; static final String PROPERTY_TRUSTSTORE_FORMAT = "Trust Store Format"; static final String PROPERTY_TRUSTSTORE_PROVIDER = "Trust Store Provider"; static final String PROPERTY_CIPHER_SUITE = "Cipher Suite"; static final String PROPERTY_CLIENT_AUTHENTICATION = "Initiator Authentication Required"; static final String PROPERTY_SERVER_ANONYMOUS = "Listener Anonymous"; static final String PROPERTY_SSL_PROTOCOLS = "Allowed SSL Protocols"; // properties set from the configuration static boolean needClientAuth = false; static boolean serverAnonymous = true; static String keyAlgorithm = null; static String keyProvider = null; static String trustAlgorithm = null; static String trustProvider = null; static String keyPassphrase = null; static String keyStoreType = null; static String keyStoreName = null; static String keyStoreFormat = null; static String keyStoreProvider = null; static String trustPassphrase = null; static String trustStoreType = null; static String trustStoreName = null; static String trustStoreFormat = null; static String trustStoreProvider = null; // socket factory that creates/wraps SSL connections static SSLSocketFactory socketFactory = null; private Log log = LogFactory.getLog(this.getClass()); // listeners to update when an SSL handshake completes // static List handshakeListeners = null; // static Map pendingHandshakes = null; boolean notifiedHandshake = false; boolean waitingForHandshake = false; boolean abortSession = false; String uri = TLSProfile.URI; private String[] sslProtocols; static List handshakeListeners = null; class TLSHandshake implements HandshakeCompletedListener { Session session; SessionCredential cred; boolean notifiedHandshake = false; boolean waitingForHandshake = false; public void handshakeCompleted(HandshakeCompletedEvent event) { log.debug("HandshakeCompleted"); synchronized (handshakeListeners) { Iterator i = TLSProfileJSSE.handshakeListeners.iterator(); while (i.hasNext()) { TLSProfileJSSEHandshakeCompletedListener l = (TLSProfileJSSEHandshakeCompletedListener) i .next(); if (l.handshakeCompleted(session, event) == false) { abortSession = true; break; } } } Hashtable h = new Hashtable(); try { h.put(SessionCredential.AUTHENTICATOR, event.getPeerCertificateChain()[0].getSubjectDN().getName()); h.put(SessionCredential.REMOTE_CERTIFICATE, event.getPeerCertificateChain()); } catch (SSLPeerUnverifiedException e) { h.put(SessionCredential.AUTHENTICATOR, ""); h.put(SessionCredential.REMOTE_CERTIFICATE, ""); } cred = new SessionCredential(h); synchronized (this) { if (waitingForHandshake) { notify(); } notifiedHandshake = true; } } } class BeepListenerHCL implements HandshakeCompletedListener { Channel channel; boolean notifiedHandshake = false; boolean waitingForHandshake = false; BeepListenerHCL(Channel tuningChannel) { this.channel = tuningChannel; } public void handshakeCompleted(HandshakeCompletedEvent event) { Session oldSession = channel.getSession(); log.debug("HandshakeCompleted"); synchronized (handshakeListeners) { Iterator i = TLSProfileJSSE.handshakeListeners.iterator(); while (i.hasNext()) { TLSProfileJSSEHandshakeCompletedListener l = (TLSProfileJSSEHandshakeCompletedListener) i .next(); if (l.handshakeCompleted(oldSession, event) == false) { BEEPError e = new BEEPError(BEEPError.CODE_REQUESTED_ACTION_ABORTED, ERR_TLS_NO_AUTHENTICATION); TLSProfileJSSE.this.abort(e, channel); return; } } } Hashtable h = new Hashtable(); try { h.put(SessionCredential.AUTHENTICATOR, event.getPeerCertificateChain()[0].getSubjectDN().getName()); h.put(SessionCredential.REMOTE_CERTIFICATE, event.getPeerCertificateChain()); } catch (SSLPeerUnverifiedException e) { h.put(SessionCredential.AUTHENTICATOR, ""); h.put(SessionCredential.REMOTE_CERTIFICATE, ""); } ProfileRegistry preg = oldSession.getProfileRegistry(); preg.removeStartChannelListener(uri); Hashtable hash = new Hashtable(); hash.put(SessionTuningProperties.ENCRYPTION, "true"); SessionTuningProperties tuning = new SessionTuningProperties(hash); // Cause the session to be recreated and reset try { TLSProfileJSSE.this.complete(channel, generateCredential(), new SessionCredential(h), tuning, preg, event.getSocket()); } catch (BEEPException e) { BEEPError error = new BEEPError(BEEPError.CODE_REQUESTED_ACTION_ABORTED, ERR_TLS_NO_AUTHENTICATION); TLSProfileJSSE.this.abort(error, channel); } } } /** * TLS provides encryption and optionally authentication for a session * by opening a channel with this profile. The default action is to * set up for a channel with encryption only, no authentication. To * mandate authentication, set the configuration via <code>init</code>.<p> * @see org.beepcore.beep.profile.Profile */ public TLSProfileJSSE() { try { SSLContext ctx = SSLContext.getInstance("TLS"); ctx.init(null, null, null); socketFactory = (SSLSocketFactory) ctx.getSocketFactory(); } catch (NoSuchAlgorithmException e) { log.error(e.getMessage()); } catch (KeyManagementException e) { log.error(e.getMessage()); } if (handshakeListeners == null) { handshakeListeners = Collections.synchronizedList(new LinkedList()); } } /** * init sets the criteria for which an SSL connection is made when * a TLS channel is started for a profile. It should only be * called once. For the properties, the initiator is defined as * the peer who starts the channel for the TLS profile, the * listener is the peer that receives the the channel start * request, irregardless of which actually started the session.<p> * * @param config <code>ProfileConfiguration</code> object that * contains key value pairs to initialize the TLS layer. None of * these are mandatory, but if you wish communication to be * anonymous with no authentication, (i.e., the listener to not * send back a certificate), you must set "Listener Anonymous" to * "true" and "Initiator Authentication Required" to "false". * The meaningful properties that can be set are these:<p> * <table> * <tr> * <td>Listener Anonymous</td><td>(true|false) must be set to false if the * listener will not authenticate itself</td> * </tr><tr> * <td>Initiator Authentication Required</td><td>(true|false) set if the * initiator should send a certificate and the listener expects a * certificate.</td> * </tr><tr> * <td>Cipher Suite</td><td><i>not yet implemented.</i>the algorithms that * can be used for encryption, authentication, and key exchange.</td> * </tr><tr> * <td>Key Algorithm</td><td>key management algorithm. See * {@link com.sun.net.ssl.KeyManagerFactory#getInstance}</td> * </tr><tr> * <td>Key Provider</td><td>provider of the key management * algorithm. Defaults to * <code>com.sun.net.ssl.internal.ssl.Provider</code> See * {@link com.sun.net.ssl.KeyManagerFactory#getInstance}</td> * </tr><tr> * <td>Trust Algorithm</td><td>algorithm to be used by the trust * manager. See * {@link com.sun.net.ssl.TrustManagerFactory#getInstance}</td> * </tr><tr> * <td>Trust Provider</td><td>provider of the trust manager. Defaults to * <code>com.sun.net.ssl.internal.ssl.Provider</code>. See * {@link com.sun.net.ssl.TrustManagerFactory#getInstance}</td> * </tr><tr> * <td>Key Store Passphrase</td><td>pass phrase used to encrypt the key * store. See {@link java.security.KeyStore#load}</td> * </tr><tr> * <td>Key Store Data Type</td><td>data type of the key store passed in. * "file" is currently the only value accepted, meaning Key Store * is the name of a file containing keys. See * {@link java.security.KeyStore#load}</td> * </tr><tr> * <td>Key Store</td><td>value of the key store, dependent on the type in * Key Store Data Type. See {@link java.security.KeyStore#load}</td> * </tr><tr> * <td>Key Store Format</td><td>format of the keys within the key store. * Default is "JKS". See {@link java.security.KeyStore#getInstance}</td> * </tr><tr> * <td>Key Store Provider</td><td>provider for the key stores. See * {@link java.security.KeyStore#getInstance}</td> * </tr><tr> * <td>Trust Store Passphrase</td><td>pass phrase used to encrypt the trust * store. See {@link java.security.KeyStore#load}</td> * </tr><tr> * <td>Trust Store Data Type</td><td>data type of the certificates in the * trust store. "file" is currently th only value accepted, * meaning the trust store is a file on the local disk. See * {@link java.security.KeyStore#load}</td> * </tr><tr> * <td>Trust Store</td><td>value of the trust store, dependent on the type * in Trust * Store Data Type See {@link java.security.KeyStore#load}</td> * </tr><tr> * <td>Trust Store Format</td><td>format of the certificates within the * trust store. * Default is "JKS". See {@link java.security.KeyStore#getInstance}</td> * </tr><tr> * <td>Trust Store Provider</td><td>provider for the trust stores. See * {@link java.security.KeyStore#getInstance}</td> * </tr><tr> * <td>Allowed SSL Protocols</td><td>Comma separated list of algorithms * that may be used for SSL/TLS negotiations. By default, this will be * whatever the {@link SSLSocket} implementation supports. * @see SSLSocket#getSupportedProtocols() * @see SSLSocket#setEnabledProtocols(String[]) * </tr><tr> * </table> * @throws BEEPException For any error in the profile configuration, a * negative response in the form of a BEEP error will be sent back to the * requesting peer. The session will continue to be open and usable, at * least from the standpoint of this peer. * * @see com.sun.net.ssl.KeyManagerFactory * @see com.sun.net.ssl.TrustManagerFactory * @see java.security.KeyStore * @see com.sun.net.ssl.SSLContext */ public StartChannelListener init(String uri, ProfileConfiguration config) throws BEEPException { KeyManagerFactory kmf = null; KeyManager[] km = null; KeyStore ks = null; TrustManagerFactory tmf = null; TrustManager[] tm = null; KeyStore ts = null; SSLContext ctx; this.sslProtocols = null; // set the URI of this instance of the profile this.uri = uri; try { // create an SSL context object ctx = SSLContext.getInstance("TLS"); } catch (java.security.NoSuchAlgorithmException e) { throw new BEEPException("TLS Algorithm Not Found. Probable " + "cause is the JSSE provider has not " + "been added to the java.security file."); } try { String protocols = config.getProperty(PROPERTY_SSL_PROTOCOLS); if (protocols != null) { this.sslProtocols = protocols.split(","); } // initialize the key managers, trust managers, and keyAlgorithm = config.getProperty(PROPERTY_KEY_MANAGER_ALGORITHM); keyProvider = config.getProperty(PROPERTY_KEY_MANAGER_PROVIDER); trustAlgorithm = config.getProperty(PROPERTY_TRUST_MANAGER_ALGORITHM); trustProvider = config.getProperty(PROPERTY_TRUST_MANAGER_PROVIDER); keyPassphrase = config.getProperty(PROPERTY_KEYSTORE_PASSPHRASE); keyStoreType = config.getProperty(PROPERTY_KEYSTORE_TYPE); keyStoreName = config.getProperty(PROPERTY_KEYSTORE_NAME); keyStoreFormat = config.getProperty(PROPERTY_KEYSTORE_FORMAT, "JKS"); keyStoreProvider = config.getProperty(PROPERTY_KEYSTORE_PROVIDER); trustPassphrase = config.getProperty(PROPERTY_TRUSTSTORE_PASSPHRASE); trustStoreType = config.getProperty(PROPERTY_TRUSTSTORE_TYPE); trustStoreName = config.getProperty(PROPERTY_TRUSTSTORE_NAME); trustStoreFormat = config.getProperty(PROPERTY_TRUSTSTORE_FORMAT, "JKS"); trustStoreProvider = config.getProperty(PROPERTY_TRUSTSTORE_PROVIDER); // determine if the client must authenticate or if the server can // needClientAuth = new Boolean(config.getProperty(PROPERTY_CLIENT_AUTHENTICATION, "false")) .booleanValue(); serverAnonymous = new Boolean(config.getProperty(PROPERTY_SERVER_ANONYMOUS, "true")).booleanValue(); if (keyAlgorithm != null) { if (keyProvider != null) { kmf = KeyManagerFactory.getInstance(keyAlgorithm, keyProvider); } else { kmf = KeyManagerFactory.getInstance(keyAlgorithm); } // add support for a default type of key manager factory? if (keyStoreProvider != null) { ks = KeyStore.getInstance(keyStoreFormat, keyStoreProvider); } else { ks = KeyStore.getInstance(keyStoreFormat); } if (keyStoreType.equals("file")) { ks.load(new FileInputStream(keyStoreName), keyPassphrase.toCharArray()); } else { throw new BEEPException(ERR_ILLEGAL_KEY_STORE); } // initialize the key factory manager kmf.init(ks, keyPassphrase.toCharArray()); km = kmf.getKeyManagers(); } else { km = null; } if (trustAlgorithm != null) { if (trustProvider != null) { tmf = TrustManagerFactory.getInstance(trustAlgorithm, trustProvider); } else { tmf = TrustManagerFactory.getInstance(trustAlgorithm); } // add support for a default type of trust manager factory? if (trustStoreProvider != null) { ts = KeyStore.getInstance(trustStoreFormat, trustStoreProvider); } else { ts = KeyStore.getInstance(trustStoreFormat); } if (trustStoreType.equals("file")) { ts.load(new FileInputStream(trustStoreName), trustPassphrase.toCharArray()); } else { throw new BEEPException(ERR_ILLEGAL_TRUST_STORE); } // initialize the trust factory manager tmf.init(ts); tm = tmf.getTrustManagers(); } else { tm = null; } // create a socket factory from the key factories and // trust factories created for the algorithms and stores // specfied. No option is given to change the secure // random number generator ctx.init(km, tm, null); socketFactory = ctx.getSocketFactory(); return this; } catch (Exception e) { log.error(e); throw new BEEPException(e); } } /** * advertiseProfile */ public boolean advertiseProfile(Session session, SessionTuningProperties tuning) throws BEEPException { return true; } /** * Called when the underlying BEEP framework receives * a "start" element for the TLS profile.<p> * * @param channel A <code>Channel</code> object which represents a channel * in this <code>Session</code>. * @param data The content of the "profile" element selected for this * channel (may be <code>null</code>). * @param encoding specifies whether the content of the "profile" element * selected for this channel is represented as a base64-encoded string. * The <code>encoding</code> is only valid if <code>data</code> is not * <code>null</code>. * * @throws StartChannelException Throwing this exception will cause an * error to be returned to the BEEP peer requesting to start a channel. * The channel is then discarded. */ public void startChannel(Channel channel, String encoding, String data) throws StartChannelException { channel.setRequestHandler(this, true); } public void receiveMSG(MessageMSG msg) { Channel channel = msg.getChannel(); InputDataStreamAdapter is = msg.getDataStream().getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); String data; try { try { data = reader.readLine(); } catch (IOException e) { msg.sendERR(BEEPError.CODE_PARAMETER_ERROR, "Error reading data"); return; } if (data.equals(READY1) == false && data.equals(READY2) == false) { msg.sendERR(BEEPError.CODE_PARAMETER_INVALID, "Expected READY element"); } this.begin(channel); msg.sendRPY(new StringOutputDataStream(PROCEED2)); } catch (BEEPException e1) { channel.getSession().terminate("unable to send ERR"); return; } try { Socket oldSocket = ((TCPSession) channel.getSession()).getSocket(); /** @TODO add support for serverName */ SSLSocket newSocket = (SSLSocket) socketFactory.createSocket(oldSocket, oldSocket.getInetAddress().getHostName(), oldSocket.getPort(), true); BeepListenerHCL l = new BeepListenerHCL(channel); newSocket.addHandshakeCompletedListener(l); newSocket.setUseClientMode(false); newSocket.setNeedClientAuth(needClientAuth); newSocket.setEnabledCipherSuites(newSocket.getSupportedCipherSuites()); if (sslProtocols != null) { newSocket.setEnabledProtocols(sslProtocols); } newSocket.startHandshake(); } catch (IOException e) { channel.getSession().terminate("TLS error: " + e.getMessage()); return; } } /** * Called when the underlying BEEP framework receives * a "close" element.<p> * * @param channel <code>Channel</code> which received the close request. * * @throws CloseChannelException Throwing this exception will return an * error to the BEEP peer requesting the close. The channel will remain * open. */ public void closeChannel(Channel channel) throws CloseChannelException { log.debug("Closing TLS channel."); } /** * Default implementation of advertiseProfile. Just returns TRUE that * the TLS profile should be advertised. */ public boolean advertiseProfile(Session session) throws BEEPException { return true; } /** * start a channel for the TLS profile. Besides issuing the * channel start request, it also performs the initiator side * chores necessary to begin encrypted communication using TLS * over a session. Parameters regarding the type of encryption * and whether or not authentication is required are specified * using the profile configuration passed to the <code>init</code> * method Upon returning, all traffic over the session will be * entrusted as per these parameters.<p> * * @see #init init - profile configuration * @param session The session to encrypt communcation for * * @return new <code>Session</code> with TLS negotiated. * @throws BEEPException an error occurs during the channel start * request or the TLS handshake (such as trying to negotiate an * anonymous connection with a peer that doesn't support an * anonymous cipher suite). */ public TCPSession startTLS(TCPSession session) throws BEEPException { Channel ch = startChannel(session, uri, false, READY2, null); // See if we got start data back String data = ch.getStartData(); if (log.isDebugEnabled()) { log.debug("Got start data of " + data); } // Consider the data (see if it's proceed) if ((data == null) || (!data.equals(PROCEED1) && !data.equals(PROCEED2))) { log.error("Invalid reply: " + data); throw new BEEPException(ERR_EXPECTED_PROCEED); } // Freeze IO and get the socket and reset it to TLS Socket oldSocket = session.getSocket(); SSLSocket newSocket = null; TLSHandshake l = new TLSHandshake(); // create the SSL Socket try { newSocket = (SSLSocket) socketFactory.createSocket(oldSocket, oldSocket.getInetAddress().getHostName(), oldSocket.getPort(), true); newSocket.addHandshakeCompletedListener(l); newSocket.setUseClientMode(true); newSocket.setNeedClientAuth(needClientAuth); newSocket.setEnabledCipherSuites(newSocket.getSupportedCipherSuites()); if (this.sslProtocols != null) { newSocket.setEnabledProtocols(sslProtocols); } // set up so the handshake listeners will be called l.session = session; log.debug("Handshake starting"); newSocket.startHandshake(); log.debug("Handshake returned"); synchronized (l) { if (!l.notifiedHandshake) { l.waitingForHandshake = true; l.wait(); l.waitingForHandshake = false; } } log.debug("Handshake done waiting"); } catch (javax.net.ssl.SSLException e) { log.error(e); throw new BEEPException(e); } catch (java.io.IOException e) { log.error(e); throw new BEEPException(ERR_TLS_SOCKET); } catch (InterruptedException e) { log.error(e); throw new BEEPException(ERR_TLS_HANDSHAKE_WAIT); } // swap it out for the new one with TLS enabled. if (abortSession) { session.close(); throw new BEEPException(ERR_TLS_NO_AUTHENTICATION); } else { Hashtable hash = new Hashtable(); hash.put(SessionTuningProperties.ENCRYPTION, "true"); SessionTuningProperties tuning = new SessionTuningProperties(hash); return (TCPSession) reset(session, generateCredential(), l.cred, tuning, session.getProfileRegistry(), newSocket); } } /** * return the default credentials for the new session to use after a TLS * negotiation is complete. * */ public static SessionCredential generateCredential() { Hashtable ht = new Hashtable(4); // @todo Could this matter since we're using TLSProfile here // and everywhere else we're using the instance variable uri? Perhaps. // I'll have to look into it. ht.put(SessionCredential.AUTHENTICATOR, TLSProfile.URI); return new SessionCredential(ht); } /** * add a listener for completed handshakes. * @param x receives handshake complete events * */ public void addHandshakeCompletedListener(TLSProfileJSSEHandshakeCompletedListener x) { removeHandshakeCompletedListener(x); handshakeListeners.add(x); } /** * remove a listener for completed handshakes. * @param x receives handshake complete events * */ public void removeHandshakeCompletedListener(TLSProfileJSSEHandshakeCompletedListener x) { handshakeListeners.remove(x); } }