src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java

Print this page
8167680 DTLS implementation bugs

@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 2015, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2015, 2016, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code 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.  Oracle designates this

@@ -40,14 +40,10 @@
  */
 final class DTLSInputRecord extends InputRecord implements DTLSRecord {
 
     private DTLSReassembler reassembler = null;
 
-    // Cache the session identifier for the detection of session-resuming
-    // handshake.
-    byte[]              prevSessionID = new byte[0];
-
     int                 readEpoch;
 
     int                 prevReadEpoch;
     Authenticator       prevReadAuthenticator;
     CipherBox           prevReadCipher;

@@ -112,19 +108,13 @@
     }
 
     @Override
     Plaintext acquirePlaintext() {
         if (reassembler != null) {
-            Plaintext plaintext = reassembler.acquirePlaintext();
-            if (reassembler.finished()) {
-                // discard all buffered unused message.
-                reassembler = null;
+            return reassembler.acquirePlaintext();
             }
 
-            return plaintext;
-        }
-
         return null;
     }
 
     @Override
     Plaintext decode(ByteBuffer packet) {

@@ -147,44 +137,58 @@
         byte minorVersion = packet.get();                  // pos: 2
         byte[] recordEnS = new byte[8];                    // epoch + seqence
         packet.get(recordEnS);
         int recordEpoch = ((recordEnS[0] & 0xFF) << 8) |
                            (recordEnS[1] & 0xFF);          // pos: 3, 4
-        long recordSeq  = Authenticator.toLong(recordEnS);
+        long recordSeq  = ((recordEnS[2] & 0xFFL) << 40) |
+                          ((recordEnS[3] & 0xFFL) << 32) |
+                          ((recordEnS[4] & 0xFFL) << 24) |
+                          ((recordEnS[5] & 0xFFL) << 16) |
+                          ((recordEnS[6] & 0xFFL) <<  8) |
+                           (recordEnS[7] & 0xFFL);         // pos: 5-10
+
         int contentLen = ((packet.get() & 0xFF) << 8) |
                           (packet.get() & 0xFF);            // pos: 11, 12
 
         if (debug != null && Debug.isOn("record")) {
-             System.out.println(Thread.currentThread().getName() +
-                    ", READ: " +
+            Debug.log("READ: " +
                     ProtocolVersion.valueOf(majorVersion, minorVersion) +
                     " " + Record.contentName(contentType) + ", length = " +
                     contentLen);
         }
 
         int recLim = srcPos + DTLSRecord.headerSize + contentLen;
-        if (this.readEpoch > recordEpoch) {
-            // Discard old records delivered before this epoch.
 
+        if (this.prevReadEpoch > recordEpoch) {
             // Reset the position of the packet buffer.
             packet.position(recLim);
+            if (debug != null && Debug.isOn("record")) {
+                Debug.printHex("READ: discard this old record", recordEnS);
+            }
             return null;
         }
 
+        // Buffer next epoch message if necessary.
         if (this.readEpoch < recordEpoch) {
-            if (contentType != Record.ct_handshake) {
-                // just discard it if not a handshake message
+            // Discard the record younger than the current epcoh if:
+            // 1. it is not a handshake message, or
+            // 2. it is not of next epoch.
+            if (((contentType != Record.ct_handshake) &&
+                    (contentType != Record.ct_change_cipher_spec)) ||
+                (this.readEpoch < (recordEpoch - 1))) {
+
                 packet.position(recLim);
-                return null;
+
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Premature record (epoch), discard it.");
             }
 
-            // Not ready to decrypt this record, may be encrypted Finished
-            // message, need to buffer it.
-            if (reassembler == null) {
-               reassembler = new DTLSReassembler();
+                return null;
             }
 
+            // Not ready to decrypt this record, may be an encrypted Finished
+            // message, need to buffer it.
             byte[] fragment = new byte[contentLen];
             packet.get(fragment);              // copy the fragment
             RecordFragment buffered = new RecordFragment(fragment, contentType,
                     majorVersion, minorVersion,
                     recordEnS, recordEpoch, recordSeq, true);

@@ -192,32 +196,37 @@
             reassembler.queueUpFragment(buffered);
 
             // consume the full record in the packet buffer.
             packet.position(recLim);
 
-            Plaintext plaintext = reassembler.acquirePlaintext();
-            if (reassembler.finished()) {
-                // discard all buffered unused message.
-                reassembler = null;
+            return reassembler.acquirePlaintext();
             }
 
-            return plaintext;
+        //
+        // Now, the message is of this epoch or the previous epoch.
+        //
+        Authenticator decodeAuthenticator;
+        CipherBox decodeCipher;
+        if (this.readEpoch == recordEpoch) {
+            decodeAuthenticator = readAuthenticator;
+            decodeCipher = readCipher;
+        } else {                        // prevReadEpoch == recordEpoch
+            decodeAuthenticator = prevReadAuthenticator;
+            decodeCipher = prevReadCipher;
         }
 
-        if (this.readEpoch == recordEpoch) {
             // decrypt the fragment
             packet.limit(recLim);
             packet.position(srcPos + DTLSRecord.headerSize);
 
             ByteBuffer plaintextFragment;
             try {
-                plaintextFragment = decrypt(readAuthenticator,
-                        readCipher, contentType, packet, recordEnS);
+            plaintextFragment = decrypt(decodeAuthenticator,
+                    decodeCipher, contentType, packet, recordEnS);
             } catch (BadPaddingException bpe) {
                 if (debug != null && Debug.isOn("ssl")) {
-                    System.out.println(Thread.currentThread().getName() +
-                            " discard invalid record: " + bpe);
+                Debug.log("Discard invalid record: " + bpe);
                 }
 
                 // invalid, discard this record [section 4.1.2.7, RFC 6347]
                 return null;
             } finally {

@@ -227,22 +236,43 @@
             }
 
             if (contentType != Record.ct_change_cipher_spec &&
                 contentType != Record.ct_handshake) {   // app data or alert
                                                         // no retransmission
+            // Cleanup the handshake reassembler if necessary.
+            if ((reassembler != null) &&
+                    (reassembler.handshakeEpoch < recordEpoch)) {
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Cleanup the handshake reassembler");
+                }
+
+                reassembler = null;
+            }
+
                return new Plaintext(contentType, majorVersion, minorVersion,
-                        recordEpoch, recordSeq, plaintextFragment);
+                    recordEpoch, Authenticator.toLong(recordEnS),
+                    plaintextFragment);
             }
 
             if (contentType == Record.ct_change_cipher_spec) {
                 if (reassembler == null) {
+                if (this.readEpoch != recordEpoch) {
                     // handshake has not started, should be an
                     // old handshake message, discard it.
+
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log(
+                                "Lagging behind ChangeCipherSpec, discard it.");
+                    }
+
                     return null;
                 }
 
-                reassembler.queueUpFragment(
+                reassembler = new DTLSReassembler(recordEpoch);
+            }
+
+            reassembler.queueUpChangeCipherSpec(
                         new RecordFragment(plaintextFragment, contentType,
                                 majorVersion, minorVersion,
                                 recordEnS, recordEpoch, recordSeq, false));
             } else {    // handshake record
                 // One record may contain 1+ more handshake messages.

@@ -252,38 +282,48 @@
                         contentType, majorVersion, minorVersion,
                         recordEnS, recordEpoch, recordSeq, plaintextFragment);
 
                     if (hsFrag == null) {
                         // invalid, discard this record
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log("Invalid handshake message, discard it.");
+                    }
+
                         return null;
                     }
 
-                    if ((reassembler == null) &&
-                            isKickstart(hsFrag.handshakeType)) {
-                       reassembler = new DTLSReassembler();
+                if (reassembler == null) {
+                    if (this.readEpoch != recordEpoch) {
+                        // handshake has not started, should be an
+                        // old handshake message, discard it.
+
+                        if (debug != null && Debug.isOn("verbose")) {
+                            Debug.log(
+                                "Lagging behind handshake record, discard it.");
                     }
 
-                    if (reassembler != null) {
+                        return null;
+                    }
+
+                    reassembler = new DTLSReassembler(recordEpoch);
+                }
+
                         reassembler.queueUpHandshake(hsFrag);
-                    }   // else, just ignore the message.
                 }
             }
 
             // Completed the read of the full record. Acquire the reassembled
             // messages.
             if (reassembler != null) {
-                Plaintext plaintext = reassembler.acquirePlaintext();
-                if (reassembler.finished()) {
-                    // discard all buffered unused message.
-                    reassembler = null;
+            return reassembler.acquirePlaintext();
                 }
 
-                return plaintext;
+        if (debug != null && Debug.isOn("verbose")) {
+            Debug.log("The reassembler is not initialized yet.");
             }
-        }
 
-        return null;    // make the complier happy
+        return null;
     }
 
     @Override
     int bytesInCompletePacket(ByteBuffer packet) throws SSLException {
 

@@ -328,27 +368,19 @@
                     "Unrecognized record version " + recordVersion +
                     " , plaintext connection?");
         }
     }
 
-    private static boolean isKickstart(byte handshakeType) {
-        return (handshakeType == HandshakeMessage.ht_client_hello) ||
-               (handshakeType == HandshakeMessage.ht_hello_request) ||
-               (handshakeType == HandshakeMessage.ht_hello_verify_request);
-    }
-
     private static HandshakeFragment parseHandshakeMessage(
             byte contentType, byte majorVersion, byte minorVersion,
             byte[] recordEnS, int recordEpoch, long recordSeq,
             ByteBuffer plaintextFragment) {
 
         int remaining = plaintextFragment.remaining();
         if (remaining < handshakeHeaderSize) {
             if (debug != null && Debug.isOn("ssl")) {
-                System.out.println(
-                        Thread.currentThread().getName() +
-                        " discard invalid record: " +
+                Debug.log("Discard invalid record: " +
                         "too small record to hold a handshake fragment");
             }
 
             // invalid, discard this record [section 4.1.2.7, RFC 6347]
             return null;

@@ -370,13 +402,11 @@
                 ((plaintextFragment.get() & 0xFF) << 16) |
                 ((plaintextFragment.get() & 0xFF) << 8) |
                  (plaintextFragment.get() & 0xFF);          // pos: 9-11
         if ((remaining - handshakeHeaderSize) < fragmentLength) {
             if (debug != null && Debug.isOn("ssl")) {
-                System.out.println(
-                        Thread.currentThread().getName() +
-                        " discard invalid record: " +
+                Debug.log("Discard invalid record: " +
                         "not a complete handshake fragment in the record");
             }
 
             // invalid, discard this record [section 4.1.2.7, RFC 6347]
             return null;

@@ -429,14 +459,46 @@
                                             // the buffer if necessary.
         }
 
         @Override
         public int compareTo(RecordFragment o) {
-            return Long.compareUnsigned(this.recordSeq, o.recordSeq);
+            if (this.contentType == Record.ct_change_cipher_spec) {
+                if (o.contentType == Record.ct_change_cipher_spec) {
+                    // Only one incoming ChangeCipherSpec message for an epoch.
+                    //
+                    // Ignore duplicated ChangeCipherSpec messages.
+                    return Integer.compare(this.recordEpoch, o.recordEpoch);
+                } else if ((this.recordEpoch == o.recordEpoch) &&
+                        (o.contentType == Record.ct_handshake)) {
+                    // ChangeCipherSpec is the latest message of an epoch.
+                    return 1;
         }
+            } else if (o.contentType == Record.ct_change_cipher_spec) {
+                if ((this.recordEpoch == o.recordEpoch) &&
+                        (this.contentType == Record.ct_handshake)) {
+                    // ChangeCipherSpec is the latest message of an epoch.
+                    return -1;
+                } else {
+                    // different epoch or this is not a handshake message
+                    return compareToSequence(o.recordEpoch, o.recordSeq);
     }
+            }
 
+            return compareToSequence(o.recordEpoch, o.recordSeq);
+        }
+
+        int compareToSequence(int epoch, long seq) {
+            if (this.recordEpoch > epoch) {
+                return 1;
+            } else if (this.recordEpoch == epoch) {
+                return Long.compare(this.recordSeq, seq);
+            } else {
+                return -1;
+            }
+        }
+    }
+
     // buffered handshake message
     private static final class HandshakeFragment extends RecordFragment {
 
         byte            handshakeType;     // handshake msg_type
         int             messageSeq;        // message_seq

@@ -463,16 +525,28 @@
         @Override
         public int compareTo(RecordFragment o) {
             if (o instanceof HandshakeFragment) {
                 HandshakeFragment other = (HandshakeFragment)o;
                 if (this.messageSeq != other.messageSeq) {
-                    // keep the insertion order for the same message
+                    // keep the insertion order of handshake messages
                     return this.messageSeq - other.messageSeq;
+                } else if (this.fragmentOffset != other.fragmentOffset) {
+                    // small fragment offset was transmitted first
+                    return this.fragmentOffset - other.fragmentOffset;
+                } else if (this.fragmentLength == other.fragmentLength) {
+                    // retransmissions, ignore duplicated messages.
+                    return 0;
                 }
+
+                // Should be repacked for suitable fragment length.
+                //
+                // Note that the acquiring processes will reassemble the
+                // the fragments later.
+                return compareToSequence(o.recordEpoch, o.recordSeq);
             }
 
-            return Long.compareUnsigned(this.recordSeq, o.recordSeq);
+            return super.compareTo(o);
         }
     }
 
     private static final class HoleDescriptor {
         int offset;             // fragment_offset

@@ -482,135 +556,176 @@
             this.offset = offset;
             this.limit = limit;
         }
     }
 
+    private static final class HandshakeFlight implements Cloneable {
+        static final byte HF_UNKNOWN = HandshakeMessage.ht_not_applicable;
+
+        byte        handshakeType;      // handshake type
+        int         flightEpoch;        // the epoch of the first message
+        int         minMessageSeq;      // minimal message sequence
+
+        int         maxMessageSeq;      // maximum message sequence
+        int         maxRecordEpoch;     // maximum record sequence number
+        long        maxRecordSeq;       // maximum record sequence number
+
+        HashMap<Byte, List<HoleDescriptor>> holesMap;
+
+        HandshakeFlight() {
+            this.handshakeType = HF_UNKNOWN;
+            this.flightEpoch = 0;
+            this.minMessageSeq = 0;
+
+            this.maxMessageSeq = 0;
+            this.maxRecordEpoch = 0;
+            this.maxRecordSeq = -1;
+
+            this.holesMap = new HashMap<>(5);
+        }
+
+        boolean isRetransmitOf(HandshakeFlight hs) {
+            return (hs != null) &&
+                   (this.handshakeType == hs.handshakeType) &&
+                   (this.minMessageSeq == hs.minMessageSeq);
+        }
+
+        @Override
+        public Object clone() {
+            HandshakeFlight hf = new HandshakeFlight();
+
+            hf.handshakeType = this.handshakeType;
+            hf.flightEpoch = this.flightEpoch;
+            hf.minMessageSeq = this.minMessageSeq;
+
+            hf.maxMessageSeq = this.maxMessageSeq;
+            hf.maxRecordEpoch = this.maxRecordEpoch;
+            hf.maxRecordSeq = this.maxRecordSeq;
+
+            hf.holesMap = new HashMap<>(this.holesMap);
+
+            return hf;
+        }
+    }
+
     final class DTLSReassembler {
+        // The handshake epoch.
+        final int handshakeEpoch;
+
+        // The buffered fragments.
         TreeSet<RecordFragment> bufferedFragments = new TreeSet<>();
 
-        HashMap<Byte, List<HoleDescriptor>> holesMap = new HashMap<>(5);
+        // The handshake flight in progress.
+        HandshakeFlight handshakeFlight = new HandshakeFlight();
 
-        // Epoch, sequence number and handshake message sequence of the
-        // beginning message of a flight.
-        byte        flightType = (byte)0xFF;
+        // The preceding handshake flight.
+        HandshakeFlight precedingFlight = null;
 
-        int         flightTopEpoch = 0;
-        long        flightTopRecordSeq = -1;
-        int         flightTopMessageSeq = 0;
-
         // Epoch, sequence number and handshake message sequence of the
         // next message acquisition of a flight.
-        int         nextRecordEpoch = 0;    // next record epoch
+        int         nextRecordEpoch;        // next record epoch
         long        nextRecordSeq = 0;      // next record sequence number
-        int         nextMessageSeq = 0;     // next handshake message number
 
         // Expect ChangeCipherSpec and Finished messages for the final flight.
         boolean     expectCCSFlight = false;
 
         // Ready to process this flight if received all messages of the flight.
         boolean     flightIsReady = false;
         boolean     needToCheckFlight = false;
 
-        // Is it a session-resuming abbreviated handshake.?
-        boolean     isAbbreviatedHandshake = false;
+        DTLSReassembler(int handshakeEpoch) {
+            this.handshakeEpoch = handshakeEpoch;
+            this.nextRecordEpoch = handshakeEpoch;
 
-        // The handshke fragment with the biggest record sequence number
-        // in a flight, not counting the Finished message.
-        HandshakeFragment lastHandshakeFragment = null;
-
-        // Is handshake (intput) finished?
-        boolean handshakeFinished = false;
-
-        DTLSReassembler() {
-            // blank
+            this.handshakeFlight.flightEpoch = handshakeEpoch;
         }
 
-        boolean finished() {
-            return handshakeFinished;
-        }
-
         void expectingFinishFlight() {
             expectCCSFlight = true;
         }
 
+        // Queue up a handshake message.
         void queueUpHandshake(HandshakeFragment hsf) {
-
-            if ((nextRecordEpoch > hsf.recordEpoch) ||
-                    (nextRecordSeq > hsf.recordSeq) ||
-                    (nextMessageSeq > hsf.messageSeq)) {
-                // too old, discard this record
+            if (!isDesirable(hsf)) {
+                // Not a dedired record, discard it.
                 return;
             }
 
+            // Clean up the retransmission messages if necessary.
+            cleanUpRetransmit(hsf);
+
             // Is it the first message of next flight?
-            if ((flightTopMessageSeq == hsf.messageSeq) &&
-                    (hsf.fragmentOffset == 0) && (flightTopRecordSeq == -1)) {
+            //
+            // Note: the Finished message is handled in the final CCS flight.
+            boolean isMinimalFlightMessage = false;
+            if (handshakeFlight.minMessageSeq == hsf.messageSeq) {
+                isMinimalFlightMessage = true;
+            } else if ((precedingFlight != null) &&
+                    (precedingFlight.minMessageSeq == hsf.messageSeq)) {
+                isMinimalFlightMessage = true;
+            }
 
-                flightType = hsf.handshakeType;
-                flightTopEpoch = hsf.recordEpoch;
-                flightTopRecordSeq = hsf.recordSeq;
+            if (isMinimalFlightMessage && (hsf.fragmentOffset == 0) &&
+                    (hsf.handshakeType != HandshakeMessage.ht_finished)) {
 
-                if (hsf.handshakeType == HandshakeMessage.ht_server_hello) {
-                    // Is it a session-resuming handshake?
-                    try {
-                        isAbbreviatedHandshake =
-                                isSessionResuming(hsf.fragment, prevSessionID);
-                    } catch (SSLException ssle) {
-                        if (debug != null && Debug.isOn("ssl")) {
-                            System.out.println(
-                                    Thread.currentThread().getName() +
-                                    " discard invalid record: " + ssle);
+                // reset the handshake flight
+                handshakeFlight.handshakeType = hsf.handshakeType;
+                handshakeFlight.flightEpoch = hsf.recordEpoch;
+                handshakeFlight.minMessageSeq = hsf.messageSeq;
                         }
 
-                        // invalid, discard it [section 4.1.2.7, RFC 6347]
-                        return;
+            if (hsf.handshakeType == HandshakeMessage.ht_finished) {
+                handshakeFlight.maxMessageSeq = hsf.messageSeq;
+                handshakeFlight.maxRecordEpoch = hsf.recordEpoch;
+                handshakeFlight.maxRecordSeq = hsf.recordSeq;
+            } else {
+                if (handshakeFlight.maxMessageSeq < hsf.messageSeq) {
+                    handshakeFlight.maxMessageSeq = hsf.messageSeq;
                     }
 
-                    if (!isAbbreviatedHandshake) {
-                        prevSessionID = getSessionID(hsf.fragment);
+                int n = (hsf.recordEpoch - handshakeFlight.maxRecordEpoch);
+                if (n > 0) {
+                    handshakeFlight.maxRecordEpoch = hsf.recordEpoch;
+                    handshakeFlight.maxRecordSeq = hsf.recordSeq;
+                } else if (n == 0) {
+                    // the same epoch
+                    if (handshakeFlight.maxRecordSeq < hsf.recordSeq) {
+                        handshakeFlight.maxRecordSeq = hsf.recordSeq;
                     }
+                }   // Otherwise, it is unlikely to happen.
                 }
-            }
 
             boolean fragmented = false;
             if ((hsf.fragmentOffset) != 0 ||
                 (hsf.fragmentLength != hsf.messageLength)) {
 
                 fragmented = true;
             }
 
-            List<HoleDescriptor> holes = holesMap.get(hsf.handshakeType);
+            List<HoleDescriptor> holes =
+                    handshakeFlight.holesMap.get(hsf.handshakeType);
             if (holes == null) {
                 if (!fragmented) {
                     holes = Collections.emptyList();
                 } else {
                     holes = new LinkedList<HoleDescriptor>();
                     holes.add(new HoleDescriptor(0, hsf.messageLength));
                 }
-                holesMap.put(hsf.handshakeType, holes);
+                handshakeFlight.holesMap.put(hsf.handshakeType, holes);
             } else if (holes.isEmpty()) {
                 // Have got the full handshake message.  This record may be
                 // a handshake message retransmission.  Discard this record.
                 //
                 // It's OK to discard retransmission as the handshake hash
                 // is computed as if each handshake message had been sent
                 // as a single fragment.
-                //
-                // Note that ClientHello messages are delivered twice in
-                // DTLS handshaking.
-                if ((hsf.handshakeType != HandshakeMessage.ht_client_hello &&
-                     hsf.handshakeType != ht_hello_verify_request) ||
-                        (nextMessageSeq != hsf.messageSeq)) {
-                    return;
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Have got the full message, discard it.");
                 }
 
-                if (fragmented) {
-                    holes = new LinkedList<HoleDescriptor>();
-                    holes.add(new HoleDescriptor(0, hsf.messageLength));
+                return;
                 }
-                holesMap.put(hsf.handshakeType, holes);
-            }
 
             if (fragmented) {
                 int fragmentLimit = hsf.fragmentOffset + hsf.fragmentLength;
                 for (int i = 0; i < holes.size(); i++) {
 

@@ -626,13 +741,11 @@
                          (hole.offset < fragmentLimit)) ||
                         ((hole.limit > hsf.fragmentOffset) &&
                          (hole.limit < fragmentLimit))) {
 
                         if (debug != null && Debug.isOn("ssl")) {
-                            System.out.println(
-                                Thread.currentThread().getName() +
-                                " discard invalid record: " +
+                            Debug.log("Discard invalid record: " +
                                 "handshake fragment ranges are overlapping");
                         }
 
                         // invalid, discard it [section 4.1.2.7, RFC 6347]
                         return;

@@ -657,108 +770,345 @@
                     // As no ranges overlap, no interact with other holes.
                     break;
                 }
             }
 
-            // append this fragment
+            // buffer this fragment
+            if (hsf.handshakeType == HandshakeMessage.ht_finished) {
+                // Need no status update.
             bufferedFragments.add(hsf);
+            } else {
+                bufferFragment(hsf);
+            }
+        }
 
-            if ((lastHandshakeFragment == null) ||
-                (lastHandshakeFragment.compareTo(hsf) < 0)) {
+        // Queue up a ChangeCipherSpec message
+        void queueUpChangeCipherSpec(RecordFragment rf) {
+            if (!isDesirable(rf)) {
+                // Not a dedired record, discard it.
+                return;
+            }
 
-                lastHandshakeFragment = hsf;
+            // Clean up the retransmission messages if necessary.
+            cleanUpRetransmit(rf);
+
+            // Is it the first message of this flight?
+            //
+            // Note: the first message of the final flight is ChangeCipherSpec.
+            if (expectCCSFlight) {
+                handshakeFlight.handshakeType = HandshakeFlight.HF_UNKNOWN;
+                handshakeFlight.flightEpoch = rf.recordEpoch;
             }
 
-            if (flightIsReady) {
-                flightIsReady = false;
+            // The epoch should be the same as the first message of the flight.
+            if (handshakeFlight.maxRecordSeq < rf.recordSeq) {
+                handshakeFlight.maxRecordSeq = rf.recordSeq;
             }
-            needToCheckFlight = true;
+
+            // buffer this fragment
+            bufferFragment(rf);
         }
 
-        // queue up change_cipher_spec or encrypted message
+        // Queue up a ciphertext message.
+        //
+        // Note: not yet be able to decrypt the message.
         void queueUpFragment(RecordFragment rf) {
-            if ((nextRecordEpoch > rf.recordEpoch) ||
-                    (nextRecordSeq > rf.recordSeq)) {
-                // too old, discard this record
+            if (!isDesirable(rf)) {
+                // Not a dedired record, discard it.
                 return;
             }
 
-            // Is it the first message of next flight?
-            if (expectCCSFlight &&
-                    (rf.contentType == Record.ct_change_cipher_spec)) {
+            // Clean up the retransmission messages if necessary.
+            cleanUpRetransmit(rf);
 
-                flightType = (byte)0xFE;
-                flightTopEpoch = rf.recordEpoch;
-                flightTopRecordSeq = rf.recordSeq;
+            // buffer this fragment
+            bufferFragment(rf);
             }
 
+        private void bufferFragment(RecordFragment rf) {
             // append this fragment
             bufferedFragments.add(rf);
 
             if (flightIsReady) {
                 flightIsReady = false;
             }
+
+            if (!needToCheckFlight) {
             needToCheckFlight = true;
         }
+        }
 
-        boolean isEmpty() {
+        private void cleanUpRetransmit(RecordFragment rf) {
+            // Does the next flight start?
+            boolean isNewFlight = false;
+            if (precedingFlight != null) {
+                if (precedingFlight.flightEpoch < rf.recordEpoch) {
+                    isNewFlight = true;
+                } else {
+                    if (rf instanceof HandshakeFragment) {
+                        HandshakeFragment hsf = (HandshakeFragment)rf;
+                        if (precedingFlight.maxMessageSeq  < hsf.messageSeq) {
+                            isNewFlight = true;
+                        }
+                    } else if (rf.contentType != Record.ct_change_cipher_spec) {
+                        // ciphertext
+                        if (precedingFlight.maxRecordEpoch < rf.recordEpoch) {
+                            isNewFlight = true;
+                        }
+                    }
+                }
+            }
+
+            if (!isNewFlight) {
+                // Need no cleanup.
+                return;
+            }
+
+            // clean up the buffer
+            for (Iterator<RecordFragment> it = bufferedFragments.iterator();
+                    it.hasNext();) {
+
+                RecordFragment frag = it.next();
+                boolean isOld = false;
+                if (frag.recordEpoch < precedingFlight.maxRecordEpoch) {
+                    isOld = true;
+                } else if (frag.recordEpoch == precedingFlight.maxRecordEpoch) {
+                    if (frag.recordSeq <= precedingFlight.maxRecordSeq) {
+                        isOld = true;
+                    }
+                }
+
+                if (!isOld && (frag instanceof HandshakeFragment)) {
+                    HandshakeFragment hsf = (HandshakeFragment)frag;
+                    isOld = (hsf.messageSeq <= precedingFlight.maxMessageSeq);
+                }
+
+                if (isOld) {
+                    it.remove();
+                } else {
+                    // Safe to break as items in the buffer are ordered. 
+                    break;
+                }
+            }
+
+            // discard retransmissions of the previous flight if any.
+            precedingFlight = null;
+        }
+
+        // Is a desired record?
+        //
+        // Check for retransmission and lost records.
+        private boolean isDesirable(RecordFragment rf) {
+            //
+            // Discard records old than the previous epoch.
+            //
+            int previousEpoch = nextRecordEpoch - 1;
+            if (rf.recordEpoch < previousEpoch) {
+                // Too old to use, discard this record.
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Too old epoch to use this record, discard it.");
+                }
+
+                return false;
+            }
+
+            //
+            // Allow retransmission of last flight of the previous epoch
+            //
+            // For example, the last server delivered flight for session
+            // resuming abbreviated handshaking consist three messages:
+            //      ServerHello
+            //      [ChangeCipherSpec]
+            //      Finished
+            //
+            // The epoch number is incremented and the sequence number is reset
+            // if the ChangeCipherSpec is sent.
+            if (rf.recordEpoch == previousEpoch) {
+                boolean isDesired = true;
+                if (precedingFlight == null) {
+                    isDesired = false;
+                } else {
+                    if (rf instanceof HandshakeFragment) {
+                        HandshakeFragment hsf = (HandshakeFragment)rf;
+                        if (precedingFlight.minMessageSeq > hsf.messageSeq) {
+                            isDesired = false;
+                        }
+                    } else if (rf.contentType == Record.ct_change_cipher_spec) {
+                        // ChangeCipherSpec
+                        if (precedingFlight.flightEpoch != rf.recordEpoch) {
+                            isDesired = false;
+                        }
+                    } else {        // ciphertext
+                        if ((rf.recordEpoch < precedingFlight.maxRecordEpoch) ||
+                            (rf.recordEpoch == precedingFlight.maxRecordEpoch &&
+                                rf.recordSeq <= precedingFlight.maxRecordSeq)) {
+                            isDesired = false;
+                        }
+                    }
+                }
+
+                if (!isDesired) {
+                    // Too old to use, discard this retransmitted record
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log("Too old retransmission to use, discard it.");
+                    }
+
+                    return false;
+                }
+            } else if ((rf.recordEpoch == nextRecordEpoch) &&
+                    (nextRecordSeq > rf.recordSeq)) {
+
+                // Previously disordered record for the current epoch.
+                //
+                // Should has been retransmitted. Discard this record.
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Lagging behind record (sequence), discard it.");
+                }
+
+                return false;
+            }
+
+            return true;
+        }
+
+        private boolean isEmpty() {
             return (bufferedFragments.isEmpty() ||
                     (!flightIsReady && !needToCheckFlight) ||
                     (needToCheckFlight && !flightIsReady()));
         }
 
         Plaintext acquirePlaintext() {
             if (bufferedFragments.isEmpty()) {
-                // reset the flight
-                if (flightIsReady) {
-                    flightIsReady = false;
-                    needToCheckFlight = false;
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("No received handshake messages");
                 }
-
                 return null;
             }
 
             if (!flightIsReady && needToCheckFlight) {
                 // check the fligth status
                 flightIsReady = flightIsReady();
 
-                // set for next flight
+                // Reset if this flight is ready.
                 if (flightIsReady) {
-                    flightTopMessageSeq = lastHandshakeFragment.messageSeq + 1;
-                    flightTopRecordSeq = -1;
+                    // Retransmitted handshake messages are not needed for
+                    // further handshaking processing.
+                    if (handshakeFlight.isRetransmitOf(precedingFlight)) {
+                        // cleanup
+                        bufferedFragments.clear();
+
+                        // Reset the next handshake flight.
+                        resetHandshakeFlight(precedingFlight);
+
+                        if (debug != null && Debug.isOn("verbose")) {
+                            Debug.log("Received a retransmission flight.");
                 }
 
+                        return Plaintext.PLAINTEXT_NULL;
+                    }
+                }
+
                 needToCheckFlight = false;
             }
 
             if (!flightIsReady) {
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("The handshake flight is not ready to use: " +
+                                handshakeFlight.handshakeType);
+                }
                 return null;
             }
 
             RecordFragment rFrag = bufferedFragments.first();
+            Plaintext plaintext;
             if (!rFrag.isCiphertext) {
                 // handshake message, or ChangeCipherSpec message
-                return acquireHandshakeMessage();
+                plaintext = acquireHandshakeMessage();
+
+                // Reset the handshake flight.
+                if (bufferedFragments.isEmpty()) {
+                    // Need not to backup the holes map.  Clear up it at first.
+                    handshakeFlight.holesMap.clear();   // cleanup holes map
+
+                    // Update the preceding flight.
+                    precedingFlight = (HandshakeFlight)handshakeFlight.clone();
+
+                    // Reset the next handshake flight.
+                    resetHandshakeFlight(precedingFlight);
+
+                    if (expectCCSFlight &&
+                            (precedingFlight.flightEpoch ==
+                                    HandshakeFlight.HF_UNKNOWN)) {
+                        expectCCSFlight = false;
+                    }
+                }
             } else {
                 // a Finished message or other ciphertexts
-                return acquireCachedMessage();
+                plaintext = acquireCachedMessage();
             }
+
+            return plaintext;
         }
 
+        //
+        // Reset the handshake flight from a previous one.
+        //
+        private void resetHandshakeFlight(HandshakeFlight prev) {
+            // Reset the next handshake flight.
+            handshakeFlight.handshakeType = HandshakeFlight.HF_UNKNOWN;
+            handshakeFlight.flightEpoch = prev.maxRecordEpoch;
+            if (prev.flightEpoch != prev.maxRecordEpoch) {
+                // a new epoch starts
+                handshakeFlight.minMessageSeq = 0;
+            } else {
+                // stay at the same epoch
+                //
+                // The minimal message sequence number will get updated if
+                // a flight retransmission happens.
+                handshakeFlight.minMessageSeq = prev.maxMessageSeq + 1;
+            }
+
+            // cleanup the maximum sequence number and epoch number.
+            //
+            // Note: actually, we need to do nothing because the reassembler
+            // of handshake messages will reset them properly even for
+            // retransmissions.
+            //
+            handshakeFlight.maxMessageSeq = 0;
+            handshakeFlight.maxRecordEpoch = handshakeFlight.flightEpoch;
+
+            // Record sequence number cannot wrap even for retransmissions.
+            handshakeFlight.maxRecordSeq = prev.maxRecordSeq + 1;
+
+            // cleanup holes map
+            handshakeFlight.holesMap.clear();
+
+            // Ready to accept new input record.
+            flightIsReady = false;
+            needToCheckFlight = false;
+        }
+
         private Plaintext acquireCachedMessage() {
 
             RecordFragment rFrag = bufferedFragments.first();
             if (readEpoch != rFrag.recordEpoch) {
                 if (readEpoch > rFrag.recordEpoch) {
                     // discard old records
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log("Discard old buffered ciphertext fragments.");
+                    }
                     bufferedFragments.remove(rFrag);    // popup the fragment
                 }
 
                 // reset the flight
                 if (flightIsReady) {
                     flightIsReady = false;
                 }
+
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Not yet ready to decrypt the cached fragments.");
+                }
                 return null;
             }
 
             bufferedFragments.remove(rFrag);    // popup the fragment
 

@@ -766,13 +1116,12 @@
             ByteBuffer plaintextFragment = null;
             try {
                 plaintextFragment = decrypt(readAuthenticator, readCipher,
                         rFrag.contentType, fragment, rFrag.recordEnS);
             } catch (BadPaddingException bpe) {
-                if (debug != null && Debug.isOn("ssl")) {
-                    System.out.println(Thread.currentThread().getName() +
-                            " discard invalid record: " + bpe);
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Discard invalid record: " + bpe);
                 }
 
                 // invalid, discard this record [section 4.1.2.7, RFC 6347]
                 return null;
             }

@@ -780,100 +1129,70 @@
             // The ciphtext handshake message can only be Finished (the
             // end of this flight), ClinetHello or HelloRequest (the
             // beginning of the next flight) message.  Need not to check
             // any ChangeCipherSpec message.
             if (rFrag.contentType == Record.ct_handshake) {
-                HandshakeFragment finFrag = null;
                 while (plaintextFragment.remaining() > 0) {
                     HandshakeFragment hsFrag = parseHandshakeMessage(
                             rFrag.contentType,
                             rFrag.majorVersion, rFrag.minorVersion,
                             rFrag.recordEnS, rFrag.recordEpoch, rFrag.recordSeq,
                             plaintextFragment);
 
                     if (hsFrag == null) {
                         // invalid, discard this record
+                        if (debug != null && Debug.isOn("verbose")) {
+                            Debug.printHex(
+                                    "Invalid handshake fragment, discard it",
+                                    plaintextFragment);
+                        }
                         return null;
                     }
 
-                    if (hsFrag.handshakeType == HandshakeMessage.ht_finished) {
-                        finFrag = hsFrag;
-
-                        // reset for the next flight
-                        this.flightType = (byte)0xFF;
-                        this.flightTopEpoch = rFrag.recordEpoch;
-                        this.flightTopMessageSeq = hsFrag.messageSeq + 1;
-                        this.flightTopRecordSeq = -1;
-                    } else {
-                        // reset the flight
-                        if (flightIsReady) {
+                    queueUpHandshake(hsFrag);
+                    // The flight ready status (flightIsReady) should have
+                    // been checked and updated for the Finished handshake
+                    // message before the decryption.  Please don't update
+                    // flightIsReady for Finished messages.
+                    if (hsFrag.handshakeType != HandshakeMessage.ht_finished) {
                             flightIsReady = false;
+                        needToCheckFlight = true;
                         }
-                        queueUpHandshake(hsFrag);
                     }
-                }
 
-                this.nextRecordSeq = rFrag.recordSeq + 1;
-                this.nextMessageSeq = 0;
-
-                if (finFrag != null) {
-                    this.nextRecordEpoch = finFrag.recordEpoch;
-                    this.nextRecordSeq = finFrag.recordSeq + 1;
-                    this.nextMessageSeq = finFrag.messageSeq + 1;
-
-                    // Finished message does not fragment.
-                    byte[] recordFrag = new byte[finFrag.messageLength + 4];
-                    Plaintext plaintext = new Plaintext(finFrag.contentType,
-                            finFrag.majorVersion, finFrag.minorVersion,
-                            finFrag.recordEpoch, finFrag.recordSeq,
-                            ByteBuffer.wrap(recordFrag));
-
-                    // fill the handshake fragment of the record
-                    recordFrag[0] = finFrag.handshakeType;
-                    recordFrag[1] =
-                            (byte)((finFrag.messageLength >>> 16) & 0xFF);
-                    recordFrag[2] =
-                            (byte)((finFrag.messageLength >>> 8) & 0xFF);
-                    recordFrag[3] = (byte)(finFrag.messageLength & 0xFF);
-
-                    System.arraycopy(finFrag.fragment, 0,
-                            recordFrag, 4, finFrag.fragmentLength);
-
-                    // handshake hashing
-                    handshakeHashing(finFrag, plaintext);
-
-                    // input handshake finished
-                    handshakeFinished = true;
-
-                    return plaintext;
-                } else {
                     return acquirePlaintext();
-                }
             } else {
                 return new Plaintext(rFrag.contentType,
                         rFrag.majorVersion, rFrag.minorVersion,
-                        rFrag.recordEpoch, rFrag.recordSeq,
+                        rFrag.recordEpoch,
+                        Authenticator.toLong(rFrag.recordEnS),
                         plaintextFragment);
             }
         }
 
         private Plaintext acquireHandshakeMessage() {
 
             RecordFragment rFrag = bufferedFragments.first();
             if (rFrag.contentType == Record.ct_change_cipher_spec) {
                 this.nextRecordEpoch = rFrag.recordEpoch + 1;
+
+                // For retransmissions, the next record sequence number is a
+                // positive value.  Don't worry about it as the acquiring of
+                // the immediately followed Finished handshake message will
+                // reset the next record sequence number correctly.
                 this.nextRecordSeq = 0;
-                // no change on next handshake message sequence number
 
-                bufferedFragments.remove(rFrag);        // popup the fragment
+                // Popup the fragment.
+                bufferedFragments.remove(rFrag);
 
                 // Reload if this message has been reserved for handshake hash.
                 handshakeHash.reload();
 
                 return new Plaintext(rFrag.contentType,
                         rFrag.majorVersion, rFrag.minorVersion,
-                        rFrag.recordEpoch, rFrag.recordSeq,
+                        rFrag.recordEpoch,
+                        Authenticator.toLong(rFrag.recordEnS),
                         ByteBuffer.wrap(rFrag.fragment));
             } else {    // rFrag.contentType == Record.ct_handshake
                 HandshakeFragment hsFrag = (HandshakeFragment)rFrag;
                 if ((hsFrag.messageLength == hsFrag.fragmentLength) &&
                     (hsFrag.fragmentOffset == 0)) {     // no fragmentation

@@ -880,17 +1199,17 @@
 
                     bufferedFragments.remove(rFrag);    // popup the fragment
 
                     // this.nextRecordEpoch = hsFrag.recordEpoch;
                     this.nextRecordSeq = hsFrag.recordSeq + 1;
-                    this.nextMessageSeq = hsFrag.messageSeq + 1;
 
                     // Note: may try to avoid byte array copy in the future.
                     byte[] recordFrag = new byte[hsFrag.messageLength + 4];
                     Plaintext plaintext = new Plaintext(hsFrag.contentType,
                             hsFrag.majorVersion, hsFrag.minorVersion,
-                            hsFrag.recordEpoch, hsFrag.recordSeq,
+                            hsFrag.recordEpoch,
+                            Authenticator.toLong(hsFrag.recordEnS),
                             ByteBuffer.wrap(recordFrag));
 
                     // fill the handshake fragment of the record
                     recordFrag[0] = hsFrag.handshakeType;
                     recordFrag[1] =

@@ -911,11 +1230,12 @@
                     //
                     // Note: may try to avoid byte array copy in the future.
                     byte[] recordFrag = new byte[hsFrag.messageLength + 4];
                     Plaintext plaintext = new Plaintext(hsFrag.contentType,
                             hsFrag.majorVersion, hsFrag.minorVersion,
-                            hsFrag.recordEpoch, hsFrag.recordSeq,
+                            hsFrag.recordEpoch,
+                            Authenticator.toLong(hsFrag.recordEnS),
                             ByteBuffer.wrap(recordFrag));
 
                     // fill the handshake fragment of the record
                     recordFrag[0] = hsFrag.handshakeType;
                     recordFrag[1] =

@@ -955,71 +1275,107 @@
 
                     // handshake hashing
                     handshakeHashing(hsFrag, plaintext);
 
                     this.nextRecordSeq = maxRecodeSN + 1;
-                    this.nextMessageSeq = msgSeq + 1;
 
                     return plaintext;
                 }
             }
         }
 
         boolean flightIsReady() {
 
+            byte flightType = handshakeFlight.handshakeType;
+            if (flightType == HandshakeFlight.HF_UNKNOWN) {
             //
             // the ChangeCipherSpec/Finished flight
             //
             if (expectCCSFlight) {
-                // Have the ChangeCipherSpec/Finished messages been received?
-                return hasFinisedMessage(bufferedFragments);
+                    // Have the ChangeCipherSpec/Finished flight been received?
+                    boolean isReady = hasFinishedMessage(bufferedFragments);
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log(
+                            "Has the final flight been received? " + isReady);
             }
 
-            if (flightType == (byte)0xFF) {
+                    return isReady;
+                }
+
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("No flight is received yet.");
+                }
+
                 return false;
             }
 
             if ((flightType == HandshakeMessage.ht_client_hello) ||
                 (flightType == HandshakeMessage.ht_hello_request) ||
                 (flightType == HandshakeMessage.ht_hello_verify_request)) {
 
                 // single handshake message flight
-                return hasCompleted(holesMap.get(flightType));
+                boolean isReady = hasCompleted(flightType);
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Is the handshake message completed? " + isReady);
             }
 
+                return isReady;
+            }
+
             //
             // the ServerHello flight
             //
             if (flightType == HandshakeMessage.ht_server_hello) {
                 // Firstly, check the first flight handshake message.
-                if (!hasCompleted(holesMap.get(flightType))) {
+                if (!hasCompleted(flightType)) {
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log(
+                            "The ServerHello message is not completed yet.");
+                    }
+
                     return false;
                 }
 
                 //
                 // an abbreviated handshake
                 //
-                if (isAbbreviatedHandshake) {
-                    // Ready to use the flight if received the
-                    // ChangeCipherSpec and Finished messages.
-                    return hasFinisedMessage(bufferedFragments);
+                if (hasFinishedMessage(bufferedFragments)) {
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log("It's an abbreviated handshake.");
                 }
 
+                    return true;
+                }
+
                 //
                 // a full handshake
                 //
-                if (lastHandshakeFragment.handshakeType !=
-                        HandshakeMessage.ht_server_hello_done) {
+                List<HoleDescriptor> holes = handshakeFlight.holesMap.get(
+                        HandshakeMessage.ht_server_hello_done);
+                if ((holes == null) || !holes.isEmpty()) {
                     // Not yet got the final message of the flight.
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log("Not yet got the ServerHelloDone message");
+                    }
+
                     return false;
                 }
 
                 // Have all handshake message been received?
-                return hasCompleted(bufferedFragments,
-                    flightTopMessageSeq, lastHandshakeFragment.messageSeq);
+                boolean isReady = hasCompleted(bufferedFragments,
+                            handshakeFlight.minMessageSeq,
+                            handshakeFlight.maxMessageSeq);
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Is the ServerHello flight (message " +
+                            handshakeFlight.minMessageSeq + "-" +
+                            handshakeFlight.maxMessageSeq +
+                            ") completed? " + isReady);
             }
 
+                return isReady;
+            }
+
             //
             // the ClientKeyExchange flight
             //
             // Note: need to consider more messages in this flight if
             //       ht_supplemental_data and ht_certificate_url are

@@ -1027,105 +1383,77 @@
             //
             if ((flightType == HandshakeMessage.ht_certificate) ||
                 (flightType == HandshakeMessage.ht_client_key_exchange)) {
 
                 // Firstly, check the first flight handshake message.
-                if (!hasCompleted(holesMap.get(flightType))) {
-                    return false;
+                if (!hasCompleted(flightType)) {
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log(
+                            "The ClientKeyExchange or client Certificate " +
+                            "message is not completed yet.");
                 }
 
-                if (!hasFinisedMessage(bufferedFragments)) {
-                    // not yet got the ChangeCipherSpec/Finished messages
                     return false;
                 }
 
-                if (flightType == HandshakeMessage.ht_client_key_exchange) {
-                    // single handshake message flight
-                    return true;
+                // Is client CertificateVerify a mandatory message?
+                if (flightType == HandshakeMessage.ht_certificate) {
+                    if (needClientVerify(bufferedFragments) &&
+                        !hasCompleted(ht_certificate_verify)) {
+
+                        if (debug != null && Debug.isOn("verbose")) {
+                            Debug.log(
+                                "Not yet have the CertificateVerify message");
                 }
 
-                //
-                // flightType == HandshakeMessage.ht_certificate
-                //
-                // We don't support certificates containing fixed
-                // Diffie-Hellman parameters.  Therefore, CertificateVerify
-                // message is required if client Certificate message presents.
-                //
-                if (lastHandshakeFragment.handshakeType !=
-                        HandshakeMessage.ht_certificate_verify) {
-                    // Not yet got the final message of the flight.
                     return false;
                 }
+                }
 
-                // Have all handshake message been received?
-                return hasCompleted(bufferedFragments,
-                    flightTopMessageSeq, lastHandshakeFragment.messageSeq);
+                if (!hasFinishedMessage(bufferedFragments)) {
+                    // not yet have the ChangeCipherSpec/Finished messages
+                    if (debug != null && Debug.isOn("verbose")) {
+                        Debug.log(
+                            "Not yet have the ChangeCipherSpec and " +
+                            "Finished messages");
             }
 
-            //
-            // Otherwise, need to receive more handshake messages.
-            //
             return false;
         }
 
-        private boolean isSessionResuming(
-                byte[] fragment, byte[] prevSid) throws SSLException {
-
-            // As the first fragment of ServerHello should be big enough
-            // to hold the session_id field, need not to worry about the
-            // fragmentation here.
-            if ((fragment == null) || (fragment.length < 38)) {
-                                    // 38: the minimal ServerHello body length
-                throw new SSLException(
-                        "Invalid ServerHello message: no sufficient data");
+                // Have all handshake message been received?
+                boolean isReady = hasCompleted(bufferedFragments,
+                            handshakeFlight.minMessageSeq,
+                            handshakeFlight.maxMessageSeq);
+                if (debug != null && Debug.isOn("verbose")) {
+                    Debug.log("Is the ClientKeyExchange flight (message " +
+                            handshakeFlight.minMessageSeq + "-" +
+                            handshakeFlight.maxMessageSeq +
+                            ") completed? " + isReady);
             }
 
-            int sidLen = fragment[34];          // 34: the length field
-            if (sidLen > 32) {                  // opaque SessionID<0..32>
-                throw new SSLException(
-                        "Invalid ServerHello message: invalid session id");
+                return isReady;
             }
 
-            if (fragment.length < 38 + sidLen) {
-                throw new SSLException(
-                        "Invalid ServerHello message: no sufficient data");
+            //
+            // Otherwise, need to receive more handshake messages.
+            //
+            if (debug != null && Debug.isOn("verbose")) {
+                Debug.log("Need to receive more handshake messages");
             }
 
-            if (sidLen != 0 && (prevSid.length == sidLen)) {
-                // may be a session-resuming handshake
-                for (int i = 0; i < sidLen; i++) {
-                    if (prevSid[i] != fragment[35 + i]) {
-                                                // 35: the session identifier
                         return false;
                     }
-                }
 
-                return true;
-            }
-
-            return false;
-        }
-
-        private byte[] getSessionID(byte[] fragment) {
-            // The validity has been checked in the call to isSessionResuming().
-            int sidLen = fragment[34];      // 34: the sessionID length field
-
-            byte[] temporary = new byte[sidLen];
-            System.arraycopy(fragment, 35, temporary, 0, sidLen);
-
-            return temporary;
-        }
-
         // Looking for the ChangeCipherSpec and Finished messages.
         //
         // As the cached Finished message should be a ciphertext, we don't
         // exactly know a ciphertext is a Finished message or not.  According
         // to the spec of TLS/DTLS handshaking, a Finished message is always
         // sent immediately after a ChangeCipherSpec message.  The first
         // ciphertext handshake message should be the expected Finished message.
-        private boolean hasFinisedMessage(
-                Set<RecordFragment> fragments) {
+        private boolean hasFinishedMessage(Set<RecordFragment> fragments) {
 
             boolean hasCCS = false;
             boolean hasFin = false;
             for (RecordFragment fragment : fragments) {
                 if (fragment.contentType == Record.ct_change_cipher_spec) {

@@ -1145,11 +1473,39 @@
             }
 
             return hasFin && hasCCS;
         }
 
-        private boolean hasCompleted(List<HoleDescriptor> holes) {
+        // Is client CertificateVerify a mandatory message?
+        //
+        // In the current implementation, client CertificateVerify is a
+        // mandatory message if the client Certificate is not empty.
+        private boolean needClientVerify(Set<RecordFragment> fragments) {
+
+            // The caller should have checked the completion of the first
+            // present handshake message.  Need not to check it again.
+            for (RecordFragment rFrag : fragments) {
+                if ((rFrag.contentType != Record.ct_handshake) ||
+                        rFrag.isCiphertext) {
+                    break;
+                }
+
+                HandshakeFragment hsFrag = (HandshakeFragment)rFrag;
+                if (hsFrag.handshakeType != HandshakeMessage.ht_certificate) {
+                    continue;
+                }
+
+                return (rFrag.fragment != null) &&
+                   (rFrag.fragment.length > DTLSRecord.minCertPlaintextSize);
+            }
+
+            return false;
+        }
+
+        private boolean hasCompleted(byte handshakeType) {
+            List<HoleDescriptor> holes =
+                    handshakeFlight.holesMap.get(handshakeType);
             if (holes == null) {
                 // not yet received this kind of handshake message
                 return false;
             }
 

@@ -1171,11 +1527,11 @@
                 HandshakeFragment hsFrag = (HandshakeFragment)rFrag;
                 if (hsFrag.messageSeq == presentMsgSeq) {
                     continue;
                 } else if (hsFrag.messageSeq == (presentMsgSeq + 1)) {
                     // check the completion of the handshake message
-                    if (!hasCompleted(holesMap.get(hsFrag.handshakeType))) {
+                    if (!hasCompleted(hsFrag.handshakeType)) {
                         return false;
                     }
 
                     presentMsgSeq = hsFrag.messageSeq;
                 } else {