Use TicketCrypter to enable TLS session resumption in QUIC.

gfe-relnote: Adds support for session resumption in TLS-based versions of QUIC. Protected by quic_enable_tls_resumption.
PiperOrigin-RevId: 308357681
Change-Id: I3889a8eec65d3903967d6ab1ca7c1b997da79606
diff --git a/quic/core/crypto/quic_crypto_server_config.cc b/quic/core/crypto/quic_crypto_server_config.cc
index 4dc34ff..36372ac 100644
--- a/quic/core/crypto/quic_crypto_server_config.cc
+++ b/quic/core/crypto/quic_crypto_server_config.cc
@@ -242,7 +242,7 @@
       proof_source_(std::move(proof_source)),
       client_cert_mode_(ClientCertMode::kNone),
       key_exchange_source_(std::move(key_exchange_source)),
-      ssl_ctx_(TlsServerConnection::CreateSslCtx()),
+      ssl_ctx_(TlsServerConnection::CreateSslCtx(proof_source_.get())),
       source_address_token_future_secs_(3600),
       source_address_token_lifetime_secs_(86400),
       enable_serving_sct_(false),
diff --git a/quic/core/crypto/tls_server_connection.cc b/quic/core/crypto/tls_server_connection.cc
index bdc941a..b647ecc 100644
--- a/quic/core/crypto/tls_server_connection.cc
+++ b/quic/core/crypto/tls_server_connection.cc
@@ -4,6 +4,8 @@
 
 #include "net/third_party/quiche/src/quic/core/crypto/tls_server_connection.h"
 
+#include "net/third_party/quiche/src/quic/core/crypto/proof_source.h"
+#include "net/third_party/quiche/src/quic/platform/api/quic_flags.h"
 #include "net/third_party/quiche/src/common/platform/api/quiche_string_piece.h"
 
 namespace quic {
@@ -13,12 +15,21 @@
       delegate_(delegate) {}
 
 // static
-bssl::UniquePtr<SSL_CTX> TlsServerConnection::CreateSslCtx() {
+bssl::UniquePtr<SSL_CTX> TlsServerConnection::CreateSslCtx(
+    ProofSource* proof_source) {
   bssl::UniquePtr<SSL_CTX> ssl_ctx = TlsConnection::CreateSslCtx();
   SSL_CTX_set_tlsext_servername_callback(ssl_ctx.get(),
                                          &SelectCertificateCallback);
   SSL_CTX_set_alpn_select_cb(ssl_ctx.get(), &SelectAlpnCallback, nullptr);
-  SSL_CTX_set_options(ssl_ctx.get(), SSL_OP_NO_TICKET);
+  // We don't actually need the SessionTicketCrypter here, but we need to know
+  // whether it's set.
+  if (GetQuicReloadableFlag(quic_enable_tls_resumption) &&
+      proof_source->SessionTicketCrypter()) {
+    SSL_CTX_set_ticket_aead_method(ssl_ctx.get(),
+                                   &TlsServerConnection::kSessionTicketMethod);
+  } else {
+    SSL_CTX_set_options(ssl_ctx.get(), SSL_OP_NO_TICKET);
+  }
   return ssl_ctx;
 }
 
@@ -81,4 +92,41 @@
                                                                max_out);
 }
 
+// static
+const SSL_TICKET_AEAD_METHOD TlsServerConnection::kSessionTicketMethod{
+    TlsServerConnection::SessionTicketMaxOverhead,
+    TlsServerConnection::SessionTicketSeal,
+    TlsServerConnection::SessionTicketOpen,
+};
+
+// static
+size_t TlsServerConnection::SessionTicketMaxOverhead(SSL* ssl) {
+  return ConnectionFromSsl(ssl)->delegate_->SessionTicketMaxOverhead();
+}
+
+// static
+int TlsServerConnection::SessionTicketSeal(SSL* ssl,
+                                           uint8_t* out,
+                                           size_t* out_len,
+                                           size_t max_out_len,
+                                           const uint8_t* in,
+                                           size_t in_len) {
+  return ConnectionFromSsl(ssl)->delegate_->SessionTicketSeal(
+      out, out_len, max_out_len,
+      quiche::QuicheStringPiece(reinterpret_cast<const char*>(in), in_len));
+}
+
+// static
+enum ssl_ticket_aead_result_t TlsServerConnection::SessionTicketOpen(
+    SSL* ssl,
+    uint8_t* out,
+    size_t* out_len,
+    size_t max_out_len,
+    const uint8_t* in,
+    size_t in_len) {
+  return ConnectionFromSsl(ssl)->delegate_->SessionTicketOpen(
+      out, out_len, max_out_len,
+      quiche::QuicheStringPiece(reinterpret_cast<const char*>(in), in_len));
+}
+
 }  // namespace quic
diff --git a/quic/core/crypto/tls_server_connection.h b/quic/core/crypto/tls_server_connection.h
index 85ce7e7..6da8114 100644
--- a/quic/core/crypto/tls_server_connection.h
+++ b/quic/core/crypto/tls_server_connection.h
@@ -5,6 +5,7 @@
 #ifndef QUICHE_QUIC_CORE_CRYPTO_TLS_SERVER_CONNECTION_H_
 #define QUICHE_QUIC_CORE_CRYPTO_TLS_SERVER_CONNECTION_H_
 
+#include "net/third_party/quiche/src/quic/core/crypto/proof_source.h"
 #include "net/third_party/quiche/src/quic/core/crypto/tls_connection.h"
 #include "net/third_party/quiche/src/common/platform/api/quiche_string_piece.h"
 
@@ -59,6 +60,50 @@
                                                         size_t* out_len,
                                                         size_t max_out) = 0;
 
+    // The following functions are used to implement an SSL_TICKET_AEAD_METHOD.
+    // See
+    // https://commondatastorage.googleapis.com/chromium-boringssl-docs/ssl.h.html#ssl_ticket_aead_result_t
+    // for details on the BoringSSL API.
+
+    // SessionTicketMaxOverhead returns the maximum number of bytes of overhead
+    // that SessionTicketSeal may add when encrypting a session ticket.
+    virtual size_t SessionTicketMaxOverhead() = 0;
+
+    // SessionTicketSeal encrypts the session ticket in |in|, putting the
+    // resulting encrypted ticket in |out|, writing the length of the bytes
+    // written to |*out_len|, which is no larger than |max_out_len|. It returns
+    // 1 on success and 0 on error.
+    virtual int SessionTicketSeal(uint8_t* out,
+                                  size_t* out_len,
+                                  size_t max_out_len,
+                                  quiche::QuicheStringPiece in) = 0;
+
+    // SessionTicketOpen is called when BoringSSL has an encrypted session
+    // ticket |in| and wants the ticket decrypted. This decryption operation can
+    // happen synchronously or asynchronously.
+    //
+    // If the decrypted ticket is not available at the time of the function
+    // call, this function returns ssl_ticket_aead_retry. If this function
+    // returns ssl_ticket_aead_retry, then SSL_do_handshake will return
+    // SSL_ERROR_PENDING_TICKET. Once the pending ticket decryption has
+    // completed, SSL_do_handshake needs to be called again.
+    //
+    // When this function is called and the decrypted ticket is available
+    // (either the ticket was decrypted synchronously, or an asynchronous
+    // operation has completed and SSL_do_handshake has been called again), the
+    // decrypted ticket is put in |out|, and the length of that output is
+    // written to |*out_len|, not to exceed |max_out_len|, and
+    // ssl_ticket_aead_success is returned. If the ticket cannot be decrypted
+    // and should be ignored, this function returns
+    // ssl_ticket_aead_ignore_ticket and a full handshake will be performed
+    // instead. If a fatal error occurs, ssl_ticket_aead_error can be returned
+    // which will terminate the handshake.
+    virtual enum ssl_ticket_aead_result_t SessionTicketOpen(
+        uint8_t* out,
+        size_t* out_len,
+        size_t max_out_len,
+        quiche::QuicheStringPiece in) = 0;
+
     // Provides the delegate for callbacks that are shared between client and
     // server.
     virtual TlsConnection::Delegate* ConnectionDelegate() = 0;
@@ -69,7 +114,7 @@
   TlsServerConnection(SSL_CTX* ssl_ctx, Delegate* delegate);
 
   // Creates and configures an SSL_CTX that is appropriate for servers to use.
-  static bssl::UniquePtr<SSL_CTX> CreateSslCtx();
+  static bssl::UniquePtr<SSL_CTX> CreateSslCtx(ProofSource* proof_source);
 
   void SetCertChain(const std::vector<CRYPTO_BUFFER*>& cert_chain);
 
@@ -105,6 +150,25 @@
                                                      size_t* out_len,
                                                      size_t max_out);
 
+  // Implementation of SSL_TICKET_AEAD_METHOD which delegates to corresponding
+  // methods in TlsServerConnection::Delegate (a.k.a. TlsServerHandshaker).
+  static const SSL_TICKET_AEAD_METHOD kSessionTicketMethod;
+
+  // The following functions make up the contents of |kSessionTicketMethod|.
+  static size_t SessionTicketMaxOverhead(SSL* ssl);
+  static int SessionTicketSeal(SSL* ssl,
+                               uint8_t* out,
+                               size_t* out_len,
+                               size_t max_out_len,
+                               const uint8_t* in,
+                               size_t in_len);
+  static enum ssl_ticket_aead_result_t SessionTicketOpen(SSL* ssl,
+                                                         uint8_t* out,
+                                                         size_t* out_len,
+                                                         size_t max_out_len,
+                                                         const uint8_t* in,
+                                                         size_t in_len);
+
   Delegate* delegate_;
 };
 
diff --git a/quic/core/tls_server_handshaker.cc b/quic/core/tls_server_handshaker.cc
index 75ff75d..108bc5a 100644
--- a/quic/core/tls_server_handshaker.cc
+++ b/quic/core/tls_server_handshaker.cc
@@ -44,6 +44,42 @@
   handshaker_ = nullptr;
 }
 
+TlsServerHandshaker::DecryptCallback::DecryptCallback(
+    TlsServerHandshaker* handshaker)
+    : handshaker_(handshaker) {}
+
+void TlsServerHandshaker::DecryptCallback::Run(std::vector<uint8_t> plaintext) {
+  if (handshaker_ == nullptr) {
+    // The callback was cancelled before we could run.
+    return;
+  }
+  handshaker_->decrypted_session_ticket_ = std::move(plaintext);
+  // DecryptCallback::Run could be called synchronously. When that happens, we
+  // are currently in the middle of a call to AdvanceHandshake.
+  // (AdvanceHandshake called SSL_do_handshake, which through some layers called
+  // SessionTicketOpen, which called TicketCrypter::Decrypt, which synchronously
+  // called this function.) In that case, the handshake will continue to be
+  // processed when this function returns.
+  //
+  // When this callback is called asynchronously (i.e. the ticket decryption is
+  // pending), TlsServerHandshaker is not actively processing handshake
+  // messages. We need to have it resume processing handshake messages by
+  // calling AdvanceHandshake.
+  if (handshaker_->state_ == STATE_TICKET_DECRYPTION_PENDING) {
+    handshaker_->AdvanceHandshake();
+  }
+  // The TicketDecrypter took ownership of this callback when Decrypt was
+  // called. Once the callback returns, it will be deleted. Remove the
+  // (non-owning) pointer to the callback from the handshaker so the handshaker
+  // doesn't have an invalid pointer hanging around.
+  handshaker_->ticket_decryption_callback_ = nullptr;
+}
+
+void TlsServerHandshaker::DecryptCallback::Cancel() {
+  DCHECK(handshaker_);
+  handshaker_ = nullptr;
+}
+
 TlsServerHandshaker::TlsServerHandshaker(
     QuicSession* session,
     const QuicCryptoServerConfig& crypto_config)
@@ -69,6 +105,10 @@
     signature_callback_->Cancel();
     signature_callback_ = nullptr;
   }
+  if (ticket_decryption_callback_) {
+    ticket_decryption_callback_->Cancel();
+    ticket_decryption_callback_ = nullptr;
+  }
 }
 
 bool TlsServerHandshaker::GetBase64SHA256ClientChannelID(
@@ -196,6 +236,9 @@
     case STATE_SIGNATURE_PENDING:
       should_close = ssl_error != SSL_ERROR_WANT_PRIVATE_KEY_OPERATION;
       break;
+    case STATE_TICKET_DECRYPTION_PENDING:
+      should_close = ssl_error != SSL_ERROR_PENDING_TICKET;
+      break;
     default:
       should_close = true;
   }
@@ -373,6 +416,71 @@
   return ssl_private_key_success;
 }
 
+size_t TlsServerHandshaker::SessionTicketMaxOverhead() {
+  DCHECK(proof_source_->SessionTicketCrypter());
+  return proof_source_->SessionTicketCrypter()->MaxOverhead();
+}
+
+int TlsServerHandshaker::SessionTicketSeal(uint8_t* out,
+                                           size_t* out_len,
+                                           size_t max_out_len,
+                                           quiche::QuicheStringPiece in) {
+  DCHECK(proof_source_->SessionTicketCrypter());
+  std::vector<uint8_t> ticket =
+      proof_source_->SessionTicketCrypter()->Encrypt(in);
+  if (max_out_len < ticket.size()) {
+    QUIC_BUG
+        << "SessionTicketCrypter returned " << ticket.size()
+        << " bytes of ciphertext, which is larger than its max overhead of "
+        << max_out_len;
+    return 0;  // failure
+  }
+  *out_len = ticket.size();
+  memcpy(out, ticket.data(), ticket.size());
+  return 1;  // success
+}
+
+ssl_ticket_aead_result_t TlsServerHandshaker::SessionTicketOpen(
+    uint8_t* out,
+    size_t* out_len,
+    size_t max_out_len,
+    quiche::QuicheStringPiece in) {
+  DCHECK(proof_source_->SessionTicketCrypter());
+
+  if (!ticket_decryption_callback_) {
+    ticket_decryption_callback_ = new DecryptCallback(this);
+    proof_source_->SessionTicketCrypter()->Decrypt(
+        in, std::unique_ptr<DecryptCallback>(ticket_decryption_callback_));
+    // Decrypt can run the callback synchronously. In that case, the callback
+    // will clear the ticket_decryption_callback_ pointer, and instead of
+    // returning ssl_ticket_aead_retry, we should continue processing to return
+    // the decrypted ticket.
+    //
+    // If the callback is not run asynchronously, return ssl_ticket_aead_retry
+    // and when the callback is complete this function will be run again to
+    // return the result.
+    if (ticket_decryption_callback_) {
+      state_ = STATE_TICKET_DECRYPTION_PENDING;
+      return ssl_ticket_aead_retry;
+    }
+  }
+  ticket_decryption_callback_ = nullptr;
+  state_ = STATE_LISTENING;
+  if (decrypted_session_ticket_.empty()) {
+    QUIC_DLOG(ERROR) << "Session ticket decryption failed; ignoring ticket";
+    // Ticket decryption failed. Ignore the ticket.
+    return ssl_ticket_aead_ignore_ticket;
+  }
+  if (max_out_len < decrypted_session_ticket_.size()) {
+    return ssl_ticket_aead_error;
+  }
+  memcpy(out, decrypted_session_ticket_.data(),
+         decrypted_session_ticket_.size());
+  *out_len = decrypted_session_ticket_.size();
+
+  return ssl_ticket_aead_success;
+}
+
 int TlsServerHandshaker::SelectCertificate(int* out_alert) {
   const char* hostname = SSL_get_servername(ssl(), TLSEXT_NAMETYPE_host_name);
   if (hostname) {
diff --git a/quic/core/tls_server_handshaker.h b/quic/core/tls_server_handshaker.h
index 066c53b..9d72d30 100644
--- a/quic/core/tls_server_handshaker.h
+++ b/quic/core/tls_server_handshaker.h
@@ -106,6 +106,16 @@
   ssl_private_key_result_t PrivateKeyComplete(uint8_t* out,
                                               size_t* out_len,
                                               size_t max_out) override;
+  size_t SessionTicketMaxOverhead() override;
+  int SessionTicketSeal(uint8_t* out,
+                        size_t* out_len,
+                        size_t max_out_len,
+                        quiche::QuicheStringPiece in) override;
+  ssl_ticket_aead_result_t SessionTicketOpen(
+      uint8_t* out,
+      size_t* out_len,
+      size_t max_out_len,
+      quiche::QuicheStringPiece in) override;
   TlsConnection::Delegate* ConnectionDelegate() override { return this; }
 
  private:
@@ -124,8 +134,22 @@
     TlsServerHandshaker* handshaker_;
   };
 
+  class QUIC_EXPORT_PRIVATE DecryptCallback
+      : public ProofSource::DecryptCallback {
+   public:
+    explicit DecryptCallback(TlsServerHandshaker* handshaker);
+    void Run(std::vector<uint8_t> plaintext) override;
+
+    // If called, Cancel causes the pending callback to be a no-op.
+    void Cancel();
+
+   private:
+    TlsServerHandshaker* handshaker_;
+  };
+
   enum State {
     STATE_LISTENING,
+    STATE_TICKET_DECRYPTION_PENDING,
     STATE_SIGNATURE_PENDING,
     STATE_SIGNATURE_COMPLETE,
     STATE_ENCRYPTION_HANDSHAKE_DATA_PROCESSED,
@@ -146,6 +170,15 @@
   ProofSource* proof_source_;
   SignatureCallback* signature_callback_ = nullptr;
 
+  // State to handle potentially asynchronous session ticket decryption.
+  // |ticket_decryption_callback_| points to the non-owned callback that was
+  // passed to ProofSource::TicketCrypter::Decrypt but hasn't finished running
+  // yet.
+  DecryptCallback* ticket_decryption_callback_ = nullptr;
+  // |decrypted_session_ticket_| contains the decrypted session ticket after the
+  // callback has run but before it is passed to BoringSSL.
+  std::vector<uint8_t> decrypted_session_ticket_;
+
   std::string hostname_;
   std::string cert_verify_sig_;
   std::unique_ptr<ProofSource::Details> proof_source_details_;
diff --git a/quic/core/tls_server_handshaker_test.cc b/quic/core/tls_server_handshaker_test.cc
index 7a30a13..52d5627 100644
--- a/quic/core/tls_server_handshaker_test.cc
+++ b/quic/core/tls_server_handshaker_test.cc
@@ -19,6 +19,8 @@
 #include "net/third_party/quiche/src/quic/test_tools/crypto_test_utils.h"
 #include "net/third_party/quiche/src/quic/test_tools/fake_proof_source.h"
 #include "net/third_party/quiche/src/quic/test_tools/quic_test_utils.h"
+#include "net/third_party/quiche/src/quic/test_tools/simple_session_cache.h"
+#include "net/third_party/quiche/src/quic/test_tools/test_ticket_crypter.h"
 #include "net/third_party/quiche/src/common/platform/api/quiche_arraysize.h"
 #include "net/third_party/quiche/src/common/platform/api/quiche_string_piece.h"
 
@@ -42,15 +44,12 @@
 class TlsServerHandshakerTest : public QuicTest {
  public:
   TlsServerHandshakerTest()
-      : proof_source_(new FakeProofSource()),
-        server_crypto_config_(QuicCryptoServerConfig::TESTING,
-                              QuicRandom::GetInstance(),
-                              std::unique_ptr<ProofSource>(proof_source_),
-                              KeyExchangeSource::Default()),
-        server_compressed_certs_cache_(
+      : server_compressed_certs_cache_(
             QuicCompressedCertsCache::kQuicCompressedCertsCacheSize),
         server_id_(kServerHostname, kServerPort, false),
-        client_crypto_config_(crypto_test_utils::ProofVerifierForTesting()) {
+        client_crypto_config_(crypto_test_utils::ProofVerifierForTesting(),
+                              std::make_unique<test::SimpleSessionCache>()) {
+    InitializeServerConfig();
     InitializeServer();
     InitializeFakeClient();
   }
@@ -64,6 +63,18 @@
     alarm_factories_.clear();
   }
 
+  void InitializeServerConfig() {
+    SetQuicReloadableFlag(quic_enable_tls_resumption, true);
+    auto ticket_crypter = std::make_unique<TestTicketCrypter>();
+    ticket_crypter_ = ticket_crypter.get();
+    auto proof_source = std::make_unique<FakeProofSource>();
+    proof_source_ = proof_source.get();
+    proof_source_->SetTicketCrypter(std::move(ticket_crypter));
+    server_crypto_config_ = std::make_unique<QuicCryptoServerConfig>(
+        QuicCryptoServerConfig::TESTING, QuicRandom::GetInstance(),
+        std::move(proof_source), KeyExchangeSource::Default());
+  }
+
   // Initializes the crypto server stream state for testing.  May be
   // called multiple times.
   void InitializeServer() {
@@ -73,7 +84,7 @@
     CreateServerSessionForTest(
         server_id_, QuicTime::Delta::FromSeconds(100000), supported_versions_,
         helpers_.back().get(), alarm_factories_.back().get(),
-        &server_crypto_config_, &server_compressed_certs_cache_,
+        server_crypto_config_.get(), &server_compressed_certs_cache_,
         &server_connection_, &server_session);
     CHECK(server_session);
     server_session_.reset(server_session);
@@ -88,7 +99,7 @@
             });
     crypto_test_utils::SetupCryptoServerConfigForTest(
         server_connection_->clock(), server_connection_->random_generator(),
-        &server_crypto_config_);
+        server_crypto_config_.get());
   }
 
   QuicCryptoServerStreamBase* server_stream() {
@@ -115,6 +126,7 @@
         .WillByDefault(Return(std::vector<std::string>({default_alpn})));
     CHECK(client_session);
     client_session_.reset(client_session);
+    moved_messages_counts_ = {0, 0};
   }
 
   void CompleteCryptoHandshake() {
@@ -185,8 +197,9 @@
   // Server state.
   PacketSavingConnection* server_connection_;
   std::unique_ptr<TestQuicSpdyServerSession> server_session_;
+  TestTicketCrypter* ticket_crypter_;  // owned by proof_source_
   FakeProofSource* proof_source_;  // owned by server_crypto_config_
-  QuicCryptoServerConfig server_crypto_config_;
+  std::unique_ptr<QuicCryptoServerConfig> server_crypto_config_;
   QuicCompressedCertsCache server_compressed_certs_cache_;
   QuicServerId server_id_;
 
@@ -347,6 +360,79 @@
   ExpectHandshakeSuccessful();
 }
 
+TEST_F(TlsServerHandshakerTest, Resumption) {
+  // Do the first handshake
+  InitializeFakeClient();
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+  EXPECT_FALSE(client_stream()->IsResumption());
+
+  // Now do another handshake
+  InitializeServer();
+  InitializeFakeClient();
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+  EXPECT_TRUE(client_stream()->IsResumption());
+}
+
+TEST_F(TlsServerHandshakerTest, ResumptionWithAsyncDecryptCallback) {
+  // Do the first handshake
+  InitializeFakeClient();
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+
+  ticket_crypter_->SetRunCallbacksAsync(true);
+  // Now do another handshake
+  InitializeServer();
+  InitializeFakeClient();
+
+  AdvanceHandshakeWithFakeClient();
+  // Test that the DecryptCallback will be run asynchronously, and then run it.
+  ASSERT_EQ(ticket_crypter_->NumPendingCallbacks(), 1u);
+  ticket_crypter_->RunPendingCallback(0);
+
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+  EXPECT_TRUE(client_stream()->IsResumption());
+}
+
+TEST_F(TlsServerHandshakerTest, ResumptionWithFailingDecryptCallback) {
+  // Do the first handshake
+  InitializeFakeClient();
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+
+  ticket_crypter_->set_fail_decrypt(true);
+  // Now do another handshake
+  InitializeServer();
+  InitializeFakeClient();
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+  EXPECT_FALSE(client_stream()->IsResumption());
+}
+
+TEST_F(TlsServerHandshakerTest, ResumptionWithFailingAsyncDecryptCallback) {
+  // Do the first handshake
+  InitializeFakeClient();
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+
+  ticket_crypter_->set_fail_decrypt(true);
+  ticket_crypter_->SetRunCallbacksAsync(true);
+  // Now do another handshake
+  InitializeServer();
+  InitializeFakeClient();
+
+  AdvanceHandshakeWithFakeClient();
+  // Test that the DecryptCallback will be run asynchronously, and then run it.
+  ASSERT_EQ(ticket_crypter_->NumPendingCallbacks(), 1u);
+  ticket_crypter_->RunPendingCallback(0);
+
+  CompleteCryptoHandshake();
+  ExpectHandshakeSuccessful();
+  EXPECT_FALSE(client_stream()->IsResumption());
+}
+
 }  // namespace
 }  // namespace test
 }  // namespace quic
diff --git a/quic/test_tools/fake_proof_source.cc b/quic/test_tools/fake_proof_source.cc
index 032560e..3763bcc 100644
--- a/quic/test_tools/fake_proof_source.cc
+++ b/quic/test_tools/fake_proof_source.cc
@@ -114,9 +114,17 @@
 }
 
 ProofSource::TicketCrypter* FakeProofSource::SessionTicketCrypter() {
+  if (ticket_crypter_) {
+    return ticket_crypter_.get();
+  }
   return delegate_->SessionTicketCrypter();
 }
 
+void FakeProofSource::SetTicketCrypter(
+    std::unique_ptr<TicketCrypter> ticket_crypter) {
+  ticket_crypter_ = std::move(ticket_crypter);
+}
+
 int FakeProofSource::NumPendingCallbacks() const {
   return pending_ops_.size();
 }
diff --git a/quic/test_tools/fake_proof_source.h b/quic/test_tools/fake_proof_source.h
index ef7669d..c179f59 100644
--- a/quic/test_tools/fake_proof_source.h
+++ b/quic/test_tools/fake_proof_source.h
@@ -15,10 +15,13 @@
 namespace quic {
 namespace test {
 
-// Implementation of ProofSource which delegates to a ProofSourceForTesting,
-// except that when the async GetProof is called, it captures the call and
-// allows tests to see that a call is pending, which they can then cause to
-// complete at a time of their choosing.
+// Implementation of ProofSource which delegates to a ProofSourceForTesting, but
+// allows for overriding certain functionality. FakeProofSource allows
+// intercepting calls to GetProof and ComputeTlsSignature to force them to run
+// asynchronously, and allow the caller to see that the call is pending and
+// resume the operation at the caller's choosing. FakeProofSource also allows
+// the caller to replace the TicketCrypter provided by
+// FakeProofSource::SessionTicketCrypter.
 class FakeProofSource : public ProofSource {
  public:
   FakeProofSource();
@@ -46,9 +49,12 @@
       uint16_t signature_algorithm,
       quiche::QuicheStringPiece in,
       std::unique_ptr<ProofSource::SignatureCallback> callback) override;
-
   TicketCrypter* SessionTicketCrypter() override;
 
+  // Sets the TicketCrypter to use. If nullptr, the TicketCrypter from
+  // ProofSourceForTesting will be returned instead.
+  void SetTicketCrypter(std::unique_ptr<TicketCrypter> ticket_crypter);
+
   // Get the number of callbacks which are pending
   int NumPendingCallbacks() const;
 
@@ -58,6 +64,7 @@
 
  private:
   std::unique_ptr<ProofSource> delegate_;
+  std::unique_ptr<TicketCrypter> ticket_crypter_;
   bool active_ = false;
 
   class PendingOp {
diff --git a/quic/test_tools/test_ticket_crypter.cc b/quic/test_tools/test_ticket_crypter.cc
new file mode 100644
index 0000000..4d0d93e
--- /dev/null
+++ b/quic/test_tools/test_ticket_crypter.cc
@@ -0,0 +1,74 @@
+// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "net/third_party/quiche/src/quic/test_tools/test_ticket_crypter.h"
+
+#include <cstring>
+
+#include "net/third_party/quiche/src/common/platform/api/quiche_arraysize.h"
+
+namespace quic {
+namespace test {
+
+namespace {
+
+// A TicketCrypter implementation is supposed to encrypt and decrypt session
+// tickets. However, the only requirement that is needed of a test
+// implementation is that calling Decrypt(Encrypt(input), callback) results in
+// callback being called with input. (The output of Encrypt must also not exceed
+// the overhead specified by MaxOverhead.) This test implementation encrypts
+// tickets by prepending kTicketPrefix to generate the ciphertext. The decrypt
+// function checks that the prefix is present and strips it; otherwise it
+// returns an empty vector to signal failure.
+constexpr char kTicketPrefix[] = "TEST TICKET";
+
+}  // namespace
+
+size_t TestTicketCrypter::MaxOverhead() {
+  return QUICHE_ARRAYSIZE(kTicketPrefix);
+}
+
+std::vector<uint8_t> TestTicketCrypter::Encrypt(quiche::QuicheStringPiece in) {
+  size_t prefix_len = QUICHE_ARRAYSIZE(kTicketPrefix);
+  std::vector<uint8_t> out(prefix_len + in.size());
+  memcpy(out.data(), kTicketPrefix, prefix_len);
+  memcpy(out.data() + prefix_len, in.data(), in.size());
+  return out;
+}
+
+std::vector<uint8_t> TestTicketCrypter::Decrypt(quiche::QuicheStringPiece in) {
+  size_t prefix_len = QUICHE_ARRAYSIZE(kTicketPrefix);
+  if (fail_decrypt_ || in.size() < prefix_len ||
+      memcmp(kTicketPrefix, in.data(), prefix_len) != 0) {
+    return std::vector<uint8_t>();
+  }
+  return std::vector<uint8_t>(in.begin() + prefix_len, in.end());
+}
+
+void TestTicketCrypter::Decrypt(
+    quiche::QuicheStringPiece in,
+    std::unique_ptr<ProofSource::DecryptCallback> callback) {
+  auto decrypted_ticket = Decrypt(in);
+  if (run_async_) {
+    pending_callbacks_.push_back({std::move(callback), decrypted_ticket});
+  } else {
+    callback->Run(decrypted_ticket);
+  }
+}
+
+void TestTicketCrypter::SetRunCallbacksAsync(bool run_async) {
+  run_async_ = run_async;
+}
+
+size_t TestTicketCrypter::NumPendingCallbacks() {
+  return pending_callbacks_.size();
+}
+
+void TestTicketCrypter::RunPendingCallback(size_t n) {
+  const PendingCallback& callback = pending_callbacks_[n];
+  callback.callback->Run(callback.decrypted_ticket);
+}
+
+}  // namespace test
+}  // namespace quic
diff --git a/quic/test_tools/test_ticket_crypter.h b/quic/test_tools/test_ticket_crypter.h
new file mode 100644
index 0000000..b596348
--- /dev/null
+++ b/quic/test_tools/test_ticket_crypter.h
@@ -0,0 +1,49 @@
+// Copyright (c) 2020 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef QUICHE_QUIC_TEST_TOOLS_TEST_TICKET_CRYPTER_H_
+#define QUICHE_QUIC_TEST_TOOLS_TEST_TICKET_CRYPTER_H_
+
+#include "net/third_party/quiche/src/quic/core/crypto/proof_source.h"
+
+namespace quic {
+namespace test {
+
+// Provides a simple implementation of ProofSource::TicketCrypter for testing.
+// THIS IMPLEMENTATION IS NOT SECURE. It is only intended for testing purposes.
+class TestTicketCrypter : public ProofSource::TicketCrypter {
+ public:
+  ~TestTicketCrypter() override = default;
+
+  // TicketCrypter interface
+  size_t MaxOverhead() override;
+  std::vector<uint8_t> Encrypt(quiche::QuicheStringPiece in) override;
+  void Decrypt(quiche::QuicheStringPiece in,
+               std::unique_ptr<ProofSource::DecryptCallback> callback) override;
+
+  void SetRunCallbacksAsync(bool run_async);
+  size_t NumPendingCallbacks();
+  void RunPendingCallback(size_t n);
+
+  // Allows configuring this TestTicketCrypter to fail decryption.
+  void set_fail_decrypt(bool fail_decrypt) { fail_decrypt_ = fail_decrypt; }
+
+ private:
+  // Performs the Decrypt operation synchronously.
+  std::vector<uint8_t> Decrypt(quiche::QuicheStringPiece in);
+
+  struct PendingCallback {
+    std::unique_ptr<ProofSource::DecryptCallback> callback;
+    std::vector<uint8_t> decrypted_ticket;
+  };
+
+  bool fail_decrypt_ = false;
+  bool run_async_ = false;
+  std::vector<PendingCallback> pending_callbacks_;
+};
+
+}  // namespace test
+}  // namespace quic
+
+#endif  // QUICHE_QUIC_TEST_TOOLS_TEST_TICKET_CRYPTER_H_