Skip to content

Commit d9a6c3c

Browse files
committed
Gate the DTLS secure renegotiation notification on the selected version, relates to github #1468.
1 parent 682520a commit d9a6c3c

2 files changed

Lines changed: 174 additions & 12 deletions

File tree

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

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,6 +1194,8 @@ protected byte[] generateServerHello(ServerHandshakeState state)
11941194
}
11951195
}
11961196

1197+
server.notifySecureRenegotiation(securityParameters.isSecureRenegotiation());
1198+
11971199
Hashtable clientHelloExtensions = state.clientHello.getExtensions();
11981200

11991201
TlsSession sessionToResume = server.getSessionToResume(state.clientHello.getSessionID());
@@ -1646,18 +1648,14 @@ protected void processClientHello(ServerHandshakeState state, ClientHello client
16461648
}
16471649

16481650
/*
1649-
* RFC 8446 / RFC 9147 remove renegotiation, so a client offering only DTLS 1.3 (or later)
1650-
* legitimately sends neither the "renegotiation_info" extension nor the SCSV - see the matching
1651-
* 'offeringDTLSv12Minus' gate in DTLSClientProtocol.generateClientHello. TlsServerProtocol gets
1652-
* this for free, because its equivalent block sits in the DTLS-1.2-and-below part of
1653-
* generateServerHello, past the point where the 1.3 path has already returned; here the
1654-
* ClientHello is processed before a version is selected, so the client's own offer is the gate.
1651+
* NOTE: server.notifySecureRenegotiation is called from generateServerHello, in the
1652+
* DTLS-1.2-and-below portion past the point where the DTLS 1.3 path has returned, exactly as
1653+
* TlsServerProtocol.generateServerHello does it. RFC 8446 / RFC 9147 remove renegotiation, so a
1654+
* client offering only DTLS 1.3 (or later) legitimately sends neither the "renegotiation_info"
1655+
* extension nor the SCSV - see the matching 'offeringDTLSv12Minus' gate in
1656+
* DTLSClientProtocol.generateClientHello - and gating on the selected version rather than on the
1657+
* client's offer is what keeps a {1.3, 1.2} offer with neither of them acceptable.
16551658
*/
1656-
if (ProtocolVersion.DTLSv12.isEqualOrLaterVersionOf(
1657-
ProtocolVersion.getEarliestDTLS(serverContext.getClientSupportedVersions())))
1658-
{
1659-
server.notifySecureRenegotiation(securityParameters.isSecureRenegotiation());
1660-
}
16611659

16621660
if (clientHelloExtensions != null)
16631661
{

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

Lines changed: 165 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)