Skip to content

Commit 3d80f72

Browse files
committed
Derive the retained handshake epoch and document the DTLS 1.3 epoch retention, relates to github #1468.
1 parent a1edf1a commit 3d80f72

5 files changed

Lines changed: 76 additions & 19 deletions

File tree

‎tls/src/main/java/org/bouncycastle/tls/DTLSRecordLayer.java‎

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,22 @@ private static void sendDatagram(DatagramSender sender, byte[] buf, int off, int
141141
* plaintext at epoch 0 while the rest of the same flight is protected at the handshake epoch - so
142142
* answering a retransmission of it means being able to read the plaintext epoch too. Handshake records
143143
* only, exactly as for retransmitEpoch.
144+
*
145+
* Epoch 0 is unauthenticated, so anyone able to put a datagram on the path can forge a record at it, and
146+
* retaining it for reading would otherwise be a way to draw an answer out of a completed client. It is
147+
* harmless because of three separate gates, all of which have to hold for it to stay harmless:
148+
*
149+
* 1. DTLSReliableHandshake.processRecord's DTLS 1.3 'expectedEpoch' check (RFC 9147 6.1). At epoch 0
150+
* only a client_hello or server_hello msg_type is accepted, so a forgery cannot stand in for anything
151+
* later in the flight; on the client, which is the only side that retains epoch 0, that leaves
152+
* server_hello fragments.
153+
* 2. DTLSReliableHandshake.processRecord only answers a retransmission once
154+
* checkAll(previousInboundFlight) finds that flight complete, and the rest of that flight is
155+
* protected at the handshake epoch, which cannot be forged. A plaintext-only forgery therefore never
156+
* completes a flight and never draws a retransmission.
157+
* 3. Post-handshake, processRecord is called with a 'windowSize' of 0 (see the retransmit callback in
158+
* DTLSReliableHandshake.finish), so every message_seq is 'too far ahead' and no DTLSReassembler is
159+
* ever allocated: the forgery cannot make us buffer anything either.
144160
*/
145161
private DTLSEpoch retransmitEpochPlaintext = null;
146162
private Timeout retransmitTimeout = null;
@@ -345,12 +361,37 @@ private void commitPendingEpochIfCurrent()
345361
{
346362
if (readEpoch == pendingEpoch && writeEpoch == pendingEpoch)
347363
{
348-
this.retiredEpoch = currentEpoch;
364+
/*
365+
* DTLS 1.2 never reads 'retiredEpoch': handshakeSuccessful retains 'currentEpoch' there, because
366+
* in DTLS 1.2 the epoch being superseded is still the current one at that point. Assigning it
367+
* only for DTLS 1.3 keeps the DTLS 1.2 path's state untouched by this field.
368+
*
369+
* TODO[dtls13] This holds only the MOST RECENT retired epoch, which is all RFC 9147 5.8.1 needs
370+
* while the only epoch change after the handshake epoch is the one to the application epoch. Once
371+
* post-handshake key update (RFC 9147 8) lands, several epochs can be retired in succession and
372+
* which of them is still readable becomes load-bearing.
373+
*/
374+
if (dtls13)
375+
{
376+
this.retiredEpoch = currentEpoch;
377+
}
378+
349379
this.currentEpoch = pendingEpoch;
350380
this.pendingEpoch = null;
351381
}
352382
}
353383

384+
/**
385+
* RFC 9147 5.8.1. DTLS 1.3 only: the epoch that {@link #handshakeSuccessful(DTLSHandshakeRetransmit)} is
386+
* about to retain for reading, i.e. the handshake epoch the peer's final flight was protected under, or
387+
* -1 if no epoch has been retired. Read by {@link DTLSReliableHandshake#finish()}, so that the epoch a
388+
* retransmission of that flight has to arrive at is taken from the record layer rather than assumed.
389+
*/
390+
int getRetiredEpoch()
391+
{
392+
return (null != retiredEpoch) ? retiredEpoch.getEpoch() : -1;
393+
}
394+
354395
void handshakeSuccessful(DTLSHandshakeRetransmit retransmit)
355396
{
356397
if (!dtls13 && (readEpoch == currentEpoch || writeEpoch == currentEpoch))

‎tls/src/main/java/org/bouncycastle/tls/DTLSReliableHandshake.java‎

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,13 @@ void finish()
408408
final Vector finalFlightAck = ackRecordNumbers;
409409
final Hashtable finalFlight = summarizeFlight(currentInboundFlight);
410410

411+
/*
412+
* RFC 9147 5.8.1. The epoch that flight was protected under, taken from the record layer -
413+
* which is about to retain exactly that epoch for reading - rather than assumed to be the
414+
* handshake epoch's current number.
415+
*/
416+
final int finalFlightEpoch = recordLayer.getRetiredEpoch();
417+
411418
sendPendingAck();
412419

413420
/*
@@ -426,7 +433,7 @@ void finish()
426433
public void receivedHandshakeRecord(int epoch, byte[] buf, int off, int len)
427434
throws IOException
428435
{
429-
if (matchesFlight(finalFlight, epoch, buf, off, len))
436+
if (matchesFlight(finalFlight, finalFlightEpoch, epoch, buf, off, len))
430437
{
431438
sendAck(finalFlightAck);
432439
}
@@ -508,13 +515,15 @@ static Hashtable summarizeFlight(Hashtable inboundFlight)
508515
* that retains epoch 0 for reading.
509516
* </p>
510517
*/
511-
static boolean matchesFlight(Hashtable flight, int epoch, byte[] buf, int off, int len)
518+
static boolean matchesFlight(Hashtable flight, int flightEpoch, int epoch, byte[] buf, int off, int len)
512519
{
513520
/*
514-
* RFC 9147 6.1. The flight this answers is the peer's final flight, every message of which is
515-
* protected under the handshake traffic keys at epoch 2.
521+
* RFC 9147 6.1. The flight this answers is the peer's final flight, and 'flightEpoch' is the epoch it
522+
* was protected under - the handshake epoch, which the record layer has retained for reading. A
523+
* record at any other epoch carries none of it, the unauthenticated epoch 0 included; a flightEpoch
524+
* of -1 means no epoch was retired, so there is nothing to recognise.
516525
*/
517-
if (2 != epoch || len < MESSAGE_HEADER_LENGTH)
526+
if (flightEpoch < 0 || flightEpoch != epoch || len < MESSAGE_HEADER_LENGTH)
518527
{
519528
return false;
520529
}

‎tls/src/main/java/org/bouncycastle/tls/DTLSServerProtocol.java‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,7 @@ protected byte[] send13ServerHelloCoda(ServerHandshakeState state, DTLSReliableH
570570
recordLayer.enablePendingEpochRead();
571571

572572
handshake.sendMessage(HandshakeType.encrypted_extensions,
573-
generate13EncryptedExtensions(state.serverExtensions));
573+
generate13EncryptedExtensions(state, state.serverExtensions));
574574

575575
if (state.selectedPSK13)
576576
{
@@ -1030,7 +1030,7 @@ protected void processClientHelloRetry(ServerHandshakeState state, byte[] body)
10301030
/**
10311031
* Mirrors TlsServerProtocol.send13EncryptedExtensionsMessage.
10321032
*/
1033-
protected byte[] generate13EncryptedExtensions(Hashtable serverExtensions)
1033+
protected byte[] generate13EncryptedExtensions(ServerHandshakeState state, Hashtable serverExtensions)
10341034
throws IOException
10351035
{
10361036
byte[] extBytes = TlsProtocol.writeExtensionsData(serverExtensions);

‎tls/src/main/java/org/bouncycastle/tls/ServerHello.java‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,13 @@ public class ServerHello
2222
private final int cipherSuite;
2323
private final Hashtable extensions;
2424

25+
/**
26+
* A TLS 1.3 HelloRetryRequest, i.e. the 4-argument constructor with the 0x0303 'legacy_version' RFC 8446
27+
* 4.1.3 gives it.
28+
*/
2529
public ServerHello(byte[] sessionID, int cipherSuite, Hashtable extensions)
2630
{
27-
this(ProtocolVersion.TLSv12, Arrays.clone(HELLO_RETRY_REQUEST_MAGIC), sessionID, cipherSuite, extensions);
31+
this(ProtocolVersion.TLSv12, sessionID, cipherSuite, extensions);
2832
}
2933

3034
/**

‎tls/src/test/java/org/bouncycastle/tls/DTLS13RetransmissionTest.java‎

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -182,13 +182,13 @@ public void testRetransmissionOfTheFinalFlightIsRecognised()
182182
FINISHED_LENGTH);
183183

184184
assertTrue("a retransmission of the final flight must be answered",
185-
DTLSReliableHandshake.matchesFlight(flight, 2, record, 0, record.length));
185+
DTLSReliableHandshake.matchesFlight(flight, 2, 2, record, 0, record.length));
186186

187187
byte[] firstHalf = handshakeRecord(HandshakeType.finished, FINISHED_LENGTH, FINAL_FLIGHT_SEQ, 0,
188188
FINISHED_LENGTH / 2);
189189

190190
assertTrue("a fragment of the final flight must be answered",
191-
DTLSReliableHandshake.matchesFlight(flight, 2, firstHalf, 0, firstHalf.length));
191+
DTLSReliableHandshake.matchesFlight(flight, 2, 2, firstHalf, 0, firstHalf.length));
192192
}
193193

194194
public void testUnauthenticatedEpochIsNotTakenForTheFinalFlight()
@@ -199,9 +199,12 @@ public void testUnauthenticatedEpochIsNotTakenForTheFinalFlight()
199199
FINISHED_LENGTH);
200200

201201
assertFalse("epoch 0 is unauthenticated and carries none of the final flight",
202-
DTLSReliableHandshake.matchesFlight(flight, 0, record, 0, record.length));
202+
DTLSReliableHandshake.matchesFlight(flight, 2, 0, record, 0, record.length));
203203
assertFalse("the application epoch carries no handshake flight",
204-
DTLSReliableHandshake.matchesFlight(flight, 3, record, 0, record.length));
204+
DTLSReliableHandshake.matchesFlight(flight, 2, 3, record, 0, record.length));
205+
206+
assertFalse("with no epoch retired there is nothing to recognise",
207+
DTLSReliableHandshake.matchesFlight(flight, -1, -1, record, 0, record.length));
205208
}
206209

207210
public void testForeignHandshakeRecordIsNotTakenForTheFinalFlight()
@@ -211,30 +214,30 @@ public void testForeignHandshakeRecordIsNotTakenForTheFinalFlight()
211214
byte[] otherSeq = handshakeRecord(HandshakeType.finished, FINISHED_LENGTH, FINAL_FLIGHT_SEQ + 1, 0,
212215
FINISHED_LENGTH);
213216
assertFalse("a message_seq the flight never contained",
214-
DTLSReliableHandshake.matchesFlight(flight, 2, otherSeq, 0, otherSeq.length));
217+
DTLSReliableHandshake.matchesFlight(flight, 2, 2, otherSeq, 0, otherSeq.length));
215218

216219
byte[] otherType = handshakeRecord(HandshakeType.key_update, FINISHED_LENGTH, FINAL_FLIGHT_SEQ, 0,
217220
FINISHED_LENGTH);
218221
assertFalse("a different message under the flight's message_seq",
219-
DTLSReliableHandshake.matchesFlight(flight, 2, otherType, 0, otherType.length));
222+
DTLSReliableHandshake.matchesFlight(flight, 2, 2, otherType, 0, otherType.length));
220223

221224
byte[] otherLength = handshakeRecord(HandshakeType.finished, FINISHED_LENGTH + 1, FINAL_FLIGHT_SEQ, 0,
222225
FINISHED_LENGTH + 1);
223226
assertFalse("a different length under the flight's message_seq",
224-
DTLSReliableHandshake.matchesFlight(flight, 2, otherLength, 0, otherLength.length));
227+
DTLSReliableHandshake.matchesFlight(flight, 2, 2, otherLength, 0, otherLength.length));
225228

226229
byte[] record = handshakeRecord(HandshakeType.finished, FINISHED_LENGTH, FINAL_FLIGHT_SEQ, 0,
227230
FINISHED_LENGTH);
228231
assertFalse("a truncated record",
229-
DTLSReliableHandshake.matchesFlight(flight, 2, record, 0, record.length - 1));
232+
DTLSReliableHandshake.matchesFlight(flight, 2, 2, record, 0, record.length - 1));
230233
assertFalse("a record too short to hold a message header",
231-
DTLSReliableHandshake.matchesFlight(flight, 2, record, 0, 4));
234+
DTLSReliableHandshake.matchesFlight(flight, 2, 2, record, 0, 4));
232235

233236
byte[] pair = new byte[record.length + otherSeq.length];
234237
System.arraycopy(record, 0, pair, 0, record.length);
235238
System.arraycopy(otherSeq, 0, pair, record.length, otherSeq.length);
236239
assertFalse("one fragment of the flight does not excuse a foreign one beside it",
237-
DTLSReliableHandshake.matchesFlight(flight, 2, pair, 0, pair.length));
240+
DTLSReliableHandshake.matchesFlight(flight, 2, 2, pair, 0, pair.length));
238241
}
239242

240243
public void testUnacknowledgedFlightStillRetransmitsEverything() throws Exception

0 commit comments

Comments
 (0)