Secure websocket support and certificate config improvements. (#3032)

This commit is contained in:
cryptocode 2021-11-01 16:45:17 +01:00 committed by GitHub
commit 1e60488192
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 631 additions and 151 deletions

View file

@ -1,5 +1,6 @@
#include <nano/lib/jsonconfig.hpp>
#include <nano/lib/rpcconfig.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/lib/tomlconfig.hpp>
#include <nano/node/daemonconfig.hpp>
#include <nano/secure/utility.hpp>
@ -903,3 +904,61 @@ TEST (toml, daemon_read_config)
ASSERT_EQ (error.get_message (), expected_message2);
}
}
/** Deserialize an tls config with non-default values */
TEST (toml, tls_config_deserialize_no_defaults)
{
std::stringstream ss;
// A config file with values that differs from devnet defaults
ss << R"toml(
enable_https=true
enable_wss=true
verbose_logging=true
server_cert_path="xyz.cert.pem"
server_key_path="xyz.key.pem"
server_key_passphrase="xyz"
server_dh_path="xyz.pem"
)toml";
nano::tomlconfig toml;
toml.read (ss);
nano::tls_config conf;
nano::tls_config defaults;
conf.deserialize_toml (toml);
ASSERT_FALSE (toml.get_error ()) << toml.get_error ().get_message ();
ASSERT_NE (conf.enable_https, defaults.enable_https);
ASSERT_NE (conf.enable_wss, defaults.enable_wss);
ASSERT_NE (conf.verbose_logging, defaults.verbose_logging);
ASSERT_NE (conf.server_cert_path, defaults.server_cert_path);
ASSERT_NE (conf.server_key_path, defaults.server_key_path);
ASSERT_NE (conf.server_key_passphrase, defaults.server_key_passphrase);
ASSERT_NE (conf.server_dh_path, defaults.server_dh_path);
}
/** Empty tls config file should match a default config object, and there should be no required values. */
TEST (toml, tls_config_defaults)
{
std::stringstream ss;
// A config with no values
ss << R"toml()toml";
nano::tomlconfig toml;
toml.read (ss);
nano::tls_config conf;
nano::tls_config defaults;
conf.deserialize_toml (toml);
ASSERT_FALSE (toml.get_error ()) << toml.get_error ().get_message ();
ASSERT_EQ (conf.enable_https, defaults.enable_wss);
ASSERT_EQ (conf.enable_wss, defaults.enable_wss);
ASSERT_EQ (conf.verbose_logging, defaults.verbose_logging);
ASSERT_EQ (conf.server_cert_path, defaults.server_cert_path);
ASSERT_EQ (conf.server_key_path, defaults.server_key_path);
ASSERT_EQ (conf.server_key_passphrase, defaults.server_key_passphrase);
ASSERT_EQ (conf.server_dh_path, defaults.server_dh_path);
}

View file

@ -71,6 +71,8 @@ add_library(
threading.cpp
timer.hpp
timer.cpp
tlsconfig.hpp
tlsconfig.cpp
tomlconfig.hpp
tomlconfig.cpp
utility.hpp

View file

@ -304,8 +304,14 @@ std::string get_qtwallet_toml_config_path (boost::filesystem::path const & data_
{
return (data_path / "config-qtwallet.toml").string ();
}
std::string get_access_toml_config_path (boost::filesystem::path const & data_path)
{
return (data_path / "config-access.toml").string ();
}
std::string get_tls_toml_config_path (boost::filesystem::path const & data_path)
{
return (data_path / "config-tls.toml").string ();
}
} // namespace nano

View file

@ -281,6 +281,7 @@ std::string get_node_toml_config_path (boost::filesystem::path const & data_path
std::string get_rpc_toml_config_path (boost::filesystem::path const & data_path);
std::string get_access_toml_config_path (boost::filesystem::path const & data_path);
std::string get_qtwallet_toml_config_path (boost::filesystem::path const & data_path);
std::string get_tls_toml_config_path (boost::filesystem::path const & data_path);
/** Checks if we are running inside a valgrind instance */
bool running_within_valgrind ();

View file

@ -153,7 +153,7 @@ nano::error nano::rpc_config::deserialize_toml (nano::tomlconfig & toml)
auto rpc_secure_l (toml.get_optional_child ("secure"));
if (rpc_secure_l)
{
secure.deserialize_toml (*rpc_secure_l);
return nano::error ("The RPC secure configuration has moved to config-tls.toml. Please update the configuration.");
}
boost::asio::ip::address_v6 address_l;

View file

@ -3,6 +3,7 @@
#include <nano/lib/config.hpp>
#include <nano/lib/errors.hpp>
#include <memory>
#include <string>
#include <thread>
#include <vector>
@ -19,8 +20,12 @@ namespace nano
{
class jsonconfig;
class tomlconfig;
class tls_config;
/** Configuration options for RPC TLS */
/**
* Configuration options for RPC TLS.
* @note This is deprecated, but kept for a few versions in order to yield a config error message on startup if it's used.
*/
class rpc_secure_config final
{
public:
@ -29,6 +34,7 @@ public:
nano::error serialize_toml (nano::tomlconfig &) const;
nano::error deserialize_toml (nano::tomlconfig &);
private:
/** If true, enable TLS */
bool enable{ false };
/** If true, log certificate verification details */
@ -85,6 +91,8 @@ public:
uint8_t max_json_depth{ 20 };
uint64_t max_request_size{ 32 * 1024 * 1024 };
nano::rpc_logging_config rpc_logging;
/** Optional TLS config */
std::shared_ptr<nano::tls_config> tls_config;
static unsigned json_version ()
{
return 1;

188
nano/lib/tlsconfig.cpp Normal file
View file

@ -0,0 +1,188 @@
#include <nano/lib/config.hpp>
#include <nano/lib/logger_mt.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/lib/tomlconfig.hpp>
#include <boost/format.hpp>
#include <iostream>
namespace nano
{
nano::error nano::tls_config::serialize_toml (nano::tomlconfig & toml) const
{
toml.put ("enable_https", enable_https, "Enable or disable https:// support.\ntype:bool");
toml.put ("enable_wss", enable_wss, "Enable or disable wss:// support.\ntype:bool");
toml.put ("verbose_logging", verbose_logging, "Enable or disable verbose TLS logging.\ntype:bool");
toml.put ("server_key_passphrase", server_key_passphrase, "Server key passphrase.\ntype:string");
toml.put ("server_cert_path", server_cert_path, "Directory containing certificates.\ntype:string,path");
toml.put ("server_key_path", server_key_path, "Path to server key PEM file.\ntype:string,path");
toml.put ("server_dh_path", server_dh_path, "Path to Diffie-Hellman params file.\ntype:string,path");
toml.put ("client_certs_path", client_certs_path, "Directory containing optional client certificates.\ntype:string,path");
return toml.get_error ();
}
nano::error nano::tls_config::deserialize_toml (nano::tomlconfig & toml)
{
toml.get<bool> ("enable_https", enable_https);
toml.get<bool> ("enable_wss", enable_wss);
toml.get<bool> ("verbose_logging", verbose_logging);
toml.get<std::string> ("server_key_passphrase", server_key_passphrase);
toml.get<std::string> ("server_cert_path", server_cert_path);
toml.get<std::string> ("server_key_path", server_key_path);
toml.get<std::string> ("server_dh_path", server_dh_path);
toml.get<std::string> ("client_certs_path", client_certs_path);
return toml.get_error ();
}
#ifdef NANO_SECURE_RPC
namespace
{
bool on_verify_certificate (bool preverified, boost::asio::ssl::verify_context & ctx, nano::tls_config & config_a, nano::logger_mt & logger_a)
{
X509_STORE_CTX * cts = ctx.native_handle ();
auto error (X509_STORE_CTX_get_error (cts));
switch (error)
{
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
logger_a.always_log ("TLS: Unable to get issuer");
break;
case X509_V_ERR_CERT_NOT_YET_VALID:
case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
logger_a.always_log ("TLS: Certificate not yet valid");
break;
case X509_V_ERR_CERT_HAS_EXPIRED:
case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
logger_a.always_log ("TLS: Certificate expired");
break;
case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
if (config_a.verbose_logging)
{
logger_a.always_log ("TLS: Self-signed certificate in chain");
}
// Allow self-signed certificates
preverified = true;
break;
case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
logger_a.always_log ("TLS: Self-signed certificate not in the list of trusted certs (forgot to subject-hash certificate filename?)");
break;
default:
break;
}
if (config_a.verbose_logging)
{
if (error != 0)
{
logger_a.always_log ("TLS: Error: ", X509_verify_cert_error_string (error));
logger_a.always_log ("TLS: Error chain depth : ", X509_STORE_CTX_get_error_depth (cts));
}
X509 * cert = X509_STORE_CTX_get_current_cert (cts);
char subject_name[512];
X509_NAME_oneline (X509_get_subject_name (cert), subject_name, sizeof (subject_name) - 1);
logger_a.always_log ("TLS: Verifying: ", subject_name);
logger_a.always_log ("TLS: Verification: ", preverified);
}
else if (!preverified)
{
logger_a.always_log ("TLS: Pre-verification failed. Turn on verbose logging for more information.");
}
return preverified;
}
void load_certs (nano::tls_config & config_a, nano::logger_mt & logger_a)
{
try
{
// This is called if the key is password protected
config_a.ssl_context.set_password_callback (
[&config_a] (std::size_t,
boost::asio::ssl::context_base::password_purpose) {
return config_a.server_key_passphrase;
});
// The following two options disables the session cache and enables stateless session resumption.
// This is necessary because of the way the RPC server abruptly terminate connections.
SSL_CTX_set_session_cache_mode (config_a.ssl_context.native_handle (), SSL_SESS_CACHE_OFF);
SSL_CTX_set_options (config_a.ssl_context.native_handle (), SSL_OP_NO_TICKET);
config_a.ssl_context.set_options (
boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::no_sslv3 | boost::asio::ssl::context::single_dh_use);
config_a.ssl_context.use_certificate_chain_file (config_a.server_cert_path);
config_a.ssl_context.use_private_key_file (config_a.server_key_path, boost::asio::ssl::context::pem);
config_a.ssl_context.use_tmp_dh_file (config_a.server_dh_path);
// Verify client certificates?
if (!config_a.client_certs_path.empty ())
{
config_a.ssl_context.set_verify_mode (boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_peer);
config_a.ssl_context.add_verify_path (config_a.client_certs_path);
config_a.ssl_context.set_verify_callback ([&config_a, &logger_a] (auto preverified, auto & ctx) {
return on_verify_certificate (preverified, ctx, config_a, logger_a);
});
}
logger_a.always_log ("TLS: successfully configured");
}
catch (boost::system::system_error const & err)
{
auto error (boost::str (boost::format ("Could not load certificate information: %1%. Make sure the paths and the passphrase in config-tls.toml are correct.") % err.what ()));
std::cerr << error << std::endl;
logger_a.always_log (error);
}
}
}
#endif
nano::error read_tls_config_toml (boost::filesystem::path const & data_path_a, nano::tls_config & config_a, nano::logger_mt & logger_a, std::vector<std::string> const & config_overrides)
{
nano::error error;
auto toml_config_path = nano::get_tls_toml_config_path (data_path_a);
// Parse and deserialize
nano::tomlconfig toml;
std::stringstream config_overrides_stream;
for (auto const & entry : config_overrides)
{
config_overrides_stream << entry << std::endl;
}
config_overrides_stream << std::endl;
// Make sure we don't create an empty toml file if it doesn't exist. Running without a tls toml file is the default.
if (!error)
{
if (boost::filesystem::exists (toml_config_path))
{
error = toml.read (config_overrides_stream, toml_config_path);
}
else
{
error = toml.read (config_overrides_stream);
}
}
if (!error)
{
error = config_a.deserialize_toml (toml);
}
if (!error && (config_a.enable_https || config_a.enable_wss))
{
#ifdef NANO_SECURE_RPC
load_certs (config_a, logger_a);
#else
auto msg ("https or wss is enabled in the TLS configuration, but the node is not built with NANO_SECURE_RPC");
std::cerr << msg << std::endl;
logger_a.always_log (msg);
std::exit (1);
#endif
}
return error;
}
}

63
nano/lib/tlsconfig.hpp Normal file
View file

@ -0,0 +1,63 @@
#pragma once
#include <nano/lib/config.hpp>
#include <nano/lib/errors.hpp>
#include <string>
#include <thread>
#include <vector>
#ifdef NANO_SECURE_RPC
#include <boost/asio/ssl/context.hpp>
#endif
namespace boost::filesystem
{
class path;
}
namespace nano
{
class logger_mt;
class jsonconfig;
class tomlconfig;
/** Configuration options for secure RPC and WebSocket connections */
class tls_config final
{
public:
nano::error serialize_toml (nano::tomlconfig &) const;
nano::error deserialize_toml (nano::tomlconfig &);
/** If true, enable TLS for RPC (only allow https, otherwise only allow http) */
bool enable_https{ false };
/** If true, enable TLS for WebSocket (only allow wss, otherwise only allow ws) */
bool enable_wss{ false };
/** If true, log certificate verification details */
bool verbose_logging{ false };
/** Must be set if the private key PEM is password protected */
std::string server_key_passphrase;
/** Path to certificate- or chain file. Must be PEM formatted. */
std::string server_cert_path;
/** Path to private key file. Must be PEM formatted.*/
std::string server_key_path;
/** Path to dhparam file */
std::string server_dh_path;
/** Optional path to directory containing client certificates */
std::string client_certs_path;
#ifdef NANO_SECURE_RPC
/** The context needs to be shared between sessions to make resumption work */
boost::asio::ssl::context ssl_context{ boost::asio::ssl::context::tlsv12_server };
#endif
};
nano::error read_tls_config_toml (boost::filesystem::path const & data_path_a, nano::tls_config & config_a, nano::logger_mt & logger_a, std::vector<std::string> const & config_overrides = std::vector<std::string> ());
}

View file

@ -1,6 +1,7 @@
#include <nano/boost/process/child.hpp>
#include <nano/lib/signal_manager.hpp>
#include <nano/lib/threading.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/lib/utility.hpp>
#include <nano/nano_node/daemon.hpp>
#include <nano/node/cli.hpp>
@ -90,6 +91,19 @@ void nano_daemon::daemon::run (boost::filesystem::path const & data_path, nano::
{
config.node.logging.init (data_path);
nano::logger_mt logger{ config.node.logging.min_time_between_log_output };
auto tls_config (std::make_shared<nano::tls_config> ());
error = nano::read_tls_config_toml (data_path, *tls_config, logger);
if (error)
{
std::cerr << error.get_message () << std::endl;
std::exit (1);
}
else
{
config.node.websocket_config.tls_config = tls_config;
}
boost::asio::io_context io_ctx;
auto opencl (nano::opencl_work::create (config.opencl_enable, config.opencl, logger, config.node.network_params.work));
nano::work_pool opencl_work (config.node.network_params.network, config.node.work_threads, config.node.pow_sleep_interval, opencl ? [&opencl] (nano::work_version const version_a, nano::root const & root_a, uint64_t difficulty_a, std::atomic<int> & ticket_a) {
@ -157,6 +171,8 @@ void nano_daemon::daemon::run (boost::filesystem::path const & data_path, nano::
std::cout << error.get_message () << std::endl;
std::exit (1);
}
rpc_config.tls_config = tls_config;
rpc_handler = std::make_unique<nano::inprocess_rpc_handler> (*node, ipc_server, config.rpc, [&ipc_server, &workers = node->workers, &io_ctx] () {
ipc_server.stop ();
workers.add_timed_task (std::chrono::steady_clock::now () + std::chrono::seconds (3), [&io_ctx] () {

View file

@ -2,6 +2,7 @@
#include <nano/lib/errors.hpp>
#include <nano/lib/signal_manager.hpp>
#include <nano/lib/threading.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/lib/utility.hpp>
#include <nano/node/cli.hpp>
#include <nano/node/ipc/ipc_server.hpp>
@ -46,6 +47,20 @@ void run (boost::filesystem::path const & data_path, std::vector<std::string> co
if (!error)
{
logging_init (data_path);
nano::logger_mt logger;
auto tls_config (std::make_shared<nano::tls_config> ());
error = nano::read_tls_config_toml (data_path, *tls_config, logger);
if (error)
{
std::cerr << error.get_message () << std::endl;
std::exit (1);
}
else
{
rpc_config.tls_config = tls_config;
}
boost::asio::io_context io_ctx;
nano::signal_manager sigman;
try

View file

@ -4,6 +4,7 @@
#include <nano/lib/errors.hpp>
#include <nano/lib/rpcconfig.hpp>
#include <nano/lib/threading.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/lib/tomlconfig.hpp>
#include <nano/lib/utility.hpp>
#include <nano/lib/walletconfig.hpp>
@ -101,6 +102,19 @@ int run_wallet (QApplication & application, int argc, char * const * argv, boost
config.node.logging.init (data_path);
nano::logger_mt logger{ config.node.logging.min_time_between_log_output };
auto tls_config (std::make_shared<nano::tls_config> ());
error = nano::read_tls_config_toml (data_path, *tls_config, logger);
if (error)
{
splash->hide ();
show_error (error.get_message ());
std::exit (1);
}
else
{
config.node.websocket_config.tls_config = tls_config;
}
boost::asio::io_context io_ctx;
nano::thread_runner runner (io_ctx, config.node.io_threads);
@ -142,6 +156,7 @@ int run_wallet (QApplication & application, int argc, char * const * argv, boost
wallet_config.account = wallet->deterministic_insert (transaction);
}
}
debug_assert (wallet->exists (wallet_config.account));
write_wallet_config (wallet_config, data_path);
node->start ();
@ -172,8 +187,11 @@ int run_wallet (QApplication & application, int argc, char * const * argv, boost
auto error = nano::read_rpc_config_toml (data_path, rpc_config, flags.rpc_config_overrides);
if (error)
{
splash->hide ();
show_error (error.get_message ());
std::exit (1);
}
rpc_config.tls_config = tls_config;
rpc_handler = std::make_unique<nano::inprocess_rpc_handler> (*node, ipc, config.rpc);
rpc = nano::get_rpc (io_ctx, rpc_config, *rpc_handler);
rpc->start ();

View file

@ -146,6 +146,8 @@ add_library(
websocket.cpp
websocketconfig.hpp
websocketconfig.cpp
websocket_stream.hpp
websocket_stream.cpp
write_database_queue.hpp
write_database_queue.cpp
xorshift.hpp)

View file

@ -1,4 +1,5 @@
#include <nano/lib/cli.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/lib/tomlconfig.hpp>
#include <nano/node/cli.hpp>
#include <nano/node/common.hpp>
@ -61,7 +62,7 @@ void nano::add_node_options (boost::program_options::options_description & descr
("rebuild_database", "Rebuild LMDB database with vacuum for best compaction")
("migrate_database_lmdb_to_rocksdb", "Migrates LMDB database to RocksDB")
("diagnostics", "Run internal diagnostics")
("generate_config", boost::program_options::value<std::string> (), "Write configuration to stdout, populated with defaults suitable for this system. Pass the configuration type node or rpc. See also use_defaults.")
("generate_config", boost::program_options::value<std::string> (), "Write configuration to stdout, populated with defaults suitable for this system. Pass the configuration type node, rpc or tls. See also use_defaults.")
("key_create", "Generates a adhoc random keypair and prints it to stdout")
("key_expand", "Derive public key and account number from <key>")
("wallet_add_adhoc", "Insert <key> in to <wallet>")
@ -684,6 +685,12 @@ std::error_code nano::handle_node_options (boost::program_options::variables_map
nano::rpc_config config{ nano::dev::network_params.network };
config.serialize_toml (toml);
}
else if (type == "tls")
{
valid_type = true;
nano::tls_config config;
config.serialize_toml (toml);
}
else
{
std::cerr << "Invalid configuration type " << type << ". Must be node or rpc." << std::endl;

View file

@ -128,7 +128,7 @@ nano::node::node (boost::asio::io_context & io_ctx_a, boost::filesystem::path co
if (config.websocket_config.enabled)
{
auto endpoint_l (nano::tcp_endpoint (boost::asio::ip::make_address_v6 (config.websocket_config.address), config.websocket_config.port));
websocket_server = std::make_shared<nano::websocket::listener> (logger, wallets, io_ctx, endpoint_l);
websocket_server = std::make_shared<nano::websocket::listener> (config.websocket_config.tls_config, logger, wallets, io_ctx, endpoint_l);
this->websocket_server->run ();
}

View file

@ -1,6 +1,7 @@
#include <nano/boost/asio/bind_executor.hpp>
#include <nano/boost/asio/dispatch.hpp>
#include <nano/boost/asio/strand.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/lib/work.hpp>
#include <nano/node/transport/transport.hpp>
#include <nano/node/wallet.hpp>
@ -232,10 +233,19 @@ bool nano::websocket::vote_options::should_filter (nano::websocket::message cons
return should_filter_l;
}
nano::websocket::session::session (nano::websocket::listener & listener_a, socket_type socket_a) :
ws_listener (listener_a), ws (std::move (socket_a)), strand (ws.get_executor ())
#ifdef NANO_SECURE_RPC
nano::websocket::session::session (nano::websocket::listener & listener_a, socket_type socket_a, boost::asio::ssl::context & ctx_a) :
ws_listener (listener_a), ws (std::move (socket_a), ctx_a)
{
ws_listener.get_logger ().try_log ("Websocket: secure session started");
}
#endif
nano::websocket::session::session (nano::websocket::listener & listener_a, socket_type socket_a) :
ws_listener (listener_a), ws (std::move (socket_a))
{
ws.text (true);
ws_listener.get_logger ().try_log ("Websocket: session started");
}
@ -253,7 +263,7 @@ nano::websocket::session::~session ()
void nano::websocket::session::handshake ()
{
auto this_l (shared_from_this ());
ws.async_accept ([this_l] (boost::system::error_code const & ec) {
ws.handshake ([this_l] (boost::system::error_code const & ec) {
if (!ec)
{
// Start reading incoming messages
@ -271,7 +281,7 @@ void nano::websocket::session::close ()
ws_listener.get_logger ().try_log ("Websocket: session closing");
auto this_l (shared_from_this ());
boost::asio::dispatch (strand,
boost::asio::dispatch (ws.get_strand (),
[this_l] () {
boost::beast::websocket::close_reason reason;
reason.code = boost::beast::websocket::close_code::normal;
@ -289,7 +299,7 @@ void nano::websocket::session::write (nano::websocket::message message_a)
{
lk.unlock ();
auto this_l (shared_from_this ());
boost::asio::post (strand,
boost::asio::post (ws.get_strand (),
[message_a, this_l] () {
bool write_in_progress = !this_l->send_queue.empty ();
this_l->send_queue.emplace_back (message_a);
@ -307,7 +317,6 @@ void nano::websocket::session::write_queued_messages ()
auto this_l (shared_from_this ());
ws.async_write (nano::shared_const_buffer (msg),
boost::asio::bind_executor (strand,
[this_l] (boost::system::error_code ec, std::size_t bytes_transferred) {
this_l->send_queue.pop_front ();
if (!ec)
@ -317,16 +326,15 @@ void nano::websocket::session::write_queued_messages ()
this_l->write_queued_messages ();
}
}
}));
});
}
void nano::websocket::session::read ()
{
auto this_l (shared_from_this ());
boost::asio::post (strand, [this_l] () {
boost::asio::post (ws.get_strand (), [this_l] () {
this_l->ws.async_read (this_l->read_buffer,
boost::asio::bind_executor (this_l->strand,
[this_l] (boost::system::error_code ec, std::size_t bytes_transferred) {
if (!ec)
{
@ -353,7 +361,7 @@ void nano::websocket::session::read ()
{
this_l->ws_listener.get_logger ().try_log ("Websocket: read failed: ", ec.message ());
}
}));
});
});
}
@ -541,7 +549,8 @@ void nano::websocket::listener::stop ()
sessions.clear ();
}
nano::websocket::listener::listener (nano::logger_mt & logger_a, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a) :
nano::websocket::listener::listener (std::shared_ptr<nano::tls_config> const & tls_config_a, nano::logger_mt & logger_a, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a) :
tls_config (tls_config_a),
logger (logger_a),
wallets (wallets_a),
acceptor (io_ctx_a),
@ -590,7 +599,18 @@ void nano::websocket::listener::on_accept (boost::system::error_code ec)
else
{
// Create the session and initiate websocket handshake
auto session (std::make_shared<nano::websocket::session> (*this, std::move (socket)));
std::shared_ptr<nano::websocket::session> session;
if (tls_config && tls_config->enable_wss)
{
#ifdef NANO_SECURE_RPC
session = std::make_shared<nano::websocket::session> (*this, std::move (socket), tls_config->ssl_context);
#endif
}
else
{
session = std::make_shared<nano::websocket::session> (*this, std::move (socket));
}
sessions_mutex.lock ();
sessions.push_back (session);
// Clean up expired sessions

View file

@ -1,13 +1,11 @@
#pragma once
#include <nano/boost/asio/strand.hpp>
#include <nano/boost/beast/core.hpp>
#include <nano/boost/beast/websocket.hpp>
#include <nano/lib/blocks.hpp>
#include <nano/lib/numbers.hpp>
#include <nano/lib/work.hpp>
#include <nano/node/common.hpp>
#include <nano/node/election.hpp>
#include <nano/node/websocket_stream.hpp>
#include <nano/secure/common.hpp>
#include <boost/property_tree/json_parser.hpp>
@ -20,15 +18,6 @@
#include <unordered_set>
#include <vector>
/* Boost v1.70 introduced breaking changes; the conditional compilation allows 1.6x to be supported as well. */
#if BOOST_VERSION < 107000
using socket_type = boost::asio::ip::tcp::socket;
#define beast_buffers boost::beast::buffers
#else
using socket_type = boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::io_context::executor_type>;
#define beast_buffers boost::beast::make_printable
#endif
namespace nano
{
class wallets;
@ -36,6 +25,7 @@ class logger_mt;
class vote;
class election_status;
class telemetry_data;
class tls_config;
enum class election_status_type : uint8_t;
namespace websocket
{
@ -233,8 +223,13 @@ namespace websocket
friend class listener;
public:
#ifdef NANO_SECURE_RPC
/** Constructor that takes ownership over \p socket_a and creates an SSL stream */
explicit session (nano::websocket::listener & listener_a, socket_type socket_a, boost::asio::ssl::context & ctx_a);
#endif
/** Constructor that takes ownership over \p socket_a */
explicit session (nano::websocket::listener & listener_a, socket_type socket_a);
~session ();
/** Perform Websocket handshake and start reading messages */
@ -252,12 +247,10 @@ namespace websocket
private:
/** The owning listener */
nano::websocket::listener & ws_listener;
/** Websocket */
boost::beast::websocket::stream<socket_type> ws;
/** Websocket stream, supporting both plain and tls connections */
nano::websocket::stream ws;
/** Buffer for received messages */
boost::beast::multi_buffer read_buffer;
/** All websocket operations that are thread unsafe must go through a strand. */
boost::asio::strand<boost::asio::io_context::executor_type> strand;
/** Outgoing messages. The send queue is protected by accessing it only through the strand */
std::deque<message> send_queue;
@ -286,7 +279,7 @@ namespace websocket
class listener final : public std::enable_shared_from_this<listener>
{
public:
listener (nano::logger_mt & logger_a, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a);
listener (std::shared_ptr<nano::tls_config> const & tls_config_a, nano::logger_mt & logger_a, nano::wallets & wallets_a, boost::asio::io_context & io_ctx_a, boost::asio::ip::tcp::endpoint endpoint_a);
/** Start accepting connections */
void run ();
@ -335,6 +328,7 @@ namespace websocket
/** Removes from subscription count of a specific topic*/
void decrease_subscriber_count (nano::websocket::topic const & topic_a);
std::shared_ptr<nano::tls_config> tls_config;
nano::logger_mt & logger;
nano::wallets & wallets;
boost::asio::ip::tcp::acceptor acceptor;

View file

@ -0,0 +1,126 @@
#include <nano/node/websocket_stream.hpp>
#include <boost/asio/bind_executor.hpp>
namespace
{
/** Type-erasing wrapper for tls and non-tls websocket streams */
template <typename stream_type>
class stream_wrapper : public nano::websocket::websocket_stream_concept
{
public:
#ifdef NANO_SECURE_RPC
stream_wrapper (socket_type socket_a, boost::asio::ssl::context & ctx_a) :
ws (std::move (socket_a), ctx_a), strand (ws.get_executor ())
{
is_tls = true;
ws.text (true);
}
#endif
stream_wrapper (socket_type socket_a) :
ws (std::move (socket_a)), strand (ws.get_executor ())
{
ws.text (true);
}
void handshake (std::function<void (boost::system::error_code const & ec)> callback_a) override
{
if (is_tls)
{
ssl_handshake (callback_a);
}
else
{
// Websocket handshake
ws.async_accept ([callback_a] (boost::system::error_code const & ec) {
callback_a (ec);
});
}
}
void ssl_handshake (std::function<void (boost::system::error_code const & ec)> callback_a)
{
#ifdef NANO_SECURE_RPC
// Only perform TLS handshakes for TLS streams
if constexpr (std::is_same<wss_type, stream_type>::value)
{
ws.next_layer ().async_handshake (boost::asio::ssl::stream_base::server, [this, callback_a] (boost::system::error_code const & ec) {
if (!ec)
{
// Websocket handshake
this->ws.async_accept ([callback_a] (boost::system::error_code const & ec) {
callback_a (ec);
});
}
else
{
callback_a (ec);
}
});
}
#endif
}
boost::asio::strand<boost::asio::io_context::executor_type> & get_strand () override
{
return strand;
}
void close (boost::beast::websocket::close_reason const & reason_a, boost::system::error_code & ec_a) override
{
ws.close (reason_a, ec_a);
}
void async_write (nano::shared_const_buffer const & buffer_a, std::function<void (boost::system::error_code, std::size_t)> callback_a) override
{
ws.async_write (buffer_a, boost::asio::bind_executor (strand, callback_a));
}
void async_read (boost::beast::multi_buffer & buffer_a, std::function<void (boost::system::error_code, std::size_t)> callback_a) override
{
ws.async_read (buffer_a, boost::asio::bind_executor (strand, callback_a));
}
private:
bool is_tls{ false };
stream_type ws;
boost::asio::strand<boost::asio::io_context::executor_type> strand;
};
}
#ifdef NANO_SECURE_RPC
nano::websocket::stream::stream (socket_type socket_a, boost::asio::ssl::context & ctx_a)
{
impl = std::make_unique<stream_wrapper<wss_type>> (std::move (socket_a), ctx_a);
}
#endif
nano::websocket::stream::stream (socket_type socket_a)
{
impl = std::make_unique<stream_wrapper<ws_type>> (std::move (socket_a));
}
[[nodiscard]] boost::asio::strand<boost::asio::io_context::executor_type> & nano::websocket::stream::get_strand ()
{
return impl->get_strand ();
}
void nano::websocket::stream::handshake (std::function<void (boost::system::error_code const & ec)> callback_a)
{
impl->handshake (callback_a);
}
void nano::websocket::stream::close (boost::beast::websocket::close_reason const & reason_a, boost::system::error_code & ec_a)
{
impl->close (reason_a, ec_a);
}
void nano::websocket::stream::async_write (nano::shared_const_buffer const & buffer_a, std::function<void (boost::system::error_code, std::size_t)> callback_a)
{
impl->async_write (buffer_a, callback_a);
}
void nano::websocket::stream::async_read (boost::beast::multi_buffer & buffer_a, std::function<void (boost::system::error_code, std::size_t)> callback_a)
{
impl->async_read (buffer_a, callback_a);
}

View file

@ -0,0 +1,62 @@
#pragma once
#include <nano/boost/asio/strand.hpp>
#include <nano/boost/beast/core.hpp>
#include <nano/boost/beast/websocket.hpp>
#include <nano/lib/asio.hpp>
#include <memory>
/* Boost v1.70 introduced breaking changes; the conditional compilation allows 1.6x to be supported as well. */
#if BOOST_VERSION < 107000
using socket_type = boost::asio::ip::tcp::socket;
#define beast_buffers boost::beast::buffers
#else
using socket_type = boost::asio::basic_stream_socket<boost::asio::ip::tcp, boost::asio::io_context::executor_type>;
#define beast_buffers boost::beast::make_printable
#endif
using ws_type = boost::beast::websocket::stream<socket_type>;
#ifdef NANO_SECURE_RPC
#include <boost/asio/ssl/context.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/websocket/ssl.hpp>
using wss_type = boost::beast::websocket::stream<boost::beast::ssl_stream<socket_type>>;
#endif
namespace nano::websocket
{
/** The minimal stream interface needed by the Nano websocket implementation */
class websocket_stream_concept
{
public:
virtual ~websocket_stream_concept () = default;
virtual boost::asio::strand<boost::asio::io_context::executor_type> & get_strand () = 0;
virtual void handshake (std::function<void (boost::system::error_code const & ec)> callback_a) = 0;
virtual void close (boost::beast::websocket::close_reason const & reason_a, boost::system::error_code & ec_a) = 0;
virtual void async_write (nano::shared_const_buffer const & buffer_a, std::function<void (boost::system::error_code, std::size_t)> callback_a) = 0;
virtual void async_read (boost::beast::multi_buffer & buffer_a, std::function<void (boost::system::error_code, std::size_t)> callback_a) = 0;
};
/**
* Beast websockets doesn't provide a common base type for tls and non-tls streams, so we use
* the type erasure idiom to be able to use both kinds of streams through a common type.
*/
class stream final : public websocket_stream_concept
{
public:
#ifdef NANO_SECURE_RPC
stream (socket_type socket_a, boost::asio::ssl::context & ctx_a);
#endif
stream (socket_type socket_a);
[[nodiscard]] boost::asio::strand<boost::asio::io_context::executor_type> & get_strand () override;
void handshake (std::function<void (boost::system::error_code const & ec)> callback_a) override;
void close (boost::beast::websocket::close_reason const & reason_a, boost::system::error_code & ec_a) override;
void async_write (nano::shared_const_buffer const & buffer_a, std::function<void (boost::system::error_code, std::size_t)> callback_a) override;
void async_read (boost::beast::multi_buffer & buffer_a, std::function<void (boost::system::error_code, std::size_t)> callback_a) override;
private:
std::unique_ptr<websocket_stream_concept> impl;
};
}

View file

@ -3,10 +3,13 @@
#include <nano/lib/config.hpp>
#include <nano/lib/errors.hpp>
#include <memory>
namespace nano
{
class jsonconfig;
class tomlconfig;
class tls_config;
namespace websocket
{
/** websocket configuration */
@ -22,6 +25,8 @@ namespace websocket
bool enabled{ false };
uint16_t port;
std::string address;
/** Optional TLS config */
std::shared_ptr<nano::tls_config> tls_config;
};
}
}

View file

@ -1,5 +1,6 @@
#include <nano/boost/asio/bind_executor.hpp>
#include <nano/lib/rpc_handler_interface.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/rpc/rpc.hpp>
#include <nano/rpc/rpc_connection.hpp>
@ -82,12 +83,10 @@ std::unique_ptr<nano::rpc> nano::get_rpc (boost::asio::io_context & io_ctx_a, na
{
std::unique_ptr<rpc> impl;
if (config_a.secure.enable)
if (config_a.tls_config && config_a.tls_config->enable_https)
{
#ifdef NANO_SECURE_RPC
impl = std::make_unique<rpc_secure> (io_ctx_a, config_a, rpc_handler_interface_a);
#else
std::cerr << "RPC configured for TLS, but the node is not compiled with TLS support" << std::endl;
#endif
}
else

View file

@ -1,4 +1,5 @@
#include <nano/boost/asio/bind_executor.hpp>
#include <nano/lib/tlsconfig.hpp>
#include <nano/rpc/rpc_connection_secure.hpp>
#include <nano/rpc/rpc_secure.hpp>
@ -7,112 +8,14 @@
#include <iostream>
bool nano::rpc_secure::on_verify_certificate (bool preverified, boost::asio::ssl::verify_context & ctx)
{
X509_STORE_CTX * cts = ctx.native_handle ();
auto error (X509_STORE_CTX_get_error (cts));
switch (error)
{
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT:
logger.always_log ("TLS: Unable to get issuer");
break;
case X509_V_ERR_CERT_NOT_YET_VALID:
case X509_V_ERR_ERROR_IN_CERT_NOT_BEFORE_FIELD:
logger.always_log ("TLS: Certificate not yet valid");
break;
case X509_V_ERR_CERT_HAS_EXPIRED:
case X509_V_ERR_ERROR_IN_CERT_NOT_AFTER_FIELD:
logger.always_log ("TLS: Certificate expired");
break;
case X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN:
if (config.secure.verbose_logging)
{
logger.always_log ("TLS: self signed certificate in chain");
}
// Allow self-signed certificates
preverified = true;
break;
case X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT:
logger.always_log ("TLS: Self signed certificate not in the list of trusted certs (forgot to subject-hash certificate filename?)");
break;
default:
break;
}
if (config.secure.verbose_logging)
{
if (error != 0)
{
logger.always_log ("TLS: Error: ", X509_verify_cert_error_string (error));
logger.always_log ("TLS: Error chain depth : ", X509_STORE_CTX_get_error_depth (cts));
}
X509 * cert = X509_STORE_CTX_get_current_cert (cts);
char subject_name[512];
X509_NAME_oneline (X509_get_subject_name (cert), subject_name, sizeof (subject_name) - 1);
logger.always_log ("TLS: Verifying: ", subject_name);
logger.always_log ("TLS: Verification: ", preverified);
}
else if (!preverified)
{
logger.always_log ("TLS: Pre-verification failed. Turn on verbose logging for more information.");
}
return preverified;
}
void nano::rpc_secure::load_certs (boost::asio::ssl::context & context_a)
{
try
{
// This is called if the key is password protected
context_a.set_password_callback (
[this] (std::size_t,
boost::asio::ssl::context_base::password_purpose) {
return config.secure.server_key_passphrase;
});
// The following two options disables the session cache and enables stateless session resumption.
// This is necessary because of the way the RPC server abruptly terminate connections.
SSL_CTX_set_session_cache_mode (context_a.native_handle (), SSL_SESS_CACHE_OFF);
SSL_CTX_set_options (context_a.native_handle (), SSL_OP_NO_TICKET);
context_a.set_options (
boost::asio::ssl::context::default_workarounds | boost::asio::ssl::context::no_sslv2 | boost::asio::ssl::context::no_sslv3 | boost::asio::ssl::context::single_dh_use);
context_a.use_certificate_chain_file (config.secure.server_cert_path);
context_a.use_private_key_file (config.secure.server_key_path, boost::asio::ssl::context::pem);
context_a.use_tmp_dh_file (config.secure.server_dh_path);
// Verify client certificates?
if (!config.secure.client_certs_path.empty ())
{
context_a.set_verify_mode (boost::asio::ssl::verify_fail_if_no_peer_cert | boost::asio::ssl::verify_peer);
context_a.add_verify_path (config.secure.client_certs_path);
context_a.set_verify_callback ([this] (auto preverified, auto & ctx) {
return this->on_verify_certificate (preverified, ctx);
});
}
}
catch (boost::system::system_error const & err)
{
auto error (boost::str (boost::format ("Could not load certificate information: %1%. Make sure the paths in the secure rpc configuration are correct.") % err.what ()));
std::cerr << error << std::endl;
logger.always_log (error);
}
}
nano::rpc_secure::rpc_secure (boost::asio::io_context & context_a, nano::rpc_config const & config_a, nano::rpc_handler_interface & rpc_handler_interface_a) :
rpc (context_a, config_a, rpc_handler_interface_a),
ssl_context (boost::asio::ssl::context::tlsv12_server)
rpc (context_a, config_a, rpc_handler_interface_a)
{
load_certs (ssl_context);
}
void nano::rpc_secure::accept ()
{
auto connection (std::make_shared<nano::rpc_connection_secure> (config, io_ctx, logger, rpc_handler_interface, this->ssl_context));
auto connection (std::make_shared<nano::rpc_connection_secure> (config, io_ctx, logger, rpc_handler_interface, config.tls_config->ssl_context));
acceptor.async_accept (connection->socket, boost::asio::bind_executor (connection->strand, [this, connection] (boost::system::error_code const & ec) {
if (ec != boost::asio::error::operation_aborted && acceptor.is_open ())
{

View file

@ -1,8 +1,6 @@
#pragma once
#include <nano/rpc/rpc.hpp>
#include <boost/asio/ssl/context.hpp>
namespace boost
{
namespace asio
@ -23,17 +21,5 @@ public:
/** Starts accepting connections */
void accept () override;
/** Installs the server certificate, key and DH, and optionally sets up client certificate verification */
void load_certs (boost::asio::ssl::context & ctx);
/**
* If client certificates are used, this is called to verify them.
* @param preverified The TLS preverification status. The callback may revalidate, such as accepting self-signed certs.
*/
bool on_verify_certificate (bool preverified, boost::asio::ssl::verify_context & ctx);
/** The context needs to be shared between sessions to make resumption work */
boost::asio::ssl::context ssl_context;
};
}