Skip to content

Commit f820453

Browse files
committed
fix(tls): address PR #311 review (crl der/fail-closed, asan, docs)
Four review findings on the TLS-gaps work: 1. use_pkcs12_file assigned its password with std::string(passphrase), reintroducing the one-past-the-end pointer that trips ASan detect_invalid_pointer_pairs on a non-null-terminated view (the same bug just fixed in use_pkcs12). Use assign(ptr, len) to match. 2. CRL loading was PEM-only and dropped parse failures silently, so a DER or malformed CRL vanished — and under soft_fail a peer the missing CRL might have revoked was then accepted (fail-open), contradicting the "PEM or DER" contract. Both backends now try PEM then DER, and a CRL that parses as neither fails the handshake closed (OpenSSL in do_handshake, WolfSSL in init_ssl_for_role). New generic test asserts a malformed CRL under soft_fail fails rather than silently accepting. 3. wolfssl_stream comment claimed PKCS#12 CA/chain entries "are not loaded"; the code loads and sends them (and testPkcs12Chain proves it). Comment corrected. 4. Added the "Copyright (c) 2026 Michael Vandeberg" line to two files substantially modified in this PR (test/unit/tls_context.cpp, include/boost/corosio/tls_stream.hpp) per the repo convention. Verified on system WolfSSL (HAVE_CRL off), a vcpkg-flag WolfSSL 5.8.2 build (HAVE_CRL on), and the asan+ubsan config with the CI ASAN_OPTIONS: all TLS suites pass.
1 parent 513a334 commit f820453

6 files changed

Lines changed: 100 additions & 13 deletions

File tree

‎include/boost/corosio/tls_stream.hpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//
22
// Copyright (c) 2025 Vinnie Falco (vinnie.falco@gmail.com)
3+
// Copyright (c) 2026 Michael Vandeberg
34
//
45
// Distributed under the Boost Software License, Version 1.0. (See accompanying
56
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

‎src/corosio/src/tls/context.cpp‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,12 @@ tls_context::use_pkcs12_file(
117117

118118
std::ostringstream ss;
119119
ss << file.rdbuf();
120-
impl_->pkcs12_data = ss.str();
121-
impl_->pkcs12_password = std::string(passphrase);
120+
impl_->pkcs12_data = ss.str();
121+
// assign(ptr, len), not std::string(passphrase): see the note in
122+
// use_pkcs12 — constructing from a string_view forms a one-past-the-end
123+
// pointer that ASan's detect_invalid_pointer_pairs rejects on a
124+
// non-null-terminated view.
125+
impl_->pkcs12_password.assign(passphrase.data(), passphrase.size());
122126
return {};
123127
}
124128

‎src/openssl/src/openssl_stream.cpp‎

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -314,6 +314,10 @@ class openssl_native_context : public native_context_base
314314
public:
315315
SSL_CTX* ctx_;
316316
tls_context_data const* cd_;
317+
// Set when a caller-supplied CRL could not be parsed as PEM or DER.
318+
// A dropped CRL would silently weaken revocation (fail-open under
319+
// soft_fail), so do_handshake refuses the handshake instead.
320+
bool crl_parse_failed_ = false;
317321

318322
explicit openssl_native_context(tls_context_data const& cd)
319323
: ctx_(nullptr)
@@ -514,19 +518,36 @@ class openssl_native_context : public native_context_base
514518
// error.
515519
if (cd.revocation != tls_revocation_policy::disabled)
516520
{
517-
for (auto const& crl_pem : cd.crls)
521+
for (auto const& crl_data : cd.crls)
518522
{
519523
BIO* bio = BIO_new_mem_buf(
520-
crl_pem.data(), static_cast<int>(crl_pem.size()));
524+
crl_data.data(), static_cast<int>(crl_data.size()));
521525
if (!bio)
526+
{
527+
crl_parse_failed_ = true;
522528
continue;
529+
}
530+
// Accept PEM or DER (the documented contract). Try PEM first,
531+
// then rewind and try DER.
523532
X509_CRL* crl =
524533
PEM_read_bio_X509_CRL(bio, nullptr, nullptr, nullptr);
534+
if (!crl)
535+
{
536+
BIO_reset(bio);
537+
crl = d2i_X509_CRL_bio(bio, nullptr);
538+
}
525539
if (crl)
526540
{
527541
X509_STORE_add_crl(store, crl);
528542
X509_CRL_free(crl);
529543
}
544+
else
545+
{
546+
// A supplied CRL that parses as neither PEM nor DER must
547+
// not be silently dropped; record it so the handshake
548+
// fails closed rather than weakening revocation.
549+
crl_parse_failed_ = true;
550+
}
530551
BIO_free(bio);
531552
}
532553
X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
@@ -554,12 +575,18 @@ class openssl_native_context : public native_context_base
554575
}
555576
};
556577

557-
inline SSL_CTX*
558-
get_openssl_context(tls_context_data const& cd)
578+
inline openssl_native_context*
579+
get_openssl_native_context(tls_context_data const& cd)
559580
{
560581
static char key;
561582
auto* p = cd.find(&key, [&] { return new openssl_native_context(cd); });
562-
return static_cast<openssl_native_context*>(p)->ctx_;
583+
return static_cast<openssl_native_context*>(p);
584+
}
585+
586+
SSL_CTX*
587+
get_openssl_context(tls_context_data const& cd)
588+
{
589+
return get_openssl_native_context(cd)->ctx_;
563590
}
564591

565592
} // namespace detail
@@ -822,6 +849,14 @@ struct openssl_stream::impl
822849

823850
capy::io_task<> do_handshake(int type)
824851
{
852+
// A caller-supplied CRL that parsed as neither PEM nor DER weakens
853+
// revocation. Refuse the handshake rather than fail open (soft_fail
854+
// would otherwise treat the dropped CRL as "status unknown").
855+
if (detail::get_openssl_native_context(
856+
detail::get_tls_context_data(ctx_))
857+
->crl_parse_failed_)
858+
co_return std::make_error_code(std::errc::invalid_argument);
859+
825860
if (used_)
826861
reset();
827862

‎src/wolfssl/src/wolfssl_stream.cpp‎

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -295,8 +295,12 @@ class wolfssl_native_context : public native_context_base
295295
public:
296296
WOLFSSL_CTX* client_ctx_;
297297
WOLFSSL_CTX* server_ctx_;
298+
// Set when a caller-supplied CRL could not be parsed as PEM or DER, so
299+
// init_ssl_for_role can fail closed rather than silently weaken
300+
// revocation (fail-open under soft_fail).
301+
bool crl_parse_failed_ = false;
298302

299-
static void
303+
void
300304
apply_common_settings(WOLFSSL_CTX* ctx, tls_context_data const& cd)
301305
{
302306
if (!ctx)
@@ -345,7 +349,8 @@ class wolfssl_native_context : public native_context_base
345349

346350
// PKCS#12 bundle: decode cert + key with the native wolfcrypt API
347351
// (the wolfSSL_d2i_PKCS12_bio wrapper needs OPENSSL_EXTRA). CA/chain
348-
// entries inside the bundle are not loaded.
352+
// entries inside the bundle are loaded and sent during the handshake
353+
// (see below), matching the OpenSSL backend.
349354
if (!cd.pkcs12_data.empty())
350355
{
351356
WC_PKCS12* p12 = wc_PKCS12_new();
@@ -530,9 +535,19 @@ class wolfssl_native_context : public native_context_base
530535
{
531536
wolfSSL_CTX_EnableCRL(ctx, WOLFSSL_CRL_CHECK);
532537
for (auto const& crl : cd.crls)
533-
wolfSSL_CTX_LoadCRLBuffer(
534-
ctx, reinterpret_cast<unsigned char const*>(crl.data()),
535-
static_cast<long>(crl.size()), WOLFSSL_FILETYPE_PEM);
538+
{
539+
auto const* buf =
540+
reinterpret_cast<unsigned char const*>(crl.data());
541+
auto const sz = static_cast<long>(crl.size());
542+
// Accept PEM or DER (the documented contract): try PEM, then
543+
// DER. A supplied CRL that parses as neither must not be
544+
// silently dropped, so record it for a fail-closed handshake.
545+
if (wolfSSL_CTX_LoadCRLBuffer(
546+
ctx, buf, sz, WOLFSSL_FILETYPE_PEM) != WOLFSSL_SUCCESS &&
547+
wolfSSL_CTX_LoadCRLBuffer(
548+
ctx, buf, sz, WOLFSSL_FILETYPE_ASN1) != WOLFSSL_SUCCESS)
549+
crl_parse_failed_ = true;
550+
}
536551
// soft_fail tolerates an undeterminable revocation status (no CRL
537552
// loaded for a cert) the way OpenSSL does. Without this, WolfSSL's
538553
// WOLFSSL_CRL_CHECK hard-fails with CRL_MISSING; the callback
@@ -1285,6 +1300,12 @@ struct wolfssl_stream::impl
12851300
wolfSSL_get_error(nullptr, 0), wolfssl_category());
12861301
}
12871302

1303+
// A caller-supplied CRL that could not be parsed (neither PEM nor
1304+
// DER) must not silently weaken revocation. Fail closed rather than
1305+
// let soft_fail accept a cert the dropped CRL might have revoked.
1306+
if (native->crl_parse_failed_)
1307+
return std::make_error_code(std::errc::invalid_argument);
1308+
12881309
// Select appropriate context based on role
12891310
WOLFSSL_CTX* native_ctx = (type == wolfssl_stream::client)
12901311
? native->client_ctx_

‎test/unit/tls_context.cpp‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//
22
// Copyright (c) 2026 Steve Gerbino
3+
// Copyright (c) 2026 Michael Vandeberg
34
//
45
// Distributed under the Boost Software License, Version 1.0. (See accompanying
56
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)

‎test/unit/tls_stream_tests.hpp‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,11 +490,15 @@ testAlpnAccessorEmpty(StreamFactory make_stream)
490490
491491
The server presents a leaf that a CRL revokes.
492492
493-
When @p crl_supported (OpenSSL):
493+
When @p crl_supported (OpenSSL, or WolfSSL built with HAVE_CRL):
494494
1. hard_fail with the CRL loaded rejects the revoked leaf.
495495
2. soft_fail with no CRL loaded accepts (status unknown is allowed).
496496
Otherwise (WolfSSL without HAVE_CRL): any revocation request fails the
497497
handshake with function_not_supported rather than skip the check.
498+
499+
On every build, a CRL that parses as neither PEM nor DER fails the
500+
handshake closed rather than being silently dropped (which would let
501+
soft_fail accept a peer the missing CRL might have revoked).
498502
*/
499503
template<typename StreamFactory>
500504
void
@@ -552,6 +556,27 @@ testCrlRevocation(StreamFactory make_stream, bool crl_supported)
552556
run_tls_test_fail(
553557
ioc, client_ctx, server_ctx, make_stream, make_stream);
554558
}
559+
560+
// A supplied CRL that parses as neither PEM nor DER must fail the
561+
// handshake, never silently downgrade to accepting the peer. This holds
562+
// on every build: a HAVE_CRL backend rejects the unparseable CRL, and a
563+
// backend without CRL support rejects any revocation request outright.
564+
// Uses soft_fail specifically: without the fail-closed guard, soft_fail
565+
// would treat the missing (dropped) CRL as "status unknown" and accept.
566+
{
567+
io_context ioc;
568+
tls_context client_ctx;
569+
// NOLINTNEXTLINE(bugprone-unused-return-value)
570+
client_ctx.add_certificate_authority(root_ca_cert_pem);
571+
// NOLINTNEXTLINE(bugprone-unused-return-value)
572+
client_ctx.set_verify_mode(tls_verify_mode::peer);
573+
// NOLINTNEXTLINE(bugprone-unused-return-value)
574+
client_ctx.add_crl("this is not a valid PEM or DER CRL");
575+
client_ctx.set_revocation_policy(tls_revocation_policy::soft_fail);
576+
auto server_ctx = revoked_server();
577+
run_tls_test_fail(
578+
ioc, client_ctx, server_ctx, make_stream, make_stream);
579+
}
555580
}
556581

557582
/** Test loading server credentials from a PKCS#12 bundle.

0 commit comments

Comments
 (0)