diff options
Diffstat (limited to 'src/com')
-rw-r--r-- | src/com/isode/stroke/elements/TLSProceed.java | 10 | ||||
-rw-r--r-- | src/com/isode/stroke/network/JavaConnection.java | 37 | ||||
-rw-r--r-- | src/com/isode/stroke/network/JavaTLSConnectionFactory.java | 21 | ||||
-rw-r--r-- | src/com/isode/stroke/streamstack/StreamLayer.java | 15 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/Certificate.java | 2 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/CertificateVerificationError.java | 9 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/PlatformTLSFactories.java | 12 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/java/JSSEContext.java | 714 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/java/JSSEContextFactory.java | 33 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/java/JavaCertificate.java | 331 | ||||
-rw-r--r-- | src/com/isode/stroke/tls/java/JavaTrustManager.java | 151 |
11 files changed, 1299 insertions, 36 deletions
diff --git a/src/com/isode/stroke/elements/TLSProceed.java b/src/com/isode/stroke/elements/TLSProceed.java index 5966d17..4bebc64 100644 --- a/src/com/isode/stroke/elements/TLSProceed.java +++ b/src/com/isode/stroke/elements/TLSProceed.java @@ -1,10 +1,9 @@ /* * Copyright (c) 2010 Remko Tronçon - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. + * All rights reserved. */ /* - * Copyright (c) 2010, Isode Limited, London, England. + * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ @@ -12,5 +11,8 @@ package com.isode.stroke.elements; public class TLSProceed implements Element { -//FIXME: parser/serialiser + + public TLSProceed() { + // + } } diff --git a/src/com/isode/stroke/network/JavaConnection.java b/src/com/isode/stroke/network/JavaConnection.java index d014f5d..9a3b5da 100644 --- a/src/com/isode/stroke/network/JavaConnection.java +++ b/src/com/isode/stroke/network/JavaConnection.java @@ -1,7 +1,6 @@ /* * Copyright (c) 2010 Remko Tronçon - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. + * All rights reserved. */ /* * Copyright (c) 2010-2012, Isode Limited, London, England. @@ -9,23 +8,18 @@ */ package com.isode.stroke.network; -import com.isode.stroke.base.ByteArray; -import com.isode.stroke.eventloop.Event.Callback; -import com.isode.stroke.eventloop.EventLoop; -import com.isode.stroke.eventloop.EventOwner; -import java.io.BufferedReader; -import java.io.DataInputStream; -import java.io.DataOutputStream; import java.io.IOException; -import java.io.InputStreamReader; +import java.io.InputStream; import java.io.OutputStream; -import java.io.PrintWriter; import java.net.Socket; import java.util.ArrayList; import java.util.Collections; import java.util.List; -import java.util.logging.Level; -import java.util.logging.Logger; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.eventloop.Event.Callback; +import com.isode.stroke.eventloop.EventLoop; +import com.isode.stroke.eventloop.EventOwner; public class JavaConnection extends Connection implements EventOwner { @@ -33,7 +27,7 @@ public class JavaConnection extends Connection implements EventOwner { private final HostAddressPort address_; private OutputStream write_; - private BufferedReader read_; + private InputStream read_; private final List<ByteArray> writeBuffer_ = Collections.synchronizedList(new ArrayList<ByteArray>()); public Worker(HostAddressPort address) { @@ -44,7 +38,7 @@ public class JavaConnection extends Connection implements EventOwner { try { socket_ = new Socket(address_.getAddress().getInetAddress(), address_.getPort()); write_ = socket_.getOutputStream(); - read_ = new BufferedReader(new InputStreamReader(socket_.getInputStream(), "utf-8")); + read_ = socket_.getInputStream(); } catch (IOException ex) { handleConnected(true); return; @@ -75,11 +69,13 @@ public class JavaConnection extends Connection implements EventOwner { } ByteArray data = new ByteArray(); try { - while (read_.ready()) { - char[] c = new char[1024]; - int i = read_.read(c, 0, c.length); + while (read_.available() != 0) { + byte[] b = new byte[1024]; + int i = read_.read(b,0,b.length); if (i > 0) { - data.append(new String(c, 0, i)); + for (int j=0; j<i; j++) { + data.append(b[j]); + } } } } catch (IOException ex) { @@ -107,7 +103,7 @@ public class JavaConnection extends Connection implements EventOwner { private void handleConnected(final boolean error) { eventLoop_.postEvent(new Callback() { public void run() { - onConnectFinished.emit(error); + onConnectFinished.emit(Boolean.valueOf(error)); } }); } @@ -182,4 +178,5 @@ public class JavaConnection extends Connection implements EventOwner { private boolean disconnecting_ = false; private Socket socket_; private Worker worker_; + } diff --git a/src/com/isode/stroke/network/JavaTLSConnectionFactory.java b/src/com/isode/stroke/network/JavaTLSConnectionFactory.java new file mode 100644 index 0000000..56ef917 --- /dev/null +++ b/src/com/isode/stroke/network/JavaTLSConnectionFactory.java @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2012, Isode Limited, London, England. + * All rights reserved. + */ + +package com.isode.stroke.network; + +import com.isode.stroke.eventloop.EventLoop; + +public class JavaTLSConnectionFactory implements ConnectionFactory { + + public JavaTLSConnectionFactory(EventLoop eventLoop) { + this.eventLoop = eventLoop; + } + + public Connection createConnection() { + return JavaConnection.create(eventLoop); + } + + private final EventLoop eventLoop; +} diff --git a/src/com/isode/stroke/streamstack/StreamLayer.java b/src/com/isode/stroke/streamstack/StreamLayer.java index 3337e0c..ac9538b 100644 --- a/src/com/isode/stroke/streamstack/StreamLayer.java +++ b/src/com/isode/stroke/streamstack/StreamLayer.java @@ -1,10 +1,9 @@ /* * Copyright (c) 2010 Remko Tronçon - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. + * All rights reserved. */ /* - * Copyright (c) 2010-2011, Isode Limited, London, England. + * Copyright (c) 2010-2012, Isode Limited, London, England. * All rights reserved. */ @@ -44,6 +43,16 @@ public abstract class StreamLayer implements LowLayer, HighLayer { assert childLayer != null; childLayer.writeData(data); } + @Override + public String toString() { + String className = this.getClass().getSimpleName(); + + // Include actual StreamLayer type based on class name of the object + return className + + "; " + + " parentLayer: " + parentLayer + + "; childLayer: " + childLayer; + } private HighLayer parentLayer; private LowLayer childLayer; diff --git a/src/com/isode/stroke/tls/Certificate.java b/src/com/isode/stroke/tls/Certificate.java index 3f22809..de23f94 100644 --- a/src/com/isode/stroke/tls/Certificate.java +++ b/src/com/isode/stroke/tls/Certificate.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 Isode Limited, London, England. + * Copyright (c) 2011-2012 Isode Limited, London, England. * All rights reserved. */ /* diff --git a/src/com/isode/stroke/tls/CertificateVerificationError.java b/src/com/isode/stroke/tls/CertificateVerificationError.java index e7f71c5..0aca027 100644 --- a/src/com/isode/stroke/tls/CertificateVerificationError.java +++ b/src/com/isode/stroke/tls/CertificateVerificationError.java @@ -1,10 +1,9 @@ /* * Copyright (c) 2010 Remko Tronçon - * Licensed under the GNU General Public License v3. - * See Documentation/Licenses/GPLv3.txt for more information. + * All rights reserved. */ /* - * Copyright (c) 2011, Isode Limited, London, England. + * Copyright (c) 2011-2012, Isode Limited, London, England. * All rights reserved. */ package com.isode.stroke.tls; @@ -26,7 +25,7 @@ public class CertificateVerificationError implements Error { InvalidSignature, InvalidCA, InvalidServerIdentity, - }; + } public CertificateVerificationError(Type type) { if (type == null) { @@ -35,5 +34,5 @@ public class CertificateVerificationError implements Error { this.type = type; } public final Type type; -}; +} diff --git a/src/com/isode/stroke/tls/PlatformTLSFactories.java b/src/com/isode/stroke/tls/PlatformTLSFactories.java index 1f8e136..f5b40d1 100644 --- a/src/com/isode/stroke/tls/PlatformTLSFactories.java +++ b/src/com/isode/stroke/tls/PlatformTLSFactories.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011 Isode Limited, London, England. + * Copyright (c) 2012 Isode Limited, London, England. * All rights reserved. */ /* @@ -8,9 +8,15 @@ */ package com.isode.stroke.tls; +import com.isode.stroke.tls.java.JSSEContextFactory; + public class PlatformTLSFactories { - public TLSContextFactory getTLSContextFactory() { - /*FIXME: Implement*/ + public TLSContextFactory getTLSContextFactory() { + // TODO: JSSEContextFactory is implemented, and so uncommenting + // this line will result in the client attempting TLS handshakes, but + // other support is required inside CoreClient etc. and so for the + // moment we just return null + //return new JSSEContextFactory(); return null; } 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; + + } +} diff --git a/src/com/isode/stroke/tls/java/JSSEContextFactory.java b/src/com/isode/stroke/tls/java/JSSEContextFactory.java new file mode 100644 index 0000000..0ddb4fd --- /dev/null +++ b/src/com/isode/stroke/tls/java/JSSEContextFactory.java @@ -0,0 +1,33 @@ +/* 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 com.isode.stroke.tls.TLSContext; +import com.isode.stroke.tls.TLSContextFactory; + +/** + * Concrete implementation of a TLSContextFactory which uses SSLEngine + * and maybe other stuff? ..tbs... + * + */ +public class JSSEContextFactory implements TLSContextFactory { + + @Override + public boolean canCreate() { + return true; + } + + @Override + public TLSContext createTLSContext() { + return new JSSEContext(); + } + +} diff --git a/src/com/isode/stroke/tls/java/JavaCertificate.java b/src/com/isode/stroke/tls/java/JavaCertificate.java new file mode 100644 index 0000000..5b326b9 --- /dev/null +++ b/src/com/isode/stroke/tls/java/JavaCertificate.java @@ -0,0 +1,331 @@ +/* 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.security.cert.CertificateEncodingException; +import java.security.cert.CertificateParsingException; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.security.auth.x500.X500Principal; + +import com.isode.stroke.base.ByteArray; +import com.isode.stroke.tls.Certificate; + +/** + * This class wraps a java.security.cert.X509Certificate to fulfil + * the requirements of the com.isode.stroke.tls.Certificate class. + */ +public class JavaCertificate extends Certificate { + private X509Certificate x509Certificate; + + // There's no ASN.1 help for this in standard Java SDK so for the + // moment we'll hard-code in the values + /** + * ASN.1 encoded representation of OID "1.3.6.1.5.5.7.8.5" + */ + protected static final byte[] ENCODED_ID_ON_XMPPADD_OID = + new byte[] { 0x06, 0x08, 0x21, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x05 }; + + /** + * ASN.1 encoded representation of OID "1.3.6.1.5.5.7.8.7" + */ + protected static final byte[] ENCODED_ID_ON_DNSSRV_OID = + new byte[] { 0x06, 0x08, 0x21, 0x06, 0x01, 0x05, 0x05, 0x07, 0x08, 0x07 }; + + + private static enum GeneralNameType { + OTHERNAME(0), + RFC822NAME(1), + DNSNAME(2), + X400ADDRESS(3), + DIRECTORYNAME(4), + EDIPARTYNAME(5), + UNIFORMRESOURCEIDENTIFIER(6), + IPADDRESS(7), + REGISTEREDID(8); + + private int val; + private GeneralNameType(int v) { + this.val = v; + } + static GeneralNameType getValue(int x) { + for (GeneralNameType g:values()) { + if (g.val == x) { + return g; + } + } + return null; + } + } + + private void processSubjectAlternativeNames() { + + Collection<List<?>> sans = null; + + try { + // Process subject alternative names. This returns a sequence + // of general names + sans = x509Certificate.getSubjectAlternativeNames(); + } + catch (CertificateParsingException e) { + // Leave all the subjectAltNames unparsed + return; + } + + if (sans == null) { + // No subjectAltNames + return; + } + + for (List<?> san : sans) { + // Each general name element contains an Integer representing the + // name type, and either a String or byte array containing the + // value + Integer type = (Integer)san.get(0); + GeneralNameType nameType = GeneralNameType.getValue(type.intValue()); + switch (nameType) { + case DNSNAME: // String + dnsNames_.add((String)san.get(1)); + break; + case OTHERNAME: // DER + byte[] encoded = (byte[])san.get(1); + // TODO: what you get here is something like + // LBER_SEQUENCE, length = 31 : + // tag : 0x06 (LBER_OID), length = 8 + // value = 1.3.6.1.5.5.7.8.5 (i.e. ID_ON_XMPPADDR_OID) + // bytes = [00]=2B [01]=06 [02]=01 [03]=05 [04]=05 [05]=07 [06]=08 [07]=05 + // + // CONTEXT[0], length = 19 : + // CONTEXT[0], length = 17 : + // tag : 0x0C UNIVERSAL[12] primitive, length = 15 + // value = "funky.isode.net" + // bytes = [00]=66 [01]=75 [02]=6E [03]=6B [04]=79 [05]=2E [06]=69 [07]=73 + // [08]=6F [09]=64 [0A]=65 [0B]=2E [0C]=6E [0D]=65 [0E]=74 + // + // And a corresponding thing for a DNSSRV SAN. + // However, there's no general ASN.1 decoder in the standard + // java library, so we will have to implement our own. For + // now, we ignore these values. + + break; + case DIRECTORYNAME: // String + case IPADDRESS: // String + case REGISTEREDID: // String representation of an OID + case RFC822NAME: // String + case UNIFORMRESOURCEIDENTIFIER: // String + case EDIPARTYNAME: // DER + case X400ADDRESS: // DER + default: + // Other types of subjectalt names are ignored + break; + } + + } + + } + + /** + * Construct a new JavaCertificate by parsing an X509Certificate + * + * @param x509Cert an X509Certificate, which must not be null + */ + public JavaCertificate(X509Certificate x509Cert) + { + if (x509Cert == null) { + throw new NullPointerException("x509Cert must not be null"); + } + x509Certificate = x509Cert; + + dnsNames_ = new ArrayList<String>(); + srvNames_ = new ArrayList<String>(); + xmppNames_ = new ArrayList<String>(); + + processSubjectAlternativeNames(); + + + } + + /** + * Return a reference to the X509Certificate object that this + * JavaCertificate is wrapping. + * + * @return an X509Certificate (won't be null). + */ + public X509Certificate getX509Certificate() { + return x509Certificate; + } + + /** + * Gets a String representation of the certificate subjectname + * + * @return certificate subject name, e.g. "CN=harry,O=acme" + */ + @Override + public String getSubjectName() { + return x509Certificate.getSubjectX500Principal().toString(); + } + + /** + * Returns a list of all the commonname values from the certificate's + * subjectDN. For example, if the subjectDN is "CN=fred,O=acme,CN=bill" + * then the list returned would contain "fred" and "bill" + * + * @return a list containing the Strings representing common name values + * in the server certificate's subjectDN. Will never return null, but may + * return an empty list. + */ + @Override + public List<String> getCommonNames() { + ArrayList<String> result = new ArrayList<String>(); + + + // There isn't a convenient way to extract commonname values from + // the certificate's subject DN (short of parsing the encoded value + // ourselves). So instead, we get a String version, ensuring that + // any CN values have a prefix we can recognize (we could probably + // rely on "CN" but this allows us to have a more distinctive value) + + X500Principal p = x509Certificate.getSubjectX500Principal(); + Map<String, String> cnMap = new HashMap<String, String>(); + + // Request that the returned String will use our label for any values + // with the commonName OID + cnMap.put(cnOID, cnLabel); + String s = p.getName("RFC2253",cnMap); + + String cnPrefix = cnLabel + "="; + + int x = s.indexOf(cnPrefix); + if (x == -1) { + return result; // No CN values to add + } + + // Crude attempt to split, noting that this may result in values + // that contain an escaped comma being chopped between more than one + // element, so we need to go through this subsequently and handle that.. + String[] split=s.split(","); + + boolean inQuote = false; + boolean escape = false; + + int e = 0; + String field = ""; + + while (e < split.length) { + String element = split[e]; + int quoteCount = 0; + for (int i=0; i<element.length(); i++) { + char c = element.charAt(i); + if (c == '"') { + quoteCount++; + } + } + escape = (element.endsWith("\\")); + + inQuote = ((quoteCount % 2) == 1); + if (!inQuote && !escape) { + // We got to the end of a field + field += element; + if (field.startsWith(cnPrefix)) { + result.add(field.substring(cnPrefix.length())); + } + field = ""; + } + else { + // the split has consumed a comma that was part of a quoted + // String. + field = field + element + ","; + } + e++; + } + return result; + } + + /** + * Returns a list of all the SRV values held in "OTHER" type subjectAltName + * fields in the server's certificate. + * + * @return a list containing the Strings representing SRV subjectAltName + * values from the server certificate. Will never return null, but may + * return an empty list. + */ + @Override + public List<String> getSRVNames() { + // TODO: At the moment it will always return + // an empty list -see processSubjectAlternativeNames() + return srvNames_; + } + + /** + * Returns a list of all the DNS subjectAltName values from the server's + * certificate. + * + * @return a list containing the Strings representing DNS subjectAltName + * values from the server certificate. Will never return null, but may + * return an empty list. + */ + @Override + public List<String> getDNSNames() { + return dnsNames_; + } + + /** + * Returns a list of all the XMPP values held in "OTHER" type subjectAltName + * fields in the server's certificate. + * + * @return a list containing the Strings representing XMPP subjectAltName + * values from the server certificate. Will never return null, but may + * return an empty list. + */ + @Override + public List<String> getXMPPAddresses() { + // TODO: At the moment it will always return + // an empty list -see processSubjectAlternativeNames() + return xmppNames_; + } + + /** + * Return the encoded representation of the certificate + * + * @return the DER encoding of the certificate. Will return null if + * the certificate is not valid. + */ + @Override + public ByteArray toDER() { + // TODO Auto-generated method stub + try { + byte[] r = x509Certificate.getEncoded(); + return new ByteArray(r); + } + catch (CertificateEncodingException e) { + return null; + } + } + + private List<String> dnsNames_ = null; + private List<String> srvNames_ = null; + private List<String> xmppNames_ = null; + + /** + * OID for commonName + */ + private final static String cnOID = "2.5.4.3"; + /** + * String to be used to identify commonName values in a DN. + */ + private final static String cnLabel = "COMMONNAME"; + +} diff --git a/src/com/isode/stroke/tls/java/JavaTrustManager.java b/src/com/isode/stroke/tls/java/JavaTrustManager.java new file mode 100644 index 0000000..4be7edf --- /dev/null +++ b/src/com/isode/stroke/tls/java/JavaTrustManager.java @@ -0,0 +1,151 @@ +/* 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.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; + +/** + * A concrete X509TrustManager implementation which provides a trust manager + * based on the default java "pkcs12" keystore. + */ +public class JavaTrustManager implements X509TrustManager { + + /** + * Construct a new object + * @param jsseContext reference to JSSEContext; must not be null. + * + * @throws SSLException if it was not possible to initialise the + * TrustManager or KeyStore + */ + JavaTrustManager(JSSEContext jsseContext) throws SSLException { + + if (jsseContext == null) { + throw new NullPointerException("JSSEContext may not be null"); + } + this.jsseContext = jsseContext; + + try { + // create a "default" JSSE X509TrustManager. + + KeyStore ks = KeyStore.getInstance("PKCS12"); + /* + + // This is how you could load trust anchors + ks.load(new FileInputStream("trustedCerts"), + "passphrase".toCharArray()); + */ + TrustManagerFactory tmf = + TrustManagerFactory.getInstance("PKIX"); + tmf.init(ks); + + TrustManager tms [] = tmf.getTrustManagers(); + + /* + * Iterate over the returned trustmanagers, look + * for an instance of X509TrustManager. If found, + * use that as our "default" trust manager. + */ + for (int i = 0; i < tms.length; i++) { + if (tms[i] instanceof X509TrustManager) { + pkixTrustManager = (X509TrustManager) tms[i]; + return; + } + } + /* + * Find some other way to initialize, or else we have to fail the + * constructor. + */ + throw new SSLException("Couldn't initialize"); + } + catch (KeyStoreException e) { + throw new SSLException(e); + } + catch (NoSuchAlgorithmException e) { + throw new SSLException(e); + } + } + + @Override + public void checkClientTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + // It's not expected that a Stroke application will ever be in the + // position of checking client certificates. Just delegate to + // default trust manager + pkixTrustManager.checkClientTrusted(chain, authType); + + } + + @Override + public void checkServerTrusted(X509Certificate[] chain, String authType) + throws CertificateException { + CertificateException certificateException = null; + + + // TODO: + // Note that we don't call the superclass method here yet, because + // it will fail with like this until the TrustManagerFactory has + // been initialised with a suitable list of trust anchors + // java.lang.RuntimeException: Unexpected error: + // java.security.InvalidAlgorithmParameterException: the trustAnchors parameter must be non-empty + + /* + try { + pkixTrustManager.checkServerTrusted(chain, authType); + } catch (CertificateException e) { + certificateException = e; + } + catch (Exception e) { + emitError(e,"checkServerTrusted failed"); + } + */ + + // TODO: The only type of verification done is the certificate validity. + // Need to make "checkServerTrusted" do certificate verification properly + // and pass in an appropriate CertificateException + if (chain != null && chain.length > 0) { + try { + chain[0].checkValidity(); + } + catch (CertificateException e) { + certificateException = e; + } + } + + jsseContext.setPeerCertificateInfo(chain, certificateException); + + } + + @Override + public X509Certificate[] getAcceptedIssuers() { + // TODO Auto-generated method stub + return null; + } + + /* + * The default PKIX X509TrustManager, to which decisions can be + * delegated when we don't make them ourselves. + */ + X509TrustManager pkixTrustManager; + + /** + * The object who wants to know what server certificates appear + */ + JSSEContext jsseContext; +} |