Skip to content

Commit 9d43a57

Browse files
committed
State the peer-keyed nonce reuse precisely and document the lock order, relates to github #1468.
The scoped re-review of the previous commit found that its corrected comment was still false in one reachable state, and that state is the one the new test creates. deriveNextWriteEpoch advances the local traffic secret when a KeyUpdate is sent, while installPendingWriteEpoch swaps the epoch in only when the acknowledgement arrives. So a peer read epoch derived while our own KeyUpdate is outstanding carries an encrypt side keyed like the PENDING write epoch, not the current one. The conclusion is unchanged, since the key is live either way and both sequence counters start at zero, but the sentence a maintainer would reason from was wrong, and checking it against the outstanding-KeyUpdate case would have suggested the guard was over-cautious. The lock order between DTLS13PostHandshake's monitor and the record layer's write lock was held by construction and documented nowhere. It is now stated at both ends: the monitor is taken first, and a call into that class from inside a write-lock block would invert the order. Two test comments named mutations that no longer do what they claim. The epoch ordering moved out of the test-facing view into getLiveReadEpoch, so reversing the view proves nothing; and relaxing only the first of the two guards on a second KeyUpdate fails on the exception raised by the second, not on the datagram count. The acknowledgement probe's settle period had been trimmed along with the key update deadlines it does not share. Its claim is a count of records that did not arrive, so the margin is restored under its own name.
1 parent 3cc8c3f commit 9d43a57

6 files changed

Lines changed: 64 additions & 25 deletions

File tree

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
* reducing to "waiting for an ACK and retransmitting the original message". The receiving side, implemented
2323
* here, is common to all of them: reassemble, dispatch by message type, acknowledge the record.
2424
* </p>
25+
* <p>
26+
* Thread safety: a send thread reaches this class through {@code checkKeyUpdateBeforeSend} while a receive
27+
* thread reaches it through the ACK listener, the record dispatch and the timers, so every non-private method
28+
* is synchronized on this object. LOCK ORDER: this monitor is taken FIRST and the record layer's write lock
29+
* second, because the work done here calls back into the record layer. A call into this class from inside a
30+
* {@code synchronized (writeLock)} block would take the two in the opposite order and can deadlock; see the
31+
* note on that field.
32+
* </p>
2533
*/
2634
class DTLS13PostHandshake
2735
implements DTLSAckListener
@@ -584,7 +592,13 @@ private void handleMessage(short msg_type, byte[] body)
584592
}
585593
}
586594

587-
/** RFC 9147 7.2. The outstanding post-handshake messages awaiting acknowledgement. */
595+
/**
596+
* RFC 9147 7.2. The outstanding post-handshake messages awaiting acknowledgement.
597+
* <p>
598+
* Synchronized only so the reference is published safely. It confers nothing on the caller, who reads and
599+
* mutates the tracker outside this monitor. Test-only; there is no production caller.
600+
* </p>
601+
*/
588602
synchronized DTLS13FlightTracker getFlightTracker()
589603
{
590604
return flightTracker;

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

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@ class DTLSEpoch
2020
* for BOTH directions: TlsAEADCipher's (D)TLS 1.3 constructor calls rekeyCipher once for the decrypt side,
2121
* keyed from the peer's traffic secret, and once for the encrypt side, keyed from the LOCAL one.
2222
* updatePeerReadEpoch updates only the peer's secret (TlsUtils.update13TrafficSecretPeer), so the epoch it
23-
* builds carries an encrypt side keyed IDENTICALLY to the current write epoch's, paired with a sequence
23+
* builds carries an encrypt side keyed from the LOCAL secret's current value, paired with a sequence
2424
* number counter of its own that starts at zero.
2525
*
26+
* Which epoch that key belongs to depends on when the derivation happens. Normally it is the current write
27+
* epoch's key. If a KeyUpdate of ours is already outstanding it is the PENDING write epoch's, because
28+
* deriveNextWriteEpoch advances the local secret when the KeyUpdate is sent and installPendingWriteEpoch
29+
* only swaps the epoch in when the ACK arrives. Either way it is a key that is live, or about to be.
30+
*
2631
* So allocating a record from this epoch would not produce something the peer cannot read. It would
27-
* produce records encrypted under the SAME AEAD key, at nonces already used for records sent at the
28-
* current write epoch. That is AEAD nonce reuse: silent, with the connection still working, and for GCM it
29-
* is enough to recover the authentication key. It is the worst outcome an epoch lookup can have, and
30-
* nothing observable would report it.
32+
* produce records encrypted under the SAME AEAD key as another epoch, at nonces that epoch either has
33+
* already used or will use, because both counters start at zero. That is AEAD nonce reuse: silent, with
34+
* the connection still working, and for GCM it is enough to recover the authentication key. It is the
35+
* worst outcome an epoch lookup can have, and nothing observable would report it.
3136
*
3237
* The structural cause, which is why this flag has to exist at all: updatePeerReadEpoch builds a full
3338
* bidirectional cipher when it needs only the read direction, and so leaves a correctly-keyed encryptor in

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

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,15 @@ private static void sendDatagram(DatagramSender sender, byte[] buf, int off, int
127127
private final DatagramTransport transport;
128128

129129
private final ByteQueue recordQueue = new ByteQueue();
130+
131+
/*
132+
* Guards the write path, which a send and a receive thread may reach at once.
133+
*
134+
* LOCK ORDER: every DTLS13PostHandshake method is synchronized on that object, and its work reaches back
135+
* into this class and takes writeLock. So the only permitted order is the post-handshake monitor first,
136+
* writeLock second. Never call into postHandshake from inside a synchronized (writeLock) block: that is
137+
* the reverse order and two threads taking the two orders at once would deadlock.
138+
*/
130139
private final Object writeLock = new Object();
131140

132141
// github #1487. While a flight is open, records are packed into as few datagrams as the MTU allows.
@@ -2269,11 +2278,12 @@ private DTLSEpoch getEpochForRetransmit(int epoch)
22692278
* (DTLSEpoch.isPeerKeyed, set only by updatePeerReadEpoch) may be read at and must never be written
22702279
* at. Its encrypt side is NOT the peer's - TlsUtils.initCipher keys both directions, the decrypt side
22712280
* from the peer's traffic secret and the encrypt side from the local one, and updatePeerReadEpoch
2272-
* updates only the peer's secret - so that epoch's encryptor is keyed identically to the current write
2273-
* epoch's, while its sequence number counter starts again at zero. Writing at it would therefore put
2274-
* records on the wire under the SAME AEAD key at nonces the current write epoch has already used, and
2275-
* a peer would read them perfectly well. See DTLSEpoch.peerKeyed for why that is worse than a
2276-
* decryption failure would be.
2281+
* updates only the peer's secret - so that epoch's encryptor is keyed from the local secret as it
2282+
* stands, which is the current write epoch's key, or the pending write epoch's if a KeyUpdate of ours
2283+
* is already outstanding. Its sequence number counter starts again at zero either way. Writing at it
2284+
* would therefore put records on the wire under the SAME AEAD key as another epoch, at nonces that
2285+
* epoch has used or will use, and a peer would read them perfectly well. See DTLSEpoch.peerKeyed for
2286+
* why that is worse than a decryption failure would be.
22772287
*
22782288
* Before post-handshake key updates every held epoch was one both directions shared, so this could not
22792289
* arise; once the read side advances on its own it can, and the epoch numbers of the two directions

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -816,10 +816,12 @@ public void testASecondKeyUpdateIsNotStartedWhileOneIsOutstanding() throws Excep
816816
* </p>
817817
* <p>
818818
* Mutations this test is built to catch: remove the outstanding-KeyUpdate latch at the top of
819-
* {@code checkKeyUpdateBeforeSend} - i.e. follow RFC 8446 instead - and the deferral half fails, because
820-
* a second KeyUpdate goes out while one is unacknowledged; drop the {@code keyUpdatePendingSend} latch
821-
* (clear it in {@code handleMessage} instead of recording it) and the discharge half fails, because the
822-
* answering KeyUpdate is never sent at all.
819+
* {@code checkKeyUpdateBeforeSend} - i.e. try to follow RFC 8446 instead - and the deferral half fails on
820+
* the {@code IllegalStateException} raised by {@code sendKeyUpdate}'s own RFC 9147 5.8.4 guard, which is
821+
* the second of the two. Relax both, so that the implementation conforms to RFC 8446, and it fails
822+
* instead on the datagram count, because a second KeyUpdate goes out while one is unacknowledged. Drop
823+
* the {@code keyUpdatePendingSend} latch (clear it in {@code handleMessage} instead of recording it) and
824+
* the discharge half fails, because the answering KeyUpdate is never sent at all.
823825
* </p>
824826
*/
825827
public void testAnUpdateRequestedIsDeferredWhileOurOwnKeyUpdateIsOutstanding() throws Exception

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

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import junit.framework.TestCase;
1111

1212
/**
13-
* RFC 9147 4.2.2 and 8. The record layer resolves a received record's epoch from one ordered collection of
14-
* live read epochs, iterated most recent first, and writes at an epoch through the same collection.
13+
* RFC 9147 4.2.2 and 8. The record layer resolves a received record's epoch from one ordered set of live read
14+
* epochs, walked most recent first by {@code getLiveReadEpoch(int)}, and writes at an epoch through the same
15+
* walk. {@code getLiveReadEpochs()} is a view over that walk for tests; the record path does not use it.
1516
* <p>
1617
* The load-bearing test here is {@link #testAliasingEpochResolvesToTheMostRecentMatch()}: only the low 2
1718
* epoch bits are on the wire, so two live epochs can alias, and the RFC resolves that to the most recent of
@@ -119,9 +120,10 @@ public void testLiveReadEpochsAreOrderedMostRecentFirst() throws Exception
119120
* bits that are on the wire, and both are live and able to decode the record, so the only thing that
120121
* decides which one the record is attributed to is the order the collection is iterated in.
121122
* <p>
122-
* Mutation this test is built to catch: reverse the iteration in {@code getLiveReadEpochs} (or in its
123-
* consumer in {@code processDTLS13Record}) and the record resolves to epoch 2 instead, failing every
124-
* assertion below.
123+
* Mutation this test is built to catch: reverse the slot order in {@code getLiveReadEpoch(int)} (or in
124+
* its consumer {@code resolveReadEpochByHeaderBits}) and the record resolves to epoch 2 instead, failing
125+
* every assertion below. Reversing {@code getLiveReadEpochs()} alone would not, since that is only the
126+
* test-facing view.
125127
* </p>
126128
*/
127129
public void testAliasingEpochResolvesToTheMostRecentMatch() throws Exception

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,8 +142,14 @@ public class DTLS13ProtocolTest
142142
* that. The settle period is spent on every run, and is what lets a peer's answer to the last record
143143
* arrive, so it has to stay long enough to be reliable and short enough not to dominate the suite.
144144
*/
145-
private static final int KEY_UPDATE_DEADLINE_MILLIS = 8000;
146-
private static final int KEY_UPDATE_SETTLE_MILLIS = 1000;
145+
private static final int POST_HANDSHAKE_DEADLINE_MILLIS = 8000;
146+
private static final int POST_HANDSHAKE_SETTLE_MILLIS = 1000;
147+
148+
/*
149+
* The ACK probe waits longer before concluding that nothing more is coming, because its whole claim is a
150+
* count of records that did NOT arrive. Trimming this trades a real margin for no measurable runtime.
151+
*/
152+
private static final int ACK_PROBE_SETTLE_MILLIS = 1500;
147153

148154
/** The application payloads of the exchanges after the handshake, distinct in both length and content. */
149155
private static final byte[] REQUEST_2 = new byte[24];
@@ -2663,8 +2669,8 @@ private void runKeyUpdate(DTLSTransport dtlsClient, ServerThread serverThread) t
26632669
*/
26642670
int target = clientEpoch3Before + (dropKeyUpdateDatagram ? 2 : 1);
26652671

2666-
pumpUntilClientRecordsAtEpoch(dtlsClient, 3, target, KEY_UPDATE_DEADLINE_MILLIS,
2667-
KEY_UPDATE_SETTLE_MILLIS, earlier);
2672+
pumpUntilClientRecordsAtEpoch(dtlsClient, 3, target, POST_HANDSHAKE_DEADLINE_MILLIS,
2673+
POST_HANDSHAKE_SETTLE_MILLIS, earlier);
26682674

26692675
this.clientKeyUpdateCopies = countRecordsAtEpoch(clientRecords(), 3) - clientEpoch3Before;
26702676

@@ -2817,7 +2823,7 @@ private void probeServerAckPath(DTLSTransport dtlsClient) throws IOException
28172823
int expected = holdFirstClientEpoch2Datagram ? 2 : 1;
28182824

28192825
this.serverEpoch3AfterHandshake = drainServerEpoch3(dtlsClient, expected,
2820-
KEY_UPDATE_DEADLINE_MILLIS, KEY_UPDATE_SETTLE_MILLIS);
2826+
POST_HANDSHAKE_DEADLINE_MILLIS, ACK_PROBE_SETTLE_MILLIS);
28212827

28222828
injectPlaintextHandshakeRecord();
28232829

0 commit comments

Comments
 (0)