summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/com/isode/stroke/elements/TLSProceed.java10
-rw-r--r--src/com/isode/stroke/network/JavaConnection.java37
-rw-r--r--src/com/isode/stroke/network/JavaTLSConnectionFactory.java21
-rw-r--r--src/com/isode/stroke/streamstack/StreamLayer.java15
-rw-r--r--src/com/isode/stroke/tls/Certificate.java2
-rw-r--r--src/com/isode/stroke/tls/CertificateVerificationError.java9
-rw-r--r--src/com/isode/stroke/tls/PlatformTLSFactories.java12
-rw-r--r--src/com/isode/stroke/tls/java/JSSEContext.java714
-rw-r--r--src/com/isode/stroke/tls/java/JSSEContextFactory.java33
-rw-r--r--src/com/isode/stroke/tls/java/JavaCertificate.java331
-rw-r--r--src/com/isode/stroke/tls/java/JavaTrustManager.java151
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;
+}