Skip to content

Commit 7947c73

Browse files
committed
Assert the server's own refusal and the second ClientHello's message_seq, relates to github #1468.
The re-review of the previous fixes found two comments claiming more than the code does and two test assertions weaker than their messages. The notification comment said the call was placed exactly as TlsServerProtocol does it. The version gating is mirrored; the ordering relative to the other TlsServer callbacks is not, and cannot be, because DTLS selects the version in generateServerHello, by which point establishClientSigAlgs and processClientExtensions have already run. Say both, rather than claiming a wholesale mirror. DTLSVerifier produces a DTLSRequest for any ClientHello carrying a cookie it has verified, so it need not have sent the HelloVerifyRequest during that call. The invariant is that the ClientHello arrived through the cookie exchange. The server-refusal test asserted the internal_error the client receives, which is not specific to this cause, so the harness now records what the server itself threw and the test asserts that diagnostic. The harness also stops printing a stack trace for a server abort a test expects, which otherwise makes a passing test look like a failing one. Counting datagrams that carry a ClientHello cannot tell a new message from a retransmission of the first, so the scripted transport records each ClientHello's message_seq and the test requires it to advance to 1, per RFC 6347 4.2.2.
1 parent 170e6a0 commit 7947c73

3 files changed

Lines changed: 84 additions & 11 deletions

File tree

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

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -134,8 +134,9 @@ protected DTLSTransport serverHandshake(ServerHandshakeState state, DTLSRequest
134134
{
135135
/*
136136
* Reached through accept(TlsServer, DatagramTransport, DTLSRequest), i.e. behind DTLSVerifier,
137-
* which has already sent a DTLS 1.2 HelloVerifyRequest on this connection - see the check in
138-
* generateServerHello.
137+
* so this ClientHello arrived through the DTLS 1.2 cookie exchange: DTLSVerifier only produces a
138+
* DTLSRequest for a ClientHello carrying a cookie it has verified, though it need not have sent
139+
* the HelloVerifyRequest itself during this call - see the check in generateServerHello.
139140
*/
140141
state.afterHelloVerifyRequest = true;
141142

@@ -1677,12 +1678,18 @@ protected void processClientHello(ServerHandshakeState state, ClientHello client
16771678

16781679
/*
16791680
* NOTE: server.notifySecureRenegotiation is called from generateServerHello, in the
1680-
* DTLS-1.2-and-below portion past the point where the DTLS 1.3 path has returned, exactly as
1681-
* TlsServerProtocol.generateServerHello does it. RFC 8446 / RFC 9147 remove renegotiation, so a
1682-
* client offering only DTLS 1.3 (or later) legitimately sends neither the "renegotiation_info"
1683-
* extension nor the SCSV - see the matching 'offeringDTLSv12Minus' gate in
1684-
* DTLSClientProtocol.generateClientHello - and gating on the selected version rather than on the
1685-
* client's offer is what keeps a {1.3, 1.2} offer with neither of them acceptable.
1681+
* DTLS-1.2-and-below portion past the point where the DTLS 1.3 path has returned. That mirrors the
1682+
* version gating of TlsServerProtocol.generateServerHello, which likewise notifies only once a
1683+
* version at or below 1.2 has been selected. It does NOT mirror its ordering relative to the other
1684+
* TlsServer callbacks: DTLS selects the version in generateServerHello, so by the time the
1685+
* notification is made here establishClientSigAlgs and server.processClientExtensions have already
1686+
* run from processClientHello, whereas TlsServerProtocol notifies before both.
1687+
*
1688+
* RFC 8446 / RFC 9147 remove renegotiation, so a client offering only DTLS 1.3 (or later)
1689+
* legitimately sends neither the "renegotiation_info" extension nor the SCSV - see the matching
1690+
* 'offeringDTLSv12Minus' gate in DTLSClientProtocol.generateClientHello - and gating on the selected
1691+
* version rather than on the client's offer is what keeps a {1.3, 1.2} offer with neither of them
1692+
* acceptable.
16861693
*/
16871694

16881695
if (clientHelloExtensions != null)

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,15 @@ public void testClientRefusesDTLSv13SelectedAfterAHelloVerifyRequest() throws Ex
9797

9898
assertEquals("the client must have answered the HelloVerifyRequest", 2,
9999
transport.clientHellosSeen());
100+
101+
/*
102+
* A retransmission of the first ClientHello would also be a second datagram carrying a ClientHello,
103+
* so check the message_seq advanced: RFC 6347 4.2.2 requires the ClientHello answering a
104+
* HelloVerifyRequest to be a new message, at message_seq 1.
105+
*/
106+
assertEquals("the first ClientHello is at message_seq 0", 0, transport.clientHelloSeq(0));
107+
assertEquals("the second ClientHello must be a new message, not a retransmission", 1,
108+
transport.clientHelloSeq(1));
100109
}
101110

102111
private short connectAndExpectFatalAlert(byte[] selectedVersion) throws Exception
@@ -250,6 +259,8 @@ private static class ScriptedServerHelloTransport
250259
{
251260
private final byte[][] script;
252261

262+
private final int[] clientHelloSeqs = new int[8];
263+
253264
private int clientHellosSeen = 0;
254265
private byte[] pending = null;
255266

@@ -268,6 +279,16 @@ int clientHellosSeen()
268279
return clientHellosSeen;
269280
}
270281

282+
/**
283+
* The DTLS handshake message_seq of the ClientHello at the given index, so that a retransmission of
284+
* an earlier ClientHello can be told apart from a genuinely new one - both are datagrams carrying a
285+
* ClientHello, and only the message_seq distinguishes them.
286+
*/
287+
int clientHelloSeq(int index)
288+
{
289+
return clientHelloSeqs[index];
290+
}
291+
271292
public int getReceiveLimit()
272293
{
273294
return MTU;
@@ -284,6 +305,13 @@ public void send(byte[] buf, int off, int len) throws IOException
284305
if (clientHellosSeen < script.length && len > 13 && (buf[off] & 0xFF) == ContentType.handshake
285306
&& (buf[off + 13] & 0xFF) == HandshakeType.client_hello)
286307
{
308+
// Handshake message header: msg_type, length, then message_seq at offsets 4 and 5
309+
if (clientHellosSeen < clientHelloSeqs.length)
310+
{
311+
clientHelloSeqs[clientHellosSeen] = ((buf[off + 17] & 0xFF) << 8)
312+
| (buf[off + 18] & 0xFF);
313+
}
314+
287315
this.pending = script[clientHellosSeen++];
288316
}
289317
}

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

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -786,6 +786,7 @@ public void testServerRefusesToSelectDTLSv13BehindAHelloVerifyRequest() throws E
786786
{
787787
Harness harness = new Harness();
788788
harness.helloVerifyRequestFrontEnd = true;
789+
harness.expectServerAbort = true;
789790
harness.clientVersions = ProtocolVersion.DTLSv13.downTo(ProtocolVersion.DTLSv12);
790791
harness.serverVersions = ProtocolVersion.DTLSv13.downTo(ProtocolVersion.DTLSv12);
791792

@@ -809,6 +810,16 @@ public void testServerRefusesToSelectDTLSv13BehindAHelloVerifyRequest() throws E
809810
assertEquals("the version the server refused to go on with", ProtocolVersion.DTLSv13,
810811
harness.serverVersion);
811812

813+
/*
814+
* The client only ever sees the resulting alert, and internal_error is not specific to this cause.
815+
* Assert on what the SERVER threw, so the test cannot pass because something else on the server went
816+
* wrong at the same point.
817+
*/
818+
assertNotNull("the server must be the side that aborted", harness.serverAbort);
819+
assertEquals("the server's own diagnostic",
820+
"internal_error(80); DTLS 1.3 cannot be negotiated behind a HelloVerifyRequest front end",
821+
harness.serverAbort.getMessage());
822+
812823
assertEquals("no application data can have been exchanged", null, harness.echo);
813824
}
814825

@@ -1750,6 +1761,9 @@ static class Harness
17501761
boolean replaySecondHelloRetryRequest = false;
17511762
int mangleSecondClientHello = MANGLE_NONE;
17521763
boolean helloVerifyRequestFrontEnd = false;
1764+
boolean expectServerAbort = false;
1765+
1766+
Exception serverAbort = null;
17531767

17541768
/*
17551769
* RFC 5746 3.4. Strips the TLS_EMPTY_RENEGOTIATION_INFO_SCSV from the ClientHello on its way out, so
@@ -2194,7 +2208,7 @@ public void notifyHandshakeComplete() throws IOException
21942208
DTLSServerProtocol serverProtocol = new DTLSServerProtocol();
21952209

21962210
ServerThread serverThread = new ServerThread(serverProtocol, server, network.getServer(),
2197-
helloVerifyRequestFrontEnd);
2211+
helloVerifyRequestFrontEnd, expectServerAbort);
21982212
serverThread.setDaemon(true);
21992213
serverThread.start();
22002214

@@ -2257,6 +2271,8 @@ public void notifyHandshakeComplete() throws IOException
22572271
this.mangled = recording.getMangled();
22582272

22592273
serverThread.shutdown();
2274+
2275+
this.serverAbort = serverThread.getCaught();
22602276
}
22612277
}
22622278

@@ -2414,15 +2430,28 @@ static class ServerThread
24142430
private final TlsServer server;
24152431
private final DatagramTransport serverTransport;
24162432
private final boolean helloVerifyRequestFrontEnd;
2433+
private final boolean expectAbort;
24172434
private volatile boolean isShutdown = false;
2435+
private volatile Exception caught = null;
24182436

24192437
ServerThread(DTLSServerProtocol serverProtocol, TlsServer server, DatagramTransport serverTransport,
2420-
boolean helloVerifyRequestFrontEnd)
2438+
boolean helloVerifyRequestFrontEnd, boolean expectAbort)
24212439
{
24222440
this.serverProtocol = serverProtocol;
24232441
this.server = server;
24242442
this.serverTransport = serverTransport;
24252443
this.helloVerifyRequestFrontEnd = helloVerifyRequestFrontEnd;
2444+
this.expectAbort = expectAbort;
2445+
}
2446+
2447+
/**
2448+
* Whatever aborted the server, for a test whose subject is the server's own refusal: the client only
2449+
* ever sees the alert that results, so asserting on this is what distinguishes the server raising the
2450+
* refusal from the client inferring something from an alert.
2451+
*/
2452+
Exception getCaught()
2453+
{
2454+
return caught;
24262455
}
24272456

24282457
public void run()
@@ -2452,7 +2481,16 @@ public void run()
24522481
}
24532482
catch (Exception e)
24542483
{
2455-
e.printStackTrace();
2484+
this.caught = e;
2485+
2486+
/*
2487+
* A test whose subject IS the server's refusal aborts here on every run, so printing would
2488+
* make a passing test look like a failing one. It asserts on getCaught() instead.
2489+
*/
2490+
if (!expectAbort)
2491+
{
2492+
e.printStackTrace();
2493+
}
24562494
}
24572495
}
24582496

0 commit comments

Comments
 (0)