diff options
Diffstat (limited to 'src/com/isode/stroke/tls/java/JSSEContext.java')
-rw-r--r-- | src/com/isode/stroke/tls/java/JSSEContext.java | 714 |
1 files changed, 714 insertions, 0 deletions
diff --git a/src/com/isode/stroke/tls/java/JSSEContext.java b/src/com/isode/stroke/tls/java/JSSEContext.java new file mode 100644 index 0000000..1d8fba7 --- /dev/null +++ b/src/com/isode/stroke/tls/java/JSSEContext.java @@ -0,0 +1,714 @@ +/* Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + * + * Acquisition and use of this software and related materials for any + * purpose requires a written licence agreement from Isode Limited, + * or a written licence from an organisation licensed by Isode Limited Limited + * to grant such a licence. + * + */ + +package com.isode.stroke.tls.java; + +import java.nio.BufferOverflowException; +import java.nio.ByteBuffer; +import java.security.KeyManagementException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateExpiredException; +import java.security.cert.CertificateNotYetValidException; +import java.security.cert.X509Certificate; +import java.util.Vector; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLEngineResult.Status; +import javax.net.ssl.SSLException; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.tls.Certificate; +import com.isode.stroke.tls.CertificateVerificationError; +import com.isode.stroke.tls.CertificateVerificationError.Type; +import com.isode.stroke.tls.PKCS12Certificate; +import com.isode.stroke.tls.TLSContext; + + +/** + * Concrete implementation of a TLSContext which uses SSLEngine + * and maybe other stuff? ..tbs... + * + */ +public class JSSEContext extends TLSContext { + + private static class JSSEContextError { + public Throwable throwable; + public String message; + /** + * Create a new object + * @param t throwable; may be null + * @param m message; may be null + */ + public JSSEContextError(Throwable t, String m) { + throwable = t; + message = m; + } + @Override + public String toString() { + return "JSSEContextError: " + + (message == null ? "No message" : message) + "; " + + (throwable == null ? "No exception" : throwable.getMessage()); + } + } + /** + * If an error occurs, it will be added to this vector + */ + private Vector<JSSEContextError> errorsEmitted = new Vector<JSSEContextError>(); + + /** + * Whether the handshake has finished + */ + private boolean handshakeCompleted = false; + + /** + * Determine whether an error has occurred. If an error has occurred, then + * you probably don't want to try doing any more stuff. + * + * @return <em>true</em> if an error has occurred, <em>false</em> + * otherwise + */ + private boolean hasError() { + return (!errorsEmitted.isEmpty()); + } + /** + * Emit an error, and keep track of which errors have been emitted + * @param t the Throwable which caused this error (may be null) + * @param m a String describing what caused this error (may be null) + */ + private void emitError(Throwable t, String m) { + JSSEContextError jsseContextError = new JSSEContextError(t,m); + errorsEmitted.add(jsseContextError); + onError.emit(); + } + + @Override + public void connect() { + try { + doSetup(); + } + catch (SSLException e) { + emitError(e,"doSetup() failed"); + } + } + + private void doSetup() throws SSLException { + + // May throw NoSuchAlgorithmException + SSLContext sslContext = null; + + sslContext = getSSLContext(); + + if (sslContext == null) { + throw new SSLException("Could not create SSLContext"); + } + + sslEngine = null; + try { + sslEngine = sslContext.createSSLEngine(); + } + catch (UnsupportedOperationException e) { + // "the underlying provider does not implement the operation" + throw new SSLException(e); + } + catch (IllegalStateException e) { + // "the SSLContextImpl requires initialization and init() has not been called" + throw new SSLException(e); + } + + sslEngine.setUseClientMode(true); // I am a client + sslEngine.setEnableSessionCreation(true); // can create new sessions + + + int appBufferMax = sslEngine.getSession().getApplicationBufferSize(); + int netBufferMax = sslEngine.getSession().getPacketBufferSize(); + + // All buffers are normally in "write" mode. Access to all of them + // must be synchronized + plainToSend = ByteBuffer.allocate(appBufferMax + 50); + wrappedToSend = ByteBuffer.allocate(netBufferMax); + encryptedReceived = ByteBuffer.allocate(netBufferMax); + unwrappedReceived = ByteBuffer.allocate(appBufferMax + 50); + + // Note that calling beginHandshake might not actually do anything; + // the SSLEngine may not actually send the handshake until it's had + // some data from the application. And the higher level won't send + // any data until it thinks the handshake is completed. + // + // So this is a hack to force the handshake to occur: on the assumption + // that the first thing to be sent once TLS is running is + // the "<" from the start of a tag, we send a less-than sign now, + // which we'll remember must be removed that from the first message + // we get told to send. + + sslEngine.beginHandshake(); + + ByteArray ba = new ByteArray("<".getBytes()); + hack = HackStatus.SENDING_FAKE_LT; + handleDataFromApplication(ba); + + } + + + /** + * Unwrap any data in the "encryptedReceived" buffer and put it into + * the "unwrappedReceived" buffer. An event will be generated to the + * end-user's listener if there's anything pending in the + * unwrappedReceived buffer. Caller should check handshake status + * after this returns + * + * @return the number of bytes that SSLEngine consumed + */ + private int unwrapPendingData() + { + SSLEngineResult sslEngineResult; + Status status; + int bytesProduced = 0; + int bytesConsumed = 0; + HandshakeStatus handshakeStatus = null; + ByteArray byteArray = null; + + synchronized(recvMutex) { + try { + encryptedReceived.flip(); + sslEngineResult = sslEngine.unwrap(encryptedReceived, unwrappedReceived); + encryptedReceived.compact(); + + // A call to unwrap can generate a status of FINISHED, which + // you won't get from SSLEngine.getHandshakeStatus. Such + // a status is an indication that we need to re-check whether + // anything's pending to be written + handshakeStatus = sslEngineResult.getHandshakeStatus(); + + + bytesConsumed += sslEngineResult.bytesConsumed(); + bytesProduced = sslEngineResult.bytesProduced(); + } + catch (SSLException e) { + throw new RuntimeException("unwrap produced: " + e); + } + + + status = sslEngineResult.getStatus(); + boolean finished = false; + while (!finished) { + switch (status) { + case BUFFER_UNDERFLOW: + // There's not enough data yet for engine to be able to decode + // a full message. Not a problem; assume that more will come + // in to the socket eventually + finished = true; + break; + case OK: + // Unwrap was OK + finished = true; + break; + case BUFFER_OVERFLOW: + // Not enough room in "unwrappedReceived" to write the data + // TODO: need to fix this + case CLOSED: + // Engine closed - don't expect this here + emitError(null, "SSLEngine.unwrap returned " + status); + return bytesConsumed; + } + } + + if (handshakeStatus == HandshakeStatus.FINISHED) { + // Special case will happen when the handshake completes following + // an unwrap. The first time we tried wrapping some plain stuff, + // it triggers the handshake but won't itself have been dealt with. + // So now the handshake has finished, we have to try sending it + // again + handshakeCompleted = true; + wrapAndSendData(); + onConnected.emit(); + } + + if (bytesProduced > 0) { + unwrappedReceived.flip(); + byte[] result = new byte[0]; + result = new byte[unwrappedReceived.remaining()]; + unwrappedReceived.get(result); + unwrappedReceived.compact(); + byteArray = new ByteArray(result); + } + + } + + // Now out of synchronized block + if (byteArray != null) { + onDataForApplication.emit(byteArray); + } + return bytesConsumed; + + } + + /** + * Use the SSLEngine to wrap everything that we've so far got + * in "plainToSend", and then send all of that to the socket. Caller + * is responsible for checking the handshake status on return + * + */ + private void wrapAndSendData() { + + int bytesSentToSocket = 0; + ByteArray byteArray = null; + SSLEngineResult sslEngineResult = null; + Status status = null; + boolean handshakeFinished = false; + + synchronized(sendMutex) { + // Check if there's anything outstanding to be sent at the + // top of the loop, so that we clear the "wrappedToSend" + // buffer before asking the engine to encrypt anything + // TODO: is this required? I don't think anything gets put in + // wrappedToSend apart from in here? + wrappedToSend.flip(); + if (wrappedToSend.hasRemaining()) { + byte[] b = new byte[(wrappedToSend.remaining())]; + wrappedToSend.get(b); + byteArray = new ByteArray(b); + } + wrappedToSend.compact(); + } // end synchronized + + if (byteArray != null) { + int s = byteArray.getSize(); + + onDataForNetwork.emit(byteArray); + bytesSentToSocket += s; + byteArray = null; + } + + // There's nothing waiting to be sent. Now see what new data needs + // encrypting + synchronized(sendMutex) { + plainToSend.flip(); + if (!plainToSend.hasRemaining()) { + // Nothing more to be encrypted + plainToSend.compact(); + return; + } + try { + sslEngineResult = sslEngine.wrap(plainToSend, wrappedToSend); + } + catch (SSLException e) { + // TODO: Is there anything more that can be done here? + // TODO: this is called inside the mutex, does this matter? + emitError(e,"SSLEngine.wrap failed"); + return; + } + plainToSend.compact(); + + status = sslEngineResult.getStatus(); + + // FINISHED can only come back for wrap() or unwrap(); so check to + // see if we just had it + if (sslEngineResult.getHandshakeStatus() == HandshakeStatus.FINISHED) { + handshakeFinished = true; + } + + switch (status) { + case OK: + // This is the only status we expect here. It means the + // data was successfully wrapped and that there's something + // to be sent. + wrappedToSend.flip(); + if (wrappedToSend.hasRemaining()) { + byte[] b = new byte[(wrappedToSend.remaining())]; + wrappedToSend.get(b); + byteArray = new ByteArray(b); + } + wrappedToSend.compact(); + break; + + case BUFFER_UNDERFLOW: + // Can't happen for a wrap + case CLOSED : + // ??? + case BUFFER_OVERFLOW: + // The "wrappedToSend" buffer, which we had previously made + // sure was empty, isn't big enough. + // TODO: I don't think this can happen though + // TODO: Note that we're in sychronized block here + emitError(null, "SSLEngine.wrap returned " + status); + return; + + } + } // end synchronized + + if (handshakeFinished) { + handshakeCompleted = true; + onConnected.emit(); + } + + if (byteArray != null) { + int s = byteArray.getSize(); + onDataForNetwork.emit(byteArray); + bytesSentToSocket += s; + byteArray = null; + } + + // Note that there may still be stuff in "plainToSend" that hasn't + // yet been consumed + return; + + } + + + /** + * Process the current handshake status. + * @return <em>true</em> if this method needs to be called again, or + * <em>false</em> if there's no more handshake status to be processed + */ + private boolean processHandshakeStatus() { + HandshakeStatus handshakeStatus; + + handshakeStatus = sslEngine.getHandshakeStatus(); + switch (handshakeStatus) { + case NOT_HANDSHAKING: + // No handshaking going on - session is available, no more + // handshake status to process + return false; + case NEED_TASK: + runDelegatedTasks(false); // false==don't create separate threads + + // after tasks have run, need to come back here and check + // handshake status again + return true; + + case NEED_WRAP: + // SSLEngine wants some data that it can wrap for sending to the + // other side + wrapAndSendData(); + // after sending data, need to check handshake status again + return true; + case NEED_UNWRAP: + + // SSLEngine wants data from other side that it can unwrap and + // process + int consumed = unwrapPendingData(); + return (consumed > 0); + + case FINISHED: + // "This value is only generated by a call to wrap/unwrap when + // that call finishes a handshake. It is never generated by + // SSLEngine.getHandshakeStatus()" + + default: + // There are no other values, but compiler requires this + throw new RuntimeException("unexpected handshake status " + handshakeStatus); + } + } + + /** + * Create and start running delegated tasks for all pending delegated tasks + * @param createThreads <em>true</em> to run tasks in separate threads, + * <em>false</em> to run them all in series in the current thread + */ + private void runDelegatedTasks(boolean createThreads) + { + Runnable nextTask = sslEngine.getDelegatedTask(); + + while (nextTask != null) { + final Runnable task = nextTask; + Thread delegatedTaskThread = new Thread() { + public void run() { + task.run(); + } + }; + + if (createThreads) { + delegatedTaskThread.setDaemon(true); + delegatedTaskThread.start(); + } + else { + delegatedTaskThread.run(); + } + nextTask = sslEngine.getDelegatedTask(); + } + } + + /** + * This method must be called to inform the JSSEContext object of the + * certificate(s) which were presented by the peer during the handshake. + * + * <p>For example, an X509TrustManager implementation may obtain + * peer certificates inside <em>checkServerTrusted</em>, and call this + * method with those certificates. + * + * @param certs chain of certificates presented by the server. Will + * subsequently be returned by any call to {@link #getPeerCertificate()} + * + * @param certificateException any exception resulting from an attempt + * to verify the certificate. May be null. If non-null, then will be used + * to generate the value that is returned by any subsequent call to + * {@link #getPeerCertificateVerificationError()} + */ + public void setPeerCertificateInfo(X509Certificate[] certs, + CertificateException certificateException) { + if (certs == null || certs.length == 0) { + return; + } + + peerCertificate = new JavaCertificate(certs[0]); + + // Swiften uses SSL_get_verify_result() for this, and the documentation + // for that says it "while the verification of a certificate can fail + // because of many reasons at the same time. Only the last verification + // error that occurred..is available". + // So once one problem is found, don't bother looking for others. + + if (certificateException != null) { + if (certificateException instanceof CertificateNotYetValidException) { + peerCertificateVerificationError = new CertificateVerificationError(Type.NotYetValid); + return; + + } + + if (certificateException instanceof CertificateExpiredException) { + peerCertificateVerificationError = new CertificateVerificationError(Type.Expired); + return; + } + } + + } + + + @Override + public boolean setClientCertificate(PKCS12Certificate cert) { + // TODO: NYI. + // It's possible this is going to change as a result of Alexey's work + // so will leave for now + return false; + } + + @Override + public void handleDataFromNetwork(ByteArray data) { + if (hasError()) { + // We have previously seen, and reported, an error. Emit again + onError.emit(); + return; + } + byte[] b = data.getData(); + + synchronized(recvMutex) { + try { + // TODO: could "encryptedReceived" already have stuff in it? + encryptedReceived.put(b); + } + catch (BufferOverflowException e) { + emitError(e, "Unable to add data to encryptedReceived"); + return; + } + } + + unwrapPendingData(); + + // Now keep checking SSLEngine until no more handshakes are required + do { + // + } while (processHandshakeStatus()); + + } + + @Override + public void handleDataFromApplication(ByteArray data) { + if (hasError()) { + // We have previously seen, and reported, an error. Emit again + onError.emit(); + return; + } + byte[] b = data.getData(); + + synchronized(sendMutex) { + try { + // Note that "plainToSend" may not be empty, because it's possible + // that calls to SSLEngine.wrap haven't yet consumed everything + // in there + switch (hack) { + case SENDING_FAKE_LT : + plainToSend.put(b); + hack = HackStatus.DISCARD_FIRST_LT; + break; + + case DISCARD_FIRST_LT: + if (b.length > 0) { + if (b[0] == (byte)'<') { + plainToSend.put(b,1,b.length - 1); + hack = HackStatus.HACK_DONE; + } + else { + emitError(null, + "First character sent after TLS started was " + + b[0] + " and not '<'"); + } + } + break; + case HACK_DONE: + plainToSend.put(b); + break; + } + } + catch (BufferOverflowException e) { + // TODO: anything else here? + emitError(e, "plainToSend.put failed"); + return; + } + } + + wrapAndSendData(); + + // Now keep checking SSLEngine until no more handshakes are required + do { + // + } while (processHandshakeStatus()); + + } + + @Override + public Certificate getPeerCertificate() { + return peerCertificate; + } + + @Override + public CertificateVerificationError getPeerCertificateVerificationError() { + return peerCertificateVerificationError; + } + + @Override + public ByteArray getFinishMessage() { + // TODO: Doesn't appear to be an obvious way to get this + // information from SSLEngine et al. For now, return null. + + return null; + } + + @Override + public String toString() { + String errors = null; + if (hasError()) { + errors = "; errors emitted:"; + for (JSSEContextError e:errorsEmitted) { + errors += "\n " + e; + } + } + + String result = + "JSSEContext with SSLEngine = " + + sslEngine + + "; handshakeCompleted=" + handshakeCompleted; + + if (errors == null) { + return result + " (no errors)"; + } + return result + errors; + } + + /** + * Construct a new JSSEContext object. + */ + public JSSEContext() { + // + } + + /** + * Reference to the SSLEngine being used + */ + private SSLEngine sslEngine; + /** + * Contains plaintext information supplied by the caller which is + * waiting to be encrypted and sent out over the socket. + */ + private ByteBuffer plainToSend; + + /** + * Contains encrypted information produced by the SSLEngine which is + * waiting to be sent over the socket + */ + private ByteBuffer wrappedToSend; + + /** + * Contains (presumably encrypted) information received from the socket + * which is waiting to be unwrapped by the SSLEngine. + */ + private ByteBuffer encryptedReceived; + + /** + * Contains data that the SSLEngine has unwrapped and is now waiting to + * be read by the caller + */ + private ByteBuffer unwrappedReceived; + /** + * Used to synchronize access to both plainToSend and wrappedToSend + */ + private Object sendMutex = new Object(); + /** + * Used to synchronize access to both encryptedReceived and unwrappedReceived + */ + private Object recvMutex = new Object(); + + /** + * The server certificate as obtained from the TLS handshake + */ + private JavaCertificate peerCertificate = null; + + /** + * The CertificateVerificationError derived from the peerCertificate. This + * may be null if no error was found. + */ + private CertificateVerificationError peerCertificateVerificationError = null; + + /** + * Used to remember what state we're in when doing the hack to overcome the + * issue of SSLEngine not starting to handshake until it's got some data + * to send + */ + private static enum HackStatus { SENDING_FAKE_LT, DISCARD_FIRST_LT, HACK_DONE } + private static HackStatus hack = HackStatus.SENDING_FAKE_LT; + + + /** + * Set up the SSLContext and JavaTrustManager that will be used for this + * JSSEContext. + * + * TODO: We probably want a way to allow callers to supply their own + * values for SSLContext and TrustManager + * + * @return an SSLContext, or null if one cannot be created. In this case, + * an error will have been emitted. + */ + private SSLContext getSSLContext() + { + try { + JavaTrustManager [] tm = new JavaTrustManager[] { new JavaTrustManager(this)}; + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init( + null, // KeyManager[] + tm, // TrustManager[] + null); // SecureRandom + return sslContext; + } + catch (SSLException e) { + emitError(e, "Couldn't create JavaTrustManager"); + } + catch (NoSuchAlgorithmException e) { + emitError(e, "Couldn't create SSLContext"); + } + catch (KeyManagementException e) { + emitError(e, "Couldn't initialise SSLContext"); + } + return null; + + } +} |