Disable compatibility mode for DTLS 1.3.

Bug: 715
Change-Id: I25509b0783852164006a156de98b1ac89f734e52
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/69947
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/ssl/d1_both.cc b/ssl/d1_both.cc
index a4499a7..ac47189 100644
--- a/ssl/d1_both.cc
+++ b/ssl/d1_both.cc
@@ -584,6 +584,11 @@
 }
 
 bool dtls1_add_change_cipher_spec(SSL *ssl) {
+  // DTLS 1.3 disables compatibility mode, which means that DTLS 1.3 never sends
+  // a ChangeCipherSpec message.
+  if (ssl_protocol_version(ssl) > TLS1_2_VERSION) {
+    return true;
+  }
   return add_outgoing(ssl, true /* ChangeCipherSpec */, Array<uint8_t>());
 }
 
diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc
index 9788b46..3bfc7ae 100644
--- a/ssl/handshake_client.cc
+++ b/ssl/handshake_client.cc
@@ -526,25 +526,28 @@
     return ssl_hs_error;
   }
 
-  // Never send a session ID in QUIC. QUIC uses TLS 1.3 at a minimum and
-  // disables TLS 1.3 middlebox compatibility mode.
-  if (ssl->quic_method == nullptr) {
-    const bool has_id_session = ssl->session != nullptr &&
-                                ssl->session->session_id_length > 0 &&
-                                ssl->session->ticket.empty();
-    const bool has_ticket_session =
-        ssl->session != nullptr && !ssl->session->ticket.empty();
-    if (has_id_session) {
-      hs->session_id_len = ssl->session->session_id_length;
-      OPENSSL_memcpy(hs->session_id, ssl->session->session_id,
-                     hs->session_id_len);
-    } else if (has_ticket_session || hs->max_version >= TLS1_3_VERSION) {
-      // Send a random session ID. TLS 1.3 always sends one, and TLS 1.2 session
-      // tickets require a placeholder value to signal resumption.
-      hs->session_id_len = sizeof(hs->session_id);
-      if (!RAND_bytes(hs->session_id, hs->session_id_len)) {
-        return ssl_hs_error;
-      }
+  const bool has_id_session = ssl->session != nullptr &&
+                              ssl->session->session_id_length > 0 &&
+                              ssl->session->ticket.empty();
+  const bool has_ticket_session =
+      ssl->session != nullptr && !ssl->session->ticket.empty();
+  // TLS 1.2 session tickets require a placeholder value to signal resumption.
+  const bool ticket_session_requires_random_id =
+      has_ticket_session &&
+      ssl_session_protocol_version(ssl->session.get()) < TLS1_3_VERSION;
+  // Compatibility mode sends a random session ID. Compatibility mode is
+  // enabled for TLS 1.3, but not when it's run over QUIC or DTLS.
+  const bool enable_compatibility_mode = hs->max_version >= TLS1_3_VERSION &&
+                                         ssl->quic_method == nullptr &&
+                                         !SSL_is_dtls(hs->ssl);
+  if (has_id_session) {
+    hs->session_id_len = ssl->session->session_id_length;
+    OPENSSL_memcpy(hs->session_id, ssl->session->session_id,
+                   hs->session_id_len);
+  } else if (ticket_session_requires_random_id || enable_compatibility_mode) {
+    hs->session_id_len = sizeof(hs->session_id);
+    if (!RAND_bytes(hs->session_id, hs->session_id_len)) {
+      return ssl_hs_error;
     }
   }
 
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index ab36e56..9260af0 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -1965,6 +1965,10 @@
 	// when running over QUIC.
 	CompatModeWithQUIC bool
 
+	// DTLS13EchoSessionID, if true, has DTLS 1.3 servers echo the client's
+	// session ID in the ServerHello.
+	DTLS13EchoSessionID bool
+
 	// EncryptSessionTicketKey, if non-nil, is the ticket key to use when
 	// encrypting tickets.
 	EncryptSessionTicketKey *[32]byte
diff --git a/ssl/test/runner/dtls.go b/ssl/test/runner/dtls.go
index b55f347..d407867 100644
--- a/ssl/test/runner/dtls.go
+++ b/ssl/test/runner/dtls.go
@@ -80,25 +80,17 @@
 	}
 	epoch := b.data[3:5]
 	seq := b.data[5:11]
-	if c.expectTLS13ChangeCipherSpec && typ == recordTypeChangeCipherSpec {
-		// CCS should only be at epoch 0
-		if !bytes.Equal(epoch, []byte{0, 0}) {
-			c.sendAlert(alertIllegalParameter)
-			return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: bad epoch, expected CCS at epoch 0"))
-		}
-	} else {
-		// For test purposes, require the sequence number be monotonically
-		// increasing, so c.in includes the minimum next sequence number. Gaps
-		// may occur if packets failed to be sent out. A real implementation
-		// would maintain a replay window and such.
-		if !bytes.Equal(epoch, c.in.seq[:2]) {
-			c.sendAlert(alertIllegalParameter)
-			return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: bad epoch, want %x, got %x", c.in.seq[:2], epoch))
-		}
-		if bytes.Compare(seq, c.in.seq[2:]) < 0 {
-			c.sendAlert(alertIllegalParameter)
-			return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: bad sequence number"))
-		}
+	// For test purposes, require the sequence number be monotonically
+	// increasing, so c.in includes the minimum next sequence number. Gaps
+	// may occur if packets failed to be sent out. A real implementation
+	// would maintain a replay window and such.
+	if !bytes.Equal(epoch, c.in.seq[:2]) {
+		c.sendAlert(alertIllegalParameter)
+		return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: bad epoch, want %x, got %x", c.in.seq[:2], epoch))
+	}
+	if bytes.Compare(seq, c.in.seq[2:]) < 0 {
+		c.sendAlert(alertIllegalParameter)
+		return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: bad sequence number"))
 	}
 	copy(c.in.seq[2:], seq)
 	n := int(b.data[11])<<8 | int(b.data[12])
@@ -108,21 +100,6 @@
 	}
 	b, c.rawInput = c.in.splitBlock(b, recordHeaderLen+n)
 
-	// Process CCS
-	if c.expectTLS13ChangeCipherSpec {
-		c.expectTLS13ChangeCipherSpec = false
-		ccsData := b.data[recordHeaderLen:]
-		if typ == recordTypeChangeCipherSpec {
-			if !bytes.Equal(ccsData, []byte{1}) {
-				return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: ChangeCipherSpec of length %d did not contain single byte value 0x01", len(ccsData)))
-			}
-			return c.dtlsDoReadRecord(want)
-		}
-		if typ != recordTypeAlert {
-			return 0, nil, c.in.setErrorLocked(fmt.Errorf("dtls: Expected ChangeCipherSpec but got record type %d", typ))
-		}
-	}
-
 	// Process message.
 	ok, off, _, alertValue := c.in.decrypt(b)
 	if !ok {
diff --git a/ssl/test/runner/handshake_client.go b/ssl/test/runner/handshake_client.go
index a7ab0e2..9362911 100644
--- a/ssl/test/runner/handshake_client.go
+++ b/ssl/test/runner/handshake_client.go
@@ -1003,12 +1003,16 @@
 	hs.finishedHash.discardHandshakeBuffer()
 
 	// The first server message must be followed by a ChangeCipherSpec.
-	c.expectTLS13ChangeCipherSpec = true
+	c.expectTLS13ChangeCipherSpec = !c.isDTLS
 
+	expectedSessionID := hs.hello.sessionID
+	if c.isDTLS {
+		expectedSessionID = nil
+	}
 	if haveHelloRetryRequest {
 		hs.writeServerHash(helloRetryRequest.marshal())
 
-		if !bytes.Equal(hs.hello.sessionID, helloRetryRequest.sessionID) {
+		if !bytes.Equal(expectedSessionID, helloRetryRequest.sessionID) {
 			return errors.New("tls: ClientHello and HelloRetryRequest session IDs did not match.")
 		}
 
@@ -1111,7 +1115,7 @@
 		}
 	}
 
-	if !bytes.Equal(hs.hello.sessionID, hs.serverHello.sessionID) {
+	if !bytes.Equal(expectedSessionID, hs.serverHello.sessionID) {
 		return errors.New("tls: ClientHello and ServerHello session IDs did not match.")
 	}
 
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index deb9ca5..9e647df 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -521,11 +521,15 @@
 
 	// We've read the ClientHello, so the next record must be preceded with ChangeCipherSpec.
 	c.expectTLS13ChangeCipherSpec = true
+	sessionID := hs.clientHello.sessionID
+	if c.isDTLS && !config.Bugs.DTLS13EchoSessionID {
+		sessionID = nil
+	}
 
 	hs.hello = &serverHelloMsg{
 		isDTLS:                c.isDTLS,
 		vers:                  c.wireVersion,
-		sessionID:             hs.clientHello.sessionID,
+		sessionID:             sessionID,
 		compressionMethod:     config.Bugs.SendCompressionMethod,
 		versOverride:          config.Bugs.SendServerHelloVersion,
 		supportedVersOverride: config.Bugs.SendServerSupportedVersionExtension,
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 1476b38..623f277 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -3710,6 +3710,25 @@
 		shouldFail:    true,
 		expectedError: ":UNEXPECTED_COMPATIBILITY_MODE:",
 	})
+
+	// Clients should reject DTLS 1.3 ServerHellos that echo the legacy
+	// session ID.
+	testCases = append(testCases, testCase{
+		protocol:      dtls,
+		name:          "DTLS13CompatibilityMode-EchoSessionID",
+		resumeSession: true,
+		config: Config{
+			MaxVersion: VersionTLS12,
+		},
+		resumeConfig: &Config{
+			MinVersion: VersionTLS13,
+			Bugs: ProtocolBugs{
+				DTLS13EchoSessionID: true,
+			},
+		},
+		shouldFail:    true,
+		expectedError: ":DECODE_ERROR:",
+	})
 }
 
 func addTestForCipherSuite(suite testCipherSuite, ver tlsVersion, protocol protocol) {
@@ -4884,6 +4903,7 @@
 	}
 
 	// TLS 1.3 basic handshake shapes. DTLS 1.3 isn't supported yet.
+	// TODO(crbug.com/boringssl/715): Enable these tests.
 	if config.protocol != dtls {
 		tests = append(tests, testCase{
 			name: "TLS13-1RTT-Client",
@@ -6154,8 +6174,8 @@
 		}
 	}
 	if config.protocol == dtls {
-		// TODO(davidben): DTLS 1.3 will want a similar thing for
-		// HelloRetryRequest.
+		// TODO(crbug.com/boringssl/715): DTLS 1.3 will want a similar
+		// thing for HelloRetryRequest.
 		tests = append(tests, testCase{
 			name: "SkipHelloVerifyRequest",
 			config: Config{
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index 73e307c..0862a00 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -111,11 +111,20 @@
   if (SSL_is_dtls(hs->ssl)) {
     server_hello_version = DTLS1_2_VERSION;
   }
+  // DTLS 1.3 disables "compatibility mode" (RFC 8446, appendix D.4). When
+  // disabled, servers MUST NOT echo the legacy_session_id (RFC 9147, section
+  // 5). The client could have sent a session ID indicating its willingness to
+  // resume a DTLS 1.2 session, so just checking that the session IDs match is
+  // incorrect.
+  bool session_id_match =
+      (SSL_is_dtls(hs->ssl) && CBS_len(&out->session_id) == 0) ||
+      (!SSL_is_dtls(hs->ssl) &&
+       CBS_mem_equal(&out->session_id, hs->session_id, hs->session_id_len));
+
   // The RFC8446 version of the structure fixes some legacy values.
   // Additionally, the session ID must echo the original one.
   if (out->legacy_version != server_hello_version ||
-      out->compression_method != 0 ||
-      !CBS_mem_equal(&out->session_id, hs->session_id, hs->session_id_len) ||
+      out->compression_method != 0 || !session_id_match ||
       CBS_len(&out->extensions) == 0) {
     OPENSSL_PUT_ERROR(SSL, SSL_R_DECODE_ERROR);
     *out_alert = SSL_AD_DECODE_ERROR;
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index d3147ea..f7804b2 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -244,9 +244,15 @@
     ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_ILLEGAL_PARAMETER);
     return ssl_hs_error;
   }
-  OPENSSL_memcpy(hs->session_id, client_hello.session_id,
-                 client_hello.session_id_len);
-  hs->session_id_len = client_hello.session_id_len;
+  // DTLS 1.3 disables compatibility mode, and even if the client advertised a
+  // session ID (for resumption in DTLS 1.2), the server "MUST NOT echo the
+  // 'legacy_session_id' value from the client" (RFC 9147, section 5) as it
+  // would in a TLS 1.3 handshake.
+  if (!SSL_is_dtls(ssl)) {
+    OPENSSL_memcpy(hs->session_id, client_hello.session_id,
+                   client_hello.session_id_len);
+    hs->session_id_len = client_hello.session_id_len;
+  }
 
   Array<SSL_CREDENTIAL *> creds;
   if (!ssl_get_credential_list(hs, &creds)) {