--- old/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java Thu Oct 27 12:47:16 2016 +++ new/src/java.base/share/classes/sun/security/ssl/DTLSInputRecord.java Thu Oct 27 12:47:16 2016 @@ -1,5 +1,5 @@ /* - * 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 @@ -42,10 +42,6 @@ private DTLSReassembler reassembler = null; - // Cache the session identifier for the detection of session-resuming - // handshake. - byte[] prevSessionID = new byte[0]; - int readEpoch; int prevReadEpoch; @@ -114,13 +110,7 @@ @Override Plaintext acquirePlaintext() { if (reassembler != null) { - Plaintext plaintext = reassembler.acquirePlaintext(); - if (reassembler.finished()) { - // discard all buffered unused message. - reassembler = null; - } - - return plaintext; + return reassembler.acquirePlaintext(); } return null; @@ -149,13 +139,18 @@ 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 + (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); @@ -162,27 +157,36 @@ } 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); + + if (debug != null && Debug.isOn("verbose")) { + Debug.log("Premature record (epoch), discard it."); + } + return null; } - // Not ready to decrypt this record, may be encrypted Finished + // Not ready to decrypt this record, may be an encrypted Finished // message, need to buffer it. - if (reassembler == null) { - reassembler = new DTLSReassembler(); - } - byte[] fragment = new byte[contentLen]; packet.get(fragment); // copy the fragment RecordFragment buffered = new RecordFragment(fragment, contentType, @@ -194,94 +198,130 @@ // 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 plaintext; + return reassembler.acquirePlaintext(); } + // + // Now, the message is of this epoch or the previous epoch. + // + Authenticator decodeAuthenticator; + CipherBox decodeCipher; if (this.readEpoch == recordEpoch) { - // decrypt the fragment - packet.limit(recLim); - packet.position(srcPos + DTLSRecord.headerSize); + decodeAuthenticator = readAuthenticator; + decodeCipher = readCipher; + } else { // prevReadEpoch == recordEpoch + decodeAuthenticator = prevReadAuthenticator; + decodeCipher = prevReadCipher; + } - ByteBuffer plaintextFragment; - try { - plaintextFragment = decrypt(readAuthenticator, - readCipher, contentType, packet, recordEnS); - } catch (BadPaddingException bpe) { - if (debug != null && Debug.isOn("ssl")) { - System.out.println(Thread.currentThread().getName() + - " discard invalid record: " + bpe); - } + // decrypt the fragment + packet.limit(recLim); + packet.position(srcPos + DTLSRecord.headerSize); - // invalid, discard this record [section 4.1.2.7, RFC 6347] - return null; - } finally { - // comsume a complete record - packet.limit(srcLim); - packet.position(recLim); + ByteBuffer plaintextFragment; + try { + plaintextFragment = decrypt(decodeAuthenticator, + decodeCipher, contentType, packet, recordEnS); + } catch (BadPaddingException bpe) { + if (debug != null && Debug.isOn("ssl")) { + Debug.log("Discard invalid record: " + bpe); } - if (contentType != Record.ct_change_cipher_spec && - contentType != Record.ct_handshake) { // app data or alert - // no retransmission - return new Plaintext(contentType, majorVersion, minorVersion, - recordEpoch, recordSeq, plaintextFragment); + // invalid, discard this record [section 4.1.2.7, RFC 6347] + return null; + } finally { + // comsume a complete record + packet.limit(srcLim); + packet.position(recLim); + } + + 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; } - if (contentType == Record.ct_change_cipher_spec) { - if (reassembler == null) { + return new Plaintext(contentType, majorVersion, minorVersion, + 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( - new RecordFragment(plaintextFragment, contentType, - majorVersion, minorVersion, - recordEnS, recordEpoch, recordSeq, false)); - } else { // handshake record - // One record may contain 1+ more handshake messages. - while (plaintextFragment.remaining() > 0) { + reassembler = new DTLSReassembler(recordEpoch); + } - HandshakeFragment hsFrag = parseHandshakeMessage( - contentType, majorVersion, minorVersion, - recordEnS, recordEpoch, recordSeq, plaintextFragment); + reassembler.queueUpChangeCipherSpec( + new RecordFragment(plaintextFragment, contentType, + majorVersion, minorVersion, + recordEnS, recordEpoch, recordSeq, false)); + } else { // handshake record + // One record may contain 1+ more handshake messages. + while (plaintextFragment.remaining() > 0) { - if (hsFrag == null) { - // invalid, discard this record - return null; - } + HandshakeFragment hsFrag = parseHandshakeMessage( + contentType, majorVersion, minorVersion, + recordEnS, recordEpoch, recordSeq, plaintextFragment); - if ((reassembler == null) && - isKickstart(hsFrag.handshakeType)) { - reassembler = new DTLSReassembler(); + if (hsFrag == null) { + // invalid, discard this record + if (debug != null && Debug.isOn("verbose")) { + Debug.log("Invalid handshake message, discard it."); } - if (reassembler != null) { - reassembler.queueUpHandshake(hsFrag); - } // else, just ignore the message. + return null; } - } - // 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; + 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."); + } + + return null; + } + + reassembler = new DTLSReassembler(recordEpoch); } - return plaintext; + reassembler.queueUpHandshake(hsFrag); } } - return null; // make the complier happy + // Completed the read of the full record. Acquire the reassembled + // messages. + if (reassembler != null) { + return reassembler.acquirePlaintext(); + } + + if (debug != null && Debug.isOn("verbose")) { + Debug.log("The reassembler is not initialized yet."); + } + + return null; } @Override @@ -330,12 +370,6 @@ } } - 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, @@ -344,9 +378,7 @@ 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"); } @@ -372,9 +404,7 @@ (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"); } @@ -431,8 +461,40 @@ @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 @@ -465,12 +527,24 @@ 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); } } @@ -484,24 +558,72 @@ } } + 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> 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 bufferedFragments = new TreeSet<>(); - HashMap> 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; @@ -510,65 +632,66 @@ 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; @@ -578,7 +701,8 @@ fragmented = true; } - List holes = holesMap.get(hsf.handshakeType); + List holes = + handshakeFlight.holesMap.get(hsf.handshakeType); if (holes == null) { if (!fragmented) { holes = Collections.emptyList(); @@ -586,7 +710,7 @@ holes = new LinkedList(); 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. @@ -594,20 +718,11 @@ // 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(); - holes.add(new HoleDescriptor(0, hsf.messageLength)); - } - holesMap.put(hsf.handshakeType, holes); + return; } if (fragmented) { @@ -628,9 +743,7 @@ (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"); } @@ -659,38 +772,59 @@ } } - // append this fragment - bufferedFragments.add(hsf); + // 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); @@ -697,10 +831,146 @@ if (flightIsReady) { flightIsReady = false; } - needToCheckFlight = true; + + 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 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())); @@ -708,12 +978,9 @@ 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; } @@ -721,10 +988,23 @@ // 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; @@ -731,19 +1011,82 @@ } 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(); @@ -750,6 +1093,9 @@ 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 } @@ -757,6 +1103,10 @@ if (flightIsReady) { flightIsReady = false; } + + if (debug != null && Debug.isOn("verbose")) { + Debug.log("Not yet ready to decrypt the cached fragments."); + } return null; } @@ -768,9 +1118,8 @@ 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] @@ -782,7 +1131,6 @@ // 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, @@ -792,66 +1140,31 @@ 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) { - flightIsReady = false; - } - queueUpHandshake(hsFrag); + 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; } } - 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(); - } + return acquirePlaintext(); } else { return new Plaintext(rFrag.contentType, rFrag.majorVersion, rFrag.minorVersion, - rFrag.recordEpoch, rFrag.recordSeq, + rFrag.recordEpoch, + Authenticator.toLong(rFrag.recordEnS), plaintextFragment); } } @@ -861,10 +1174,15 @@ 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(); @@ -871,7 +1189,8 @@ 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; @@ -882,13 +1201,13 @@ // 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 @@ -913,7 +1232,8 @@ 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 @@ -957,7 +1277,6 @@ handshakeHashing(hsFrag, plaintext); this.nextRecordSeq = maxRecodeSN + 1; - this.nextMessageSeq = msgSeq + 1; return plaintext; } @@ -966,15 +1285,26 @@ boolean flightIsReady() { - // - // the ChangeCipherSpec/Finished flight - // - if (expectCCSFlight) { - // Have the ChangeCipherSpec/Finished messages been received? - return hasFinisedMessage(bufferedFragments); - } + byte flightType = handshakeFlight.handshakeType; + if (flightType == HandshakeFlight.HF_UNKNOWN) { + // + // the ChangeCipherSpec/Finished flight + // + if (expectCCSFlight) { + // 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; } @@ -983,7 +1313,12 @@ (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; } // @@ -991,7 +1326,12 @@ // 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; } @@ -998,24 +1338,40 @@ // // 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 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; } // @@ -1029,92 +1385,65 @@ (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"); + } + + return false; + } } - // - // 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. + 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"); + } + 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 ClientKeyExchange flight (message " + + handshakeFlight.minMessageSeq + "-" + + handshakeFlight.maxMessageSeq + + ") completed? " + isReady); + } + + return isReady; } // // 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"); + if (debug != null && Debug.isOn("verbose")) { + Debug.log("Need to receive more handshake messages"); } - int sidLen = fragment[34]; // 34: the length field - if (sidLen > 32) { // opaque SessionID<0..32> - throw new SSLException( - "Invalid ServerHello message: invalid session id"); - } - - if (fragment.length < 38 + sidLen) { - throw new SSLException( - "Invalid ServerHello message: no sufficient data"); - } - - 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 @@ -1122,8 +1451,7 @@ // 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 fragments) { + private boolean hasFinishedMessage(Set fragments) { boolean hasCCS = false; boolean hasFin = false; @@ -1147,7 +1475,35 @@ return hasFin && hasCCS; } - private boolean hasCompleted(List 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 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 holes = + handshakeFlight.holesMap.get(handshakeType); if (holes == null) { // not yet received this kind of handshake message return false; @@ -1173,7 +1529,7 @@ 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; }