From 85e8d02e5b001e50a691d2729f193b0c9402ddfe Mon Sep 17 00:00:00 2001 From: badaix Date: Mon, 27 Jan 2025 22:19:42 +0100 Subject: [PATCH] Mutual SSL authentication --- client/client_connection.cpp | 4 ++-- client/client_settings.hpp | 8 ++++++- client/controller.cpp | 39 +++++++++++++++++++++++++------ client/snapclient.cpp | 31 +++++++++++++++++++++---- common/popl.hpp | 10 ++++---- server/control_server.cpp | 45 ++++++++++++++++++++++++++++++++++-- server/etc/snapserver.conf | 7 ++++++ server/server_settings.hpp | 40 +++++++++++++++++++++++++------- server/snapserver.cpp | 10 ++++++++ 9 files changed, 164 insertions(+), 30 deletions(-) diff --git a/client/client_connection.cpp b/client/client_connection.cpp index 9f3f4025..b8e39a61 100644 --- a/client/client_connection.cpp +++ b/client/client_connection.cpp @@ -535,7 +535,7 @@ ssl_websocket& ClientConnectionWss::getWs() return ssl_ws_.value(); ssl_ws_.emplace(strand_, ssl_context_); - if (server_.certificate.has_value()) + if (server_.server_certificate.has_value()) { ssl_ws_->next_layer().set_verify_mode(boost::asio::ssl::verify_peer); ssl_ws_->next_layer().set_verify_callback([](bool preverified, boost::asio::ssl::verify_context& ctx) @@ -551,7 +551,7 @@ ssl_websocket& ClientConnectionWss::getWs() char subject_name[256]; X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle()); X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256); - LOG(INFO, LOG_TAG) << "verifying cert: '" << subject_name << "', pre verified: " << preverified << "\n"; + LOG(INFO, LOG_TAG) << "Verifying cert: '" << subject_name << "', pre verified: " << preverified << "\n"; return preverified; }); diff --git a/client/client_settings.hpp b/client/client_settings.hpp index 7d63448a..0e671c00 100644 --- a/client/client_settings.hpp +++ b/client/client_settings.hpp @@ -67,7 +67,13 @@ struct ClientSettings /// server port size_t port{1704}; /// server certificate - std::optional certificate; + std::optional server_certificate; + /// Certificate file + std::filesystem::path certificate; + /// Private key file + std::filesystem::path certificate_key; + /// Password for encrypted key file + std::string key_password; /// Is ssl in use? bool isSsl() const { diff --git a/client/controller.cpp b/client/controller.cpp index d6865ea9..dc81462d 100644 --- a/client/controller.cpp +++ b/client/controller.cpp @@ -80,17 +80,42 @@ Controller::Controller(boost::asio::io_context& io_context, const ClientSettings : io_context_(io_context), ssl_context_(boost::asio::ssl::context::tlsv12_client), timer_(io_context), settings_(settings), stream_(nullptr), decoder_(nullptr), player_(nullptr), serverSettings_(nullptr) { - if (settings.server.isSsl() && settings.server.certificate.has_value()) + if (settings.server.isSsl()) { boost::system::error_code ec; - ssl_context_.set_default_verify_paths(ec); - if (ec.failed()) - LOG(WARNING, LOG_TAG) << "Failed to load system certificates: " << ec << "\n"; - if (!settings.server.certificate->empty()) + if (settings.server.server_certificate.has_value()) { - ssl_context_.load_verify_file(settings.server.certificate.value().string(), ec); + LOG(DEBUG, LOG_TAG) << "Loading server certificate\n"; + ssl_context_.set_default_verify_paths(ec); if (ec.failed()) - throw SnapException("Failed to load certificate: " + settings.server.certificate.value().string() + ": " + ec.message()); + LOG(WARNING, LOG_TAG) << "Failed to load system certificates: " << ec << "\n"; + if (!settings.server.server_certificate->empty()) + { + ssl_context_.load_verify_file(settings.server.server_certificate.value().string(), ec); + if (ec.failed()) + throw SnapException("Failed to load server certificate: " + settings.server.server_certificate.value().string() + ": " + ec.message()); + } + } + + if (!settings.server.certificate.empty() && !settings.server.certificate_key.empty()) + { + if (!settings.server.key_password.empty()) + { + ssl_context_.set_password_callback( + [pw = settings.server.key_password](size_t max_length, boost::asio::ssl::context_base::password_purpose purpose) -> string + { + LOG(DEBUG, LOG_TAG) << "getPassword, purpose: " << purpose << ", max length: " << max_length << "\n"; + return pw; + }); + } + LOG(DEBUG, LOG_TAG) << "Loading certificate file: " << settings.server.certificate << "\n"; + ssl_context_.use_certificate_chain_file(settings.server.certificate.string(), ec); + if (ec.failed()) + throw SnapException("Failed to load certificate: " + settings.server.certificate.string() + ": " + ec.message()); + LOG(DEBUG, LOG_TAG) << "Loading certificate key file: " << settings.server.certificate_key << "\n"; + ssl_context_.use_private_key_file(settings.server.certificate_key.string(), boost::asio::ssl::context::pem, ec); + if (ec.failed()) + throw SnapException("Failed to load private key file: " + settings.server.certificate_key.string() + ": " + ec.message()); } } } diff --git a/client/snapclient.cpp b/client/snapclient.cpp index 281c3f5f..41ca2cf5 100644 --- a/client/snapclient.cpp +++ b/client/snapclient.cpp @@ -147,7 +147,12 @@ int main(int argc, char** argv) auto port_opt = op.add>("p", "port", "(deprecated, use [url]) Server port", 1704, &settings.server.port); op.add>("i", "instance", "Instance id when running multiple instances on the same host", 1, &settings.instance); op.add>("", "hostID", "Unique host id, default is MAC address", "", &settings.host_id); - auto server_cert_opt = op.add>("", "server-cert", "Verify server with certificate", "default certificates"); + auto server_cert_opt = + op.add>("", "server-cert", "Verify server with certificate (PEM format)", "default certificates"); + op.add>("", "cert", "Client certificate file (PEM format)", settings.server.certificate, &settings.server.certificate); + op.add>("", "cert-key", "Client private key file (PEM format)", settings.server.certificate_key, + &settings.server.certificate_key); + op.add>("", "key-password", "Key password (for encrypted private key)", settings.server.key_password, &settings.server.key_password); // PCM device specific #if defined(HAS_ALSA) || defined(HAS_PULSE) || defined(HAS_WASAPI) @@ -349,13 +354,28 @@ int main(int argc, char** argv) if (server_cert_opt->is_set()) { if (server_cert_opt->get_default() == server_cert_opt->value()) - settings.server.certificate = ""; + settings.server.server_certificate = ""; else - settings.server.certificate = std::filesystem::weakly_canonical(server_cert_opt->value()); - if (settings.server.certificate.value_or("").empty()) + settings.server.server_certificate = std::filesystem::weakly_canonical(server_cert_opt->value()); + if (settings.server.server_certificate.value_or("").empty()) LOG(INFO, LOG_TAG) << "Server certificate: default certificates\n"; else - LOG(INFO, LOG_TAG) << "Server certificate: " << settings.server.certificate.value_or("") << "\n"; + LOG(INFO, LOG_TAG) << "Server certificate: " << settings.server.server_certificate.value_or("") << "\n"; + } + + if (!settings.server.certificate.empty() && !settings.server.certificate_key.empty()) + { + namespace fs = std::filesystem; + settings.server.certificate = fs::weakly_canonical(settings.server.certificate); + if (!fs::exists(settings.server.certificate)) + throw SnapException("Certificate file not found: " + settings.server.certificate.native()); + settings.server.certificate_key = fs::weakly_canonical(settings.server.certificate_key); + if (!fs::exists(settings.server.certificate_key)) + throw SnapException("Certificate_key file not found: " + settings.server.certificate_key.native()); + } + else if (settings.server.certificate.empty() != settings.server.certificate_key.empty()) + { + throw SnapException("Both SSL 'certificate' and 'certificate_key' must be set or empty"); } #if !defined(HAS_AVAHI) && !defined(HAS_BONJOUR) @@ -500,6 +520,7 @@ int main(int argc, char** argv) int num_threads = 0; std::vector threads; + threads.reserve(num_threads); for (int n = 0; n < num_threads; ++n) threads.emplace_back([&] { io_context.run(); }); io_context.run(); diff --git a/common/popl.hpp b/common/popl.hpp index 6ee354fe..a59b7073 100644 --- a/common/popl.hpp +++ b/common/popl.hpp @@ -483,7 +483,7 @@ public: std::string print(const Attribute& max_attribute = Attribute::optional) const override; private: - std::string to_string(Option_ptr option) const; + std::string to_string(const Option_ptr& option) const; }; @@ -501,7 +501,7 @@ public: std::string print(const Attribute& max_attribute = Attribute::optional) const override; private: - std::string to_string(Option_ptr option) const; + std::string to_string(const Option_ptr& option) const; }; @@ -1122,7 +1122,7 @@ inline ConsoleOptionPrinter::ConsoleOptionPrinter(const OptionParser* option_par } -inline std::string ConsoleOptionPrinter::to_string(Option_ptr option) const +inline std::string ConsoleOptionPrinter::to_string(const Option_ptr& option) const { std::stringstream line; if (option->short_name() != 0) @@ -1142,7 +1142,7 @@ inline std::string ConsoleOptionPrinter::to_string(Option_ptr option) const std::stringstream defaultStr; if (option->get_default(defaultStr)) { - if (!defaultStr.str().empty()) + if (!defaultStr.str().empty() && (defaultStr.str() != "\"\"")) line << " (=" << defaultStr.str() << ")"; } } @@ -1216,7 +1216,7 @@ inline GroffOptionPrinter::GroffOptionPrinter(const OptionParser* option_parser) } -inline std::string GroffOptionPrinter::to_string(Option_ptr option) const +inline std::string GroffOptionPrinter::to_string(const Option_ptr& option) const { std::stringstream line; if (option->short_name() != 0) diff --git a/server/control_server.cpp b/server/control_server.cpp index fa74df41..9950c67b 100644 --- a/server/control_server.cpp +++ b/server/control_server.cpp @@ -22,6 +22,7 @@ // local headers #include "common/aixlog.hpp" #include "common/json.hpp" +#include "common/snap_exception.hpp" #include "control_session_http.hpp" #include "control_session_tcp.hpp" #include "server_settings.hpp" @@ -54,10 +55,50 @@ ControlServer::ControlServer(boost::asio::io_context& io_context, const ServerSe return pw; }); } + if (!ssl.certificate.empty() && !ssl.certificate_key.empty()) { - ssl_context_.use_certificate_chain_file(ssl.certificate); - ssl_context_.use_private_key_file(ssl.certificate_key, boost::asio::ssl::context::pem); + boost::system::error_code ec; + ssl_context_.use_certificate_chain_file(ssl.certificate, ec); + if (ec.failed()) + throw SnapException("Failed to load certificate: " + settings.ssl.certificate.string() + ": " + ec.message()); + ssl_context_.use_private_key_file(ssl.certificate_key, boost::asio::ssl::context::pem, ec); + if (ec.failed()) + throw SnapException("Failed to load private key file: " + settings.ssl.certificate_key.string() + ": " + ec.message()); + } + + if (settings.ssl.verify_clients) + { + boost::system::error_code ec; + ssl_context_.set_default_verify_paths(ec); + if (ec.failed()) + LOG(WARNING, LOG_TAG) << "Failed to load system certificates: " << ec << "\n"; + for (const auto& cert_path : settings_.ssl.client_certs) + { + LOG(DEBUG, LOG_TAG) << "Loading client certificate: " << cert_path << "\n"; + ssl_context_.load_verify_file(cert_path.string(), ec); + if (ec.failed()) + throw SnapException("Failed to load client certificate: " + cert_path.string() + ": " + ec.message()); + } + + ssl_context_.set_verify_mode(boost::asio::ssl::verify_peer | boost::asio::ssl::verify_fail_if_no_peer_cert); + ssl_context_.set_verify_callback([](bool preverified, boost::asio::ssl::verify_context& ctx) + { + // The verify callback can be used to check whether the certificate that is + // being presented is valid for the peer. For example, RFC 2818 describes + // the steps involved in doing this for HTTPS. Consult the OpenSSL + // documentation for more details. Note that the callback is called once + // for each certificate in the certificate chain, starting from the root + // certificate authority. + + // In this example we will simply print the certificate's subject name. + char subject_name[256]; + X509* cert = X509_STORE_CTX_get_current_cert(ctx.native_handle()); + X509_NAME_oneline(X509_get_subject_name(cert), subject_name, 256); + LOG(INFO, LOG_TAG) << "Verifying cert: '" << subject_name << "', pre verified: " << preverified << "\n"; + + return preverified; + }); } // ssl_context_.use_tmp_dh_file("dh4096.pem"); } diff --git a/server/etc/snapserver.conf b/server/etc/snapserver.conf index aaa7f53a..0c0d275b 100644 --- a/server/etc/snapserver.conf +++ b/server/etc/snapserver.conf @@ -62,6 +62,13 @@ # Password for decryption of the certificate_key (only needed for encrypted certificate_key file) #key_password = +# Verify client certificates +#verify_clients = false + +# List of client CA certificate files, can be configured multiple times +#client_cert = +#client_cert = + # ############################################################################### diff --git a/server/server_settings.hpp b/server/server_settings.hpp index d3935227..fd11025f 100644 --- a/server/server_settings.hpp +++ b/server/server_settings.hpp @@ -31,27 +31,43 @@ struct ServerSettings { + /// Launch settings struct Server { + /// Number of worker threads int threads{-1}; + /// PID file, if running as daemon std::string pid_file{"/var/run/snapserver/pid"}; + /// User when running as deaemon std::string user{"snapserver"}; + /// Group when running as deaemon std::string group; + /// Server data dir std::string data_dir; }; + /// SSL settings struct Ssl { + /// Certificate file std::filesystem::path certificate; + /// Private key file std::filesystem::path certificate_key; + /// Password for encrypted key file std::string key_password; + /// Verify client certificates + bool verify_clients = false; + /// Client CA certificates + std::vector client_certs; + /// @return if SSL is enabled bool enabled() const { return !certificate.empty() && !certificate_key.empty(); } }; + /// User settings struct User { explicit User(const std::string& user_permissions_password) @@ -67,8 +83,8 @@ struct ServerSettings std::string password; }; - std::vector users; + /// HTTP settings struct Http { bool enabled{true}; @@ -82,6 +98,7 @@ struct ServerSettings std::string url_prefix; }; + /// TCP streaming client settings struct Tcp { bool enabled{true}; @@ -89,6 +106,7 @@ struct ServerSettings std::vector bind_to_address{{"::"}}; }; + /// Stream settings struct Stream { size_t port{1704}; @@ -102,22 +120,28 @@ struct ServerSettings std::vector bind_to_address{{"::"}}; }; + /// Client settings struct StreamingClient { + /// Initial volume of new clients uint16_t initialVolume{100}; }; + /// Logging settings struct Logging { + /// log sing std::string sink; + /// log filter std::string filter{"*:info"}; }; - Server server; - Ssl ssl; - Http http; - Tcp tcp; - Stream stream; - StreamingClient streamingclient; - Logging logging; + Server server; ///< Server settings + Ssl ssl; ///< SSL settings + std::vector users; ///< User settings + Http http; ///< HTTP settings + Tcp tcp; ///< TCP settings + Stream stream; ///< Stream settings + StreamingClient streamingclient; ///< Client settings + Logging logging; ///< Logging settings }; diff --git a/server/snapserver.cpp b/server/snapserver.cpp index 2b68c697..f2aa3825 100644 --- a/server/snapserver.cpp +++ b/server/snapserver.cpp @@ -86,6 +86,9 @@ int main(int argc, char* argv[]) conf.add>("", "ssl.certificate_key", "private key file (PEM format)", settings.ssl.certificate_key, &settings.ssl.certificate_key); conf.add>("", "ssl.key_password", "key password (for encrypted private key)", settings.ssl.key_password, &settings.ssl.key_password); + conf.add>("", "ssl.verify_clients", "Verify client certificates", settings.ssl.verify_clients, &settings.ssl.verify_clients); + auto client_cert_opt = + conf.add>("", "ssl.client_cert", "List of client CA certificate files, can be configured multiple times", ""); #if 0 // feature: users // Users setting @@ -276,6 +279,13 @@ int main(int argc, char* argv[]) settings.ssl.certificate_key = make_absolute(settings.ssl.certificate_key); if (!fs::exists(settings.ssl.certificate_key)) throw SnapException("SSL certificate_key file not found: " + settings.ssl.certificate_key.native()); + for (size_t n = 0; n < client_cert_opt->count(); ++n) + { + auto cert_file = std::filesystem::weakly_canonical(client_cert_opt->value(n)); + if (!fs::exists(cert_file)) + throw SnapException("Client certificate file not found: " + cert_file.string()); + settings.ssl.client_certs.push_back(std::move(cert_file)); + } } else if (settings.ssl.certificate.empty() != settings.ssl.certificate_key.empty()) {