Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

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

### Added

- TLS groups offered by node RPC interfaces can now be configured with the optional `tls.groups` node setting. Successful TLS handshakes report the negotiated group at debug level. The default group list remains `P-521`, `P-384`, and `P-256`.

### Changed

- `ccf::http::ParsedQuery` (in `include/ccf/http_query.h`), returned by `ccf::http::parse_query()`, is now a `std::multimap<std::string, std::string, std::less<>>` that owns its decoded keys and values, rather than a `std::multimap<std::string_view, std::string_view>` pointing into the source query string. Owned storage is required because each key and value is now URL-decoded individually after splitting, which produces bytes not present in the original query. Application code that consumed the previous `std::string_view` keys/values may need to be updated (#8024).
Expand Down
26 changes: 26 additions & 0 deletions doc/host_config_schema/host_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -373,6 +373,32 @@
"description": "This section includes configuration of how the node should start (either start, join or recover) and associated information",
"required": ["type"]
},
"tls": {
"type": "object",
"properties": {
"groups": {
"type": "array",
"items": {
"type": "string",
"enum": [

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The node configuration is not captured by attestation, and therefore also not by join policy, so it cannot contain security-sensitive settings such as supported groups. Otherwise a joining node could pick weak values that would neither be auditable through the ledger, nor rejected by policy.

I think this is ideally moved to the code, where we detect if we crypto provider support PQC values, and extend the list in that case. If it must be configurable per node, then it would have to be a CLI value, but I don't think it's necessary.

"P-521",
"P-384",
"P-256",
"X25519",
"X448",
"X25519MLKEM768",
"SecP256r1MLKEM768",
"SecP384r1MLKEM1024"
]
},
"minItems": 1,
"default": ["P-521", "P-384", "P-256"],
"description": "Ordered list of OpenSSL TLS group names offered by this node. The first mutually supported group is preferred"
}
},
"description": "Transport Layer Security configuration for RPC interfaces",
"additionalProperties": false
},
"node_certificate": {
"type": "object",
"properties": {
Expand Down
12 changes: 12 additions & 0 deletions doc/operations/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,18 @@ The configuration for each CCF node must be contained in a single JSON configura

.. include:: generated_config.rst

TLS groups
----------

The optional ``tls.groups`` setting contains an ordered list of OpenSSL TLS
group names offered by the node's RPC interfaces. The default remains
``["P-521", "P-384", "P-256"]``. A node fails to start if the list is empty or
contains a group that its OpenSSL providers do not support. Operators can use
this setting to deploy new groups without rebuilding CCF, while retaining
explicit control over compatibility and fallback order. CCF also rejects group
names outside its approved set, even if the active OpenSSL providers support
them.


IPv6 Addresses
--------------
Expand Down
8 changes: 8 additions & 0 deletions include/ccf/node/startup_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@ namespace ccf
ccf::consensus::Configuration consensus = {};
ccf::NodeInfoNetwork network;

struct TLS
{
std::vector<std::string> groups = {"P-521", "P-384", "P-256"};

bool operator==(const TLS&) const = default;
};
TLS tls = {};

struct NodeCertificateInfo
{
std::string subject_name = "CN=CCF Node";
Expand Down
5 changes: 5 additions & 0 deletions src/common/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,10 @@ namespace ccf
{LoggerLevel::FAIL, "Fail"},
{LoggerLevel::FATAL, "Fatal"}});

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCFConfig::TLS);
DECLARE_JSON_REQUIRED_FIELDS(CCFConfig::TLS);
DECLARE_JSON_OPTIONAL_FIELDS(CCFConfig::TLS, groups);

DECLARE_JSON_TYPE_WITH_OPTIONAL_FIELDS(CCFConfig::NodeCertificateInfo);
DECLARE_JSON_REQUIRED_FIELDS(CCFConfig::NodeCertificateInfo);
DECLARE_JSON_OPTIONAL_FIELDS(
Expand Down Expand Up @@ -133,6 +137,7 @@ namespace ccf
DECLARE_JSON_OPTIONAL_FIELDS(
CCFConfig,
worker_threads,
tls,
node_certificate,
consensus,
ledger,
Expand Down
3 changes: 2 additions & 1 deletion src/enclave/enclave.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,8 @@ namespace ccf
{
start_type = start_type_;

rpcsessions->update_listening_interface_options(ccf_config_.network);
rpcsessions->update_listening_interface_options(
ccf_config_.network, ccf_config_.tls.groups);

node->set_n2n_message_limit(ccf_config_.node_to_node_message_limit);

Expand Down
12 changes: 9 additions & 3 deletions src/enclave/rpc_sessions.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ namespace ccf
nullptr;
std::shared_ptr<CommitCallbackSubsystem> commit_callbacks_subsystem =
nullptr;
std::vector<std::string> tls_groups = {"P-521", "P-384", "P-256"};

ccf::pal::Mutex lock;
std::unordered_map<
Expand Down Expand Up @@ -194,10 +195,13 @@ namespace ccf
}

void update_listening_interface_options(
const ccf::NodeInfoNetwork& node_info)
const ccf::NodeInfoNetwork& node_info,
const std::vector<std::string>& configured_tls_groups)
{
std::lock_guard<ccf::pal::Mutex> guard(lock);

tls_groups = configured_tls_groups;

for (const auto& [name, interface] : node_info.rpc_interfaces)
{
auto& li = listening_interfaces[name];
Expand Down Expand Up @@ -361,7 +365,8 @@ namespace ccf
listen_interface_id,
per_listen_interface.max_open_sessions_soft);

auto ctx = std::make_unique<::tls::Server>(certs[listen_interface_id]);
auto ctx = std::make_unique<::tls::Server>(
certs[listen_interface_id], false, tls_groups);
std::shared_ptr<Session> capped_session;
if (per_listen_interface.app_protocol == "HTTP2")
{
Expand Down Expand Up @@ -442,7 +447,8 @@ namespace ccf
{
ctx = std::make_unique<::tls::Server>(
certs[listen_interface_id],
per_listen_interface.app_protocol == "HTTP2");
per_listen_interface.app_protocol == "HTTP2",
tls_groups);
}

auto session = make_server_session(
Expand Down
2 changes: 2 additions & 0 deletions src/host/run.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "tcp.h"
#include "ticker.h"
#include "time_bound_logger.h"
#include "tls/context.h"
#include "udp.h"

#include <CLI11/CLI11.hpp>
Expand Down Expand Up @@ -974,6 +975,7 @@ namespace ccf
}

host::HostConfig config = config_json;
ccf::tls::validate_groups(config.tls.groups);

if (config.logging.format == host::LogFormat::JSON)
{
Expand Down
3 changes: 3 additions & 0 deletions src/node/node_state.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
#include "share_manager.h"
#include "snapshots/fetch.h"
#include "snapshots/filenames.h"
#include "tls/context.h"
#include "uvm_endorsements.h"

#include <arpa/inet.h>
Expand Down Expand Up @@ -1132,6 +1133,8 @@ namespace ccf
config.join.service_cert.data(),
config.join.service_cert.size());
curl_handle.set_opt(CURLOPT_CAPATH, nullptr);
const auto tls_group_list = ccf::tls::groups_to_list(config.tls.groups);
curl_handle.set_opt(CURLOPT_SSL_EC_CURVES, tls_group_list.c_str());

// Bound each attempt so a stalled connection is eventually abandoned,
// releasing the single-in-flight gate above so the periodic join timer
Expand Down
6 changes: 5 additions & 1 deletion src/tls/client.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,11 @@ namespace tls
std::shared_ptr<Cert> cert;

public:
Client(std::shared_ptr<Cert> cert_) : Context(true), cert(std::move(cert_))
Client(
std::shared_ptr<Cert> cert_,
const std::vector<std::string>& groups = {"P-521", "P-384", "P-256"}) :
Context(true, groups),
cert(std::move(cert_))
{
cert->configure_ssl(ssl, cfg);
}
Expand Down
105 changes: 100 additions & 5 deletions src/tls/context.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,91 @@
#include "ds/internal_logger.h"
#include "tls/tls.h"

#include <algorithm>
#include <array>
#include <cctype>
#include <memory>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <optional>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>

namespace ccf::tls
{
inline constexpr std::array<std::string_view, 8> approved_groups = {
"P-521",
"P-384",
"P-256",
"X25519",
"X448",
"X25519MLKEM768",
"SecP256r1MLKEM768",
"SecP384r1MLKEM1024"};

inline std::string groups_to_list(const std::vector<std::string>& groups)
{
if (groups.empty())
{
throw std::invalid_argument("TLS groups must not be empty");
}

std::string group_list;
for (const auto& group : groups)
{
constexpr size_t max_group_name_size = 64;
const auto valid_character = [](const unsigned char c) {
return std::isalnum(c) != 0 || c == '-' || c == '_' || c == '.';
};
if (
group.empty() || group.size() > max_group_name_size ||
!std::all_of(group.begin(), group.end(), valid_character))
{
throw std::invalid_argument(
"Invalid TLS group '" + group +
"': expected 1-64 alphanumeric, '-', '_' or '.' characters");
}
if (std::find(approved_groups.begin(), approved_groups.end(), group) ==
approved_groups.end())
{
throw std::invalid_argument("TLS group '" + group + "' is not approved");
}

if (!group_list.empty())
{
group_list += ':';
}
group_list += group;
}
return group_list;
}

inline void validate_groups(const std::vector<std::string>& groups)
{
const auto group_list = groups_to_list(groups);
ccf::crypto::OpenSSL::Unique_SSL_CTX context(TLS_method());
const auto rc = SSL_CTX_set1_groups_list(context, group_list.c_str());
if (rc != 1)
{
const auto error_code = ERR_get_error();
throw std::runtime_error(
"OpenSSL rejected TLS groups '" + group_list + "': " +
ccf::crypto::OpenSSL::error_string(error_code));
}
}

class Context
{
protected:
ccf::crypto::OpenSSL::Unique_SSL_CTX cfg;
ccf::crypto::OpenSSL::Unique_SSL ssl;

public:
Context(bool client) :
Context(
bool client,
const std::vector<std::string>& groups = {"P-521", "P-384", "P-256"}) :
cfg(client ? TLS_client_method() : TLS_server_method()),
ssl(cfg)
{
Expand Down Expand Up @@ -56,9 +127,13 @@ namespace ccf::tls
SSL_CTX_set_ciphersuites(cfg, ciphersuites);
SSL_set_ciphersuites(ssl, ciphersuites);

// Restrict the curves to approved ones
SSL_CTX_set1_curves_list(cfg, "P-521:P-384:P-256");
SSL_set1_curves_list(ssl, "P-521:P-384:P-256");
// Restrict the supported groups to those configured by the operator.
const auto group_list = groups_to_list(groups);
validate_groups(groups);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the relationship between groups and curves?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OpenSSL’s curves setters are exact legacy aliases for the groups setters—not separate constraints. “Groups” is the TLS 1.3 terminology encompassing EC curves, finite-field DH, and hybrid/PQ groups. Calling either replaces the same list; SSL_CTX_* sets context defaults, while SSL_* sets the individual connection.

The list controls key exchange and, for TLS ≤1.2, EC signature groups. In TLS 1.3, signatures are separate. Also, “first mutually supported group wins” is not strictly true: OpenSSL may prefer an existing key share to avoid a retry. OpenSSL 3.5 requires /-separated security tiers to strictly prefer PQ over classical fallback; this PR currently joins everything with :.

References: OpenSSL 3.3 groups/curves API and OpenSSL 3.5 group-tuple semantics.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLS 1.3 separates key exchange groups from signature schemes:

SSL_CTX_set1_sigalgs_list(ctx, "ecdsa_secp384r1_sha384:rsa_pss_rsae_sha256");
SSL_set1_sigalgs_list(ssl, "ecdsa_secp384r1_sha384:rsa_pss_rsae_sha256");

For client-certificate authentication, there are corresponding *_set1_client_sigalgs_list() calls. PR #8076 does not set these, so OpenSSL’s defaults govern signatures; its groups setting controls only TLS 1.3 key agreement.

ccf::crypto::OpenSSL::CHECK1(
SSL_CTX_set1_groups_list(cfg, group_list.c_str()));
ccf::crypto::OpenSSL::CHECK1(
SSL_set1_groups_list(ssl, group_list.c_str()));

// Allow buffer to be relocated between WANT_WRITE retries, and do partial
// writes if possible
Expand All @@ -82,6 +157,23 @@ namespace ccf::tls

virtual ~Context() = default;

[[nodiscard]] std::optional<std::string> get_negotiated_group() const
{
const auto group_id = SSL_get_negotiated_group(ssl);
if (group_id == 0)
{
return std::nullopt;
}

const auto* group_name = SSL_group_to_name(ssl, group_id);
if (group_name == nullptr)
{
return std::nullopt;
}

return group_name;
}

virtual void set_bio(
void* cb_obj, BIO_callback_fn_ex send, BIO_callback_fn_ex recv)
{
Expand Down Expand Up @@ -110,7 +202,10 @@ namespace ccf::tls
// Success in OpenSSL is 1, MBed is 0
if (rc > 0)
{
LOG_TRACE_FMT("Context::handshake() : Success");
const auto group = get_negotiated_group();
LOG_DEBUG_FMT(
"Context::handshake() : Success, negotiated TLS group {}",
group.value_or("<unknown>"));
return 0;
}

Expand Down
7 changes: 5 additions & 2 deletions src/tls/server.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,11 @@ namespace tls
std::shared_ptr<Cert> cert;

public:
Server(const std::shared_ptr<Cert>& cert_, bool http2 = false) :
Context(false),
Server(
const std::shared_ptr<Cert>& cert_,
bool http2 = false,
const std::vector<std::string>& groups = {"P-521", "P-384", "P-256"}) :
Context(false, groups),
cert(cert_)
{
cert->configure_ssl(ssl, cfg);
Expand Down
Loading
Loading