added SSL
[org.ibex.net.git] / src / org / ibex / net / SSL.java
diff --git a/src/org/ibex/net/SSL.java b/src/org/ibex/net/SSL.java
new file mode 100644 (file)
index 0000000..04519cd
--- /dev/null
@@ -0,0 +1,1010 @@
+/*
+ * org.ibex.net.SSL - By Brian Alliet
+ * Copyright (C) 2004 Brian Alliet
+ * 
+ * Based on TinySSL by Adam Megacz
+ * Copyright (C) 2003 Adam Megacz <adam@xwt.org> all rights reserved.
+ * 
+ * You may modify, copy, and redistribute this code under the terms of
+ * the GNU Lesser General Public License version 2.1, with the exception
+ * of the portion of clause 6a after the semicolon (aka the "obnoxious
+ * relink clause")
+ */
+
+package org.ibex.net;
+
+import org.ibex.crypto.*;
+import java.security.SecureRandom;
+
+import java.net.Socket;
+import java.net.SocketException;
+
+import java.io.*;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.Random;
+import java.util.Vector;
+
+// FEATURE: Server socket
+
+public class SSL extends Socket {
+    private String hostname;
+    
+    private int negotiated;
+    
+    private boolean tls = true;
+    private boolean sha;
+    
+    private final DataInputStream rawIS;
+    private final DataOutputStream rawOS;
+    
+    private final InputStream sslIS;
+    private final OutputStream sslOS;
+    
+    private byte[] sessionID;
+
+    private Digest clientWriteMACDigest;        
+    private Digest serverWriteMACDigest;        
+    private byte[] masterSecret;
+    
+    private RC4 writeRC4;
+    private RC4 readRC4;
+    
+    private long serverSequenceNumber;
+    private long clientSequenceNumber;
+    
+    private int warnings;
+    private boolean closed;
+    
+    // These are only used during negotiation
+    private byte[] serverRandom;
+    private byte[] clientRandom;
+    private byte[] preMasterSecret;
+    
+    // Buffers
+    private byte[] mac;
+    
+    private byte[] pending = new byte[16384];
+    private int pendingStart;
+    private int pendingLength;
+
+    private byte[] sendRecordBuf = new byte[16384];
+    
+    private int handshakeDataStart;
+    private int handshakeDataLength;
+    private byte[] readRecordBuf = new byte[16384+20];   // 20 == sizeof(sha1 hash)
+    private byte[] readRecordScratch = new byte[16384+20];
+    
+    private ByteArrayOutputStream handshakesBuffer;
+    
+    // End Buffers
+    
+    // Static variables
+    private final static byte[] pad1 = new byte[48];
+    private final static byte[] pad2 = new byte[48];
+    private final static byte[] pad1_sha = new byte[40];
+    private final static byte[] pad2_sha = new byte[40];
+    
+    static {
+        for(int i=0; i<pad1.length; i++) pad1[i] = (byte)0x36;
+        for(int i=0; i<pad2.length; i++) pad2[i] = (byte)0x5C;
+        for(int i=0; i<pad1_sha.length; i++) pad1_sha[i] = (byte)0x36;
+        for(int i=0; i<pad2_sha.length; i++) pad2_sha[i] = (byte)0x5C;
+    }
+    
+    private final static Hashtable caKeys = new Hashtable();
+    private static VerifyCallback verifyCallback;
+    
+    //
+    // Constructors
+    //
+    public SSL(String host) throws IOException { this(host,443); }
+    public SSL(String host, int port) throws IOException { this(host,port,true); }
+    public SSL(String host, int port, boolean negotiate) throws IOException { this(host,port,negotiate,null); }
+    public SSL(String host, int port, State state) throws IOException { this(host,port,true,state); }
+    public SSL(String host, int port, boolean negotiate, State state) throws IOException {
+        super(host,port);
+        hostname = host;
+        rawIS = new DataInputStream(new BufferedInputStream(super.getInputStream()));
+        rawOS = new DataOutputStream(new BufferedOutputStream(super.getOutputStream()));
+        sslIS = new SSLInputStream();
+        sslOS = new SSLOutputStream();
+        if(negotiate) negotiate(state);
+    }
+
+    public synchronized void setTLS(boolean b) { if(negotiated!=0) throw new IllegalStateException("already negotiated"); tls = b; }
+    
+    public void negotiate() throws IOException { negotiate(null); }
+    public synchronized void negotiate(State state) throws IOException {
+        if(negotiated != 0) throw new IllegalStateException("already negotiated");
+        
+        handshakesBuffer = new ByteArrayOutputStream();
+        
+        try {
+            sendClientHello(state != null ? state.sessionID : null);
+            flush();
+            debug("sent ClientHello (" + (tls?"TLSv1.0":"SSLv3.0")+")");
+            
+            receiveServerHello();
+            debug("got ServerHello (" + (tls?"TLSv1.0":"SSLv3.0")+")");
+            
+            boolean resume = 
+                state != null && sessionID.length == state.sessionID.length && 
+                eq(state.sessionID,0,sessionID,0,sessionID.length);
+            
+            if(resume) 
+                negotiateResume(state);
+            else
+                negotiateNew();
+            
+            // we're done with these now
+            clientRandom = serverRandom = preMasterSecret = null;
+            handshakesBuffer = null;
+            
+            log("Negotiation with " + hostname + " complete (" + (tls?"TLSv1.0":"SSLv3.0")+")");
+        } finally {
+            if((negotiated & 3) != 3) {
+                negotiated = 0;
+                try { super.close(); } catch(IOException e) { /* ignore */ }
+                closed = true;
+            }
+        }
+    }
+    
+    private void negotiateResume(State state) throws IOException {
+        masterSecret = state.masterSecret;
+        
+        initCrypto();
+        log("initializec crypto");
+        
+        receiveChangeCipherSpec();
+        debug("Received ChangeCipherSpec");
+        negotiated |= 2;
+        receieveFinished();
+        debug("Received Finished");
+        
+        sendChangeCipherSpec();
+        debug("Sent ChangeCipherSpec");
+        negotiated |= 1;
+        sendFinished();
+        debug("Sent Finished");
+    }
+    
+    private void negotiateNew() throws IOException {
+        X509.Certificate[] certs = receiveServerCertificates();
+        debug("got Certificate");
+        
+        boolean gotCertificateRequest = false;
+        OUTER: for(;;) {
+            byte[] buf = readHandshake();
+            switch(buf[0]) {
+            case 14: // ServerHelloDone
+                if(buf.length != 4) throw new Exn("ServerHelloDone contained trailing garbage");
+                debug("got ServerHelloDone");
+                break OUTER;
+            case 13: // CertificateRequest
+                debug("Got a CertificateRequest message but we don't suport client certificates");
+                gotCertificateRequest = true;
+                break;
+            default:
+                throw new Exn("unknown handshake type " + buf[0]);
+            }
+        }
+        
+        if(gotCertificateRequest)
+            sendHandshake((byte)11,new byte[3]); // send empty cert list
+        
+        try {
+            if(!hostname.equalsIgnoreCase(certs[0].getCN()))
+                throw new Exn("Certificate is for " + certs[0].getCN() + " not " + hostname);
+            verifyCerts(certs);
+        } catch(Exn e) {
+            if(verifyCallback == null) throw e;
+            synchronized(SSL.class) {
+                if(!verifyCallback.checkCerts(certs,hostname,e)) throw e;
+            }
+        }
+        
+        computeMasterSecret();
+        
+        sendClientKeyExchange(certs[0]);
+        debug("sent ClientKeyExchange");
+        
+        initCrypto();
+        
+        sendChangeCipherSpec();
+        debug("sent ChangeCipherSpec");
+        negotiated |= 1;
+        sendFinished();
+        debug("sent Finished");
+        flush();
+        
+        receiveChangeCipherSpec();
+        debug("got ChangeCipherSpec");
+        negotiated |= 2;
+        receieveFinished();
+        debug("got Finished");
+    }
+    
+    public State getSessionState() {
+        if((negotiated&3)!=3 || !closed || warnings != 0) return null;
+        return new State(sessionID,masterSecret);
+    }
+    public boolean isActive() { return !closed; }
+    public boolean isNegotiated() { return (negotiated&3) == 3; }
+    
+    private void sendClientHello(byte[] sessionID) throws IOException {
+        if(sessionID != null && sessionID.length > 256) throw new IllegalArgumentException("sessionID");
+        // 2 = version, 32 = randomvalue, 1 = sessionID size, 2 = cipher list size, 4 = the two ciphers,
+        // 2 = compression length/no compression
+        int p = 0;
+        byte[] buf = new byte[2+32+1+(sessionID == null ? 0 : sessionID.length)+2+2+4];
+        buf[p++] = 0x03; // major version
+        buf[p++] = tls ? (byte)0x01 : (byte)0x00;
+        
+        clientRandom = new byte[32];
+        int now = (int)(System.currentTimeMillis() / 1000L);
+        new Random().nextBytes(clientRandom);
+        clientRandom[0] = (byte)(now>>>24);
+        clientRandom[1] = (byte)(now>>>16);
+        clientRandom[2] = (byte)(now>>>8);
+        clientRandom[3] = (byte)(now>>>0);
+        System.arraycopy(clientRandom,0,buf,p,32);
+        p += 32;
+        
+        buf[p++] = sessionID != null ? (byte)sessionID.length : 0;
+        if(sessionID != null && sessionID.length != 0) System.arraycopy(sessionID,0,buf,p,sessionID.length);
+        p += sessionID != null ? sessionID.length : 0;
+        buf[p++] = 0x00; // 4 bytes of ciphers
+        buf[p++] = 0x04;
+        buf[p++] = 0x00; // SSL_RSA_WITH_RC4_128_SHA
+        buf[p++] = 0x05;
+        buf[p++] = 0x00; // SSL_RSA_WITH_RC4_128_MD5
+        buf[p++] = 0x04; 
+        
+        buf[p++] = 0x01;
+        buf[p++] = 0x00;
+                
+        sendHandshake((byte)1,buf);
+        flush();
+    }
+    
+    private void receiveServerHello() throws IOException {
+        // ServerHello
+        byte[] buf = readHandshake();
+        if(buf[0] != 2) throw new Exn("expected a ServerHello message");
+        
+        if(buf.length < 6 + 32 + 1) throw new Exn("ServerHello too small");
+        if(buf.length < 6 + 32 + 1 + buf[6+32] + 3) throw new Exn("ServerHello too small " + buf.length+" "+buf[6+32]); 
+        
+        if(buf[4] != 0x03 || !(buf[5]==0x00 || buf[5]==0x01)) throw new Exn("server wants to use version " + buf[4] + "." + buf[5]);
+        tls = buf[5] == 0x01;
+        int p = 6;
+        serverRandom = new byte[32];
+        System.arraycopy(buf,p,serverRandom,0,32);
+        p += 32;
+        sessionID = new byte[buf[p++]&0xff];
+        if(sessionID.length != 0) System.arraycopy(buf,p,sessionID,0,sessionID.length);
+        p += sessionID.length;
+        int cipher = ((buf[p]&0xff)<<8) | (buf[p+1]&0xff);
+        p += 2;
+        switch(cipher) {
+            case 0x0004: sha = false; debug("Using SSL_RSA_WITH_RC4_128_MD5"); break;
+            case 0x0005: sha = true;  debug("Using SSL_RSA_WITH_RC4_128_SHA"); break;
+            default: throw new Exn("Unsupported cipher " + cipher);
+        }
+        mac = new byte[sha ? 20 : 16];
+        if(buf[p++] != 0x0) throw new Exn("unsupported compression " + buf[p-1]);
+    }
+    
+    private X509.Certificate[] receiveServerCertificates() throws IOException {
+        byte[] buf = readHandshake();
+        if(buf[0] != 11) throw new Exn("expected a Certificate message");
+        if((((buf[4]&0xff)<<16)|((buf[5]&0xff)<<8)|((buf[6]&0xff)<<0)) != buf.length-7) throw new Exn("size mismatch in Certificate message");
+        int p = 7;
+        int count = 0;
+        
+        for(int i=p;i<buf.length-3;i+=((buf[p+0]&0xff)<<16)|((buf[p+1]&0xff)<<8)|((buf[p+2]&0xff)<<0)) count++;
+        if(count == 0) throw new Exn("server didn't provide any certificates");
+        X509.Certificate[] certs = new X509.Certificate[count];
+        count = 0;
+        while(p < buf.length) {
+            int len = ((buf[p+0]&0xff)<<16)|((buf[p+1]&0xff)<<8)|((buf[p+2]&0xff)<<0);
+            p += 3;
+            if(p + len > buf.length) throw new Exn("Certificate message cut short");
+            certs[count++] = new X509.Certificate(new ByteArrayInputStream(buf,p,len));
+            p += len;
+        }
+        return certs;
+    }
+    
+    private void sendClientKeyExchange(X509.Certificate serverCert) throws IOException {
+        byte[] encryptedPreMasterSecret;
+        RSA.PublicKey pks = serverCert.getRSAPublicKey();
+        PKCS1 pkcs1 = new PKCS1(new RSA(pks.modulus,pks.exponent,false),random);
+        encryptedPreMasterSecret = pkcs1.encode(preMasterSecret);
+        byte[] buf;
+        if(tls) {
+            buf = new byte[encryptedPreMasterSecret.length+2];
+            buf[0] = (byte) (encryptedPreMasterSecret.length>>>8);
+            buf[1] = (byte) (encryptedPreMasterSecret.length>>>0);
+            System.arraycopy(encryptedPreMasterSecret,0,buf,2,encryptedPreMasterSecret.length);
+        } else {
+            // ugh... netscape didn't send the length bytes and now every SSLv3 implementation
+            // must implement this bug
+            buf = encryptedPreMasterSecret;
+        }
+        sendHandshake((byte)16,buf);
+    }
+    
+    private void sendChangeCipherSpec() throws IOException {
+        sendRecord((byte)20,new byte[] { 0x01 });
+    }
+    
+    private void computeMasterSecret() {
+        preMasterSecret = new byte[48];
+        preMasterSecret[0] = 0x03; // version_high
+        preMasterSecret[1] = tls ? (byte) 0x01 : (byte) 0x00; // version_low
+        randomBytes(preMasterSecret,2,46);
+        
+        if(tls) {
+            masterSecret = tlsPRF(48,preMasterSecret,getBytes("master secret"),concat(clientRandom,serverRandom));
+        } else {
+            masterSecret = concat(new byte[][] {
+                    md5(new byte[][] { preMasterSecret,
+                            sha1(new byte[][] { new byte[] { 0x41 }, preMasterSecret, clientRandom, serverRandom })}),
+                            md5(new byte[][] { preMasterSecret,
+                                    sha1(new byte[][] { new byte[] { 0x42, 0x42 }, preMasterSecret, clientRandom, serverRandom })}),
+                                    md5(new byte[][] { preMasterSecret,
+                                            sha1(new byte[][] { new byte[] { 0x43, 0x43, 0x43 }, preMasterSecret, clientRandom, serverRandom })})
+            } );    
+        }
+    }
+    
+    public void initCrypto() {
+        byte[] keyMaterial;
+        
+        if(tls) {
+            keyMaterial = tlsPRF(
+                    (mac.length + 16 + 0)*2, // MAC len + key len + iv len
+                    masterSecret,
+                    getBytes("key expansion"),
+                    concat(serverRandom,clientRandom)
+            );
+        } else {
+            keyMaterial = new byte[] { };
+            for(int i=0; keyMaterial.length < 72; i++) {
+                byte[] crap = new byte[i + 1];
+                for(int j=0; j<crap.length; j++) crap[j] = (byte)(((byte)0x41) + ((byte)i));
+                keyMaterial = concat(new byte[][] { keyMaterial,
+                        md5(new byte[][] { masterSecret,
+                                sha1(new byte[][] { crap, masterSecret, serverRandom, clientRandom }) }) });
+            }            
+        }
+
+        byte[] clientWriteMACSecret = new byte[mac.length];
+        byte[] serverWriteMACSecret = new byte[mac.length];
+        byte[] clientWriteKey = new byte[16];
+        byte[] serverWriteKey = new byte[16];
+        
+        int p = 0;
+        System.arraycopy(keyMaterial, p, clientWriteMACSecret, 0, mac.length); p += mac.length;
+        System.arraycopy(keyMaterial, p, serverWriteMACSecret, 0, mac.length); p += mac.length;
+        System.arraycopy(keyMaterial, p, clientWriteKey, 0, 16); p += 16; 
+        System.arraycopy(keyMaterial, p, serverWriteKey, 0, 16); p += 16;
+        
+        Digest inner;
+        
+        writeRC4 = new RC4(clientWriteKey);
+        inner = sha ? (Digest)new SHA1() : (Digest)new MD5();
+        clientWriteMACDigest = tls ? (Digest) new HMAC(inner,clientWriteMACSecret) : (Digest)new SSLv3HMAC(inner,clientWriteMACSecret);
+        
+        readRC4 = new RC4(serverWriteKey);
+        inner = sha ? (Digest)new SHA1() : (Digest)new MD5();
+        serverWriteMACDigest = tls ? (Digest)new HMAC(inner,serverWriteMACSecret) : (Digest)new SSLv3HMAC(inner,serverWriteMACSecret);
+    }
+    
+    private void sendFinished() throws IOException {
+        byte[] handshakes = handshakesBuffer.toByteArray();
+        if(tls) {
+            sendHandshake((byte)20, tlsPRF(
+                    12,
+                    masterSecret,
+                    getBytes("client finished"),
+                    concat(md5(handshakes),sha1(handshakes))));
+            
+        } else {
+            sendHandshake((byte)20, concat(new byte[][] { 
+                    md5(new byte[][] { masterSecret, pad2, 
+                                       md5(new byte[][] { handshakes, new byte[] { (byte)0x43, (byte)0x4C, (byte)0x4E, (byte)0x54 },
+                                                          masterSecret, pad1 }) }),
+                    sha1(new byte[][] { masterSecret, pad2_sha,
+                                       sha1(new byte[][] { handshakes, new byte[] { (byte)0x43, (byte)0x4C, (byte)0x4E, (byte)0x54 },
+                                                          masterSecret, pad1_sha } ) })
+                }));
+        }
+    }
+        
+    private void receiveChangeCipherSpec() throws IOException {    
+        int size = readRecord((byte)20);
+        if(size == -1) throw new Exn("got eof when expecting a ChangeCipherSpec message");
+        if(size != 1 || readRecordBuf[0] != 0x01) throw new Exn("Invalid ChangeCipherSpec message");
+    }
+    
+    private void receieveFinished() throws IOException {
+        byte[] handshakes = handshakesBuffer.toByteArray();
+        byte[] buf = readHandshake();
+        if(buf[0] != 20) throw new Exn("expected a Finished message");
+        byte[] expected;
+        
+        if(tls) {
+            if(buf.length != 4 + 12) throw new Exn("Finished message too short");
+            expected = tlsPRF(
+                    12,masterSecret,
+                    getBytes("server finished"),
+                    concat(md5(handshakes),sha1(handshakes)));
+        } else {
+            if(buf.length != 4 + 16 +20) throw new Exn("Finished message too short");
+            expected = concat(new byte[][] {
+                    md5(new byte[][] { masterSecret, pad2,
+                            md5(new byte[][] { handshakes, new byte[] { (byte)0x53, (byte)0x52, (byte)0x56, (byte)0x52 },
+                                    masterSecret, pad1 }) }),
+                                    sha1(new byte[][] { masterSecret, pad2_sha,
+                                            sha1(new byte[][] { handshakes, new byte[] { (byte)0x53, (byte)0x52, (byte)0x56, (byte)0x52 },
+                                                    masterSecret, pad1_sha } ) } ) } );
+        }
+        if(!eq(expected,0,buf,4,expected.length)) throw new Exn("server finished message mismatch");
+    }
+    
+    private void flush() throws IOException { rawOS.flush(); }
+
+    private void sendHandshake(byte type, byte[] payload) throws IOException {
+        if(payload.length > (1<<24)) throw new IllegalArgumentException("payload.length");
+        byte[] buf = new byte[4+payload.length];
+        buf[0] = type;
+        buf[1] = (byte)(payload.length>>>16);
+        buf[2] = (byte)(payload.length>>>8);
+        buf[3] = (byte)(payload.length>>>0);
+        System.arraycopy(payload,0,buf,4,payload.length);
+        handshakesBuffer.write(buf);
+        sendRecord((byte)22,buf);
+    }
+    
+    private void sendRecord(byte proto, byte[] buf) throws IOException { sendRecord(proto,buf,0,buf.length); }
+    private void sendRecord(byte proto, byte[] payload, int off, int totalLen) throws IOException {
+        int macLength = (negotiated & 1) != 0 ? mac.length : 0;
+        while(totalLen > 0) {
+            int len = min(totalLen,16384-macLength);
+            rawOS.writeByte(proto);
+            rawOS.writeShort(tls ? 0x0301 : 0x0300);
+            if((negotiated & 1) != 0) {
+                computeMAC(proto,payload,off,len,clientWriteMACDigest,clientSequenceNumber);
+                // FEATURE: Encode in place
+                writeRC4.process(payload,off,sendRecordBuf,0,len);
+                writeRC4.process(mac,0,sendRecordBuf,len,macLength);
+                rawOS.writeShort(len + macLength);
+                rawOS.write(sendRecordBuf,0, len +macLength);
+                clientSequenceNumber++;
+            } else {
+                rawOS.writeShort(len);
+                rawOS.write(payload,off,len);
+            }
+            totalLen -= len;
+            off += len;
+        }
+    }
+    
+    private byte[] readHandshake() throws IOException {
+        if(handshakeDataLength == 0) {
+            handshakeDataStart = 0;
+            handshakeDataLength = readRecord((byte)22);
+            if(handshakeDataLength == -1) throw new Exn("got eof when expecting a handshake packet");
+        }
+        byte[] buf = readRecordBuf;
+        int len = ((buf[handshakeDataStart+1]&0xff)<<16)|((buf[handshakeDataStart+2]&0xff)<<8)|((buf[handshakeDataStart+3]&0xff)<<0);
+        // Handshake messages can theoretically span multiple records, but in practice this does not occur
+        if(len > handshakeDataLength) {
+            sendAlert(true,10); // 10 == unexpected message
+            throw new Exn("handshake message size too large " + len + " vs " + (handshakeDataLength-handshakeDataStart));
+        }
+        byte[] ret = new byte[4+len];
+        System.arraycopy(buf,handshakeDataStart,ret,0,ret.length);
+        handshakeDataLength -= ret.length;
+        handshakeDataStart += ret.length;
+        handshakesBuffer.write(ret);
+        return ret;
+    }
+    
+    private int readRecord(byte reqProto) throws IOException {
+        int macLength = (negotiated & 2) != 0 ? mac.length : 0;
+        for(;;) {
+            byte proto;
+            int version, len;
+            
+            try {
+                proto = rawIS.readByte();
+            } catch(EOFException e) {
+                // this may or may not be an error. it is up to the application protocol
+                closed = true;
+                super.close();
+                throw new PrematureCloseExn();
+            }
+            try {
+                version = rawIS.readShort();
+                if(version != 0x0300 && version != 0x0301) throw new Exn("invalid version ");
+                len = rawIS.readShort();
+                if(len <= 0 || len > 16384+((negotiated&2)!=0 ? macLength : 0)) throw new Exn("invalid length " + len);
+                rawIS.readFully((negotiated&2)!=0 ? readRecordScratch : readRecordBuf,0,len);
+            } catch(EOFException e) {
+                // an EOF here is always an error (we don't pass the EOF back on to the app
+                // because it isn't a "legitimate" eof)
+                throw new Exn("Hit EOF too early");
+            }
+            
+            if((negotiated & 2) != 0) {
+                if(len < macLength) throw new Exn("packet size < macLength");
+                // FEATURE: Decode in place
+                readRC4.process(readRecordScratch,0,readRecordBuf,0,len);
+                computeMAC(proto,readRecordBuf,0,len-macLength,serverWriteMACDigest,serverSequenceNumber);
+                for(int i=0;i<macLength;i++)
+                    if(mac[i] != readRecordBuf[len-macLength+i])
+                        throw new Exn("mac mismatch");
+                len -= macLength;
+                serverSequenceNumber++;
+            }
+            
+            if(proto == reqProto) return len;
+            
+            switch(proto) {
+                case 21: { // ALERT
+                    if(len != 2) throw new Exn("invalid lengh for alert");
+                    int level = readRecordBuf[0];
+                    int desc = readRecordBuf[1];
+                    if(level == 1) {
+                        if(desc == 0) { // CloseNotify
+                            debug("Server requested connection closure");
+                            try {
+                                sendCloseNotify();
+                            } catch(SocketException e) { /* incomplete close, thats ok */ }
+                            closed = true;
+                            super.close();
+                            return -1;
+                        } else {
+                            warnings++;
+                            log("SSL ALERT WARNING: desc: " + desc);
+                        }
+                    } else if(level == 2) {
+                        throw new Exn("SSL ALERT FATAL: desc: " +desc);
+                    } else {
+                        throw new Exn("invalid alert level");
+                    }
+                    break;
+                }
+                case 22: { // Handshake
+                    int type = readRecordBuf[0];
+                    int hslen = ((readRecordBuf[1]&0xff)<<16)|((readRecordBuf[2]&0xff)<<8)|((readRecordBuf[3]&0xff)<<0);
+                    if(hslen > len - 4) throw new Exn("Multiple sequential handshake messages received after negotiation");
+                    if(type == 0) { // HellloRequest
+                        if(tls) sendAlert(false,100); // politely refuse, 100 == NoRegnegotiation
+                    } else {
+                        throw new Exn("Unexpected Handshake type: " + type);
+                    }
+                }
+                default: throw new Exn("Unexpected protocol: " + proto);
+            }
+        }
+    }
+    
+    private static void longToBytes(long l, byte[] buf, int off) {
+        for(int i=0;i<8;i++) buf[off+i] = (byte)(l>>>(8*(7-i)));
+    }
+    private void computeMAC(byte proto, byte[] payload, int off, int len, Digest digest, long sequenceNumber) {
+        if(tls) {
+            longToBytes(sequenceNumber,mac,0);
+            mac[8] = proto;
+            mac[9] = 0x03; // version
+            mac[10] = 0x01;
+            mac[11] = (byte)(len>>>8);
+            mac[12] = (byte)(len>>>0);
+            
+            digest.update(mac,0,13);
+            digest.update(payload,off,len);
+            digest.doFinal(mac,0);
+        } else {
+            longToBytes(sequenceNumber, mac, 0);
+            mac[8] = proto;
+            mac[9] = (byte)(len>>>8);
+            mac[10] = (byte)(len>>>0);
+            
+            digest.update(mac, 0, 11);
+            digest.update(payload, off, len);
+            digest.doFinal(mac, 0);
+        }
+    }
+    
+    private void sendCloseNotify() throws IOException { sendRecord((byte)21, new byte[] { 0x01, 0x00 }); }
+    private void sendAlert(boolean fatal, int message) throws IOException {
+        byte[] buf = new byte[] { fatal ? (byte)2 :(byte)1, (byte)message };
+        sendRecord((byte)21,buf);
+        flush();
+    }
+    
+    //
+    // Hash functions
+    //
+    
+    // Shared digest objects
+    private MD5 masterMD5 = new MD5();
+    private SHA1 masterSHA1 = new SHA1();
+    
+    private byte[] md5(byte[] in) { return md5( new byte[][] { in }); }
+    private byte[] md5(byte[][] inputs) {
+        masterMD5.reset();
+        for(int i=0; i<inputs.length; i++) masterMD5.update(inputs[i], 0, inputs[i].length);
+        byte[] ret = new byte[masterMD5.getDigestSize()];
+        masterMD5.doFinal(ret, 0);
+        return ret;
+    }
+    
+    private byte[] sha1(byte[] in)  { return sha1(new byte[][] { in }); }
+    private byte[] sha1(byte[][] inputs) {
+        masterSHA1.reset();
+        for(int i=0; i<inputs.length; i++) masterSHA1.update(inputs[i], 0, inputs[i].length);
+        byte[] ret = new byte[masterSHA1.getDigestSize()];
+        masterSHA1.doFinal(ret, 0);
+        return ret;
+    }
+    
+    /*  RFC-2246
+     PRF(secret, label, seed) = P_MD5(S1, label + seed) XOR P_SHA-1(S2, label + seed);
+     L_S = length in bytes of secret;
+     L_S1 = L_S2 = ceil(L_S / 2);
+     
+     The secret is partitioned into two halves (with the possibility of
+     one shared byte) as described above, S1 taking the first L_S1 bytes
+     and S2 the last L_S2 bytes.
+     
+     P_hash(secret, seed) = HMAC_hash(secret, A(1) + seed) +
+     HMAC_hash(secret, A(2) + seed) +
+     HMAC_hash(secret, A(3) + seed) + ...
+     
+     A(0) = seed
+     A(i) = HMAC_hash(secret, A(i-1))
+     */           
+    private byte[] tlsPRF(int size,byte[] secret, byte[] label, byte[] seed) {
+        if(size > 112) throw new IllegalArgumentException("size > 112");
+        seed = concat(label,seed);
+        
+        int half_length = (secret.length + 1) / 2;
+        byte[] s1 = new byte[half_length];
+        System.arraycopy(secret,0,s1,0,half_length);
+        byte[] s2 = new byte[half_length];
+        System.arraycopy(secret,secret.length - half_length, s2, 0, half_length);
+
+        Digest hmac_md5 = new HMAC(new MD5(),s1);
+        Digest hmac_sha = new HMAC(new SHA1(),s2);
+        
+        byte[] md5out = new byte[112];
+        byte[] shaout = new byte[120];
+        byte[] digest = new byte[20];
+        int n;
+        
+        n = 0;
+        hmac_md5.update(seed,0,seed.length);
+        hmac_md5.doFinal(digest,0);
+        
+        // digest == md5_a_1
+        while(n < size) {
+            hmac_md5.update(digest,0,16);
+            hmac_md5.update(seed,0,seed.length);
+            hmac_md5.doFinal(md5out,n);
+            hmac_md5.update(digest,0,16);
+            hmac_md5.doFinal(digest,0);
+            n += 16;
+        }
+        
+        n = 0;
+        hmac_sha.update(seed,0,seed.length);
+        hmac_sha.doFinal(digest,0);
+        
+        while(n < size) {
+            hmac_sha.update(digest,0,20);
+            hmac_sha.update(seed,0,seed.length);
+            hmac_sha.doFinal(shaout,n);
+            hmac_sha.update(digest,0,20);
+            hmac_sha.doFinal(digest,0);
+            n += 20;
+         }
+            
+        byte[] ret = new byte[size];
+        for(int i=0;i<size;i++) ret[i] = (byte)(md5out[i] ^ shaout[i]);
+        return ret;
+    }
+
+    public static class SSLv3HMAC extends Digest {
+        private final Digest h;
+        private final byte[] digest;
+        private final byte[] key;
+        private final int padSize;
+        
+        public int getDigestSize() { return h.getDigestSize(); }
+        
+        public SSLv3HMAC(Digest h, byte[] key) {
+            this.h = h;
+            this.key = key;
+            switch(h.getDigestSize()) {
+                case 16: padSize = 48; break;
+                case 20: padSize = 40; break;
+                default: throw new IllegalArgumentException("unsupported digest size");
+            }
+            digest = new byte[h.getDigestSize()];
+            reset();
+        }
+        public void reset() {
+            h.reset();
+            h.update(key,0,key.length);
+            h.update(pad1,0,padSize);
+        }
+        public void update(byte[] b, int off, int len) { h.update(b,off,len); }
+        public void doFinal(byte[] out, int off){
+            h.doFinal(digest,0);
+            h.update(key,0,key.length);
+            h.update(pad2,0,padSize);
+            h.update(digest,0,digest.length);
+            h.doFinal(out,off);
+            reset();
+        }
+        protected void processWord(byte[] in, int inOff) {}
+        protected void processLength(long bitLength) {}
+        protected void processBlock() {}
+    }
+    
+    //
+    // Static Methods
+    //
+    
+    private static SecureRandom random = new SecureRandom();
+    public static synchronized void randomBytes(byte[] buf, int off, int len) {
+        byte[] bytes =  new byte[len];
+        random.nextBytes(bytes);
+        System.arraycopy(bytes,0,buf,off,len);
+    }
+    
+    public static byte[] concat(byte[] a, byte[] b) { return concat(new byte[][] { a, b }); }
+    public static byte[] concat(byte[] a, byte[] b, byte[] c) { return concat(new byte[][] { a, b, c }); }
+    public static byte[] concat(byte[][] inputs) {
+        int total = 0;
+        for(int i=0; i<inputs.length; i++) total += inputs[i].length;
+        byte[] ret = new byte[total];
+        for(int i=0,pos=0; i<inputs.length;pos+=inputs[i].length,i++)
+            System.arraycopy(inputs[i], 0, ret, pos, inputs[i].length);
+        return ret;
+    }
+    
+    public static byte[] getBytes(String s) {
+        try {
+            return s.getBytes("US-ASCII");
+        } catch (UnsupportedEncodingException e) {
+            return null; // will never happen
+        }
+    }
+    
+    public static boolean eq(byte[] a, int aoff, byte[] b, int boff, int len){
+        for(int i=0;i<len;i++) if(a[aoff+i] != b[boff+i]) return false;
+        return true;
+    }
+    
+    //
+    // InputStream/OutputStream/Socket interfaces
+    //
+    public OutputStream getOutputStream() { return sslOS; }
+    public InputStream getInputStream() { return sslIS; }
+    public synchronized void close() throws IOException {
+        if(!closed) {
+            if(negotiated != 0) {
+                sendCloseNotify();
+                flush();
+                // don't bother sending a close_notify back to the server 
+                // this is an incomplete close which is allowed by the spec
+            }
+            super.close();
+            closed = true;
+        }
+    }
+    
+    private int read(byte[] buf, int off, int len) throws IOException {
+        if(pendingLength == 0) {
+            if(closed) return -1;
+            int readLen = readRecord((byte)23);
+            if(readLen == -1) return -1; // EOF
+            len = min(len,readLen);
+            System.arraycopy(readRecordBuf,0,buf,off,len);
+            if(readLen > len) System.arraycopy(readRecordBuf,len,pending,0,readLen-len);
+            pendingStart = 0;
+            pendingLength = readLen - len;
+            return len;
+        } else {
+            len = min(len,pendingLength);
+            System.arraycopy(pending,pendingStart,buf,off,len);
+            pendingLength -= len;
+            pendingStart += len;
+            return len;
+        }
+    }
+    
+    private void write(byte[] buf, int off, int len) throws IOException {
+        if(closed) throw new SocketException("Socket closed");
+        sendRecord((byte)23,buf,off,len);
+        flush();
+    }
+    
+    private class SSLInputStream extends InputStream {
+        public int available() throws IOException {
+            synchronized(SSL.this) {
+                return negotiated != 0 ? pendingLength : rawIS.available();
+            }
+        }
+        public int read() throws IOException {
+            synchronized(SSL.this) {
+                if(negotiated==0) return rawIS.read();
+                if(pendingLength > 0) {
+                    pendingLength--;
+                    return pending[pendingStart++];
+                } else {
+                    byte[] buf = new byte[1];
+                    int n = read(buf);
+                    return n == -1 ? -1 : buf[0]&0xff;
+                }
+            }
+        }
+        public int read(byte[] buf, int off, int len) throws IOException {
+            synchronized(SSL.this) {
+                return negotiated!=0 ? SSL.this.read(buf,off,len) : rawIS.read(buf,off,len);
+            }
+        }
+        public long skip(long n) throws IOException {
+            synchronized(SSL.this) {
+                if(negotiated==0) return rawIS.skip(n);
+                if(pendingLength > 0) {
+                    n = min((int)n,pendingLength);
+                    pendingLength -= n;
+                    pendingStart += n;
+                    return n;
+                }
+                return super.skip(n);
+            }
+        }
+    }
+    
+    private class SSLOutputStream extends OutputStream {
+        public void flush() throws IOException { rawOS.flush(); }
+        public void write(int b) throws IOException { write(new byte[] { (byte)b }); }
+        public void write(byte[] buf, int off, int len) throws IOException {
+            synchronized(SSL.this) {
+                if(negotiated!=0)
+                    SSL.this.write(buf,off,len);
+                else
+                    rawOS.write(buf,off,len);
+            }
+        }
+    }
+    
+    public static class Exn extends IOException { public Exn(String s) { super(s); } }
+    public static class PrematureCloseExn extends Exn {
+        public PrematureCloseExn() { super("Connection was closed by the remote WITHOUT a close_noify"); }
+    }
+    
+    public static boolean debugOn = false;
+    private static void debug(Object o) { if(debugOn) System.err.println("[BriSSL-Debug] " + o.toString()); }
+    private static void log(Object o) { System.err.println("[BriSSL] " + o.toString()); }
+            
+    private static void verifyCerts(X509.Certificate[] certs) throws DER.Exception, Exn {
+        try {
+            verifyCerts_(certs);
+        } catch(RuntimeException e) {
+            e.printStackTrace();
+            throw new Exn("Error while verifying certificates: " + e);
+        }
+    }
+    
+    private static void verifyCerts_(X509.Certificate[] certs) throws DER.Exception, Exn {
+        boolean ignoreLast = false;
+        for(int i=0;i<certs.length;i++) {
+            debug("Cert " + i + ": " + certs[i].subject + " ok");
+            if(!certs[i].isValid())
+                throw new Exn("Certificate " + i + " in certificate chain is not valid (" + certs[i].startDate + " - " + certs[i].endDate + ")");
+            if(i != 0) {
+                X509.Certificate.BC bc = certs[i].basicContraints;
+                if(bc == null) {
+                    if(i == certs.length - 1) {
+                        ignoreLast = true;
+                        break;
+                    }
+                    throw new Exn("CA-cert lacks Basic Constraints");
+                } else {
+                    if(!bc.isCA) throw new Exn("non-CA certificate used for signing");
+                    if(bc.pathLenConstraint != null && bc.pathLenConstraint.longValue() < i-1) throw new Exn("CA cert can't be used this deep");
+                }
+            }
+            if(i != certs.length - 1) {
+                if(!certs[i].issuer.equals(certs[i+1].subject))
+                    throw new Exn("Issuer for certificate " + i + " does not match next in chain");
+                if(!certs[i].isSignedBy(certs[i+1]))
+                    throw new Exn("Certificate " + i + " in chain is not signed by the next certificate");
+            }
+        }
+        
+        X509.Certificate cert = certs[ignoreLast ? certs.length - 2 : certs.length-1];
+        
+        RSA.PublicKey pks = (RSA.PublicKey) caKeys.get(cert.issuer);
+        if(pks == null) throw new Exn("Certificate is signed by an unknown CA (" + cert.issuer + ")");
+        if(!cert.isSignedWith(pks)) throw new Exn("Certificate is not signed by its CA");
+        log("" + cert.subject + " is signed by " + cert.issuer);
+    }
+    
+    public static void addCACert(byte[] b) throws IOException { addCACert(new ByteArrayInputStream(b)); }
+    public static void addCACert(InputStream is) throws IOException { addCACert(new X509.Certificate(is)); }
+    public static void addCACert(X509.Certificate cert) throws DER.Exception { addCAKey(cert.subject,cert.getRSAPublicKey()); }
+    public static void addCAKey(X509.Name subject, RSA.PublicKey pks)  {
+        synchronized(caKeys) {
+            if(caKeys.get(subject) != null)
+                throw new IllegalArgumentException(subject.toString() + " already exists!");
+            caKeys.put(subject,pks);
+        }
+    }
+    
+    static {
+        try {
+            // This will force a <clinit> which'll load the certs
+            Class.forName("org.ibex.net.ssl.RootCerts");
+            log("Loaded root keys from org.ibex.net.ssl.RootCerts");
+        } catch(ClassNotFoundException e) {
+            InputStream is = SSL.class.getClassLoader().getResourceAsStream("org.ibex/net/ssl/rootcerts.dat");
+            if(is != null) {
+                try {
+                    addCompactCAKeys(is);
+                    log("Loaded root certs from rootcerts.dat");
+                } catch(IOException e2) {
+                    log("Error loading certs from rootcerts.dat: " + e2.getMessage()); 
+                }
+            }
+        }
+    }
+        
+    public static int addCompactCAKeys(InputStream is) throws IOException {
+        synchronized(caKeys) {
+            try {
+                Vector seq = (Vector) new DER.InputStream(is).readObject();
+                for(Enumeration e = seq.elements(); e.hasMoreElements();) {
+                    Vector seq2 = (Vector) e.nextElement();
+                    X509.Name subject = new X509.Name(seq2.elementAt(0));
+                    RSA.PublicKey pks = new RSA.PublicKey(seq2.elementAt(1));
+                    addCAKey(subject,pks);
+                }
+                return seq.size();
+            } catch(RuntimeException e) {
+                e.printStackTrace();
+                throw new IOException("error while reading stream: " + e);
+            }
+        }
+    }
+    
+    public static synchronized void setVerifyCallback(VerifyCallback cb) { verifyCallback = cb; }
+    
+    // State Info
+    public static class State {
+        byte[] sessionID;
+        byte[] masterSecret;
+        State(byte[] sessionID, byte[] masterSecret) {
+            this.sessionID = sessionID;
+            this.masterSecret = masterSecret;
+        }
+    }
+    
+    public interface VerifyCallback {
+        public boolean checkCerts(X509.Certificate[] certs, String hostname, Exn exn);
+    }
+    
+    // Helper methods
+    private static final int min(int a, int b) { return a < b ? a : b; }
+}