@@ -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
0 commit comments