mirror of
https://github.com/yhirose/cpp-httplib.git
synced 2026-10-22 11:46:13 +00:00
use the SAN type tag in Mbed TLS verify_hostname and get_cert_sans (#2614)
* use the SAN type tag in Mbed TLS verify_hostname and get_cert_sans Mbed TLS keeps a subjectAltName entry's GeneralName tag in buf.tag and the bare value in buf.p / buf.len. verify_hostname ignored the tag, so a dNSName whose bytes equal an address authenticated that IP host, and an iPAddress or rfc822Name was matched as a DNS pattern. get_cert_sans looked for the tag inside the value, so it reported no entries for an ordinary certificate, or part of a dNSName as an entry of its own. * Shorten the SAN type comments --------- Co-authored-by: yhirose <yuji.hirose.bug@gmail.com>
This commit is contained in:
1 parent
6d1049462d
commit
edc9760005
5 files changed
+184
-70
No files matched your search
@@ -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 ip_len = impl::parse_ip_address(host_str, ip_bytes);
|
||||||
auto is_ip = ip_len > 0;
|
auto is_ip = ip_len > 0;
|
||||||
|
|
||||||
// Check Subject Alternative Names (SAN)
|
// Check Subject Alternative Names (SAN). Mbed TLS keeps the GeneralName type
|
||||||
// In Mbed TLS 3.x, subject_alt_names contains raw values without ASN.1 tags
|
// in buf.tag and the raw value in buf.p / buf.len.
|
||||||
// - DNS names: raw string bytes
|
|
||||||
// - IP addresses: raw IP bytes (4 for IPv4, 16 for IPv6)
|
|
||||||
const mbedtls_x509_sequence *san = &mcert->subject_alt_names;
|
const mbedtls_x509_sequence *san = &mcert->subject_alt_names;
|
||||||
while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) {
|
while (san != nullptr && san->buf.p != nullptr && san->buf.len > 0) {
|
||||||
const unsigned char *p = san->buf.p;
|
const unsigned char *p = san->buf.p;
|
||||||
size_t len = san->buf.len;
|
size_t len = san->buf.len;
|
||||||
|
auto san_type = san->buf.tag & MBEDTLS_ASN1_TAG_VALUE_MASK;
|
||||||
|
|
||||||
if (is_ip) {
|
if (is_ip) {
|
||||||
// For an IP host, only a matching iPAddress SAN of the same family
|
// For an IP host, only a matching iPAddress SAN of the same family
|
||||||
// (4 bytes for IPv4, 16 bytes for IPv6) may authenticate it.
|
// (4 bytes for IPv4, 16 bytes for IPv6) may authenticate it.
|
||||||
if (len == ip_len && memcmp(p, ip_bytes, ip_len) == 0) { return true; }
|
if (san_type == MBEDTLS_X509_SAN_IP_ADDRESS && len == ip_len &&
|
||||||
} else {
|
memcmp(p, ip_bytes, ip_len) == 0) {
|
||||||
// Check if this SAN is a DNS name (printable ASCII string)
|
return true;
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
|
} 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;
|
san = san->next;
|
||||||
}
|
}
|
||||||
@@ -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;
|
const mbedtls_x509_sequence *cur = &x509->subject_alt_names;
|
||||||
while (cur != nullptr) {
|
while (cur != nullptr) {
|
||||||
if (cur->buf.len > 0) {
|
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;
|
const unsigned char *p = cur->buf.p;
|
||||||
size_t len = cur->buf.len;
|
size_t value_len = cur->buf.len;
|
||||||
|
|
||||||
// First byte is the tag
|
SanEntry entry;
|
||||||
unsigned char tag = *p;
|
switch (cur->buf.tag & MBEDTLS_ASN1_TAG_VALUE_MASK) {
|
||||||
p++;
|
case MBEDTLS_X509_SAN_DNS_NAME:
|
||||||
len--;
|
entry.type = SanType::DNS;
|
||||||
|
entry.value = std::string(reinterpret_cast<const char *>(p), value_len);
|
||||||
// Parse length (simple single-byte length assumed)
|
break;
|
||||||
if (len > 0 && *p < 0x80) {
|
case MBEDTLS_X509_SAN_IP_ADDRESS:
|
||||||
size_t value_len = *p;
|
entry.type = SanType::IP;
|
||||||
p++;
|
if (value_len == 4) {
|
||||||
len--;
|
// IPv4
|
||||||
|
char buf[16];
|
||||||
if (value_len <= len) {
|
snprintf(buf, sizeof(buf), "%d.%d.%d.%d", p[0], p[1], p[2], p[3]);
|
||||||
SanEntry entry;
|
entry.value = buf;
|
||||||
// ASN.1 context tags for GeneralName
|
} else if (value_len == 16) {
|
||||||
switch (tag & 0x1F) {
|
// IPv6
|
||||||
case 2: // dNSName
|
char buf[64];
|
||||||
entry.type = SanType::DNS;
|
snprintf(buf, sizeof(buf),
|
||||||
entry.value =
|
"%02x%02x:%02x%02x:%02x%02x:%02x%02x:"
|
||||||
std::string(reinterpret_cast<const char *>(p), value_len);
|
"%02x%02x:%02x%02x:%02x%02x:%02x%02x",
|
||||||
break;
|
p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8], p[9],
|
||||||
case 7: // iPAddress
|
p[10], p[11], p[12], p[13], p[14], p[15]);
|
||||||
entry.type = SanType::IP;
|
entry.value = buf;
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!entry.value.empty()) { sans.push_back(std::move(entry)); }
|
|
||||||
}
|
}
|
||||||
|
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;
|
cur = cur->next;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,6 +146,13 @@ if(HTTPLIB_IS_USING_OPENSSL)
|
|||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||||
COMMAND_ERROR_IS_FATAL ANY
|
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()
|
endif()
|
||||||
|
|
||||||
add_subdirectory(fuzzing)
|
add_subdirectory(fuzzing)
|
||||||
@@ -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
|
# different address. The SAN address must match; the CN address
|
||||||
# must be ignored.
|
# 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
|
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
|
||||||
+11
-1
@@ -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@']
|
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
|
# Copy test files to the build directory
|
||||||
configure_file(input: 'ca-bundle.crt', output: 'ca-bundle.crt', copy: true)
|
configure_file(input: 'ca-bundle.crt', output: 'ca-bundle.crt', copy: true)
|
||||||
configure_file(input: 'image.jpg', output: 'image.jpg', copy: true)
|
configure_file(input: 'image.jpg', output: 'image.jpg', copy: true)
|
||||||
@@ -178,7 +187,8 @@ test(
|
|||||||
client_encrypted_pbes1_key_pem,
|
client_encrypted_pbes1_key_pem,
|
||||||
client_encrypted_cert_pem,
|
client_encrypted_cert_pem,
|
||||||
cert_ip_cn_pem,
|
cert_ip_cn_pem,
|
||||||
cert_ipv6_pem
|
cert_ipv6_pem,
|
||||||
|
cert_san_types_pem
|
||||||
],
|
],
|
||||||
workdir: meson.current_build_dir(),
|
workdir: meson.current_build_dir(),
|
||||||
timeout: 300
|
timeout: 300
|
||||||
|
|||||||
+118
@@ -42,6 +42,7 @@ inline std::string u8_to_string(const char8_t *s) {
|
|||||||
#define SERVER_CERT2_FILE "./cert2.pem"
|
#define SERVER_CERT2_FILE "./cert2.pem"
|
||||||
#define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.pem"
|
#define SERVER_CERT_IP_CN_FILE "./cert_ip_cn.pem"
|
||||||
#define SERVER_CERT_IPV6_FILE "./cert_ipv6.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 SERVER_PRIVATE_KEY_FILE "./key.pem"
|
||||||
#define CA_CERT_FILE "./ca-bundle.crt"
|
#define CA_CERT_FILE "./ca-bundle.crt"
|
||||||
#define CLIENT_CA_CERT_FILE "./rootCA.cert.pem"
|
#define CLIENT_CA_CERT_FILE "./rootCA.cert.pem"
|
||||||
@@ -14869,6 +14870,123 @@ TEST(SSLClientServerTest, TlsVerifyHostnameIpv6San) {
|
|||||||
EXPECT_FALSE(cn_ipv6_matched)
|
EXPECT_FALSE(cn_ipv6_matched)
|
||||||
<< "An IPv6 host must not be authenticated via the certificate CN";
|
<< "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
|
#endif
|
||||||
|
|
||||||
// mbedTLS-specific callback constructor test
|
// mbedTLS-specific callback constructor test
|
||||||
|
|||||||
Reference in new issue
Block a user