From 35863cb11d0cf15f3002d7b12857b721fa9753e6 Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Tue, 13 Oct 2026 13:47:49 -0600 Subject: [PATCH] fix(tls): trust every certificate in a CA bundle The OpenSSL engine read only the first certificate of each trust-anchor entry, so load_verify_file() on a multi-certificate bundle silently trusted one root. Read until the PEM data is exhausted, as Asio does, and fail setup on any certificate that does not parse. WolfSSL already loads every certificate it can use from a bundle and fails only when none load. Keep that: WolfSSL rejects roots that system bundles ship (expired ones under peer verification, serial number 0 on some builds), and failing on those would reject the whole store. An empty buffer returned 0 rather than an error code, which let empty CA data pass setup; map it to an error. Correct the TLS documentation that contradicted the implementation: - drop the HTTPS example warning that set_default_verify_paths() loads nothing, the tutorial note that it contributes nothing, and both first-certificate notes - state that hostname verification runs only under the peer or require_peer verify mode, while an IP literal on a WolfSSL build without IP-literal support fails under every mode - on WolfSSL without HAVE_CRL, only a non-disabled revocation policy fails the handshake; a CRL under the disabled policy is ignored - list every WolfSSL build-dependent feature with its configure switch - describe per backend what set_default_verify_paths() loads and how a CA bundle with a bad certificate is handled - setters report only files that cannot be opened and malformed ALPN names; most settings are captured at the first handshake, while callbacks are read later - drop backends that do not exist (mbedTLS, Schannel) - split the protocol_versions snippet, whose inverted version window could never handshake, into two contexts Closes #350 --- .../pages/3.tutorials/3d.tls-context.adoc | 83 +++++++++++-------- doc/modules/ROOT/pages/4.guide/4l.tls.adoc | 80 +++++++++++------- include/boost/corosio/tls_context.hpp | 49 ++++++----- include/boost/corosio/tls_stream.hpp | 7 +- include/boost/corosio/wolfssl_stream.hpp | 6 +- src/openssl/src/detail/engine.cpp | 18 ++-- src/wolfssl/src/detail/engine.cpp | 10 ++- test/doc/snippets/4l_tls.cpp | 13 +-- test/unit/openssl_engine.cpp | 39 +++++++++ test/unit/openssl_stream.cpp | 1 + test/unit/tls_stream_tests.hpp | 33 ++++++++ test/unit/wolfssl_engine.cpp | 37 +++++++++ test/unit/wolfssl_stream.cpp | 1 + 13 files changed, 267 insertions(+), 110 deletions(-) diff --git a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc index 2a5260b6e..dd1ee8d6e 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc @@ -27,7 +27,7 @@ include::example$snippets/3d_tls_context.cpp[tag=assume] == Introduction The cpp:tls_context[] class is a portable abstraction for TLS configuration that -works across multiple TLS backends (WolfSSL, OpenSSL, mbedTLS, etc.). It +works across the TLS backends (OpenSSL and WolfSSL). It handles: * Loading certificates and private keys @@ -41,12 +41,20 @@ for establishing secure connections. [NOTE] ==== -Two implemented features depend on how WolfSSL was built. -`set_default_verify_paths()` needs `WOLFSSL_SYS_CA_CERTS` to load the system -store. `set_verify_callback()` needs `WOLFSSL_ALWAYS_VERIFY_CB`; without it, -installing a callback fails the handshake with -`std::errc::function_not_supported` rather than failing open. Both are -enabled by `--enable-opensslextra`. +On WolfSSL, some features depend on how the library was built: + +* `set_default_verify_paths()` needs `WOLFSSL_SYS_CA_CERTS` + (`--enable-sys-ca-certs`, on by default) to load the system store. +* `set_verify_callback()` needs `WOLFSSL_ALWAYS_VERIFY_CB`, which + `--enable-opensslextra` defines. +* `set_alpn()` needs `HAVE_ALPN` (`--enable-alpn`). +* CRL checking needs `HAVE_CRL` (`--enable-crl`). +* Verifying an IP literal passed to `tls_stream::set_hostname()` needs + both `OPENSSL_EXTRA` and `WOLFSSL_IP_ALT_NAME` + (`--enable-opensslextra --enable-ip-alt-name`). + +Except for the system store, a missing feature fails the handshake with +`std::errc::function_not_supported` rather than failing open. ==== == Construction @@ -55,10 +63,11 @@ A cpp:tls_context[] is a shared handle to an opaque implementation. Copies share the same underlying state, making it easy to pass contexts by value and share them across multiple TLS streams. -IMPORTANT: Don't modify a context after creating a stream from it. The -configuration is captured when the first stream is constructed, so later -changes reach neither existing nor new streams. If you need a different -configuration, create a separate context. +IMPORTANT: Don't modify a context after creating a stream from it. Most +settings are captured at the first handshake of any stream using it; +others, such as callbacks, are read later. A change may therefore apply +partly or not at all. If you need a different configuration, create a +separate context. [source,cpp] ---- @@ -111,7 +120,7 @@ into a single password-protected file: include::example$snippets/3d_tls_context.cpp[tag=pkcs12_file,indent=0] ---- -NOTE: The bundle is decoded when the first stream is created; a wrong +NOTE: The bundle is decoded at the first handshake; a wrong passphrase or malformed bundle surfaces as a handshake failure. Intermediate certificates in the bundle are loaded and sent during the handshake on both backends. @@ -154,16 +163,20 @@ For HTTPS clients connecting to public servers, use the system's CA store: include::example$snippets/3d_tls_context.cpp[tag=system_trust,indent=0] ---- -This uses the operating system's trusted certificates: +What "system store" means depends on the backend: -* Linux: `/etc/ssl/certs` or distribution-specific paths -* macOS: System Keychain -* Windows: Windows Certificate Store - -On OpenSSL the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are -honored. On WolfSSL this requires a build with `WOLFSSL_SYS_CA_CERTS`; -without it, supply trust anchors explicitly with `load_verify_file()` or -`add_certificate_authority()` (below). +* OpenSSL: the CA file and directory OpenSSL was built with, under its + `OPENSSLDIR` (typically `/etc/ssl` on Linux). The `SSL_CERT_FILE` and + `SSL_CERT_DIR` environment variables override them. OpenSSL does not + read the macOS Keychain or the Windows Certificate Store. On those + platforms, supply trust anchors with `load_verify_file()` unless that + OpenSSL installation ships its own CA bundle. +* WolfSSL: on Linux it reads the first readable CA directory among + `/etc/ssl/certs` and the `/etc/pki` locations. On macOS it reads the + user, admin, and system trust settings. On Windows it reads the Root and + CA stores. This requires a build with `WOLFSSL_SYS_CA_CERTS`. Without it, supply + trust anchors explicitly with `load_verify_file()` or + `add_certificate_authority()` (below). === Custom CA Bundle @@ -174,10 +187,6 @@ For internal PKI or testing, load a custom CA bundle: include::example$snippets/3d_tls_context.cpp[tag=ca_bundle,indent=0] ---- -NOTE: With OpenSSL, `load_verify_file()` registers only the *first* -certificate from a multi-cert bundle (WolfSSL handles multi-cert files); -add the rest with repeated `add_certificate_authority()` calls. - === CA Directory Add a directory of CA certificates: @@ -210,9 +219,6 @@ You can combine multiple trust sources: include::example$snippets/3d_tls_context.cpp[tag=combine_trust,indent=0] ---- -NOTE: The `set_default_verify_paths()` call above contributes nothing in -this release. Only the explicitly added CA is actually trusted. - == Protocol Configuration Control which TLS versions and cipher suites are allowed for connections. @@ -300,6 +306,10 @@ This enables: present (required for virtual hosting) * **Hostname matching**: Validates the certificate matches the expected host +Hostname matching applies only when the verify mode is `peer` or +`require_peer`. Under the default `none`, a certificate for a different +host is accepted. + === Chain Depth Limit how many intermediate certificates are allowed: @@ -369,8 +379,9 @@ include::example$snippets/3d_tls_context.cpp[tag=crl_load,indent=0] CRLs must be refreshed periodically as they expire. NOTE: On WolfSSL, CRL checking requires a `HAVE_CRL` build; without it, -supplying a CRL or a non-disabled policy fails the handshake with -`std::errc::function_not_supported`. +a revocation policy other than `disabled` fails the handshake with +`std::errc::function_not_supported`. A CRL supplied while the policy +stays `disabled` is ignored. === Bootstrap vs. Hardened Connections @@ -483,12 +494,12 @@ to detect errors: include::example$snippets/3d_tls_context.cpp[tag=error_handling,indent=0] ---- -Common errors include: - -* File not found or permission denied -* Invalid certificate or key format -* Key doesn't match certificate -* Wrong passphrase for encrypted key or PKCS#12 +The setters only record configuration, so few of them can fail. Those +that read a file report one that cannot be opened, and `set_alpn()` +rejects a malformed protocol name. Content is decoded when the native +context is first built. An invalid certificate or key format, a key that +doesn't match its certificate, or a wrong passphrase therefore surfaces +as a handshake failure. == Next Steps diff --git a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc index 7a86cd6ec..dd458b2ad 100644 --- a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc +++ b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc @@ -57,13 +57,21 @@ revocation (`add_crl()` / `add_crl_file()` with [NOTE] ==== -Two implemented features depend on how WolfSSL was built. -`set_default_verify_paths()` requires `WOLFSSL_SYS_CA_CERTS` to load the -system store; without it, supply trust anchors explicitly. -`set_verify_callback()` requires `WOLFSSL_ALWAYS_VERIFY_CB` — without it, -installing a callback fails the handshake with +On WolfSSL, some features depend on how the library was built: + +* `set_default_verify_paths()` needs `WOLFSSL_SYS_CA_CERTS` + (`--enable-sys-ca-certs`, on by default) to load the system store; + without it, supply trust anchors explicitly. +* `set_verify_callback()` needs `WOLFSSL_ALWAYS_VERIFY_CB`, which + `--enable-opensslextra` defines. +* `set_alpn()` needs `HAVE_ALPN` (`--enable-alpn`). +* CRL checking needs `HAVE_CRL` (`--enable-crl`). +* Verifying an IP literal passed to `set_hostname()` needs both + `OPENSSL_EXTRA` and `WOLFSSL_IP_ALT_NAME` + (`--enable-opensslextra --enable-ip-alt-name`). + +Except for the system store, a missing feature fails the handshake with `std::errc::function_not_supported` rather than failing open (see below). -Both are enabled by `--enable-opensslextra`. ==== The typical flow: @@ -79,8 +87,10 @@ The cpp:tls_context[] class stores TLS configuration: certificates, keys, trust anchors, protocol settings, and verification options. Contexts are shared handles—copies share the same underlying state. -IMPORTANT: Don't modify a context after creating streams from it. The -configuration is captured when the first stream is constructed. +IMPORTANT: Don't modify a context after creating streams from it. Most +settings are captured at the first handshake of any stream using it; +others, such as callbacks, are read later. A change may therefore apply +partly or not at all. === Creating a Context @@ -127,7 +137,7 @@ PKCS#12 (`.pfx` or `.p12`) files bundle certificate, key, and chain together: include::example$snippets/4l_tls.cpp[tag=pkcs12,indent=0] ---- -NOTE: The bundle is decoded when the first stream is created; a malformed +NOTE: The bundle is decoded at the first handshake; a malformed bundle or wrong passphrase surfaces as a handshake failure. Intermediate certificates in the bundle are loaded and sent during the handshake on both backends. @@ -138,17 +148,22 @@ Configure which Certificate Authorities (CAs) to trust for peer verification. ==== System Trust Store -Use the operating system's default CA certificates: +Use the system's default CA certificates: [source,cpp] ---- include::example$snippets/4l_tls.cpp[tag=system_trust,indent=0] ---- -On OpenSSL the `SSL_CERT_FILE` and `SSL_CERT_DIR` environment variables are -honored. The WolfSSL backend requires a build with `WOLFSSL_SYS_CA_CERTS`. -Without it this call has no effect, and a CA bundle must be supplied -explicitly via `load_verify_file()` or `add_certificate_authority()`. +What this loads depends on the backend. OpenSSL reads the CA file and +directory it was built with, or those named by the `SSL_CERT_FILE` and +`SSL_CERT_DIR` environment variables. It does not read the macOS Keychain +or the Windows Certificate Store. WolfSSL reads the platform store: +`/etc/ssl/certs` and similar on Linux, the trust settings on macOS, and +the Root and CA stores on Windows. It requires a build with +`WOLFSSL_SYS_CA_CERTS`. Where the system store is unavailable, this call +has no effect, and a CA bundle must be supplied explicitly via +`load_verify_file()` or `add_certificate_authority()`. ==== Custom CA Certificates @@ -157,11 +172,11 @@ explicitly via `load_verify_file()` or `add_certificate_authority()`. include::example$snippets/4l_tls.cpp[tag=custom_cas,indent=0] ---- -NOTE: With OpenSSL, `load_verify_file()` registers only the *first* -certificate from a multi-cert bundle (WolfSSL handles multi-cert files); -add others with repeated `add_certificate_authority()` calls. For -`add_verify_path()`, OpenSSL requires the hashed-filename layout produced -by `openssl rehash`, while WolfSSL loads every file in the directory. +NOTE: Every valid certificate in a bundle is trusted. If one fails to +load, OpenSSL fails the handshake, while WolfSSL skips that certificate +and fails the handshake only when none load. For `add_verify_path()`, OpenSSL requires the +hashed-filename layout produced by `openssl rehash`, while WolfSSL loads +every file in the directory. === Protocol Configuration @@ -245,10 +260,19 @@ This does two things: to present (important for virtual hosting) 2. Verifies the server certificate matches this hostname +The hostname is checked only when the verify mode is `peer` or +`require_peer`. Under the default `none`, a certificate for a different +host is accepted. + An IP literal (IPv4 or IPv6) is verified against the certificate's iPAddress entries instead of its DNS names. No SNI is sent, because RFC 6066 excludes literals. +NOTE: On WolfSSL, verifying an IP literal requires a build with both +`OPENSSL_EXTRA` and `WOLFSSL_IP_ALT_NAME`. Without them, setting an IP +literal fails the handshake with `std::errc::function_not_supported` +under every verify mode, including `none`. + ==== Verification Depth Limit the certificate chain depth: @@ -305,8 +329,9 @@ revoked is rejected. Under `hard_fail`, an undeterminable status is also rejected. `disabled` (the default) skips revocation entirely. NOTE: On WolfSSL, CRL checking requires a `HAVE_CRL` build. Without it, -supplying a CRL or a non-disabled policy fails the handshake with +a revocation policy other than `disabled` fails the handshake with `std::errc::function_not_supported` rather than skip the check silently. +A CRL supplied while the policy stays `disabled` is ignored. == TLS Streams @@ -442,18 +467,9 @@ include::example$snippets/4l_tls.cpp[tag=stream_overloads,indent=0] Complete example connecting to an HTTPS server: -[WARNING] -==== -This example uses `set_default_verify_paths()`, which does *not* currently -load any trust anchors, so it verifies nothing against a real public server. -To verify the server today, replace that call with an explicit CA bundle, -for example: - -[source,cpp] ----- -include::example$snippets/4l_tls.cpp[tag=ca_bundle,indent=0] ----- -==== +NOTE: On WolfSSL builds without `WOLFSSL_SYS_CA_CERTS`, +`set_default_verify_paths()` cannot load the system store; supply the +server's CA explicitly with `load_verify_file()` instead. [source,cpp] ---- diff --git a/include/boost/corosio/tls_context.hpp b/include/boost/corosio/tls_context.hpp index b7b3fdb10..cd9e35864 100644 --- a/include/boost/corosio/tls_context.hpp +++ b/include/boost/corosio/tls_context.hpp @@ -210,18 +210,17 @@ tls_context_data const& get_tls_context_data(tls_context const&) noexcept; share the same underlying state. This allows contexts to be passed by value and shared across multiple TLS streams. - This class abstracts the configuration phase of TLS across multiple - backend implementations, among them OpenSSL, WolfSSL, mbedTLS and - Schannel. Portable code therefore works regardless of which TLS - library is linked. + This class abstracts the configuration phase of TLS across the + OpenSSL and WolfSSL backends. Portable code therefore works + regardless of which TLS library is linked. @par Modification After Stream Creation Modifying a context after creating a TLS stream from it - results in undefined behavior. The context's configuration is - captured when the first stream is constructed, and subsequent - modifications are not reflected in existing or new streams - sharing the context. + results in undefined behavior. Most settings are captured at the + first handshake of any stream using it; others, such as callbacks, + are read later. A modification may therefore apply partly or not at + all. If different configurations are needed, create separate context objects. @@ -492,15 +491,17 @@ class BOOST_COROSIO_DECL tls_context /** Add a certificate authority for peer verification. - Adds a single CA certificate to the trust store used for verifying - peer certificates. Call this multiple times to add multiple CAs, - or use `load_verify_file()` for a bundle. + Adds one or more CA certificates to the trust store used for + verifying peer certificates. The data may contain multiple + concatenated PEM certificates. @param ca The CA certificate data in PEM format. - @return Success. The certificate is recorded and decoded when the - native context is first built; a malformed certificate - surfaces as a handshake failure. + @return Success. The certificates are recorded and decoded when + the native context is first built. Whether a malformed + certificate fails the handshake depends on the backend. On + OpenSSL, any one does. WolfSSL skips certificates it cannot + use and fails only when none load. @see load_verify_file @see set_default_verify_paths @@ -517,7 +518,8 @@ class BOOST_COROSIO_DECL tls_context @return Success, or an error if the file could not be read. The certificates are decoded when the native context is first - built; malformed certificates surface as a handshake failure. + built. Malformed certificates are handled as described for + `add_certificate_authority()`. @par Example @par !example load_verify_file @@ -555,8 +557,8 @@ class BOOST_COROSIO_DECL tls_context /** Use the system default CA certificate store. - Configures the context to use the operating system's default - trust store for peer certificate verification. This is the + Configures the context to use the backend's default trust store + for peer certificate verification. This is the recommended approach for HTTPS clients connecting to public servers. @@ -571,10 +573,11 @@ class BOOST_COROSIO_DECL tls_context must reject unverified peers should therefore also use `set_verify_mode( tls_verify_mode::peer )`. - @note The OpenSSL backend honors the `SSL_CERT_FILE` and - `SSL_CERT_DIR` environment variables. The WolfSSL backend - requires a build with `WOLFSSL_SYS_CA_CERTS`; without it the - system store is unavailable and this call has no effect. + @note OpenSSL reads the CA file and directory it was built with, + or those named by `SSL_CERT_FILE` and `SSL_CERT_DIR`. It does + not read the macOS Keychain or the Windows Certificate Store. + WolfSSL reads the platform store and requires a build with + `WOLFSSL_SYS_CA_CERTS`; without it this call has no effect. @par Example @par !example set_default_verify_paths @@ -836,8 +839,8 @@ class BOOST_COROSIO_DECL tls_context @note CRLs are consulted only when a revocation policy is set via @ref set_revocation_policy. On WolfSSL, CRL checking requires a - build with `HAVE_CRL`; without it, supplying a CRL or a - revocation policy fails the handshake with + build with `HAVE_CRL`; without it, a revocation policy + other than `disabled` fails the handshake with `std::errc::function_not_supported`. @see add_crl_file diff --git a/include/boost/corosio/tls_stream.hpp b/include/boost/corosio/tls_stream.hpp index ee1df6a60..50ee9cda4 100644 --- a/include/boost/corosio/tls_stream.hpp +++ b/include/boost/corosio/tls_stream.hpp @@ -195,9 +195,10 @@ class BOOST_COROSIO_DECL tls_stream Configures the hostname sent in the TLS Server Name Indication extension and matched against the peer - certificate during verification. The value takes effect - at the next `handshake()`; an established session is not - affected. It persists across `reset()`, so a stream reused + certificate during verification. Matching happens only when + the context's verify mode is not `tls_verify_mode::none`. + The value takes effect at the next `handshake()`; an + established session is not affected. It persists across `reset()`, so a stream reused to reach a different host must set the new name before handshaking again. diff --git a/include/boost/corosio/wolfssl_stream.hpp b/include/boost/corosio/wolfssl_stream.hpp index f0196ff4d..b2450025b 100644 --- a/include/boost/corosio/wolfssl_stream.hpp +++ b/include/boost/corosio/wolfssl_stream.hpp @@ -266,9 +266,9 @@ BOOST_COROSIO_DECL bool wolfssl_supports_alpn() noexcept; /** Report whether this WolfSSL build can check certificate revocation. CRL checking requires a WolfSSL built with `HAVE_CRL`. On a build - without it, supplying a CRL or a revocation policy fails the handshake - with `std::errc::function_not_supported` rather than silently skipping - revocation. + without it, a revocation policy other than `disabled` fails the + handshake with `std::errc::function_not_supported` rather than + silently skipping revocation. @return `true` if revocation checking is supported by this build. diff --git a/src/openssl/src/detail/engine.cpp b/src/openssl/src/detail/engine.cpp index ea84d58b4..f95f50184 100644 --- a/src/openssl/src/detail/engine.cpp +++ b/src/openssl/src/detail/engine.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -548,24 +549,31 @@ class openssl_native_context : public native_context_base // silently: the store would verify against fewer anchors than // requested and reject a legitimate peer as untrusted. Fail // closed instead, tolerating only a duplicate the store already - // holds. + // holds. An entry may hold a bundle; it ends cleanly only when + // no further PEM block follows at least one certificate. BIO* bio = BIO_new_mem_buf(ca.data(), static_cast(ca.size())); if (!bio) { setup_failed_ = true; continue; } - X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); - if (cert) + for (bool added = false;; added = true) { + X509* cert = PEM_read_bio_X509(bio, nullptr, nullptr, nullptr); + if (!cert) + { + unsigned long const err = ERR_peek_last_error(); + if (!added || ERR_GET_LIB(err) != ERR_LIB_PEM || + ERR_GET_REASON(err) != PEM_R_NO_START_LINE) + setup_failed_ = true; + break; + } if (X509_STORE_add_cert(store, cert) != 1 && ERR_GET_REASON(ERR_peek_last_error()) != X509_R_CERT_ALREADY_IN_HASH_TABLE) setup_failed_ = true; X509_free(cert); } - else - setup_failed_ = true; ERR_clear_error(); BIO_free(bio); } diff --git a/src/wolfssl/src/detail/engine.cpp b/src/wolfssl/src/detail/engine.cpp index 4b78f328b..287f72707 100644 --- a/src/wolfssl/src/detail/engine.cpp +++ b/src/wolfssl/src/detail/engine.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -484,12 +485,17 @@ class wolfssl_native_context : public native_context_base // A trust anchor that fails to load must not pass silently: the // context would then verify against an empty store and every // handshake would fail ASN_NO_SIGNER_E. Fail closed and keep the - // library's reason so init reports it directly. + // library's reason so init reports it directly. A bundle fails + // only when none of its certificates load: WolfSSL rejects + // roots that system bundles ship (expired ones under peer + // verification, serial number 0 on some builds), and failing + // on those would reject the whole store. An empty buffer + // returns 0 rather than an error code. int const ret = wolfSSL_CTX_load_verify_buffer( ctx, reinterpret_cast(ca.data()), static_cast(ca.size()), WOLFSSL_FILETYPE_PEM); if (ret != WOLFSSL_SUCCESS) - setup_error_ = setup_error_ ? setup_error_ : ret; + setup_error_ = setup_error_ ? setup_error_ : (ret ? ret : 1); } // Trust anchors from the system store and explicit directories. diff --git a/test/doc/snippets/4l_tls.cpp b/test/doc/snippets/4l_tls.cpp index 17cb634a4..452ba6edc 100644 --- a/test/doc/snippets/4l_tls.cpp +++ b/test/doc/snippets/4l_tls.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -518,9 +519,7 @@ struct tls_test ctx.add_certificate_authority(ca_pem); // CA file (may contain multiple certs) - // tag::ca_bundle[] ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt"); - // end::ca_bundle[] // Directory of hashed CA files ctx.add_verify_path("/etc/ssl/certs"); @@ -530,13 +529,15 @@ struct tls_test void testProtocolVersions() { - corosio::tls_context ctx; // tag::protocol_versions[] // Require TLS 1.3 minimum - ctx.set_min_protocol_version(corosio::tls_version::tls_1_3); + corosio::tls_context modern; + modern.set_min_protocol_version(corosio::tls_version::tls_1_3); - // Cap at TLS 1.2 (unusual, but possible) - ctx.set_max_protocol_version(corosio::tls_version::tls_1_2); + // Pin TLS 1.2 for a legacy peer (unusual, but possible) + corosio::tls_context legacy; + legacy.set_min_protocol_version(corosio::tls_version::tls_1_2); + legacy.set_max_protocol_version(corosio::tls_version::tls_1_2); // end::protocol_versions[] BOOST_TEST(true); } diff --git a/test/unit/openssl_engine.cpp b/test/unit/openssl_engine.cpp index 208488629..8a6841110 100644 --- a/test/unit/openssl_engine.cpp +++ b/test/unit/openssl_engine.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -532,6 +533,9 @@ struct openssl_engine_test testInvertedVersionWindowFailsSetup(); testGarbagePrivateKeyFailsSetup(); testGarbageCaFailsSetup(); + testNonPemCaFailsSetup(); + testEmptyCaFailsSetup(); + testCorruptCaInBundleFailsSetup(); testGarbagePkcs12FailsSetup(); testGarbageCrlFailsSetup(); testInvalidCipherListFailsSetup(); @@ -679,6 +683,41 @@ struct openssl_engine_test BOOST_TEST(!!eng.check_context()); } + // A trust anchor with no PEM block at all must fail setup. + void testNonPemCaFailsSetup() + { + tls_context ctx; + std::ignore = ctx.add_certificate_authority("not a certificate"); + + ossl_engine eng; + BOOST_TEST(!eng.init(ctx)); + BOOST_TEST(!!eng.check_context()); + } + + // An empty trust anchor must fail setup. + void testEmptyCaFailsSetup() + { + tls_context ctx; + std::ignore = ctx.add_certificate_authority(""); + + ossl_engine eng; + BOOST_TEST(!eng.init(ctx)); + BOOST_TEST(!!eng.check_context()); + } + + // A corrupt certificate after a valid one fails the whole bundle. + void testCorruptCaInBundleFailsSetup() + { + tls_context ctx; + std::ignore = ctx.add_certificate_authority( + std::string(test::ca_cert_pem) + + "-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n"); + + ossl_engine eng; + BOOST_TEST(!eng.init(ctx)); + BOOST_TEST(!!eng.check_context()); + } + // A PKCS#12 bundle that does not decode must fail setup closed, not // leave the context silently credential-less. void testGarbagePkcs12FailsSetup() diff --git a/test/unit/openssl_stream.cpp b/test/unit/openssl_stream.cpp index 7dbbfe900..e36e1f755 100644 --- a/test/unit/openssl_stream.cpp +++ b/test/unit/openssl_stream.cpp @@ -475,6 +475,7 @@ struct openssl_stream_test test::testShutdownCancel(make_stream); test::testSocketErrorPropagation(make_stream); test::testCertificateValidation(make_stream); + test::testCaBundle(make_stream); test::testSni(make_stream); test::testSniCallback(make_stream); test::testHostnamePersistence(make_stream); diff --git a/test/unit/tls_stream_tests.hpp b/test/unit/tls_stream_tests.hpp index c4ecc8c3f..630628832 100644 --- a/test/unit/tls_stream_tests.hpp +++ b/test/unit/tls_stream_tests.hpp @@ -494,6 +494,39 @@ testCertificateValidation(StreamFactory make_stream) } } +/** Test that every certificate in a multi-certificate CA bundle is trusted. + + The trusted CA is placed second so a loader that stops after the first + certificate fails verification. An expired root ahead of it must not + keep the bundle from loading, as system bundles can carry such roots. +*/ +template +void +testCaBundle(StreamFactory make_stream) +{ + // Trusted CA is the second certificate in the bundle + { + io_context ioc; + tls_context client_ctx; + require_ok(client_ctx.add_certificate_authority( + std::string(wrong_ca_cert_pem) + ca_cert_pem)); + require_ok(client_ctx.set_verify_mode(tls_verify_mode::peer)); + run_tls_test( + ioc, client_ctx, make_server_context(), make_stream, make_stream); + } + + // Expired root ahead of the trusted CA + { + io_context ioc; + tls_context client_ctx; + require_ok(client_ctx.add_certificate_authority( + std::string(expired_cert_pem) + ca_cert_pem)); + require_ok(client_ctx.set_verify_mode(tls_verify_mode::peer)); + run_tls_test( + ioc, client_ctx, make_server_context(), make_stream, make_stream); + } +} + /** Test SNI (Server Name Indication). */ template void diff --git a/test/unit/wolfssl_engine.cpp b/test/unit/wolfssl_engine.cpp index 8ca687438..eab9682e0 100644 --- a/test/unit/wolfssl_engine.cpp +++ b/test/unit/wolfssl_engine.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2026 Steve Gerbino +// Copyright (c) 2026 Michael Vandeberg // // Distributed under the Boost Software License, Version 1.0. (See accompanying // file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) @@ -574,6 +575,9 @@ struct wolfssl_engine_test testInvertedVersionWindowFailsSetup(); testGarbagePrivateKeyFailsSetup(); testGarbageCaFailsSetup(); + testNonPemCaFailsSetup(); + testEmptyCaFailsSetup(); + testCorruptCaInBundleSkipped(); testGarbagePkcs12FailsSetup(); testGarbageCrlFailsSetup(); testInvalidCipherListFailsSetup(); @@ -643,6 +647,39 @@ struct wolfssl_engine_test BOOST_TEST(!!eng.init(ctx, tls_role::client, std::string())); } + // A trust anchor with no PEM block at all must fail setup. + void testNonPemCaFailsSetup() + { + tls_context ctx; + std::ignore = ctx.add_certificate_authority("not a certificate"); + + wssl_engine eng; + BOOST_TEST(!!eng.init(ctx, tls_role::client, std::string())); + } + + // An empty trust anchor must fail setup. + void testEmptyCaFailsSetup() + { + tls_context ctx; + std::ignore = ctx.add_certificate_authority(""); + + wssl_engine eng; + BOOST_TEST(!!eng.init(ctx, tls_role::client, std::string())); + } + + // Unlike OpenSSL, a corrupt certificate after a valid one is skipped: + // a bundle fails only when none of its certificates load. + void testCorruptCaInBundleSkipped() + { + tls_context ctx; + std::ignore = ctx.add_certificate_authority( + std::string(test::ca_cert_pem) + + "-----BEGIN CERTIFICATE-----\nAAAA\n-----END CERTIFICATE-----\n"); + + wssl_engine eng; + BOOST_TEST(!eng.init(ctx, tls_role::client, std::string())); + } + // A PKCS#12 bundle that does not decode must fail setup closed. void testGarbagePkcs12FailsSetup() { diff --git a/test/unit/wolfssl_stream.cpp b/test/unit/wolfssl_stream.cpp index 1084be3d3..8957fa0b6 100644 --- a/test/unit/wolfssl_stream.cpp +++ b/test/unit/wolfssl_stream.cpp @@ -167,6 +167,7 @@ struct wolfssl_stream_test test::testShutdownCancel(make_stream); test::testSocketErrorPropagation(make_stream); test::testCertificateValidation(make_stream); + test::testCaBundle(make_stream); test::testSni(make_stream); test::testSniCallback(make_stream); test::testHostnamePersistence(make_stream);