From d270715a099aee30dc6bc7a0128a574a07ee7d5e Mon Sep 17 00:00:00 2001 From: Michael Vandeberg Date: Tue, 7 Jul 2026 12:31:10 -0600 Subject: [PATCH] fix(tls): wire up trust store and verify callback (#297) Corosio's TLS was disclosed as "not safe against the public internet": set_default_verify_paths() left an empty trust store and set_verify_callback() failed to link. This wires both up on the OpenSSL and WolfSSL backends and makes the verify callback genuinely usable and honest across backends. Trust store: - Consume use_default_verify_paths and verify_paths in both backends (SSL_CTX_set_default_verify_paths / SSL_CTX_load_verify_locations; wolfSSL_CTX_load_system_CA_certs guarded by WOLFSSL_SYS_CA_CERTS, plus wolfSSL_CTX_load_verify_locations for directories). Verify callback: - Define the previously declared-but-undefined set_verify_callback template (fixes the link error) and add the private impl setter. - verify_context gains certificate(), returning the DER of the cert under verification, populated natively in each backend (OpenSSL i2d_X509; WolfSSL store->certs[error_depth], no OPENSSL_EXTRA required). This is the portable primitive for certificate pinning. - OpenSSL invokes the callback per-cert including on success, so it can both relax and tighten verification. WolfSSL only invokes it on success when built with WOLFSSL_ALWAYS_VERIFY_CB (implied by --enable-opensslextra); on a minimal build a tightening callback would silently fail open, so a context carrying a callback instead fails the handshake with std::errc::function_not_supported rather than mislead. Tests: - Shared testVerifyCallback covers the portable contract (override-accept, decline, DER inspection) with a callback_supported=false path asserting the fail-closed behavior. - testVerifyCallbackOnSuccess (on-success invocation, reject-valid-cert, content-based pinning) runs on OpenSSL and on capable WolfSSL builds. - OpenSSL add_verify_path (hashed dir) / set_default_verify_paths (SSL_CERT_FILE); WolfSSL add_verify_path (plain dir). CI: - All vcpkg lanes already build a capable WolfSSL (the port hardcodes OPENSSL_EXTRA), so the full-parity path is exercised. A new "wolfssl-minimal" Linux lane uses a vcpkg overlay port with OPENSSL_EXTRA disabled to exercise the fail-closed path. Docs: - Rewrite the set_verify_callback docstring and the TLS guide/tutorial pages to state the portable contract and the WolfSSL build requirement; fix the rotted tls_context_examples.cpp to compile against the real std::error_code API and demonstrate DER-based pinning. --- .github/generate-matrix.py | 24 ++ .../wolfssl/portfile.cmake | 97 +++++ .../vcpkg-overlay-ports/wolfssl/vcpkg.json | 40 ++ .github/workflows/ci.yml | 12 + .../pages/3.tutorials/3d.tls-context.adoc | 132 ++++--- doc/modules/ROOT/pages/4.guide/4l.tls.adoc | 112 +++--- example/tls_context_examples.cpp | 373 ++++++++++-------- include/boost/corosio/tls_context.hpp | 187 +++++++-- include/boost/corosio/wolfssl_stream.hpp | 21 + src/corosio/src/tls/context.cpp | 8 + src/corosio/src/tls/detail/context_impl.hpp | 3 +- src/openssl/src/openssl_stream.cpp | 46 ++- src/wolfssl/src/wolfssl_stream.cpp | 134 +++++++ test/unit/openssl_stream.cpp | 66 ++++ test/unit/tls_stream_tests.hpp | 202 ++++++++++ test/unit/wolfssl_stream.cpp | 54 +++ 16 files changed, 1202 insertions(+), 309 deletions(-) create mode 100644 .github/vcpkg-overlay-ports/wolfssl/portfile.cmake create mode 100644 .github/vcpkg-overlay-ports/wolfssl/vcpkg.json diff --git a/.github/generate-matrix.py b/.github/generate-matrix.py index 70f22bd74..c58f429fb 100644 --- a/.github/generate-matrix.py +++ b/.github/generate-matrix.py @@ -141,6 +141,9 @@ def generate_name(compiler_family, entry): if entry.get("superproject-cmake"): modifiers.append("superproject CMake") + if entry.get("wolfssl-minimal"): + modifiers.append("wolfssl-minimal") + if entry.get("shared") is False: modifiers.append("static") @@ -255,6 +258,26 @@ def generate_superproject_cmake_variant(compiler_family, spec): return entry +def generate_wolfssl_minimal_variant(compiler_family, spec): + """Build against a minimal WolfSSL to exercise the verify-callback + fail-closed path. + + The default vcpkg wolfssl port hardcodes OPENSSL_EXTRA (which implies + WOLFSSL_ALWAYS_VERIFY_CB), so every normal lane runs a "capable" WolfSSL + that invokes verify callbacks on success. This variant points vcpkg at a + repo overlay port with OPENSSL_EXTRA disabled, matching a stock + distribution build, so corosio's set_verify_callback fail-closed path + (returns std::errc::function_not_supported) is exercised in CI. + """ + entry = make_entry(compiler_family, spec, **{ + "wolfssl-minimal": True, + "build-cmake": True, + }) + entry.pop("is-latest", None) + entry.pop("is-earliest", None) + return entry + + def apply_clang_tidy(entry, spec): """Add clang-tidy flag and install package to an entry.""" entry["clang-tidy"] = True @@ -298,6 +321,7 @@ def main(): if family == "gcc": matrix.append(generate_superproject_cmake_variant(family, spec)) + matrix.append(generate_wolfssl_minimal_variant(family, spec)) if family == "clang": matrix.append(generate_x86_variant(family, spec)) diff --git a/.github/vcpkg-overlay-ports/wolfssl/portfile.cmake b/.github/vcpkg-overlay-ports/wolfssl/portfile.cmake new file mode 100644 index 000000000..f71645880 --- /dev/null +++ b/.github/vcpkg-overlay-ports/wolfssl/portfile.cmake @@ -0,0 +1,97 @@ +# Overlay port: identical to the upstream vcpkg `wolfssl` port EXCEPT that +# WOLFSSL_OPENSSLEXTRA is disabled. The upstream port hardcodes +# -DWOLFSSL_OPENSSLEXTRA=yes, which (via wolfssl/wolfcrypt/settings.h) also +# defines WOLFSSL_ALWAYS_VERIFY_CB and WOLFSSL_VERIFY_CB_ALL_CERTS. That +# makes WolfSSL invoke a verify callback on success as well as failure. +# +# This overlay produces a *minimal* WolfSSL (OPENSSL_EXTRA off), matching a +# stock distribution build, so CI can exercise corosio's fail-closed path +# for set_verify_callback (which returns std::errc::function_not_supported +# when the callback cannot be honored on such a build). Used only by the +# dedicated "wolfssl-minimal" CI lane via VCPKG_OVERLAY_PORTS; all other +# lanes use the upstream (capable) port. +# +# Keep this in sync with the pinned upstream port +# (microsoft/vcpkg ports/wolfssl) apart from the single OPENSSLEXTRA line. + +vcpkg_from_github( + OUT_SOURCE_PATH SOURCE_PATH + REPO wolfssl/wolfssl + REF "v${VERSION}-stable" + SHA512 29f52644966f21908e0d3f795c62b0f5af9cd2d766db20c6ed5c588611f19f048119827fe6e787ccc3ce676d8c97cf7ab409d996df0e3acb812d6cd01364de61 + HEAD_REF master + PATCHES + ) + +if ("asio" IN_LIST FEATURES) + set(ENABLE_ASIO yes) +else() + set(ENABLE_ASIO no) +endif() + +if ("dtls" IN_LIST FEATURES) + set(ENABLE_DTLS yes) +else() + set(ENABLE_DTLS no) +endif() + +if ("quic" IN_LIST FEATURES) + set(ENABLE_QUIC yes) +else() + set(ENABLE_QUIC no) +endif() + +vcpkg_cmake_get_vars(cmake_vars_file) +include("${cmake_vars_file}") + +foreach(config RELEASE DEBUG) + string(APPEND VCPKG_COMBINED_C_FLAGS_${config} " -DHAVE_EX_DATA -DNO_WOLFSSL_STUB -DWOLFSSL_ALT_CERT_CHAINS -DWOLFSSL_DES_ECB -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DWOLFSSL_CERT_GEN -DWOLFSSL_ASN_TEMPLATE -DWOLFSSL_KEY_GEN -DHAVE_PKCS7 -DHAVE_AES_KEYWRAP -DWOLFSSL_AES_DIRECT -DHAVE_X963_KDF") + if ("secret-callback" IN_LIST FEATURES) + string(APPEND VCPKG_COMBINED_C_FLAGS_${config} " -DHAVE_SECRET_CALLBACK") + endif() +endforeach() + +vcpkg_cmake_configure( + SOURCE_PATH ${SOURCE_PATH} + OPTIONS + -DWOLFSSL_BUILD_OUT_OF_TREE=yes + -DWOLFSSL_EXAMPLES=no + -DWOLFSSL_CRYPT_TESTS=no + -DWOLFSSL_OPENSSLEXTRA=no + -DWOLFSSL_TPM=yes + -DWOLFSSL_TLSX=yes + -DWOLFSSL_OCSP=yes + -DWOLFSSL_OCSPSTAPLING=yes + -DWOLFSSL_OCSPSTAPLING_V2=yes + -DWOLFSSL_CRL=yes + -DWOLFSSL_DES3=yes + -DWOLFSSL_HPKE=yes + -DWOLFSSL_SNI=yes + -DWOLFSSL_ASIO=${ENABLE_ASIO} + -DWOLFSSL_DTLS=${ENABLE_DTLS} + -DWOLFSSL_DTLS13=${ENABLE_DTLS} + -DWOLFSSL_DTLS_CID=${ENABLE_DTLS} + -DWOLFSSL_QUIC=${ENABLE_QUIC} + -DWOLFSSL_SESSION_TICKET=${ENABLE_QUIC} + OPTIONS_RELEASE + -DCMAKE_C_FLAGS=${VCPKG_COMBINED_C_FLAGS_RELEASE} + OPTIONS_DEBUG + -DCMAKE_C_FLAGS=${VCPKG_COMBINED_C_FLAGS_DEBUG} + -DWOLFSSL_DEBUG=yes) + +vcpkg_cmake_install() +vcpkg_copy_pdbs() +vcpkg_cmake_config_fixup(CONFIG_PATH lib/cmake/wolfssl) + +if(VCPKG_TARGET_IS_IOS OR VCPKG_TARGET_IS_OSX) + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/lib/pkgconfig/wolfssl.pc" "Libs.private: " "Libs.private: -framework CoreFoundation -framework Security ") + if(NOT VCPKG_BUILD_TYPE) + vcpkg_replace_string("${CURRENT_PACKAGES_DIR}/debug/lib/pkgconfig/wolfssl.pc" "Libs.private: " "Libs.private: -framework CoreFoundation -framework Security ") + endif() +endif() +vcpkg_fixup_pkgconfig() + +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/include") +file(REMOVE_RECURSE "${CURRENT_PACKAGES_DIR}/debug/share") + +vcpkg_install_copyright(FILE_LIST "${SOURCE_PATH}/COPYING") diff --git a/.github/vcpkg-overlay-ports/wolfssl/vcpkg.json b/.github/vcpkg-overlay-ports/wolfssl/vcpkg.json new file mode 100644 index 000000000..7beadaa59 --- /dev/null +++ b/.github/vcpkg-overlay-ports/wolfssl/vcpkg.json @@ -0,0 +1,40 @@ +{ + "name": "wolfssl", + "version": "5.8.2", + "port-version": 1, + "description": "TLS and Cryptographic library for many platforms", + "homepage": "https://wolfssl.com", + "license": "GPL-3.0-or-later", + "supports": "!uwp", + "dependencies": [ + { + "name": "vcpkg-cmake", + "host": true + }, + { + "name": "vcpkg-cmake-config", + "host": true + }, + { + "name": "vcpkg-cmake-get-vars", + "host": true + } + ], + "features": { + "asio": { + "description": "Enable asio support" + }, + "curve25519-blinding": { + "description": "Enables callback to provide TLS keys for debugging" + }, + "dtls": { + "description": "DTLS support" + }, + "quic": { + "description": "Enable quic support" + }, + "secret-callback": { + "description": "Enables callback to provide TLS keys for debugging" + } + } +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ceaa60f3a..b62244a28 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -230,6 +230,18 @@ jobs: } EOF + # For the wolfssl-minimal lane, point vcpkg at the repo overlay port + # that disables OPENSSL_EXTRA (and thus WOLFSSL_ALWAYS_VERIFY_CB). This + # yields a "minimal" WolfSSL like a stock distribution build, so + # corosio's set_verify_callback fail-closed path (returns + # std::errc::function_not_supported) is exercised. All other lanes use + # the upstream (capable) port. + - name: Enable minimal-WolfSSL overlay port + if: ${{ matrix.wolfssl-minimal }} + shell: bash + run: | + echo "VCPKG_OVERLAY_PORTS=${GITHUB_WORKSPACE}/corosio-root/.github/vcpkg-overlay-ports" >> $GITHUB_ENV + - name: Set vcpkg triplet if: matrix.vcpkg-triplet shell: bash 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 aecd785fb..a1b404f61 100644 --- a/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc +++ b/doc/modules/ROOT/pages/3.tutorials/3d.tls-context.adoc @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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) @@ -41,23 +42,25 @@ Use `tls_context` to configure TLS settings once, then pass it to TLS streams for establishing secure connections. [WARNING] -.Implementation status +.Not yet implemented ==== -Several settings shown in this tutorial are accepted by the API but are -*not yet wired up* by the OpenSSL or WolfSSL backends in this release; -they are stored and silently ignored: `set_default_verify_paths()`, -`set_verify_callback()` (also fails to link), `set_alpn()`, -`set_min_protocol_version()` / `set_max_protocol_version()`, -`add_verify_path()`, all revocation features (`add_crl*`, -`set_ocsp_staple` / `set_require_ocsp_staple`, `set_revocation_policy`), -and `use_pkcs12*` (returns `std::errc::function_not_supported`). -`set_ciphersuites()` is applied by OpenSSL only. - -**To verify a peer today you must supply CA certificates explicitly** via -`load_verify_file()` or `add_certificate_authority()` — system trust via -`set_default_verify_paths()` does not work yet. The working surface is -`set_verify_mode()`, `set_verify_depth()`, custom trust anchors, -credentials, and `set_hostname()`. +Some settings shown in this tutorial are accepted by the API but *not yet +wired up* by the backends; they are stored and silently ignored: +`set_alpn()`, `set_min_protocol_version()` / `set_max_protocol_version()`, +all revocation features (`add_crl*`, `set_ocsp_staple` / +`set_require_ocsp_staple`, `set_revocation_policy`), and `use_pkcs12*` +(returns `std::errc::function_not_supported`). `set_ciphersuites()` is +applied by OpenSSL only; WolfSSL ignores it. +==== + +[NOTE] +==== +Two implemented features depend on how WolfSSL was built: +`set_default_verify_paths()` needs `WOLFSSL_SYS_CA_CERTS` to load the system +store, and `set_verify_callback()` needs `WOLFSSL_ALWAYS_VERIFY_CB` +(otherwise installing a callback fails the handshake with +`std::errc::function_not_supported` rather than failing open). Both are +enabled by `--enable-opensslextra`. ==== == Construction @@ -196,13 +199,10 @@ This uses the operating system's trusted certificates: * macOS: System Keychain * Windows: Windows Certificate Store -[WARNING] -==== -`set_default_verify_paths()` is *not yet wired up*: it is a no-op and the -OS trust store is never loaded, so a client relying on it cannot verify a -public server. Until this is implemented, supply trust anchors explicitly -with `load_verify_file()` or `add_certificate_authority()` (below). -==== +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). === Custom CA Bundle @@ -214,23 +214,23 @@ For internal PKI or testing, load a custom CA bundle: ctx.load_verify_file( "/path/to/ca-bundle.crt" ); ---- -NOTE: `load_verify_file()` and `add_certificate_authority()` are the -currently working way to establish trust anchors. 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. +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 -On systems with hashed certificate directories (created by `c_rehash`): +Add a directory of CA certificates: [source,cpp] ---- ctx.add_verify_path( "/etc/ssl/certs" ); ---- -NOTE: `add_verify_path()` is not yet applied; the directory is accepted -but never loaded. +NOTE: With OpenSSL the directory must use the hashed-filename layout +created by `openssl rehash` / `c_rehash`, since certificates are looked up +on demand by subject-name hash. WolfSSL loads every certificate file in +the directory. === Individual CA Certificates @@ -374,34 +374,43 @@ The default (typically 100) is sufficient for most chains. === Custom Verification -For advanced use cases, install a custom verification callback: - -[WARNING] -==== -`set_verify_callback()` is not yet implemented: the template is declared -but never defined, so code that instantiates it *fails to link*. Use -`set_verify_mode()` with explicitly supplied trust anchors instead. -==== +For advanced use cases, install a custom verification callback. It runs +during the handshake; return `true` to accept the certificate or `false` +to reject it. Inspect the certificate portably with +`verify_ctx.certificate()` (its DER bytes); `native_handle()` also exposes +the backend's `X509_STORE_CTX*`. [source,cpp] ---- ctx.set_verify_callback( - []( bool preverified, auto& verify_ctx ) -> bool + []( bool preverified, corosio::verify_context& verify_ctx ) -> bool { - // preverified: result of standard verification - // verify_ctx: access to certificate information - if( !preverified ) - { - // Log or inspect the failure - return false; // reject - } - - // Additional custom checks... - return true; // accept + return false; // chain did not verify + + auto der = verify_ctx.certificate(); // DER of the current cert + return der.size() == expected_pin.size() && + std::equal( der.begin(), der.end(), expected_pin.begin() ); }); ---- +[NOTE] +==== +Which certificates the callback sees depends on the backend: + +* OpenSSL — invoked for every certificate, including ones that passed, so + it can both relax verification (accept a rejected cert) and tighten it + (reject an otherwise-valid cert, e.g. pinning). +* WolfSSL with `WOLFSSL_ALWAYS_VERIFY_CB` (via `--enable-opensslextra`) — + same as OpenSSL. +* WolfSSL without it — the library invokes the callback only on failure, + so it cannot be honored on success. To avoid silently ignoring a + tightening callback (which would fail open), a context carrying a + callback instead fails the handshake with + `std::errc::function_not_supported`. Rebuild WolfSSL with + `WOLFSSL_ALWAYS_VERIFY_CB`, or omit the callback. +==== + == Revocation Checking Certificate revocation checking verifies that certificates haven't been @@ -477,13 +486,13 @@ ctx.set_require_ocsp_staple( true ); A common pattern uses two context configurations: -[WARNING] +[NOTE] ==== -Both contexts below rely on `set_default_verify_paths()` (a no-op here) for -trust and on revocation policy / CRLs for the "hardened" path — none of -which are applied yet. As written, neither context verifies anything. To -verify a peer today, load a CA bundle explicitly with -`load_verify_file()`; the revocation hardening is not yet available. +Both contexts below verify the peer via `set_default_verify_paths()` and +`set_verify_mode( tls_verify_mode::peer )`. The revocation hardening on the +"hardened" path (`add_crl_file()` / `set_revocation_policy()`) is *not yet +applied* by the backends, so that context currently offers the same +verification as the bootstrap one. ==== [source,cpp] @@ -564,14 +573,11 @@ scenarios. === HTTPS Client Context -[WARNING] -==== -This example trusts public CAs via `set_default_verify_paths()`, which is a -no-op in this release, so it verifies nothing. Replace it with an explicit -CA bundle to actually verify the server, for example +NOTE: This example trusts public CAs via `set_default_verify_paths()`. On +WolfSSL builds without `WOLFSSL_SYS_CA_CERTS`, load an explicit CA bundle +instead, for example `ctx.load_verify_file( "/etc/ssl/certs/ca-certificates.crt" );`. The -`set_min_protocol_version()` call is also inert. -==== +`set_min_protocol_version()` call is inert in this release. [source,cpp] ---- diff --git a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc index 06d298051..510988f72 100644 --- a/doc/modules/ROOT/pages/4.guide/4l.tls.adoc +++ b/doc/modules/ROOT/pages/4.guide/4l.tls.adoc @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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) @@ -40,35 +41,48 @@ confidentiality, integrity, and authentication. Corosio supports TLS through: * **wolfssl_stream** — TLS implementation using WolfSSL * **openssl_stream** — TLS implementation using OpenSSL +A verified-safe client trusts the system CAs, requires a peer certificate, +and checks the hostname: + +[source,cpp] +---- +tls_context ctx; +ctx.set_default_verify_paths(); // trust the system CAs +ctx.set_verify_mode(tls_verify_mode::peer); // require + verify the peer +ctx.set_hostname("example.com"); // SNI + hostname check +---- + +Trust anchors can also be supplied explicitly with +`add_certificate_authority()`, `load_verify_file()`, or `add_verify_path()`, +and `set_verify_callback()` installs a custom verification hook. + [WARNING] -.Implementation status +.Not yet implemented ==== -Several `tls_context` settings are accepted by the API but are *not yet -wired up* by the OpenSSL or WolfSSL backends in this release. They are +Several other `tls_context` settings are accepted by the API but are *not +yet wired up* by the OpenSSL or WolfSSL backends in this release. They are stored and silently ignored: -* `set_default_verify_paths()` — the OS trust store is never loaded. A - client that relies on it has an *empty trust store* and cannot verify a - public server. -* `set_verify_callback()` — declared but not defined; instantiating it - *fails to link*. * `set_alpn()` — ALPN is never negotiated. * `set_min_protocol_version()` / `set_max_protocol_version()` — version bounds are never applied; the range is the native default. * `add_crl()` / `add_crl_file()`, `set_ocsp_staple()` / `set_require_ocsp_staple()`, `set_revocation_policy()` — all revocation checking is inert. -* `add_verify_path()` — directory of CAs is never loaded. * `use_pkcs12()` / `use_pkcs12_file()` — return `std::errc::function_not_supported`. * `set_ciphersuites()` — applied by OpenSSL only; WolfSSL ignores it. +==== -**To verify a peer today you must supply CA certificates explicitly** via -`load_verify_file()` or `add_certificate_authority()`. The working -configuration surface is: `set_verify_mode()`, `set_verify_depth()`, -custom trust anchors (`add_certificate_authority()` / `load_verify_file()`), -credentials (`use_certificate*` / `use_private_key*`), and `set_hostname()` -for SNI plus client-side hostname verification. +[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 +`std::errc::function_not_supported` rather than failing open (see below). +Both are enabled by `--enable-opensslextra`. ==== The typical flow: @@ -186,14 +200,10 @@ Use the operating system's default CA certificates: ctx.set_default_verify_paths(); ---- -[WARNING] -==== -`set_default_verify_paths()` is *not yet wired up*: it is a no-op and the -OS trust store is never loaded. A client relying on it has an empty trust -store and cannot verify a public server. Until this is implemented, load a -CA bundle explicitly with `load_verify_file()` or -`add_certificate_authority()`. -==== +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 which this call has no effect and a CA bundle must be supplied +explicitly via `load_verify_file()` or `add_certificate_authority()`. ==== Custom CA Certificates @@ -209,12 +219,11 @@ ctx.load_verify_file("/etc/ssl/certs/ca-certificates.crt"); ctx.add_verify_path("/etc/ssl/certs"); ---- -NOTE: `add_certificate_authority()` and `load_verify_file()` are the -currently working way to establish trust anchors. 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. `add_verify_path()` is not -yet applied — the directory is never loaded. +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. === Protocol Configuration @@ -314,28 +323,42 @@ ctx.set_verify_depth(10); // Max 10 intermediate certs ==== Custom Verification Callback -For advanced verification logic: - -[WARNING] -==== -`set_verify_callback()` is not yet implemented: the template is declared -but never defined, so code that instantiates it *fails to link*. Use -`set_verify_mode()` with explicitly supplied trust anchors instead. -==== +For advanced verification logic, install a callback. It runs during the +handshake; returning `true` accepts the certificate and `false` rejects +it. Inspect the certificate portably with `certificate()`, which returns +its DER bytes — ideal for certificate pinning. (`native_handle()` also +exposes the backend's `X509_STORE_CTX*` for backend-specific use.) [source,cpp] ---- ctx.set_verify_callback( - [](bool preverified, /* verify_context& */ auto& ctx) { - // Return true to accept, false to reject + [](bool preverified, corosio::verify_context& ctx) { if (!preverified) - return false; // Reject if basic checks failed + return false; // reject if the chain didn't verify - // Additional custom checks... - return true; + auto der = ctx.certificate(); // DER of the current certificate + return der.size() == expected_pin.size() && + std::equal(der.begin(), der.end(), expected_pin.begin()); }); ---- +[NOTE] +==== +Which certificates the callback sees depends on the backend: + +* OpenSSL — invoked for every certificate in the chain, including ones + that passed, so it can both *relax* (accept a rejected cert) and + *tighten* (reject an otherwise-valid cert, e.g. pinning) verification. +* WolfSSL built with `WOLFSSL_ALWAYS_VERIFY_CB` (via + `--enable-opensslextra`) — same as OpenSSL. +* WolfSSL without that option — the library invokes the callback only on + *failure*, so it cannot be honored on a successful handshake. To avoid + silently ignoring a tightening callback (which would fail open), a + context that carries a callback instead *fails the handshake* with + `std::errc::function_not_supported`. Rebuild WolfSSL with + `WOLFSSL_ALWAYS_VERIFY_CB`, or omit the callback. +==== + === Revocation Checking WARNING: None of the revocation features below are wired up yet. CRLs @@ -738,8 +761,9 @@ server_ctx.load_verify_file("client-ca.pem"); === Client Side -NOTE: `set_default_verify_paths()` below is a no-op in this release; supply -the server's CA explicitly with `load_verify_file()` to actually verify it. +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/example/tls_context_examples.cpp b/example/tls_context_examples.cpp index 44b99cbad..f6c919956 100644 --- a/example/tls_context_examples.cpp +++ b/example/tls_context_examples.cpp @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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) @@ -8,12 +9,32 @@ // // Examples demonstrating tls_context API usage patterns. -// These examples are not meant to compile; they validate API ergonomics. +// +// The configuration setters return a std::error_code rather than throwing. +// These examples use the small must() helper to turn a failure into an +// exception where that keeps the example terse; load_checked() and +// load_mixed() below show the explicit error_code style instead. #include +#include +#include +#include +#include +#include +#include +#include + using namespace boost::corosio; +// Throw std::system_error if an operation reported a failure. +static void +must(std::error_code ec) +{ + if (ec) + throw std::system_error(ec); +} + // // HTTPS Client Context // @@ -22,16 +43,16 @@ using namespace boost::corosio; tls_context make_https_client() { tls_context ctx; - + // Use system trust store for public websites - ctx.set_default_verify_paths().value(); - + must(ctx.set_default_verify_paths()); + // Verify the server certificate - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + must(ctx.set_verify_mode( tls_verify_mode::peer )); + // Set the hostname for SNI and certificate verification ctx.set_hostname( "api.example.com" ); - + return ctx; } @@ -39,13 +60,13 @@ tls_context make_https_client() tls_context make_pinned_ca_client( std::string_view ca_pem ) { tls_context ctx; - + // Only trust this specific CA - ctx.add_certificate_authority( ca_pem ).value(); - - ctx.set_verify_mode( tls_verify_mode::peer ).value(); + must(ctx.add_certificate_authority( ca_pem )); + + must(ctx.set_verify_mode( tls_verify_mode::peer )); ctx.set_hostname( "internal.example.com" ); - + return ctx; } @@ -53,13 +74,13 @@ tls_context make_pinned_ca_client( std::string_view ca_pem ) tls_context make_http2_client() { tls_context ctx; - - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + + must(ctx.set_default_verify_paths()); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + // Prefer HTTP/2, fall back to HTTP/1.1 - ctx.set_alpn( { "h2", "http/1.1" } ).value(); - + must(ctx.set_alpn( { "h2", "http/1.1" } )); + return ctx; } @@ -71,14 +92,14 @@ tls_context make_http2_client() tls_context make_basic_server() { tls_context ctx; - + // Load certificate chain and private key - ctx.use_certificate_chain_file( "server-fullchain.pem" ).value(); - ctx.use_private_key_file( "server.key", tls_file_format::pem ).value(); - + must(ctx.use_certificate_chain_file( "server-fullchain.pem" )); + must(ctx.use_private_key_file( "server.key", tls_file_format::pem )); + // Don't verify clients (no mTLS) - ctx.set_verify_mode( tls_verify_mode::none ).value(); - + must(ctx.set_verify_mode( tls_verify_mode::none )); + return ctx; } @@ -86,17 +107,17 @@ tls_context make_basic_server() tls_context make_mtls_server() { tls_context ctx; - + // Server credentials - ctx.use_certificate_chain_file( "server-fullchain.pem" ).value(); - ctx.use_private_key_file( "server.key", tls_file_format::pem ).value(); - + must(ctx.use_certificate_chain_file( "server-fullchain.pem" )); + must(ctx.use_private_key_file( "server.key", tls_file_format::pem )); + // Trust this CA for client certificates - ctx.load_verify_file( "client-ca.crt" ).value(); - + must(ctx.load_verify_file( "client-ca.crt" )); + // Require clients to present a valid certificate - ctx.set_verify_mode( tls_verify_mode::require_peer ).value(); - + must(ctx.set_verify_mode( tls_verify_mode::require_peer )); + return ctx; } @@ -104,12 +125,12 @@ tls_context make_mtls_server() tls_context make_server_from_pfx() { tls_context ctx; - + // Load all credentials from a single file - ctx.use_pkcs12_file( "server.pfx", "bundle-password" ).value(); - - ctx.set_verify_mode( tls_verify_mode::none ).value(); - + must(ctx.use_pkcs12_file( "server.pfx", "bundle-password" )); + + must(ctx.set_verify_mode( tls_verify_mode::none )); + return ctx; } @@ -117,18 +138,22 @@ tls_context make_server_from_pfx() tls_context make_server_encrypted_key() { tls_context ctx; - + // Set password callback before loading encrypted key ctx.set_password_callback( []( std::size_t max_len, tls_password_purpose purpose ) { + (void)max_len; + (void)purpose; // Read from environment or secret manager - return std::string( std::getenv( "TLS_KEY_PASSWORD" ) ); + char const* pw = std::getenv( "TLS_KEY_PASSWORD" ); + return std::string( pw ? pw : "" ); }); - - ctx.use_certificate_chain_file( "server.crt" ).value(); - ctx.use_private_key_file( "server-encrypted.key", tls_file_format::pem ).value(); - + + must(ctx.use_certificate_chain_file( "server.crt" )); + must(ctx.use_private_key_file( + "server-encrypted.key", tls_file_format::pem )); + return ctx; } @@ -140,15 +165,15 @@ tls_context make_server_encrypted_key() tls_context make_mtls_client() { tls_context ctx; - + // Client credentials for mTLS - ctx.use_certificate_file( "client.crt", tls_file_format::pem ).value(); - ctx.use_private_key_file( "client.key", tls_file_format::pem ).value(); - + must(ctx.use_certificate_file( "client.crt", tls_file_format::pem )); + must(ctx.use_private_key_file( "client.key", tls_file_format::pem )); + // Trust specific CA for server verification - ctx.load_verify_file( "server-ca.crt" ).value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + must(ctx.load_verify_file( "server-ca.crt" )); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + return ctx; } @@ -160,13 +185,13 @@ tls_context make_mtls_client() tls_context make_tls13_only() { tls_context ctx; - - ctx.set_min_protocol_version( tls_version::tls_1_3 ).value(); - ctx.set_max_protocol_version( tls_version::tls_1_3 ).value(); - - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + + must(ctx.set_min_protocol_version( tls_version::tls_1_3 )); + must(ctx.set_max_protocol_version( tls_version::tls_1_3 )); + + must(ctx.set_default_verify_paths()); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + return ctx; } @@ -174,13 +199,13 @@ tls_context make_tls13_only() tls_context make_tls12_plus() { tls_context ctx; - - ctx.set_min_protocol_version( tls_version::tls_1_2 ).value(); + + must(ctx.set_min_protocol_version( tls_version::tls_1_2 )); // No max = allow newest - - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + + must(ctx.set_default_verify_paths()); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + return ctx; } @@ -192,16 +217,16 @@ tls_context make_tls12_plus() tls_context make_high_security() { tls_context ctx; - + // Only ECDHE key exchange with AESGCM or ChaCha20 - ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" ).value(); - + must(ctx.set_ciphersuites( "ECDHE+AESGCM:ECDHE+CHACHA20" )); + // TLS 1.3 only - ctx.set_min_protocol_version( tls_version::tls_1_3 ).value(); - - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + must(ctx.set_min_protocol_version( tls_version::tls_1_3 )); + + must(ctx.set_default_verify_paths()); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + return ctx; } @@ -213,15 +238,15 @@ tls_context make_high_security() tls_context make_client_with_crl( std::string_view crl_path ) { tls_context ctx; - - ctx.set_default_verify_paths().value(); - ctx.add_crl_file( crl_path ).value(); - + + must(ctx.set_default_verify_paths()); + must(ctx.add_crl_file( crl_path )); + // Fail if certificate is revoked, allow if status unknown - ctx.set_revocation_policy( tls::revocation_policy::soft_fail ); - - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + ctx.set_revocation_policy( tls_revocation_policy::soft_fail ); + + must(ctx.set_verify_mode( tls_verify_mode::peer )); + return ctx; } @@ -229,13 +254,13 @@ tls_context make_client_with_crl( std::string_view crl_path ) tls_context make_client_require_ocsp() { tls_context ctx; - - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + + must(ctx.set_default_verify_paths()); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + // Require server to provide OCSP staple ctx.set_require_ocsp_staple( true ); - + return ctx; } @@ -243,13 +268,13 @@ tls_context make_client_require_ocsp() tls_context make_server_with_ocsp( std::string_view ocsp_response ) { tls_context ctx; - - ctx.use_certificate_chain_file( "server.crt" ).value(); - ctx.use_private_key_file( "server.key", tls_file_format::pem ).value(); - + + must(ctx.use_certificate_chain_file( "server.crt" )); + must(ctx.use_private_key_file( "server.key", tls_file_format::pem )); + // Provide pre-fetched OCSP response to clients - ctx.set_ocsp_staple( ocsp_response ).value(); - + must(ctx.set_ocsp_staple( ocsp_response )); + return ctx; } @@ -257,16 +282,16 @@ tls_context make_server_with_ocsp( std::string_view ocsp_response ) tls_context make_hardened_client() { tls_context ctx; - - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + + must(ctx.set_default_verify_paths()); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + // Fail if revocation status cannot be determined - ctx.set_revocation_policy( tls::revocation_policy::hard_fail ); - + ctx.set_revocation_policy( tls_revocation_policy::hard_fail ); + // Require OCSP staple from server ctx.set_require_ocsp_staple( true ); - + return ctx; } @@ -274,31 +299,30 @@ tls_context make_hardened_client() // Custom Verification // -// Client with custom verification callback -tls_context make_client_custom_verify() +// Client that pins a specific certificate via a verification callback. +tls_context make_client_custom_verify( std::span pin ) { tls_context ctx; - - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - - ctx.set_verify_callback( - []( bool preverified, auto& verify_ctx ) -> bool + + must(ctx.set_default_verify_paths()); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + + must(ctx.set_verify_callback( + [pin]( bool preverified, verify_context& verify_ctx ) -> bool { + // Require the chain to verify normally first. if( !preverified ) - { - // Standard verification failed - could log here return false; - } - - // Additional custom checks could go here: - // - Check specific certificate fields - // - Verify against a pinned certificate - // - Log certificate details - - return true; - }).value(); - + + // Then pin: accept only if the certificate's DER matches. The + // DER bytes are available portably across backends via + // certificate(); the raw X509_STORE_CTX* is also reachable + // through verify_ctx.native_handle() for backend-specific use. + auto der = verify_ctx.certificate(); + return der.size() == pin.size() && + std::equal( der.begin(), der.end(), pin.begin() ); + })); + return ctx; } @@ -306,13 +330,13 @@ tls_context make_client_custom_verify() tls_context make_client_limited_depth() { tls_context ctx; - - ctx.set_default_verify_paths().value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + + must(ctx.set_default_verify_paths()); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + // Allow at most 2 intermediate certificates - ctx.set_verify_depth( 2 ).value(); - + must(ctx.set_verify_depth( 2 )); + return ctx; } @@ -327,14 +351,14 @@ tls_context make_from_memory( std::string_view ca_pem ) { tls_context ctx; - + // From vault/secret manager - ctx.use_certificate_chain( cert_pem ).value(); - ctx.use_private_key( key_pem, tls_file_format::pem ).value(); - ctx.add_certificate_authority( ca_pem ).value(); - - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + must(ctx.use_certificate_chain( cert_pem )); + must(ctx.use_private_key( key_pem, tls_file_format::pem )); + must(ctx.add_certificate_authority( ca_pem )); + + must(ctx.set_verify_mode( tls_verify_mode::peer )); + return ctx; } @@ -344,10 +368,10 @@ tls_context make_from_pkcs12_memory( std::string_view passphrase ) { tls_context ctx; - - ctx.use_pkcs12( pkcs12_data, passphrase ).value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); - + + must(ctx.use_pkcs12( pkcs12_data, passphrase )); + must(ctx.set_verify_mode( tls_verify_mode::peer )); + return ctx; } @@ -359,10 +383,10 @@ tls_context make_from_pkcs12_memory( tls_context make_from_der() { tls_context ctx; - - ctx.use_certificate_file( "server.der", tls_file_format::der ).value(); - ctx.use_private_key_file( "server.key.der", tls_file_format::der ).value(); - + + must(ctx.use_certificate_file( "server.der", tls_file_format::der )); + must(ctx.use_private_key_file( "server.key.der", tls_file_format::der )); + return ctx; } @@ -375,18 +399,21 @@ void demonstrate_sharing() { // Create a context tls_context original; - original.set_default_verify_paths().value(); - original.set_verify_mode( tls_verify_mode::peer ).value(); - + must(original.set_default_verify_paths()); + must(original.set_verify_mode( tls_verify_mode::peer )); + // Share via copy - both point to same underlying state tls_context copy1 = original; tls_context copy2 = original; - + (void)copy1; + (void)copy2; + // Changes to copy1 affect copy2 and original // (they all share the same impl) - + // Move transfers ownership tls_context moved = std::move( original ); + (void)moved; // original is now empty } @@ -398,33 +425,33 @@ void demonstrate_sharing() void load_throwing() { tls_context ctx; - - ctx.use_certificate_chain_file( "cert.pem" ).value(); // throws on error - ctx.use_private_key_file( "key.pem", tls_file_format::pem ).value(); - ctx.set_default_verify_paths().value(); + + must(ctx.use_certificate_chain_file( "cert.pem" )); // throws on error + must(ctx.use_private_key_file( "key.pem", tls_file_format::pem )); + must(ctx.set_default_verify_paths()); } // Check errors explicitly bool load_checked( tls_context& ctx, std::string& error_msg ) { - if( auto r = ctx.use_certificate_chain_file( "cert.pem" ); !r ) + if( auto ec = ctx.use_certificate_chain_file( "cert.pem" ); ec ) { - error_msg = "Certificate: " + std::string( r.error().message() ); + error_msg = "Certificate: " + ec.message(); return false; } - - if( auto r = ctx.use_private_key_file( "key.pem", tls_file_format::pem ); !r ) + + if( auto ec = ctx.use_private_key_file( "key.pem", tls_file_format::pem ); ec ) { - error_msg = "Key: " + std::string( r.error().message() ); + error_msg = "Key: " + ec.message(); return false; } - - if( auto r = ctx.set_default_verify_paths(); !r ) + + if( auto ec = ctx.set_default_verify_paths(); ec ) { - error_msg = "CA store: " + std::string( r.error().message() ); + error_msg = "CA store: " + ec.message(); return false; } - + return true; } @@ -432,31 +459,41 @@ bool load_checked( tls_context& ctx, std::string& error_msg ) void load_mixed() { tls_context ctx; - + // File loading might fail at runtime - if( auto r = ctx.use_certificate_chain_file( "cert.pem" ); !r ) + if( auto ec = ctx.use_certificate_chain_file( "cert.pem" ); ec ) { // Handle missing file gracefully - std::cerr << "Certificate not found: " << r.error().message() << "\n"; + std::cerr << "Certificate not found: " << ec.message() << "\n"; return; } - + // Protocol settings won't fail if arguments are valid - ctx.set_min_protocol_version( tls_version::tls_1_2 ).value(); - ctx.set_verify_mode( tls_verify_mode::peer ).value(); + must(ctx.set_min_protocol_version( tls_version::tls_1_2 )); + must(ctx.set_verify_mode( tls_verify_mode::peer )); } // -// Main (not compiled, just for documentation) +// Main // int main() { // These examples demonstrate API ergonomics - - auto https = make_https_client(); - auto server = make_basic_server(); - auto mtls = make_mtls_server(); - + try + { + auto https = make_https_client(); + auto server = make_basic_server(); + auto mtls = make_mtls_server(); + (void)https; + (void)server; + (void)mtls; + } + catch( std::exception const& e ) + { + std::cerr << "error: " << e.what() << "\n"; + return 1; + } + return 0; } diff --git a/include/boost/corosio/tls_context.hpp b/include/boost/corosio/tls_context.hpp index 510fd851a..e13b03d7e 100644 --- a/include/boost/corosio/tls_context.hpp +++ b/include/boost/corosio/tls_context.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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) @@ -12,7 +13,9 @@ #include +#include #include +#include #include #include #include @@ -127,6 +130,75 @@ enum class tls_password_purpose class tls_context; +/** A non-owning view of certificate verification state. + + An instance is passed to the callback installed via + tls_context::set_verify_callback during the TLS handshake. It + exposes the backend's native verification handle so the callback + can inspect the certificate and chain currently being verified. + + The value returned by native_handle() is, for the OpenSSL and + WolfSSL backends, an `X509_STORE_CTX*`. For portable inspection that + works across backends (for example certificate pinning), prefer + certificate(), which returns the DER encoding of the certificate + currently being verified. + + @par Lifetime + + The wrapped handle and the certificate() bytes are owned by the TLS + backend and are valid only for the duration of a single callback + invocation. Do not retain them beyond the call. + + @see tls_context::set_verify_callback +*/ +class verify_context +{ + void* handle_; + unsigned char const* der_; + std::size_t der_len_; + +public: + /** Construct from a native handle and the current certificate. + + @param handle The backend verification handle (for OpenSSL and + WolfSSL, an `X509_STORE_CTX*`). + @param der Pointer to the DER encoding of the certificate under + verification, or `nullptr` if unavailable. + @param der_len Length of the DER encoding in bytes. + */ + verify_context( + void* handle, unsigned char const* der, std::size_t der_len) noexcept + : handle_(handle), der_(der), der_len_(der_len) + { + } + + /** Return the native verification handle. + + Cast the result to the backend's verification context type + (e.g. `X509_STORE_CTX*`) to inspect the certificate chain using + backend-specific APIs. + + @return The native handle, or `nullptr` if none is available. + */ + void* native_handle() const noexcept { return handle_; } + + /** Return the DER encoding of the certificate being verified. + + This is the portable way to inspect the peer certificate from a + verification callback: it works identically on every backend, + without depending on backend-specific build options. A DER + certificate is an ASN.1 `SEQUENCE`, so the first byte is `0x30`. + + @return A view of the certificate's DER bytes, valid only for the + duration of the callback. Empty if the certificate is not + available. + */ + std::span certificate() const noexcept + { + return {der_, der_len_}; + } +}; + namespace detail { struct tls_context_data; tls_context_data const& get_tls_context_data(tls_context const&) noexcept; @@ -485,19 +557,21 @@ class BOOST_COROSIO_DECL tls_context /** Add a directory of CA certificates for verification. - Adds a directory containing CA certificate files. Each file must - contain a single certificate in PEM format, named using the - subject name hash (as generated by `openssl rehash` or - `c_rehash`). + Adds a directory of CA certificates to the trust store. The + directory is applied when the native context is first built from + this context. - @param path Path to the directory containing hashed CA certificates. + The expected directory layout depends on the backend. OpenSSL + performs on-demand lookups and requires each certificate file to + be named by its subject-name hash (as generated by + `openssl rehash` or `c_rehash`); WolfSSL loads every certificate + file in the directory. - @return Success, or an error if the directory is invalid. + @param path Path to the directory of CA certificates. - @note Not yet applied by the backends in this release; the - directory is accepted but never loaded. Use - `load_verify_file()` or `add_certificate_authority()` to - supply trust anchors. + @return Success. The path is recorded and applied when the native + context is built; a directory that cannot be read at that time + is skipped rather than reported here. @par Example @code @@ -516,28 +590,33 @@ class BOOST_COROSIO_DECL tls_context recommended approach for HTTPS clients connecting to public servers. - On different platforms this uses: - - Linux: `/etc/ssl/certs` or distribution-specific paths - - macOS: System Keychain - - Windows: Windows Certificate Store + The system store is loaded when the native context is first built + from this context. For a verified-safe client, combine this with + `set_verify_mode( tls_verify_mode::peer )` and, when connecting by + name, `set_hostname()`. - @return Success, or an error if the system store could not be loaded. + @return Success. The request is recorded and applied when the + native context is built; if the system store cannot be loaded + at that time it is skipped rather than reported here, so a + context that must reject unverified peers should also use + `set_verify_mode( tls_verify_mode::peer )`. - @note Not yet applied by the backends in this release. This is a - no-op: the OS trust store is never loaded, so a client that - relies on it has an empty trust store and cannot verify a - public server. To verify a peer today, supply CA certificates - explicitly via `load_verify_file()` or - `add_certificate_authority()`. + @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. @par Example @code // Trust the same CAs as the system ctx.set_default_verify_paths(); + ctx.set_verify_mode( tls_verify_mode::peer ); + ctx.set_hostname( "example.com" ); @endcode @see load_verify_file @see add_verify_path + @see set_verify_mode */ std::error_code set_default_verify_paths(); @@ -680,25 +759,58 @@ class BOOST_COROSIO_DECL tls_context beyond the standard checks and can override verification results. - The callback receives the verification result so far and - information about the certificate being verified. Return - `true` to accept the certificate, `false` to reject. + The callback receives the built-in verification result so far and + a verify_context describing the certificate being verified. Return + `true` to accept the certificate, `false` to reject. Inspect the + certificate portably via `verify_context::certificate()` (its DER + encoding) — for example to pin a specific certificate. + + @par Backend Support + + The exact set of certificates the callback sees differs by backend: + + - OpenSSL: the callback runs once per certificate in the chain, + including certificates that passed the built-in checks. It can + therefore both relax verification (return `true` for a + certificate the library rejected) and tighten it (return `false` + for a certificate the library accepted, e.g. pinning). + - WolfSSL built with `WOLFSSL_ALWAYS_VERIFY_CB` (implied by + `--enable-opensslextra`): same as OpenSSL. + - WolfSSL without that option: the library invokes the callback + only on verification *failure*, so it cannot be honored on a + successful handshake. To avoid silently ignoring a + verification-tightening callback (which would fail open), a + context that carries a callback instead **fails the handshake** + with `std::errc::function_not_supported` on such a build. Rebuild + WolfSSL with `WOLFSSL_ALWAYS_VERIFY_CB`, or omit the callback. @tparam Callback A callable with signature `bool( bool preverified, verify_context& ctx )`. @param callback The verification callback. - @return Success, or an error if the callback could not be set. + @return Success. The callback is recorded here and applied during the + handshake. On a WolfSSL build that cannot honor it, the handshake + fails with `std::errc::function_not_supported` (see Backend + Support). - @note Not yet implemented in this release. This template is - declared but not defined; code that instantiates it fails to - link. Use `set_verify_mode()` with explicitly supplied trust - anchors instead. + @par Example + @code + ctx.set_verify_mode( tls_verify_mode::peer ); + ctx.set_verify_callback( + []( bool preverified, verify_context& ctx ) -> bool + { + if( ! preverified ) + return false; + // Pin: accept only a certificate whose DER matches. + auto der = ctx.certificate(); + return der.size() == expected_pin.size() && + std::equal( der.begin(), der.end(), expected_pin.begin() ); + }); + @endcode - @note The `verify_context` type provides access to the - certificate and chain information. Its exact interface - depends on the TLS backend. + @see verify_context + @see set_verify_mode */ template std::error_code set_verify_callback(Callback callback); @@ -765,6 +877,9 @@ class BOOST_COROSIO_DECL tls_context void set_password_callback_impl( std::function callback); + void set_verify_callback_impl( + std::function callback); + public: // // Revocation Checking @@ -930,6 +1045,14 @@ tls_context::set_password_callback(Callback callback) set_password_callback_impl(std::move(callback)); } +template +std::error_code +tls_context::set_verify_callback(Callback callback) +{ + set_verify_callback_impl(std::move(callback)); + return {}; +} + } // namespace boost::corosio #endif diff --git a/include/boost/corosio/wolfssl_stream.hpp b/include/boost/corosio/wolfssl_stream.hpp index cca36dc0c..4f45bd9c2 100644 --- a/include/boost/corosio/wolfssl_stream.hpp +++ b/include/boost/corosio/wolfssl_stream.hpp @@ -216,6 +216,27 @@ class BOOST_COROSIO_DECL wolfssl_stream final : public tls_stream BOOST_COROSIO_DECL std::error_category const& wolfssl_category() noexcept; +/** Report whether this build's WolfSSL can honor a verify callback. + + A verify callback installed via @ref tls_context::set_verify_callback + can only be honored on a successful handshake when the linked WolfSSL + was built with `WOLFSSL_ALWAYS_VERIFY_CB` (implied by + `--enable-opensslextra`). On a build without it, WolfSSL invokes the + callback only on verification failure, so a callback that tightens + verification would silently fail open; the @ref wolfssl_stream backend + instead fails the handshake with `std::errc::function_not_supported` + when a callback is present. + + This function lets callers detect that situation up front. + + @return `true` if verify callbacks are fully supported by this build, + `false` if installing one will cause the handshake to fail. + + @see tls_context::set_verify_callback +*/ +BOOST_COROSIO_DECL bool +wolfssl_supports_verify_callback() noexcept; + } // namespace boost::corosio #endif diff --git a/src/corosio/src/tls/context.cpp b/src/corosio/src/tls/context.cpp index fc5204515..2b7891113 100644 --- a/src/corosio/src/tls/context.cpp +++ b/src/corosio/src/tls/context.cpp @@ -1,6 +1,7 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) // 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) @@ -217,6 +218,13 @@ tls_context::set_password_callback_impl( impl_->password_callback = std::move(callback); } +void +tls_context::set_verify_callback_impl( + std::function callback) +{ + impl_->verify_callback = std::move(callback); +} + // // Revocation Checking // diff --git a/src/corosio/src/tls/detail/context_impl.hpp b/src/corosio/src/tls/detail/context_impl.hpp index a917032bd..5c08e5f5d 100644 --- a/src/corosio/src/tls/detail/context_impl.hpp +++ b/src/corosio/src/tls/detail/context_impl.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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) @@ -64,7 +65,7 @@ struct tls_context_data tls_verify_mode verification_mode = tls_verify_mode::none; int verify_depth = 100; std::string hostname; - std::function verify_callback; + std::function verify_callback; // SNI (Server Name Indication) diff --git a/src/openssl/src/openssl_stream.cpp b/src/openssl/src/openssl_stream.cpp index 90afd5672..988f6693f 100644 --- a/src/openssl/src/openssl_stream.cpp +++ b/src/openssl/src/openssl_stream.cpp @@ -172,6 +172,38 @@ password_callback(char* buf, int size, int rwflag, void* userdata) return len; } +// Trampoline installed via SSL_CTX_set_verify. Recovers the portable +// context data from the SSL_CTX ex_data (populated for every context), +// wraps the store context, and delegates to the user's callback. +static int +verify_callback_trampoline(int preverified, X509_STORE_CTX* store_ctx) +{ + SSL* ssl = static_cast(X509_STORE_CTX_get_ex_data( + store_ctx, SSL_get_ex_data_X509_STORE_CTX_idx())); + if (!ssl) + return preverified; + + auto* cd = static_cast( + SSL_CTX_get_ex_data(SSL_get_SSL_CTX(ssl), sni_ctx_data_index)); + if (!cd || !cd->verify_callback) + return preverified; + + // Expose the current certificate's DER so the callback can inspect it + // portably. i2d_X509 allocates; free it after the callback returns. + X509* cert = X509_STORE_CTX_get_current_cert(store_ctx); + unsigned char* der = nullptr; + int der_len = cert ? i2d_X509(cert, &der) : 0; + + verify_context vc( + store_ctx, der, + der_len > 0 ? static_cast(der_len) : 0); + bool const ok = cd->verify_callback(preverified != 0, vc); + + if (der) + OPENSSL_free(der); + return ok ? 1 : 0; +} + static int sni_callback(SSL* ssl, int* /* alert */, void* /* arg */) { @@ -228,7 +260,9 @@ class openssl_native_context : public native_context_base else if (cd.verification_mode == tls_verify_mode::require_peer) verify_mode_flag = SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT; - SSL_CTX_set_verify(ctx_, verify_mode_flag, nullptr); + SSL_CTX_set_verify( + ctx_, verify_mode_flag, + cd.verify_callback ? &verify_callback_trampoline : nullptr); if (!cd.entity_certificate.empty()) { @@ -321,6 +355,16 @@ class openssl_native_context : public native_context_base } } + // Trust anchors from the system store and explicit directories. + // Failures leave the affected source unloaded rather than aborting + // context creation; the error queue is cleared so it does not leak + // into a later handshake. + if (cd.use_default_verify_paths) + SSL_CTX_set_default_verify_paths(ctx_); + for (auto const& path : cd.verify_paths) + SSL_CTX_load_verify_locations(ctx_, nullptr, path.c_str()); + ERR_clear_error(); + SSL_CTX_set_verify_depth(ctx_, cd.verify_depth); if (!cd.ciphersuites.empty()) diff --git a/src/wolfssl/src/wolfssl_stream.cpp b/src/wolfssl/src/wolfssl_stream.cpp index abd1e2d67..5c5f6eaca 100644 --- a/src/wolfssl/src/wolfssl_stream.cpp +++ b/src/wolfssl/src/wolfssl_stream.cpp @@ -124,6 +124,16 @@ wolfssl_category() noexcept return instance; } +bool +wolfssl_supports_verify_callback() noexcept +{ +#if defined(WOLFSSL_ALWAYS_VERIFY_CB) + return true; +#else + return false; +#endif +} + // // Native context caching // @@ -153,6 +163,64 @@ wolfssl_sni_callback(WOLFSSL* ssl, int* /* alert */, void* arg) return 0; // Accept } +#if defined(WOLFSSL_ALWAYS_VERIFY_CB) +// WOLFSSL_CTX ex_data slot holding the portable tls_context_data pointer, +// used to reach the user's verify callback from the trampoline. Allocated +// dynamically (see apply_common_settings) rather than hardcoded, because +// slot 0 is the OpenSSL-compat app-data slot. +static int verify_cd_ex_index = -1; + +// Trampoline installed via wolfSSL_CTX_set_verify. +// +// This is only wired up when the linked WolfSSL was built with +// WOLFSSL_ALWAYS_VERIFY_CB (implied by --enable-opensslextra), which makes +// WolfSSL invoke the callback for every certificate including on success. +// Without it, WolfSSL calls the callback only on failure, so a callback +// that tightens verification would silently fail open; on such builds the +// callback is refused entirely (see init_ssl_for_role). +// +// The context data is recovered via the OpenSSL-compatible ex_data chain +// (store -> WOLFSSL -> WOLFSSL_CTX -> ex_data), which those builds provide. +// The native store->userCtx field is NOT used: it is left unset by WolfSSL +// on OPENSSL_EXTRA builds, so relying on it silently drops the callback. +static int +wolfssl_verify_callback(int preverified, WOLFSSL_X509_STORE_CTX* store) +{ + WOLFSSL* ssl = static_cast(wolfSSL_X509_STORE_CTX_get_ex_data( + store, wolfSSL_get_ex_data_X509_STORE_CTX_idx())); + if (!ssl) + return preverified; + + WOLFSSL_CTX* wctx = wolfSSL_get_SSL_CTX(ssl); + auto* cd = wctx ? static_cast( + wolfSSL_CTX_get_ex_data(wctx, verify_cd_ex_index)) + : nullptr; + if (!cd || !cd->verify_callback) + return preverified; + + // Expose the current certificate's DER so the callback can inspect it. + // On OPENSSL_EXTRA builds (the only builds this trampoline runs on) the + // current certificate is available via the compat API; get_der returns a + // pointer into the certificate's own storage (no allocation, valid for + // the callback's duration). + unsigned char const* der = nullptr; + std::size_t der_len = 0; + if (WOLFSSL_X509* cert = wolfSSL_X509_STORE_CTX_get_current_cert(store)) + { + int sz = 0; + unsigned char const* d = wolfSSL_X509_get_der(cert, &sz); + if (d && sz > 0) + { + der = d; + der_len = static_cast(sz); + } + } + + verify_context vc(store, der, der_len); + return cd->verify_callback(preverified != 0, vc) ? 1 : 0; +} +#endif // WOLFSSL_ALWAYS_VERIFY_CB + /** Cached WolfSSL contexts owning WOLFSSL_CTX for client and server. Created on first stream construction for a given tls_context, @@ -179,7 +247,39 @@ class wolfssl_native_context : public native_context_base else if (cd.verification_mode == tls_verify_mode::require_peer) verify_mode_flag = WOLFSSL_VERIFY_PEER | WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT; + + // On capable builds install the verify trampoline and stash the + // context data in the WOLFSSL_CTX ex_data, where the trampoline + // recovers it. On builds that cannot honor the callback the + // handshake is refused in init_ssl_for_role instead. +#if defined(WOLFSSL_ALWAYS_VERIFY_CB) + if (cd.verify_callback) + { + if (verify_cd_ex_index < 0) + { +#if defined(HAVE_EX_DATA_CRYPTO) + // Allocate a dedicated ex_data slot when the API is available. + verify_cd_ex_index = wolfSSL_CTX_get_ex_new_index( + 0, nullptr, nullptr, nullptr, nullptr); +#endif + // wolfSSL_CTX_get_ex_new_index requires HAVE_EX_DATA_CRYPTO, + // which OPENSSL_EXTRA does not imply. Where it is unavailable + // (or returned slot 0, the OpenSSL-compat app-data slot), + // fall back to a fixed non-app-data slot. corosio stores no + // other ex_data on this WOLFSSL_CTX. + if (verify_cd_ex_index <= 0) + verify_cd_ex_index = 1; + } + wolfSSL_CTX_set_ex_data( + ctx, verify_cd_ex_index, const_cast(&cd)); + wolfSSL_CTX_set_verify( + ctx, verify_mode_flag, wolfssl_verify_callback); + } + else + wolfSSL_CTX_set_verify(ctx, verify_mode_flag, nullptr); +#else wolfSSL_CTX_set_verify(ctx, verify_mode_flag, nullptr); +#endif // Apply certificate chain if provided (entity cert + intermediates) // wolfSSL_CTX_use_certificate_chain_buffer loads entity as cert, rest as chain @@ -271,6 +371,16 @@ class wolfssl_native_context : public native_context_base static_cast(ca.size()), WOLFSSL_FILETYPE_PEM); } + // Trust anchors from the system store and explicit directories. + // A failing source is left unloaded rather than aborting context + // creation. +#ifdef WOLFSSL_SYS_CA_CERTS + if (cd.use_default_verify_paths) + wolfSSL_CTX_load_system_CA_certs(ctx); +#endif + for (auto const& path : cd.verify_paths) + wolfSSL_CTX_load_verify_locations(ctx, nullptr, path.c_str()); + // Apply verify depth wolfSSL_CTX_set_verify_depth(ctx, cd.verify_depth); } @@ -1022,6 +1132,30 @@ struct wolfssl_stream::impl wolfSSL_SetIOReadCtx(ssl_, this); wolfSSL_SetIOWriteCtx(ssl_, this); + // Verify callback handling. + if (cd.verify_callback) + { +#if defined(WOLFSSL_ALWAYS_VERIFY_CB) + // Capable build: the trampoline and its context data are already + // installed on the WOLFSSL_CTX (see apply_common_settings); the + // session inherits them. Nothing to do per-session. +#else + // This WolfSSL build invokes the verify callback only on failure, + // never on success, so a callback that tightens verification + // (e.g. certificate pinning) would silently fail open. Rather + // than mislead, refuse the connection. Rebuild WolfSSL with + // WOLFSSL_ALWAYS_VERIFY_CB (e.g. --enable-opensslextra) to use + // verify callbacks, or omit the callback. + // + // Free the session first: leaving ssl_ non-null would let a + // retried handshake() pass the `if (ssl_) return {}` sentinel and + // proceed on this unconfigured (and unsupported) session. + wolfSSL_free(ssl_); + ssl_ = nullptr; + return std::make_error_code(std::errc::function_not_supported); +#endif + } + // Apply per-session config (SNI + hostname verification) from context if (type == wolfssl_stream::client && !cd.hostname.empty()) { diff --git a/test/unit/openssl_stream.cpp b/test/unit/openssl_stream.cpp index 9b8282746..674c1924f 100644 --- a/test/unit/openssl_stream.cpp +++ b/test/unit/openssl_stream.cpp @@ -15,6 +15,9 @@ #ifdef BOOST_COROSIO_HAS_OPENSSL +#include +#include + namespace boost::corosio { // Callable wrapper for passing to test helper templates @@ -140,6 +143,65 @@ struct openssl_stream_test } } + /** Test that set_default_verify_paths() is applied without breaking + context creation. + + Behaviorally exercising the system trust store would require + redirecting it (SSL_CERT_FILE), which is not portable across the CI + matrix, so this only asserts the call path runs cleanly: a client + that trusts the CA explicitly *and* calls set_default_verify_paths() + still completes the handshake. The load-from-path mechanism is + covered behaviorally by testAddVerifyPath(). + */ + void testDefaultVerifyPaths() + { + using namespace test; + + io_context ioc; + auto client_ctx = make_client_context(); + // Adding the system store on top of the explicit CA must not break + // context creation or verification. + client_ctx.set_default_verify_paths(); // NOLINT(bugprone-unused-return-value) + + auto server_ctx = make_server_context(); + run_tls_test(ioc, client_ctx, server_ctx, make_stream, make_stream); + } + + /** Test that add_verify_path() loads CAs from a hashed directory. + + OpenSSL CApath lookups require files named by subject-name hash (as + produced by `openssl rehash`). The test CA's subject hash is a + constant for OpenSSL 1.0.0+ (SHA-1 over the canonical subject), so + the filename is hardcoded here to avoid linking the test against + libcrypto. Recompute with `openssl x509 -in ca.pem -hash -noout` if + the test certificate changes. + */ + void testAddVerifyPath() + { + using namespace test; + + auto dir = std::filesystem::temp_directory_path() / + "corosio_test_capath"; + std::filesystem::create_directories(dir); + auto ca_file = dir / "d13e2296.0"; // subject hash of ca_cert_pem + { + std::ofstream out(ca_file, std::ios::binary); + out << ca_cert_pem; + } + + { + io_context ioc; + tls_context client_ctx; + client_ctx.add_verify_path(dir.string()); // NOLINT(bugprone-unused-return-value) + client_ctx.set_verify_mode(tls_verify_mode::peer); // NOLINT(bugprone-unused-return-value) + + auto server_ctx = make_server_context(); + run_tls_test(ioc, client_ctx, server_ctx, make_stream, make_stream); + } + + std::filesystem::remove(ca_file); + } + void run() { test::testHandshakeFuse(make_stream); @@ -154,6 +216,8 @@ struct openssl_stream_test test::testCertificateValidation(make_stream); test::testSni(make_stream); test::testSniCallback(make_stream); + test::testVerifyCallback(make_stream); + test::testVerifyCallbackOnSuccess(make_stream); test::testMtls(make_stream); test::testMoveSemantics(make_stream); test::testAbruptClose(make_stream); @@ -166,6 +230,8 @@ struct openssl_stream_test testCertificateChain(); testErrorCategory(); + testDefaultVerifyPaths(); + testAddVerifyPath(); testName(); testNextLayer(); } diff --git a/test/unit/tls_stream_tests.hpp b/test/unit/tls_stream_tests.hpp index 58a767b08..6b213cedb 100644 --- a/test/unit/tls_stream_tests.hpp +++ b/test/unit/tls_stream_tests.hpp @@ -1,5 +1,6 @@ // // Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com) +// 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) @@ -467,6 +468,207 @@ testSniCallback(StreamFactory make_stream) } } +/** Test the certificate verification callback (portable contract). + + Exercises the guarantees that hold on every backend that supports the + callback: it is consulted when the backend's built-in verification + fails, its return value determines the outcome, and it can inspect the + certificate's DER portably. + + 1. Override accept: an untrusted CA would normally fail, but a + callback returning true completes the handshake, and the + callback observes preverified == false with a usable native + handle and DER bytes. + 2. Decline: an untrusted CA with a callback returning false leaves + the handshake failed. + + @param callback_supported Whether this backend build can honor a + verify callback. Pass `false` for builds that cannot invoke the + callback on a successful handshake (e.g. WolfSSL without + WOLFSSL_ALWAYS_VERIFY_CB): installing a callback must then fail + the handshake with `std::errc::function_not_supported` rather than + silently ignore it (which would let a tightening callback fail + open). +*/ +template +void +testVerifyCallback(StreamFactory make_stream, bool callback_supported = true) +{ + if (!callback_supported) + { + // Fail-closed: a context carrying a verify_callback must fail the + // handshake with a clear error, never silently accept. + { + io_context ioc; + auto client_ctx = make_client_context(); + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.set_verify_callback( + [](bool preverified, verify_context&) -> bool { + return preverified; + }); + auto server_ctx = make_server_context(); + std::error_code client_ec; + run_tls_test_fail( + ioc, client_ctx, server_ctx, make_stream, make_stream, + &client_ec); + BOOST_TEST(client_ec == std::errc::function_not_supported); + } + + // A retried handshake on the same stream must stay fail-closed. The + // fail-closed path returns before any I/O, so this needs no peer; + // it is a regression guard against leaving the native session + // non-null (which would let the retry bypass the check). + { + io_context ioc; + auto [m1, m2] = corosio::test::make_mocket_pair(ioc); + (void)m2; + auto client_ctx = make_client_context(); + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.set_verify_callback( + [](bool preverified, verify_context&) -> bool { + return preverified; + }); + auto client = make_stream(m1, client_ctx); + using stream_type = std::remove_reference_t; + + std::error_code ec1; + std::error_code ec2; + auto attempt = [&](std::error_code& out) -> capy::task<> { + auto [ec] = co_await client.handshake(stream_type::client); + out = ec; + }; + capy::run_async(ioc.get_executor())(attempt(ec1)); + ioc.run(); + ioc.restart(); + capy::run_async(ioc.get_executor())(attempt(ec2)); + ioc.run(); + + BOOST_TEST(ec1 == std::errc::function_not_supported); + BOOST_TEST(ec2 == std::errc::function_not_supported); + + m1.close(); // NOLINT(bugprone-unused-return-value) + } + return; + } + + // 1. Override accept: untrusted CA (preverified == false) but callback + // returns true -> handshake succeeds. Without the callback this + // configuration fails, so success proves the callback is consulted + // and its return value controls the result. + { + io_context ioc; + bool saw_unverified = false; + + auto client_ctx = make_wrong_ca_context(); + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.set_verify_callback( + [&saw_unverified](bool preverified, verify_context& vc) -> bool { + if (!preverified) + { + saw_unverified = true; + BOOST_TEST(vc.native_handle() != nullptr); + // Portable certificate access: the callback must see the + // DER bytes of the cert under verification on both + // backends. A DER certificate is an ASN.1 SEQUENCE, so + // the first byte is 0x30. + auto der = vc.certificate(); + BOOST_TEST(!der.empty()); + if (!der.empty()) + BOOST_TEST(der[0] == 0x30); + } + return true; + }); + + auto server_ctx = make_server_context(); + run_tls_test(ioc, client_ctx, server_ctx, make_stream, make_stream); + BOOST_TEST(saw_unverified); + } + + // 2. Decline: untrusted CA and callback returns false -> stays failed. + { + io_context ioc; + auto client_ctx = make_wrong_ca_context(); + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.set_verify_callback( + [](bool, verify_context&) -> bool { return false; }); + + auto server_ctx = make_server_context(); + run_tls_test_fail( + ioc, client_ctx, server_ctx, make_stream, make_stream); + } +} + +/** Test that the verify callback also runs on successful verification. + + Backends that support the full callback contract (OpenSSL always; + WolfSSL with WOLFSSL_ALWAYS_VERIFY_CB) invoke it for a certificate that + passed the built-in checks, which lets a callback reject an otherwise + valid certificate (e.g. for pinning) and inspect it on the success + path. Only call this for backends where the callback is fully + supported. +*/ +template +void +testVerifyCallbackOnSuccess(StreamFactory make_stream) +{ + // Invoked on success: trusted CA + pass-through callback succeeds, and + // the certificate is inspectable on the success path. + { + io_context ioc; + bool invoked = false; + bool saw_cert = false; + + auto client_ctx = make_client_context(); + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.set_verify_callback( + [&](bool preverified, verify_context& vc) -> bool { + invoked = true; + if (preverified && !vc.certificate().empty() && + vc.certificate()[0] == 0x30) + saw_cert = true; + return preverified; + }); + + auto server_ctx = make_server_context(); + run_tls_test(ioc, client_ctx, server_ctx, make_stream, make_stream); + BOOST_TEST(invoked); + BOOST_TEST(saw_cert); + } + + // Reject a valid cert: trusted CA but callback returns false -> fail. + { + io_context ioc; + auto client_ctx = make_client_context(); + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.set_verify_callback( + [](bool, verify_context&) -> bool { return false; }); + + auto server_ctx = make_server_context(); + run_tls_test_fail( + ioc, client_ctx, server_ctx, make_stream, make_stream); + } + + // Content-based pinning: reject unless the leaf DER matches an expected + // pin. A deliberately-wrong pin must fail the handshake even though the + // chain is otherwise valid. + { + io_context ioc; + auto client_ctx = make_client_context(); + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.set_verify_callback( + [](bool preverified, verify_context& vc) -> bool { + if (!preverified) + return false; + auto der = vc.certificate(); + return der.size() == 1 && der[0] == 0xFF; // never matches + }); + + auto server_ctx = make_server_context(); + run_tls_test_fail( + ioc, client_ctx, server_ctx, make_stream, make_stream); + } +} + /** Test move construction and move assignment of a live stream. The moved-into stream must keep working after a completed diff --git a/test/unit/wolfssl_stream.cpp b/test/unit/wolfssl_stream.cpp index 91ab37cf8..239813e13 100644 --- a/test/unit/wolfssl_stream.cpp +++ b/test/unit/wolfssl_stream.cpp @@ -21,6 +21,9 @@ #ifdef BOOST_COROSIO_HAS_WOLFSSL +#include +#include + namespace boost::corosio { // Callable wrapper for passing to test helper templates @@ -136,6 +139,40 @@ struct wolfssl_stream_test } } + /** Test that add_verify_path() loads CAs from a directory. + + WolfSSL loads every certificate file in the directory, so no + hash-based naming is required. A client that trusts the CA only + through add_verify_path() must be able to verify the server. + */ + void testAddVerifyPath() + { + using namespace test; + + auto dir = std::filesystem::temp_directory_path() / + "corosio_wolfssl_capath"; + std::filesystem::create_directories(dir); + auto ca_file = dir / "test_ca.pem"; + { + std::ofstream out(ca_file, std::ios::binary); + out << ca_cert_pem; + } + + { + io_context ioc; + tls_context client_ctx; + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.add_verify_path(dir.string()); + // NOLINTNEXTLINE(bugprone-unused-return-value) + client_ctx.set_verify_mode(tls_verify_mode::peer); + + auto server_ctx = make_server_context(); + run_tls_test(ioc, client_ctx, server_ctx, make_stream, make_stream); + } + + std::filesystem::remove(ca_file); + } + void run() { test::testHandshakeFuse(make_stream); @@ -152,6 +189,22 @@ struct wolfssl_stream_test test::testCertificateValidation(make_stream); test::testSni(make_stream); test::testSniCallback(make_stream); + // Whether the linked WolfSSL can honor a verify callback on success + // (WOLFSSL_ALWAYS_VERIFY_CB) is a build-time property, queried here + // at runtime so the test needs no WolfSSL headers. + if (wolfssl_supports_verify_callback()) + { + // Capable build: full OpenSSL-parity semantics (override, + // decline, inspect, tighten). + test::testVerifyCallback(make_stream, /*callback_supported=*/true); + test::testVerifyCallbackOnSuccess(make_stream); + } + else + { + // Minimal build: the callback cannot fire on success, so corosio + // fails closed rather than let a tightening callback fail open. + test::testVerifyCallback(make_stream, /*callback_supported=*/false); + } test::testMtls(make_stream); test::testMoveSemantics(make_stream); test::testAbruptClose(make_stream); @@ -166,6 +219,7 @@ struct wolfssl_stream_test testCertificateChain(); testErrorCategory(); + testAddVerifyPath(); testName(); testNextLayer(); }