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
@@ -2878,13 +2878,6 @@ private:
|
||||
#endif
|
||||
|
||||
friend class ClientImpl;
|
||||
|
||||
#ifdef CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
private:
|
||||
bool verify_host(X509 *server_cert) const;
|
||||
bool verify_host_with_subject_alt_name(X509 *server_cert) const;
|
||||
bool verify_host_with_common_name(X509 *server_cert) const;
|
||||
#endif
|
||||
};
|
||||
#endif // CPPHTTPLIB_SSL_ENABLED
|
||||
|
||||
@@ -16526,6 +16519,21 @@ inline bool parse_ipv4(const std::string &str, unsigned char *out) {
|
||||
return *p == '\0';
|
||||
}
|
||||
|
||||
// Parse an IP literal (IPv4 or IPv6) into raw network-order bytes.
|
||||
// `out` must have room for at least 16 bytes. Returns the address length
|
||||
// (4 for IPv4, 16 for IPv6) on success, or 0 if the string is not an IP
|
||||
// literal. Used to match a host against iPAddress SANs the same way the
|
||||
// OpenSSL backend does via X509_check_ip.
|
||||
inline size_t parse_ip_address(const std::string &str, unsigned char *out) {
|
||||
if (is_ipv4_address(str)) { return parse_ipv4(str, out) ? 4 : 0; }
|
||||
struct in6_addr addr6 = {};
|
||||
if (inet_pton(AF_INET6, str.c_str(), &addr6) == 1) {
|
||||
memcpy(out, &addr6, 16);
|
||||
return 16;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
// Enumerate Windows system certificates and call callback with DER data
|
||||
template <typename Callback>
|
||||
@@ -17725,99 +17733,6 @@ inline std::string verify_error_string(long error_code) {
|
||||
|
||||
} // namespace tls
|
||||
|
||||
inline bool SSLClient::verify_host(X509 *server_cert) const {
|
||||
/* Quote from RFC2818 section 3.1 "Server Identity"
|
||||
|
||||
If a subjectAltName extension of type dNSName is present, that MUST
|
||||
be used as the identity. Otherwise, the (most specific) Common Name
|
||||
field in the Subject field of the certificate MUST be used. Although
|
||||
the use of the Common Name is existing practice, it is deprecated and
|
||||
Certification Authorities are encouraged to use the dNSName instead.
|
||||
|
||||
Matching is performed using the matching rules specified by
|
||||
[RFC2459]. If more than one identity of a given type is present in
|
||||
the certificate (e.g., more than one dNSName name, a match in any one
|
||||
of the set is considered acceptable.) Names may contain the wildcard
|
||||
character * which is considered to match any single domain name
|
||||
component or component fragment. E.g., *.a.com matches foo.a.com but
|
||||
not bar.foo.a.com. f*.com matches foo.com but not bar.com.
|
||||
|
||||
In some cases, the URI is specified as an IP address rather than a
|
||||
hostname. In this case, the iPAddress subjectAltName must be present
|
||||
in the certificate and must exactly match the IP in the URI.
|
||||
|
||||
*/
|
||||
return verify_host_with_subject_alt_name(server_cert) ||
|
||||
verify_host_with_common_name(server_cert);
|
||||
}
|
||||
|
||||
inline bool
|
||||
SSLClient::verify_host_with_subject_alt_name(X509 *server_cert) const {
|
||||
auto ret = false;
|
||||
|
||||
auto type = GEN_DNS;
|
||||
|
||||
struct in6_addr addr6 = {};
|
||||
struct in_addr addr = {};
|
||||
size_t addr_len = 0;
|
||||
|
||||
#ifndef __MINGW32__
|
||||
if (inet_pton(AF_INET6, host_.c_str(), &addr6)) {
|
||||
type = GEN_IPADD;
|
||||
addr_len = sizeof(struct in6_addr);
|
||||
} else if (inet_pton(AF_INET, host_.c_str(), &addr)) {
|
||||
type = GEN_IPADD;
|
||||
addr_len = sizeof(struct in_addr);
|
||||
}
|
||||
#endif
|
||||
|
||||
auto alt_names = static_cast<const struct stack_st_GENERAL_NAME *>(
|
||||
X509_get_ext_d2i(server_cert, NID_subject_alt_name, nullptr, nullptr));
|
||||
|
||||
if (alt_names) {
|
||||
auto dsn_matched = false;
|
||||
auto ip_matched = false;
|
||||
|
||||
auto count = sk_GENERAL_NAME_num(alt_names);
|
||||
|
||||
for (decltype(count) i = 0; i < count && !dsn_matched; i++) {
|
||||
auto val = sk_GENERAL_NAME_value(alt_names, i);
|
||||
if (!val || val->type != type) { continue; }
|
||||
|
||||
auto name =
|
||||
reinterpret_cast<const char *>(ASN1_STRING_get0_data(val->d.ia5));
|
||||
if (name == nullptr) { continue; }
|
||||
|
||||
auto name_len = static_cast<size_t>(ASN1_STRING_length(val->d.ia5));
|
||||
|
||||
switch (type) {
|
||||
case GEN_DNS:
|
||||
dsn_matched =
|
||||
detail::match_hostname(std::string(name, name_len), host_);
|
||||
break;
|
||||
|
||||
case GEN_IPADD:
|
||||
if (!memcmp(&addr6, name, addr_len) || !memcmp(&addr, name, addr_len)) {
|
||||
ip_matched = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (dsn_matched || ip_matched) { ret = true; }
|
||||
}
|
||||
|
||||
GENERAL_NAMES_free(const_cast<STACK_OF(GENERAL_NAME) *>(
|
||||
reinterpret_cast<const STACK_OF(GENERAL_NAME) *>(alt_names)));
|
||||
return ret;
|
||||
}
|
||||
|
||||
inline bool SSLClient::verify_host_with_common_name(X509 *server_cert) const {
|
||||
auto cn = tls::get_cert_subject_cn(static_cast<tls::cert_t>(server_cert));
|
||||
if (cn.empty()) { return false; }
|
||||
return detail::match_hostname(cn, host_);
|
||||
}
|
||||
|
||||
#endif // CPPHTTPLIB_OPENSSL_SUPPORT
|
||||
|
||||
/*
|
||||
@@ -18620,10 +18535,10 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
|
||||
auto mcert = static_cast<const mbedtls_x509_crt *>(cert);
|
||||
std::string host_str(hostname);
|
||||
|
||||
// Check if hostname is an IP address
|
||||
bool is_ip = impl::is_ipv4_address(host_str);
|
||||
unsigned char ip_bytes[4];
|
||||
if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); }
|
||||
// Check if hostname is an IP address (IPv4 or IPv6)
|
||||
unsigned char ip_bytes[16];
|
||||
auto ip_len = impl::parse_ip_address(host_str, ip_bytes);
|
||||
auto is_ip = ip_len > 0;
|
||||
|
||||
// Check Subject Alternative Names (SAN)
|
||||
// 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) {
|
||||
size_t len = san->buf.len;
|
||||
|
||||
if (is_ip) {
|
||||
// Check if this SAN is an IPv4 address (4 bytes)
|
||||
if (len == 4 && memcmp(p, ip_bytes, 4) == 0) { return true; }
|
||||
// Check if this SAN is an IPv6 address (16 bytes) - skip for now
|
||||
// For an IP host, only a matching iPAddress SAN of the same family
|
||||
// (4 bytes for IPv4, 16 bytes for IPv6) may authenticate it.
|
||||
if (len == ip_len && memcmp(p, ip_bytes, ip_len) == 0) { return true; }
|
||||
} else {
|
||||
// Check if this SAN is a DNS name (printable ASCII string)
|
||||
bool is_dns = len > 0;
|
||||
@@ -18652,21 +18567,25 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
|
||||
san = san->next;
|
||||
}
|
||||
|
||||
// Fallback: Check Common Name (CN) in subject
|
||||
char cn[256];
|
||||
int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject);
|
||||
if (ret > 0) {
|
||||
std::string cn_str(cn);
|
||||
// Fallback: Check Common Name (CN) in subject. Skipped for IP-literal hosts:
|
||||
// an IP identity is only valid via an iPAddress SAN, never the CN (RFC 9110;
|
||||
// the OpenSSL backend's X509_check_ip behaves the same way).
|
||||
if (!is_ip) {
|
||||
char cn[256];
|
||||
int ret = mbedtls_x509_dn_gets(cn, sizeof(cn), &mcert->subject);
|
||||
if (ret > 0) {
|
||||
std::string cn_str(cn);
|
||||
|
||||
// Look for "CN=" in the DN string
|
||||
size_t cn_pos = cn_str.find("CN=");
|
||||
if (cn_pos != std::string::npos) {
|
||||
size_t start = cn_pos + 3;
|
||||
size_t end = cn_str.find(',', start);
|
||||
std::string cn_value =
|
||||
cn_str.substr(start, end == std::string::npos ? end : end - start);
|
||||
// Look for "CN=" in the DN string
|
||||
size_t cn_pos = cn_str.find("CN=");
|
||||
if (cn_pos != std::string::npos) {
|
||||
size_t start = cn_pos + 3;
|
||||
size_t end = cn_str.find(',', start);
|
||||
std::string cn_value =
|
||||
cn_str.substr(start, end == std::string::npos ? end : end - start);
|
||||
|
||||
if (detail::match_hostname(cn_value, host_str)) { return true; }
|
||||
if (detail::match_hostname(cn_value, host_str)) { return true; }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19772,10 +19691,10 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
|
||||
auto x509 = static_cast<WOLFSSL_X509 *>(cert);
|
||||
std::string host_str(hostname);
|
||||
|
||||
// Check if hostname is an IP address
|
||||
bool is_ip = impl::is_ipv4_address(host_str);
|
||||
unsigned char ip_bytes[4];
|
||||
if (is_ip) { impl::parse_ipv4(host_str, ip_bytes); }
|
||||
// Check if hostname is an IP address (IPv4 or IPv6)
|
||||
unsigned char ip_bytes[16];
|
||||
auto ip_len = impl::parse_ip_address(host_str, ip_bytes);
|
||||
auto is_ip = ip_len > 0;
|
||||
|
||||
// Check Subject Alternative Names
|
||||
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) {
|
||||
}
|
||||
}
|
||||
} else if (is_ip && names->type == WOLFSSL_GEN_IPADD) {
|
||||
// IP address
|
||||
// IP address: only an iPAddress SAN of the same family (4 bytes for
|
||||
// IPv4, 16 bytes for IPv6) may authenticate the host.
|
||||
unsigned char *ip_data = wolfSSL_ASN1_STRING_data(names->d.iPAddress);
|
||||
int ip_len = wolfSSL_ASN1_STRING_length(names->d.iPAddress);
|
||||
if (ip_data && ip_len == 4 && memcmp(ip_data, ip_bytes, 4) == 0) {
|
||||
auto san_ip_len = wolfSSL_ASN1_STRING_length(names->d.iPAddress);
|
||||
if (ip_data && san_ip_len == static_cast<int>(ip_len) &&
|
||||
memcmp(ip_data, ip_bytes, ip_len) == 0) {
|
||||
wolfSSL_sk_free(san_names);
|
||||
return true;
|
||||
}
|
||||
@@ -19814,8 +19735,10 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
|
||||
wolfSSL_sk_free(san_names);
|
||||
}
|
||||
|
||||
// Fallback: Check Common Name (CN) in subject
|
||||
WOLFSSL_X509_NAME *subject = wolfSSL_X509_get_subject_name(x509);
|
||||
// Fallback: Check Common Name (CN) in subject. Skipped for IP-literal hosts:
|
||||
// an IP identity is only valid via an iPAddress SAN, never the CN (RFC 9110;
|
||||
// the OpenSSL backend's X509_check_ip behaves the same way).
|
||||
auto subject = is_ip ? nullptr : wolfSSL_X509_get_subject_name(x509);
|
||||
if (subject) {
|
||||
char cn[256] = {};
|
||||
int cn_len = wolfSSL_X509_NAME_get_text_by_NID(subject, NID_commonName, cn,
|
||||
|
||||
Reference in new issue
Block a user