Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
113 changes: 44 additions & 69 deletions httplib.h
Original file line number Diff line number Diff line change
Expand Up @@ -21056,29 +21056,24 @@ inline bool verify_hostname(cert_t cert, const char *hostname) {
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
// - DNS names: raw string bytes
// - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6)
// Check Subject Alternative Names (SAN). Mbed TLS keeps the GeneralName type
// in buf.tag and the raw value in buf.p / buf.len.
const mbedtls_x509_sequence *san = &mcert->subject_alt_names;
while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) {
const unsigned char *p = san->buf.p;
size_t len = san->buf.len;
auto san_type = san->buf.tag & MBEDTLS_ASN1_TAG_VALUE_MASK;

if (is_ip) {
// 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;
for (size_t i = 0; i < len && is_dns; i++) {
if (p[i] < 32 || p[i] > 126) { is_dns = false; }
}
if (is_dns) {
std::string san_name(reinterpret_cast<const char *>(p), len);
if (detail::match_hostname(san_name, host_str)) { return true; }
if (san_type == MBEDTLS_X509_SAN_IP_ADDRESS && len == ip_len &&
memcmp(p, ip_bytes, ip_len) == 0) {
return true;
}
} else if (san_type == MBEDTLS_X509_SAN_DNS_NAME) {
std::string san_name(reinterpret_cast<const char *>(p), len);
if (detail::match_hostname(san_name, host_str)) { return true; }
}
san = san->next;
}
Expand Down Expand Up @@ -21157,65 +21152,45 @@ inline bool get_cert_sans(cert_t cert, std::vector<SanEntry> &sans) {
const mbedtls_x509_sequence *cur = &x509->subject_alt_names;
while (cur != nullptr) {
if (cur->buf.len > 0) {
// Mbed TLS stores SAN as ASN.1 sequences
// The tag byte indicates the type
const unsigned char *p = cur->buf.p;
size_t len = cur->buf.len;

// First byte is the tag
unsigned char tag = *p;
p++;
len--;

// Parse length (simple single-byte length assumed)
if (len > 0 && *p < 0x80) {
size_t value_len = *p;
p++;
len--;

if (value_len <= len) {
SanEntry entry;
// ASN.1 context tags for GeneralName
switch (tag & 0x1F) {
case 2: // dNSName
entry.type = SanType::DNS;
entry.value =
std::string(reinterpret_cast<const char *>(p), value_len);
break;
case 7: // iPAddress
entry.type = SanType::IP;
if (value_len == 4) {
// IPv4
char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
entry.value = buf;
} else if (value_len == 16) {
// IPv6
char buf[64];
snprintf(buf, sizeof(buf),
"%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
"%02x%02x:%02x%02x:%02x%02x:%02x%02x",
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8],
p[9], p[10], p[11], p[12], p[13], p[14], p[15]);
entry.value = buf;
}
break;
case 1: // rfc822Name (email)
entry.type = SanType::EMAIL;
entry.value =
std::string(reinterpret_cast<const char *>(p), value_len);
break;
case 6: // uniformResourceIdentifier
entry.type = SanType::URI;
entry.value =
std::string(reinterpret_cast<const char *>(p), value_len);
break;
default: entry.type = SanType::OTHER; break;
}
size_t value_len = cur->buf.len;

if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
SanEntry entry;
switch (cur->buf.tag & MBEDTLS_ASN1_TAG_VALUE_MASK) {
case MBEDTLS_X509_SAN_DNS_NAME:
entry.type = SanType::DNS;
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
break;
case MBEDTLS_X509_SAN_IP_ADDRESS:
entry.type = SanType::IP;
if (value_len == 4) {
// IPv4
char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
entry.value = buf;
} else if (value_len == 16) {
// IPv6
char buf[64];
snprintf(buf, sizeof(buf),
"%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
"%02x%02x:%02x%02x:%02x%02x:%02x%02x",
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9],
p[10], p[11], p[12], p[13], p[14], p[15]);
entry.value = buf;
}
break;
case MBEDTLS_X509_SAN_RFC822_NAME:
entry.type = SanType::EMAIL;
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
break;
case MBEDTLS_X509_SAN_UNIFORM_RESOURCE_IDENTIFIER:
entry.type = SanType::URI;
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
break;
default: entry.type = SanType::OTHER; break;
}

if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
}
cur = cur->next;
}
Expand Down
7 changes: 7 additions & 0 deletions test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,13 @@ if(HTTPLIB_IS_USING_OPENSSL)
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
# 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".
execute_process(
COMMAND ${OPENSSL_COMMAND} 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
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMAND_ERROR_IS_FATAL ANY
)
endif()

add_subdirectory(fuzzing)
4 changes: 4 additions & 0 deletions test/gen-certs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=127.0.0.1" -
# different address. The SAN address must match; the CN address
# must be ignored.
openssl req -x509 -key key.pem -sha256 -days 3650 -nodes -subj "/CN=::1" -addext "subjectAltName=IP:2001:db8::1" -out cert_ipv6.pem

# 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
12 changes: 11 additions & 1 deletion test/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,15 @@ cert_ipv6_pem = custom_target(
command: [openssl, 'req', '-x509', '-key', '@INPUT@', '-sha256', '-days', '3650', '-nodes', '-subj', '/CN=::1', '-addext', 'subjectAltName=IP:2001:db8::1', '-out', '@OUTPUT@']
)

# 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".
cert_san_types_pem = custom_target(
'cert_san_types_pem',
input: key_pem,
output: 'cert_san_types.pem',
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@']
)

# 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)
Expand Down Expand Up @@ -178,7 +187,8 @@ test(
client_encrypted_pbes1_key_pem,
client_encrypted_cert_pem,
cert_ip_cn_pem,
cert_ipv6_pem
cert_ipv6_pem,
cert_san_types_pem
],
workdir: meson.current_build_dir(),
timeout: 300
Expand Down
118 changes: 118 additions & 0 deletions test/test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ inline std::string u8_to_string(const char8_t *s) {
#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_CERT_SAN_TYPES_FILE "./cert_san_types.pem"
#define SERVER_PRIVATE_KEY_FILE "./key.pem"
#define CA_CERT_FILE "./ca-bundle.crt"
#define CLIENT_CA_CERT_FILE "./rootCA.cert.pem"
Expand Down Expand Up @@ -14869,6 +14870,123 @@ TEST(SSLClientServerTest, TlsVerifyHostnameIpv6San) {
EXPECT_FALSE(cn_ipv6_matched)
<< "An IPv6 host must not be authenticated via the certificate CN";
}

// A SAN entry must only match a host of its own type: the bytes of the dNSName
// "a.zz" are also 97.46.122.122, and 42.46.122.122 reads as "*.zz".
TEST(SSLClientServerTest, TlsVerifyHostnameSanType) {
using namespace httplib::tls;

// SANs: DNS:a.zz, IP:42.46.122.122
SSLServer svr(SERVER_CERT_SAN_TYPES_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 dns_san_matched = false;
bool ip_san_matched = false;
bool ip_matched_via_dns_san = true;
bool dns_matched_via_ip_san = 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;

dns_san_matched = ctx.check_hostname("a.zz");
ip_san_matched = ctx.check_hostname("42.46.122.122");

ip_matched_via_dns_san = ctx.check_hostname("97.46.122.122");
dns_matched_via_ip_san = ctx.check_hostname("b.zz");

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(dns_san_matched) << "verify_hostname should match a dNSName SAN";
EXPECT_TRUE(ip_san_matched)
<< "verify_hostname should match an iPAddress SAN";
EXPECT_FALSE(ip_matched_via_dns_san)
<< "An IP host must not be authenticated via a dNSName SAN";
EXPECT_FALSE(dns_matched_via_ip_san)
<< "A DNS host must not be authenticated via an iPAddress SAN";
}

// sans() must report each SAN entry under its own type.
TEST(SSLClientServerTest, TlsCertSansEntryTypes) {
using namespace httplib::tls;

// SANs: DNS:a.zz, IP:42.46.122.122
SSLServer svr(SERVER_CERT_SAN_TYPES_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;
std::vector<SanEntry> sans;

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;

sans = ctx.sans();

return true; // Accept for the purpose of this test
});

cli.Get("/test");

ASSERT_TRUE(verify_callback_called)
<< "Verify callback should have been called";

auto has_san = [&](SanType type, const std::string &value) {
return std::any_of(sans.begin(), sans.end(), [&](const SanEntry &san) {
return san.type == type && san.value == value;
});
};

EXPECT_TRUE(has_san(SanType::DNS, "a.zz"))
<< "sans() should report the dNSName SAN";
EXPECT_TRUE(has_san(SanType::IP, "42.46.122.122"))
<< "sans() should report the iPAddress SAN";

EXPECT_FALSE(has_san(SanType::IP, "97.46.122.122"))
<< "sans() must not report the dNSName SAN as an address";
EXPECT_FALSE(has_san(SanType::DNS, "*.zz"))
<< "sans() must not report the iPAddress SAN as a DNS name";
}
#endif

// mbedTLS-specific callback constructor test
Expand Down
Loading