Skip to content

Commit 205b036

Browse files
committed
Refuse to negotiate DTLS 1.3 behind a HelloVerifyRequest front end, relates to github #1468.
1 parent 23bcade commit 205b036

3 files changed

Lines changed: 167 additions & 36 deletions

File tree

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

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,13 @@ protected DTLSTransport serverHandshake(ServerHandshakeState state, DTLSRequest
132132
}
133133
else
134134
{
135+
/*
136+
* 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.
139+
*/
140+
state.afterHelloVerifyRequest = true;
141+
135142
processClientHello(state, request.getClientHello());
136143

137144
request = null;
@@ -1167,6 +1174,27 @@ protected byte[] generateServerHello(ServerHandshakeState state)
11671174

11681175
if (ProtocolVersion.DTLSv13.isEqualOrEarlierVersionOf(serverVersion))
11691176
{
1177+
/*
1178+
* RFC 9147 5.1. DTLS 1.3 has no HelloVerifyRequest at all: its denial-of-service cookie is
1179+
* carried by a HelloRetryRequest instead. A connection reached through DTLSVerifier has already
1180+
* had a HelloVerifyRequest sent on it, and DTLSClientProtocol rightly refuses a DTLS 1.3
1181+
* selection that arrives after one, so this handshake cannot complete. Fail here, where the cause
1182+
* is known, rather than leave the operator to diagnose their own configuration from an alert
1183+
* raised on the client.
1184+
*
1185+
* NOTE: Capping the offered versions at DTLS 1.2 instead is not an option: the RFC 8446 4.1.3
1186+
* downgrade sentinel written below is derived from server.getProtocolVersions(), not from the
1187+
* capped list, so a 1.3-capable client would abort on the sentinel regardless.
1188+
*
1189+
* TODO[dtls13] Support the RFC 9147 5.1 stateless HelloRetryRequest cookie exchange in
1190+
* DTLSVerifier, so that a DTLS 1.3 handshake can be fronted by a cookie exchange too.
1191+
*/
1192+
if (state.afterHelloVerifyRequest)
1193+
{
1194+
throw new TlsFatalAlert(AlertDescription.internal_error,
1195+
"DTLS 1.3 cannot be negotiated behind a HelloVerifyRequest front end");
1196+
}
1197+
11701198
/*
11711199
* RFC 9147 5.1. DTLS 1.3 records carry 'legacy_record_version' 0xfefd (DTLS 1.2) in the
11721200
* plaintext records that precede the first protected epoch.
@@ -1737,6 +1765,7 @@ protected static class ServerHandshakeState
17371765
Hashtable serverExtensions = null;
17381766
boolean expectSessionTicket = false;
17391767
boolean helloRetryRequestSent = false;
1768+
boolean afterHelloVerifyRequest = false;
17401769
byte[] retryCookie = null;
17411770
int retryGroup = -1;
17421771
TlsKeyExchange keyExchange = null;

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

Lines changed: 107 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,63 @@ public void testDTLSv13ServerHelloWithoutKeyShare() throws Exception
5252
alertDescription);
5353
}
5454

55+
/**
56+
* RFC 9147 5.1. DTLS 1.3 has no HelloVerifyRequest, so a server that sent one must not then select DTLS
57+
* 1.3: the handshake would have been reached through a denial-of-service countermeasure DTLS 1.3 does not
58+
* have, and without this check the DTLS 1.2 cookie exchange is a way in to a 1.3 handshake. A
59+
* version-straddling client - one that offers DTLS 1.2 as well - has to accept the HelloVerifyRequest,
60+
* because the peer may genuinely be a DTLS 1.2 server, so the refusal can only come afterwards.
61+
* <p>
62+
* The server half of this sequence is scripted rather than driven by DTLSServerProtocol, because a
63+
* conforming BC server no longer produces it: DTLSServerProtocol.generateServerHello refuses to select
64+
* DTLS 1.3 behind a HelloVerifyRequest front end at all (see
65+
* DTLS13ProtocolTest.testServerRefusesToSelectDTLSv13BehindAHelloVerifyRequest). Only a synthetic peer
66+
* can still exercise the client's side of the rule.
67+
* </p>
68+
*/
69+
public void testClientRefusesDTLSv13SelectedAfterAHelloVerifyRequest() throws Exception
70+
{
71+
byte[] cookie = new byte[]{ (byte)0xA1, (byte)0xB2, (byte)0xC3, (byte)0xD4 };
72+
73+
byte[][] script = new byte[][]{
74+
// RFC 6347 4.2.1. HelloVerifyRequest, message_seq 0, answering the first ClientHello
75+
createHelloVerifyRequestRecord(cookie, 0),
76+
// and then DTLS 1.3 anyway, at the message_seq the client expects a ServerHello at after one
77+
createServerHelloRecord(new byte[]{ (byte)0xFE, (byte)0xFC }, 1, 1)
78+
};
79+
80+
ScriptedServerHelloTransport transport = new ScriptedServerHelloTransport(script);
81+
82+
TlsFatalAlert fatalAlert = connectAndExpectFatalAlert(transport,
83+
ProtocolVersion.DTLSv13.downTo(ProtocolVersion.DTLSv12));
84+
85+
assertEquals("alert for DTLS 1.3 selected after a HelloVerifyRequest",
86+
AlertDescription.illegal_parameter, fatalAlert.getAlertDescription());
87+
88+
/*
89+
* The alert description alone would not say which check raised it - the DTLS 1.3 ServerHello scripted
90+
* here carries no "key_share" either, and that check raises the same alert a few lines later. The
91+
* message, and the fact that the client answered the HelloVerifyRequest with a second ClientHello,
92+
* pin it to the rule under test.
93+
*/
94+
assertEquals("the HelloVerifyRequest check must be the one that raised it",
95+
"illegal_parameter(47); Server selected DTLS 1.3 after sending a HelloVerifyRequest",
96+
fatalAlert.getMessage());
97+
98+
assertEquals("the client must have answered the HelloVerifyRequest", 2,
99+
transport.clientHellosSeen());
100+
}
101+
55102
private short connectAndExpectFatalAlert(byte[] selectedVersion) throws Exception
103+
{
104+
ScriptedServerHelloTransport transport = new ScriptedServerHelloTransport(
105+
new byte[][]{ createServerHelloRecord(selectedVersion, 0, 0) });
106+
107+
return connectAndExpectFatalAlert(transport, ProtocolVersion.DTLSv13.only()).getAlertDescription();
108+
}
109+
110+
private TlsFatalAlert connectAndExpectFatalAlert(ScriptedServerHelloTransport transport,
111+
final ProtocolVersion[] clientVersions) throws Exception
56112
{
57113
MockDTLSClient client = new MockDTLSClient(null)
58114
{
@@ -63,7 +119,7 @@ public int[] getCipherSuites()
63119

64120
protected ProtocolVersion[] getSupportedVersions()
65121
{
66-
return ProtocolVersion.DTLSv13.only();
122+
return clientVersions;
67123
}
68124
};
69125

@@ -72,9 +128,6 @@ protected ProtocolVersion[] getSupportedVersions()
72128

73129
DTLSClientProtocol clientProtocol = new DTLSClientProtocol();
74130

75-
ScriptedServerHelloTransport transport = new ScriptedServerHelloTransport(
76-
createServerHelloRecord(selectedVersion));
77-
78131
try
79132
{
80133
clientProtocol.connect(client, transport);
@@ -83,18 +136,35 @@ protected ProtocolVersion[] getSupportedVersions()
83136
{
84137
assertTrue("server never received a ClientHello", transport.sawClientHello());
85138

86-
return fatalAlert.getAlertDescription();
139+
return fatalAlert;
87140
}
88141

89142
fail("expected the client to raise a fatal alert");
90-
return -1;
143+
return null;
144+
}
145+
146+
/**
147+
* RFC 6347 4.2.1. A DTLS plaintext record carrying a HelloVerifyRequest, whose 'server_version' is DTLS
148+
* 1.0 as that section requires regardless of the version being negotiated.
149+
*/
150+
private static byte[] createHelloVerifyRequestRecord(byte[] cookie, long recordSeq) throws IOException
151+
{
152+
ByteArrayOutputStream body = new ByteArrayOutputStream();
153+
// server_version: DTLS 1.0
154+
body.write(0xFE);
155+
body.write(0xFF);
156+
body.write(cookie.length);
157+
body.write(cookie, 0, cookie.length);
158+
159+
return createPlaintextRecord(HandshakeType.hello_verify_request, 0, recordSeq, body.toByteArray());
91160
}
92161

93162
/**
94163
* A DTLS plaintext record (RFC 9147 4) carrying a single unfragmented ServerHello, selecting the given
95164
* version through the "supported_versions" extension.
96165
*/
97-
private static byte[] createServerHelloRecord(byte[] selectedVersion) throws IOException
166+
private static byte[] createServerHelloRecord(byte[] selectedVersion, int messageSeq, long recordSeq)
167+
throws IOException
98168
{
99169
ByteArrayOutputStream extensions = new ByteArrayOutputStream();
100170
writeUint16(extensions, ExtensionType.supported_versions);
@@ -120,26 +190,34 @@ private static byte[] createServerHelloRecord(byte[] selectedVersion) throws IOE
120190
writeUint16(body, extensionsData.length);
121191
body.write(extensionsData, 0, extensionsData.length);
122192

123-
byte[] bodyData = body.toByteArray();
193+
return createPlaintextRecord(HandshakeType.server_hello, messageSeq, recordSeq, body.toByteArray());
194+
}
124195

196+
/**
197+
* RFC 9147 4. One epoch-0 plaintext record carrying one unfragmented handshake message. The
198+
* 'sequence_number' is the caller's, because the record layer's replay window discards a repeat of one it
199+
* has already accepted, so each record of a scripted sequence needs its own.
200+
*/
201+
private static byte[] createPlaintextRecord(short msgType, int messageSeq, long recordSeq, byte[] bodyData)
202+
throws IOException
203+
{
125204
ByteArrayOutputStream record = new ByteArrayOutputStream();
126205
record.write(ContentType.handshake);
127206
// legacy_record_version
128207
record.write(0xFE);
129208
record.write(0xFD);
130209
// epoch
131210
writeUint16(record, 0);
132-
// sequence_number
133-
for (int i = 0; i < 6; ++i)
211+
// sequence_number (48 bits)
212+
for (int i = 5; i >= 0; --i)
134213
{
135-
record.write(0);
214+
record.write((int)((recordSeq >>> (8 * i)) & 0xFF));
136215
}
137216
writeUint16(record, 12 + bodyData.length);
138217

139-
record.write(HandshakeType.server_hello);
218+
record.write(msgType);
140219
writeUint24(record, bodyData.length);
141-
// message_seq
142-
writeUint16(record, 0);
220+
writeUint16(record, messageSeq);
143221
// fragment_offset
144222
writeUint24(record, 0);
145223
// fragment_length
@@ -163,24 +241,31 @@ private static void writeUint24(ByteArrayOutputStream buf, int i)
163241
}
164242

165243
/**
166-
* Answers the first ClientHello with one prepared record and nothing thereafter.
244+
* Answers each ClientHello with the next prepared record, and nothing once the script runs out. One
245+
* record per ClientHello is enough for both shapes used here: a single ServerHello, and a
246+
* HelloVerifyRequest followed by the ServerHello that answers the second ClientHello.
167247
*/
168248
private static class ScriptedServerHelloTransport
169249
implements DatagramTransport
170250
{
171-
private final byte[] serverHelloRecord;
251+
private final byte[][] script;
172252

173-
private boolean sawClientHello = false;
253+
private int clientHellosSeen = 0;
174254
private byte[] pending = null;
175255

176-
ScriptedServerHelloTransport(byte[] serverHelloRecord)
256+
ScriptedServerHelloTransport(byte[][] script)
177257
{
178-
this.serverHelloRecord = serverHelloRecord;
258+
this.script = script;
179259
}
180260

181261
boolean sawClientHello()
182262
{
183-
return sawClientHello;
263+
return clientHellosSeen > 0;
264+
}
265+
266+
int clientHellosSeen()
267+
{
268+
return clientHellosSeen;
184269
}
185270

186271
public int getReceiveLimit()
@@ -196,11 +281,10 @@ public int getSendLimit()
196281
public void send(byte[] buf, int off, int len) throws IOException
197282
{
198283
// DTLS plaintext record header (13 bytes), then the DTLS handshake message header
199-
if (!sawClientHello && len > 13 && (buf[off] & 0xFF) == ContentType.handshake
284+
if (clientHellosSeen < script.length && len > 13 && (buf[off] & 0xFF) == ContentType.handshake
200285
&& (buf[off + 13] & 0xFF) == HandshakeType.client_hello)
201286
{
202-
this.sawClientHello = true;
203-
this.pending = serverHelloRecord;
287+
this.pending = script[clientHellosSeen++];
204288
}
205289
}
206290

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

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -748,7 +748,7 @@ public void testSecondHelloRetryRequestRejected() throws Exception
748748
* DTLS 1.2 countermeasure front end is put in front of the server here precisely because it is the thing
749749
* that would otherwise send one.
750750
*/
751-
public void testHelloVerifyRequestRefusedWhenOnlyDTLSv13Offered() throws Exception
751+
public void testClientRefusesAHelloVerifyRequestWhenOnlyDTLSv13WasOffered() throws Exception
752752
{
753753
Harness harness = new Harness();
754754
harness.helloVerifyRequestFrontEnd = true;
@@ -768,38 +768,56 @@ public void testHelloVerifyRequestRefusedWhenOnlyDTLSv13Offered() throws Excepti
768768
}
769769

770770
/**
771-
* RFC 9147 5.1. The other half of the same rule, and the one a version-straddling client reaches: a client
772-
* that also offers DTLS 1.2 must accept a HelloVerifyRequest, since the server may be a 1.2 server - but
773-
* having answered one it must not then let the server select DTLS 1.3, because that handshake would have
774-
* been reached through a countermeasure DTLS 1.3 does not have. Without this check the cookie exchange is
775-
* a way in to a 1.3 handshake.
771+
* RFC 9147 5.1. The server half of the same rule. A version-straddling client - one that also offers DTLS
772+
* 1.2 - legitimately answers a HelloVerifyRequest, so the server is reached, and the server must then
773+
* refuse to select DTLS 1.3 on that connection: DTLS 1.3 has no HelloVerifyRequest, and a client that
774+
* answered one rightly refuses a 1.3 selection afterwards (see
775+
* DTLS13ClientProtocolTest.testClientRefusesDTLSv13SelectedAfterAHelloVerifyRequest, which scripts a
776+
* server that does it anyway). The refusal is raised on the server, with a diagnostic naming the cause,
777+
* so an operator who lists DTLSv13 and keeps DTLSVerifier in front of it learns that from their own logs
778+
* rather than from an alert on somebody else's client.
779+
* <p>
780+
* Capping the offered versions at DTLS 1.2 instead is not an option: the RFC 8446 4.1.3 downgrade
781+
* sentinel is derived from the server's configured versions, not from the capped list, so a 1.3-capable
782+
* client would abort on the sentinel anyway - see the NOTE in DTLSServerProtocol.generateServerHello.
783+
* </p>
776784
*/
777-
public void testDTLSv13RefusedAfterAHelloVerifyRequest() throws Exception
785+
public void testServerRefusesToSelectDTLSv13BehindAHelloVerifyRequest() throws Exception
778786
{
779787
Harness harness = new Harness();
780788
harness.helloVerifyRequestFrontEnd = true;
781789
harness.clientVersions = ProtocolVersion.DTLSv13.downTo(ProtocolVersion.DTLSv12);
782-
harness.serverHandshakeTimeoutMillis = 4000;
790+
harness.serverVersions = ProtocolVersion.DTLSv13.downTo(ProtocolVersion.DTLSv12);
783791

784792
try
785793
{
786794
harness.run(16);
787795

788-
fail("expected the client to refuse DTLS 1.3 after a HelloVerifyRequest");
796+
fail("expected the server to refuse DTLS 1.3 behind a HelloVerifyRequest front end");
789797
}
790-
catch (TlsFatalAlert fatalAlert)
798+
catch (TlsFatalAlertReceived fatalAlertReceived)
791799
{
792-
assertEquals("alert for DTLS 1.3 selected after a HelloVerifyRequest",
793-
AlertDescription.illegal_parameter, fatalAlert.getAlertDescription());
800+
assertEquals("alert for DTLS 1.3 selected behind a HelloVerifyRequest front end",
801+
AlertDescription.internal_error, fatalAlertReceived.getAlertDescription());
794802
}
803+
804+
/*
805+
* The refusal is raised where the cause is known, which is after the version has been selected, so
806+
* DTLS 1.3 really was what the server was about to send - the handshake is not merely failing for
807+
* want of a common version.
808+
*/
809+
assertEquals("the version the server refused to go on with", ProtocolVersion.DTLSv13,
810+
harness.serverVersion);
811+
812+
assertEquals("no application data can have been exchanged", null, harness.echo);
795813
}
796814

797815
/**
798816
* The counterpart of the two above: the HelloVerifyRequest path that DTLS 1.2 depends on is untouched. A
799817
* client and server that both also speak DTLS 1.2 complete a 1.2 handshake through the cookie exchange,
800818
* exactly as the DTLS 1.2 suites do.
801819
*/
802-
public void testHelloVerifyRequestStillWorksForDTLSv12() throws Exception
820+
public void testHelloVerifyRequestStillCompletesADTLSv12Handshake() throws Exception
803821
{
804822
Harness harness = new Harness();
805823
harness.helloVerifyRequestFrontEnd = true;

0 commit comments

Comments
 (0)