Add a test for out-of-order ChangeCipherSpec in DTLS.

We were missing this case. It is possible to receive an early unencrypted
ChangeCipherSpec alert in DTLS because they aren't ordered relative to the
handshake. Test this case. (ChangeCipherSpec in DTLS is kind of pointless.)

Change-Id: I84268bc1821734f606fb20bfbeda91abf372f32c
Reviewed-on: https://boringssl-review.googlesource.com/8460
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index ae03850..2a5565b 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -493,6 +493,11 @@
 	// and 1.0.1 modes, respectively.
 	EarlyChangeCipherSpec int
 
+	// StrayChangeCipherSpec causes every pre-ChangeCipherSpec handshake
+	// message in DTLS to be prefaced by stray ChangeCipherSpec record. This
+	// may be used to test DTLS's handling of reordered ChangeCipherSpec.
+	StrayChangeCipherSpec bool
+
 	// FragmentAcrossChangeCipherSpec causes the implementation to fragment
 	// the Finished (or NextProto) message around the ChangeCipherSpec
 	// messages.
diff --git a/ssl/test/runner/dtls.go b/ssl/test/runner/dtls.go
index d87ab9f..06ec974 100644
--- a/ssl/test/runner/dtls.go
+++ b/ssl/test/runner/dtls.go
@@ -141,7 +141,30 @@
 func (c *Conn) dtlsWriteRecord(typ recordType, data []byte) (n int, err error) {
 	if typ != recordTypeHandshake {
 		// Only handshake messages are fragmented.
-		return c.dtlsWriteRawRecord(typ, data)
+		n, err = c.dtlsWriteRawRecord(typ, data)
+		if err != nil {
+			return
+		}
+
+		if typ == recordTypeChangeCipherSpec {
+			err = c.out.changeCipherSpec(c.config)
+			if err != nil {
+				// Cannot call sendAlert directly,
+				// because we already hold c.out.Mutex.
+				c.tmp[0] = alertLevelError
+				c.tmp[1] = byte(err.(alert))
+				c.writeRecord(recordTypeAlert, c.tmp[0:2])
+				return n, c.out.setErrorLocked(&net.OpError{Op: "local error", Err: err})
+			}
+		}
+		return
+	}
+
+	if c.out.cipher == nil && c.config.Bugs.StrayChangeCipherSpec {
+		_, err = c.dtlsWriteRawRecord(recordTypeChangeCipherSpec, []byte{1})
+		if err != nil {
+			return
+		}
 	}
 
 	maxLen := c.config.Bugs.MaxHandshakeRecordLength
@@ -354,18 +377,6 @@
 	n = len(data)
 
 	c.out.freeBlock(b)
-
-	if typ == recordTypeChangeCipherSpec {
-		err = c.out.changeCipherSpec(c.config)
-		if err != nil {
-			// Cannot call sendAlert directly,
-			// because we already hold c.out.Mutex.
-			c.tmp[0] = alertLevelError
-			c.tmp[1] = byte(err.(alert))
-			c.writeRecord(recordTypeAlert, c.tmp[0:2])
-			return n, c.out.setErrorLocked(&net.OpError{Op: "local error", Err: err})
-		}
-	}
 	return
 }
 
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index e6bfba4..709cd8d 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -1277,6 +1277,15 @@
 			expectedError: ":UNEXPECTED_RECORD:",
 		},
 		{
+			protocol: dtls,
+			name:     "StrayChangeCipherSpec",
+			config: Config{
+				Bugs: ProtocolBugs{
+					StrayChangeCipherSpec: true,
+				},
+			},
+		},
+		{
 			name: "SkipNewSessionTicket",
 			config: Config{
 				Bugs: ProtocolBugs{