Add mock QUIC transport to runner

The mock QUIC transport used has a very simple record layer: A record
starts with a single byte (either 'H' or 'A') identifying the record to
be handshake or application data, then a 4-byte network order integer
indicating the length of the payload, followed by the encryption secret
that would be used for protecting that payload, followed by the payload
itself. The encoded length is only the length of the payload, not that
of the payload and secret (or the whole record).

Bug: 293
Change-Id: Icb706a94ef1ad77e86ef8728b73db8832ee65e1b
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/39144
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/ssl/test/CMakeLists.txt b/ssl/test/CMakeLists.txt
index ebc16f1..bb9bd81 100644
--- a/ssl/test/CMakeLists.txt
+++ b/ssl/test/CMakeLists.txt
@@ -6,6 +6,7 @@
   async_bio.cc
   bssl_shim.cc
   handshake_util.cc
+  mock_quic_transport.cc
   packeted_bio.cc
   settings_writer.cc
   test_config.cc
@@ -23,6 +24,7 @@
     async_bio.cc
     handshake_util.cc
     handshaker.cc
+    mock_quic_transport.cc
     packeted_bio.cc
     settings_writer.cc
     test_config.cc
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index acef905..c9845f3 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -60,6 +60,7 @@
 #include "../internal.h"
 #include "async_bio.h"
 #include "handshake_util.h"
+#include "mock_quic_transport.h"
 #include "packeted_bio.h"
 #include "settings_writer.h"
 #include "test_config.h"
@@ -178,6 +179,9 @@
 static int DoRead(SSL *ssl, uint8_t *out, size_t max_out) {
   const TestConfig *config = GetTestConfig(ssl);
   TestState *test_state = GetTestState(ssl);
+  if (test_state->quic_transport) {
+    return test_state->quic_transport->ReadApplicationData(out, max_out);
+  }
   int ret;
   do {
     if (config->async) {
@@ -231,7 +235,14 @@
 // WriteAll writes |in_len| bytes from |in| to |ssl|, resolving any asynchronous
 // operations. It returns the result of the final |SSL_write| call.
 static int WriteAll(SSL *ssl, const void *in_, size_t in_len) {
+  TestState *test_state = GetTestState(ssl);
   const uint8_t *in = reinterpret_cast<const uint8_t *>(in_);
+  if (test_state->quic_transport) {
+    if (!test_state->quic_transport->WriteApplicationData(in, in_len)) {
+      return -1;
+    }
+    return in_len;
+  }
   int ret;
   do {
     ret = SSL_write(ssl, in, in_len);
@@ -731,8 +742,13 @@
     GetTestState(ssl.get())->async_bio = async_scoped.get();
     bio = std::move(async_scoped);
   }
-  SSL_set_bio(ssl.get(), bio.get(), bio.get());
-  bio.release();  // SSL_set_bio takes ownership.
+  if (config->is_quic) {
+    GetTestState(ssl.get())->quic_transport.reset(
+        new MockQuicTransport(std::move(bio), ssl.get()));
+  } else {
+    SSL_set_bio(ssl.get(), bio.get(), bio.get());
+    bio.release();  // SSL_set_bio takes ownership.
+  }
 
   bool ret = DoExchange(out_session, &ssl, config, is_resume, false, writer);
   if (!config->is_server && is_resume && config->expect_reject_early_data) {
diff --git a/ssl/test/handshake_util.cc b/ssl/test/handshake_util.cc
index fe96751..5265bd9 100644
--- a/ssl/test/handshake_util.cc
+++ b/ssl/test/handshake_util.cc
@@ -50,6 +50,10 @@
     return SSL_renegotiate(ssl);
   }
 
+  if (test_state->quic_transport && ssl_err == SSL_ERROR_WANT_READ) {
+    return test_state->quic_transport->ReadHandshake();
+  }
+
   if (!config->async) {
     // Only asynchronous tests should trigger other retries.
     return false;
diff --git a/ssl/test/mock_quic_transport.cc b/ssl/test/mock_quic_transport.cc
new file mode 100644
index 0000000..e63ccbf
--- /dev/null
+++ b/ssl/test/mock_quic_transport.cc
@@ -0,0 +1,210 @@
+/* Copyright (c) 2019, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#include "mock_quic_transport.h"
+
+#include <openssl/span.h>
+
+#include <cstring>
+#include <limits>
+
+namespace {
+
+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));
+}
+
+}  // 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),
+      ssl_(ssl) {}
+
+bool MockQuicTransport::SetSecrets(enum ssl_encryption_level_t level,
+                                   const uint8_t *read_secret,
+                                   const uint8_t *write_secret,
+                                   size_t secret_len) {
+  if (read_secret) {
+    read_secrets_[level].resize(secret_len);
+    memcpy(read_secrets_[level].data(), read_secret, secret_len);
+  }
+  if (write_secret) {
+    write_secrets_[level].resize(secret_len);
+    memcpy(write_secrets_[level].data(), write_secret, secret_len);
+  }
+  return true;
+}
+
+namespace {
+
+bool ReadAll(BIO *bio, bssl::Span<uint8_t> out) {
+  size_t len = out.size();
+  uint8_t *buf = out.data();
+  while (len > 0) {
+    int chunk_len = std::numeric_limits<int>::max();
+    if (len <= static_cast<unsigned int>(std::numeric_limits<int>::max())) {
+      chunk_len = len;
+    }
+    int ret = BIO_read(bio, buf, chunk_len);
+    if (ret <= 0) {
+      return false;
+    }
+    buf += ret;
+    len -= ret;
+  }
+  return true;
+}
+
+bool ReadHeader(BIO *bio, uint8_t *out_tag, size_t *out_len) {
+  uint8_t header[5];
+  if (!ReadAll(bio, header)) {
+    return false;
+  }
+
+  *out_len = header[1] << 24 | header[2] << 16 | header[3] << 8 | header[4];
+  *out_tag = header[0];
+  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)) {
+    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;
+  }
+  return SSL_provide_quic_data(ssl_, SSL_quic_read_level(ssl_), buf.data(),
+                               buf.size());
+}
+
+int MockQuicTransport::ReadApplicationData(uint8_t *out, size_t max_out) {
+  if (pending_app_data_.size() > 0) {
+    size_t len = pending_app_data_.size() - app_data_offset_;
+    if (len > max_out) {
+      len = max_out;
+    }
+    memcpy(out, pending_app_data_.data() + app_data_offset_, len);
+    app_data_offset_ += len;
+    if (app_data_offset_ == pending_app_data_.size()) {
+      pending_app_data_.clear();
+      app_data_offset_ = 0;
+    }
+    return len;
+  }
+
+  uint8_t tag = 0;
+  size_t len;
+  while (true) {
+    if (!ReadHeader(bio_.get(), &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;
+    }
+
+    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) {
+      return -1;
+    }
+  }
+
+  uint8_t *buf = out;
+  if (len > max_out) {
+    pending_app_data_.resize(len);
+    buf = pending_app_data_.data();
+  }
+  app_data_offset_ = 0;
+  if (!ReadAll(bio_.get(), bssl::MakeSpan(buf, len))) {
+    return -1;
+  }
+  if (len > max_out) {
+    memcpy(out, buf, max_out);
+    app_data_offset_ = max_out;
+    return max_out;
+  }
+  return 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;
+}
+
+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;
+  }
+  return true;
+}
+
+bool MockQuicTransport::Flush() { return BIO_flush(bio_.get()); }
diff --git a/ssl/test/mock_quic_transport.h b/ssl/test/mock_quic_transport.h
new file mode 100644
index 0000000..21f7dc6
--- /dev/null
+++ b/ssl/test/mock_quic_transport.h
@@ -0,0 +1,52 @@
+/* Copyright (c) 2019, Google Inc.
+ *
+ * Permission to use, copy, modify, and/or distribute this software for any
+ * purpose with or without fee is hereby granted, provided that the above
+ * copyright notice and this permission notice appear in all copies.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+ * SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+ * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
+
+#ifndef HEADER_MOCK_QUIC_TRANSPORT
+#define HEADER_MOCK_QUIC_TRANSPORT
+
+#include <openssl/base.h>
+#include <openssl/bio.h>
+#include <openssl/ssl.h>
+
+#include <vector>
+
+class MockQuicTransport {
+ public:
+  explicit MockQuicTransport(bssl::UniquePtr<BIO> bio, SSL *ssl);
+
+  bool SetSecrets(enum ssl_encryption_level_t level, const uint8_t *read_secret,
+                  const uint8_t *write_secret, size_t secret_len);
+
+  bool ReadHandshake();
+  bool WriteHandshakeData(enum ssl_encryption_level_t level,
+                          const uint8_t *data, size_t len);
+  // Returns the number of bytes read.
+  int ReadApplicationData(uint8_t *out, size_t max_out);
+  bool WriteApplicationData(const uint8_t *in, size_t len);
+  bool Flush();
+
+ private:
+  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_;
+
+  SSL *ssl_;  // Unowned.
+};
+
+
+#endif  // HEADER_MOCK_QUIC_TRANSPORT
diff --git a/ssl/test/runner/common.go b/ssl/test/runner/common.go
index 14f5886..a62050a 100644
--- a/ssl/test/runner/common.go
+++ b/ssl/test/runner/common.go
@@ -935,6 +935,10 @@
 	// PacketAdaptor is the packetAdaptor to use to simulate timeouts.
 	PacketAdaptor *packetAdaptor
 
+	// MockQUICTransport is the mockQUICTransport used when testing
+	// QUIC interfaces.
+	MockQUICTransport *mockQUICTransport
+
 	// ReorderHandshakeFragments, if true, causes handshake fragments in
 	// DTLS to overlap and be sent in the wrong order. It also causes
 	// pre-CCS flights to be sent twice. (Post-CCS flights consist of
diff --git a/ssl/test/runner/conn.go b/ssl/test/runner/conn.go
index 1d04541..ad551c5 100644
--- a/ssl/test/runner/conn.go
+++ b/ssl/test/runner/conn.go
@@ -760,6 +760,9 @@
 	if !c.isClient {
 		side = clientWrite
 	}
+	if c.config.Bugs.MockQUICTransport != nil {
+		c.config.Bugs.MockQUICTransport.readSecret = secret
+	}
 	c.in.useTrafficSecret(version, suite, secret, side)
 	c.seenHandshakePackEnd = false
 	return nil
@@ -770,6 +773,9 @@
 	if c.isClient {
 		side = clientWrite
 	}
+	if c.config.Bugs.MockQUICTransport != nil {
+		c.config.Bugs.MockQUICTransport.writeSecret = secret
+	}
 	c.out.useTrafficSecret(version, suite, secret, side)
 }
 
@@ -959,14 +965,18 @@
 		break
 	}
 
-	if c.expectTLS13ChangeCipherSpec {
+	if c.expectTLS13ChangeCipherSpec && c.config.Bugs.MockQUICTransport == nil {
 		if err := c.readTLS13ChangeCipherSpec(); err != nil {
 			return err
 		}
 	}
 
 Again:
-	typ, b, err := c.doReadRecord(want)
+	doReadRecord := c.doReadRecord
+	if c.config.Bugs.MockQUICTransport != nil {
+		doReadRecord = c.config.Bugs.MockQUICTransport.readRecord
+	}
+	typ, b, err := doReadRecord(want)
 	if err != nil {
 		return err
 	}
@@ -1141,6 +1151,9 @@
 	if c.isDTLS {
 		return c.dtlsWriteRecord(typ, data)
 	}
+	if c.config.Bugs.MockQUICTransport != nil {
+		return c.config.Bugs.MockQUICTransport.writeRecord(typ, data)
+	}
 
 	if typ == recordTypeHandshake {
 		if c.config.Bugs.SendHelloRequestBeforeEveryHandshakeMessage {
diff --git a/ssl/test/runner/mock_quic_transport.go b/ssl/test/runner/mock_quic_transport.go
new file mode 100644
index 0000000..4d971c4
--- /dev/null
+++ b/ssl/test/runner/mock_quic_transport.go
@@ -0,0 +1,98 @@
+// Copyright (c) 2019, Google Inc.
+//
+// Permission to use, copy, modify, and/or distribute this software for any
+// purpose with or without fee is hereby granted, provided that the above
+// copyright notice and this permission notice appear in all copies.
+//
+// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
+// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
+// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
+// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+package runner
+
+import (
+	"bytes"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"net"
+)
+
+const tagHandshake = byte('H')
+const tagApplication = byte('A')
+
+type mockQUICTransport struct {
+	net.Conn
+	readSecret, writeSecret []byte
+}
+
+func newMockQUICTransport(conn net.Conn) *mockQUICTransport {
+	return &mockQUICTransport{Conn: conn}
+}
+
+func (m *mockQUICTransport) read() (byte, []byte, error) {
+	header := make([]byte, 5)
+	if _, err := io.ReadFull(m.Conn, header); err != nil {
+		return 0, nil, err
+	}
+	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) {
+	typ, contents, err := m.read()
+	if err != nil {
+		return 0, nil, err
+	}
+	var returnType recordType
+	if typ == tagHandshake {
+		returnType = recordTypeHandshake
+	} else if typ == tagApplication {
+		returnType = recordTypeApplicationData
+	} else {
+		return 0, nil, fmt.Errorf("unknown type %d\n", typ)
+	}
+	return returnType, &block{contents, 0, nil}, nil
+}
+
+func (m *mockQUICTransport) writeRecord(typ recordType, data []byte) (int, error) {
+	tag := tagHandshake
+	if typ == recordTypeApplicationData {
+		tag = tagApplication
+	} else if typ != recordTypeHandshake {
+		return 0, fmt.Errorf("unsupported record type %d\n", typ)
+	}
+	payload := make([]byte, 1+4+len(m.writeSecret)+len(data))
+	payload[0] = tag
+	binary.BigEndian.PutUint32(payload[1:5], uint32(len(data)))
+	copy(payload[5:], m.writeSecret)
+	copy(payload[5+len(m.writeSecret):], data)
+	if _, err := m.Conn.Write(payload); err != nil {
+		return 0, err
+	}
+	return len(data), nil
+}
+
+func (m *mockQUICTransport) Write(b []byte) (int, error) {
+	panic("unexpected call to Write")
+}
+
+func (m *mockQUICTransport) Read(b []byte) (int, error) {
+	panic("unexpected call to Read")
+}
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index ba0d307..b3ea83a 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -465,6 +465,7 @@
 const (
 	tls protocol = iota
 	dtls
+	quic
 )
 
 const (
@@ -744,6 +745,16 @@
 			config.Bugs.PacketAdaptor.debug = connDebug
 		}
 	}
+	if test.protocol == quic {
+		config.Bugs.MockQUICTransport = newMockQUICTransport(conn)
+		// The MockQUICTransport will panic if Read or Write is
+		// called. When a MockQUICTransport is set, separate
+		// methods should be used to actually read and write
+		// records. By setting the conn to it here, it ensures
+		// Read or Write aren't accidentally used instead of the
+		// methods provided by MockQUICTransport.
+		conn = config.Bugs.MockQUICTransport
+	}
 
 	if test.replayWrites {
 		conn = newReplayAdaptor(conn)
@@ -1230,6 +1241,8 @@
 
 	if test.protocol == dtls {
 		flags = append(flags, "-dtls")
+	} else if test.protocol == quic {
+		flags = append(flags, "-quic")
 	}
 
 	var resumeCount int
@@ -1287,6 +1300,8 @@
 		protocol := "tls"
 		if test.protocol == dtls {
 			protocol = "dtls"
+		} else if test.protocol == quic {
+			protocol = "quic"
 		}
 
 		side := "client"
@@ -1481,6 +1496,7 @@
 	// excludeFlag is the legacy shim flag to disable the version.
 	excludeFlag string
 	hasDTLS     bool
+	hasQUIC     bool
 	// versionDTLS, if non-zero, is the DTLS-specific representation of the version.
 	versionDTLS uint16
 	// versionWire, if non-zero, is the wire representation of the
@@ -1508,6 +1524,16 @@
 	return vers.version
 }
 
+func (vers tlsVersion) supportsProtocol(protocol protocol) bool {
+	if protocol == dtls {
+		return vers.hasDTLS
+	}
+	if protocol == quic {
+		return vers.hasQUIC
+	}
+	return true
+}
+
 var tlsVersions = []tlsVersion{
 	{
 		name:        "TLS1",
@@ -1532,6 +1558,7 @@
 		name:        "TLS13",
 		version:     VersionTLS13,
 		excludeFlag: "-no-tls13",
+		hasQUIC:     true,
 		versionWire: VersionTLS13,
 	},
 }
@@ -1543,7 +1570,7 @@
 
 	var ret []tlsVersion
 	for _, vers := range tlsVersions {
-		if vers.hasDTLS {
+		if vers.supportsProtocol(protocol) {
 			ret = append(ret, vers)
 		}
 	}
@@ -3310,6 +3337,12 @@
 		}
 		prefix = "D"
 	}
+	if protocol == quic {
+		if !ver.hasQUIC {
+			return
+		}
+		prefix = "QUIC-"
+	}
 
 	var cert Certificate
 	var certFile string
@@ -3436,23 +3469,26 @@
 		expectedError = ":DECRYPTION_FAILED_OR_BAD_RECORD_MAC:"
 	}
 
-	testCases = append(testCases, testCase{
-		protocol: protocol,
-		name:     prefix + ver.name + "-" + suite.name + "-BadRecord",
-		config: Config{
-			MinVersion:           ver.version,
-			MaxVersion:           ver.version,
-			CipherSuites:         []uint16{suite.id},
-			Certificates:         []Certificate{cert},
-			PreSharedKey:         []byte(psk),
-			PreSharedKeyIdentity: pskIdentity,
-		},
-		flags:            flags,
-		damageFirstWrite: true,
-		messageLen:       maxPlaintext,
-		shouldFail:       shouldFail,
-		expectedError:    expectedError,
-	})
+	// TODO(nharper): Consider enabling this test for QUIC.
+	if protocol != quic {
+		testCases = append(testCases, testCase{
+			protocol: protocol,
+			name:     prefix + ver.name + "-" + suite.name + "-BadRecord",
+			config: Config{
+				MinVersion:           ver.version,
+				MaxVersion:           ver.version,
+				CipherSuites:         []uint16{suite.id},
+				Certificates:         []Certificate{cert},
+				PreSharedKey:         []byte(psk),
+				PreSharedKeyIdentity: pskIdentity,
+			},
+			flags:            flags,
+			damageFirstWrite: true,
+			messageLen:       maxPlaintext,
+			shouldFail:       shouldFail,
+			expectedError:    expectedError,
+		})
+	}
 }
 
 func addCipherSuiteTests() {
@@ -3460,7 +3496,7 @@
 
 	for _, suite := range testCipherSuites {
 		for _, ver := range tlsVersions {
-			for _, protocol := range []protocol{tls, dtls} {
+			for _, protocol := range []protocol{tls, dtls, quic} {
 				addTestForCipherSuite(suite, ver, protocol)
 			}
 		}
@@ -4372,6 +4408,7 @@
 // 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} {
 			addStateMachineCoverageTests(stateMachineTestConfig{
@@ -4878,7 +4915,7 @@
 
 	// OCSP stapling tests.
 	for _, vers := range tlsVersions {
-		if config.protocol == dtls && !vers.hasDTLS {
+		if !vers.supportsProtocol(config.protocol) {
 			continue
 		}
 		tests = append(tests, testCase{
@@ -5023,7 +5060,7 @@
 
 	// Certificate verification tests.
 	for _, vers := range tlsVersions {
-		if config.protocol == dtls && !vers.hasDTLS {
+		if !vers.supportsProtocol(config.protocol) {
 			continue
 		}
 		for _, useCustomCallback := range []bool{false, true} {
@@ -5713,6 +5750,9 @@
 		if config.protocol == dtls {
 			test.name += "-DTLS"
 		}
+		if config.protocol == quic {
+			test.name += "-QUIC"
+		}
 		if config.async {
 			test.name += "-Async"
 			test.flags = append(test.flags, "-async")
@@ -5805,7 +5845,7 @@
 }
 
 func addVersionNegotiationTests() {
-	for _, protocol := range []protocol{tls, dtls} {
+	for _, protocol := range []protocol{tls, dtls, quic} {
 		for _, shimVers := range allVersions(protocol) {
 			// Assemble flags to disable all newer versions on the shim.
 			var flags []string
@@ -5828,6 +5868,9 @@
 				if protocol == dtls {
 					suffix += "-DTLS"
 				}
+				if protocol == quic {
+					suffix += "-QUIC"
+				}
 
 				// Determine the expected initial record-layer versions.
 				clientVers := shimVers.version
@@ -5904,11 +5947,17 @@
 		if vers.hasDTLS {
 			protocols = append(protocols, dtls)
 		}
+		if vers.hasQUIC {
+			protocols = append(protocols, quic)
+		}
 		for _, protocol := range protocols {
 			suffix := vers.name
 			if protocol == dtls {
 				suffix += "-DTLS"
 			}
+			if protocol == quic {
+				suffix += "-QUIC"
+			}
 
 			testCases = append(testCases, testCase{
 				protocol: protocol,
@@ -6246,7 +6295,7 @@
 }
 
 func addMinimumVersionTests() {
-	for _, protocol := range []protocol{tls, dtls} {
+	for _, protocol := range []protocol{tls, dtls, quic} {
 		for _, shimVers := range allVersions(protocol) {
 			// Assemble flags to disable all older versions on the shim.
 			var flags []string
@@ -6263,6 +6312,9 @@
 				if protocol == dtls {
 					suffix += "-DTLS"
 				}
+				if protocol == quic {
+					suffix += "-QUIC"
+				}
 
 				var expectedVersion uint16
 				var shouldFail bool
@@ -7709,11 +7761,17 @@
 			if sessionVers.hasDTLS && resumeVers.hasDTLS {
 				protocols = append(protocols, dtls)
 			}
+			if sessionVers.hasQUIC && resumeVers.hasQUIC {
+				protocols = append(protocols, quic)
+			}
 			for _, protocol := range protocols {
 				suffix := "-" + sessionVers.name + "-" + resumeVers.name
 				if protocol == dtls {
 					suffix += "-DTLS"
 				}
+				if protocol == quic {
+					suffix += "-QUIC"
+				}
 
 				if sessionVers.version == resumeVers.version {
 					testCases = append(testCases, testCase{
@@ -11742,10 +11800,13 @@
 // WrongMessageType to fully test a per-message bug.
 func makePerMessageTests() []perMessageTest {
 	var ret []perMessageTest
+	// TODO(nharper): Consider supporting QUIC?
 	for _, protocol := range []protocol{tls, dtls} {
 		var suffix string
 		if protocol == dtls {
 			suffix = "-DTLS"
+		} else if protocol == quic {
+			suffix = "-QUIC"
 		}
 
 		ret = append(ret, perMessageTest{
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 2a84810..ab8df10 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -53,6 +53,7 @@
 const Flag<bool> kBoolFlags[] = {
     {"-server", &TestConfig::is_server},
     {"-dtls", &TestConfig::is_dtls},
+    {"-quic", &TestConfig::is_quic},
     {"-fallback-scsv", &TestConfig::fallback_scsv},
     {"-require-any-client-certificate",
      &TestConfig::require_any_client_certificate},
@@ -1131,6 +1132,37 @@
   return ssl_select_cert_success;
 }
 
+static int SetQuicEncryptionSecrets(SSL *ssl, enum ssl_encryption_level_t level,
+                                    const uint8_t *read_secret,
+                                    const uint8_t *write_secret,
+                                    size_t secret_len) {
+  return GetTestState(ssl)->quic_transport->SetSecrets(
+      level, read_secret, write_secret, secret_len);
+}
+
+static int AddQuicHandshakeData(SSL *ssl, enum ssl_encryption_level_t level,
+                                const uint8_t *data, size_t len) {
+  return GetTestState(ssl)->quic_transport->WriteHandshakeData(level, data,
+                                                               len);
+}
+
+static int FlushQuicFlight(SSL *ssl) {
+  return GetTestState(ssl)->quic_transport->Flush();
+}
+
+static int SendQuicAlert(SSL *ssl, enum ssl_encryption_level_t level,
+                         uint8_t alert) {
+  // TODO(nharper): Support processing alerts.
+  return 0;
+}
+
+static const SSL_QUIC_METHOD g_quic_method = {
+    SetQuicEncryptionSecrets,
+    AddQuicHandshakeData,
+    FlushQuicFlight,
+    SendQuicAlert,
+};
+
 bssl::UniquePtr<SSL_CTX> TestConfig::SetupCtx(SSL_CTX *old_ctx) const {
   bssl::UniquePtr<SSL_CTX> ssl_ctx(
       SSL_CTX_new(is_dtls ? DTLS_method() : TLS_method()));
@@ -1320,6 +1352,10 @@
     SSL_CTX_set_options(ssl_ctx.get(), SSL_OP_CIPHER_SERVER_PREFERENCE);
   }
 
+  if (is_quic) {
+    SSL_CTX_set_quic_method(ssl_ctx.get(), &g_quic_method);
+  }
+
   return ssl_ctx;
 }
 
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 24011a8..d588483 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -27,6 +27,7 @@
   int port = 0;
   bool is_server = false;
   bool is_dtls = false;
+  bool is_quic = false;
   int resume_count = 0;
   std::string write_settings;
   bool fallback_scsv = false;
diff --git a/ssl/test/test_state.h b/ssl/test/test_state.h
index 2aa9e30..745c4c4 100644
--- a/ssl/test/test_state.h
+++ b/ssl/test/test_state.h
@@ -15,11 +15,13 @@
 #ifndef HEADER_TEST_STATE
 #define HEADER_TEST_STATE
 
+#include <openssl/base.h>
+
 #include <memory>
 #include <string>
 #include <vector>
 
-#include <openssl/base.h>
+#include "mock_quic_transport.h"
 
 struct TestState {
   // Serialize writes |pending_session| and |msg_callback_text| to |out|, for
@@ -37,6 +39,7 @@
   BIO *async_bio = nullptr;
   // packeted_bio is the packeted BIO which simulates read timeouts.
   BIO *packeted_bio = nullptr;
+  std::unique_ptr<MockQuicTransport> quic_transport;
   bssl::UniquePtr<EVP_PKEY> channel_id;
   bool cert_ready = false;
   bssl::UniquePtr<SSL_SESSION> session;