Compare commits

..
6 Commits
Author SHA1 Message Date
yhirose 00afaaed6a Release v0.60.1 2026-10-07 23:40:36 -04:00
yhirose e803f5e413 Let the SSL chain policy alone judge the Windows chain (#2618)
verify_cert_with_windows_schannel() rejected a chain whenever
TrustStatus.dwErrorStatus was non-zero, before
CertVerifyCertificateChainPolicy() ran. The
CERT_CHAIN_POLICY_IGNORE_ALL_REV_UNKNOWN_FLAGS flag passed to that
policy check was therefore dead code: a certificate without revocation
information, or one whose CRL could not be fetched, failed with
CERT_TRUST_REVOCATION_STATUS_UNKNOWN.

Drop the pre-check so the SSL chain policy is the only judge.
Revocation checking becomes best-effort: a revoked certificate and
every other chain error are still rejected, while an undetermined
revocation status is accepted.

On a rejected chain, ssl_backend_error() now holds the policy status,
such as CERT_E_UNTRUSTEDROOT, instead of the trust status bit mask.
2026-10-07 23:19:36 -04:00
Michael Weitzel 57b8aca6da Avoid name clash with class WebSocketClient by explicit qualification (#2616)
Adds an explicit namespace qualification and a forward declaration for
class WebSocketClient. This change avoids a name clash on code bases
containing unrelated WebSocketClient classes.
2026-10-07 22:38:50 -04:00
KBSandyoudie006 213029685a Serve the whole body for a suffix range longer than it (#2615)
RFC 9110 14.1.2: if the representation is shorter than the suffix-length,
the entire representation is used. range_error computed a negative first
byte position for bytes=-8 on a 7-byte body and answered 416. Clamp it
at 0, as #711 did before the range handling was reworked.

Co-authored-by: youdie006 <youdie006@users.noreply.github.com>
2026-10-07 22:20:53 -04:00
yhirose 17eeb18feb Release v0.60.0 2026-10-06 22:57:42 -04:00
metsw24-maxandyhirose edc9760005 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>
2026-10-05 21:13:50 -04:00
7 changed files with 211 additions and 88 deletions

No files matched your search

+1 -1
View File
@@ -230,7 +230,7 @@ cpp-httplib automatically integrates with the OS certificate store on macOS and
| Platform | Behavior | Disable (compile time) |
| :------- | :------- | :--------------------- |
| macOS | Loads system certs from Keychain (link `CoreFoundation` and `Security` with `-framework`). Requires Apple Clang; GCC is not supported for this feature. | `CPPHTTPLIB_DISABLE_MACOSX_AUTOMATIC_ROOT_CERTIFICATES` |
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend, with revocation checking. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
| Windows | Verifies the certificate chain with CryptoAPI (`CertGetCertificateChain` / `CertVerifyCertificateChainPolicy`) instead of the TLS backend. Revocation checking is best-effort: a revoked server certificate is rejected, while one whose revocation status cannot be determined is accepted. Windows fetches missing roots and intermediates on demand. With a custom CA, the TLS backend verifies the chain instead; with `set_server_certificate_verifier()`, both do. | `CPPHTTPLIB_DISABLE_WINDOWS_AUTOMATIC_ROOT_CERTIFICATES_UPDATE` |
On Windows, verification can also be disabled at runtime:
+1 -1
View File
@@ -4,7 +4,7 @@ langs = ["en", "ja"]
[site]
title = "cpp-httplib"
version = "0.59.0"
version = "0.60.1"
hostname = "https://yhirose.github.io"
base_path = "/cpp-httplib"
footer_message = "© 2026 Yuji Hirose. All rights reserved."
+52 -79
View File
@@ -8,8 +8,8 @@
#ifndef CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_HTTPLIB_H
#define CPPHTTPLIB_VERSION "0.59.0"
#define CPPHTTPLIB_VERSION_NUM "0x003b00"
#define CPPHTTPLIB_VERSION "0.60.1"
#define CPPHTTPLIB_VERSION_NUM "0x003c01"
#ifdef _WIN32
#if defined(_WIN32_WINNT) && _WIN32_WINNT < 0x0A00
@@ -4412,6 +4412,8 @@ private:
namespace ws {
class WebSocketClient;
enum class Opcode : uint8_t {
Continuation = 0x0,
Text = 0x1,
@@ -4513,7 +4515,7 @@ public:
private:
friend class httplib::Server;
friend class WebSocketClient;
friend class httplib::ws::WebSocketClient;
WebSocket(
Stream &strm, const Request &req, bool is_server,
@@ -9910,8 +9912,10 @@ inline bool range_error(Request &req, Response &res) {
last_pos = content_len;
}
// RFC 9110 14.1.2: a suffix-length longer than the representation
// selects the entire representation.
if (first_pos == -1) {
first_pos = content_len - last_pos;
first_pos = (std::max)(static_cast<ssize_t>(0), content_len - last_pos);
last_pos = content_len - 1;
}
@@ -10854,12 +10858,6 @@ inline bool verify_cert_with_windows_schannel(
auto chain_guard =
scope_exit([&] { CertFreeCertificateChain(chain_context); });
// Check if chain has errors
if (chain_context->TrustStatus.dwErrorStatus != CERT_TRUST_NO_ERROR) {
out_error = chain_context->TrustStatus.dwErrorStatus;
return false;
}
// Verify SSL policy
SSL_EXTRA_CERT_CHAIN_POLICY_PARA extra_policy_para = {};
extra_policy_para.cbSize = sizeof(extra_policy_para);
@@ -21056,29 +21054,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;
}
@@ -21157,65 +21150,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;
size_t value_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;
}
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;
}
+7
View File
@@ -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
View File
@@ -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
+11 -1
View File
@@ -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)
@@ -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
+135 -6
View File
@@ -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"
@@ -7241,8 +7242,21 @@ TEST_F(ServerTest, GetStreamedWithRangeSuffix1) {
}
TEST_F(ServerTest, GetStreamedWithRangeSuffix2) {
// RFC 9110 14.1.2: a suffix-length longer than the representation selects
// the entire representation.
for (auto range : {"bytes=-8", "bytes=-9999"}) {
auto res = cli_.Get("/streamed-with-range", Headers{{"Range", range}});
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::PartialContent_206, res->status) << range;
EXPECT_EQ("7", res->get_header_value("Content-Length")) << range;
EXPECT_EQ("bytes 0-6/7", res->get_header_value("Content-Range")) << range;
EXPECT_EQ(std::string("abcdefg"), res->body) << range;
}
}
TEST_F(ServerTest, GetStreamedWithRangeSuffixZero) {
auto res =
cli_.Get("/streamed-with-range?error", Headers{{"Range", "bytes=-9999"}});
cli_.Get("/streamed-with-range?error", Headers{{"Range", "bytes=-0"}});
ASSERT_TRUE(res) << "Error: " << to_string(res.error());
EXPECT_EQ(StatusCode::RangeNotSatisfiable_416, res->status);
EXPECT_EQ("0", res->get_header_value("Content-Length"));
@@ -14175,9 +14189,8 @@ TEST(SSLClientTest, WindowsCertificateVerification_ServerIntermediates_Online) {
<< " ssl_backend_error=" << res.ssl_backend_error();
}
// Windows, not the backend, decides on the chain: the error carries a
// CryptoAPI trust status, which no backend error for a self-signed
// certificate has.
// Windows, not the backend, decides on the chain: the error is a CryptoAPI
// policy status, which no backend reports for a self-signed certificate.
TEST(SSLClientTest, WindowsCertificateVerification_RejectsUntrustedRoot) {
SSLServer svr(SERVER_CERT2_FILE, SERVER_PRIVATE_KEY_FILE);
ASSERT_TRUE(svr.is_valid());
@@ -14197,8 +14210,7 @@ TEST(SSLClientTest, WindowsCertificateVerification_RejectsUntrustedRoot) {
auto res = cli.Get("/");
ASSERT_FALSE(res);
EXPECT_EQ(Error::SSLServerVerification, res.error());
EXPECT_NE(0u, res.ssl_backend_error() & CERT_TRUST_IS_UNTRUSTED_ROOT)
<< "ssl_backend_error=" << res.ssl_backend_error();
EXPECT_EQ(static_cast<DWORD>(CERT_E_UNTRUSTEDROOT), res.ssl_backend_error());
}
#endif
@@ -14869,6 +14881,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