fix(tls): refuse a server certificate that does not verify - #21
Merged
Merged
Conversation
With verifySsl = true the TLS config used MBEDTLS_SSL_VERIFY_OPTIONAL, which lets the handshake finish whatever the certificate looks like, and nothing read mbedtls_ssl_get_verify_result afterwards. A self-signed certificate, one for another host and an expired one were all accepted. Use MBEDTLS_SSL_VERIFY_REQUIRED. A failed handshake now says why in the response's statusText: the text from mbedtls_x509_crt_verify_info for a verification failure, the mbedTLS error text for any other handshake error. A refused TCP connection still reads "Connection failed". When no CA bundle can be found and verifySsl is true, the connection now fails with a message that points at SSL_CERT_FILE, instead of going ahead without verification. Tests serve self-signed, expired and wrong-name certificates from the existing in-process server and trust them through SSL_CERT_FILE, so they need no network. On Windows, where there is no bundle file, load_ca_certs reads the roots in the system ROOT certificate store that are usable for TLS servers (serverAuth) when SSL_CERT_FILE is not set. That part is built only where platform::uses_winsock is true, and has not been run on Windows.
…re is built and run Three additions on top of the contributor's fix, each a consequence of it. `examples/openkal` read every status 0 as "no network" and exited 0. With verification now enforced, a CA bundle the openkal stack cannot find would have passed there as a skipped run. A statusText that names a refused certificate, a missing or unparsable bundle, or a failed handshake now exits 1. The README states the two differences a build above openkal has: on Windows the socket interface is POSIX, so the ROOT store is not read and SSL_CERT_FILE is required; and connectTimeoutMs does not bound the TCP connect, because kal_net_connect has no deferred form. The Windows ROOT store reader is compiled on no platform CI covered. A windows-2022 job (msvc and llvm) builds the library and runs test_ca_store, which asserts that the store is found with nothing configured and that a public chain verifies against it. An unreachable host is a skip; a refusal is a failure. Co-authored-by: speak-agent <[email protected]>
The msvc row of the new Windows job stopped at the link:
entropy_poll.obj : error LNK2019: unresolved external symbol
BCryptGenRandom referenced in function mbedtls_platform_entropy_poll
mbedTLS's package names the library as `-lbcrypt`, which clang reads and
link.exe ignores. The llvm row, the same sources under the same ABI, linked and
verified a public chain against the ROOT store. The request is now also made
with `#pragma comment(lib, ...)`, as socket.cppm makes it for ws2_32 and
ca_bundle.cppm for crypt32.
Co-authored-by: speak-agent <[email protected]>
Sunrisepeak
added a commit
to yspbwx2010/tinyhttps
that referenced
this pull request
Oct 1, 2026
…ranch The two changes meet in TlsSocket and in HttpClient::open_connection, and the conflicts are resolved here without changing what either side meant: * the move constructor and move assignment carry both the lower session (`lower_`, this branch) and the failure reason (`error_`, mcpplibs#21); * the failure paths of setup_tls take mcpplibs#21's form, `return fail(reason)`; * a connection that could not be opened reports the proxy's refusal when there is one, else the TLS session's reason, else `Connection failed`; * CHANGELOG keeps both entries under Unreleased. What the combination needs beyond resolving the text follows in the next commit. Co-authored-by: speak-agent <[email protected]>
Sunrisepeak
added a commit
to yspbwx2010/tinyhttps
that referenced
this pull request
Oct 1, 2026
… it was refused What the certificate fix and the proxy branch need of each other beyond the text the merge resolved: * TlsSocket::fail() closes the transport with drop_transport(), so a handshake that fails inside a tunnel closes the session to the proxy as well, not only the descriptor a tunnelled session does not use. * connect_over(lower) clears the previous failure reason, as connect() and connect_over(Socket&&) do. * A TLS connection to an https:// proxy that fails reports the session's reason after the hop, `proxy: could not open a TLS connection to <proxy>: certificate verification failed: ...`. Without it the refusal mcpplibs#21 exists to explain reached the caller with the reason dropped. test_proxy gains the case: an https proxy whose certificate has no trusted issuer is refused with the reason, and receives no CONNECT. It fails without the proxy.cppm change. The version is 0.3.3 rather than 0.4.0. mcpp reads `tinyhttps = "0.3.0"` as ^0.3.0, which stops below 0.4.0, so only a patch release reaches the consumers that need the verification fix without editing a manifest. CHANGELOG states the fail-closed behaviour change first. Co-authored-by: speak-agent <[email protected]>
Sunrisepeak
added a commit
that referenced
this pull request
Oct 1, 2026
Proxy authentication, https:// proxies and SOCKS5; contributor commits by Cloud_Yun. Maintainer commits merge master (#21) into the branch, verify the proxy hop and report its refusal reason, and set the version to 0.3.3. Co-authored-by: speak-agent <[email protected]>
Member
|
Thank you for the precise report and the fix, merged with your commit unchanged. The maintainer commits on top add: a windows-2022 CI job (msvc and llvm) that runs the Windows ROOT store reader and verifies a public chain against it (it passed on both toolchains); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #20
With
verifySsl = truethe handshake accepted any server certificate, becausesetup_tlsusedMBEDTLS_SSL_VERIFY_OPTIONALand nothing read the verification result afterwards. This switches toMBEDTLS_SSL_VERIFY_REQUIRED, so the chain, the validity dates and the hostname are all checked by mbedTLS during the handshake, and makes the failure say why.A failed handshake used to surface only as
Connection failed.TlsSocketnow keeps the reason (error()), andperform_exchangereturns it in place of the generic text when there is one: the text frommbedtls_x509_crt_verify_infofor a verification failure, the mbedTLS error text for any other handshake error, and a message for a CA bundle that cannot be parsed. A refused TCP connection still readsConnection failed. The comment intls.cppmsaying callers could inspect the result after the handshake is removed, since there was never a way to do that.One behaviour change: when
verifySslis true andload_ca_certsfinds no bundle (SSL_CERT_FILEunset or unreadable and none of the system locations present), the connection now fails instead of going ahead unverified. The message says to setSSL_CERT_FILEorverifySsl = false. I think failing is right, and curl does the same, but it is the part I would most like a second opinion on.It matters most on Windows, where there is no bundle file, so this also reads the system
ROOTcertificate store there:load_ca_certscallsCertOpenSystemStoreW/CertEnumCertificatesInStore(crypt32, linked with#pragma comment, assocket.cppmdoes for ws2_32) whenSSL_CERT_FILEis not set, and returns the certificates as PEM, so the rest is unchanged. Roots whose enhanced key usage does not include serverAuth are skipped, since Windows keeps roots it no longer trusts for TLS servers in that store behind such a restriction. Two limits: Windows downloads some roots on demand, so a root a browser would fetch may be missing from the store and the connection is refused until it is there orSSL_CERT_FILEis set; and a MinGW build has to add-lcrypt32itself (as it already adds-lws2_32), because clang ignores#pragma comment(lib, ...)there. It is guarded byplatform::uses_winsockas inplatform.cppm, so the Windows build that goes through the POSIX socket interface (openkal) does not read the store and still needsSSL_CERT_FILE. I could not build this part as a whole or run it: there is no Windows toolchain on my machine and CI here only covers Ubuntu. All I could do was compile the store-reading function on its own forx86_64-w64-windows-gnuagainst Wine's Windows headers, with a stand-in forstd::string, to check the API calls; the module itself has not been built for Windows and the function has not been run on Windows. Please treat that function as untested until someone with a Windows machine has tried it.verifySsl = falseis unchanged, and so is the CA loading order.I checked it against the four certificates from the issue (
openssl s_serveron 127.0.0.1, the issue's client,SSL_CERT_FILEpointing at the test CA):With
SSL_CERT_FILEunset, so only the system bundle, all four fail with a verification error. With/etc/ssland/etc/pkihidden and noSSL_CERT_FILE:and a port with nothing listening still gives
Connection failed.Tests are in
tests/test_tls_verify.cpp. They serve a self-signed, an expired and a wrong-name certificate from the existing in-process server intests/tls_test_server.hpp(it now takes an optional certificate and key, and carries two more test pairs) and trust them throughSSL_CERT_FILEpointing at a temporary file, so they use no network and not the machine's CA store. They cover a trusted certificate being accepted, an untrusted issuer, an expired certificate, a wrong hostname and an unparsable bundle being refused with the reason instatusText, andverifySsl = falsestill connecting. All six pass with the change; against the unpatched sources four of them fail. Not covered: the missing-bundle case, becauseload_ca_certsfalls back to the system locations and the test machine usually has one; I checked it by hiding them as above.examples/openkaltreats any status 0 as "no network" and exits 0, so a certificate failure there would read as a skipped run; I left it alone.test_poolandtest_framingpass unchanged on my machine. I ran them with a small stand-in for the gtest macros, becausecompat.gtestwas not available offline here, so a run in CI is still worth waiting for. I did not bump the version;CHANGELOG.mdhas an Unreleased entry and the README'sverifySslrow mentions the bundle and the Windows store.