// Copyright 2000-2005 the Contributors, as shown in the revision logs. // Licensed under the Apache Public Source License 2.0 ("the License"). // You may not use this file except in compliance with the License. /* * org.ibex.net.SSL - By Brian Alliet * Copyright (C) 2004 Brian Alliet * * Based on TinySSL by Adam Megacz * Copyright (C) 2003 Adam Megacz all rights reserved. * */ 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 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) 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 (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(); return -1; } 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 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 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 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 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; } }