runner: Retain past epochs

This is the test implementation, so we won't be particularly careful
about this. This resolves a TODO in resetCipher. Later it will be used
for the new retransmit testing machinery to read or write from past
epochs when we expect it.

Bug: 42290594
Change-Id: I8c51fc7adfbb8e671bd1c810c0fc6fa504e4a9ab
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/72650
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Nick Harper <nharper@chromium.org>
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index a2c7d6b..e8b6c0a 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -173,6 +173,11 @@
 	return c.conn.SetWriteDeadline(t)
 }
 
+// Arbitrarily cap the number of past epochs to 4. This is far more than is
+// necessary. We set a limit only so tests can freely trigger unboundedly many
+// KeyUpdates.
+const maxEpochs = 4
+
 type epochState struct {
 	epoch                 uint16
 	cipher                any // cipher algorithm
@@ -191,6 +196,7 @@
 	wireVersion uint16 // wire version
 	isDTLS      bool
 	epoch       epochState
+	pastEpochs  []epochState
 
 	nextEpoch epochState
 
@@ -216,6 +222,30 @@
 	return err
 }
 
+func (hc *halfConn) getEpoch(epochValue uint16) (*epochState, bool) {
+	if hc.epoch.epoch == epochValue {
+		return &hc.epoch, true
+	}
+	for i := range hc.pastEpochs {
+		if hc.pastEpochs[i].epoch == epochValue {
+			return &hc.pastEpochs[i], true
+		}
+	}
+	return nil, false
+}
+
+func (hc *halfConn) changeEpoch(epoch epochState) {
+	if len(hc.pastEpochs) < maxEpochs {
+		hc.pastEpochs = append(hc.pastEpochs, hc.epoch)
+	} else {
+		for i := 1; i < len(hc.pastEpochs); i++ {
+			hc.pastEpochs[i-1] = hc.pastEpochs[i]
+		}
+		hc.pastEpochs[len(hc.pastEpochs)-1] = hc.epoch
+	}
+	hc.epoch = epoch
+}
+
 func (hc *halfConn) newEpochState(epoch uint16, cipher any, mac macFunction) epochState {
 	ret := epochState{epoch: epoch, cipher: cipher, mac: mac}
 	if hc.isDTLS {
@@ -246,7 +276,8 @@
 	if hc.nextEpoch.cipher == nil {
 		return alertInternalError
 	}
-	hc.epoch, hc.nextEpoch = hc.nextEpoch, epochState{}
+	hc.changeEpoch(hc.nextEpoch)
+	hc.nextEpoch = epochState{}
 
 	if hc.config.Bugs.NullAllCiphers {
 		hc.epoch.cipher = nullCipher{}
@@ -279,18 +310,19 @@
 		newEpoch.cipher = nullCipher{}
 	}
 	hc.trafficSecret = secret
-	hc.epoch = newEpoch
+	hc.changeEpoch(newEpoch)
 }
 
 // resetCipher resets the cipher state back to no encryption to be able
 // to send an unencrypted ClientHello in response to HelloRetryRequest
 // after 0-RTT data was rejected.
 func (hc *halfConn) resetCipher() {
-	// In all cases, the cipher is set to nil so that second ClientHello
-	// will be sent with no encryption (instead of with early data keys).
-	hc.epoch.cipher = nil
-	// TODO(crbug.com/42290594): Instead, retain both the initial and 0-RTT
-	// epoch states as separate epochs and discard the 0-RTT one.
+	initialEpoch, ok := hc.getEpoch(0)
+	if !ok {
+		panic("tls: could not find initial epoch")
+	}
+	hc.epoch = *initialEpoch
+	hc.pastEpochs = nil
 }
 
 // incSeq increments the sequence number.