浏览代码

Do not resume sessions on an OpenSSL context with a verify callback

A resumed handshake does not show the client certificate again, so since
OpenSSL mTLS servers resume sessions, a verify callback, such as one
checking a deny list, was skipped for as long as the ticket was good
(7200 s by default). Turn off session tickets, TLS 1.3 ones included,
and the session cache on a context with a callback, as on wolfSSL.
yhirose 2 天之前
父节点
当前提交
a09e00b03d
共有 2 个文件被更改,包括 88 次插入 和 40 次删除
  1. 10 0
      httplib.h
  2. 78 40
      test/test.cc

+ 10 - 0
httplib.h

@@ -20550,6 +20550,16 @@ inline bool set_verify_callback(ctx_t ctx, VerifyCallback callback) {
   // A handshake running the previous callback keeps it until it returns.
   delete previous;
 
+  // A resumed handshake shows no certificate to run the callback on.
+  if (stored) {
+    SSL_CTX_set_options(ssl_ctx, SSL_OP_NO_TICKET);
+    SSL_CTX_set_session_cache_mode(ssl_ctx, SSL_SESS_CACHE_OFF);
+#if !defined(OPENSSL_IS_BORINGSSL) && !defined(LIBRESSL_VERSION_NUMBER)
+    // TLS 1.3 tickets, which SSL_OP_NO_TICKET leaves on
+    SSL_CTX_set_num_tickets(ssl_ctx, 0);
+#endif
+  }
+
   // Keeps the other mode bits, so a server that requires a client
   // certificate goes on requiring one.
   SSL_CTX_set_verify(ssl_ctx,

+ 78 - 40
test/test.cc

@@ -4976,37 +4976,22 @@ TEST(SSLClientServerTest, ThrowingVerifyCallbackRejectsTheCertificate) {
 }
 
 #ifdef CPPHTTPLIB_OPENSSL_SUPPORT
-// A client that resumes its session gets through, but not once the client CA
-// has changed, as a resumed session does not show the certificate again.
-TEST(SSLClientServerTest, ClientResumesSessionUntilClientCAChanges) {
-  SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE);
-  ASSERT_TRUE(svr.is_valid());
-  svr.Get("/test", [](const Request &, Response &res) {
-    res.set_content("ok", "text/plain");
-  });
-
-  thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); });
-  auto se = detail::scope_exit([&] {
-    svr.stop();
-    t.join();
-    ASSERT_FALSE(svr.is_running());
-  });
-  svr.wait_until_ready();
-
-  auto ctx = SSL_CTX_new(TLS_client_method());
-  auto ctx_se = detail::scope_exit([&] { SSL_CTX_free(ctx); });
-  ASSERT_EQ(
-      1, SSL_CTX_use_certificate_file(ctx, CLIENT_CERT_FILE, SSL_FILETYPE_PEM));
-  ASSERT_EQ(1, SSL_CTX_use_PrivateKey_file(ctx, CLIENT_PRIVATE_KEY_FILE,
-                                           SSL_FILETYPE_PEM));
-
-  SSL_SESSION *session = nullptr;
-  auto session_se = detail::scope_exit([&] { SSL_SESSION_free(session); });
+// Sends each request over a new connection with a client certificate,
+// resuming the session of the one before when there is one.
+class ResumingClient {
+public:
+  ResumingClient() : ctx_(SSL_CTX_new(TLS_client_method())) {
+    SSL_CTX_use_certificate_file(ctx_, CLIENT_CERT_FILE, SSL_FILETYPE_PEM);
+    SSL_CTX_use_PrivateKey_file(ctx_, CLIENT_PRIVATE_KEY_FILE,
+                                SSL_FILETYPE_PEM);
+  }
+  ~ResumingClient() {
+    SSL_SESSION_free(session_);
+    SSL_CTX_free(ctx_);
+  }
 
-  // A request over a new connection, resuming `session` when there is one and
-  // keeping the connection's own in its place. Tells whether it was answered.
-  auto reused = false;
-  auto get = [&]() {
+  // Tells whether the request was answered.
+  bool get() {
     auto error = Error::Success;
     auto sock = detail::create_client_socket(
         HOST, "", PORT, AF_UNSPEC, false, false, nullptr,
@@ -5016,12 +5001,12 @@ TEST(SSLClientServerTest, ClientResumesSessionUntilClientCAChanges) {
     if (sock == INVALID_SOCKET) { return false; }
     auto sock_se = detail::scope_exit([&] { detail::close_socket(sock); });
 
-    auto ssl = SSL_new(ctx);
+    auto ssl = SSL_new(ctx_);
     auto ssl_se = detail::scope_exit([&] { SSL_free(ssl); });
     SSL_set_fd(ssl, static_cast<int>(sock));
-    if (session) { SSL_set_session(ssl, session); }
+    if (session_) { SSL_set_session(ssl, session_); }
     if (SSL_connect(ssl) != 1) { return false; }
-    reused = SSL_session_reused(ssl) == 1;
+    reused_ = SSL_session_reused(ssl) == 1;
 
     // Reading the response also takes in a TLS 1.3 session ticket.
     std::string req = "GET /test HTTP/1.1\r\nHost: x\r\n"
@@ -5036,17 +5021,70 @@ TEST(SSLClientServerTest, ClientResumesSessionUntilClientCAChanges) {
 
     // Without a proper shutdown, OpenSSL marks the session not resumable.
     SSL_shutdown(ssl);
-    SSL_SESSION_free(session);
-    session = SSL_get1_session(ssl);
+    SSL_SESSION_free(session_);
+    session_ = SSL_get1_session(ssl);
     return res.find("200 OK") != std::string::npos;
-  };
+  }
 
-  ASSERT_TRUE(get());
-  ASSERT_TRUE(get());
-  EXPECT_TRUE(reused);
+  // Whether the last connection resumed a session
+  bool reused() const { return reused_; }
+
+private:
+  SSL_CTX *ctx_;
+  SSL_SESSION *session_ = nullptr;
+  bool reused_ = false;
+};
+
+// A client that resumes its session gets through, but not once the client CA
+// has changed, as a resumed session does not show the certificate again.
+TEST(SSLClientServerTest, ClientResumesSessionUntilClientCAChanges) {
+  SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE);
+  ASSERT_TRUE(svr.is_valid());
+  svr.Get("/test", [](const Request &, Response &res) {
+    res.set_content("ok", "text/plain");
+  });
+
+  thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+  svr.wait_until_ready();
+
+  ResumingClient cli;
+  ASSERT_TRUE(cli.get());
+  ASSERT_TRUE(cli.get());
+  EXPECT_TRUE(cli.reused());
 
   ASSERT_TRUE(replace_client_ca(svr));
-  EXPECT_FALSE(get());
+  EXPECT_FALSE(cli.get());
+}
+
+// A resumed session does not show the certificate again, so the server does
+// not resume one with a verify callback to run on it.
+TEST(SSLClientServerTest, NoSessionResumptionWithVerifyCallback) {
+  SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE, CLIENT_CA_CERT_FILE);
+  ASSERT_TRUE(svr.is_valid());
+  tls::set_verify_callback(svr.tls_context(), [](const tls::VerifyContext &v) {
+    return v.preverify_ok;
+  });
+  svr.Get("/test", [](const Request &, Response &res) {
+    res.set_content("ok", "text/plain");
+  });
+
+  thread t = thread([&]() { ASSERT_TRUE(svr.listen(HOST, PORT)); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+    ASSERT_FALSE(svr.is_running());
+  });
+  svr.wait_until_ready();
+
+  ResumingClient cli;
+  ASSERT_TRUE(cli.get());
+  ASSERT_TRUE(cli.get());
+  EXPECT_FALSE(cli.reused());
 }
 #endif