Add async certificate verification callback.

This also serves as a certificate verification callback for
CRYPTO_BUFFER-based consumers. Remove the silly
SSL_CTX_i_promise_to_verify_certs_after_the_handshake placeholder.

Bug: 54, chromium:347402
Change-Id: I4c6b445cb9cd7204218acb2e5d1625e6f37aff6f
Reviewed-on: https://boringssl-review.googlesource.com/17964
Reviewed-by: David Benjamin <davidben@google.com>
diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h
index 16aeaff..328ec1c 100644
--- a/include/openssl/ssl.h
+++ b/include/openssl/ssl.h
@@ -517,6 +517,13 @@
  * used to reuse the underlying connection for the retry. */
 #define SSL_ERROR_EARLY_DATA_REJECTED 15
 
+/* SSL_ERROR_WANT_CERTIFICATE_VERIFY indicates the operation failed because
+ * certificate verification was incomplete. The caller may retry the operation
+ * when certificate verification is complete.
+ *
+ * See also |SSL_CTX_set_custom_verify|. */
+#define SSL_ERROR_WANT_CERTIFICATE_VERIFY 16
+
 /* SSL_set_mtu sets the |ssl|'s MTU in DTLS to |mtu|. It returns one on success
  * and zero on failure. */
 OPENSSL_EXPORT int SSL_set_mtu(SSL *ssl, unsigned mtu);
@@ -2201,6 +2208,39 @@
                                    int (*callback)(int ok,
                                                    X509_STORE_CTX *store_ctx));
 
+enum ssl_verify_result_t {
+  ssl_verify_ok,
+  ssl_verify_invalid,
+  ssl_verify_retry,
+};
+
+/* SSL_CTX_set_custom_verify configures certificate verification. |mode| is one
+ * of the |SSL_VERIFY_*| values defined above. |callback| performs the
+ * certificate verification.
+ *
+ * The callback may call |SSL_get0_peer_certificates| for the certificate chain
+ * to validate. The callback should return |ssl_verify_ok| if the certificate is
+ * valid. If the certificate is invalid, the callback should return
+ * |ssl_verify_invalid| and optionally set |*out_alert| to an alert to send to
+ * the peer. Some useful alerts include |SSL_AD_CERTIFICATE_EXPIRED|,
+ * |SSL_AD_CERTIFICATE_REVOKED|, |SSL_AD_UNKNOWN_CA|, |SSL_AD_BAD_CERTIFICATE|,
+ * |SSL_AD_CERTIFICATE_UNKNOWN|, and |SSL_AD_INTERNAL_ERROR|. See RFC 5246
+ * section 7.2.2 for their precise meanings. If unspecified,
+ * |SSL_AD_CERTIFICATE_UNKNOWN| will be sent by default.
+ *
+ * To verify a certificate asynchronously, the callback may return
+ * |ssl_verify_retry|. The handshake will then pause with |SSL_get_error|
+ * returning |SSL_ERROR_WANT_CERTIFICATE_VERIFY|. */
+OPENSSL_EXPORT void SSL_CTX_set_custom_verify(
+    SSL_CTX *ctx, int mode,
+    enum ssl_verify_result_t (*callback)(SSL *ssl, uint8_t *out_alert));
+
+/* SSL_set_custom_verify behaves like |SSL_CTX_set_custom_verify| but configures
+ * an individual |SSL|. */
+OPENSSL_EXPORT void SSL_set_custom_verify(
+    SSL *ssl, int mode,
+    enum ssl_verify_result_t (*callback)(SSL *ssl, uint8_t *out_alert));
+
 /* SSL_CTX_get_verify_mode returns |ctx|'s verify mode, set by
  * |SSL_CTX_set_verify|. */
 OPENSSL_EXPORT int SSL_CTX_get_verify_mode(const SSL_CTX *ctx);
@@ -2320,13 +2360,6 @@
     SSL_CTX *ctx, int (*callback)(X509_STORE_CTX *store_ctx, void *arg),
     void *arg);
 
-/* SSL_CTX_i_promise_to_verify_certs_after_the_handshake indicates that the
- * caller understands that the |CRYPTO_BUFFER|-based methods currently require
- * post-handshake verification of certificates and thus it's ok to accept any
- * certificates during the handshake. */
-OPENSSL_EXPORT void SSL_CTX_i_promise_to_verify_certs_after_the_handshake(
-    SSL_CTX *ctx);
-
 /* SSL_enable_signed_cert_timestamps causes |ssl| (which must be the client end
  * of a connection) to request SCTs from the server. See
  * https://tools.ietf.org/html/rfc6962.
@@ -3748,6 +3781,7 @@
 #define SSL_PRIVATE_KEY_OPERATION 9
 #define SSL_PENDING_TICKET 10
 #define SSL_EARLY_DATA_REJECTED 11
+#define SSL_CERTIFICATE_VERIFY 12
 
 /* SSL_want returns one of the above values to determine what the most recent
  * operation on |ssl| was blocked on. Use |SSL_get_error| instead. */
@@ -4194,6 +4228,9 @@
   int (*app_verify_callback)(X509_STORE_CTX *store_ctx, void *arg);
   void *app_verify_arg;
 
+  enum ssl_verify_result_t (*custom_verify_callback)(SSL *ssl,
+                                                     uint8_t *out_alert);
+
   /* Default password callback. */
   pem_password_cb *default_passwd_callback;
 
@@ -4374,12 +4411,6 @@
    * otherwise. */
   unsigned grease_enabled:1;
 
-  /* i_promise_to_verify_certs_after_the_handshake indicates that the
-   * application is using the |CRYPTO_BUFFER|-based methods and understands
-   * that this currently requires post-handshake verification of
-   * certificates. */
-  unsigned i_promise_to_verify_certs_after_the_handshake:1;
-
   /* allow_unknown_alpn_protos is one if the client allows unsolicited ALPN
    * protocols from the peer. */
   unsigned allow_unknown_alpn_protos:1;
diff --git a/include/openssl/ssl3.h b/include/openssl/ssl3.h
index 2b241ba..39cd07b 100644
--- a/include/openssl/ssl3.h
+++ b/include/openssl/ssl3.h
@@ -334,6 +334,7 @@
 /* server */
 /* extra state */
 #define SSL3_ST_SW_FLUSH (0x100 | SSL_ST_ACCEPT)
+#define SSL3_ST_VERIFY_CLIENT_CERT (0x101 | SSL_ST_ACCEPT)
 /* read from client */
 #define SSL3_ST_SR_CLNT_HELLO_A (0x110 | SSL_ST_ACCEPT)
 #define SSL3_ST_SR_CLNT_HELLO_B (0x111 | SSL_ST_ACCEPT)
diff --git a/ssl/handshake_client.cc b/ssl/handshake_client.cc
index 9efbf0a..10c10a2 100644
--- a/ssl/handshake_client.cc
+++ b/ssl/handshake_client.cc
@@ -173,7 +173,6 @@
 static int ssl3_get_server_hello(SSL_HANDSHAKE *hs);
 static int ssl3_get_server_certificate(SSL_HANDSHAKE *hs);
 static int ssl3_get_cert_status(SSL_HANDSHAKE *hs);
-static int ssl3_verify_server_cert(SSL_HANDSHAKE *hs);
 static int ssl3_get_server_key_exchange(SSL_HANDSHAKE *hs);
 static int ssl3_get_certificate_request(SSL_HANDSHAKE *hs);
 static int ssl3_get_server_hello_done(SSL_HANDSHAKE *hs);
@@ -292,9 +291,16 @@
 
       case SSL3_ST_VERIFY_SERVER_CERT:
         if (ssl_cipher_uses_certificate_auth(hs->new_cipher)) {
-          ret = ssl3_verify_server_cert(hs);
-          if (ret <= 0) {
-            goto end;
+          switch (ssl_verify_peer_cert(hs)) {
+            case ssl_verify_ok:
+              break;
+            case ssl_verify_invalid:
+              ret = -1;
+              goto end;
+            case ssl_verify_retry:
+              ssl->rwstate = SSL_CERTIFICATE_VERIFY;
+              ret = -1;
+              goto end;
           }
         }
         hs->state = SSL3_ST_CR_KEY_EXCH_A;
@@ -1185,15 +1191,6 @@
   return 1;
 }
 
-static int ssl3_verify_server_cert(SSL_HANDSHAKE *hs) {
-  SSL *const ssl = hs->ssl;
-  if (!ssl->ctx->x509_method->session_verify_cert_chain(hs->new_session, ssl)) {
-    return -1;
-  }
-
-  return 1;
-}
-
 static int ssl3_get_server_key_exchange(SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
   EC_KEY *ecdh = NULL;
diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc
index ee5358c..00ac549 100644
--- a/ssl/handshake_server.cc
+++ b/ssl/handshake_server.cc
@@ -282,6 +282,23 @@
             goto end;
           }
         }
+        hs->state = SSL3_ST_VERIFY_CLIENT_CERT;
+        break;
+
+      case SSL3_ST_VERIFY_CLIENT_CERT:
+        if (sk_CRYPTO_BUFFER_num(hs->new_session->certs) > 0) {
+          switch (ssl_verify_peer_cert(hs)) {
+            case ssl_verify_ok:
+              break;
+            case ssl_verify_invalid:
+              ret = -1;
+              goto end;
+            case ssl_verify_retry:
+              ssl->rwstate = SSL_CERTIFICATE_VERIFY;
+              ret = -1;
+              goto end;
+          }
+        }
         hs->state = SSL3_ST_SR_KEY_EXCH_A;
         break;
 
@@ -1264,10 +1281,6 @@
     hs->new_session->peer_sha256_valid = 1;
   }
 
-  if (!ssl->ctx->x509_method->session_verify_cert_chain(hs->new_session, ssl)) {
-    return -1;
-  }
-
   return 1;
 }
 
diff --git a/ssl/internal.h b/ssl/internal.h
index ba98783..e2a3af4 100644
--- a/ssl/internal.h
+++ b/ssl/internal.h
@@ -1007,6 +1007,7 @@
   ssl_hs_early_data_rejected,
   ssl_hs_read_end_of_early_data,
   ssl_hs_read_change_cipher_spec,
+  ssl_hs_certificate_verify,
 };
 
 struct ssl_handshake_st {
@@ -1341,6 +1342,9 @@
                          const SSL_EXTENSION_TYPE *ext_types,
                          size_t num_ext_types, int ignore_unknown);
 
+/* ssl_verify_peer_cert verifies the peer certificate for |hs|. */
+enum ssl_verify_result_t ssl_verify_peer_cert(SSL_HANDSHAKE *hs);
+
 
 /* SSLKEYLOGFILE functions. */
 
@@ -1597,7 +1601,8 @@
   /* session_verify_cert_chain verifies the certificate chain in |session|,
    * sets |session->verify_result| and returns one on success or zero on
    * error. */
-  int (*session_verify_cert_chain)(SSL_SESSION *session, SSL *ssl);
+  int (*session_verify_cert_chain)(SSL_SESSION *session, SSL *ssl,
+                                   uint8_t *out_alert);
 
   /* hs_flush_cached_ca_names drops any cached |X509_NAME|s from |hs|. */
   void (*hs_flush_cached_ca_names)(SSL_HANDSHAKE *hs);
@@ -1990,6 +1995,9 @@
   int (*verify_callback)(int ok,
                          X509_STORE_CTX *ctx); /* fail if callback returns 0 */
 
+  enum ssl_verify_result_t (*custom_verify_callback)(SSL *ssl,
+                                                     uint8_t *out_alert);
+
   void (*info_callback)(const SSL *ssl, int type, int value);
 
   /* Server-only: psk_identity_hint is the identity hint to send in
diff --git a/ssl/s3_both.cc b/ssl/s3_both.cc
index 79f71fa..85de99c 100644
--- a/ssl/s3_both.cc
+++ b/ssl/s3_both.cc
@@ -830,3 +830,34 @@
 
   return 1;
 }
+
+enum ssl_verify_result_t ssl_verify_peer_cert(SSL_HANDSHAKE *hs) {
+  SSL *const ssl = hs->ssl;
+  uint8_t alert = SSL_AD_CERTIFICATE_UNKNOWN;
+  enum ssl_verify_result_t ret;
+  if (ssl->custom_verify_callback != nullptr) {
+    ret = ssl->custom_verify_callback(ssl, &alert);
+    switch (ret) {
+      case ssl_verify_ok:
+        hs->new_session->verify_result = X509_V_OK;
+        break;
+      case ssl_verify_invalid:
+        hs->new_session->verify_result = X509_V_ERR_APPLICATION_VERIFICATION;
+        break;
+      case ssl_verify_retry:
+        break;
+    }
+  } else {
+    ret = ssl->ctx->x509_method->session_verify_cert_chain(hs->new_session, ssl,
+                                                           &alert)
+              ? ssl_verify_ok
+              : ssl_verify_invalid;
+  }
+
+  if (ret == ssl_verify_invalid) {
+    OPENSSL_PUT_ERROR(SSL, SSL_R_CERTIFICATE_VERIFY_FAILED);
+    ssl3_send_alert(ssl, SSL3_AL_FATAL, alert);
+  }
+
+  return ret;
+}
diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc
index 7441925..b2d5f02 100644
--- a/ssl/ssl_lib.cc
+++ b/ssl/ssl_lib.cc
@@ -392,6 +392,7 @@
   ssl->msg_callback_arg = ctx->msg_callback_arg;
   ssl->verify_mode = ctx->verify_mode;
   ssl->verify_callback = ctx->default_verify_callback;
+  ssl->custom_verify_callback = ctx->custom_verify_callback;
   ssl->retain_only_sha256_of_client_certs =
       ctx->retain_only_sha256_of_client_certs;
 
@@ -984,6 +985,9 @@
 
     case SSL_EARLY_DATA_REJECTED:
       return SSL_ERROR_EARLY_DATA_REJECTED;
+
+    case SSL_CERTIFICATE_VERIFY:
+      return SSL_ERROR_WANT_CERTIFICATE_VERIFY;
   }
 
   return SSL_ERROR_SYSCALL;
@@ -1554,12 +1558,22 @@
   return TLSEXT_NAMETYPE_host_name;
 }
 
-void SSL_CTX_enable_signed_cert_timestamps(SSL_CTX *ctx) {
-  ctx->signed_cert_timestamps_enabled = 1;
+void SSL_CTX_set_custom_verify(
+    SSL_CTX *ctx, int mode,
+    enum ssl_verify_result_t (*callback)(SSL *ssl, uint8_t *out_alert)) {
+  ctx->verify_mode = mode;
+  ctx->custom_verify_callback = callback;
 }
 
-void SSL_CTX_i_promise_to_verify_certs_after_the_handshake(SSL_CTX *ctx) {
-  ctx->i_promise_to_verify_certs_after_the_handshake = 1;
+void SSL_set_custom_verify(
+    SSL *ssl, int mode,
+    enum ssl_verify_result_t (*callback)(SSL *ssl, uint8_t *out_alert)) {
+  ssl->verify_mode = mode;
+  ssl->custom_verify_callback = callback;
+}
+
+void SSL_CTX_enable_signed_cert_timestamps(SSL_CTX *ctx) {
+  ctx->signed_cert_timestamps_enabled = 1;
 }
 
 void SSL_enable_signed_cert_timestamps(SSL *ssl) {
diff --git a/ssl/ssl_test.cc b/ssl/ssl_test.cc
index 2c648ac..640718a 100644
--- a/ssl/ssl_test.cc
+++ b/ssl/ssl_test.cc
@@ -3276,7 +3276,11 @@
   ASSERT_TRUE(SSL_CTX_set_chain_and_key(server_ctx.get(), &chain[0],
                                         chain.size(), key.get(), nullptr));
 
-  SSL_CTX_i_promise_to_verify_certs_after_the_handshake(client_ctx.get());
+  SSL_CTX_set_custom_verify(
+      client_ctx.get(), SSL_VERIFY_PEER,
+      [](SSL *ssl, uint8_t *out_alert) -> ssl_verify_result_t {
+        return ssl_verify_ok;
+      });
 
   bssl::UniquePtr<SSL> client, server;
   ASSERT_TRUE(ConnectClientAndServer(&client, &server, client_ctx.get(),
diff --git a/ssl/ssl_x509.cc b/ssl/ssl_x509.cc
index 77fc0e2..125e105 100644
--- a/ssl/ssl_x509.cc
+++ b/ssl/ssl_x509.cc
@@ -620,7 +620,9 @@
 }
 
 static int ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,
-                                                     SSL *ssl) {
+                                                     SSL *ssl,
+                                                     uint8_t *out_alert) {
+  *out_alert = SSL_AD_INTERNAL_ERROR;
   STACK_OF(X509) *const cert_chain = session->x509_chain;
   if (cert_chain == NULL || sk_X509_num(cert_chain) == 0) {
     return 0;
@@ -666,8 +668,7 @@
 
   /* If |SSL_VERIFY_NONE|, the error is non-fatal, but we keep the result. */
   if (verify_ret <= 0 && ssl->verify_mode != SSL_VERIFY_NONE) {
-    ssl3_send_alert(ssl, SSL3_AL_FATAL, ssl_verify_alarm_type(ctx.error));
-    OPENSSL_PUT_ERROR(SSL, SSL_R_CERTIFICATE_VERIFY_FAILED);
+    *out_alert = ssl_verify_alarm_type(ctx.error);
     goto err;
   }
 
diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc
index cd846be..2e98780 100644
--- a/ssl/test/bssl_shim.cc
+++ b/ssl/test/bssl_shim.cc
@@ -112,6 +112,7 @@
   bool alpn_select_done = false;
   bool is_resume = false;
   bool early_callback_ready = false;
+  bool custom_verify_ready = false;
 };
 
 static void TestStateExFree(void *parent, void *ptr, CRYPTO_EX_DATA *ad,
@@ -684,27 +685,52 @@
   return 1;
 }
 
-static int VerifySucceed(X509_STORE_CTX *store_ctx, void *arg) {
-  SSL* ssl = (SSL*)X509_STORE_CTX_get_ex_data(store_ctx,
-      SSL_get_ex_data_X509_STORE_CTX_idx());
+static bool CheckVerifyCallback(SSL *ssl) {
   const TestConfig *config = GetTestConfig(ssl);
-
   if (!config->expected_ocsp_response.empty()) {
     const uint8_t *data;
     size_t len;
     SSL_get0_ocsp_response(ssl, &data, &len);
     if (len == 0) {
       fprintf(stderr, "OCSP response not available in verify callback\n");
-      return 0;
+      return false;
     }
   }
 
+  return true;
+}
+
+static int CertVerifyCallback(X509_STORE_CTX *store_ctx, void *arg) {
+  SSL* ssl = (SSL*)X509_STORE_CTX_get_ex_data(store_ctx,
+      SSL_get_ex_data_X509_STORE_CTX_idx());
+  const TestConfig *config = GetTestConfig(ssl);
+  if (!CheckVerifyCallback(ssl)) {
+    return 0;
+  }
+
+  if (config->verify_fail) {
+    store_ctx->error = X509_V_ERR_APPLICATION_VERIFICATION;
+    return 0;
+  }
+
   return 1;
 }
 
-static int VerifyFail(X509_STORE_CTX *store_ctx, void *arg) {
-  store_ctx->error = X509_V_ERR_APPLICATION_VERIFICATION;
-  return 0;
+static ssl_verify_result_t CustomVerifyCallback(SSL *ssl, uint8_t *out_alert) {
+  const TestConfig *config = GetTestConfig(ssl);
+  if (!CheckVerifyCallback(ssl)) {
+    return ssl_verify_invalid;
+  }
+
+  if (config->async && !GetTestState(ssl)->custom_verify_ready) {
+    return ssl_verify_retry;
+  }
+
+  if (config->verify_fail) {
+    return ssl_verify_invalid;
+  }
+
+  return ssl_verify_ok;
 }
 
 static int NextProtosAdvertisedCallback(SSL *ssl, const uint8_t **out,
@@ -1139,10 +1165,8 @@
     return nullptr;
   }
 
-  if (config->verify_fail) {
-    SSL_CTX_set_cert_verify_callback(ssl_ctx.get(), VerifyFail, NULL);
-  } else {
-    SSL_CTX_set_cert_verify_callback(ssl_ctx.get(), VerifySucceed, NULL);
+  if (!config->use_custom_verify_callback) {
+    SSL_CTX_set_cert_verify_callback(ssl_ctx.get(), CertVerifyCallback, NULL);
   }
 
   if (!config->signed_cert_timestamps.empty() &&
@@ -1270,6 +1294,9 @@
     case SSL_ERROR_WANT_PRIVATE_KEY_OPERATION:
       test_state->private_key_retries++;
       return true;
+    case SSL_ERROR_WANT_CERTIFICATE_VERIFY:
+      test_state->custom_verify_ready = true;
+      return true;
     default:
       return false;
   }
@@ -1763,20 +1790,23 @@
   if (!config->use_old_client_cert_callback) {
     SSL_set_cert_cb(ssl.get(), CertCallback, nullptr);
   }
+  int mode = SSL_VERIFY_NONE;
   if (config->require_any_client_certificate) {
-    SSL_set_verify(ssl.get(), SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
-                   NULL);
+    mode = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
   }
   if (config->verify_peer) {
-    SSL_set_verify(ssl.get(), SSL_VERIFY_PEER, NULL);
+    mode = SSL_VERIFY_PEER;
   }
   if (config->verify_peer_if_no_obc) {
     // Set SSL_VERIFY_FAIL_IF_NO_PEER_CERT so testing whether client
     // certificates were requested is easy.
-    SSL_set_verify(ssl.get(),
-                   SSL_VERIFY_PEER | SSL_VERIFY_PEER_IF_NO_OBC |
-                       SSL_VERIFY_FAIL_IF_NO_PEER_CERT,
-                   NULL);
+    mode = SSL_VERIFY_PEER | SSL_VERIFY_PEER_IF_NO_OBC |
+           SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
+  }
+  if (config->use_custom_verify_callback) {
+    SSL_set_custom_verify(ssl.get(), mode, CustomVerifyCallback);
+  } else if (mode != SSL_VERIFY_NONE) {
+    SSL_set_verify(ssl.get(), mode, NULL);
   }
   if (config->false_start) {
     SSL_set_mode(ssl.get(), SSL_MODE_ENABLE_FALSE_START);
diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go
index e526576..8ced44d 100644
--- a/ssl/test/runner/runner.go
+++ b/ssl/test/runner/runner.go
@@ -4553,47 +4553,49 @@
 		if config.protocol == dtls && !vers.hasDTLS {
 			continue
 		}
-		for _, testType := range []testType{clientTest, serverTest} {
-			suffix := "-Client"
-			if testType == serverTest {
-				suffix = "-Server"
-			}
-			suffix += "-" + vers.name
+		for _, useCustomCallback := range []bool{false, true} {
+			for _, testType := range []testType{clientTest, serverTest} {
+				suffix := "-Client"
+				if testType == serverTest {
+					suffix = "-Server"
+				}
+				suffix += "-" + vers.name
+				if useCustomCallback {
+					suffix += "-CustomCallback"
+				}
 
-			flag := "-verify-peer"
-			if testType == serverTest {
-				flag = "-require-any-client-certificate"
-			}
+				flags := []string{"-verify-peer"}
+				if testType == serverTest {
+					flags = append(flags, "-require-any-client-certificate")
+				}
+				if useCustomCallback {
+					flags = append(flags, "-use-custom-verify-callback")
+				}
 
-			tests = append(tests, testCase{
-				testType: testType,
-				name:     "CertificateVerificationSucceed" + suffix,
-				config: Config{
-					MaxVersion:   vers.version,
-					Certificates: []Certificate{rsaCertificate},
-				},
-				tls13Variant: vers.tls13Variant,
-				flags: []string{
-					flag,
-					"-expect-verify-result",
-				},
-				resumeSession: true,
-			})
-			tests = append(tests, testCase{
-				testType: testType,
-				name:     "CertificateVerificationFail" + suffix,
-				config: Config{
-					MaxVersion:   vers.version,
-					Certificates: []Certificate{rsaCertificate},
-				},
-				tls13Variant: vers.tls13Variant,
-				flags: []string{
-					flag,
-					"-verify-fail",
-				},
-				shouldFail:    true,
-				expectedError: ":CERTIFICATE_VERIFY_FAILED:",
-			})
+				tests = append(tests, testCase{
+					testType: testType,
+					name:     "CertificateVerificationSucceed" + suffix,
+					config: Config{
+						MaxVersion:   vers.version,
+						Certificates: []Certificate{rsaCertificate},
+					},
+					tls13Variant:  vers.tls13Variant,
+					flags:         append([]string{"-expect-verify-result"}, flags...),
+					resumeSession: true,
+				})
+				tests = append(tests, testCase{
+					testType: testType,
+					name:     "CertificateVerificationFail" + suffix,
+					config: Config{
+						MaxVersion:   vers.version,
+						Certificates: []Certificate{rsaCertificate},
+					},
+					tls13Variant:  vers.tls13Variant,
+					flags:         append([]string{"-verify-fail"}, flags...),
+					shouldFail:    true,
+					expectedError: ":CERTIFICATE_VERIFY_FAILED:",
+				})
+			}
 		}
 
 		// By default, the client is in a soft fail mode where the peer
diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc
index f925504..fa7dfe1 100644
--- a/ssl/test/test_config.cc
+++ b/ssl/test/test_config.cc
@@ -129,6 +129,7 @@
   { "-handshake-twice", &TestConfig::handshake_twice },
   { "-allow-unknown-alpn-protos", &TestConfig::allow_unknown_alpn_protos },
   { "-enable-ed25519", &TestConfig::enable_ed25519 },
+  { "-use-custom-verify-callback", &TestConfig::use_custom_verify_callback },
 };
 
 const Flag<std::string> kStringFlags[] = {
diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h
index e157936..1e5912e 100644
--- a/ssl/test/test_config.h
+++ b/ssl/test/test_config.h
@@ -144,6 +144,7 @@
   bool handshake_twice = false;
   bool allow_unknown_alpn_protos = false;
   bool enable_ed25519 = false;
+  bool use_custom_verify_callback = false;
 };
 
 bool ParseConfig(int argc, char **argv, TestConfig *out_initial,
diff --git a/ssl/tls13_both.cc b/ssl/tls13_both.cc
index 83ff78a..6a6c3c6 100644
--- a/ssl/tls13_both.cc
+++ b/ssl/tls13_both.cc
@@ -102,6 +102,11 @@
         hs->wait = ssl_hs_ok;
         return -1;
 
+      case ssl_hs_certificate_verify:
+        ssl->rwstate = SSL_CERTIFICATE_VERIFY;
+        hs->wait = ssl_hs_ok;
+        return -1;
+
       case ssl_hs_early_data_rejected:
         ssl->rwstate = SSL_EARLY_DATA_REJECTED;
         /* Cause |SSL_write| to start failing immediately. */
@@ -357,12 +362,6 @@
   }
 
   hs->new_session->peer_sha256_valid = retain_sha256;
-
-  if (!ssl->ctx->x509_method->session_verify_cert_chain(hs->new_session,
-                                                        ssl)) {
-    goto err;
-  }
-
   ret = 1;
 
 err:
diff --git a/ssl/tls13_client.cc b/ssl/tls13_client.cc
index 7f961bf..9153dd7 100644
--- a/ssl/tls13_client.cc
+++ b/ssl/tls13_client.cc
@@ -494,6 +494,16 @@
 static enum ssl_hs_wait_t do_process_server_certificate_verify(
     SSL_HANDSHAKE *hs) {
   SSL *const ssl = hs->ssl;
+  switch (ssl_verify_peer_cert(hs)) {
+    case ssl_verify_ok:
+      break;
+    case ssl_verify_invalid:
+      return ssl_hs_error;
+    case ssl_verify_retry:
+      hs->tls13_state = state_process_server_certificate_verify;
+      return ssl_hs_certificate_verify;
+  }
+
   if (!ssl_check_message_type(ssl, SSL3_MT_CERTIFICATE_VERIFY) ||
       !tls13_process_certificate_verify(hs) ||
       !ssl_hash_current_message(hs)) {
diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc
index 4e66016..dd7e86d 100644
--- a/ssl/tls13_server.cc
+++ b/ssl/tls13_server.cc
@@ -766,6 +766,16 @@
     return ssl_hs_ok;
   }
 
+  switch (ssl_verify_peer_cert(hs)) {
+    case ssl_verify_ok:
+      break;
+    case ssl_verify_invalid:
+      return ssl_hs_error;
+    case ssl_verify_retry:
+      hs->tls13_state = state_process_client_certificate_verify;
+      return ssl_hs_certificate_verify;
+  }
+
   if (!ssl_check_message_type(ssl, SSL3_MT_CERTIFICATE_VERIFY) ||
       !tls13_process_certificate_verify(hs) ||
       !ssl_hash_current_message(hs)) {
diff --git a/ssl/tls_method.cc b/ssl/tls_method.cc
index d039b7d..0bcdf92 100644
--- a/ssl/tls_method.cc
+++ b/ssl/tls_method.cc
@@ -240,15 +240,9 @@
 }
 static void ssl_noop_x509_session_clear(SSL_SESSION *session) {}
 static int ssl_noop_x509_session_verify_cert_chain(SSL_SESSION *session,
-                                                   SSL *ssl) {
-  if (!ssl->ctx->i_promise_to_verify_certs_after_the_handshake) {
-    ssl3_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_UNKNOWN_CA);
-    OPENSSL_PUT_ERROR(SSL, SSL_R_CERTIFICATE_VERIFY_FAILED);
-    return 0;
-  }
-
-  session->verify_result = X509_V_OK;
-  return 1;
+                                                   SSL *ssl,
+                                                   uint8_t *out_alert) {
+  return 0;
 }
 
 static void ssl_noop_x509_hs_flush_cached_ca_names(SSL_HANDSHAKE *hs) {}