/*
*
*
* Copyright 1990-2007 Sun Microsystems, Inc. All Rights Reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version
* 2 only, as published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* General Public License version 2 for more details (a copy is
* included at /legal/license.txt).
*
* You should have received a copy of the GNU General Public License
* version 2 along with this work; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA
*
* Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa
* Clara, CA 95054 or visit www.sun.com if you need additional
* information or have any questions.
*/
package com.sun.midp.ssl;
import java.io.IOException;
import java.io.InputStream;
import java.lang.Exception;
import java.util.Vector;
import javax.microedition.pki.CertificateException;
import com.sun.midp.log.Logging;
import com.sun.midp.log.LogChannels;
import com.sun.midp.crypto.*;
import com.sun.midp.pki.*;
/**
* This class implements the SSL handshake protocol which is responsible
* for negotiating security parameters used by the record layer.
* Currently, only client-side functionality is implemented.
*/
// visible only within this package
class Handshake {
/** ARCFOUR_128_SHA (0x05). */
static final byte ARCFOUR_128_SHA = 0x05;
/** ARCFOUR_128_MD5 (0x04). */
static final byte ARCFOUR_128_MD5 = 0x04;
/** ARCFOUR_40_MD5 (0x03). */
static final byte ARCFOUR_40_MD5 = 0x03;
/**
* This contains the cipher suite encoding length in the first
* two bytes, followed by an encoding of the cipher suites followed
* by the compression suite length in one byte and the compression
* suite. For now, we only propose the two most commonly used
* cipher suites.
*/
private static final byte[] SUITES_AND_COMP = {
// Use this to propose 128-bit encryption as preferred
0x00, 0x06, 0x00, ARCFOUR_128_SHA, 0x00, ARCFOUR_128_MD5,
0x00, ARCFOUR_40_MD5, 0x01, 0x00
// Use this to propose 40-bit encryption as preferred
// 0x00, 0x06, 0x00, ARCFOUR_40_MD5, 0x00, ARCFOUR_128_RSA,
// 0x00, ARCFOUR_128_SHA, 0x01, 0x00
};
/**
* Array of suite names.
*/
private static String[] suiteNames = {
"", "", "",
"TLS_RSA_EXPORT_WITH_RC4_40_MD5",
"TLS_RSA_WITH_RC4_128_MD5",
"TLS_RSA_WITH_RC4_128_SHA"
};
/**
* Each handshake message has a four-byte header containing
* the type (1 byte) and length (3 byte).
*/
private static final byte HDR_SIZE = 4;
// Handshake message types
/** Hello Request (0). */
private static final byte HELLO_REQ = 0;
/** Client Hello (1). */
private static final byte C_HELLO = 1;
/** Server Hello (2). */
private static final byte S_HELLO = 2;
/** Certificate (11). */
private static final byte CERT = 11;
/** Server Key Exchange (12). */
private static final byte S_KEYEXCH = 12;
/** Certificate Request (13). */
private static final byte CERT_REQ = 13;
/** Server Hello Done (14). */
private static final byte S_DONE = 14;
/** Certificate Verify (15). */
private static final byte CERT_VRFY = 15;
/** Client Key Exchange (16). */
private static final byte C_KEYEXCH = 16;
/** Finished (20). */
private static final byte FINISH = 20;
// Number of bytes in an MD5/SHA digest
/** Number of bytes in an MD5 Digest (16). */
private static final byte MD5_SIZE = 16;
/** Number of bytes in an SHA Digest (20). */
private static final byte SHA_SIZE = 20;
/**
* The Finish message contains one MD5 and one SHA hash
* and has a length of 4+16+20 = 40 = 0x24 bytes.
*/
private static final byte[] FINISH_PREFIX = {
FINISH, 0x00, 0x00, 0x24
};
/** Handle to trusted certificate store. */
private CertStore certStore = null;
/** Current record to process. */
private Record rec;
/** Peer host name . */
private String peerHost;
/** Peer port number. */
private int peerPort;
/** Local random number seed. */
private SecureRandom rnd = null;
/** Previous session context to this host and port, if there was one. */
private Session cSession = null;
/** Session id returned by server. */
private byte[] sSessionId = null;
/** Client random number. */
private byte[] crand = null;
/** Server random number. */
private byte[] srand = null;
/** Proposed SSL version. */
private byte ver;
/** Role (always CLIENT for now). */
private byte role;
/** Negotiated cipher suite. */
byte negSuite;
/** Name of negotiated cipher suite. */
String negSuiteName;
/** Flag to indicate certificate request received. */
private byte gotCertReq = 0;
/** Pre-master secret. */
private byte[] preMaster = null;
/** Master secret. */
private byte[] master = null;
/**
* Public key used to encrypt the appropriate
* usage of sKey certs in chain.
*/
private RSAPublicKey eKey = null;
// we also need a temporary place to store the server certificate
// in parseChain so it can be examined later rcvSrvrKeyExch() for
// keyUsage checks and the parent connection.
/** Temporary storage for server certificate. */
X509Certificate sCert = null;
/*
* These accumulate MD5 and SHA digests of all handshake
* messages seen so far.
*/
/** Accumulation of MD5 digests. */
private MessageDigest ourMD5 = null;
/** Accumulation of SHA digests. */
private MessageDigest ourSHA = null;
/*
* The following fields maintain a buffer of available handshake
* messages. Note that a single SSL record may include multiple
* handshake messages.
*/
/** Start of message in data buffer. */
private int start = 0;
/** Start of next message in data buffer. */
private int nextMsgStart = 0;
/** Count of bytes left in the data buffer. */
private int cnt = 0;
/**
* Validates a chain of certificates and returns the RSA public
* key from the first certificate in that chain. The format of
* the chain is specific to the ServerCertificate payload in an
* SSL handshake.
*
* @param msg byte array containing the SSL ServerCertificate
* payload (this is a chain of DER-encoded X.509
* certificates, in which each certificate is preceded
* by a 3-byte length field)
* @param off offset in the byte array where the cert chain begins
* @param end position in the byte array where the cert chain ends + 1
*
* @return server's certificate in the chain
*
* @exception IOException if the there is a binary formating error
* @exception CertificateException if there a verification error
*/
private X509Certificate parseChain(byte[] msg, int off, int end)
throws IOException, CertificateException {
Vector certs = new Vector();
int len;
// We have a 3-byte length field before each cert in list
while (off < (end - 3)) {
len = ((msg[off++] & 0xff) << 16) +
((msg[off++] & 0xff) << 8) + (msg[off++] & 0xff);
if (len < 0 || len + off > msg.length) {
throw new IOException("SSL certificate length too long");
}
certs.addElement(
X509Certificate.generateCertificate(msg, off, len));
off += len;
}
/*
* The key usage extension of the server certificate is checked later
* a based on the key exchange. Only the extended key usage is checked
* now.
*/
X509Certificate.verifyChain(certs, -1,
X509Certificate.SERVER_AUTH_EXT_KEY_USAGE, certStore);
// The first cert if specified to be the server cert.
return (X509Certificate)certs.elementAt(0);
}
/**
* Creates an Handshake object that is used to negotiate a
* version 3 handshake with an SSL peer.
*
* @param host hostname of the peer
* @param port port number of the peer
* @param r Record instance through which handshake
* will occur
* @param tcs trusted certificate store containing certificates
*
* @exception RuntimeException if SHA-1 or MD5 is not available
*/
Handshake(String host, int port, Record r, CertStore tcs) {
peerHost = new String(host);
peerPort = port;
rec = r;
certStore = tcs;
eKey = null;
gotCertReq = 0;
start = 0;
cnt = 0;
try {
ourMD5 = MessageDigest.getInstance("MD5");
ourSHA = MessageDigest.getInstance("SHA-1");
rnd = SecureRandom.getInstance(SecureRandom.ALG_SECURE_RANDOM);
} catch (NoSuchAlgorithmException e) {
// should only happen, if digests are not included in the build
throw new RuntimeException(e.getMessage());
}
}
/**
* Obtains the next available handshake message.
* <p>
* The message returned has the header plus the number of
* bytes indicated in the handshake message header.</p>
*
* @param type the desired handshake message type
* @return number of bytes in the next handshake message
* of the desired type or -1 if the next message is not of
* the desired type
* @exception IOException if there is a problem reading the
* next handshake message
*/
private int getNextMsg(byte type) throws IOException {
if (cnt == 0) {
rec.rdRec(true, Record.HNDSHK);
if (rec.plainTextLength < HDR_SIZE) {
throw new IOException("getNextMsg refill failed");
}
cnt = rec.plainTextLength;
nextMsgStart = 0;
}
if (rec.inputData[nextMsgStart] == type) {
int len = ((rec.inputData[nextMsgStart + 1] & 0xff) << 16) +
((rec.inputData[nextMsgStart + 2] & 0xff) << 8) +
(rec.inputData[nextMsgStart + 3] & 0xff) + HDR_SIZE;
if (cnt < len) {
throw new IOException("Refill got short msg " +
"c=" + cnt + " l=" + len);
}
start = nextMsgStart;
nextMsgStart += len;
cnt -= len;
return len;
} else {
return -1;
}
}
/**
* Sends an SSL version 3.0 Client hello handshake message.
* <P />
* @exception IOException if there is a problem writing to
* the record layer
*/
private void sndHello3() throws IOException {
cSession = Session.get(peerHost, peerPort);
int len = (cSession == null) ? 0 : cSession.id.length;
/*
* Size = 4 (HDR_SIZE) + 2 (client_version) + 32 (crand.length) +
* 1 (session length) + len + 2 (cipher suite length) +
* (2*CipherSuiteList.length) + 1 (compression length) + 1 (comp code)
*/
byte[] msg = new byte[39 + len + SUITES_AND_COMP.length];
int idx = 0;
// Fill the header -- type (1 byte) length (3 bytes)
msg[idx++] = C_HELLO;
int mlen = msg.length - HDR_SIZE;
msg[idx++] = (byte) (mlen >>> 16);
msg[idx++] = (byte) (mlen >>> 8);
msg[idx++] = (byte) (mlen & 0xff);
// ... client_version
msg[idx++] = (byte) (ver >>> 4);
msg[idx++] = (byte) (ver & 0x0f);
// ... random
/*
* IMPL_NOTE: overwrite the first four bytes of crand with
* current time and date in standard 32-bit UNIX format.
*/
crand = new byte[32];
rnd.nextBytes(crand, 0, 32);
System.arraycopy(crand, 0, msg, idx, crand.length);
idx += crand.length;
// ... session_id
msg[idx++] = (byte) (len & 0xff);
if (cSession != null) {
System.arraycopy(cSession.id, 0, msg, idx, cSession.id.length);
idx += cSession.id.length;
}
// ... cipher_suites and compression methods
System.arraycopy(SUITES_AND_COMP, 0, msg, idx, SUITES_AND_COMP.length);
ourMD5.update(msg, 0, msg.length);
ourSHA.update(msg, 0, msg.length);
// Finally, write this handshake record
rec.wrRec(Record.HNDSHK, msg, 0, msg.length);
}
/**
* Receives a Server hello handshake message.
* <P />
* @return 0 on success, -1 on failure
* @exception IOException if there is a problem reading the
* message
*/
private int rcvSrvrHello() throws IOException {
int msgLength = getNextMsg(S_HELLO);
int idx = start + HDR_SIZE;
int endOfMsg = start + msgLength;
/*
* Message must be long enough to contain a 4-byte header,
* 2-byte version, a 32-byte random, a 1-byte session Id
* length (plus variable lenght session Id), 2 byte cipher
* suite, 1 byte compression method.
*/
if (msgLength < 42) {
return -1;
}
// Get the server version
if ((rec.inputData[start + idx++] != (ver >>> 4)) ||
(rec.inputData[start + idx++] != (ver & 0x0f))) {
return -1;
}
// .. the 32-byte server random
srand = new byte[32];
System.arraycopy(rec.inputData, idx, srand, 0, 32);
idx += 32;
// ... the session_Id length in 1 byte (and session_Id)
int slen = rec.inputData[idx++] & 0xff;
if (slen != 0) {
if (endOfMsg < idx + slen) {
return -1;
}
sSessionId = new byte[slen];
System.arraycopy(rec.inputData, idx, sSessionId, 0, slen);
idx += slen;
}
// ... the cipher suite
/*
* NOTE: this impl works because the cipher suites
* we support, the second byte directly maps to suite code.
*/
idx++;
negSuite = rec.inputData[idx++];
/*
* Check the cipher suite and compression method. The compression
* method better be 0x00 since that is the only one we ever propose.
*/
if ((negSuite != ARCFOUR_128_SHA) &&
(negSuite != ARCFOUR_128_MD5) &&
(negSuite != ARCFOUR_40_MD5) &&
(rec.inputData[idx++] != (byte) 0x00)) {
return -1;
}
// Update the hash of handshake messages
ourMD5.update(rec.inputData, start, msgLength);
ourSHA.update(rec.inputData, start, msgLength);
negSuiteName = suiteNames[negSuite];
if (Logging.REPORT_LEVEL <= Logging.INFORMATION) {
Logging.report(Logging.INFORMATION, LogChannels.LC_SECURITY,
"Negotiated " + negSuiteName);
}
return 0;
}
/**
* Receives a Server certificate message containing a certificate
* chain starting with the server certificate.
* <P />
* @return 0 if a trustworthy server certificate is found, -1 otherwise
* @exception IOException if there is a problem reading the message
*/
private int rcvCert() throws IOException {
int msgLength;
int endOfMsg;
int idx;
int len;
msgLength = getNextMsg(CERT);
endOfMsg = start + msgLength;
/*
* Message should atleast have a 4-byte header and an empty cert
* list with 3-byte length
*/
if (msgLength < 7) {
return -1;
}
idx = start + HDR_SIZE;
len = 0;
// Check the length ...
len = ((rec.inputData[idx++] & 0xff) << 16) +
((rec.inputData[idx++] & 0xff) << 8) + (rec.inputData[idx++] &
0xff);
if ((idx + len) > endOfMsg)
return -1;
// Parse the certificate chain and get the server's public key
sCert = parseChain(rec.inputData, idx, endOfMsg);
// Update the hash of handshake messages
ourMD5.update(rec.inputData, start, msgLength);
ourSHA.update(rec.inputData, start, msgLength);
return 0;
}
/**
* Receives a Server key exchange message. For now only RSA key
* exchange is supported and this message includes temporary
* RSA public key parameters signed by the server's long-term
* private key. This message is optional.
* <P />
* @return 0 on success, -1 on failure
* @exception IOException if there is a problem reading the
* message
* @exception RuntimeException if SHA-1 or MD5 is not available
*/
private int rcvSrvrKeyExch() throws IOException {
int msgLength = getNextMsg(S_KEYEXCH);
int idx = start + HDR_SIZE;
int endOfMsg = start + msgLength;
RSAPublicKey sKey = (RSAPublicKey)sCert.getPublicKey();
int keyUsage = sCert.getKeyUsage();
/*
* NOTE: Based on what we propose, the only key exch is RSA
* Also note that the server key exchange is optional and used
* only if the public key included in the certificate chain
* is unsuitable for encrypting the pre-master secret.
*/
if (msgLength == -1) {
// We can use the server key to encrypt premaster secret
eKey = sKey;
/*
* Make sure sKey can be used for premaster secret encryption,
* i.e. if key usage extension is present, the key encipherment
* bit must be set
*/
if (keyUsage != -1 &&
(keyUsage & X509Certificate.KEY_ENCIPHER_KEY_USAGE) !=
X509Certificate.KEY_ENCIPHER_KEY_USAGE) {
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_SECURITY,
"The keyEncipherment was bit is " +
"set in server certificate key " +
"usage extension.");
}
throw new CertificateException(sCert,
CertificateException.INAPPROPRIATE_KEY_USAGE);
}
return 0;
}
// read and verify the encryption key parameters
if (endOfMsg < (idx + 4)) {
return -1;
}
// read the modulus length
int len = ((rec.inputData[idx++] & 0xff) << 16) +
(rec.inputData[idx++] & 0xff);
if (endOfMsg < (idx + len + 2)) {
return -1;
}
int modulusPos;
int modulusLen;
int exponentPos;
int exponentLen;
// ... and the modulus
/*
* Some weird sites (e.g. www.verisign.com) encode a
* 512-bit modulus in 65 (rather than 64 bytes) with the
* first byte set to zero. We accomodate this behavior
* by using a special check.
*/
if ((len == 65) && (rec.inputData[idx] == (byte)0x00)) {
modulusPos = idx + 1;
modulusLen = 64;
} else {
modulusPos = idx;
modulusLen = len;
}
idx += len;
// read the exponent length
len = ((rec.inputData[idx++] & 0xff) << 16) +
(rec.inputData[idx++] & 0xff);
if (endOfMsg < (idx + len)) {
return -1;
}
// ... and the exponent
exponentPos = idx;
exponentLen = len;
eKey = new RSAPublicKey(rec.inputData, modulusPos, modulusLen,
rec.inputData, exponentPos, exponentLen);
idx += len;
// mark where ServerRSAparams end
int end = idx;
// Now read the signature length
len = ((rec.inputData[idx++] & 0xff) << 16) +
(rec.inputData[idx++] & 0xff);
if (endOfMsg < (idx + len)) {
return -1;
}
// ... and the signature
byte[] sig = new byte[len];
System.arraycopy(rec.inputData, idx, sig, 0, sig.length);
idx += len;
if (endOfMsg != idx) {
return -1;
}
// Compute the expected hash
byte[] dat = new byte[MD5_SIZE + SHA_SIZE];
try {
MessageDigest di = MessageDigest.getInstance("MD5");
di.update(crand, 0, crand.length);
di.update(srand, 0, srand.length);
di.update(rec.inputData, HDR_SIZE, end - HDR_SIZE);
di.digest(dat, 0, MD5_SIZE);
di = MessageDigest.getInstance("SHA-1");
di.update(crand, 0, crand.length);
di.update(srand, 0, srand.length);
di.update(rec.inputData, HDR_SIZE, end - HDR_SIZE);
di.digest(dat, MD5_SIZE, SHA_SIZE);
} catch (Exception e) {
throw new RuntimeException("No MD5 or SHA");
}
try {
Cipher rsa = Cipher.getInstance("RSA");
rsa.init(Cipher.DECRYPT_MODE, sKey);
byte[] res = new byte[sKey.getModulusLen()];
int val = rsa.doFinal(sig, 0, sig.length, res, 0);
if (!Utils.byteMatch(res, 0, dat, 0, dat.length)) {
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_SECURITY,
"RSA params failed verification");
}
return -1;
}
} catch (Exception e) {
throw new IOException("RSA decryption caught " + e);
}
// Update the hash of handshake messages
ourMD5.update(rec.inputData, start, msgLength);
ourSHA.update(rec.inputData, start, msgLength);
return 0;
}
/**
* Receives a Certificate request message. This message is optional.
* <P />
* @return 0 (this method always completes successfully)
* @exception IOException if there is a problem reading the
* message
*/
private int rcvCertReq() throws IOException {
int msgLength = getNextMsg(CERT_REQ);
if (msgLength == -1) {
return 0; // certificate request is optional
}
/*
* We do not support client-side certificates so if we see
* a request for a certificate, remember it here so we can
* complain later
*/
gotCertReq = (byte) 1;
// Update the hash of handshake messages
ourMD5.update(rec.inputData, start, msgLength);
ourSHA.update(rec.inputData, start, msgLength);
// NOTE: We return zero without attempting to parse the message body.
return 0;
}
/**
* Receives a Server hello done message.
* <P />
* @return 0 on success, -1 on error
* @exception IOException if there is a problem reading the
* message
*/
private int rcvSrvrHelloDone() throws IOException {
int msgLength = getNextMsg(S_DONE);
// A server_hello_done message has no body, just the header
if (msgLength != HDR_SIZE) {
return -1;
}
// Update the hash of handshake messages
ourMD5.update(rec.inputData, start, msgLength);
ourSHA.update(rec.inputData, start, msgLength);
return 0;
}
/**
* Sends a Client key exchange message. For now, only RSA key
* exchange is supported and this message contains a pre-master
* secret encrypted with the RSA public key of the server.
* <P />
* @exception IOException if there is a problem writing to the
* record layer
*/
private void sndKeyExch() throws IOException {
/*
* If we get here, the server agreed to an RSA key exchange
* and the RSA public key to be used for encrypting the
* pre-master secret is available in eKey.
*/
if (gotCertReq == 1) {
// Send back an error ... we do not support client auth
rec.alert(Record.FATAL, Record.NO_CERT);
throw new IOException("No client cert");
} else { // NOTE: The only possible key exch is RSA
// Generate a 48-byte random pre-master secret
preMaster = new byte[48];
rnd.nextBytes(preMaster, 0, 48);
// ... first two bytes must have client version
preMaster[0] = (byte) (ver >>> 4);
preMaster[1] = (byte) (ver & 0x0f);
// Prepare a message containing the RSA encrypted pre-master
int modLen = eKey.getModulusLen();
byte[] msg = new byte[HDR_SIZE + modLen];
int idx = 0;
// Fill the type
msg[idx++] = C_KEYEXCH;
// ... message length
msg[idx++] = (byte) (modLen >>> 16);
msg[idx++] = (byte) (modLen >>> 8);
msg[idx++] = (byte) (modLen & 0xff);
// ... the encrypted pre-master secret
try {
Cipher rsa = Cipher.getInstance("RSA");
rsa.init(Cipher.ENCRYPT_MODE, eKey);
int val = rsa.doFinal(preMaster, 0, 48, msg, idx);
if (val != modLen)
throw new IOException("RSA result too short");
} catch (Exception e) {
throw new IOException("premaster encryption caught " + e);
}
// Update the hash of handshake messages
ourMD5.update(msg, 0, msg.length);
ourSHA.update(msg, 0, msg.length);
rec.wrRec(Record.HNDSHK, msg, 0, msg.length);
}
}
/**
* Derives the master key based on the pre-master secret and
* random values exchanged in the client and server hello messages.
* <P />
* @exception IOException if there is a problem during the computation
*/
private void mkMaster() throws IOException {
byte[] expansion[] = {
{ (byte) 0x41 }, // 'A'
{ (byte) 0x42, (byte) 0x42 }, // 'BB'
{ (byte) 0x43, (byte) 0x43, (byte) 0x43 }, // 'CCC'
};
MessageDigest md = null;
MessageDigest sd = null;
/*
* First, we compute the 48-byte (three MD5 outputs) master secret
*
* master_secret =
* MD5(pre_master + SHA('A' + pre_master +
* ClientHello.random + ServerHello.random)) +
* MD5(pre_master + SHA('BB' + pre_master +
* ClientHello.random + ServerHello.random)) +
* MD5(pre_master + SHA('CCC' + pre_master +
* ClientHello.random + ServerHello.random));
*
* To simplify things, we use
* tmp = pre_master + ClientHello.random + ServerHello.random;
*/
byte[] tmp = new byte[preMaster.length + crand.length + srand.length];
System.arraycopy(preMaster, 0, tmp, 0, preMaster.length);
System.arraycopy(crand, 0, tmp, preMaster.length, crand.length);
System.arraycopy(srand, 0, tmp, preMaster.length + crand.length,
srand.length);
try {
md = MessageDigest.getInstance("MD5");
sd = MessageDigest.getInstance("SHA-1");
} catch (NoSuchAlgorithmException e) {
/*
* We should never catch this here (if these are missing,
* we will catch this exception in the constructor)
*/
throw new RuntimeException("No MD5 or SHA");
}
master = new byte[48];
try {
for (int i = 0; i < 3; i++) {
md.update(preMaster, 0, preMaster.length);
sd.update(expansion[i], 0, expansion[i].length);
byte[] res = new byte[SHA_SIZE];
sd.update(tmp, 0, tmp.length);
sd.digest(res, 0, res.length);
md.update(res, 0, res.length);
md.digest(master, i << 4, MD5_SIZE);
}
} catch (DigestException e) {
/*
* We should never catch this here.
*/
throw new RuntimeException("digest exception");
}
}
/**
* Sends a ChangeCipherSpec protocol message (this is not really
* a handshake protocol message).
* <P />
* @exception IOException if there is a problem writing to the
* record layer
*/
private void sndChangeCipher() throws IOException {
byte[] msg = new byte[1];
// change cipher spec consists of a single byte with value 1
msg[0] = (byte) 0x01;
rec.wrRec(Record.CCS, msg, 0, 1); // msg.length is 1
}
/**
* Computes the content of a Finished message.
* <P />
* @param who the role (either Record.CLIENT or
* Record.SERVER) for which the finish message is computed
* @return a byte array containing the hash of all handshake
* messages seen so far
* @exception IOException if handshake digests could not be computed
*/
private byte[] computeFinished(byte who) throws IOException {
byte[] sender[] = {
{ 0x53, 0x52, 0x56, 0x52}, // for server
{ 0x43, 0x4c, 0x4e, 0x54} // for client
};
byte[] msg = new byte[MD5_SIZE + SHA_SIZE];
byte[] tmp = null;
try {
// long t1 = System.currentTimeMillis();
MessageDigest d = (MessageDigest) ourMD5.clone();
d.update(sender[who], 0, 4);
d.update(master, 0, master.length);
tmp = new byte[MD5_SIZE];
// MD5 padding length is 48
d.update(MAC.PAD1, 0, 48);
d.digest(tmp, 0, tmp.length);
d.update(master, 0, master.length);
d.update(MAC.PAD2, 0, 48);
d.update(tmp, 0, tmp.length);
d.digest(msg, 0, MD5_SIZE);
d = (MessageDigest) ourSHA.clone();
d.update(sender[who], 0, 4);
d.update(master, 0, master.length);
tmp = new byte[SHA_SIZE];
// SHA padding length is 40
d.update(MAC.PAD1, 0, 40);
d.digest(tmp, 0, tmp.length);
d.update(master, 0, master.length);
d.update(MAC.PAD2, 0, 40);
d.update(tmp, 0, tmp.length);
d.digest(msg, MD5_SIZE, SHA_SIZE);
return msg;
} catch (Exception e) {
throw new IOException("MessageDigest not cloneable");
}
}
/**
* Sends a Finished message.
* <P />
* @exception IOException if there is a problem writing to the
* record layer
*/
private void sndFinished() throws IOException {
// HDR_SIZE + MD5_SIZE + SHA_SIZE is 40
byte[] msg = new byte[40];
System.arraycopy(FINISH_PREFIX, 0, msg, 0, 4);
// MD5_SIZE + SHA_SIZE is 36
System.arraycopy(computeFinished(role), 0, msg, 4, 36);
// Update the hash of handshake messages
ourMD5.update(msg, 0, msg.length);
ourSHA.update(msg, 0, msg.length);
rec.wrRec(Record.HNDSHK, msg, 0, msg.length);
}
/**
* Receives a ChangeCipherSpec protocol message (this is
* not a handshake message).
* <P />
* @return 0 on success, -1 on error
* @exception IOException if there is a problem reading the
* message
*/
private int rcvChangeCipher() throws IOException {
/*
* We make sure that there are no unread handshake messages
* in the internal store when we get here.
*/
if (cnt != 0) {
if (Logging.REPORT_LEVEL <= Logging.ERROR) {
Logging.report(Logging.ERROR, LogChannels.LC_SECURITY,
"Unread handshake mesg in store");
}
return -1;
}
/*
* Note that CCS is not a handshake message (it is its own protocol)
* The record layer header is 5 bytes and the CCS body is one
* byte with value 0x01.
*/
rec.rdRec(true, Record.CCS);
if ((rec.inputData == null) || (rec.inputData.length != 1) ||
(rec.inputData[0] != (byte) 0x01)) {
return -1;
}
return 0;
}
/**
* Receives a Finished message and verifies that it contains
* the correct hash of handshake messages.
* <P />
* @return 0 on success, -1 on error
* @exception IOException if there is a problem reading the
* message
*/
private int rcvFinished() throws IOException {
int msgLength = getNextMsg(FINISH);
if (msgLength != 40) {
return -1;
}
// Compute the expected hash
byte[] expected = computeFinished((byte) (1 - role));
if (!Utils.byteMatch(rec.inputData, start + HDR_SIZE, expected, 0,
expected.length)) {
return -1;
} else {
// Update the hash of handshake messages
ourMD5.update(rec.inputData, start, msgLength);
ourSHA.update(rec.inputData, start, msgLength);
// now = System.currentTimeMillis();
return 0;
}
}
/**
* Initiates an SSL handshake with the peer specified previously
* in the constructor.
* <P />
* @param aswho role played in the handshake (for now only
* Record.CLIENT is supported)
* @exception IOException if the handshake fails for some reason
*/
// IMPL_NOTE: Allow handshake parameters such as ver, cipher suites
// and compression methods to be passed as arguments.
void doHandShake(byte aswho) throws IOException {
long t1 = System.currentTimeMillis();
int code = 0;
ver = (byte) 0x30; // IMPL_NOTE: This is hardcoded for now
role = aswho;
byte val = 0;
sndHello3();
if (rcvSrvrHello() < 0) {
complain("Bad ServerHello");
};
if ((sSessionId == null) || (cSession == null) ||
(sSessionId.length != cSession.id.length) ||
!Utils.byteMatch(sSessionId, 0, cSession.id, 0,
sSessionId.length)) {
// Session not resumed
try {
code = rcvCert();
} catch (CertificateException e) {
complain(e);
}
if (code < 0) {
complain("Corrupt server certificate message");
}
// ... get server_key_exchange (optional)
try {
code = rcvSrvrKeyExch();
} catch (CertificateException e) {
complain(e);
}
if (code < 0) {
complain("Bad ServerKeyExchange");
}
// ... get certificate_request (optional)
rcvCertReq();
if (rcvSrvrHelloDone() < 0) {
complain("Bad ServerHelloDone");
}
// ... send client_key_exchange
sndKeyExch();
mkMaster();
try {
rec.init(role, crand, srand, negSuite, master);
} catch (Exception e) {
complain("Record.init() caught " + e);
}
// ... send change_cipher_spec
sndChangeCipher();
// ... send finished
sndFinished();
// ... get change_cipher_spec
if (rcvChangeCipher() < 0) {
complain("Bad ChangeCipherSpec");
}
// ... get finished
if (rcvFinished() < 0) {
complain("Bad Finished");
}
} else {
/*
* The server agreed to resume a session.
* Get the needed values from the previous session
* now since the references could be overwritten if a
* concurrent connection is made to this host and port.
*/
master = cSession.master;
sCert = cSession.cert;
try {
rec.init(role, crand, srand, negSuite, master);
} catch (Exception e) {
complain("Record.init() caught " + e);
}
// ... get change_cipher_spec
if (rcvChangeCipher() < 0) {
complain("Bad ChangeCipherSpec");
}
// ... get finished
if (rcvFinished() < 0) {
complain("Bad Finished");
}
// ... send change_cipher_spec
sndChangeCipher();
// ... send finished
sndFinished();
}
Session.add(peerHost, peerPort, sSessionId, master, sCert);
// Zero out the premaster and master secrets
if (preMaster != null) {
// premaster can be null if we resumed an SSL session
for (int i = 0; i < preMaster.length; i++) {
preMaster[i] = 0;
}
}
for (int i = 0; i < master.length; i++) {
master[i] = 0;
}
}
/**
* Sends a fatal alert indicating handshake_failure and marks
* the corresponding SSL session is non-resumable.
* <p />
* @param msg string containing the exception message to be reported
* @exception IOException with the specified string
*/
private void complain(String msg) throws IOException {
complain(new IOException(msg));
}
/**
* Sends a fatal alert indicating handshake_failure and marks
* the corresponding SSL session is non-resumable.
* <p />
* @param e the IOException to be reported
* @exception IOException
*/
private void complain(IOException e) throws IOException {
try {
rec.alert(Record.FATAL, Record.HNDSHK_FAIL);
if (sSessionId != null) {
Session.del(peerHost, peerPort, sSessionId);
}
} catch (Throwable t) {
// Ignore, we are processing an exception currently
}
throw e;
}
}
/**
* This class implements methods to maintain resumable SSL
* sessions.
*/
// visible within the package
class Session {
/** Maximum number of cached resumable sessions. */
private static final byte MAX_SESSIONS = 4;
/**
* Stores the last index where a session was overwritten, we
* try to do a round-robin selection of places to overwrite
*/
private static int delIdx = 0;
/*
* A session is uniquely identified by the combination of
* host, port and session identifier. The master secret is
* included in the cached session information.
*/
/** Target host name. */
String host;
/** Target port number. */
int port;
/** Session identifier. */
byte[] id;
/** Master secret. */
byte[] master;
/** Target Certificate. */
X509Certificate cert;
/** A cache of currently resumable sessions. */
private static Session[] sessions = new Session[MAX_SESSIONS];
/**
* Gets the master secret associated with a resumable session.
* The session is uniquely identified by the combination of the
* host, port.
*
* @param h host name of peer
* @param p port number of peer
*
* @return matching session
*/
static synchronized Session get(String h, int p) {
for (int i = 0; i < MAX_SESSIONS; i++) {
if ((sessions[i] == null) ||
(sessions[i].id == null)) continue;
if (sessions[i].host.compareTo(h) == 0 &&
sessions[i].port == p) {
return sessions[i];
}
}
return null;
}
/**
* Adds a new session with the specified parameters to the cache
* of resumable sessions. At any given time, this class maintains
* at most one resusumable session for any host/port pair.
* <P />
* @param h host name of peer
* @param p port number of peer
* @param id session identifier
* @param mas master secret
* @param cert certificate of peer
*/
static synchronized void add(String h, int p, byte[] id, byte[] mas,
X509Certificate cert) {
// IMPL_NOTE: This will change if we stop using linear arrays
int idx = MAX_SESSIONS;
for (int i = 0; i < MAX_SESSIONS; i++) {
if ((sessions[i] == null) ||
(sessions[i].id == null)) {
idx = i; // possible candidate for overwriting
continue;
}
if ((sessions[i].host.compareTo(h) == 0) &&
(sessions[i].port == p)) { // preferred candidate
idx = i;
break;
}
}
/*
* If all else is taken, overwrite the one specified by
* delIdx and move delIdx over to the next one. Simulates FIFO.
*/
if (idx == MAX_SESSIONS) {
idx = delIdx;
delIdx++;
if (delIdx == MAX_SESSIONS) delIdx = 0;
}
if (sessions[idx] == null) {
sessions[idx] = new Session();
}
sessions[idx].id = id;
/*
* Since the master will change after this method, we need to
* copy it, to preserve its current value for later.
*/
sessions[idx].master = new byte[mas.length];
System.arraycopy(mas, 0, sessions[idx].master, 0, mas.length);
sessions[idx].host = new String(h); // "h" will be a substring of URL
sessions[idx].port = p;
sessions[idx].cert = cert;
}
/**
* Deletes the session identified by the specfied parameters
* from the cache of resumable sessions.
* <P />
* @param h host name of peer
* @param p port number of peer
* @param sid session identifier
*/
static synchronized void del(String h, int p, byte[] sid) {
for (int i = 0; i < MAX_SESSIONS; i++) {
if ((sessions[i] == null) ||
(sessions[i].id == null)) continue;
if (Utils.byteMatch(sessions[i].id, 0,
sid, 0,
sid.length) &&
(sessions[i].host.compareTo(h) == 0) &&
(sessions[i].port == p)) {
sessions[i].id = null;
sessions[i].master = null;
sessions[i].host = null;
sessions[i].cert = null;
break;
}
}
}
}
|