Test that post-handshake flows do not implicitly ACK Finished

Resolves an old TODO.

Bug: 42290594
Change-Id: I23d90f589ef8f2440f6d581efb4c5967428ad710
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/73288
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Nick Harper <nharper@chromium.org>
diff --git a/ssl/d1_both.cc b/ssl/d1_both.cc
index b337b37..a40710d 100644
--- a/ssl/d1_both.cc
+++ b/ssl/d1_both.cc
@@ -408,9 +408,6 @@
       // apply immediately after the handshake. As a client, receiving a
       // KeyUpdate or NewSessionTicket does not imply the server has received
       // our Finished. The server may have sent those messages in half-RTT.
-      //
-      // TODO(crbug.com/42290594): Once post-handshake messages are working,
-      // write a test for the half-RTT KeyUpdate case.
       implicit_ack = true;
     }
 
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index fbb8bf9..84fc93e 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -12351,6 +12351,76 @@
 					expectedError: ":READ_TIMEOUT_EXPIRED:",
 				})
 
+				// Neither post-handshake messages nor application data implicitly
+				// ACK the Finished flight. The server may have sent either in
+				// half-RTT data. Test that the client continues to retransmit
+				// despite this.
+				testCases = append(testCases, testCase{
+					protocol: dtls,
+					name:     "DTLS-Retransmit-Client-NoImplictACKFinished" + suffix,
+					config: Config{
+						MaxVersion: vers.version,
+						Bugs: ProtocolBugs{
+							ACKFlightDTLS: func(c *DTLSController, prev, received []DTLSMessage, records []DTLSRecordNumberInfo) {
+								// Merge the Finished flight into the NewSessionTicket.
+								c.MergeIntoNextFlight()
+							},
+							WriteFlightDTLS: func(c *DTLSController, prev, received, next []DTLSMessage, records []DTLSRecordNumberInfo) {
+								if next[0].Type != typeNewSessionTicket {
+									c.WriteFlight(next)
+									return
+								}
+								if len(received) == 0 || received[0].Type != typeFinished {
+									panic("Finished should be merged with NewSessionTicket")
+								}
+								// Merge NewSessionTicket into the KeyUpdate.
+								if next[len(next)-1].Type != typeKeyUpdate {
+									c.MergeIntoNextFlight()
+									return
+								}
+
+								// Write NewSessionTicket and the KeyUpdate and
+								// read the ACK.
+								c.WriteFlight(next)
+								ackTimeout := useTimeouts[0] / 4
+								c.AdvanceClock(ackTimeout)
+								c.ReadACK(c.InEpoch())
+
+								// The retransmit timer is still running.
+								c.AdvanceClock(useTimeouts[0] - ackTimeout)
+								c.ReadRetransmit()
+
+								// Application data can flow at the old epoch.
+								msg := []byte("test")
+								c.WriteAppData(c.OutEpoch()-1, msg)
+								c.ReadAppData(c.InEpoch(), expectedReply(msg))
+
+								// The retransmit timer is still running.
+								c.AdvanceClock(useTimeouts[1])
+								c.ReadRetransmit()
+
+								// Advance the shim to the next epoch.
+								c.WriteAppData(c.OutEpoch(), msg)
+								c.ReadAppData(c.InEpoch(), expectedReply(msg))
+
+								// The retransmit timer is still running. The shim
+								// actually could implicitly ACK at this point, but
+								// RFC 9147 does not list this as an implicit ACK.
+								c.AdvanceClock(useTimeouts[2])
+								c.ReadRetransmit()
+
+								// Finally ACK the final flight. Now the shim will
+								// stop the timer.
+								c.WriteACK(c.OutEpoch(), records)
+								c.ExpectNoNextTimeout()
+							},
+						},
+					},
+					sendKeyUpdates:   1,
+					keyUpdateRequest: keyUpdateNotRequested,
+					flags:            flags,
+				})
+
 				// If the server never receives an ACK for NewSessionTicket, it
 				// is eventually fatal.
 				testCases = append(testCases, testCase{