Quellcode durchsuchen

match a wildcard only in the leftmost label in match_hostname (#2619)

* match a wildcard only in the leftmost label in match_hostname

* Shorten the wildcard comment in match_hostname

---------

Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
metsw24-max vor 1 Tag
Ursprung
Commit
8f094fe292
5 geänderte Dateien mit 86 neuen und 3 gelöschten Zeilen
  1. 4 2
      httplib.h
  2. 9 0
      test/CMakeLists.txt
  3. 5 0
      test/gen-certs.sh
  4. 12 1
      test/meson.build
  5. 56 0
      test/test.cc

+ 4 - 2
httplib.h

@@ -10759,12 +10759,14 @@ inline bool match_hostname(const std::string &pattern,
   // Compare each component with wildcard support
   // Supports: "*" (full wildcard), "prefix*" (partial wildcard)
   // https://bugs.launchpad.net/ubuntu/+source/firefox-3.0/+bug/376484
+  // Only the leftmost label may carry a wildcard (RFC 6125 6.4.3)
   auto itr = pattern_components.begin();
   for (const auto &h : host_components) {
     auto &p = *itr;
-    if (!detail::case_ignore::equal(p, h) && p != "*") {
+    auto is_leftmost = itr == pattern_components.begin();
+    if (!detail::case_ignore::equal(p, h) && !(is_leftmost && p == "*")) {
       bool partial_match = false;
-      if (!p.empty() && p[p.size() - 1] == '*') {
+      if (is_leftmost && !p.empty() && p[p.size() - 1] == '*') {
         const auto prefix_length = p.size() - 1;
         if (prefix_length == 0) {
           partial_match = true;

+ 9 - 0
test/CMakeLists.txt

@@ -153,6 +153,15 @@ if(HTTPLIB_IS_USING_OPENSSL)
         WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
         COMMAND_ERROR_IS_FATAL ANY
     )
+    # cert_wildcard_san.pem: a leftmost wildcard next to one that is not
+    #                 leftmost. "*.leftmost.example.test" matches a single
+    #                 label; the wildcard in "www.*.example.test" must not be
+    #                 honoured.
+    execute_process(
+        COMMAND ${OPENSSL_COMMAND} req -x509 -key key.pem -sha256 -days 3650 -nodes -subj /CN=wildcard-san -addext subjectAltName=DNS:*.leftmost.example.test,DNS:www.*.example.test -out cert_wildcard_san.pem
+        WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
+        COMMAND_ERROR_IS_FATAL ANY
+    )
 endif()
 
 add_subdirectory(fuzzing)

+ 5 - 0
test/gen-certs.sh

@@ -37,3 +37,8 @@ openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext
 # cert_san_types.pem: the bytes of each SAN read as the other type:
 #                 DNS:a.zz is 97.46.122.122, IP:42.46.122.122 is "*.zz".
 openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=san-types" -addext "subjectAltName=DNS:a.zz,IP:42.46.122.122" -out cert_san_types.pem
+
+# cert_wildcard_san.pem: a leftmost wildcard next to one that is not leftmost.
+#                 "*.leftmost.example.test" matches a single label; the
+#                 wildcard in "www.*.example.test" must not be honoured.
+openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=wildcard-san" -addext "subjectAltName=DNS:*.leftmost.example.test,DNS:www.*.example.test" -out cert_wildcard_san.pem

+ 12 - 1
test/meson.build

@@ -146,6 +146,16 @@ cert_san_types_pem = custom_target(
   command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=san-types', '-addext', 'subjectAltName=DNS:a.zz,IP:42.46.122.122', '-out', '@OUTPUT@']
 )
 
+# cert_wildcard_san.pem: a leftmost wildcard next to one that is not leftmost.
+# "*.leftmost.example.test" matches a single label; the wildcard in
+# "www.*.example.test" must not be honoured.
+cert_wildcard_san_pem = custom_target(
+  'cert_wildcard_san_pem',
+  input: key_pem,
+  output: 'cert_wildcard_san.pem',
+  command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=wildcard-san', '-addext', 'subjectAltName=DNS:*.leftmost.example.test,DNS:www.*.example.test', '-out', '@OUTPUT@']
+)
+
 # Copy test files to the build directory
 configure_file(input: 'ca-bundle.crt', output: 'ca-bundle.crt', copy: true)
 configure_file(input: 'image.jpg',     output: 'image.jpg',     copy: true)
@@ -188,7 +198,8 @@ test(
     client_encrypted_cert_pem,
     cert_ip_cn_pem,
     cert_ipv6_pem,
-    cert_san_types_pem
+    cert_san_types_pem,
+    cert_wildcard_san_pem
   ],
   workdir: meson.current_build_dir(),
   timeout: 300

+ 56 - 0
test/test.cc

@@ -43,6 +43,7 @@ inline std::string u8_to_string(const char8_t *s) {
 #define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.pem"
 #define SERVER_CERT_IPV6_FILE "./cert_ipv6.pem"
 #define SERVER_CERT_SAN_TYPES_FILE "./cert_san_types.pem"
+#define SERVER_CERT_WILDCARD_SAN_FILE "./cert_wildcard_san.pem"
 #define SERVER_PRIVATE_KEY_FILE "./key.pem"
 #define CA_CERT_FILE "./ca-bundle.crt"
 #define CLIENT_CA_CERT_FILE "./rootCA.cert.pem"
@@ -14940,6 +14941,61 @@ TEST(SSLClientServerTest, TlsVerifyHostnameSanType) {
       << "A DNS host must not be authenticated via an iPAddress SAN";
 }
 
+// RFC 6125 6.4.3: only the leftmost label of a dNSName may be a wildcard, so
+// "www.*.example.test" names one host and not every host under example.test.
+TEST(SSLClientServerTest, TlsVerifyHostnameWildcardLabel) {
+  using namespace httplib::tls;
+
+  // SANs: DNS:*.leftmost.example.test, DNS:www.*.example.test
+  SSLServer svr(SERVER_CERT_WILDCARD_SAN_FILE, SERVER_PRIVATE_KEY_FILE);
+  ASSERT_TRUE(svr.is_valid());
+
+  svr.Get("/test", [](const Request &, Response &res) {
+    res.set_content("ok", "text/plain");
+  });
+
+  auto port = svr.bind_to_any_port(HOST);
+  thread t([&]() { svr.listen_after_bind(); });
+  auto se = detail::scope_exit([&] {
+    svr.stop();
+    t.join();
+  });
+  svr.wait_until_ready();
+
+  bool verify_callback_called = false;
+  bool leftmost_wildcard_matched = false;
+  bool wildcard_spanned_labels = true;
+  bool inner_wildcard_matched = true;
+
+  SSLClient cli(HOST, port);
+  cli.enable_server_certificate_verification(true);
+  cli.set_ca_cert_path(CA_CERT_FILE);
+  cli.set_connection_timeout(5);
+
+  cli.set_server_certificate_verifier([&](const VerifyContext &ctx) -> bool {
+    verify_callback_called = true;
+    if (!ctx.cert) return false;
+
+    leftmost_wildcard_matched = ctx.check_hostname("a.leftmost.example.test");
+
+    wildcard_spanned_labels = ctx.check_hostname("a.b.leftmost.example.test");
+    inner_wildcard_matched = ctx.check_hostname("www.evil.example.test");
+
+    return true; // Accept for the purpose of this test
+  });
+
+  cli.Get("/test");
+
+  ASSERT_TRUE(verify_callback_called)
+      << "Verify callback should have been called";
+  EXPECT_TRUE(leftmost_wildcard_matched)
+      << "A leftmost wildcard should match a single label";
+  EXPECT_FALSE(wildcard_spanned_labels)
+      << "A wildcard label must not match more than one label";
+  EXPECT_FALSE(inner_wildcard_matched)
+      << "A wildcard outside the leftmost label must not be honoured";
+}
+
 // sans() must report each SAN entry under its own type.
 TEST(SSLClientServerTest, TlsCertSansEntryTypes) {
   using namespace httplib::tls;