Enable more runner tests for QUIC

Change-Id: Id1922197c5218460210e6404ad60b60afc591984
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/40284
Reviewed-by: David Benjamin <davidben@google.com>
Commit-Queue: David Benjamin <davidben@google.com>
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index c9845f3..4e2fa50 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -534,7 +534,7 @@
     }
   }
 
-  if (!config->expect_quic_transport_params.empty()) {
+  if (!config->expect_quic_transport_params.empty() && expect_handshake_done) {
     const uint8_t *peer_params;
     size_t peer_params_len;
     SSL_get_peer_quic_transport_params(ssl, &peer_params, &peer_params_len);
diff --git a/ssl/test/mock_quic_transport.cc b/ssl/test/mock_quic_transport.cc
index 23445cb..0929432 100644
--- a/ssl/test/mock_quic_transport.cc
+++ b/ssl/test/mock_quic_transport.cc
@@ -23,23 +23,14 @@
 
 const uint8_t kTagHandshake = 'H';
 const uint8_t kTagApplication = 'A';
-
-bool write_header(BIO *bio, uint8_t tag, size_t len) {
-  uint8_t header[5];
-  header[0] = tag;
-  header[1] = (len >> 24) & 0xff;
-  header[2] = (len >> 16) & 0xff;
-  header[3] = (len >> 8) & 0xff;
-  header[4] = len & 0xff;
-  return BIO_write_all(bio, header, sizeof(header));
-}
+const uint8_t kTagAlert = 'L';
 
 }  // namespace
 
 MockQuicTransport::MockQuicTransport(bssl::UniquePtr<BIO> bio, SSL *ssl)
     : bio_(std::move(bio)),
-      read_secrets_(ssl_encryption_application + 1),
-      write_secrets_(ssl_encryption_application + 1),
+      read_levels_(ssl_encryption_application + 1),
+      write_levels_(ssl_encryption_application + 1),
       ssl_(ssl) {}
 
 bool MockQuicTransport::SetReadSecret(enum ssl_encryption_level_t level,
@@ -47,7 +38,8 @@
                                       const uint8_t *secret,
                                       size_t secret_len) {
   // TODO(davidben): Assert the various encryption secret invariants.
-  read_secrets_[level].assign(secret, secret + secret_len);
+  read_levels_[level].cipher = SSL_CIPHER_get_value(cipher);
+  read_levels_[level].secret.assign(secret, secret + secret_len);
   return true;
 }
 
@@ -56,7 +48,8 @@
                                        const uint8_t *secret,
                                        size_t secret_len) {
   // TODO(davidben): Assert the various encryption secret invariants.
-  write_secrets_[level].assign(secret, secret + secret_len);
+  write_levels_[level].cipher = SSL_CIPHER_get_value(cipher);
+  write_levels_[level].secret.assign(secret, secret + secret_len);
   return true;
 }
 
@@ -80,39 +73,53 @@
   return true;
 }
 
-bool ReadHeader(BIO *bio, uint8_t *out_tag, size_t *out_len) {
-  uint8_t header[5];
-  if (!ReadAll(bio, header)) {
+}  // namespace
+
+bool MockQuicTransport::ReadHeader(uint8_t *out_tag, size_t *out_len) {
+  uint8_t header[7];
+  if (!ReadAll(bio_.get(), header)) {
     return false;
   }
-
-  *out_len = header[1] << 24 | header[2] << 16 | header[3] << 8 | header[4];
   *out_tag = header[0];
+  uint16_t cipher_suite = header[1] << 8 | header[2];
+  size_t remaining_bytes =
+      header[3] << 24 | header[4] << 16 | header[5] << 8 | header[6];
+
+  enum ssl_encryption_level_t level = SSL_quic_read_level(ssl_);
+  if (*out_tag == kTagApplication) {
+    if (SSL_in_early_data(ssl_)) {
+      level = ssl_encryption_early_data;
+    } else {
+      level = ssl_encryption_application;
+    }
+  }
+  if (cipher_suite != read_levels_[level].cipher) {
+    return false;
+  }
+  const std::vector<uint8_t> &secret = read_levels_[level].secret;
+  std::vector<uint8_t> read_secret(secret.size());
+  if (remaining_bytes < secret.size()) {
+    return false;
+  }
+  remaining_bytes -= secret.size();
+  if (!ReadAll(bio_.get(), bssl::MakeSpan(read_secret)) ||
+      read_secret != secret) {
+    return false;
+  }
+  *out_len = remaining_bytes;
   return true;
 }
 
-}  // namespace
-
 bool MockQuicTransport::ReadHandshake() {
-  enum ssl_encryption_level_t level = SSL_quic_read_level(ssl_);
   uint8_t tag;
   size_t len;
-  if (!ReadHeader(bio_.get(), &tag, &len)) {
+  if (!ReadHeader(&tag, &len)) {
     return false;
   }
   if (tag != kTagHandshake) {
     return false;
   }
 
-  const std::vector<uint8_t> &secret = read_secrets_[level];
-  std::vector<uint8_t> read_secret(secret.size());
-  if (!ReadAll(bio_.get(), bssl::MakeSpan(read_secret))) {
-    return false;
-  }
-  if (read_secret != secret) {
-    return false;
-  }
-
   std::vector<uint8_t> buf(len);
   if (!ReadAll(bio_.get(), bssl::MakeSpan(buf))) {
     return false;
@@ -139,34 +146,36 @@
   uint8_t tag = 0;
   size_t len;
   while (true) {
-    if (!ReadHeader(bio_.get(), &tag, &len)) {
+    if (!ReadHeader(&tag, &len)) {
       // Assume that a failure to read the header means there's no more to read,
       // not an error reading.
       return 0;
     }
-    if (tag != kTagHandshake && tag != kTagApplication) {
-      return -1;
-    }
-    const std::vector<uint8_t> &secret =
-        read_secrets_[ssl_encryption_application];
-    std::vector<uint8_t> read_secret(secret.size());
-    if (!ReadAll(bio_.get(), bssl::MakeSpan(read_secret))) {
-      return -1;
-    }
-    if (read_secret != secret) {
-      return -1;
-    }
     if (tag == kTagApplication) {
       break;
     }
+    if (tag != kTagHandshake) {
+      return -1;
+    }
 
     std::vector<uint8_t> buf(len);
     if (!ReadAll(bio_.get(), bssl::MakeSpan(buf))) {
       return -1;
     }
     if (SSL_provide_quic_data(ssl_, SSL_quic_read_level(ssl_), buf.data(),
-                              buf.size()) != 1 ||
-        SSL_process_quic_post_handshake(ssl_) != 1) {
+                              buf.size()) != 1) {
+      return -1;
+    }
+    if (SSL_in_init(ssl_)) {
+      int ret = SSL_do_handshake(ssl_);
+      if (ret < 0) {
+        int ssl_err = SSL_get_error(ssl_, ret);
+        if (ssl_err == SSL_ERROR_WANT_READ) {
+          continue;
+        }
+        return -1;
+      }
+    } else if (SSL_process_quic_post_handshake(ssl_) != 1) {
       return -1;
     }
   }
@@ -188,26 +197,42 @@
   return len;
 }
 
+bool MockQuicTransport::WriteRecord(enum ssl_encryption_level_t level,
+                                    uint8_t tag, const uint8_t *data,
+                                    size_t len) {
+  uint16_t cipher_suite = write_levels_[level].cipher;
+  const std::vector<uint8_t> &secret = write_levels_[level].secret;
+  size_t tlv_len = secret.size() + len;
+  uint8_t header[7];
+  header[0] = tag;
+  header[1] = (cipher_suite >> 8) & 0xff;
+  header[2] = cipher_suite & 0xff;
+  header[3] = (tlv_len >> 24) & 0xff;
+  header[4] = (tlv_len >> 16) & 0xff;
+  header[5] = (tlv_len >> 8) & 0xff;
+  header[6] = tlv_len & 0xff;
+  return BIO_write_all(bio_.get(), header, sizeof(header)) &&
+         BIO_write_all(bio_.get(), secret.data(), secret.size()) &&
+         BIO_write_all(bio_.get(), data, len);
+}
+
 bool MockQuicTransport::WriteHandshakeData(enum ssl_encryption_level_t level,
                                            const uint8_t *data, size_t len) {
-  const std::vector<uint8_t> &secret = write_secrets_[level];
-  if (!write_header(bio_.get(), kTagHandshake, len) ||
-      BIO_write_all(bio_.get(), secret.data(), secret.size()) != 1 ||
-      BIO_write_all(bio_.get(), data, len) != 1) {
-    return false;
-  }
-  return true;
+  return WriteRecord(level, kTagHandshake, data, len);
 }
 
 bool MockQuicTransport::WriteApplicationData(const uint8_t *in, size_t len) {
-  const std::vector<uint8_t> &secret =
-      write_secrets_[ssl_encryption_application];
-  if (!write_header(bio_.get(), kTagApplication, len) ||
-      BIO_write_all(bio_.get(), secret.data(), secret.size()) != 1 ||
-      BIO_write_all(bio_.get(), in, len) != 1) {
-    return false;
+  enum ssl_encryption_level_t level = ssl_encryption_application;
+  if (SSL_in_early_data(ssl_) && !SSL_is_server(ssl_)) {
+    level = ssl_encryption_early_data;
   }
-  return true;
+  return WriteRecord(level, kTagApplication, in, len);
 }
 
 bool MockQuicTransport::Flush() { return BIO_flush(bio_.get()); }
+
+bool MockQuicTransport::SendAlert(enum ssl_encryption_level_t level,
+                                  uint8_t alert) {
+  uint8_t alert_msg[] = {2, alert};
+  return WriteRecord(level, kTagAlert, alert_msg, sizeof(alert_msg));
+}
diff --git a/ssl/test/mock_quic_transport.h b/ssl/test/mock_quic_transport.h
index 6dfed5b..a56652d 100644
--- a/ssl/test/mock_quic_transport.h
+++ b/ssl/test/mock_quic_transport.h
@@ -39,15 +39,35 @@
   int ReadApplicationData(uint8_t *out, size_t max_out);
   bool WriteApplicationData(const uint8_t *in, size_t len);
   bool Flush();
+  bool SendAlert(enum ssl_encryption_level_t level, uint8_t alert);
 
  private:
+  // Reads a record header from |bio_| and returns whether the record was read
+  // successfully. As part of reading the header, this function checks that the
+  // cipher suite and secret in the header are correct. On success, the tag
+  // indicating the TLS record type is put in  |*out_tag|, the length of the TLS
+  // record is put in |*out_len|, and the next thing to be read from |bio_| is
+  // |*out_len| bytes of the TLS record.
+  bool ReadHeader(uint8_t *out_tag, size_t *out_len);
+
+  // Writes a MockQuicTransport record to |bio_| at encryption level |level|
+  // with record type |tag| and a TLS record payload of length |len| from
+  // |data|.
+  bool WriteRecord(enum ssl_encryption_level_t level, uint8_t tag,
+                   const uint8_t *data, size_t len);
+
   bssl::UniquePtr<BIO> bio_;
 
   std::vector<uint8_t> pending_app_data_;
   size_t app_data_offset_;
 
-  std::vector<std::vector<uint8_t>> read_secrets_;
-  std::vector<std::vector<uint8_t>> write_secrets_;
+  struct EncryptionLevel {
+    uint16_t cipher;
+    std::vector<uint8_t> secret;
+  };
+
+  std::vector<EncryptionLevel> read_levels_;
+  std::vector<EncryptionLevel> write_levels_;
 
   SSL *ssl_;  // Unowned.
 };
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 0142ff6..a449f2f 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -761,6 +761,7 @@
 	}
 	if c.config.Bugs.MockQUICTransport != nil {
 		c.config.Bugs.MockQUICTransport.readSecret = secret
+		c.config.Bugs.MockQUICTransport.readCipherSuite = suite.id
 	}
 	c.in.useTrafficSecret(version, suite, secret, side)
 	c.seenHandshakePackEnd = false
@@ -774,10 +775,26 @@
 	}
 	if c.config.Bugs.MockQUICTransport != nil {
 		c.config.Bugs.MockQUICTransport.writeSecret = secret
+		c.config.Bugs.MockQUICTransport.writeCipherSuite = suite.id
 	}
 	c.out.useTrafficSecret(version, suite, secret, side)
 }
 
+func (c *Conn) setSkipEarlyData() {
+	if c.config.Bugs.MockQUICTransport != nil {
+		c.config.Bugs.MockQUICTransport.skipEarlyData = true
+	} else {
+		c.skipEarlyData = true
+	}
+}
+
+func (c *Conn) shouldSkipEarlyData() bool {
+	if c.config.Bugs.MockQUICTransport != nil {
+		return c.config.Bugs.MockQUICTransport.skipEarlyData
+	}
+	return c.skipEarlyData
+}
+
 func (c *Conn) doReadRecord(want recordType) (recordType, *block, error) {
 RestartReadRecord:
 	if c.isDTLS {
@@ -904,6 +921,9 @@
 }
 
 func (c *Conn) readTLS13ChangeCipherSpec() error {
+	if c.config.Bugs.MockQUICTransport != nil {
+		return nil
+	}
 	if !c.expectTLS13ChangeCipherSpec {
 		panic("c.expectTLS13ChangeCipherSpec not set")
 	}
@@ -964,7 +984,7 @@
 		break
 	}
 
-	if c.expectTLS13ChangeCipherSpec && c.config.Bugs.MockQUICTransport == nil {
+	if c.expectTLS13ChangeCipherSpec {
 		if err := c.readTLS13ChangeCipherSpec(); err != nil {
 			return err
 		}
@@ -1986,6 +2006,9 @@
 		ticketNonce:                 nonce,
 		maxEarlyDataSize:            c.config.MaxEarlyDataSize,
 	}
+	if c.config.Bugs.MockQUICTransport != nil && m.maxEarlyDataSize > 0 {
+		m.maxEarlyDataSize = 0xffffffff
+	}
 
 	if c.config.Bugs.SendTicketLifetime != 0 {
 		m.ticketLifetime = uint32(c.config.Bugs.SendTicketLifetime / time.Second)
diff --git a/ssl/test/runner/handshake_server.go b/ssl/test/runner/handshake_server.go
index 28523f0..3d13b9b 100644
--- a/ssl/test/runner/handshake_server.go
+++ b/ssl/test/runner/handshake_server.go
@@ -621,7 +621,7 @@
 		}
 
 		if hs.clientHello.hasEarlyData {
-			c.skipEarlyData = true
+			c.setSkipEarlyData()
 		}
 
 		// Read new ClientHello.
@@ -737,7 +737,7 @@
 				c.input = nil
 			}
 		} else {
-			c.skipEarlyData = true
+			c.setSkipEarlyData()
 		}
 	}
 
@@ -988,7 +988,7 @@
 	}
 	c.flushHandshake()
 
-	if encryptedExtensions.extensions.hasEarlyData && !c.skipEarlyData {
+	if encryptedExtensions.extensions.hasEarlyData && !c.shouldSkipEarlyData() {
 		for _, expectedMsg := range config.Bugs.ExpectLateEarlyData {
 			if err := c.readRecord(recordTypeApplicationData); err != nil {
 				return err
@@ -1022,7 +1022,7 @@
 	}
 
 	// Read end_of_early_data.
-	if encryptedExtensions.extensions.hasEarlyData {
+	if encryptedExtensions.extensions.hasEarlyData && config.Bugs.MockQUICTransport == nil {
 		msg, err := c.readHandshake()
 		if err != nil {
 			return err
diff --git a/ssl/test/runner/mock_quic_transport.go b/ssl/test/runner/mock_quic_transport.go
index 4d971c4..27a8bc3 100644
--- a/ssl/test/runner/mock_quic_transport.go
+++ b/ssl/test/runner/mock_quic_transport.go
@@ -24,10 +24,28 @@
 
 const tagHandshake = byte('H')
 const tagApplication = byte('A')
+const tagAlert = byte('L')
 
+// mockQUICTransport provides a record layer for sending/receiving messages
+// when testing TLS over QUIC. It is only intended for testing, as it runs over
+// an in-order reliable transport, looks nothing like the QUIC wire image, and
+// provides no confidentiality guarantees. (In fact, it leaks keys in the
+// clear.)
+//
+// Messages from TLS that are sent over a mockQUICTransport are wrapped in a
+// TLV-like format. The first byte of a mockQUICTransport message is a tag
+// indicating the TLS record type. This is followed by the 2 byte cipher suite
+// ID of the cipher suite that would have been used to encrypt the record. Next
+// is a 4-byte big-endian length indicating the length of the remaining payload.
+// The payload starts with the key that would be used to encrypt the record, and
+// the remainder of the payload is the plaintext of the TLS record. Note that
+// the 4-byte length covers the length of the key and plaintext, but not the
+// cipher suite ID or tag.
 type mockQUICTransport struct {
 	net.Conn
-	readSecret, writeSecret []byte
+	readSecret, writeSecret           []byte
+	readCipherSuite, writeCipherSuite uint16
+	skipEarlyData                     bool
 }
 
 func newMockQUICTransport(conn net.Conn) *mockQUICTransport {
@@ -35,24 +53,39 @@
 }
 
 func (m *mockQUICTransport) read() (byte, []byte, error) {
-	header := make([]byte, 5)
-	if _, err := io.ReadFull(m.Conn, header); err != nil {
-		return 0, nil, err
+	for {
+		header := make([]byte, 7)
+		if _, err := io.ReadFull(m.Conn, header); err != nil {
+			return 0, nil, err
+		}
+		cipherSuite := binary.BigEndian.Uint16(header[1:3])
+		length := binary.BigEndian.Uint32(header[3:])
+		value := make([]byte, length)
+		if _, err := io.ReadFull(m.Conn, value); err != nil {
+			return 0, nil, fmt.Errorf("Error reading record")
+		}
+		if cipherSuite != m.readCipherSuite {
+			if m.skipEarlyData {
+				continue
+			}
+			return 0, nil, fmt.Errorf("Received cipher suite %d does not match expected %d", cipherSuite, m.readCipherSuite)
+		}
+		if len(m.readSecret) > len(value) {
+			return 0, nil, fmt.Errorf("Input length too short")
+		}
+		secret := value[:len(m.readSecret)]
+		out := value[len(m.readSecret):]
+		if !bytes.Equal(secret, m.readSecret) {
+			if m.skipEarlyData {
+				continue
+			}
+			return 0, nil, fmt.Errorf("secrets don't match: got %x but expected %x", secret, m.readSecret)
+		}
+		if m.skipEarlyData && header[0] == tagHandshake {
+			m.skipEarlyData = false
+		}
+		return header[0], out, nil
 	}
-	var length uint32
-	binary.Read(bytes.NewBuffer(header[1:]), binary.BigEndian, &length)
-	secret := make([]byte, len(m.readSecret))
-	if _, err := io.ReadFull(m.Conn, secret); err != nil {
-		return 0, nil, err
-	}
-	if !bytes.Equal(secret, m.readSecret) {
-		return 0, nil, fmt.Errorf("secrets don't match")
-	}
-	out := make([]byte, int(length))
-	if _, err := io.ReadFull(m.Conn, out); err != nil {
-		return 0, nil, err
-	}
-	return header[0], out, nil
 }
 
 func (m *mockQUICTransport) readRecord(want recordType) (recordType, *block, error) {
@@ -65,6 +98,8 @@
 		returnType = recordTypeHandshake
 	} else if typ == tagApplication {
 		returnType = recordTypeApplicationData
+	} else if typ == tagAlert {
+		returnType = recordTypeAlert
 	} else {
 		return 0, nil, fmt.Errorf("unknown type %d\n", typ)
 	}
@@ -78,11 +113,13 @@
 	} else if typ != recordTypeHandshake {
 		return 0, fmt.Errorf("unsupported record type %d\n", typ)
 	}
-	payload := make([]byte, 1+4+len(m.writeSecret)+len(data))
+	length := len(m.writeSecret) + len(data)
+	payload := make([]byte, 1+2+4+length)
 	payload[0] = tag
-	binary.BigEndian.PutUint32(payload[1:5], uint32(len(data)))
-	copy(payload[5:], m.writeSecret)
-	copy(payload[5+len(m.writeSecret):], data)
+	binary.BigEndian.PutUint16(payload[1:3], m.writeCipherSuite)
+	binary.BigEndian.PutUint32(payload[3:7], uint32(length))
+	copy(payload[7:], m.writeSecret)
+	copy(payload[7+len(m.writeSecret):], data)
 	if _, err := m.Conn.Write(payload); err != nil {
 		return 0, err
 	}
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index 6a00a64..eda922d 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -3496,7 +3496,9 @@
 		expectedError = ":DECRYPTION_FAILED_OR_BAD_RECORD_MAC:"
 	}
 
-	// TODO(nharper): Consider enabling this test for QUIC.
+	// When QUIC is used, the QUIC stack handles record encryption/decryption.
+	// Thus it is not possible for the TLS stack in QUIC mode to receive a
+	// bad record (i.e. one that fails to decrypt).
 	if protocol != quic {
 		testCases = append(testCases, testCase{
 			protocol: protocol,
@@ -4435,18 +4437,24 @@
 // various conditions. Some of these are redundant with other tests, but they
 // only cover the synchronous case.
 func addAllStateMachineCoverageTests() {
-	// TODO(nharper): Add QUIC support for these tests?
 	for _, async := range []bool{false, true} {
-		for _, protocol := range []protocol{tls, dtls} {
+		for _, protocol := range []protocol{tls, dtls, quic} {
+			if protocol == quic && async == true {
+				// QUIC doesn't work with async mode.
+				continue
+			}
 			addStateMachineCoverageTests(stateMachineTestConfig{
 				protocol: protocol,
 				async:    async,
 			})
-			addStateMachineCoverageTests(stateMachineTestConfig{
-				protocol:          protocol,
-				async:             async,
-				implicitHandshake: true,
-			})
+			// QUIC doesn't work with implicit handshakes.
+			if protocol != quic {
+				addStateMachineCoverageTests(stateMachineTestConfig{
+					protocol:          protocol,
+					async:             async,
+					implicitHandshake: true,
+				})
+			}
 			addStateMachineCoverageTests(stateMachineTestConfig{
 				protocol:       protocol,
 				async:          async,
@@ -4466,73 +4474,77 @@
 
 	// Basic handshake, with resumption. Client and server,
 	// session ID and session ticket.
-	tests = append(tests, testCase{
-		name: "Basic-Client",
-		config: Config{
-			MaxVersion: VersionTLS12,
-		},
-		resumeSession: true,
-		// Ensure session tickets are used, not session IDs.
-		noSessionCache: true,
-		flags:          []string{"-expect-no-hrr"},
-	})
-	tests = append(tests, testCase{
-		name: "Basic-Client-RenewTicket",
-		config: Config{
-			MaxVersion: VersionTLS12,
-			Bugs: ProtocolBugs{
-				RenewTicketOnResume: true,
+	// The following tests have a max version of 1.2, so they are not suitable
+	// for use with QUIC.
+	if config.protocol != quic {
+		tests = append(tests, testCase{
+			name: "Basic-Client",
+			config: Config{
+				MaxVersion: VersionTLS12,
 			},
-		},
-		flags:                []string{"-expect-ticket-renewal"},
-		resumeSession:        true,
-		resumeRenewedSession: true,
-	})
-	tests = append(tests, testCase{
-		name: "Basic-Client-NoTicket",
-		config: Config{
-			MaxVersion:             VersionTLS12,
-			SessionTicketsDisabled: true,
-		},
-		resumeSession: true,
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "Basic-Server",
-		config: Config{
-			MaxVersion: VersionTLS12,
-			Bugs: ProtocolBugs{
-				RequireSessionTickets: true,
+			resumeSession: true,
+			// Ensure session tickets are used, not session IDs.
+			noSessionCache: true,
+			flags:          []string{"-expect-no-hrr"},
+		})
+		tests = append(tests, testCase{
+			name: "Basic-Client-RenewTicket",
+			config: Config{
+				MaxVersion: VersionTLS12,
+				Bugs: ProtocolBugs{
+					RenewTicketOnResume: true,
+				},
 			},
-		},
-		resumeSession: true,
-		flags: []string{
-			"-expect-no-session-id",
-			"-expect-no-hrr",
-		},
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "Basic-Server-NoTickets",
-		config: Config{
-			MaxVersion:             VersionTLS12,
-			SessionTicketsDisabled: true,
-		},
-		resumeSession: true,
-		flags:         []string{"-expect-session-id"},
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "Basic-Server-EarlyCallback",
-		config: Config{
-			MaxVersion: VersionTLS12,
-		},
-		flags:         []string{"-use-early-callback"},
-		resumeSession: true,
-	})
+			flags:                []string{"-expect-ticket-renewal"},
+			resumeSession:        true,
+			resumeRenewedSession: true,
+		})
+		tests = append(tests, testCase{
+			name: "Basic-Client-NoTicket",
+			config: Config{
+				MaxVersion:             VersionTLS12,
+				SessionTicketsDisabled: true,
+			},
+			resumeSession: true,
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "Basic-Server",
+			config: Config{
+				MaxVersion: VersionTLS12,
+				Bugs: ProtocolBugs{
+					RequireSessionTickets: true,
+				},
+			},
+			resumeSession: true,
+			flags: []string{
+				"-expect-no-session-id",
+				"-expect-no-hrr",
+			},
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "Basic-Server-NoTickets",
+			config: Config{
+				MaxVersion:             VersionTLS12,
+				SessionTicketsDisabled: true,
+			},
+			resumeSession: true,
+			flags:         []string{"-expect-session-id"},
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "Basic-Server-EarlyCallback",
+			config: Config{
+				MaxVersion: VersionTLS12,
+			},
+			flags:         []string{"-use-early-callback"},
+			resumeSession: true,
+		})
+	}
 
-	// TLS 1.3 basic handshake shapes.
-	if config.protocol == tls {
+	// TLS 1.3 basic handshake shapes. DTLS 1.3 isn't supported yet.
+	if config.protocol != dtls {
 		tests = append(tests, testCase{
 			name: "TLS13-1RTT-Client",
 			config: Config{
@@ -4594,31 +4606,34 @@
 			flags:         []string{"-expect-hrr"},
 		})
 
-		tests = append(tests, testCase{
-			testType: clientTest,
-			name:     "TLS13-EarlyData-TooMuchData-Client",
-			config: Config{
-				MaxVersion:       VersionTLS13,
-				MinVersion:       VersionTLS13,
-				MaxEarlyDataSize: 2,
-			},
-			resumeConfig: &Config{
-				MaxVersion:       VersionTLS13,
-				MinVersion:       VersionTLS13,
-				MaxEarlyDataSize: 2,
-				Bugs: ProtocolBugs{
-					ExpectEarlyData: [][]byte{{'h', 'e'}},
+		// Tests that specify a MaxEarlyDataSize don't work with QUIC.
+		if config.protocol != quic {
+			tests = append(tests, testCase{
+				testType: clientTest,
+				name:     "TLS13-EarlyData-TooMuchData-Client",
+				config: Config{
+					MaxVersion:       VersionTLS13,
+					MinVersion:       VersionTLS13,
+					MaxEarlyDataSize: 2,
 				},
-			},
-			resumeShimPrefix: "llo",
-			resumeSession:    true,
-			flags: []string{
-				"-enable-early-data",
-				"-expect-ticket-supports-early-data",
-				"-on-resume-expect-accept-early-data",
-				"-on-resume-shim-writes-first",
-			},
-		})
+				resumeConfig: &Config{
+					MaxVersion:       VersionTLS13,
+					MinVersion:       VersionTLS13,
+					MaxEarlyDataSize: 2,
+					Bugs: ProtocolBugs{
+						ExpectEarlyData: [][]byte{{'h', 'e'}},
+					},
+				},
+				resumeShimPrefix: "llo",
+				resumeSession:    true,
+				flags: []string{
+					"-enable-early-data",
+					"-expect-ticket-supports-early-data",
+					"-on-resume-expect-accept-early-data",
+					"-on-resume-shim-writes-first",
+				},
+			})
+		}
 
 		// Unfinished writes can only be tested when operations are async. EarlyData
 		// can't be tested as part of an ImplicitHandshake in this case since
@@ -4680,47 +4695,54 @@
 			})
 		}
 
-		tests = append(tests, testCase{
-			testType: serverTest,
-			name:     "TLS13-MaxEarlyData-Server",
-			config: Config{
-				MaxVersion: VersionTLS13,
-				MinVersion: VersionTLS13,
-				Bugs: ProtocolBugs{
-					SendEarlyData:           [][]byte{bytes.Repeat([]byte{1}, 14336+1)},
-					ExpectEarlyDataAccepted: true,
+		// Early data has no size limit in QUIC.
+		if config.protocol != quic {
+			tests = append(tests, testCase{
+				testType: serverTest,
+				name:     "TLS13-MaxEarlyData-Server",
+				config: Config{
+					MaxVersion: VersionTLS13,
+					MinVersion: VersionTLS13,
+					Bugs: ProtocolBugs{
+						SendEarlyData:           [][]byte{bytes.Repeat([]byte{1}, 14336+1)},
+						ExpectEarlyDataAccepted: true,
+					},
 				},
-			},
-			messageCount:  2,
-			resumeSession: true,
-			flags: []string{
-				"-enable-early-data",
-				"-on-resume-expect-accept-early-data",
-			},
-			shouldFail:    true,
-			expectedError: ":TOO_MUCH_READ_EARLY_DATA:",
-		})
+				messageCount:  2,
+				resumeSession: true,
+				flags: []string{
+					"-enable-early-data",
+					"-on-resume-expect-accept-early-data",
+				},
+				shouldFail:    true,
+				expectedError: ":TOO_MUCH_READ_EARLY_DATA:",
+			})
+		}
 	}
 
 	// TLS client auth.
-	tests = append(tests, testCase{
-		testType: clientTest,
-		name:     "ClientAuth-NoCertificate-Client",
-		config: Config{
-			MaxVersion: VersionTLS12,
-			ClientAuth: RequestClientCert,
-		},
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "ClientAuth-NoCertificate-Server",
-		config: Config{
-			MaxVersion: VersionTLS12,
-		},
-		// Setting SSL_VERIFY_PEER allows anonymous clients.
-		flags: []string{"-verify-peer"},
-	})
-	if config.protocol == tls {
+	// The following tests have a max version of 1.2, so they are not suitable
+	// for use with QUIC.
+	if config.protocol != quic {
+		tests = append(tests, testCase{
+			testType: clientTest,
+			name:     "ClientAuth-NoCertificate-Client",
+			config: Config{
+				MaxVersion: VersionTLS12,
+				ClientAuth: RequestClientCert,
+			},
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "ClientAuth-NoCertificate-Server",
+			config: Config{
+				MaxVersion: VersionTLS12,
+			},
+			// Setting SSL_VERIFY_PEER allows anonymous clients.
+			flags: []string{"-verify-peer"},
+		})
+	}
+	if config.protocol != dtls {
 		tests = append(tests, testCase{
 			testType: clientTest,
 			name:     "ClientAuth-NoCertificate-Client-TLS13",
@@ -4739,18 +4761,20 @@
 			flags: []string{"-verify-peer"},
 		})
 	}
-	tests = append(tests, testCase{
-		testType: clientTest,
-		name:     "ClientAuth-RSA-Client",
-		config: Config{
-			MaxVersion: VersionTLS12,
-			ClientAuth: RequireAnyClientCert,
-		},
-		flags: []string{
-			"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
-			"-key-file", path.Join(*resourceDir, rsaKeyFile),
-		},
-	})
+	if config.protocol != quic {
+		tests = append(tests, testCase{
+			testType: clientTest,
+			name:     "ClientAuth-RSA-Client",
+			config: Config{
+				MaxVersion: VersionTLS12,
+				ClientAuth: RequireAnyClientCert,
+			},
+			flags: []string{
+				"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
+				"-key-file", path.Join(*resourceDir, rsaKeyFile),
+			},
+		})
+	}
 	tests = append(tests, testCase{
 		testType: clientTest,
 		name:     "ClientAuth-RSA-Client-TLS13",
@@ -4763,18 +4787,20 @@
 			"-key-file", path.Join(*resourceDir, rsaKeyFile),
 		},
 	})
-	tests = append(tests, testCase{
-		testType: clientTest,
-		name:     "ClientAuth-ECDSA-Client",
-		config: Config{
-			MaxVersion: VersionTLS12,
-			ClientAuth: RequireAnyClientCert,
-		},
-		flags: []string{
-			"-cert-file", path.Join(*resourceDir, ecdsaP256CertificateFile),
-			"-key-file", path.Join(*resourceDir, ecdsaP256KeyFile),
-		},
-	})
+	if config.protocol != quic {
+		tests = append(tests, testCase{
+			testType: clientTest,
+			name:     "ClientAuth-ECDSA-Client",
+			config: Config{
+				MaxVersion: VersionTLS12,
+				ClientAuth: RequireAnyClientCert,
+			},
+			flags: []string{
+				"-cert-file", path.Join(*resourceDir, ecdsaP256CertificateFile),
+				"-key-file", path.Join(*resourceDir, ecdsaP256KeyFile),
+			},
+		})
+	}
 	tests = append(tests, testCase{
 		testType: clientTest,
 		name:     "ClientAuth-ECDSA-Client-TLS13",
@@ -4787,15 +4813,17 @@
 			"-key-file", path.Join(*resourceDir, ecdsaP256KeyFile),
 		},
 	})
-	tests = append(tests, testCase{
-		testType: clientTest,
-		name:     "ClientAuth-NoCertificate-OldCallback",
-		config: Config{
-			MaxVersion: VersionTLS12,
-			ClientAuth: RequestClientCert,
-		},
-		flags: []string{"-use-old-client-cert-callback"},
-	})
+	if config.protocol != quic {
+		tests = append(tests, testCase{
+			testType: clientTest,
+			name:     "ClientAuth-NoCertificate-OldCallback",
+			config: Config{
+				MaxVersion: VersionTLS12,
+				ClientAuth: RequestClientCert,
+			},
+			flags: []string{"-use-old-client-cert-callback"},
+		})
+	}
 	tests = append(tests, testCase{
 		testType: clientTest,
 		name:     "ClientAuth-NoCertificate-OldCallback-TLS13",
@@ -4805,19 +4833,21 @@
 		},
 		flags: []string{"-use-old-client-cert-callback"},
 	})
-	tests = append(tests, testCase{
-		testType: clientTest,
-		name:     "ClientAuth-OldCallback",
-		config: Config{
-			MaxVersion: VersionTLS12,
-			ClientAuth: RequireAnyClientCert,
-		},
-		flags: []string{
-			"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
-			"-key-file", path.Join(*resourceDir, rsaKeyFile),
-			"-use-old-client-cert-callback",
-		},
-	})
+	if config.protocol != quic {
+		tests = append(tests, testCase{
+			testType: clientTest,
+			name:     "ClientAuth-OldCallback",
+			config: Config{
+				MaxVersion: VersionTLS12,
+				ClientAuth: RequireAnyClientCert,
+			},
+			flags: []string{
+				"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
+				"-key-file", path.Join(*resourceDir, rsaKeyFile),
+				"-use-old-client-cert-callback",
+			},
+		})
+	}
 	tests = append(tests, testCase{
 		testType: clientTest,
 		name:     "ClientAuth-OldCallback-TLS13",
@@ -4831,15 +4861,17 @@
 			"-use-old-client-cert-callback",
 		},
 	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "ClientAuth-Server",
-		config: Config{
-			MaxVersion:   VersionTLS12,
-			Certificates: []Certificate{rsaCertificate},
-		},
-		flags: []string{"-require-any-client-certificate"},
-	})
+	if config.protocol != quic {
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "ClientAuth-Server",
+			config: Config{
+				MaxVersion:   VersionTLS12,
+				Certificates: []Certificate{rsaCertificate},
+			},
+			flags: []string{"-require-any-client-certificate"},
+		})
+	}
 	tests = append(tests, testCase{
 		testType: serverTest,
 		name:     "ClientAuth-Server-TLS13",
@@ -4851,94 +4883,96 @@
 	})
 
 	// Test each key exchange on the server side for async keys.
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "Basic-Server-RSA",
-		config: Config{
-			MaxVersion:   VersionTLS12,
-			CipherSuites: []uint16{TLS_RSA_WITH_AES_128_GCM_SHA256},
-		},
-		flags: []string{
-			"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
-			"-key-file", path.Join(*resourceDir, rsaKeyFile),
-		},
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "Basic-Server-ECDHE-RSA",
-		config: Config{
-			MaxVersion:   VersionTLS12,
-			CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
-		},
-		flags: []string{
-			"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
-			"-key-file", path.Join(*resourceDir, rsaKeyFile),
-		},
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "Basic-Server-ECDHE-ECDSA",
-		config: Config{
-			MaxVersion:   VersionTLS12,
-			CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
-		},
-		flags: []string{
-			"-cert-file", path.Join(*resourceDir, ecdsaP256CertificateFile),
-			"-key-file", path.Join(*resourceDir, ecdsaP256KeyFile),
-		},
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "Basic-Server-Ed25519",
-		config: Config{
-			MaxVersion:   VersionTLS12,
-			CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
-		},
-		flags: []string{
-			"-cert-file", path.Join(*resourceDir, ed25519CertificateFile),
-			"-key-file", path.Join(*resourceDir, ed25519KeyFile),
-			"-verify-prefs", strconv.Itoa(int(signatureEd25519)),
-		},
-	})
+	if config.protocol != quic {
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "Basic-Server-RSA",
+			config: Config{
+				MaxVersion:   VersionTLS12,
+				CipherSuites: []uint16{TLS_RSA_WITH_AES_128_GCM_SHA256},
+			},
+			flags: []string{
+				"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
+				"-key-file", path.Join(*resourceDir, rsaKeyFile),
+			},
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "Basic-Server-ECDHE-RSA",
+			config: Config{
+				MaxVersion:   VersionTLS12,
+				CipherSuites: []uint16{TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
+			},
+			flags: []string{
+				"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
+				"-key-file", path.Join(*resourceDir, rsaKeyFile),
+			},
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "Basic-Server-ECDHE-ECDSA",
+			config: Config{
+				MaxVersion:   VersionTLS12,
+				CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+			},
+			flags: []string{
+				"-cert-file", path.Join(*resourceDir, ecdsaP256CertificateFile),
+				"-key-file", path.Join(*resourceDir, ecdsaP256KeyFile),
+			},
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "Basic-Server-Ed25519",
+			config: Config{
+				MaxVersion:   VersionTLS12,
+				CipherSuites: []uint16{TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256},
+			},
+			flags: []string{
+				"-cert-file", path.Join(*resourceDir, ed25519CertificateFile),
+				"-key-file", path.Join(*resourceDir, ed25519KeyFile),
+				"-verify-prefs", strconv.Itoa(int(signatureEd25519)),
+			},
+		})
 
-	// No session ticket support; server doesn't send NewSessionTicket.
-	tests = append(tests, testCase{
-		name: "SessionTicketsDisabled-Client",
-		config: Config{
-			MaxVersion:             VersionTLS12,
-			SessionTicketsDisabled: true,
-		},
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "SessionTicketsDisabled-Server",
-		config: Config{
-			MaxVersion:             VersionTLS12,
-			SessionTicketsDisabled: true,
-		},
-	})
+		// No session ticket support; server doesn't send NewSessionTicket.
+		tests = append(tests, testCase{
+			name: "SessionTicketsDisabled-Client",
+			config: Config{
+				MaxVersion:             VersionTLS12,
+				SessionTicketsDisabled: true,
+			},
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "SessionTicketsDisabled-Server",
+			config: Config{
+				MaxVersion:             VersionTLS12,
+				SessionTicketsDisabled: true,
+			},
+		})
 
-	// Skip ServerKeyExchange in PSK key exchange if there's no
-	// identity hint.
-	tests = append(tests, testCase{
-		name: "EmptyPSKHint-Client",
-		config: Config{
-			MaxVersion:   VersionTLS12,
-			CipherSuites: []uint16{TLS_PSK_WITH_AES_128_CBC_SHA},
-			PreSharedKey: []byte("secret"),
-		},
-		flags: []string{"-psk", "secret"},
-	})
-	tests = append(tests, testCase{
-		testType: serverTest,
-		name:     "EmptyPSKHint-Server",
-		config: Config{
-			MaxVersion:   VersionTLS12,
-			CipherSuites: []uint16{TLS_PSK_WITH_AES_128_CBC_SHA},
-			PreSharedKey: []byte("secret"),
-		},
-		flags: []string{"-psk", "secret"},
-	})
+		// Skip ServerKeyExchange in PSK key exchange if there's no
+		// identity hint.
+		tests = append(tests, testCase{
+			name: "EmptyPSKHint-Client",
+			config: Config{
+				MaxVersion:   VersionTLS12,
+				CipherSuites: []uint16{TLS_PSK_WITH_AES_128_CBC_SHA},
+				PreSharedKey: []byte("secret"),
+			},
+			flags: []string{"-psk", "secret"},
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "EmptyPSKHint-Server",
+			config: Config{
+				MaxVersion:   VersionTLS12,
+				CipherSuites: []uint16{TLS_PSK_WITH_AES_128_CBC_SHA},
+				PreSharedKey: []byte("secret"),
+			},
+			flags: []string{"-psk", "secret"},
+		})
+	}
 
 	// OCSP stapling tests.
 	for _, vers := range tlsVersions {
@@ -5530,8 +5564,58 @@
 			},
 		})
 
+		// Channel ID and NPN at the same time, to ensure their relative
+		// ordering is correct.
+		tests = append(tests, testCase{
+			name: "ChannelID-NPN-Client",
+			config: Config{
+				MaxVersion:       VersionTLS12,
+				RequestChannelID: true,
+				NextProtos:       []string{"foo"},
+			},
+			flags: []string{
+				"-send-channel-id", path.Join(*resourceDir, channelIDKeyFile),
+				"-select-next-proto", "foo",
+			},
+			resumeSession:         true,
+			expectChannelID:       true,
+			expectedNextProto:     "foo",
+			expectedNextProtoType: npn,
+		})
+		tests = append(tests, testCase{
+			testType: serverTest,
+			name:     "ChannelID-NPN-Server",
+			config: Config{
+				MaxVersion: VersionTLS12,
+				ChannelID:  channelIDKey,
+				NextProtos: []string{"bar"},
+			},
+			flags: []string{
+				"-expect-channel-id",
+				base64.StdEncoding.EncodeToString(channelIDBytes),
+				"-advertise-npn", "\x03foo\x03bar\x03baz",
+				"-expect-next-proto", "bar",
+			},
+			resumeSession:         true,
+			expectChannelID:       true,
+			expectedNextProto:     "bar",
+			expectedNextProtoType: npn,
+		})
+
+		// Bidirectional shutdown with the runner initiating.
+		tests = append(tests, testCase{
+			name: "Shutdown-Runner",
+			config: Config{
+				Bugs: ProtocolBugs{
+					ExpectCloseNotify: true,
+				},
+			},
+			flags: []string{"-check-close-notify"},
+		})
+	}
+	if config.protocol != dtls {
 		// Test Channel ID
-		for _, ver := range tlsVersions {
+		for _, ver := range allVersions(config.protocol) {
 			if ver.version < VersionTLS10 {
 				continue
 			}
@@ -5609,180 +5693,137 @@
 			}
 		}
 
-		// Channel ID and NPN at the same time, to ensure their relative
-		// ordering is correct.
-		tests = append(tests, testCase{
-			name: "ChannelID-NPN-Client",
-			config: Config{
-				MaxVersion:       VersionTLS12,
-				RequestChannelID: true,
-				NextProtos:       []string{"foo"},
-			},
-			flags: []string{
-				"-send-channel-id", path.Join(*resourceDir, channelIDKeyFile),
-				"-select-next-proto", "foo",
-			},
-			resumeSession:         true,
-			expectChannelID:       true,
-			expectedNextProto:     "foo",
-			expectedNextProtoType: npn,
-		})
-		tests = append(tests, testCase{
-			testType: serverTest,
-			name:     "ChannelID-NPN-Server",
-			config: Config{
-				MaxVersion: VersionTLS12,
-				ChannelID:  channelIDKey,
-				NextProtos: []string{"bar"},
-			},
-			flags: []string{
-				"-expect-channel-id",
-				base64.StdEncoding.EncodeToString(channelIDBytes),
-				"-advertise-npn", "\x03foo\x03bar\x03baz",
-				"-expect-next-proto", "bar",
-			},
-			resumeSession:         true,
-			expectChannelID:       true,
-			expectedNextProto:     "bar",
-			expectedNextProtoType: npn,
-		})
-
-		// Bidirectional shutdown with the runner initiating.
-		tests = append(tests, testCase{
-			name: "Shutdown-Runner",
-			config: Config{
-				Bugs: ProtocolBugs{
-					ExpectCloseNotify: true,
-				},
-			},
-			flags: []string{"-check-close-notify"},
-		})
-
 		if !config.implicitHandshake {
 			// Bidirectional shutdown with the shim initiating. The runner,
 			// in the meantime, sends garbage before the close_notify which
 			// the shim must ignore. This test is disabled under implicit
 			// handshake tests because the shim never reads or writes.
-			tests = append(tests, testCase{
-				name: "Shutdown-Shim",
-				config: Config{
-					MaxVersion: VersionTLS12,
-					Bugs: ProtocolBugs{
-						ExpectCloseNotify: true,
-					},
-				},
-				shimShutsDown:     true,
-				sendEmptyRecords:  1,
-				sendWarningAlerts: 1,
-				flags:             []string{"-check-close-notify"},
-			})
 
-			// The shim should reject unexpected application data
-			// when shutting down.
-			tests = append(tests, testCase{
-				name: "Shutdown-Shim-ApplicationData",
-				config: Config{
-					MaxVersion: VersionTLS12,
-					Bugs: ProtocolBugs{
-						ExpectCloseNotify: true,
+			// Tests that require checking for a close notify alert don't work with
+			// QUIC because alerts are handled outside of the TLS stack in QUIC.
+			if config.protocol != quic {
+				tests = append(tests, testCase{
+					name: "Shutdown-Shim",
+					config: Config{
+						MaxVersion: VersionTLS12,
+						Bugs: ProtocolBugs{
+							ExpectCloseNotify: true,
+						},
 					},
-				},
-				shimShutsDown:     true,
-				messageCount:      1,
-				sendEmptyRecords:  1,
-				sendWarningAlerts: 1,
-				flags:             []string{"-check-close-notify"},
-				shouldFail:        true,
-				expectedError:     ":APPLICATION_DATA_ON_SHUTDOWN:",
-			})
+					shimShutsDown:     true,
+					sendEmptyRecords:  1,
+					sendWarningAlerts: 1,
+					flags:             []string{"-check-close-notify"},
+				})
 
-			// Test that SSL_shutdown still processes KeyUpdate.
-			tests = append(tests, testCase{
-				name: "Shutdown-Shim-KeyUpdate",
-				config: Config{
-					MinVersion: VersionTLS13,
-					MaxVersion: VersionTLS13,
-					Bugs: ProtocolBugs{
-						ExpectCloseNotify: true,
+				// The shim should reject unexpected application data
+				// when shutting down.
+				tests = append(tests, testCase{
+					name: "Shutdown-Shim-ApplicationData",
+					config: Config{
+						MaxVersion: VersionTLS12,
+						Bugs: ProtocolBugs{
+							ExpectCloseNotify: true,
+						},
 					},
-				},
-				shimShutsDown:    true,
-				sendKeyUpdates:   1,
-				keyUpdateRequest: keyUpdateRequested,
-				flags:            []string{"-check-close-notify"},
-			})
+					shimShutsDown:     true,
+					messageCount:      1,
+					sendEmptyRecords:  1,
+					sendWarningAlerts: 1,
+					flags:             []string{"-check-close-notify"},
+					shouldFail:        true,
+					expectedError:     ":APPLICATION_DATA_ON_SHUTDOWN:",
+				})
 
-			// Test that SSL_shutdown processes HelloRequest
-			// correctly.
-			tests = append(tests, testCase{
-				name: "Shutdown-Shim-HelloRequest-Ignore",
-				config: Config{
-					MinVersion: VersionTLS12,
-					MaxVersion: VersionTLS12,
-					Bugs: ProtocolBugs{
-						SendHelloRequestBeforeEveryAppDataRecord: true,
-						ExpectCloseNotify:                        true,
+				// Test that SSL_shutdown still processes KeyUpdate.
+				tests = append(tests, testCase{
+					name: "Shutdown-Shim-KeyUpdate",
+					config: Config{
+						MinVersion: VersionTLS13,
+						MaxVersion: VersionTLS13,
+						Bugs: ProtocolBugs{
+							ExpectCloseNotify: true,
+						},
 					},
-				},
-				shimShutsDown: true,
-				flags: []string{
-					"-renegotiate-ignore",
-					"-check-close-notify",
-				},
-			})
-			tests = append(tests, testCase{
-				name: "Shutdown-Shim-HelloRequest-Reject",
-				config: Config{
-					MinVersion: VersionTLS12,
-					MaxVersion: VersionTLS12,
-					Bugs: ProtocolBugs{
-						ExpectCloseNotify: true,
-					},
-				},
-				shimShutsDown: true,
-				renegotiate:   1,
-				shouldFail:    true,
-				expectedError: ":NO_RENEGOTIATION:",
-				flags:         []string{"-check-close-notify"},
-			})
-			tests = append(tests, testCase{
-				name: "Shutdown-Shim-HelloRequest-CannotHandshake",
-				config: Config{
-					MinVersion: VersionTLS12,
-					MaxVersion: VersionTLS12,
-					Bugs: ProtocolBugs{
-						ExpectCloseNotify: true,
-					},
-				},
-				shimShutsDown: true,
-				renegotiate:   1,
-				shouldFail:    true,
-				expectedError: ":NO_RENEGOTIATION:",
-				flags: []string{
-					"-check-close-notify",
-					"-renegotiate-freely",
-				},
-			})
+					shimShutsDown:    true,
+					sendKeyUpdates:   1,
+					keyUpdateRequest: keyUpdateRequested,
+					flags:            []string{"-check-close-notify"},
+				})
 
-			tests = append(tests, testCase{
-				testType: serverTest,
-				name:     "Shutdown-Shim-Renegotiate-Server-Forbidden",
-				config: Config{
-					MaxVersion: VersionTLS12,
-					Bugs: ProtocolBugs{
-						ExpectCloseNotify: true,
+				// Test that SSL_shutdown processes HelloRequest
+				// correctly.
+				tests = append(tests, testCase{
+					name: "Shutdown-Shim-HelloRequest-Ignore",
+					config: Config{
+						MinVersion: VersionTLS12,
+						MaxVersion: VersionTLS12,
+						Bugs: ProtocolBugs{
+							SendHelloRequestBeforeEveryAppDataRecord: true,
+							ExpectCloseNotify:                        true,
+						},
 					},
-				},
-				shimShutsDown: true,
-				renegotiate:   1,
-				shouldFail:    true,
-				expectedError: ":NO_RENEGOTIATION:",
-				flags: []string{
-					"-check-close-notify",
-				},
-			})
+					shimShutsDown: true,
+					flags: []string{
+						"-renegotiate-ignore",
+						"-check-close-notify",
+					},
+				})
+				tests = append(tests, testCase{
+					name: "Shutdown-Shim-HelloRequest-Reject",
+					config: Config{
+						MinVersion: VersionTLS12,
+						MaxVersion: VersionTLS12,
+						Bugs: ProtocolBugs{
+							ExpectCloseNotify: true,
+						},
+					},
+					shimShutsDown: true,
+					renegotiate:   1,
+					shouldFail:    true,
+					expectedError: ":NO_RENEGOTIATION:",
+					flags:         []string{"-check-close-notify"},
+				})
+				tests = append(tests, testCase{
+					name: "Shutdown-Shim-HelloRequest-CannotHandshake",
+					config: Config{
+						MinVersion: VersionTLS12,
+						MaxVersion: VersionTLS12,
+						Bugs: ProtocolBugs{
+							ExpectCloseNotify: true,
+						},
+					},
+					shimShutsDown: true,
+					renegotiate:   1,
+					shouldFail:    true,
+					expectedError: ":NO_RENEGOTIATION:",
+					flags: []string{
+						"-check-close-notify",
+						"-renegotiate-freely",
+					},
+				})
+
+				tests = append(tests, testCase{
+					testType: serverTest,
+					name:     "Shutdown-Shim-Renegotiate-Server-Forbidden",
+					config: Config{
+						MaxVersion: VersionTLS12,
+						Bugs: ProtocolBugs{
+							ExpectCloseNotify: true,
+						},
+					},
+					shimShutsDown: true,
+					renegotiate:   1,
+					shouldFail:    true,
+					expectedError: ":NO_RENEGOTIATION:",
+					flags: []string{
+						"-check-close-notify",
+					},
+				})
+			}
 		}
-	} else {
+	}
+	if config.protocol == dtls {
 		// TODO(davidben): DTLS 1.3 will want a similar thing for
 		// HelloRetryRequest.
 		tests = append(tests, testCase{
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 3f524ff..ad8ecdb 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -1162,8 +1162,7 @@
 
 static int SendQuicAlert(SSL *ssl, enum ssl_encryption_level_t level,
                          uint8_t alert) {
-  // TODO(nharper): Support processing alerts.
-  return 0;
+  return GetTestState(ssl)->quic_transport->SendAlert(level, alert);
 }
 
 static const SSL_QUIC_METHOD g_quic_method = {