@@ -815,6 +815,83 @@ public void testHelloVerifyRequestStillWorksForDTLSv12() throws Exception
815815 assertTrue ("echoed application data differs" , Arrays .areEqual (harness .request , harness .echo ));
816816 }
817817
818+ /**
819+ * RFC 8446 / RFC 9147 remove renegotiation, so a DTLS 1.3 client legitimately sends neither the RFC 5746
820+ * "renegotiation_info" extension nor the TLS_EMPTY_RENEGOTIATION_INFO_SCSV. A client that straddles the
821+ * versions - offering DTLS 1.3 and DTLS 1.2 - and implements neither must still be able to complete a DTLS
822+ * 1.3 handshake, because the server's RFC 5746 rule is a property of the version it SELECTS, not of the
823+ * versions the client offered. TlsServerProtocol gets this right by placing its notifySecureRenegotiation
824+ * call in the part of generateServerHello that the 1.3 path returns before reaching.
825+ */
826+ public void testDTLSv13WithNeitherSecureRenegotiationSignal () throws Exception
827+ {
828+ Harness harness = new Harness ();
829+ harness .clientVersions = ProtocolVersion .DTLSv13 .downTo (ProtocolVersion .DTLSv12 );
830+ harness .suppressSecureRenegotiationOffer = true ;
831+
832+ harness .run (16 );
833+
834+ assertEquals ("client negotiated version" , ProtocolVersion .DTLSv13 , harness .clientVersion );
835+ assertEquals ("server negotiated version" , ProtocolVersion .DTLSv13 , harness .serverVersion );
836+
837+ assertEquals ("notifySecureRenegotiation reached on a DTLS 1.3 handshake" , 0 ,
838+ harness .serverSecureRenegotiationNotifications );
839+
840+ assertNotNull ("no application data echoed back" , harness .echo );
841+ assertTrue ("echoed application data differs" , Arrays .areEqual (harness .request , harness .echo ));
842+ }
843+
844+ /**
845+ * The DTLS 1.2 half of the same rule, unchanged: where DTLS 1.2 is the version actually selected, a client
846+ * that sent neither the "renegotiation_info" extension nor the SCSV is still refused by MockDTLSServer's
847+ * inherited RFC 5746 behaviour. The client's offer is identical to the test above; only the version the
848+ * server can select differs, which is exactly what the callback must be gated on.
849+ */
850+ public void testDTLSv12WithNeitherSecureRenegotiationSignalStillRefused () throws Exception
851+ {
852+ Harness harness = new Harness ();
853+ harness .clientVersions = ProtocolVersion .DTLSv13 .downTo (ProtocolVersion .DTLSv12 );
854+ harness .serverVersions = ProtocolVersion .DTLSv12 .only ();
855+ harness .suppressSecureRenegotiationOffer = true ;
856+
857+ try
858+ {
859+ harness .run (16 );
860+
861+ fail ("expected the DTLS 1.2 server to refuse a ClientHello with no RFC 5746 signal" );
862+ }
863+ catch (TlsFatalAlertReceived fatalAlertReceived )
864+ {
865+ assertEquals ("alert for a DTLS 1.2 ClientHello with no RFC 5746 signal" ,
866+ AlertDescription .handshake_failure , fatalAlertReceived .getAlertDescription ());
867+ }
868+
869+ assertEquals ("notifySecureRenegotiation not reached on a DTLS 1.2 handshake" , 1 ,
870+ harness .serverSecureRenegotiationNotifications );
871+ }
872+
873+ /**
874+ * And the DTLS 1.2 positive case: the same server, reached by the same version-straddling client, where the
875+ * SCSV is left in place. The callback is reached exactly once and the handshake completes.
876+ */
877+ public void testDTLSv12WithTheSecureRenegotiationSCSV () throws Exception
878+ {
879+ Harness harness = new Harness ();
880+ harness .clientVersions = ProtocolVersion .DTLSv13 .downTo (ProtocolVersion .DTLSv12 );
881+ harness .serverVersions = ProtocolVersion .DTLSv12 .only ();
882+
883+ harness .run (16 );
884+
885+ assertEquals ("client negotiated version" , ProtocolVersion .DTLSv12 , harness .clientVersion );
886+ assertEquals ("server negotiated version" , ProtocolVersion .DTLSv12 , harness .serverVersion );
887+
888+ assertEquals ("notifySecureRenegotiation reached once on a DTLS 1.2 handshake" , 1 ,
889+ harness .serverSecureRenegotiationNotifications );
890+
891+ assertNotNull ("no application data echoed back" , harness .echo );
892+ assertTrue ("echoed application data differs" , Arrays .areEqual (harness .request , harness .echo ));
893+ }
894+
818895 /**
819896 * RFC 8446 4.2.2. The second ClientHello must echo the cookie exactly, and a server that accepted anything
820897 * else would have no way to tell its own HelloRetryRequest's answer from an unrelated ClientHello. One byte
@@ -1655,6 +1732,13 @@ static class Harness
16551732 boolean replaySecondHelloRetryRequest = false ;
16561733 int mangleSecondClientHello = MANGLE_NONE ;
16571734 boolean helloVerifyRequestFrontEnd = false ;
1735+
1736+ /*
1737+ * RFC 5746 3.4. Strips the TLS_EMPTY_RENEGOTIATION_INFO_SCSV from the ClientHello on its way out, so
1738+ * that a version-straddling offer carries neither the SCSV nor the "renegotiation_info" extension -
1739+ * what a third-party DTLS 1.3 client that never implemented RFC 5746 sends.
1740+ */
1741+ boolean suppressSecureRenegotiationOffer = false ;
16581742 ProtocolVersion [] clientVersions = ProtocolVersion .DTLSv13 .only ();
16591743 ProtocolVersion [] serverVersions = ProtocolVersion .DTLSv13 .only ();
16601744
@@ -1685,6 +1769,9 @@ static class Harness
16851769
16861770 ProtocolVersion clientVersion = null ;
16871771 ProtocolVersion serverVersion = null ;
1772+
1773+ // How many times the server's notifySecureRenegotiation callback was reached
1774+ int serverSecureRenegotiationNotifications = 0 ;
16881775 int clientCipherSuite = -1 ;
16891776 int serverCipherSuite = -1 ;
16901777 byte [] request = null ;
@@ -1959,6 +2046,18 @@ public int getHandshakeTimeoutMillis()
19592046 return serverHandshakeTimeoutMillis ;
19602047 }
19612048
2049+ /*
2050+ * RFC 5746. Counted so a test can assert that a DTLS 1.3 handshake never reaches it - RFC
2051+ * 8446 / RFC 9147 removed renegotiation, so the callback belongs to the DTLS 1.2-and-below
2052+ * path only. The default behaviour (reject when the flag is false) is preserved.
2053+ */
2054+ public void notifySecureRenegotiation (boolean secureRenegotiation ) throws IOException
2055+ {
2056+ ++serverSecureRenegotiationNotifications ;
2057+
2058+ super .notifySecureRenegotiation (secureRenegotiation );
2059+ }
2060+
19622061 public int [] getSupportedGroups () throws IOException
19632062 {
19642063 if (!forceHelloRetryRequest )
@@ -2098,7 +2197,11 @@ public void notifyHandshakeComplete() throws IOException
20982197
20992198 try
21002199 {
2101- DTLSTransport dtlsClient = new DTLSClientProtocol ().connect (client , recording );
2200+ DTLSClientProtocol clientProtocol = suppressSecureRenegotiationOffer
2201+ ? new NoSecureRenegotiationOfferClientProtocol ()
2202+ : new DTLSClientProtocol ();
2203+
2204+ DTLSTransport dtlsClient = clientProtocol .connect (client , recording );
21022205
21032206 if (probeServerAckPath )
21042207 {
@@ -2225,6 +2328,67 @@ private void injectPlaintextHandshakeRecord() throws IOException
22252328 }
22262329 }
22272330
2331+ /**
2332+ * A client that offers DTLS 1.2 but implements none of RFC 5746: the
2333+ * TLS_EMPTY_RENEGOTIATION_INFO_SCSV that DTLSClientProtocol.generateClientHello appends is removed from
2334+ * the encoded ClientHello before it is digested or sent, and BC never offers the "renegotiation_info"
2335+ * extension itself, so neither signal reaches the server. Editing the encoded body (rather than the
2336+ * offered suites) keeps the client's own transcript hash and the server's in step, because
2337+ * DTLSReliableHandshake digests what generateClientHello returned.
2338+ */
2339+ static class NoSecureRenegotiationOfferClientProtocol
2340+ extends DTLSClientProtocol
2341+ {
2342+ protected byte [] generateClientHello (ClientHandshakeState state ) throws IOException
2343+ {
2344+ return removeCipherSuite (super .generateClientHello (state ),
2345+ CipherSuite .TLS_EMPTY_RENEGOTIATION_INFO_SCSV );
2346+ }
2347+
2348+ /**
2349+ * Rewrites the 'cipher_suites' vector of an encoded DTLS ClientHello body without one suite. The body
2350+ * is client_version(2) random(32) session_id(1+n) cookie(1+n) cipher_suites(2+n) ...
2351+ */
2352+ private static byte [] removeCipherSuite (byte [] body , int cipherSuite )
2353+ {
2354+ int pos = 2 + 32 ;
2355+ pos += 1 + (body [pos ] & 0xFF );
2356+ pos += 1 + (body [pos ] & 0xFF );
2357+
2358+ int suitesLength = readUint16 (body , pos );
2359+ int suitesOff = pos + 2 ;
2360+
2361+ byte [] out = new byte [body .length - 2 ];
2362+ System .arraycopy (body , 0 , out , 0 , suitesOff );
2363+ TlsUtils .writeUint16 (suitesLength - 2 , out , pos );
2364+
2365+ int written = suitesOff ;
2366+ boolean removed = false ;
2367+ for (int i = 0 ; i < suitesLength ; i += 2 )
2368+ {
2369+ if (!removed && readUint16 (body , suitesOff + i ) == cipherSuite )
2370+ {
2371+ removed = true ;
2372+ continue ;
2373+ }
2374+
2375+ out [written ++] = body [suitesOff + i ];
2376+ out [written ++] = body [suitesOff + i + 1 ];
2377+ }
2378+
2379+ if (!removed )
2380+ {
2381+ throw new IllegalStateException ("ClientHello did not offer cipher suite 0x"
2382+ + Integer .toHexString (cipherSuite ));
2383+ }
2384+
2385+ System .arraycopy (body , suitesOff + suitesLength , out , written ,
2386+ body .length - (suitesOff + suitesLength ));
2387+
2388+ return out ;
2389+ }
2390+ }
2391+
22282392 static class ServerThread
22292393 extends Thread
22302394 {
0 commit comments