Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
aaab091
Add LoopExecutor
eddyashton Jun 23, 2026
113d71c
checkpoint: OpenSSL-native transport subsystem (green) before RPC cut…
eddyashton Jun 24, 2026
b543bf7
WIP: OpenSSL-native RPC cutover - sessions plaintext+SessionWriter, t…
eddyashton Jun 24, 2026
aea2b27
OpenSSL-native RPC cutover: relocate RPC listening into enclave lib, …
eddyashton Jun 24, 2026
45a1a27
Wire outbound client TLS (peer CA verification + client cert via tls:…
eddyashton Jun 24, 2026
82fc341
Listener resolves bind address via getaddrinfo (hostnames + IPv6), fi…
eddyashton Jun 25, 2026
bbb9d3b
Graceful connection close: flush buffered output before teardown so a…
eddyashton Jun 25, 2026
8d81979
Fix spurious connection closes: clear OpenSSL thread-local error queu…
eddyashton Jun 25, 2026
592fc5f
Restore UDP echo via DatagramServer (UDP socket in epoll loop) + echo…
eddyashton Jun 25, 2026
d9f0a51
Format pass
eddyashton Jun 26, 2026
396cfa8
Cleanup: remove dead code from cutover - LoopExecutor (unused), orpha…
eddyashton Jun 26, 2026
50c47b7
Removed unused SNI
eddyashton Jun 30, 2026
d2a3b8a
Remove unused function
eddyashton Jun 30, 2026
55422b6
Idle connection timeout: move idle_connection_timeout HostConfig->CCF…
eddyashton Jun 30, 2026
6591f98
Restore custom protocol sessions on OpenSSL RPC path
eddyashton Jun 30, 2026
a0a4972
Format
eddyashton Jun 30, 2026
61be04b
Fix RPC session metrics for session-initiated closes
eddyashton Jun 30, 2026
3d5b4c2
Address RPC transport review feedback
eddyashton Jul 1, 2026
66fbfb5
Format
eddyashton Jul 1, 2026
6f38be3
Update comments
eddyashton Jul 1, 2026
e15f4c4
buh
eddyashton Jul 1, 2026
5ca4413
Handle open ledger chunks in SNP recovery checks
eddyashton Jul 1, 2026
ebf2bd8
Fix JWT tests - prefer to bind on IPv4
eddyashton Jul 1, 2026
d0dbfdf
Format
eddyashton Jul 1, 2026
eb982cf
Stop RPC transports before host shutdown
eddyashton Jul 1, 2026
c41e1c3
Report outbound client connection closes
eddyashton Jul 2, 2026
59da3e7
Add some debug logging
eddyashton Jul 6, 2026
eb18b7d
Remove debug
eddyashton Jul 23, 2026
0a42036
Remove more TEMP debug logging
eddyashton Jul 24, 2026
b539ff4
Add CURLE_SSL_CONNECT_ERROR to transient transport error classifications
eddyashton Jul 24, 2026
05da0bc
Merge cleanup N
eddyashton Jul 30, 2026
8866b05
Restore explicit-port-before-ephemeral RPC interface bind ordering
eddyashton Jul 30, 2026
f70101a
Merge branch 'main' of https://github.com/microsoft/CCF into rpc_conn…
eddyashton Aug 4, 2026
f7ee1ff
Better context management - an AL4 fix
eddyashton Aug 4, 2026
7b83c5e
Merge branch 'main' into rpc_connection_manager
eddyashton Aug 4, 2026
97ca674
Refactor HTTP session classes to directly inherit from ccf::Plaintext…
eddyashton Aug 4, 2026
3de7b3f
Rewrite docs, audit compat with previous
eddyashton Aug 4, 2026
43b55d4
Remove some dead code
eddyashton Aug 4, 2026
77d8aa3
More unification and cleanup
eddyashton Aug 4, 2026
46b59b6
Merge branch 'main' of https://github.com/microsoft/CCF into rpc_conn…
eddyashton Aug 4, 2026
eee4afe
Merge branch 'main' into rpc_connection_manager
achamayou Aug 5, 2026
3bd9f7e
Use uv_poll_t rather than a custom epoll loop
eddyashton Aug 5, 2026
8b6a09c
Merge branch 'main' of https://github.com/microsoft/CCF into rpc_conn…
eddyashton Aug 5, 2026
cb13dc2
Merge branch 'rpc_connection_manager' of https://github.com/microsoft…
eddyashton Aug 5, 2026
6834e42
Tidy
eddyashton Aug 5, 2026
c1f13d0
TSAN the unit test
eddyashton Aug 5, 2026
511532f
Tidier
eddyashton Aug 5, 2026
72c1b22
Handle empty port case in OpenSSL and Datagram server initialization
eddyashton Aug 5, 2026
e2d94ca
Disable Nagle on inbound RPC connections
Aug 5, 2026
4da54d7
Merge branch 'main' into rpc_connection_manager
achamayou Aug 6, 2026
ab55b28
Fix thread dispatch
eddyashton Aug 6, 2026
8a810fe
Merge branch 'rpc_connection_manager' of https://github.com/microsoft…
eddyashton Aug 6, 2026
85b11a4
Review tidying
eddyashton Aug 6, 2026
c50ab93
More fixes? Impossible to know at this point
eddyashton Aug 6, 2026
0a3a178
Merge branch 'main' into rpc_connection_manager
achamayou Aug 6, 2026
46a4df2
Reduce copies
eddyashton Aug 7, 2026
71b8c53
Merge branch 'main' into rpc_connection_manager
achamayou Aug 7, 2026
d7d2ea4
Restore inbound backpressure
eddyashton Aug 7, 2026
4e96084
More review tidy-up
eddyashton Aug 7, 2026
3769e9e
Merge branch 'rpc_connection_manager' of https://github.com/microsoft…
eddyashton Aug 10, 2026
9ad1eb0
Merge branch 'main' of https://github.com/microsoft/CCF into rpc_conn…
eddyashton Aug 10, 2026
0af7572
More review feedback cleanup
eddyashton Aug 10, 2026
2fd0a95
Format and pyproject version
eddyashton Aug 10, 2026
c82475f
Wake zero-worker enclaves for queued TLS work
eddyashton Aug 11, 2026
8ae80ff
Merge branch 'main' into rpc_connection_manager
eddyashton Aug 11, 2026
66371ba
Update changelog and improve OpenSSL server tests and header file
eddyashton Aug 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## [7.0.13]

[7.0.13]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.13

### Changed

- TLS is now terminated by OpenSSL directly on the socket, rather than being relayed over the ringbuffer and decrypted through a memory BIO. The session interfaces in `include/ccf/node/session.h` and `include/ccf/research/custom_protocol_subsystem_interface.h` have changed shape accordingly: a session now receives and emits plaintext, taking ownership of the inbound buffer, and writes its output through a `ccf::SessionWriter` rather than a `tls::Context` (#8117).

### Fixed

- TLS interfaces without a configured certificate now remain unlistening until a certificate is supplied, so clients receive an immediate connection refusal rather than entering an unusable TLS connection (#8117).
- Nodes configured with zero worker threads now wake the enclave main loop immediately when the OpenSSL transport queues work, rather than waiting for the next tick or polling timeout (#8117).

## [7.0.12]

[7.0.12]: https://github.com/microsoft/CCF/releases/tag/ccf-7.0.12
Expand Down
17 changes: 10 additions & 7 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -715,10 +715,14 @@ if(BUILD_TESTS)
)

add_unit_test(
rpc_connections_test
${CMAKE_CURRENT_SOURCE_DIR}/src/host/test/rpc_connections.cpp
openssl_server_test
${CMAKE_CURRENT_SOURCE_DIR}/src/host/test/openssl_server_test.cpp
)
target_link_libraries(openssl_server_test PRIVATE ccf_tasks uv)
target_compile_definitions(
openssl_server_test
PRIVATE TEST_HYBRID_TLS_GROUPS=$<BOOL:${TEST_HYBRID_TLS_GROUPS}>
)
target_link_libraries(rpc_connections_test PRIVATE uv)

add_unit_test(
raft_test
Expand Down Expand Up @@ -874,12 +878,11 @@ if(BUILD_TESTS)
${CMAKE_CURRENT_SOURCE_DIR}/src/node/test/node_info_json.cpp
)

add_unit_test(tls_test ${CMAKE_CURRENT_SOURCE_DIR}/src/tls/test/main.cpp)
target_link_libraries(tls_test PRIVATE ${CMAKE_THREAD_LIBS_INIT})
target_compile_definitions(
add_unit_test(
tls_test
PRIVATE TEST_HYBRID_TLS_GROUPS=$<BOOL:${TEST_HYBRID_TLS_GROUPS}>
${CMAKE_CURRENT_SOURCE_DIR}/src/clients/tls/test/main.cpp
)
target_link_libraries(tls_test PRIVATE ${CMAKE_THREAD_LIBS_INIT})

add_unit_test(
base64_test
Expand Down
166 changes: 108 additions & 58 deletions doc/architecture/tls_internals.rst

Large diffs are not rendered by default.

9 changes: 5 additions & 4 deletions doc/contribute/onboarding.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,12 +63,13 @@ Note that this diagram deliberately does not represent host-to-enclave communica
.. mermaid::

flowchart TB
Client[HTTPS/1.1 Client <a href='../build_apps/auth/index.html'>auth</a>] -- TLS 1.2 or 1.3 --> TLSSession
TLSSession[TLS Session <a href='https://github.com/microsoft/CCF/blob/main/src/enclave/tls_session.h'>src</a>] -- PlainText --> HTTPSession
Client[HTTPS/1.1 Client <a href='../build_apps/auth/index.html'>auth</a>] -- TLS 1.2 or 1.3 --> TLSConnection
TLSConnection[TLS Connection <a href='https://github.com/microsoft/CCF/blob/main/src/host/tls/openssl_server.h'>src</a>] -- PlainText --> HTTPSession
HTTPSession -- PlainText --> SessionWriter[Session Writer <a href='https://github.com/microsoft/CCF/blob/main/src/enclave/session_writer.h'>src</a>]
SessionWriter --> TLSConnection
HTTPSession[HTTP Session <a href='https://github.com/microsoft/CCF/blob/main/src/http/http_session.h'>src</a>] -- Request --> Endpoint[Application Endpoint <a href='../build_apps/api.html#application-endpoint-registration'>doc</a>]
Endpoint -- Response --> HTTPSession
HTTPSession --> TLSSession
TLSSession --> Client
TLSConnection --> Client
Endpoint -- WriteSet --> Store[Store <a href='../build_apps/kv/index.html'>doc</a>]
Store -- LedgerEntry --> Ledger[Ledger <a href='../architecture/ledger.html'>doc</a>]
Ledger -- LedgerEntry --> Disk
Expand Down
51 changes: 49 additions & 2 deletions include/ccf/node/session.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
#pragma once

#include <cstdint>
#include <span>
#include <vector>

namespace ccf
{
Expand All @@ -12,8 +12,55 @@ namespace ccf
public:
virtual ~Session() = default;

virtual void handle_incoming_data(std::span<const uint8_t> data) = 0;
// Inbound bytes for this session. Ownership is transferred, so that the
// buffer the transport has already built is moved through the session
// rather than copied again.
virtual void handle_incoming_data(std::vector<uint8_t>&& data) = 0;
virtual void send_data(std::vector<uint8_t>&& data) = 0;
virtual void close_session() = 0;
};

// Abstract output sink injected into Sessions: a Session hands its outbound
// bytes (and connection-teardown requests) to a SessionWriter, which is
// implemented by the RPC transport.
//
// IMPORTANT: Sessions may invoke these methods from worker threads, so
// implementations MUST be thread-safe and must marshal any socket operations
// onto their I/O thread if required.
class SessionWriter
{
public:
virtual ~SessionWriter() = default;

// `id` throughout is the connection identifier the session was created
// with (ccf::tls::ConnID).

// Queue bytes to be written to the socket associated with `id`. Ownership
// is transferred, so that a response which may be arbitrarily large is
// moved through to the transport rather than copied again.
//
// Fire-and-forget: there is currently no backpressure signal.
//
// FUTURE: to surface genuine TCP-layer backpressure, an implementation
// should report when a connection's pending-write queue exceeds a watermark
// (tracking per-connection queued bytes) and return a writable/would-block
// status here.
virtual void write_outbound(int64_t id, std::vector<uint8_t>&& data) = 0;

// Tear down the connection: stop the underlying socket and drop the
// session.
virtual void close_socket(int64_t id) = 0;

// Report that `bytes` of previously delivered inbound data have now been
// processed. The transport uses this to decide when it may read more: it
// stops reading once the node is holding more unprocessed inbound data
// than it is willing to, and resumes as sessions catch up. Without it a
// client could make the node queue work faster than it retires it, for as
// long as it liked.
//
// A session which does not report is not penalised beyond its own
// connection - the transport releases whatever is still outstanding when
// the connection closes - so this defaults to a no-op.
virtual void inbound_consumed(int64_t /*id*/, size_t /*bytes*/) {}
};
}
6 changes: 6 additions & 0 deletions include/ccf/node/startup_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ namespace ccf

ccf::ds::SizeString historical_cache_soft_limit = {"512MB"};

// How long an idle RPC (client TLS) connection is kept before it is closed.
// std::nullopt disables idle closure (connections are never closed for
// being idle).
std::optional<ccf::ds::TimeString> idle_connection_timeout =
ccf::ds::TimeString("60s");

ccf::consensus::Configuration consensus = {};
ccf::NodeInfoNetwork network;

Expand Down
5 changes: 2 additions & 3 deletions include/ccf/research/custom_protocol_subsystem_interface.h
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,14 @@ namespace ccf
{
namespace tls
{
class Context;
using ConnID = int64_t;
}

class CustomProtocolSubsystemInterface : public AbstractNodeSubSystem
{
public:
using CreateSessionFn = std::function<std::shared_ptr<Session>(
ccf::tls::ConnID, const std::unique_ptr<tls::Context>&&)>;
ccf::tls::ConnID, ccf::SessionWriter&)>;

~CustomProtocolSubsystemInterface() override = default;

Expand All @@ -41,7 +40,7 @@ namespace ccf
virtual std::shared_ptr<Session> create_session(
const std::string& protocol_name,
ccf::tls::ConnID conn_id,
const std::unique_ptr<tls::Context>&& ctx) = 0;
ccf::SessionWriter& writer) = 0;

struct Essentials
{
Expand Down
2 changes: 1 addition & 1 deletion python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[project]
name = "ccf"
version = "7.0.12"
version = "7.0.13"
authors = [
{ name="CCF Team", email="CCF-Sec@microsoft.com" },
]
Expand Down
14 changes: 14 additions & 0 deletions src/clients/tls/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# TLS certificate helpers (client tooling only)

These helpers configure an OpenSSL client with a trusted root and an owned
identity:

- `CA` parses one or more root certificates and populates an `SSL_CTX`'s
trusted certificate store.
- `Cert` holds a client's own certificate and private key, and applies them
(and the peer verification mode) to an `SSL_CTX` or `SSL`.

Their only consumer is `TlsClient` in the parent directory, used by the C++
test and perf clients. **No node code uses them.** The node's inbound TLS is
handled by `src/host/tls`, which builds its own `SSL_CTX`, and its outbound
requests go through libcurl.
File renamed without changes.
2 changes: 1 addition & 1 deletion src/tls/cert.h → src/clients/tls/cert.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
#pragma once

#include "ccf/crypto/openssl/openssl_wrappers.h"
#include "clients/tls/ca.h"
#include "crypto/openssl/ec_key_pair.h"
#include "ds/internal_logger.h"
#include "tls/ca.h"

#include <cstring>
#include <memory>
Expand Down
129 changes: 129 additions & 0 deletions src/clients/tls/test/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the Apache 2.0 License.

// Unit tests for the certificate helpers used by the C++ test clients, which
// configure an OpenSSL context and connection with a trusted root and an owned
// identity. The TLS transport itself is tested in
// src/host/test/openssl_server_test.cpp.

#include "ccf/crypto/ec_key_pair.h"
#include "ccf/crypto/verifier.h"
#include "ccf/ds/x509_time_fmt.h"
#include "clients/tls/ca.h"
#include "clients/tls/cert.h"
#include "crypto/certs.h"

#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN
#include <chrono>
#include <doctest/doctest.h>
#include <memory>
#include <optional>
#include <string>

namespace
{
constexpr size_t certificate_validity_period_days = 365;

std::string valid_from_yesterday()
{
using namespace std::literals;
return ccf::ds::to_x509_time_string(std::chrono::system_clock::now() - 24h);
}

ccf::crypto::Pem generate_self_signed_cert(
const ccf::crypto::ECKeyPairPtr& kp, const std::string& name)
{
return ccf::crypto::create_self_signed_cert(
kp, name, {}, valid_from_yesterday(), certificate_validity_period_days);
}

struct NetworkCA
{
ccf::crypto::ECKeyPairPtr kp;
ccf::crypto::Pem cert;
};

/// Get self-signed CA certificate.
NetworkCA get_ca()
{
auto kp = ccf::crypto::make_ec_key_pair();
return {kp, generate_self_signed_cert(kp, "CN=issuer")};
}

/// Creates a ::tls::Cert endorsed by the given CA.
std::unique_ptr<::tls::Cert> get_dummy_cert(
NetworkCA& net_ca, const std::string& name, bool auth_required = true)
{
auto ca = std::make_unique<::tls::CA>(net_ca.cert.str());

// Create a signing request and sign with the CA
auto kp = ccf::crypto::make_ec_key_pair();
auto crt = ccf::crypto::create_endorsed_cert(
kp,
"CN=" + name,
{},
valid_from_yesterday(),
certificate_validity_period_days,
net_ca.kp->private_key_pem(),
net_ca.cert);

// Verify node certificate with the CA's certificate
auto v = ccf::crypto::make_verifier(crt);
REQUIRE(v->verify_certificate({&net_ca.cert}));

return std::make_unique<::tls::Cert>(
std::move(ca), crt, kp->private_key_pem(), std::nullopt, auth_required);
}
}

TEST_CASE("CA configures trusted certificate store")
{
auto ca = get_ca();
::tls::CA trusted_ca(ca.cert.str(), true);
ccf::crypto::OpenSSL::Unique_SSL_CTX ctx(TLS_method());

trusted_ca.configure_trusted_cert_store(ctx);

auto* store = SSL_CTX_get_cert_store(ctx);
REQUIRE(store != nullptr);
auto* params = X509_STORE_get0_param(store);
REQUIRE(params != nullptr);
REQUIRE(
(X509_VERIFY_PARAM_get_flags(params) & X509_V_FLAG_PARTIAL_CHAIN) != 0);
}

TEST_CASE("Cert configures TLS verification and own certificate")
{
auto ca = get_ca();

SUBCASE("auth_required requires a peer certificate")
{
auto cert = get_dummy_cert(ca, "server");
ccf::crypto::OpenSSL::Unique_SSL_CTX ctx(TLS_method());

cert->configure_context(ctx);
ccf::crypto::OpenSSL::Unique_SSL ssl(ctx);
cert->configure_connection(ssl);

constexpr auto expected_verify_mode =
SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT;
REQUIRE(SSL_CTX_get_verify_mode(ctx) == expected_verify_mode);
REQUIRE(SSL_get_verify_mode(ssl) == expected_verify_mode);
REQUIRE(SSL_CTX_get0_certificate(ctx) != nullptr);
REQUIRE(SSL_get_certificate(ssl) != nullptr);
}

SUBCASE("without auth_required a peer certificate is requested, not required")
{
auto cert = get_dummy_cert(ca, "server", false);
ccf::crypto::OpenSSL::Unique_SSL_CTX ctx(TLS_method());

cert->configure_context(ctx);
ccf::crypto::OpenSSL::Unique_SSL ssl(ctx);
cert->configure_connection(ssl);

// The connection inherits the context's verification mode
REQUIRE((SSL_get_verify_mode(ssl) & SSL_VERIFY_PEER) != 0);
REQUIRE((SSL_get_verify_mode(ssl) & SSL_VERIFY_FAIL_IF_NO_PEER_CERT) == 0);
}
}
4 changes: 2 additions & 2 deletions src/clients/tls_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
#pragma once

#include "ccf/crypto/openssl/openssl_wrappers.h"
#include "clients/tls/ca.h"
#include "clients/tls/cert.h"
#include "ds/internal_logger.h"
#include "tls/ca.h"
#include "tls/cert.h"

#include <cstdint>
#include <cstring>
Expand Down
3 changes: 2 additions & 1 deletion src/common/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ namespace ccf
files_cleanup,
node_to_node_message_limit,
historical_cache_soft_limit,
identity_history_fetch);
identity_history_fetch,
idle_connection_timeout);

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(RecoveryDecisionProtocolConfig);
DECLARE_JSON_REQUIRED_FIELDS(
Expand Down
18 changes: 18 additions & 0 deletions src/ds/work_beacon.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,24 @@ namespace ccf::ds

condition_variable.notify_all();
}

void notify_work_available_coalesced()
{
bool notify = false;
{
std::lock_guard<std::mutex> lock(mutex);
if (work_available == 0)
{
work_available = 1;
notify = true;
}
}

if (notify)
{
condition_variable.notify_all();
}
}
};

using WorkBeaconPtr = std::shared_ptr<WorkBeacon>;
Expand Down
Loading
Loading