Add experimental handshake hints API.

See go/handshake-hints (internal).

CL originally by Bin Wu <wub@google.com>. I just reworked the tests and
tidied it up a bit. This is the start of a replacement for the split
handshakes API. For now, only TLS 1.3 is supported. It starts with an
initial set of hints, but we can add more later. (In particular, we
should probably apply the remote handshaker's extension order to avoid
needing to capability protect such changes.)

Change-Id: I7b6a6dfaa84c6c6e3436d2a4026c3652b8a79f0f
Reviewed-on: https://boringssl-review.googlesource.com/c/boringssl/+/46535
Commit-Queue: David Benjamin <davidben@google.com>
Reviewed-by: Adam Langley <agl@google.com>
diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata
index 04c7d53..a279ca6 100644
--- a/crypto/err/ssl.errordata
+++ b/crypto/err/ssl.errordata
@@ -43,6 +43,7 @@
 SSL,132,CLIENTHELLO_TLSEXT
 SSL,133,CONNECTION_REJECTED
 SSL,134,CONNECTION_TYPE_NOT_SET
+SSL,316,COULD_NOT_PARSE_HINTS
 SSL,135,CUSTOM_EXTENSION_ERROR
 SSL,136,DATA_LENGTH_TOO_LONG
 SSL,137,DECODE_ERROR
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 8d8e091..aef6b35 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -567,6 +567,11 @@
 // See also |ssl_renegotiate_explicit|.
 #define SSL_ERROR_WANT_RENEGOTIATE 19
 
+// SSL_ERROR_HANDSHAKE_HINTS_READY indicates the handshake has progressed enough
+// for |SSL_serialize_handshake_hints| to be called. See also
+// |SSL_request_handshake_hints|.
+#define SSL_ERROR_HANDSHAKE_HINTS_READY 20
+
 // SSL_error_description returns a string representation of |err|, where |err|
 // is one of the |SSL_ERROR_*| constants returned by |SSL_get_error|, or NULL
 // if the value is unrecognized.
@@ -3793,6 +3798,101 @@
 OPENSSL_EXPORT uint64_t SSL_get_write_sequence(const SSL *ssl);
 
 
+// Handshake hints.
+//
+// *** EXPERIMENTAL — DO NOT USE WITHOUT CHECKING ***
+//
+// Some server deployments make asynchronous RPC calls in both ClientHello
+// dispatch and private key operations. In TLS handshakes where the private key
+// operation occurs in the first round-trip, this results in two consecutive RPC
+// round-trips. Handshake hints allow the RPC service to predicte a signature.
+// If correctly predicted, this can skip the second RPC call.
+//
+// First, the server installs a certificate selection callback (see
+// |SSL_CTX_set_select_certificate_cb|). When that is called, it performs the
+// RPC as before, but includes the ClientHello and a capabilities string from
+// |SSL_serialize_capabilities|.
+//
+// Next, the RPC service creates its own |SSL| object, applies the results of
+// certificate selection, calls |SSL_request_handshake_hints|, and runs the
+// handshake. If this successfully computes handshake hints (see
+// |SSL_serialize_handshake_hints|), the RPC server should send the hints
+// alongside any certificate selection results.
+//
+// Finally, the server calls |SSL_set_handshake_hints| and applies any
+// configuration from the RPC server. It then completes the handshake as before.
+// If the hints apply, BoringSSL will use the predicted signature and skip the
+// private key callbacks. Otherwise, BoringSSL will call private key callbacks
+// to generate a signature as before.
+//
+// Callers should synchronize configuration across the two services.
+// Configuration mismatches and some cases of version skew are not fatal, but
+// may result in the hints not applying. Additionally, some handshake flows use
+// the private key in later round-trips, such as TLS 1.3 HelloRetryRequest. In
+// those cases, BoringSSL will not predict a signature as there is no benefit.
+// Callers must allow for handshakes to complete without a predicted signature.
+//
+// For now, only TLS 1.3 is hinted. TLS 1.2 will work, but the hints will be
+// empty.
+
+// SSL_serialize_capabilities writes an opaque byte string to |out| describing
+// some of |ssl|'s capabilities. It returns one on success and zero on error.
+//
+// This string is used by BoringSSL internally to reduce the impact of version
+// skew.
+OPENSSL_EXPORT int SSL_serialize_capabilities(const SSL *ssl, CBB *out);
+
+// SSL_request_handshake_hints configures |ssl| to generate a handshake hint for
+// |client_hello|. It returns one on success and zero on error. |client_hello|
+// should contain a serialized ClientHello structure, from the |client_hello|
+// and |client_hello_len| fields of the |SSL_CLIENT_HELLO| structure.
+// |capabilities| should contain the output of |SSL_serialize_capabilities|.
+//
+// When configured, |ssl| will perform no I/O (so there is no need to configure
+// |BIO|s). For QUIC, the caller should still configure an |SSL_QUIC_METHOD|,
+// but the callbacks themselves will never be called and may be left NULL or
+// report failure. |SSL_provide_quic_data| also should not be called.
+//
+// If hint generation is successful, |SSL_do_handshake| will stop the handshake
+// early with |SSL_get_error| returning |SSL_ERROR_HANDSHAKE_HINTS_READY|. At
+// this point, the caller should run |SSL_serialize_handshake_hints| to extract
+// the resulting hints.
+//
+// Hint generation may fail if, e.g., |ssl| was unable to process the
+// ClientHello. Callers should then complete the certificate selection RPC and
+// continue the original handshake with no hint. It will likely fail, but this
+// reports the correct alert to the client and is more robust in case of
+// mismatch.
+OPENSSL_EXPORT int SSL_request_handshake_hints(SSL *ssl,
+                                               const uint8_t *client_hello,
+                                               size_t client_hello_len,
+                                               const uint8_t *capabilities,
+                                               size_t capabilities_len);
+
+// SSL_serialize_handshake_hints writes an opaque byte string to |out|
+// containing the handshake hints computed by |out|. It returns one on success
+// and zero on error. This function should only be called if
+// |SSL_request_handshake_hints| was configured and the handshake terminated
+// with |SSL_ERROR_HANDSHAKE_HINTS_READY|.
+//
+// This string may be passed to |SSL_set_handshake_hints| on another |SSL| to
+// avoid an extra signature call.
+OPENSSL_EXPORT int SSL_serialize_handshake_hints(const SSL *ssl, CBB *out);
+
+// SSL_set_handshake_hints configures |ssl| to use |hints| as handshake hints.
+// It returns one on success and zero on error. The handshake will then continue
+// as before, but apply predicted values from |hints| where applicable.
+//
+// Hints may contain connection and session secrets, so they must not leak and
+// must come from a source trusted to terminate the connection. However, they
+// will not change |ssl|'s configuration. The caller is responsible for
+// serializing and applying options from the RPC server as needed. This ensures
+// |ssl|'s behavior is self-consistent and consistent with the caller's local
+// decisions.
+OPENSSL_EXPORT int SSL_set_handshake_hints(SSL *ssl, const uint8_t *hints,
+                                           size_t hints_len);
+
+
 // Obscure functions.
 
 // SSL_CTX_set_msg_callback installs |cb| as the message callback for |ctx|.
@@ -5148,6 +5248,7 @@
     const SSL *ssl, Span<const uint8_t> *out_read_traffic_secret,
     Span<const uint8_t> *out_write_traffic_secret);
 
+
 BSSL_NAMESPACE_END
 
 }  // extern C++
@@ -5371,6 +5472,7 @@
 #define SSL_R_ECH_SERVER_WOULD_HAVE_NO_RETRY_CONFIGS 313
 #define SSL_R_INVALID_CLIENT_HELLO_INNER 314
 #define SSL_R_INVALID_ALPN_PROTOCOL_LIST 315
+#define SSL_R_COULD_NOT_PARSE_HINTS 316
 #define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000
 #define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010
 #define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020
diff --git a/ssl/handoff.cc b/ssl/handoff.cc
index 6cf0a44..aededdb 100644
--- a/ssl/handoff.cc
+++ b/ssl/handoff.cc
@@ -15,6 +15,7 @@
 #include <openssl/ssl.h>
 
 #include <openssl/bytestring.h>
+#include <openssl/err.h>
 
 #include "internal.h"
 
@@ -708,3 +709,245 @@
 }
 
 BSSL_NAMESPACE_END
+
+using namespace bssl;
+
+int SSL_serialize_capabilities(const SSL *ssl, CBB *out) {
+  CBB seq;
+  if (!CBB_add_asn1(out, &seq, CBS_ASN1_SEQUENCE) ||
+      !serialize_features(&seq) ||  //
+      !CBB_flush(out)) {
+    return 0;
+  }
+
+  return 1;
+}
+
+int SSL_request_handshake_hints(SSL *ssl, const uint8_t *client_hello,
+                                size_t client_hello_len,
+                                const uint8_t *capabilities,
+                                size_t capabilities_len) {
+  if (SSL_is_dtls(ssl)) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
+    return 0;
+  }
+
+  CBS cbs, seq;
+  CBS_init(&cbs, capabilities, capabilities_len);
+  UniquePtr<SSL_HANDSHAKE_HINTS> hints = MakeUnique<SSL_HANDSHAKE_HINTS>();
+  if (hints == nullptr ||
+      !CBS_get_asn1(&cbs, &seq, CBS_ASN1_SEQUENCE) ||
+      !apply_remote_features(ssl, &seq)) {
+    return 0;
+  }
+
+  SSL3_STATE *const s3 = ssl->s3;
+  s3->v2_hello_done = true;
+  s3->has_message = true;
+
+  Array<uint8_t> client_hello_msg;
+  ScopedCBB client_hello_cbb;
+  CBB client_hello_body;
+  if (!ssl->method->init_message(ssl, client_hello_cbb.get(),
+                                 &client_hello_body, SSL3_MT_CLIENT_HELLO) ||
+      !CBB_add_bytes(&client_hello_body, client_hello, client_hello_len) ||
+      !ssl->method->finish_message(ssl, client_hello_cbb.get(),
+                                   &client_hello_msg)) {
+    return 0;
+  }
+
+  s3->hs_buf.reset(BUF_MEM_new());
+  if (!s3->hs_buf || !BUF_MEM_append(s3->hs_buf.get(), client_hello_msg.data(),
+                                     client_hello_msg.size())) {
+    return 0;
+  }
+
+  s3->hs->hints_requested = true;
+  s3->hs->hints = std::move(hints);
+  return 1;
+}
+
+// |SSL_HANDSHAKE_HINTS| is serialized as the following ASN.1 structure. We use
+// implicit tagging to make it a little more compact.
+//
+// HandshakeHints ::= SEQUENCE {
+//     serverRandom            [0] IMPLICIT OCTET STRING OPTIONAL,
+//     keyShareHint            [1] IMPLICIT KeyShareHint OPTIONAL,
+//     signatureHint           [2] IMPLICIT SignatureHint OPTIONAL,
+//     -- At most one of decryptedPSKHint or ignorePSKHint may be present. It
+//     -- corresponds to the first entry in pre_shared_keys. TLS 1.2 session
+//     -- tickets will use a separate hint, to ensure the caller does not mix
+//     -- them up.
+//     decryptedPSKHint        [3] IMPLICIT OCTET STRING OPTIONAL,
+//     ignorePSKHint           [4] IMPLICIT NULL OPTIONAL,
+// }
+//
+// KeyShareHint ::= SEQUENCE {
+//     groupId                 INTEGER,
+//     publicKey               OCTET STRING,
+//     secret                  OCTET STRING,
+// }
+//
+// SignatureHint ::= SEQUENCE {
+//     algorithm               INTEGER,
+//     input                   OCTET STRING,
+//     subjectPublicKeyInfo    OCTET STRING,
+//     signature               OCTET STRING,
+// }
+
+// HandshakeHints tags.
+static const unsigned kServerRandomTag = CBS_ASN1_CONTEXT_SPECIFIC | 0;
+static const unsigned kKeyShareHintTag =
+    CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 1;
+static const unsigned kSignatureHintTag =
+    CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 2;
+static const unsigned kDecryptedPSKTag = CBS_ASN1_CONTEXT_SPECIFIC | 3;
+static const unsigned kIgnorePSKTag = CBS_ASN1_CONTEXT_SPECIFIC | 4;
+
+int SSL_serialize_handshake_hints(const SSL *ssl, CBB *out) {
+  const SSL_HANDSHAKE *hs = ssl->s3->hs.get();
+  if (!ssl->server || !hs->hints_requested) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
+    return 0;
+  }
+
+  const SSL_HANDSHAKE_HINTS *hints = hs->hints.get();
+  CBB seq, server_random, key_share_hint, signature_hint, decrypted_psk,
+      ignore_psk;
+  if (!CBB_add_asn1(out, &seq, CBS_ASN1_SEQUENCE)) {
+    return 0;
+  }
+
+  if (!hints->server_random.empty()) {
+    if (!CBB_add_asn1(&seq, &server_random, kServerRandomTag) ||
+        !CBB_add_bytes(&server_random, hints->server_random.data(),
+                       hints->server_random.size())) {
+      return 0;
+    }
+  }
+
+  if (hints->key_share_group_id != 0 && !hints->key_share_public_key.empty() &&
+      !hints->key_share_secret.empty()) {
+    if (!CBB_add_asn1(&seq, &key_share_hint, kKeyShareHintTag) ||
+        !CBB_add_asn1_uint64(&key_share_hint, hints->key_share_group_id) ||
+        !CBB_add_asn1_octet_string(&key_share_hint,
+                                   hints->key_share_public_key.data(),
+                                   hints->key_share_public_key.size()) ||
+        !CBB_add_asn1_octet_string(&key_share_hint,
+                                   hints->key_share_secret.data(),
+                                   hints->key_share_secret.size())) {
+      return 0;
+    }
+  }
+
+  if (hints->signature_algorithm != 0 && !hints->signature_input.empty() &&
+      !hints->signature.empty()) {
+    if (!CBB_add_asn1(&seq, &signature_hint, kSignatureHintTag) ||
+        !CBB_add_asn1_uint64(&signature_hint, hints->signature_algorithm) ||
+        !CBB_add_asn1_octet_string(&signature_hint,
+                                    hints->signature_input.data(),
+                                    hints->signature_input.size()) ||
+        !CBB_add_asn1_octet_string(&signature_hint,
+                                    hints->signature_spki.data(),
+                                    hints->signature_spki.size()) ||
+        !CBB_add_asn1_octet_string(&signature_hint, hints->signature.data(),
+                                    hints->signature.size())) {
+      return 0;
+    }
+  }
+
+  if (!hints->decrypted_psk.empty()) {
+    if (!CBB_add_asn1(&seq, &decrypted_psk, kDecryptedPSKTag) ||
+        !CBB_add_bytes(&decrypted_psk, hints->decrypted_psk.data(),
+                       hints->decrypted_psk.size())) {
+      return 0;
+    }
+  }
+
+  if (hints->ignore_psk &&  //
+      !CBB_add_asn1(&seq, &ignore_psk, kIgnorePSKTag)) {
+    return 0;
+  }
+
+  return CBB_flush(out);
+}
+
+int SSL_set_handshake_hints(SSL *ssl, const uint8_t *hints, size_t hints_len) {
+  if (SSL_is_dtls(ssl)) {
+    OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED);
+    return 0;
+  }
+
+  UniquePtr<SSL_HANDSHAKE_HINTS> hints_obj = MakeUnique<SSL_HANDSHAKE_HINTS>();
+  if (hints_obj == nullptr) {
+    return 0;
+  }
+
+  CBS cbs, seq, server_random, key_share, signature_hint, ticket, ignore_psk;
+  int has_server_random, has_key_share, has_signature_hint, has_ticket,
+      has_ignore_psk;
+  CBS_init(&cbs, hints, hints_len);
+  if (!CBS_get_asn1(&cbs, &seq, CBS_ASN1_SEQUENCE) ||
+      !CBS_get_optional_asn1(&seq, &server_random, &has_server_random,
+                             kServerRandomTag) ||
+      !CBS_get_optional_asn1(&seq, &key_share, &has_key_share,
+                             kKeyShareHintTag) ||
+      !CBS_get_optional_asn1(&seq, &signature_hint, &has_signature_hint,
+                             kSignatureHintTag) ||
+      !CBS_get_optional_asn1(&seq, &ticket, &has_ticket, kDecryptedPSKTag) ||
+      !CBS_get_optional_asn1(&seq, &ignore_psk, &has_ignore_psk,
+                             kIgnorePSKTag)) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_COULD_NOT_PARSE_HINTS);
+    return 0;
+  }
+
+  if (has_server_random && !hints_obj->server_random.CopyFrom(server_random)) {
+    return 0;
+  }
+
+  if (has_key_share) {
+    uint64_t group_id;
+    CBS public_key, secret;
+    if (!CBS_get_asn1_uint64(&key_share, &group_id) ||  //
+        group_id == 0 || group_id > 0xffff ||
+        !CBS_get_asn1(&key_share, &public_key, CBS_ASN1_OCTETSTRING) ||
+        !hints_obj->key_share_public_key.CopyFrom(public_key) ||
+        !CBS_get_asn1(&key_share, &secret, CBS_ASN1_OCTETSTRING) ||
+        !hints_obj->key_share_secret.CopyFrom(secret)) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_COULD_NOT_PARSE_HINTS);
+      return 0;
+    }
+    hints_obj->key_share_group_id = static_cast<uint16_t>(group_id);
+  }
+
+  if (has_signature_hint) {
+    uint64_t sig_alg;
+    CBS input, spki, signature;
+    if (!CBS_get_asn1_uint64(&signature_hint, &sig_alg) ||  //
+        sig_alg == 0 || sig_alg > 0xffff ||
+        !CBS_get_asn1(&signature_hint, &input, CBS_ASN1_OCTETSTRING) ||
+        !hints_obj->signature_input.CopyFrom(input) ||
+        !CBS_get_asn1(&signature_hint, &spki, CBS_ASN1_OCTETSTRING) ||
+        !hints_obj->signature_spki.CopyFrom(spki) ||
+        !CBS_get_asn1(&signature_hint, &signature, CBS_ASN1_OCTETSTRING) ||
+        !hints_obj->signature.CopyFrom(signature)) {
+      OPENSSL_PUT_ERROR(SSL, SSL_R_COULD_NOT_PARSE_HINTS);
+      return 0;
+    }
+    hints_obj->signature_algorithm = static_cast<uint16_t>(sig_alg);
+  }
+
+  if (has_ticket && !hints_obj->decrypted_psk.CopyFrom(ticket)) {
+    return 0;
+  }
+
+  if (has_ignore_psk) {
+    if (CBS_len(&ignore_psk) != 0) {
+      return 0;
+    }
+    hints_obj->ignore_psk = true;
+  }
+
+  ssl->s3->hs->hints = std::move(hints_obj);
+  return 1;
+}
diff --git a/ssl/handshake.cc b/ssl/handshake.cc
index f5b6ca0..289c612 100644
--- a/ssl/handshake.cc
+++ b/ssl/handshake.cc
@@ -149,6 +149,7 @@
       pending_private_key_op(false),
       grease_seeded(false),
       handback(false),
+      hints_requested(false),
       cert_compression_negotiated(false),
       apply_jdk11_workaround(false) {
   assert(ssl);
@@ -713,6 +714,10 @@
         hs->wait = ssl_hs_ok;
         return 1;
 
+      case ssl_hs_hints_ready:
+        ssl->s3->rwstate = SSL_ERROR_HANDSHAKE_HINTS_READY;
+        return -1;
+
       case ssl_hs_ok:
         break;
     }
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc
index 1c5f0cf..2eaec01 100644
--- a/ssl/handshake_server.cc
+++ b/ssl/handshake_server.cc
@@ -830,6 +830,12 @@
   // or below.
   assert(!hs->ech_accept);
 
+  // TODO(davidben): Also compute hints for TLS 1.2. When doing so, update the
+  // check in bssl_shim.cc to test this.
+  if (hs->hints_requested) {
+    return ssl_hs_hints_ready;
+  }
+
   ssl->s3->early_data_reason = ssl_early_data_protocol_version;
 
   SSLMessage msg_unused;
diff --git a/ssl/internal.h b/ssl/internal.h
index 6edd26c..16e100b 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1577,6 +1577,7 @@
   ssl_hs_read_end_of_early_data,
   ssl_hs_read_change_cipher_spec,
   ssl_hs_certificate_verify,
+  ssl_hs_hints_ready,
 };
 
 enum ssl_grease_index_t {
@@ -1644,6 +1645,26 @@
   handback_max_value = handback_tls13,
 };
 
+// SSL_HANDSHAKE_HINTS contains handshake hints for a connection. See
+// |SSL_request_handshake_hints| and related functions.
+struct SSL_HANDSHAKE_HINTS {
+  static constexpr bool kAllowUniquePtr = true;
+
+  Array<uint8_t> server_random;
+
+  uint16_t key_share_group_id = 0;
+  Array<uint8_t> key_share_public_key;
+  Array<uint8_t> key_share_secret;
+
+  uint16_t signature_algorithm = 0;
+  Array<uint8_t> signature_input;
+  Array<uint8_t> signature_spki;
+  Array<uint8_t> signature;
+
+  Array<uint8_t> decrypted_psk;
+  bool ignore_psk = false;
+};
+
 struct SSL_HANDSHAKE {
   explicit SSL_HANDSHAKE(SSL *ssl);
   ~SSL_HANDSHAKE();
@@ -1842,6 +1863,13 @@
   // key_block is the record-layer key block for TLS 1.2 and earlier.
   Array<uint8_t> key_block;
 
+  // hints contains the handshake hints for this connection. If
+  // |hints_requested| is true, this field is non-null and contains the pending
+  // hints to filled as the predicted handshake progresses. Otherwise, this
+  // field, if non-null, contains hints configured by the caller and will
+  // influence the handshake on match.
+  UniquePtr<SSL_HANDSHAKE_HINTS> hints;
+
   // ech_accept, on the server, indicates whether the server should overwrite
   // part of ServerHello.random with the ECH accept_confirmation value.
   bool ech_accept : 1;
@@ -1931,6 +1959,11 @@
   // |SSL_apply_handoff|.
   bool handback : 1;
 
+  // hints_requested indicates the caller has requested handshake hints. Only
+  // the first round-trip of the handshake will complete, after which the
+  // |hints| structure can be serialized.
+  bool hints_requested : 1;
+
   // cert_compression_negotiated is true iff |cert_compression_alg_id| is valid.
   bool cert_compression_negotiated : 1;
 
diff --git a/ssl/s3_both.cc b/ssl/s3_both.cc
index 92d3a24..7ad8210 100644
--- a/ssl/s3_both.cc
+++ b/ssl/s3_both.cc
@@ -251,7 +251,8 @@
       MakeConstSpan(reinterpret_cast<const uint8_t *>(pending_hs_data->data),
                     pending_hs_data->length);
   if (ssl->quic_method) {
-    if (!ssl->quic_method->add_handshake_data(ssl, ssl->s3->write_level,
+    if ((ssl->s3->hs == nullptr || !ssl->s3->hs->hints_requested) &&
+        !ssl->quic_method->add_handshake_data(ssl, ssl->s3->write_level,
                                               data.data(), data.size())) {
       OPENSSL_PUT_ERROR(SSL, SSL_R_QUIC_INTERNAL_ERROR);
       return false;
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 522c09e..260d3cd7 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -1378,6 +1378,7 @@
     case SSL_ERROR_EARLY_DATA_REJECTED:
     case SSL_ERROR_WANT_CERTIFICATE_VERIFY:
     case SSL_ERROR_WANT_RENEGOTIATE:
+    case SSL_ERROR_HANDSHAKE_HINTS_READY:
       return ssl->s3->rwstate;
 
     case SSL_ERROR_WANT_READ: {
@@ -1463,6 +1464,8 @@
       return "HANDOFF";
     case SSL_ERROR_HANDBACK:
       return "HANDBACK";
+    case SSL_ERROR_HANDSHAKE_HINTS_READY:
+      return "HANDSHAKE_HINTS_READY";
     default:
       return nullptr;
   }
diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc
index 3a88cd1..e8d5f2e 100644
--- a/ssl/t1_lib.cc
+++ b/ssl/t1_lib.cc
@@ -4059,6 +4059,7 @@
     SSL_HANDSHAKE *hs, UniquePtr<SSL_SESSION> *out_session,
     bool *out_renew_ticket, Span<const uint8_t> ticket,
     Span<const uint8_t> session_id) {
+  SSL *const ssl = hs->ssl;
   *out_renew_ticket = false;
   out_session->reset();
 
@@ -4067,9 +4068,21 @@
     return ssl_ticket_aead_ignore_ticket;
   }
 
+  // Tickets in TLS 1.3 are tied into pre-shared keys (PSKs), unlike in TLS 1.2
+  // where that concept doesn't exist. The |decrypted_psk| and |ignore_psk|
+  // hints only apply to PSKs. We check the version to determine which this is.
+  const bool is_psk = ssl_protocol_version(ssl) >= TLS1_3_VERSION;
+
   Array<uint8_t> plaintext;
   enum ssl_ticket_aead_result_t result;
-  if (hs->ssl->session_ctx->ticket_aead_method != NULL) {
+  SSL_HANDSHAKE_HINTS *const hints = hs->hints.get();
+  if (is_psk && hints && !hs->hints_requested &&
+      !hints->decrypted_psk.empty()) {
+    result = plaintext.CopyFrom(hints->decrypted_psk) ? ssl_ticket_aead_success
+                                                      : ssl_ticket_aead_error;
+  } else if (is_psk && hints && !hs->hints_requested && hints->ignore_psk) {
+    result = ssl_ticket_aead_ignore_ticket;
+  } else if (ssl->session_ctx->ticket_aead_method != NULL) {
     result = ssl_decrypt_ticket_with_method(hs, &plaintext, out_renew_ticket,
                                             ticket);
   } else {
@@ -4078,9 +4091,8 @@
     // length should be well under the minimum size for the session material and
     // HMAC.
     if (ticket.size() < SSL_TICKET_KEY_NAME_LEN + EVP_MAX_IV_LENGTH) {
-      return ssl_ticket_aead_ignore_ticket;
-    }
-    if (hs->ssl->session_ctx->ticket_key_cb != NULL) {
+      result = ssl_ticket_aead_ignore_ticket;
+    } else if (ssl->session_ctx->ticket_key_cb != NULL) {
       result =
           ssl_decrypt_ticket_with_cb(hs, &plaintext, out_renew_ticket, ticket);
     } else {
@@ -4088,13 +4100,22 @@
     }
   }
 
+  if (is_psk && hints && hs->hints_requested) {
+    if (result == ssl_ticket_aead_ignore_ticket) {
+      hints->ignore_psk = true;
+    } else if (result == ssl_ticket_aead_success &&
+               !hints->decrypted_psk.CopyFrom(plaintext)) {
+      return ssl_ticket_aead_error;
+    }
+  }
+
   if (result != ssl_ticket_aead_success) {
     return result;
   }
 
   // Decode the session.
   UniquePtr<SSL_SESSION> session(SSL_SESSION_from_bytes(
-      plaintext.data(), plaintext.size(), hs->ssl->ctx.get()));
+      plaintext.data(), plaintext.size(), ssl->ctx.get()));
   if (!session) {
     ERR_clear_error();  // Don't leave an error on the queue.
     return ssl_ticket_aead_ignore_ticket;
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index 6a5fca3..8931349 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -671,6 +671,27 @@
             SSL_used_hello_retry_request(ssl) ? "" : "no ");
     return false;
   }
+
+  // Test that handshake hints correctly skipped the expected operations.
+  //
+  // TODO(davidben): Add support for TLS 1.2 hints and remove the version check.
+  // Also add a check for the session cache lookup.
+  if (config->handshake_hints && !config->allow_hint_mismatch &&
+      SSL_version(ssl) == TLS1_3_VERSION) {
+    const TestState *state = GetTestState(ssl);
+    if (!SSL_used_hello_retry_request(ssl) && state->used_private_key) {
+      fprintf(
+          stderr,
+          "Performed private key operation, but hint should have skipped it\n");
+      return false;
+    }
+
+    if (state->ticket_decrypt_done) {
+      fprintf(stderr,
+              "Performed ticket decryption, but hint should have skipped it\n");
+      return false;
+    }
+  }
   return true;
 }
 
@@ -697,6 +718,17 @@
   } else {
     SSL_set_connect_state(ssl.get());
   }
+  if (config->handshake_hints) {
+#if defined(HANDSHAKER_SUPPORTED)
+    GetTestState(ssl.get())->get_handshake_hints_cb =
+        [&](const SSL_CLIENT_HELLO *client_hello) {
+          return GetHandshakeHint(ssl.get(), writer, is_resume, client_hello);
+        };
+#else
+    fprintf(stderr, "The external handshaker can only be used on Linux\n");
+    return false;
+#endif
+  }
 
   int sock = Connect(config->port);
   if (sock == -1) {
@@ -1180,8 +1212,8 @@
   CRYPTO_library_init();
 
   TestConfig initial_config, resume_config, retry_config;
-  if (!ParseConfig(argc - 1, argv + 1, &initial_config, &resume_config,
-                   &retry_config)) {
+  if (!ParseConfig(argc - 1, argv + 1, /*is_shim=*/true, &initial_config,
+                   &resume_config, &retry_config)) {
     return Usage(argv[0]);
   }
 
diff --git a/ssl/test/fuzzer.h b/ssl/test/fuzzer.h
index eacbdc0..8f7a355 100644
--- a/ssl/test/fuzzer.h
+++ b/ssl/test/fuzzer.h
@@ -515,6 +515,15 @@
           break;
         }
 
+        case kHintsTag: {
+          CBS hints;
+          if (!CBS_get_u24_length_prefixed(cbs, &hints)) {
+            return nullptr;
+          }
+          SSL_set_handshake_hints(ssl.get(), CBS_data(&hints), CBS_len(&hints));
+          break;
+        }
+
         default:
           return nullptr;
       }
diff --git a/ssl/test/fuzzer_tags.h b/ssl/test/fuzzer_tags.h
index eb9991d..3946df7 100644
--- a/ssl/test/fuzzer_tags.h
+++ b/ssl/test/fuzzer_tags.h
@@ -45,4 +45,7 @@
 // kHandbackTag is followed by te output of |SSL_serialize_handback|.
 static const uint16_t kHandbackTag = 4;
 
+// kHintsTag is followed by the output of |SSL_serialize_handshake_hints|.
+static const uint16_t kHintsTag = 5;
+
 #endif  // HEADER_SSL_TEST_FUZZER_TAGS
diff --git a/ssl/test/handshake_util.cc b/ssl/test/handshake_util.cc
index 7fa0fb5..0fdf47f 100644
--- a/ssl/test/handshake_util.cc
+++ b/ssl/test/handshake_util.cc
@@ -34,6 +34,7 @@
 #include "test_config.h"
 #include "test_state.h"
 
+#include <openssl/bytestring.h>
 #include <openssl/ssl.h>
 
 using namespace bssl;
@@ -232,7 +233,7 @@
       return false;
     }
     switch (msg) {
-      case kControlMsgHandback:
+      case kControlMsgDone:
         return true;
       case kControlMsgError:
         return false;
@@ -304,22 +305,68 @@
 
 class ScopedFD {
  public:
-  explicit ScopedFD(int fd): fd_(fd) {}
-  ~ScopedFD() { Close(); }
-  ScopedFD(const ScopedFD &) = delete;
-  ScopedFD &operator=(const ScopedFD &) = delete;
+  ScopedFD() : fd_(-1) {}
+  explicit ScopedFD(int fd) : fd_(fd) {}
+  ~ScopedFD() { Reset(); }
 
-  void Close() {
+  ScopedFD(ScopedFD &&other) { *this = std::move(other); }
+  ScopedFD &operator=(ScopedFD &&other) {
+    Reset(other.fd_);
+    other.fd_ = -1;
+    return *this;
+  }
+
+  int fd() const { return fd_; }
+
+  void Reset(int fd = -1) {
     if (fd_ >= 0) {
       close(fd_);
     }
-    fd_ = -1;
+    fd_ = fd;
   }
 
  private:
   int fd_;
 };
 
+class ScopedProcess {
+ public:
+  ScopedProcess() : pid_(-1) {}
+  ~ScopedProcess() { Reset(); }
+
+  ScopedProcess(ScopedProcess &&other) { *this = std::move(other); }
+  ScopedProcess &operator=(ScopedProcess &&other) {
+    Reset(other.pid_);
+    other.pid_ = -1;
+    return *this;
+  }
+
+  pid_t pid() const { return pid_; }
+
+  void Reset(pid_t pid = -1) {
+    if (pid_ >= 0) {
+      kill(pid_, SIGTERM);
+      int unused;
+      Wait(&unused);
+    }
+    pid_ = pid;
+  }
+
+  bool Wait(int *out_status) {
+    if (pid_ < 0) {
+      return false;
+    }
+    if (waitpid_eintr(pid_, out_status, 0) != pid_) {
+      return false;
+    }
+    pid_ = -1;
+    return true;
+  }
+
+ private:
+  pid_t pid_;
+};
+
 class FileActionsDestroyer {
  public:
   explicit FileActionsDestroyer(posix_spawn_file_actions_t *actions)
@@ -332,11 +379,9 @@
   posix_spawn_file_actions_t *actions_;
 };
 
-// RunHandshaker forks and execs the handshaker binary, handing off |input|,
-// and, after proxying some amount of handshake traffic, handing back |out|.
-static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume,
-                          Span<const uint8_t> input,
-                          std::vector<uint8_t> *out) {
+static bool StartHandshaker(ScopedProcess *out, ScopedFD *out_control,
+                            const TestConfig *config, bool is_resume,
+                            posix_spawn_file_actions_t *actions) {
   if (config->handshaker_path.empty()) {
     fprintf(stderr, "no -handshaker-path specified\n");
     return false;
@@ -347,13 +392,62 @@
     return false;
   }
 
+  std::vector<const char *> args;
+  args.push_back(config->handshaker_path.c_str());
+  static const char kResumeFlag[] = "-handshaker-resume";
+  if (is_resume) {
+    args.push_back(kResumeFlag);
+  }
+  // config->argv omits argv[0].
+  for (int j = 0; j < config->argc; ++j) {
+    args.push_back(config->argv[j]);
+  }
+  args.push_back(nullptr);
+
   // A datagram socket guarantees that writes are all-or-nothing.
   int control[2];
   if (socketpair(AF_LOCAL, SOCK_DGRAM, 0, control) != 0) {
     perror("socketpair");
     return false;
   }
-  ScopedFD control0_closer(control[0]), control1_closer(control[1]);
+  ScopedFD scoped_control0(control[0]), scoped_control1(control[1]);
+
+  assert(control[1] != kFdHandshakerToProxy);
+  assert(control[1] != kFdProxyToHandshaker);
+  if (posix_spawn_file_actions_addclose(actions, control[0]) != 0) {
+    return false;
+  }
+  if (control[1] != kFdControl &&
+      posix_spawn_file_actions_adddup2(actions, control[1], kFdControl) != 0) {
+    return false;
+  }
+
+  fflush(stdout);
+  fflush(stderr);
+
+  // MSan doesn't know that |posix_spawn| initializes its output, so initialize
+  // it to -1.
+  pid_t pid = -1;
+  if (posix_spawn(&pid, args[0], actions, nullptr,
+                  const_cast<char *const *>(args.data()), environ) != 0) {
+    return false;
+  }
+
+  out->Reset(pid);
+  *out_control = std::move(scoped_control0);
+  return true;
+}
+
+// RunHandshaker forks and execs the handshaker binary, handing off |input|,
+// and, after proxying some amount of handshake traffic, handing back |out|.
+static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume,
+                          Span<const uint8_t> input,
+                          std::vector<uint8_t> *out) {
+  posix_spawn_file_actions_t actions;
+  if (posix_spawn_file_actions_init(&actions) != 0) {
+    return false;
+  }
+  FileActionsDestroyer actions_destroyer(&actions);
 
   int rfd[2], wfd[2];
   // We use pipes, rather than some other mechanism, for their buffers.  During
@@ -378,38 +472,15 @@
   }
   ScopedFD wfd0_closer(wfd[0]), wfd1_closer(wfd[1]);
 
-  fflush(stdout);
-  fflush(stderr);
+  assert(kFdControl != rfd[0]);
+  assert(kFdControl != wfd[1]);
+  assert(kFdHandshakerToProxy != rfd[0]);
+  assert(kFdProxyToHandshaker != wfd[1]);
 
-  std::vector<const char *> args;
-  args.push_back(config->handshaker_path.c_str());
-  static const char kResumeFlag[] = "-handshaker-resume";
-  if (is_resume) {
-    args.push_back(kResumeFlag);
-  }
-  // config->argv omits argv[0].
-  for (int j = 0; j < config->argc; ++j) {
-    args.push_back(config->argv[j]);
-  }
-  args.push_back(nullptr);
-
-  posix_spawn_file_actions_t actions;
-  if (posix_spawn_file_actions_init(&actions) != 0) {
-    return false;
-  }
-  FileActionsDestroyer actions_destroyer(&actions);
-  if (posix_spawn_file_actions_addclose(&actions, control[0]) != 0 ||
-      posix_spawn_file_actions_addclose(&actions, rfd[1]) != 0 ||
+  if (posix_spawn_file_actions_addclose(&actions, rfd[1]) != 0 ||
       posix_spawn_file_actions_addclose(&actions, wfd[0]) != 0) {
     return false;
   }
-  assert(kFdControl != rfd[0]);
-  assert(kFdControl != wfd[1]);
-  if (control[1] != kFdControl &&
-      posix_spawn_file_actions_adddup2(&actions, control[1], kFdControl) != 0) {
-    return false;
-  }
-  assert(kFdProxyToHandshaker != wfd[1]);
   if (rfd[0] != kFdProxyToHandshaker &&
       posix_spawn_file_actions_adddup2(&actions, rfd[0],
                                        kFdProxyToHandshaker) != 0) {
@@ -418,28 +489,25 @@
   if (wfd[1] != kFdHandshakerToProxy &&
       posix_spawn_file_actions_adddup2(&actions, wfd[1],
                                        kFdHandshakerToProxy) != 0) {
-      return false;
-  }
-
-  // MSan doesn't know that |posix_spawn| initializes its output, so initialize
-  // it to -1.
-  pid_t handshaker_pid = -1;
-  if (posix_spawn(&handshaker_pid, args[0], &actions, nullptr,
-                  const_cast<char *const *>(args.data()), environ) != 0) {
     return false;
   }
 
-  control1_closer.Close();
-  rfd0_closer.Close();
-  wfd1_closer.Close();
+  ScopedProcess handshaker;
+  ScopedFD control;
+  if (!StartHandshaker(&handshaker, &control, config, is_resume, &actions)) {
+    return false;
+  }
 
-  if (write_eintr(control[0], input.data(), input.size()) == -1) {
+  rfd0_closer.Reset();
+  wfd1_closer.Reset();
+
+  if (write_eintr(control.fd(), input.data(), input.size()) == -1) {
     perror("write");
     return false;
   }
-  bool ok = Proxy(bio, config->async, control[0], rfd[1], wfd[0]);
+  bool ok = Proxy(bio, config->async, control.fd(), rfd[1], wfd[0]);
   int wstatus;
-  if (waitpid_eintr(handshaker_pid, &wstatus, 0) != handshaker_pid) {
+  if (!handshaker.Wait(&wstatus)) {
     perror("waitpid");
     return false;
   }
@@ -453,7 +521,7 @@
 
   constexpr size_t kBufSize = 1024 * 1024;
   std::vector<uint8_t> buf(kBufSize);
-  ssize_t len = read_eintr(control[0], buf.data(), buf.size());
+  ssize_t len = read_eintr(control.fd(), buf.data(), buf.size());
   if (len == -1) {
     perror("read");
     return false;
@@ -463,6 +531,66 @@
   return true;
 }
 
+static bool RequestHandshakeHint(const TestConfig *config, bool is_resume,
+                                 Span<const uint8_t> input, bool *out_has_hints,
+                                 std::vector<uint8_t> *out_hints) {
+  posix_spawn_file_actions_t actions;
+  if (posix_spawn_file_actions_init(&actions) != 0) {
+    return false;
+  }
+  FileActionsDestroyer actions_destroyer(&actions);
+  ScopedProcess handshaker;
+  ScopedFD control;
+  if (!StartHandshaker(&handshaker, &control, config, is_resume, &actions)) {
+    return false;
+  }
+
+  if (write_eintr(control.fd(), input.data(), input.size()) == -1) {
+    perror("write");
+    return false;
+  }
+
+  char msg;
+  if (read_eintr(control.fd(), &msg, 1) != 1) {
+    perror("read");
+    return false;
+  }
+
+  switch (msg) {
+    case kControlMsgDone: {
+      constexpr size_t kBufSize = 1024 * 1024;
+      out_hints->resize(kBufSize);
+      ssize_t len =
+          read_eintr(control.fd(), out_hints->data(), out_hints->size());
+      if (len == -1) {
+        perror("read");
+        return false;
+      }
+      out_hints->resize(len);
+      *out_has_hints = true;
+      break;
+    }
+    case kControlMsgError:
+      *out_has_hints = false;
+      break;
+    default:
+      fprintf(stderr, "Unknown control message from handshaker: %c\n", msg);
+      return false;
+  }
+
+  int wstatus;
+  if (!handshaker.Wait(&wstatus)) {
+    perror("waitpid");
+    return false;
+  }
+  if (wstatus) {
+    fprintf(stderr, "handshaker exited irregularly\n");
+    return false;
+  }
+
+  return true;
+}
+
 // PrepareHandoff accepts the |ClientHello| from |ssl| and serializes state to
 // be passed to the handshaker.  The serialized state includes both the SSL
 // handoff, as well test-related state.
@@ -543,4 +671,35 @@
   return true;
 }
 
+bool GetHandshakeHint(SSL *ssl, SettingsWriter *writer, bool is_resume,
+                      const SSL_CLIENT_HELLO *client_hello) {
+  ScopedCBB input;
+  CBB child;
+  if (!CBB_init(input.get(), client_hello->client_hello_len + 256) ||
+      !CBB_add_u24_length_prefixed(input.get(), &child) ||
+      !CBB_add_bytes(&child, client_hello->client_hello,
+                     client_hello->client_hello_len) ||
+      !CBB_add_u24_length_prefixed(input.get(), &child) ||
+      !SSL_serialize_capabilities(ssl, &child) ||  //
+      !CBB_flush(input.get())) {
+    return false;
+  }
+
+  bool has_hints;
+  std::vector<uint8_t> hints;
+  if (!RequestHandshakeHint(
+          GetTestConfig(ssl), is_resume,
+          MakeConstSpan(CBB_data(input.get()), CBB_len(input.get())),
+          &has_hints, &hints)) {
+    return false;
+  }
+  if (has_hints &&
+      (!writer->WriteHints(hints) ||
+       !SSL_set_handshake_hints(ssl, hints.data(), hints.size()))) {
+    return false;
+  }
+
+  return true;
+}
+
 #endif  // defined(HANDSHAKER_SUPPORTED)
diff --git a/ssl/test/handshake_util.h b/ssl/test/handshake_util.h
index fa365a4..7fee20b 100644
--- a/ssl/test/handshake_util.h
+++ b/ssl/test/handshake_util.h
@@ -44,17 +44,24 @@
 bool DoSplitHandshake(bssl::UniquePtr<SSL> *ssl, SettingsWriter *writer,
                       bool is_resume);
 
+// GetHandshakeHint requests a handshake hint from the handshaker process and
+// configures the result on |ssl|. It returns true on success and false on
+// error.
+bool GetHandshakeHint(SSL *ssl, SettingsWriter *writer, bool is_resume,
+                      const SSL_CLIENT_HELLO *client_hello);
+
 // The protocol between the proxy and the handshaker is defined by these
-// single-character prefixes.
+// single-character prefixes. |kControlMsgDone| uses 'H' for compatibility with
+// older binaries.
 constexpr char kControlMsgWantRead = 'R';        // Handshaker wants data
 constexpr char kControlMsgWriteCompleted = 'W';  // Proxy has sent data
-constexpr char kControlMsgHandback = 'H';        // Proxy should resume control
+constexpr char kControlMsgDone = 'H';            // Proxy should resume control
 constexpr char kControlMsgError = 'E';           // Handshaker hit an error
 
 // The protocol between the proxy and handshaker uses these file descriptors.
-constexpr int kFdControl = 3;                    // Bi-directional dgram socket.
-constexpr int kFdProxyToHandshaker = 4;          // Uni-directional pipe.
-constexpr int kFdHandshakerToProxy = 5;          // Uni-directional pipe.
+constexpr int kFdControl = 20;                   // Bi-directional dgram socket.
+constexpr int kFdProxyToHandshaker = 21;         // Uni-directional pipe.
+constexpr int kFdHandshakerToProxy = 22;         // Uni-directional pipe.
 #endif  // HANDSHAKER_SUPPORTED
 
 #endif  // HEADER_TEST_HANDSHAKE
diff --git a/ssl/test/handshaker.cc b/ssl/test/handshaker.cc
index 8129091..ac89063 100644
--- a/ssl/test/handshaker.cc
+++ b/ssl/test/handshaker.cc
@@ -12,11 +12,14 @@
  * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
  * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */
 
+#include <assert.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <signal.h>
 #include <unistd.h>
 
+#include <memory>
+
 #include <openssl/bytestring.h>
 #include <openssl/rand.h>
 #include <openssl/ssl.h>
@@ -55,7 +58,11 @@
   if (!ctx) {
     return false;
   }
-  UniquePtr<SSL> ssl = config->NewSSL(ctx.get(), nullptr, nullptr);
+  UniquePtr<SSL> ssl =
+      config->NewSSL(ctx.get(), /*session=*/nullptr, /*test_state=*/nullptr);
+  if (!ssl) {
+    return false;
+  }
 
   // Set |O_NONBLOCK| in order to break out of the loop when we hit
   // |SSL_ERROR_WANT_READ|, so that we can send |kControlMsgWantRead| to the
@@ -100,6 +107,8 @@
     }
   }
   if (!HandbackReady(ssl.get(), ret)) {
+    fprintf(stderr, "Handshaker: %s\n",
+            SSL_error_description(SSL_get_error(ssl.get(), ret)));
     ERR_print_errors_fp(stderr);
     return false;
   }
@@ -115,7 +124,7 @@
     return false;
   }
 
-  char msg = kControlMsgHandback;
+  char msg = kControlMsgDone;
   if (write_eintr(control, &msg, 1) == -1 ||
       write_eintr(control, CBB_data(output.get()), CBB_len(output.get())) ==
           -1) {
@@ -125,6 +134,84 @@
   return true;
 }
 
+bool GenerateHandshakeHint(const TestConfig *config,
+                           bssl::Span<const uint8_t> request, int control) {
+  // The handshake hint contains the ClientHello and the capabilities string.
+  CBS cbs = request;
+  CBS client_hello, capabilities;
+  if (!CBS_get_u24_length_prefixed(&cbs, &client_hello) ||
+      !CBS_get_u24_length_prefixed(&cbs, &capabilities) ||  //
+      CBS_len(&cbs) != 0) {
+    fprintf(stderr, "Handshaker: Could not parse hint request\n");
+    return false;
+  }
+
+  UniquePtr<SSL_CTX> ctx = config->SetupCtx(/*old_ctx=*/nullptr);
+  if (!ctx) {
+    return false;
+  }
+
+  UniquePtr<SSL> ssl =
+      config->NewSSL(ctx.get(), /*session=*/nullptr,
+                     std::unique_ptr<TestState>(new TestState));
+  if (!ssl) {
+    return false;
+  }
+
+  // TODO(davidben): When split handshakes is replaced, move this into |NewSSL|.
+  assert(config->is_server);
+  SSL_set_accept_state(ssl.get());
+
+  if (!SSL_request_handshake_hints(
+          ssl.get(), CBS_data(&client_hello), CBS_len(&client_hello),
+          CBS_data(&capabilities), CBS_len(&capabilities))) {
+    fprintf(stderr, "Handshaker: SSL_request_handshake_hints failed\n");
+    return false;
+  }
+
+  int ret = 0;
+  do {
+    ret = CheckIdempotentError("SSL_do_handshake", ssl.get(),
+                               [&] { return SSL_do_handshake(ssl.get()); });
+  } while (RetryAsync(ssl.get(), ret));
+
+  if (ret > 0) {
+    fprintf(stderr, "Handshaker: handshake unexpectedly succeeded.\n");
+    return false;
+  }
+
+  if (SSL_get_error(ssl.get(), ret) != SSL_ERROR_HANDSHAKE_HINTS_READY) {
+    // Errors here may be expected if the test is testing a failing case. The
+    // shim should continue executing without a hint, so we report an error
+    // "successfully". This allows the shim to distinguish this from the other
+    // unexpected error cases.
+    //
+    // We intentionally avoid printing the error in this case, to avoid mixing
+    // up test expectations with errors from the shim.
+    char msg = kControlMsgError;
+    if (write_eintr(control, &msg, 1) == -1) {
+      return false;
+    }
+    return true;
+  }
+
+  bssl::ScopedCBB hints;
+  if (!CBB_init(hints.get(), 256) ||
+      !SSL_serialize_handshake_hints(ssl.get(), hints.get())) {
+    fprintf(stderr, "Handshaker: failed to serialize handshake hints\n");
+    return false;
+  }
+
+  char msg = kControlMsgDone;
+  if (write_eintr(control, &msg, 1) == -1 ||
+      write_eintr(control, CBB_data(hints.get()), CBB_len(hints.get())) == -1) {
+    perror("write");
+    return false;
+  }
+
+  return true;
+}
+
 int SignalError() {
   const char msg = kControlMsgError;
   if (write_eintr(kFdControl, &msg, 1) != 1) {
@@ -137,12 +224,12 @@
 
 int main(int argc, char **argv) {
   TestConfig initial_config, resume_config, retry_config;
-  if (!ParseConfig(argc - 1, argv + 1, &initial_config, &resume_config,
-                   &retry_config)) {
+  if (!ParseConfig(argc - 1, argv + 1, /*is_shim=*/false, &initial_config,
+                   &resume_config, &retry_config)) {
     return SignalError();
   }
-  const TestConfig *config = initial_config.handshaker_resume
-      ? &resume_config : &initial_config;
+  const TestConfig *config =
+      initial_config.handshaker_resume ? &resume_config : &initial_config;
 #if defined(BORINGSSL_UNSAFE_DETERMINISTIC_MODE)
   if (initial_config.handshaker_resume) {
     // If the PRNG returns exactly the same values when trying to resume then a
@@ -157,15 +244,23 @@
   // read() will return the entire message in one go, because it's a datagram
   // socket.
   constexpr size_t kBufSize = 1024 * 1024;
-  std::vector<uint8_t> handoff(kBufSize);
-  ssize_t len = read_eintr(kFdControl, handoff.data(), handoff.size());
+  std::vector<uint8_t> request(kBufSize);
+  ssize_t len = read_eintr(kFdControl, request.data(), request.size());
   if (len == -1) {
     perror("read");
     return 2;
   }
-  if (!Handshaker(config, kFdProxyToHandshaker, kFdHandshakerToProxy, handoff,
-                  kFdControl)) {
-    return SignalError();
+  request.resize(static_cast<size_t>(len));
+
+  if (config->handshake_hints) {
+    if (!GenerateHandshakeHint(config, request, kFdControl)) {
+      return SignalError();
+    }
+  } else {
+    if (!Handshaker(config, kFdProxyToHandshaker, kFdHandshakerToProxy,
+                    request, kFdControl)) {
+      return SignalError();
+    }
   }
   return 0;
 }
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index be934fe..9e400f4 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -63,6 +63,7 @@
 	pipe               = flag.Bool("pipe", false, "If true, print status output suitable for piping into another program.")
 	testToRun          = flag.String("test", "", "Semicolon-separated patterns of tests to run, or empty to run all tests")
 	skipTest           = flag.String("skip", "", "Semicolon-separated patterns of tests to skip")
+	allowHintMismatch  = flag.String("allow-hint-mismatch", "", "Semicolon-separated patterns of tests where hints may mismatch")
 	numWorkersFlag     = flag.Int("num-workers", runtime.NumCPU(), "The number of workers to run in parallel.")
 	shimPath           = flag.String("shim-path", "../../../build/ssl/test/bssl_shim", "The location of the shim binary.")
 	handshakerPath     = flag.String("handshaker-path", "../../../build/ssl/test/handshaker", "The location of the handshaker binary.")
@@ -698,6 +699,9 @@
 	// should retry for early rejection. In a server test, this is whether the
 	// test expects the shim to reject early data.
 	expectEarlyDataRejected bool
+	// skipSplitHandshake, if true, will skip the generation of a split
+	// handshake copy of the test.
+	skipSplitHandshake bool
 }
 
 var testCases []testCase
@@ -1835,12 +1839,12 @@
 	return ret
 }
 
-func convertToSplitHandshakeTests(tests []testCase) (splitHandshakeTests []testCase) {
+func convertToSplitHandshakeTests(tests []testCase) (splitHandshakeTests []testCase, err error) {
 	var stdout bytes.Buffer
 	shim := exec.Command(*shimPath, "-is-handshaker-supported")
 	shim.Stdout = &stdout
 	if err := shim.Run(); err != nil {
-		panic(err)
+		return nil, err
 	}
 
 	switch strings.TrimSpace(string(stdout.Bytes())) {
@@ -1849,14 +1853,20 @@
 	case "Yes":
 		break
 	default:
-		panic("Unknown output from shim: 0x" + hex.EncodeToString(stdout.Bytes()))
+		return nil, fmt.Errorf("unknown output from shim: %q", stdout.Bytes())
+	}
+
+	var allowHintMismatchPattern []string
+	if len(*allowHintMismatch) > 0 {
+		allowHintMismatchPattern = strings.Split(*allowHintMismatch, ";")
 	}
 
 NextTest:
 	for _, test := range tests {
 		if test.protocol != tls ||
 			test.testType != serverTest ||
-			strings.Contains(test.name, "DelegatedCredentials") {
+			strings.Contains(test.name, "DelegatedCredentials") ||
+			test.skipSplitHandshake {
 			continue
 		}
 
@@ -1868,14 +1878,40 @@
 
 		shTest := test
 		shTest.name += "-Split"
-		shTest.flags = make([]string, len(test.flags), len(test.flags)+1)
+		shTest.flags = make([]string, len(test.flags), len(test.flags)+3)
 		copy(shTest.flags, test.flags)
 		shTest.flags = append(shTest.flags, "-handoff", "-handshaker-path", *handshakerPath)
 
 		splitHandshakeTests = append(splitHandshakeTests, shTest)
 	}
 
-	return splitHandshakeTests
+	for _, test := range tests {
+		if test.protocol == dtls ||
+			test.testType != serverTest {
+			continue
+		}
+
+		var matched bool
+		if len(allowHintMismatchPattern) > 0 {
+			matched, err = match(allowHintMismatchPattern, nil, test.name)
+			if err != nil {
+				return nil, fmt.Errorf("error matching pattern: %s", err)
+			}
+		}
+
+		shTest := test
+		shTest.name += "-Hints"
+		shTest.flags = make([]string, len(test.flags), len(test.flags)+3)
+		copy(shTest.flags, test.flags)
+		shTest.flags = append(shTest.flags, "-handshake-hints", "-handshaker-path", *handshakerPath)
+		if matched {
+			shTest.flags = append(shTest.flags, "-allow-hint-mismatch")
+		}
+
+		splitHandshakeTests = append(splitHandshakeTests, shTest)
+	}
+
+	return splitHandshakeTests, nil
 }
 
 func addBasicTests() {
@@ -17275,6 +17311,229 @@
 	}
 }
 
+func addHintMismatchTests() {
+	// Each of these tests skips split handshakes because split handshakes does
+	// not handle a mismatch between shim and handshaker. Handshake hints,
+	// however, are designed to tolerate the mismatch.
+	//
+	// Note also these tests do not specify -handshake-hints directly. Instead,
+	// we define normal tests, that run even without a handshaker, and rely on
+	// convertToSplitHandshakeTests to generate a handshaker hints variant. This
+	// avoids repeating the -is-handshaker-supported and -handshaker-path logic.
+	// (While not useful, the tests will still pass without a handshaker.)
+	for _, protocol := range []protocol{tls, quic} {
+		// If the signing payload is different, the handshake still completes
+		// successfully. Different ALPN preferences will trigger a mismatch.
+		testCases = append(testCases, testCase{
+			name:               protocol.String() + "-HintMismatch-SignatureInput",
+			testType:           serverTest,
+			protocol:           protocol,
+			skipSplitHandshake: true,
+			config: Config{
+				MinVersion: VersionTLS13,
+				MaxVersion: VersionTLS13,
+				NextProtos: []string{"foo", "bar"},
+			},
+			flags: []string{
+				"-allow-hint-mismatch",
+				"-on-shim-select-alpn", "foo",
+				"-on-handshaker-select-alpn", "bar",
+			},
+			expectations: connectionExpectations{
+				nextProto:     "foo",
+				nextProtoType: alpn,
+			},
+		})
+
+		// The shim and handshaker may have different curve preferences.
+		testCases = append(testCases, testCase{
+			name:               protocol.String() + "-HintMismatch-KeyShare",
+			testType:           serverTest,
+			protocol:           protocol,
+			skipSplitHandshake: true,
+			config: Config{
+				MinVersion: VersionTLS13,
+				MaxVersion: VersionTLS13,
+				// Send both curves in the key share list, to avoid getting
+				// mixed up with HelloRetryRequest.
+				DefaultCurves: []CurveID{CurveX25519, CurveP256},
+			},
+			flags: []string{
+				"-allow-hint-mismatch",
+				"-on-shim-curves", strconv.Itoa(int(CurveX25519)),
+				"-on-handshaker-curves", strconv.Itoa(int(CurveP256)),
+			},
+			expectations: connectionExpectations{
+				curveID: CurveX25519,
+			},
+		})
+
+		// If the handshaker does HelloRetryRequest, it will omit most hints.
+		// The shim should still work.
+		testCases = append(testCases, testCase{
+			name:               protocol.String() + "-HintMismatch-HandshakerHelloRetryRequest",
+			testType:           serverTest,
+			protocol:           protocol,
+			skipSplitHandshake: true,
+			config: Config{
+				MinVersion:    VersionTLS13,
+				MaxVersion:    VersionTLS13,
+				DefaultCurves: []CurveID{CurveX25519},
+			},
+			flags: []string{
+				"-allow-hint-mismatch",
+				"-on-shim-curves", strconv.Itoa(int(CurveX25519)),
+				"-on-handshaker-curves", strconv.Itoa(int(CurveP256)),
+			},
+			expectations: connectionExpectations{
+				curveID: CurveX25519,
+			},
+		})
+
+		// If the shim does HelloRetryRequest, the hints from the handshaker
+		// will be ignored. This is not reported as a mismatch because hints
+		// would not have helped the shim anyway.
+		testCases = append(testCases, testCase{
+			name:               protocol.String() + "-HintMismatch-ShimHelloRetryRequest",
+			testType:           serverTest,
+			protocol:           protocol,
+			skipSplitHandshake: true,
+			config: Config{
+				MinVersion:    VersionTLS13,
+				MaxVersion:    VersionTLS13,
+				DefaultCurves: []CurveID{CurveX25519},
+			},
+			flags: []string{
+				"-on-shim-curves", strconv.Itoa(int(CurveP256)),
+				"-on-handshaker-curves", strconv.Itoa(int(CurveX25519)),
+			},
+			expectations: connectionExpectations{
+				curveID: CurveP256,
+			},
+		})
+
+		// The shim and handshaker may have different signature algorithm
+		// preferences.
+		testCases = append(testCases, testCase{
+			name:               protocol.String() + "-HintMismatch-SignatureAlgorithm",
+			testType:           serverTest,
+			protocol:           protocol,
+			skipSplitHandshake: true,
+			config: Config{
+				MinVersion: VersionTLS13,
+				MaxVersion: VersionTLS13,
+				VerifySignatureAlgorithms: []signatureAlgorithm{
+					signatureRSAPSSWithSHA256,
+					signatureRSAPSSWithSHA384,
+				},
+			},
+			flags: []string{
+				"-allow-hint-mismatch",
+				"-cert-file", path.Join(*resourceDir, rsaCertificateFile),
+				"-key-file", path.Join(*resourceDir, rsaKeyFile),
+				"-on-shim-signing-prefs", strconv.Itoa(int(signatureRSAPSSWithSHA256)),
+				"-on-handshaker-signing-prefs", strconv.Itoa(int(signatureRSAPSSWithSHA384)),
+			},
+			expectations: connectionExpectations{
+				peerSignatureAlgorithm: signatureRSAPSSWithSHA256,
+			},
+		})
+
+		// The shim and handshaker may disagree on whether resumption is allowed.
+		// We run the first connection with tickets enabled, so the client is
+		// issued a ticket, then disable tickets on the second connection.
+		testCases = append(testCases, testCase{
+			name:               protocol.String() + "-HintMismatch-NoTickets1",
+			testType:           serverTest,
+			protocol:           protocol,
+			skipSplitHandshake: true,
+			config: Config{
+				MinVersion: VersionTLS13,
+				MaxVersion: VersionTLS13,
+			},
+			flags: []string{
+				"-on-resume-allow-hint-mismatch",
+				"-on-shim-on-resume-no-ticket",
+			},
+			resumeSession:        true,
+			expectResumeRejected: true,
+		})
+		testCases = append(testCases, testCase{
+			name:               protocol.String() + "-HintMismatch-NoTickets2",
+			testType:           serverTest,
+			protocol:           protocol,
+			skipSplitHandshake: true,
+			config: Config{
+				MinVersion: VersionTLS13,
+				MaxVersion: VersionTLS13,
+			},
+			flags: []string{
+				"-on-resume-allow-hint-mismatch",
+				"-on-handshaker-on-resume-no-ticket",
+			},
+			resumeSession: true,
+		})
+
+		// The shim and handshaker may disagree on whether to request a client
+		// certificate.
+		testCases = append(testCases, testCase{
+			name:               protocol.String() + "-HintMismatch-CertificateRequest",
+			testType:           serverTest,
+			protocol:           protocol,
+			skipSplitHandshake: true,
+			config: Config{
+				MinVersion:   VersionTLS13,
+				MaxVersion:   VersionTLS13,
+				Certificates: []Certificate{rsaCertificate},
+			},
+			flags: []string{
+				"-allow-hint-mismatch",
+				"-on-shim-require-any-client-certificate",
+			},
+		})
+
+		// The shim and handshaker may negotiate different versions altogether.
+		if protocol != quic {
+			testCases = append(testCases, testCase{
+				name:               protocol.String() + "-HintMismatch-Version1",
+				testType:           serverTest,
+				protocol:           protocol,
+				skipSplitHandshake: true,
+				config: Config{
+					MinVersion: VersionTLS12,
+					MaxVersion: VersionTLS13,
+				},
+				flags: []string{
+					"-allow-hint-mismatch",
+					"-on-shim-max-version", strconv.Itoa(VersionTLS12),
+					"-on-handshaker-max-version", strconv.Itoa(VersionTLS13),
+				},
+				expectations: connectionExpectations{
+					version: VersionTLS12,
+				},
+			})
+			testCases = append(testCases, testCase{
+				name:               protocol.String() + "-HintMismatch-Version2",
+				testType:           serverTest,
+				protocol:           protocol,
+				skipSplitHandshake: true,
+				config: Config{
+					MinVersion: VersionTLS12,
+					MaxVersion: VersionTLS13,
+				},
+				flags: []string{
+					"-allow-hint-mismatch",
+					"-on-shim-max-version", strconv.Itoa(VersionTLS13),
+					"-on-handshaker-max-version", strconv.Itoa(VersionTLS12),
+				},
+				expectations: connectionExpectations{
+					version: VersionTLS13,
+				},
+			})
+		}
+	}
+}
+
 func worker(statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) {
 	defer wg.Done()
 
@@ -17472,8 +17731,14 @@
 	addJDK11WorkaroundTests()
 	addDelegatedCredentialTests()
 	addEncryptedClientHelloTests()
+	addHintMismatchTests()
 
-	testCases = append(testCases, convertToSplitHandshakeTests(testCases)...)
+	toAppend, err := convertToSplitHandshakeTests(testCases)
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "Error making split handshake tests: %s", err)
+		os.Exit(1)
+	}
+	testCases = append(testCases, toAppend...)
 
 	var wg sync.WaitGroup
 
diff --git a/ssl/test/settings_writer.cc b/ssl/test/settings_writer.cc
index fe8d42e..8605222 100644
--- a/ssl/test/settings_writer.cc
+++ b/ssl/test/settings_writer.cc
@@ -85,29 +85,26 @@
 }
 
 bool SettingsWriter::WriteHandoff(bssl::Span<const uint8_t> handoff) {
-  if (path_.empty()) {
-    return true;
-  }
-
-  CBB child;
-  if (!CBB_add_u16(cbb_.get(), kHandoffTag) ||
-      !CBB_add_u24_length_prefixed(cbb_.get(), &child) ||
-      !CBB_add_bytes(&child, handoff.data(), handoff.size()) ||
-      !CBB_flush(cbb_.get())) {
-    return false;
-  }
-  return true;
+  return WriteData(kHandoffTag, handoff);
 }
 
 bool SettingsWriter::WriteHandback(bssl::Span<const uint8_t> handback) {
+  return WriteData(kHandbackTag, handback);
+}
+
+bool SettingsWriter::WriteHints(bssl::Span<const uint8_t> hints) {
+  return WriteData(kHintsTag, hints);
+}
+
+bool SettingsWriter::WriteData(uint16_t tag, bssl::Span<const uint8_t> data) {
   if (path_.empty()) {
     return true;
   }
 
   CBB child;
-  if (!CBB_add_u16(cbb_.get(), kHandbackTag) ||
+  if (!CBB_add_u16(cbb_.get(), tag) ||
       !CBB_add_u24_length_prefixed(cbb_.get(), &child) ||
-      !CBB_add_bytes(&child, handback.data(), handback.size()) ||
+      !CBB_add_bytes(&child, data.data(), data.size()) ||
       !CBB_flush(cbb_.get())) {
     return false;
   }
diff --git a/ssl/test/settings_writer.h b/ssl/test/settings_writer.h
index 179a5a4..e1ffdc1 100644
--- a/ssl/test/settings_writer.h
+++ b/ssl/test/settings_writer.h
@@ -34,10 +34,12 @@
   bool Commit();
 
   bool WriteHandoff(bssl::Span<const uint8_t> handoff);
-
   bool WriteHandback(bssl::Span<const uint8_t> handback);
+  bool WriteHints(bssl::Span<const uint8_t> hints);
 
  private:
+  bool WriteData(uint16_t tag, bssl::Span<const uint8_t> data);
+
   std::string path_;
   bssl::ScopedCBB cbb_;
 };
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index 7d66d23..fff536f 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -27,6 +27,8 @@
 
 #include "../../crypto/internal.h"
 #include "../internal.h"
+#include "handshake_util.h"
+#include "mock_quic_transport.h"
 #include "test_state.h"
 
 namespace {
@@ -134,6 +136,8 @@
     {"-allow-false-start-without-alpn",
      &TestConfig::allow_false_start_without_alpn},
     {"-handoff", &TestConfig::handoff},
+    {"-handshake-hints", &TestConfig::handshake_hints},
+    {"-allow-hint-mismatch", &TestConfig::allow_hint_mismatch},
     {"-use-ocsp-callback", &TestConfig::use_ocsp_callback},
     {"-set-ocsp-in-callback", &TestConfig::set_ocsp_in_callback},
     {"-decline-ocsp-callback", &TestConfig::decline_ocsp_callback},
@@ -268,7 +272,7 @@
   return true;
 }
 
-bool ParseFlag(char *flag, int argc, char **argv, int *i,
+bool ParseFlag(const char *flag, int argc, char **argv, int *i,
                bool skip, TestConfig *out_config) {
   bool *bool_field = FindField(out_config, kBoolFlags, flag);
   if (bool_field != NULL) {
@@ -398,13 +402,21 @@
   return false;
 }
 
-const char kInit[] = "-on-initial";
-const char kResume[] = "-on-resume";
-const char kRetry[] = "-on-retry";
+// RemovePrefix checks if |*str| begins with |prefix| + "-". If so, it advances
+// |*str| past |prefix| (but not past the "-") and returns true. Otherwise, it
+// returns false and leaves |*str| unmodified.
+bool RemovePrefix(const char **str, const char *prefix) {
+  size_t prefix_len = strlen(prefix);
+  if (strncmp(*str, prefix, strlen(prefix)) == 0 && (*str)[prefix_len] == '-') {
+    *str += strlen(prefix);
+    return true;
+  }
+  return false;
+}
 
 }  // namespace
 
-bool ParseConfig(int argc, char **argv,
+bool ParseConfig(int argc, char **argv, bool is_shim,
                  TestConfig *out_initial,
                  TestConfig *out_resume,
                  TestConfig *out_retry) {
@@ -412,21 +424,36 @@
   out_initial->argv = out_resume->argv = out_retry->argv = argv;
   for (int i = 0; i < argc; i++) {
     bool skip = false;
-    char *flag = argv[i];
-    if (strncmp(flag, kInit, strlen(kInit)) == 0) {
-      if (!ParseFlag(flag + strlen(kInit), argc, argv, &i, skip, out_initial)) {
+    const char *flag = argv[i];
+
+    // -on-shim and -on-handshaker prefixes enable flags only on the shim or
+    // handshaker.
+    if (RemovePrefix(&flag, "-on-shim")) {
+      if (!is_shim) {
+        skip = true;
+      }
+    } else if (RemovePrefix(&flag, "-on-handshaker")) {
+      if (is_shim) {
+        skip = true;
+      }
+    }
+
+    // The following prefixes allow different configurations for each of the
+    // initial, resumption, and 0-RTT retry handshakes.
+    if (RemovePrefix(&flag, "-on-initial")) {
+      if (!ParseFlag(flag, argc, argv, &i, skip, out_initial)) {
         return false;
       }
-    } else if (strncmp(flag, kResume, strlen(kResume)) == 0) {
-      if (!ParseFlag(flag + strlen(kResume), argc, argv, &i, skip,
-                     out_resume)) {
+    } else if (RemovePrefix(&flag, "-on-resume")) {
+      if (!ParseFlag(flag, argc, argv, &i, skip, out_resume)) {
         return false;
       }
-    } else if (strncmp(flag, kRetry, strlen(kRetry)) == 0) {
-      if (!ParseFlag(flag + strlen(kRetry), argc, argv, &i, skip, out_retry)) {
+    } else if (RemovePrefix(&flag, "-on-retry")) {
+      if (!ParseFlag(flag, argc, argv, &i, skip, out_retry)) {
         return false;
       }
     } else {
+      // Unprefixed flags apply to all three.
       int i_init = i;
       int i_resume = i;
       if (!ParseFlag(flag, argc, argv, &i_init, skip, out_initial) ||
@@ -1052,10 +1079,15 @@
   return 1;
 }
 
+static ssl_private_key_result_t AsyncPrivateKeyComplete(SSL *ssl, uint8_t *out,
+                                                        size_t *out_len,
+                                                        size_t max_out);
+
 static ssl_private_key_result_t AsyncPrivateKeySign(
     SSL *ssl, uint8_t *out, size_t *out_len, size_t max_out,
     uint16_t signature_algorithm, const uint8_t *in, size_t in_len) {
   TestState *test_state = GetTestState(ssl);
+  test_state->used_private_key = true;
   if (!test_state->private_key_result.empty()) {
     fprintf(stderr, "AsyncPrivateKeySign called with operation pending.\n");
     abort();
@@ -1096,8 +1128,7 @@
   }
   test_state->private_key_result.resize(len);
 
-  // The signature will be released asynchronously in |AsyncPrivateKeyComplete|.
-  return ssl_private_key_retry;
+  return AsyncPrivateKeyComplete(ssl, out, out_len, max_out);
 }
 
 static ssl_private_key_result_t AsyncPrivateKeyDecrypt(SSL *ssl, uint8_t *out,
@@ -1106,6 +1137,7 @@
                                                        const uint8_t *in,
                                                        size_t in_len) {
   TestState *test_state = GetTestState(ssl);
+  test_state->used_private_key = true;
   if (!test_state->private_key_result.empty()) {
     fprintf(stderr, "AsyncPrivateKeyDecrypt called with operation pending.\n");
     abort();
@@ -1124,8 +1156,7 @@
 
   test_state->private_key_result.resize(*out_len);
 
-  // The decryption will be released asynchronously in |AsyncPrivateComplete|.
-  return ssl_private_key_retry;
+  return AsyncPrivateKeyComplete(ssl, out, out_len, max_out);
 }
 
 static ssl_private_key_result_t AsyncPrivateKeyComplete(SSL *ssl, uint8_t *out,
@@ -1138,9 +1169,9 @@
     abort();
   }
 
-  if (test_state->private_key_retries < 2) {
+  if (GetTestConfig(ssl)->async && test_state->private_key_retries < 2) {
     // Only return the decryption on the second attempt, to test both incomplete
-    // |decrypt| and |decrypt_complete|.
+    // |sign|/|decrypt| and |complete|.
     return ssl_private_key_retry;
   }
 
@@ -1174,7 +1205,10 @@
   if (pkey) {
     TestState *test_state = GetTestState(ssl);
     const TestConfig *config = GetTestConfig(ssl);
-    if (config->async) {
+    if (config->async || config->handshake_hints) {
+      // Install a custom private key if testing asynchronous callbacks, or if
+      // testing handshake hints. In the handshake hints case, we wish to check
+      // that hints only mismatch when allowed.
       test_state->private_key = std::move(pkey);
       SSL_set_private_key_method(ssl, &g_async_private_key_method);
     } else if (!SSL_use_PrivateKey(ssl, pkey.get())) {
@@ -1195,12 +1229,14 @@
 
 static enum ssl_select_cert_result_t SelectCertificateCallback(
     const SSL_CLIENT_HELLO *client_hello) {
-  const TestConfig *config = GetTestConfig(client_hello->ssl);
-  GetTestState(client_hello->ssl)->early_callback_called = true;
+  SSL *ssl = client_hello->ssl;
+  const TestConfig *config = GetTestConfig(ssl);
+  TestState *test_state = GetTestState(ssl);
+  test_state->early_callback_called = true;
 
   if (!config->expect_server_name.empty()) {
     const char *server_name =
-        SSL_get_servername(client_hello->ssl, TLSEXT_NAMETYPE_host_name);
+        SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
     if (server_name == nullptr ||
         std::string(server_name) != config->expect_server_name) {
       fprintf(stderr,
@@ -1214,48 +1250,73 @@
     return ssl_select_cert_error;
   }
 
-  // Install the certificate in the early callback.
-  if (config->use_early_callback) {
-    bool early_callback_ready =
-        GetTestState(client_hello->ssl)->early_callback_ready;
-    if (config->async && !early_callback_ready) {
-      // Install the certificate asynchronously.
-      return ssl_select_cert_retry;
-    }
-    if (!InstallCertificate(client_hello->ssl)) {
-      return ssl_select_cert_error;
-    }
+  // Simulate some asynchronous work in the early callback.
+  if ((config->use_early_callback || test_state->get_handshake_hints_cb) &&
+      config->async && !test_state->early_callback_ready) {
+    return ssl_select_cert_retry;
   }
+
+  if (test_state->get_handshake_hints_cb &&
+      !test_state->get_handshake_hints_cb(client_hello)) {
+    return ssl_select_cert_error;
+  }
+
+  if (config->use_early_callback && !InstallCertificate(ssl)) {
+    return ssl_select_cert_error;
+  }
+
   return ssl_select_cert_success;
 }
 
 static int SetQuicReadSecret(SSL *ssl, enum ssl_encryption_level_t level,
                              const SSL_CIPHER *cipher, const uint8_t *secret,
                              size_t secret_len) {
-  return GetTestState(ssl)->quic_transport->SetReadSecret(level, cipher, secret,
-                                                          secret_len);
+  MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get();
+  if (quic_transport == nullptr) {
+    fprintf(stderr, "No QUIC transport.\n");
+    return 0;
+  }
+  return quic_transport->SetReadSecret(level, cipher, secret, secret_len);
 }
 
 static int SetQuicWriteSecret(SSL *ssl, enum ssl_encryption_level_t level,
                               const SSL_CIPHER *cipher, const uint8_t *secret,
                               size_t secret_len) {
-  return GetTestState(ssl)->quic_transport->SetWriteSecret(level, cipher,
-                                                           secret, secret_len);
+  MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get();
+  if (quic_transport == nullptr) {
+    fprintf(stderr, "No QUIC transport.\n");
+    return 0;
+  }
+  return quic_transport->SetWriteSecret(level, cipher, 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);
+  MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get();
+  if (quic_transport == nullptr) {
+    fprintf(stderr, "No QUIC transport.\n");
+    return 0;
+  }
+  return quic_transport->WriteHandshakeData(level, data, len);
 }
 
 static int FlushQuicFlight(SSL *ssl) {
-  return GetTestState(ssl)->quic_transport->Flush();
+  MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get();
+  if (quic_transport == nullptr) {
+    fprintf(stderr, "No QUIC transport.\n");
+    return 0;
+  }
+  return quic_transport->Flush();
 }
 
 static int SendQuicAlert(SSL *ssl, enum ssl_encryption_level_t level,
                          uint8_t alert) {
-  return GetTestState(ssl)->quic_transport->SendAlert(level, alert);
+  MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get();
+  if (quic_transport == nullptr) {
+    fprintf(stderr, "No QUIC transport.\n");
+    return 0;
+  }
+  return quic_transport->SendAlert(level, alert);
 }
 
 static const SSL_QUIC_METHOD g_quic_method = {
@@ -1320,7 +1381,10 @@
   SSL_CTX_set_info_callback(ssl_ctx.get(), InfoCallback);
   SSL_CTX_sess_set_new_cb(ssl_ctx.get(), NewSessionCallback);
 
-  if (use_ticket_callback) {
+  if (use_ticket_callback || handshake_hints) {
+    // If using handshake hints, always enable the ticket callback, so we can
+    // check that hints only mismatch when allowed. The ticket callback also
+    // uses a constant key, which simplifies the test.
     SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx.get(), TicketKeyCallback);
   }
 
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index 4946bc7..f4e3f61 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -167,6 +167,8 @@
   std::string expect_msg_callback;
   bool allow_false_start_without_alpn = false;
   bool handoff = false;
+  bool handshake_hints = false;
+  bool allow_hint_mismatch = false;
   bool use_ocsp_callback = false;
   bool set_ocsp_in_callback = false;
   bool decline_ocsp_callback = false;
@@ -198,7 +200,7 @@
                               std::unique_ptr<TestState> test_state) const;
 };
 
-bool ParseConfig(int argc, char **argv, TestConfig *out_initial,
+bool ParseConfig(int argc, char **argv, bool is_shim, TestConfig *out_initial,
                  TestConfig *out_resume, TestConfig *out_retry);
 
 bool SetTestConfig(SSL *ssl, const TestConfig *config);
diff --git a/ssl/test/test_state.h b/ssl/test/test_state.h
index e5c96e5..2c558a4 100644
--- a/ssl/test/test_state.h
+++ b/ssl/test/test_state.h
@@ -17,6 +17,7 @@
 
 #include <openssl/base.h>
 
+#include <functional>
 #include <memory>
 #include <string>
 #include <vector>
@@ -48,6 +49,8 @@
   bool handshake_done = false;
   // private_key is the underlying private key used when testing custom keys.
   bssl::UniquePtr<EVP_PKEY> private_key;
+  // When private key methods are used, whether the private key was used.
+  bool used_private_key = false;
   std::vector<uint8_t> private_key_result;
   // private_key_retries is the number of times an asynchronous private key
   // operation has been retried.
@@ -64,6 +67,7 @@
   // completion. This tests that the callback is not called again after this.
   bool cert_verified = false;
   int explicit_renegotiates = 0;
+  std::function<bool(const SSL_CLIENT_HELLO*)> get_handshake_hints_cb;
 };
 
 bool SetTestState(SSL *ssl, std::unique_ptr<TestState> state);
diff --git a/ssl/tls13_both.cc b/ssl/tls13_both.cc
index c6bc2b1..5837187 100644
--- a/ssl/tls13_both.cc
+++ b/ssl/tls13_both.cc
@@ -580,10 +580,40 @@
     return ssl_private_key_failure;
   }
 
-  enum ssl_private_key_result_t sign_result = ssl_private_key_sign(
-      hs, sig, &sig_len, max_sig_len, signature_algorithm, msg);
-  if (sign_result != ssl_private_key_success) {
-    return sign_result;
+  SSL_HANDSHAKE_HINTS *const hints = hs->hints.get();
+  Array<uint8_t> spki;
+  if (hints) {
+    ScopedCBB spki_cbb;
+    if (!CBB_init(spki_cbb.get(), 64) ||
+        !EVP_marshal_public_key(spki_cbb.get(), hs->local_pubkey.get()) ||
+        !CBBFinishArray(spki_cbb.get(), &spki)) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+      return ssl_private_key_failure;
+    }
+  }
+
+  if (hints && !hs->hints_requested &&
+      signature_algorithm == hints->signature_algorithm &&
+      MakeConstSpan(msg) == hints->signature_input &&
+      MakeConstSpan(spki) == hints->signature_spki &&
+      !hints->signature.empty() && hints->signature.size() <= max_sig_len) {
+    // Signature algorithm and input both match. Reuse the signature from hints.
+    sig_len = hints->signature.size();
+    OPENSSL_memcpy(sig, hints->signature.data(), sig_len);
+  } else {
+    enum ssl_private_key_result_t sign_result = ssl_private_key_sign(
+        hs, sig, &sig_len, max_sig_len, signature_algorithm, msg);
+    if (sign_result != ssl_private_key_success) {
+      return sign_result;
+    }
+    if (hints && hs->hints_requested) {
+      hints->signature_algorithm = signature_algorithm;
+      hints->signature_input = std::move(msg);
+      hints->signature_spki = std::move(spki);
+      if (!hints->signature.CopyFrom(MakeSpan(sig, sig_len))) {
+        return ssl_private_key_failure;
+      }
+    }
   }
 
   if (!CBB_did_write(&child, sig_len) ||
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 21f3661..c9624c8 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -63,14 +63,33 @@
   }
 
   Array<uint8_t> secret;
-  ScopedCBB public_key;
-  UniquePtr<SSLKeyShare> key_share = SSLKeyShare::Create(group_id);
-  if (!key_share ||
-      !CBB_init(public_key.get(), 32) ||
-      !key_share->Accept(public_key.get(), &secret, &alert, peer_key) ||
-      !CBBFinishArray(public_key.get(), &hs->ecdh_public_key)) {
-    ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
-    return false;
+  SSL_HANDSHAKE_HINTS *const hints = hs->hints.get();
+  if (hints && !hs->hints_requested && hints->key_share_group_id == group_id &&
+      !hints->key_share_secret.empty()) {
+    // Copy DH secret from hints.
+    if (!hs->ecdh_public_key.CopyFrom(hints->key_share_public_key) ||
+        !secret.CopyFrom(hints->key_share_secret)) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+      return false;
+    }
+  } else {
+    ScopedCBB public_key;
+    UniquePtr<SSLKeyShare> key_share = SSLKeyShare::Create(group_id);
+    if (!key_share ||  //
+        !CBB_init(public_key.get(), 32) ||
+        !key_share->Accept(public_key.get(), &secret, &alert, peer_key) ||
+        !CBBFinishArray(public_key.get(), &hs->ecdh_public_key)) {
+      ssl_send_alert(ssl, SSL3_AL_FATAL, alert);
+      return false;
+    }
+    if (hints && hs->hints_requested) {
+      hints->key_share_group_id = group_id;
+      if (!hints->key_share_public_key.CopyFrom(hs->ecdh_public_key) ||
+          !hints->key_share_secret.CopyFrom(secret)) {
+        ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR);
+        return false;
+      }
+    }
   }
 
   return tls13_advance_key_schedule(hs, secret);
@@ -538,6 +557,9 @@
 
 static enum ssl_hs_wait_t do_send_hello_retry_request(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
+  if (hs->hints_requested) {
+    return ssl_hs_hints_ready;
+  }
 
   ScopedCBB cbb;
   CBB body, session_id, extensions;
@@ -734,7 +756,18 @@
   SSL *const ssl = hs->ssl;
 
   Span<uint8_t> random(ssl->s3->server_random);
-  RAND_bytes(random.data(), random.size());
+
+  SSL_HANDSHAKE_HINTS *const hints = hs->hints.get();
+  if (hints && !hs->hints_requested &&
+      hints->server_random.size() == random.size()) {
+    OPENSSL_memcpy(random.data(), hints->server_random.data(), random.size());
+  } else {
+    RAND_bytes(random.data(), random.size());
+    if (hints && hs->hints_requested &&
+        !hints->server_random.CopyFrom(random)) {
+      return ssl_hs_error;
+    }
+  }
 
   assert(!hs->ech_accept || hs->ech_is_inner_present);
 
@@ -864,6 +897,10 @@
 
 static enum ssl_hs_wait_t do_send_server_finished(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
+  if (hs->hints_requested) {
+    return ssl_hs_hints_ready;
+  }
+
   if (!tls13_add_finished(hs) ||
       // Update the secret to the master secret and derive traffic keys.
       !tls13_advance_key_schedule(
diff --git a/ssl/tls_method.cc b/ssl/tls_method.cc
index 8165d1c..326cbe7 100644
--- a/ssl/tls_method.cc
+++ b/ssl/tls_method.cc
@@ -93,7 +93,8 @@
   }
 
   if (ssl->quic_method != nullptr) {
-    if (!ssl->quic_method->set_read_secret(ssl, level, aead_ctx->cipher(),
+    if ((ssl->s3->hs == nullptr || !ssl->s3->hs->hints_requested) &&
+        !ssl->quic_method->set_read_secret(ssl, level, aead_ctx->cipher(),
                                            secret_for_quic.data(),
                                            secret_for_quic.size())) {
       return false;
@@ -121,7 +122,8 @@
   }
 
   if (ssl->quic_method != nullptr) {
-    if (!ssl->quic_method->set_write_secret(ssl, level, aead_ctx->cipher(),
+    if ((ssl->s3->hs == nullptr || !ssl->s3->hs->hints_requested) &&
+        !ssl->quic_method->set_write_secret(ssl, level, aead_ctx->cipher(),
                                             secret_for_quic.data(),
                                             secret_for_quic.size())) {
       return false;