Skip to content

Commit ba390f2

Browse files
committed
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
1 parent 7307c41 commit ba390f2

3 files changed

Lines changed: 169 additions & 131 deletions

File tree

httplib.h

Lines changed: 54 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -2878,13 +2878,6 @@ class SSLClient final : public ClientImpl {
28782878
#endif
28792879

28802880
friend class ClientImpl;
2881-
2882-
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
2883-
private:
2884-
bool verify_host(X509 *server_cert) const;
2885-
bool verify_host_with_subject_alt_name(X509 *server_cert) const;
2886-
bool verify_host_with_common_name(X509 *server_cert) const;
2887-
#endif
28882881
};
28892882
#endif // CPPHTTPLIB_SSL_ENABLED
28902883

@@ -16526,6 +16519,21 @@ inline bool parse_ipv4(const std::string &str, unsigned char *out) {
1652616519
return *p == '\0';
1652716520
}
1652816521

16522+
// Parse an IP literal (IPv4 or IPv6) into raw network-order bytes.
16523+
// `out` must have room for at least 16 bytes. Returns the address length
16524+
// (4 for IPv4, 16 for IPv6) on success, or 0 if the string is not an IP
16525+
// literal. Used to match a host against iPAddress SANs the same way the
16526+
// OpenSSL backend does via X509_check_ip.
16527+
inline size_t parse_ip_address(const std::string &str, unsigned char *out) {
16528+
if (is_ipv4_address(str)) { return parse_ipv4(str, out) ? 4 : 0; }
16529+
struct in6_addr addr6 = {};
16530+
if (inet_pton(AF_INET6, str.c_str(), &addr6) == 1) {
16531+
memcpy(out, &addr6, 16);
16532+
return 16;
16533+
}
16534+
return 0;
16535+
}
16536+
1652916537
#ifdef _WIN32
1653016538
// Enumerate Windows system certificates and call callback with DER data
1653116539
template <typename Callback>
@@ -17725,99 +17733,6 @@ inline std::string verify_error_string(long error_code) {
1772517733

1772617734
} // namespace tls
1772717735

17728-
inline bool SSLClient::verify_host(X509 *server_cert) const {
17729-
/* Quote from RFC2818 section 3.1 "Server Identity"
17730-
17731-
If a subjectAltName extension of type dNSName is present, that MUST
17732-
be used as the identity. Otherwise, the (most specific) Common Name
17733-
field in the Subject field of the certificate MUST be used. Although
17734-
the use of the Common Name is existing practice, it is deprecated and
17735-
Certification Authorities are encouraged to use the dNSName instead.
17736-
17737-
Matching is performed using the matching rules specified by
17738-
[RFC2459]. If more than one identity of a given type is present in
17739-
the certificate (e.g., more than one dNSName name, a match in any one
17740-
of the set is considered acceptable.) Names may contain the wildcard
17741-
character * which is considered to match any single domain name
17742-
component or component fragment. E.g., *.a.com matches foo.a.com but
17743-
not bar.foo.a.com. f*.com matches foo.com but not bar.com.
17744-
17745-
In some cases, the URI is specified as an IP address rather than a
17746-
hostname. In this case, the iPAddress subjectAltName must be present
17747-
in the certificate and must exactly match the IP in the URI.
17748-
17749-
*/
17750-
return verify_host_with_subject_alt_name(server_cert) ||
17751-
verify_host_with_common_name(server_cert);
17752-
}
17753-
17754-
inline bool
17755-
SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
17756-
auto ret = false;
17757-
17758-
auto type = GEN_DNS;
17759-
17760-
struct in6_addr addr6 = {};
17761-
struct in_addr addr = {};
17762-
size_t addr_len = 0;
17763-
17764-
#ifndef __MINGW32__
17765-
if (inet_pton(AF_INET6, host_.c_str(), &addr6)) {
17766-
type = GEN_IPADD;
17767-
addr_len = sizeof(struct in6_addr);
17768-
} else if (inet_pton(AF_INET, host_.c_str(), &addr)) {
17769-
type = GEN_IPADD;
17770-
addr_len = sizeof(struct in_addr);
17771-
}
17772-
#endif
17773-
17774-
auto alt_names = static_cast<const struct stack_st_GENERAL_NAME *>(
17775-
X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr));
17776-
17777-
if (alt_names) {
17778-
auto dsn_matched = false;
17779-
auto ip_matched = false;
17780-
17781-
auto count = sk_GENERAL_NAME_num(alt_names);
17782-
17783-
for (decltype(count) i = 0; i < count && !dsn_matched; i++) {
17784-
auto val = sk_GENERAL_NAME_value(alt_names, i);
17785-
if (!val || val->type != type) { continue; }
17786-
17787-
auto name =
17788-
reinterpret_cast<const char *>(ASN1_STRING_get0_data(val->d.ia5));
17789-
if (name == nullptr) { continue; }
17790-
17791-
auto name_len = static_cast<size_t>(ASN1_STRING_length(val->d.ia5));
17792-
17793-
switch (type) {
17794-
case GEN_DNS:
17795-
dsn_matched =
17796-
detail::match_hostname(std::string(name, name_len), host_);
17797-
break;
17798-
17799-
case GEN_IPADD:
17800-
if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) {
17801-
ip_matched = true;
17802-
}
17803-
break;
17804-
}
17805-
}
17806-
17807-
if (dsn_matched || ip_matched) { ret = true; }
17808-
}
17809-
17810-
GENERAL_NAMES_free(const_cast<STACK_OF(GENERAL_NAME) *>(
17811-
reinterpret_cast<const STACK_OF(GENERAL_NAME) *>(alt_names)));
17812-
return ret;
17813-
}
17814-
17815-
inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const {
17816-
auto cn = tls::get_cert_subject_cn(static_cast<tls::cert_t>(server_cert));
17817-
if (cn.empty()) { return false; }
17818-
return detail::match_hostname(cn, host_);
17819-
}
17820-
1782117736
#endif // CPPHTTPLIB_OPENSSL_SUPPORT
1782217737

1782317738
/*
@@ -18620,10 +18535,10 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
1862018535
auto mcert = static_cast<const mbedtls_x509_crt *>(cert);
1862118536
std::string host_str(hostname);
1862218537

18623-
// Check if hostname is an IP address
18624-
bool is_ip = impl::is_ipv4_address(host_str);
18625-
unsigned char ip_bytes[4];
18626-
if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); }
18538+
// Check if hostname is an IP address (IPv4 or IPv6)
18539+
unsigned char ip_bytes[16];
18540+
auto ip_len = impl::parse_ip_address(host_str, ip_bytes);
18541+
auto is_ip = ip_len > 0;
1862718542

1862818543
// Check Subject Alternative Names (SAN)
1862918544
// In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags
@@ -18635,9 +18550,9 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
1863518550
size_t len = san->buf.len;
1863618551

1863718552
if (is_ip) {
18638-
// Check if this SAN is an IPv4 address (4 bytes)
18639-
if (len == 4 && memcmp(p, ip_bytes, 4) == 0) { return true; }
18640-
// Check if this SAN is an IPv6 address (16 bytes) - skip for now
18553+
// For an IP host, only a matching iPAddress SAN of the same family
18554+
// (4 bytes for IPv4, 16 bytes for IPv6) may authenticate it.
18555+
if (len == ip_len && memcmp(p, ip_bytes, ip_len) == 0) { return true; }
1864118556
} else {
1864218557
// Check if this SAN is a DNS name (printable ASCII string)
1864318558
bool is_dns = len > 0;
@@ -18652,21 +18567,25 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
1865218567
san = san->next;
1865318568
}
1865418569

18655-
// Fallback: Check Common Name (CN) in subject
18656-
char cn[256];
18657-
int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject);
18658-
if (ret > 0) {
18659-
std::string cn_str(cn);
18660-
18661-
// Look for "CN=" in the DN string
18662-
size_t cn_pos = cn_str.find("CN=");
18663-
if (cn_pos != std::string::npos) {
18664-
size_t start = cn_pos + 3;
18665-
size_t end = cn_str.find(',', start);
18666-
std::string cn_value =
18667-
cn_str.substr(start, end == std::string::npos ? end : end - start);
18668-
18669-
if (detail::match_hostname(cn_value, host_str)) { return true; }
18570+
// Fallback: Check Common Name (CN) in subject. Skipped for IP-literal hosts:
18571+
// an IP identity is only valid via an iPAddress SAN, never the CN (RFC 9110;
18572+
// the OpenSSL backend's X509_check_ip behaves the same way).
18573+
if (!is_ip) {
18574+
char cn[256];
18575+
int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject);
18576+
if (ret > 0) {
18577+
std::string cn_str(cn);
18578+
18579+
// Look for "CN=" in the DN string
18580+
size_t cn_pos = cn_str.find("CN=");
18581+
if (cn_pos != std::string::npos) {
18582+
size_t start = cn_pos + 3;
18583+
size_t end = cn_str.find(',', start);
18584+
std::string cn_value =
18585+
cn_str.substr(start, end == std::string::npos ? end : end - start);
18586+
18587+
if (detail::match_hostname(cn_value, host_str)) { return true; }
18588+
}
1867018589
}
1867118590
}
1867218591

@@ -19772,10 +19691,10 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
1977219691
auto x509 = static_cast<WOLFSSL_X509 *>(cert);
1977319692
std::string host_str(hostname);
1977419693

19775-
// Check if hostname is an IP address
19776-
bool is_ip = impl::is_ipv4_address(host_str);
19777-
unsigned char ip_bytes[4];
19778-
if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); }
19694+
// Check if hostname is an IP address (IPv4 or IPv6)
19695+
unsigned char ip_bytes[16];
19696+
auto ip_len = impl::parse_ip_address(host_str, ip_bytes);
19697+
auto is_ip = ip_len > 0;
1977919698

1978019699
// Check Subject Alternative Names
1978119700
auto *san_names = static_cast<WOLF_STACK_OF(WOLFSSL_GENERAL_NAME) *>(
@@ -19802,10 +19721,12 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
1980219721
}
1980319722
}
1980419723
} else if (is_ip && names->type == WOLFSSL_GEN_IPADD) {
19805-
// IP address
19724+
// IP address: only an iPAddress SAN of the same family (4 bytes for
19725+
// IPv4, 16 bytes for IPv6) may authenticate the host.
1980619726
unsigned char *ip_data = wolfSSL_ASN1_STRING_data(names->d.iPAddress);
19807-
int ip_len = wolfSSL_ASN1_STRING_length(names->d.iPAddress);
19808-
if (ip_data && ip_len == 4 && memcmp(ip_data, ip_bytes, 4) == 0) {
19727+
auto san_ip_len = wolfSSL_ASN1_STRING_length(names->d.iPAddress);
19728+
if (ip_data && san_ip_len == static_cast<int>(ip_len) &&
19729+
memcmp(ip_data, ip_bytes, ip_len) == 0) {
1980919730
wolfSSL_sk_free(san_names);
1981019731
return true;
1981119732
}
@@ -19814,8 +19735,10 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
1981419735
wolfSSL_sk_free(san_names);
1981519736
}
1981619737

19817-
// Fallback: Check Common Name (CN) in subject
19818-
WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509);
19738+
// Fallback: Check Common Name (CN) in subject. Skipped for IP-literal hosts:
19739+
// an IP identity is only valid via an iPAddress SAN, never the CN (RFC 9110;
19740+
// the OpenSSL backend's X509_check_ip behaves the same way).
19741+
auto subject = is_ip ? nullptr : wolfSSL_X509_get_subject_name(x509);
1981919742
if (subject) {
1982019743
char cn[256] = {};
1982119744
int cn_len = wolfSSL_X509_NAME_get_text_by_NID(subject, NID_commonName, cn,

test/gen-certs.sh

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,14 @@ openssl genrsa -passout pass:test123! 2048 > key_encrypted.pem
1616
openssl req -new -batch -config test.conf -key key_encrypted.pem | openssl x509 -days 3650 -req -signkey key_encrypted.pem > cert_encrypted.pem
1717
openssl genrsa 2048 | openssl pkcs8 -topk8 -v1 PBE-SHA1-3DES -passout pass:test012! -out client_encrypted.key.pem
1818
openssl req -new -batch -config test.conf -key client_encrypted.key.pem -passin pass:test012! | openssl x509 -days 370 -req -CA rootCA.cert.pem -CAkey rootCA.key.pem -CAcreateserial > client_encrypted.cert.pem
19+
20+
# Certificates for IP-host hostname verification regression tests.
21+
# cert_ip_cn.pem: CN is an IPv4 literal with NO subjectAltName. An IP host must
22+
# NOT be authenticated via the CN, so verifying it against this
23+
# cert must fail.
24+
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=127.0.0.1" -out cert_ip_cn.pem
25+
26+
# cert_ipv6.pem: CN is an IPv6 literal plus an IPv6 iPAddress SAN for a
27+
# different address. The SAN address must match; the CN address
28+
# must be ignored.
29+
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext "subjectAltName=IP:2001:db8::1" -out cert_ipv6.pem

test/test.cc

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ inline std::string u8_to_string(const char8_t *s) {
3838

3939
#define SERVER_CERT_FILE "./cert.pem"
4040
#define SERVER_CERT2_FILE "./cert2.pem"
41+
#define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.pem"
42+
#define SERVER_CERT_IPV6_FILE "./cert_ipv6.pem"
4143
#define SERVER_PRIVATE_KEY_FILE "./key.pem"
4244
#define CA_CERT_FILE "./ca-bundle.crt"
4345
#define CLIENT_CA_CERT_FILE "./rootCA.cert.pem"
@@ -10939,6 +10941,108 @@ TEST(SSLClientServerTest, TlsVerifyHostname) {
1093910941
EXPECT_FALSE(verify_result_wrong)
1094010942
<< "verify_hostname should not match 'wronghost.example.com'";
1094110943
}
10944+
10945+
// An IP-literal host must only be authenticated via an iPAddress SAN, never via
10946+
// the certificate's Common Name (RFC 9110). This mirrors the OpenSSL backend's
10947+
// X509_check_ip behavior and must hold for every backend.
10948+
TEST(SSLClientServerTest, TlsVerifyHostnameIpNotMatchedByCommonName) {
10949+
using namespace httplib::tls;
10950+
10951+
// Certificate CN is the IPv4 literal "127.0.0.1" and it carries no SAN.
10952+
SSLServer svr(SERVER_CERT_IP_CN_FILE, SERVER_PRIVATE_KEY_FILE);
10953+
ASSERT_TRUE(svr.is_valid());
10954+
10955+
svr.Get("/test", [](const Request &, Response &res) {
10956+
res.set_content("ok", "text/plain");
10957+
});
10958+
10959+
thread t([&]() { svr.listen(HOST, PORT); });
10960+
auto se = detail::scope_exit([&] {
10961+
svr.stop();
10962+
t.join();
10963+
});
10964+
svr.wait_until_ready();
10965+
10966+
bool verify_callback_called = false;
10967+
bool ip_matched_via_cn = true;
10968+
10969+
SSLClient cli(HOST, PORT);
10970+
cli.enable_server_certificate_verification(true);
10971+
cli.set_ca_cert_path(CA_CERT_FILE);
10972+
cli.set_connection_timeout(5);
10973+
10974+
cli.set_server_certificate_verifier([&](const VerifyContext &ctx) -> bool {
10975+
verify_callback_called = true;
10976+
if (!ctx.cert) return false;
10977+
10978+
// The IP appears only in the CN, so it must NOT be accepted.
10979+
ip_matched_via_cn = ctx.check_hostname("127.0.0.1");
10980+
10981+
return true; // Accept for the purpose of this test
10982+
});
10983+
10984+
cli.Get("/test");
10985+
10986+
ASSERT_TRUE(verify_callback_called)
10987+
<< "Verify callback should have been called";
10988+
EXPECT_FALSE(ip_matched_via_cn)
10989+
<< "An IP host must not be authenticated via the certificate CN";
10990+
}
10991+
10992+
// IPv6 hosts must be matched against IPv6 iPAddress SANs (and only those).
10993+
TEST(SSLClientServerTest, TlsVerifyHostnameIpv6San) {
10994+
using namespace httplib::tls;
10995+
10996+
// Certificate CN is "::1" and it carries an IPv6 SAN for "2001:db8::1".
10997+
SSLServer svr(SERVER_CERT_IPV6_FILE, SERVER_PRIVATE_KEY_FILE);
10998+
ASSERT_TRUE(svr.is_valid());
10999+
11000+
svr.Get("/test", [](const Request &, Response &res) {
11001+
res.set_content("ok", "text/plain");
11002+
});
11003+
11004+
thread t([&]() { svr.listen(HOST, PORT); });
11005+
auto se = detail::scope_exit([&] {
11006+
svr.stop();
11007+
t.join();
11008+
});
11009+
svr.wait_until_ready();
11010+
11011+
bool verify_callback_called = false;
11012+
bool san_matched = false;
11013+
bool wrong_ipv6_matched = true;
11014+
bool cn_ipv6_matched = true;
11015+
11016+
SSLClient cli(HOST, PORT);
11017+
cli.enable_server_certificate_verification(true);
11018+
cli.set_ca_cert_path(CA_CERT_FILE);
11019+
cli.set_connection_timeout(5);
11020+
11021+
cli.set_server_certificate_verifier([&](const VerifyContext &ctx) -> bool {
11022+
verify_callback_called = true;
11023+
if (!ctx.cert) return false;
11024+
11025+
// Matches the IPv6 iPAddress SAN.
11026+
san_matched = ctx.check_hostname("2001:db8::1");
11027+
// A different IPv6 address must not match.
11028+
wrong_ipv6_matched = ctx.check_hostname("2001:db8::2");
11029+
// "::1" lives only in the CN, so it must not be accepted.
11030+
cn_ipv6_matched = ctx.check_hostname("::1");
11031+
11032+
return true; // Accept for the purpose of this test
11033+
});
11034+
11035+
cli.Get("/test");
11036+
11037+
ASSERT_TRUE(verify_callback_called)
11038+
<< "Verify callback should have been called";
11039+
EXPECT_TRUE(san_matched)
11040+
<< "verify_hostname should match an IPv6 iPAddress SAN";
11041+
EXPECT_FALSE(wrong_ipv6_matched)
11042+
<< "verify_hostname should not match a non-matching IPv6 address";
11043+
EXPECT_FALSE(cn_ipv6_matched)
11044+
<< "An IPv6 host must not be authenticated via the certificate CN";
11045+
}
1094211046
#endif
1094311047

1094411048
// mbedTLS-specific callback constructor test

0 commit comments

Comments
 (0)