mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-17 10:59:39 +00:00
Restrict IP-host hostname verification to iPAddress SANs on Mbed TLS and wolfSSL
An IP-literal host must only be authenticated via a matching iPAddress SAN, never via the certificate's Common Name (RFC 9110), as the OpenSSL backend already does through X509_check_ip. The Mbed TLS and wolfSSL backends instead fell back to the CN when no IP SAN matched, and recognized IPv4 only. This is a more complete solution for #2476, which gated the CN fallback for IPv4 hosts only; here the same gap is closed for IPv6 as well, and IPv6 iPAddress SANs are actually matched. - Add impl::parse_ip_address() to parse IPv4/IPv6 literals into raw bytes - Match IPv6 (16-byte) iPAddress SANs, not just IPv4 - Skip the CN fallback for IP-literal hosts (both IPv4 and IPv6) - Remove the unused SSLClient::verify_host* dead code - Add regression tests and test certificates for the IP-host cases
This commit is contained in:
1 parent
7307c41aa8
commit
ba390f2399
3 files changed
+167
-129
No files matched your search
+104
@@ -38,6 +38,8 @@ inline std::string u8_to_string(const char8_t *s) {
|
||||
|
||||
#define SERVER_CERT_FILE "./cert.pem"
|
||||
#define SERVER_CERT2_FILE "./cert2.pem"
|
||||
#define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.pem"
|
||||
#define SERVER_CERT_IPV6_FILE "./cert_ipv6.pem"
|
||||
#define SERVER_PRIVATE_KEY_FILE "./key.pem"
|
||||
#define CA_CERT_FILE "./ca-bundle.crt"
|
||||
#define CLIENT_CA_CERT_FILE "./rootCA.cert.pem"
|
||||
@@ -10939,6 +10941,108 @@ TEST(SSLClientServerTest, TlsVerifyHostname) {
|
||||
EXPECT_FALSE(verify_result_wrong)
|
||||
<< "verify_hostname should not match 'wronghost.example.com'";
|
||||
}
|
||||
|
||||
// An IP-literal host must only be authenticated via an iPAddress SAN, never via
|
||||
// the certificate's Common Name (RFC 9110). This mirrors the OpenSSL backend's
|
||||
// X509_check_ip behavior and must hold for every backend.
|
||||
TEST(SSLClientServerTest, TlsVerifyHostnameIpNotMatchedByCommonName) {
|
||||
using namespace httplib::tls;
|
||||
|
||||
// Certificate CN is the IPv4 literal "127.0.0.1" and it carries no SAN.
|
||||
SSLServer svr(SERVER_CERT_IP_CN_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
|
||||
svr.Get("/test", [](const Request &, Response &res) {
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
thread t([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
bool verify_callback_called = false;
|
||||
bool ip_matched_via_cn = 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;
|
||||
|
||||
// The IP appears only in the CN, so it must NOT be accepted.
|
||||
ip_matched_via_cn = ctx.check_hostname("127.0.0.1");
|
||||
|
||||
return true; // Accept for the purpose of this test
|
||||
});
|
||||
|
||||
cli.Get("/test");
|
||||
|
||||
ASSERT_TRUE(verify_callback_called)
|
||||
<< "Verify callback should have been called";
|
||||
EXPECT_FALSE(ip_matched_via_cn)
|
||||
<< "An IP host must not be authenticated via the certificate CN";
|
||||
}
|
||||
|
||||
// IPv6 hosts must be matched against IPv6 iPAddress SANs (and only those).
|
||||
TEST(SSLClientServerTest, TlsVerifyHostnameIpv6San) {
|
||||
using namespace httplib::tls;
|
||||
|
||||
// Certificate CN is "::1" and it carries an IPv6 SAN for "2001:db8::1".
|
||||
SSLServer svr(SERVER_CERT_IPV6_FILE, SERVER_PRIVATE_KEY_FILE);
|
||||
ASSERT_TRUE(svr.is_valid());
|
||||
|
||||
svr.Get("/test", [](const Request &, Response &res) {
|
||||
res.set_content("ok", "text/plain");
|
||||
});
|
||||
|
||||
thread t([&]() { svr.listen(HOST, PORT); });
|
||||
auto se = detail::scope_exit([&] {
|
||||
svr.stop();
|
||||
t.join();
|
||||
});
|
||||
svr.wait_until_ready();
|
||||
|
||||
bool verify_callback_called = false;
|
||||
bool san_matched = false;
|
||||
bool wrong_ipv6_matched = true;
|
||||
bool cn_ipv6_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;
|
||||
|
||||
// Matches the IPv6 iPAddress SAN.
|
||||
san_matched = ctx.check_hostname("2001:db8::1");
|
||||
// A different IPv6 address must not match.
|
||||
wrong_ipv6_matched = ctx.check_hostname("2001:db8::2");
|
||||
// "::1" lives only in the CN, so it must not be accepted.
|
||||
cn_ipv6_matched = ctx.check_hostname("::1");
|
||||
|
||||
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(san_matched)
|
||||
<< "verify_hostname should match an IPv6 iPAddress SAN";
|
||||
EXPECT_FALSE(wrong_ipv6_matched)
|
||||
<< "verify_hostname should not match a non-matching IPv6 address";
|
||||
EXPECT_FALSE(cn_ipv6_matched)
|
||||
<< "An IPv6 host must not be authenticated via the certificate CN";
|
||||
}
|
||||
#endif
|
||||
|
||||
// mbedTLS-specific callback constructor test
|
||||
|
||||
Reference in new issue
Block a user