فهرست منبع

Complete server certificate updates on every backend

OpenSSL allowed client-initiated renegotiation, which runs a handshake
update_certs_pem() does not wait for, and kept sessions and ticket keys
across a change of client CAs, so a client verified under the old ones
could resume. wolfSSL added new client CAs to the old instead of
replacing them, and a bad certificate or key left Mbed TLS and wolfSSL
half updated.

Turn renegotiation off, forget the sessions and ticket keys on a client
CA change, replace wolfSSL's CAs, and check new material before swapping
it in. Note that the update must not be called from a TLS callback.
yhirose 1 روز پیش
والد
کامیت
fcf7568230
4فایلهای تغییر یافته به همراه103 افزوده شده و 15 حذف شده
  1. 1 1
      docs-src/pages/en/cookbook/t03-ssl-server.md
  2. 1 1
      docs-src/pages/ja/cookbook/t03-ssl-server.md
  3. 69 13
      httplib.h
  4. 32 0
      test/test.cc

+ 1 - 1
docs-src/pages/en/cookbook/t03-ssl-server.md

@@ -60,7 +60,7 @@ Before a certificate expires, you may want to swap it out without restarting the
 svr.update_certs_pem(new_cert_pem, new_key_pem);
 ```
 
-Existing connections keep using the old cert; new connections use the new one. It waits for handshakes in progress to finish, at most the read timeout, and new connections wait until it returns.
+Existing connections keep using the old cert; new connections use the new one. It waits for handshakes in progress to finish, at most the read timeout, and new connections wait until it returns. So don't call it from a TLS callback that runs during a handshake: it would wait for that handshake.
 
 ## Generating a test certificate
 

+ 1 - 1
docs-src/pages/ja/cookbook/t03-ssl-server.md

@@ -60,7 +60,7 @@ httplib::SSLServer svr(pem);
 svr.update_certs_pem(new_cert_pem, new_key_pem);
 ```
 
-既存の接続はそのまま、これから確立する接続は新しい証明書で動きます。差し替えは進行中のハンドシェイクが終わってから行われ(長くても読み取りタイムアウトまで)、その間に来た接続は差し替えが終わるまで待ちます。
+既存の接続はそのまま、これから確立する接続は新しい証明書で動きます。差し替えは進行中のハンドシェイクが終わってから行われ(長くても読み取りタイムアウトまで)、その間に来た接続は差し替えが終わるまで待ちます。そのため、ハンドシェイク中に呼ばれるTLSのコールバックからは呼ばないでください。そのハンドシェイク自身を待ってしまいます。
 
 ## 証明書の準備
 

+ 69 - 13
httplib.h

@@ -463,6 +463,7 @@ using socket_t = int;
 
 #include <openssl/err.h>
 #include <openssl/evp.h>
+#include <openssl/rand.h>
 #include <openssl/ssl.h>
 #include <openssl/x509v3.h>
 
@@ -19675,8 +19676,11 @@ inline bool set_client_cert_file(ctx_t ctx, const char *cert_path,
 inline ctx_t create_server_context() {
   SSL_CTX *ctx = SSL_CTX_new(TLS_server_method());
   if (ctx) {
+    // No renegotiation, which would run a handshake outside the one that
+    // SSLServer::update_certs_pem() waits for, as the other backends have it
     SSL_CTX_set_options(ctx, SSL_OP_NO_COMPRESSION |
-                                 SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION);
+                                 SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION |
+                                 SSL_OP_NO_RENEGOTIATION);
     SSL_CTX_set_min_proto_version(ctx, TLS1_2_VERSION);
   }
   return static_cast<ctx_t>(ctx);
@@ -20313,6 +20317,19 @@ inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {
     SSL_CTX_set_client_CA_list(ssl_ctx, ca_list);
   }
 
+  // A resumed session is not verified again, so forget the sessions and the
+  // ticket keys from before, or a client verified under the old CAs could go
+  // on resuming.
+#if OPENSSL_VERSION_NUMBER >= 0x30400000L
+  SSL_CTX_flush_sessions_ex(ssl_ctx, (std::numeric_limits<time_t>::max)());
+#else
+  SSL_CTX_flush_sessions(ssl_ctx, (std::numeric_limits<long>::max)());
+#endif
+  unsigned char ticket_keys[80];
+  if (RAND_bytes(ticket_keys, sizeof(ticket_keys)) == 1) {
+    SSL_CTX_set_tlsext_ticket_keys(ssl_ctx, ticket_keys, sizeof(ticket_keys));
+  }
+
   return true;
 }
 
@@ -21650,15 +21667,20 @@ inline bool update_server_cert(ctx_t ctx, const char *cert_pem,
   if (!ctx || !cert_pem || !key_pem) { return false; }
   auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
 
-  // Free existing certificate and key
-  mbedtls_x509_crt_free(&mbed_ctx->own_cert);
-  mbedtls_pk_free(&mbed_ctx->own_key);
-  mbedtls_x509_crt_init(&mbed_ctx->own_cert);
-  mbedtls_pk_init(&mbed_ctx->own_key);
+  // Parse into new objects first, so that a bad pair leaves the old one in
+  // place, and swap them in after; the old ones are freed on the way out.
+  mbedtls_x509_crt cert;
+  mbedtls_pk_context key;
+  mbedtls_x509_crt_init(&cert);
+  mbedtls_pk_init(&key);
+  auto se = detail::scope_exit([&] {
+    mbedtls_x509_crt_free(&cert);
+    mbedtls_pk_free(&key);
+  });
 
   // Parse certificate PEM
   int ret = mbedtls_x509_crt_parse(
-      &mbed_ctx->own_cert, reinterpret_cast<const unsigned char *>(cert_pem),
+      &cert, reinterpret_cast<const unsigned char *>(cert_pem),
       strlen(cert_pem) + 1);
   if (ret != 0) {
     impl::mbedtls_last_error() = ret;
@@ -21668,14 +21690,14 @@ inline bool update_server_cert(ctx_t ctx, const char *cert_pem,
   // Parse private key PEM
 #if defined(CPPHTTPLIB_MBEDTLS_V3) && !defined(CPPHTTPLIB_MBEDTLS_V4)
   ret = mbedtls_pk_parse_key(
-      &mbed_ctx->own_key, reinterpret_cast<const unsigned char *>(key_pem),
+      &key, reinterpret_cast<const unsigned char *>(key_pem),
       strlen(key_pem) + 1,
       password ? reinterpret_cast<const unsigned char *>(password) : nullptr,
       password ? strlen(password) : 0, mbedtls_ctr_drbg_random,
       &mbed_ctx->ctr_drbg);
 #else
   ret = mbedtls_pk_parse_key(
-      &mbed_ctx->own_key, reinterpret_cast<const unsigned char *>(key_pem),
+      &key, reinterpret_cast<const unsigned char *>(key_pem),
       strlen(key_pem) + 1,
       password ? reinterpret_cast<const unsigned char *>(password) : nullptr,
       password ? strlen(password) : 0);
@@ -21685,6 +21707,9 @@ inline bool update_server_cert(ctx_t ctx, const char *cert_pem,
     return false;
   }
 
+  std::swap(mbed_ctx->own_cert, cert);
+  std::swap(mbed_ctx->own_key, key);
+
   // Configure SSL to use the new certificate and key
   ret = mbedtls_ssl_conf_own_cert(&mbed_ctx->conf, &mbed_ctx->own_cert,
                                   &mbed_ctx->own_key);
@@ -21700,18 +21725,20 @@ inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {
   if (!ctx || !ca_pem) { return false; }
   auto *mbed_ctx = static_cast<impl::MbedTlsContext *>(ctx);
 
-  // Free existing CA chain
-  mbedtls_x509_crt_free(&mbed_ctx->ca_chain);
-  mbedtls_x509_crt_init(&mbed_ctx->ca_chain);
+  // Parse into a new chain first, so that a bad one leaves the old in place
+  mbedtls_x509_crt chain;
+  mbedtls_x509_crt_init(&chain);
+  auto se = detail::scope_exit([&] { mbedtls_x509_crt_free(&chain); });
 
   // Parse CA PEM
   int ret = mbedtls_x509_crt_parse(
-      &mbed_ctx->ca_chain, reinterpret_cast<const unsigned char *>(ca_pem),
+      &chain, reinterpret_cast<const unsigned char *>(ca_pem),
       strlen(ca_pem) + 1);
   if (ret != 0) {
     impl::mbedtls_last_error() = ret;
     return false;
   }
+  std::swap(mbed_ctx->ca_chain, chain);
 
   // Update SSL config to use new CA chain
   mbedtls_ssl_conf_ca_chain(&mbed_ctx->conf, &mbed_ctx->ca_chain, nullptr);
@@ -22850,6 +22877,27 @@ inline bool update_server_cert(ctx_t ctx, const char *cert_pem,
   if (!ctx || !cert_pem || !key_pem) { return false; }
   auto *wctx = static_cast<impl::WolfSSLContext *>(ctx);
 
+  // Try the pair on a scratch context first, so a bad one leaves the server
+  // with its old certificate and key rather than half of each.
+  {
+    auto tmp = wolfSSL_CTX_new(wolfTLSv1_2_server_method());
+    if (!tmp) { return false; }
+    auto se = detail::scope_exit([&] { wolfSSL_CTX_free(tmp); });
+    if (password) { impl::set_wolfssl_password_cb(tmp, password); }
+    if (wolfSSL_CTX_use_certificate_buffer(
+            tmp, reinterpret_cast<const unsigned char *>(cert_pem),
+            static_cast<long>(strlen(cert_pem)),
+            SSL_FILETYPE_PEM) != SSL_SUCCESS ||
+        wolfSSL_CTX_use_PrivateKey_buffer(
+            tmp, reinterpret_cast<const unsigned char *>(key_pem),
+            static_cast<long>(strlen(key_pem)),
+            SSL_FILETYPE_PEM) != SSL_SUCCESS) {
+      impl::wolfssl_last_error() =
+          static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
+      return false;
+    }
+  }
+
   // Load new certificate
   int ret = wolfSSL_CTX_use_certificate_buffer(
       wctx->ctx, reinterpret_cast<const unsigned char *>(cert_pem),
@@ -22880,6 +22928,13 @@ inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {
   if (!ctx || !ca_pem) { return false; }
   auto *wctx = static_cast<impl::WolfSSLContext *>(ctx);
 
+  // Replace the CAs rather than add to them, as the other backends do, once
+  // the new ones are known to load
+  auto store = create_ca_store(ca_pem, strlen(ca_pem));
+  if (!store) { return false; }
+  free_ca_store(store);
+  wolfSSL_CTX_UnloadCAs(wctx->ctx);
+
   int ret = wolfSSL_CTX_load_verify_buffer(
       wctx->ctx, reinterpret_cast<const unsigned char *>(ca_pem),
       static_cast<long>(strlen(ca_pem)), SSL_FILETYPE_PEM);
@@ -22888,6 +22943,7 @@ inline bool update_server_client_ca(ctx_t ctx, const char *ca_pem) {
         static_cast<uint64_t>(wolfSSL_ERR_peek_last_error());
     return false;
   }
+  wctx->ca_pem_data_ = ca_pem;
   return true;
 }
 

+ 32 - 0
test/test.cc

@@ -4777,6 +4777,38 @@ TEST(SSLClientServerTest, UpdateCertsPemWithClientAuth) {
   EXPECT_EQ("ok", res->body);
 }
 
+TEST(SSLClientServerTest, UpdateCertsPemReplacesClientCA) {
+  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 get = [] {
+    SSLClient cli(HOST, PORT, CLIENT_CERT_FILE, CLIENT_PRIVATE_KEY_FILE);
+    cli.enable_server_certificate_verification(false);
+    return cli.Get("/test");
+  };
+  ASSERT_TRUE(get());
+
+  // A CA that did not sign the client's certificate takes the old one's place.
+  std::string cert_pem, key_pem, ca_pem;
+  read_file(SERVER_CERT_FILE, cert_pem);
+  read_file(SERVER_PRIVATE_KEY_FILE, key_pem);
+  read_file(SERVER_CERT2_FILE, ca_pem);
+  ASSERT_TRUE(
+      svr.update_certs_pem(cert_pem.c_str(), key_pem.c_str(), ca_pem.c_str()));
+  EXPECT_FALSE(get());
+}
+
 TEST(SSLClientServerTest, UpdateCertsPemWaitsForHandshakeInProgress) {
   SSLServer svr(SERVER_CERT_FILE, SERVER_PRIVATE_KEY_FILE);
   ASSERT_TRUE(svr.is_valid());