diff options
| -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; +}  | 
 Swift