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);