Mutual SSL authentication

This commit is contained in:
badaix 2025-01-27 22:19:42 +01:00
parent be301c6931
commit 85e8d02e5b
9 changed files with 164 additions and 30 deletions

View file

@ -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;
});

View file

@ -67,7 +67,13 @@ struct ClientSettings
/// server port
size_t port{1704};
/// server certificate
std::optional<std::filesystem::path> certificate;
std::optional<std::filesystem::path> 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
{

View file

@ -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());
}
}
}

View file

@ -147,7 +147,12 @@ int main(int argc, char** argv)
auto port_opt = op.add<Value<size_t>>("p", "port", "(deprecated, use [url]) Server port", 1704, &settings.server.port);
op.add<Value<size_t>>("i", "instance", "Instance id when running multiple instances on the same host", 1, &settings.instance);
op.add<Value<string>>("", "hostID", "Unique host id, default is MAC address", "", &settings.host_id);
auto server_cert_opt = op.add<Implicit<std::filesystem::path>>("", "server-cert", "Verify server with certificate", "default certificates");
auto server_cert_opt =
op.add<Implicit<std::filesystem::path>>("", "server-cert", "Verify server with certificate (PEM format)", "default certificates");
op.add<Value<std::filesystem::path>>("", "cert", "Client certificate file (PEM format)", settings.server.certificate, &settings.server.certificate);
op.add<Value<std::filesystem::path>>("", "cert-key", "Client private key file (PEM format)", settings.server.certificate_key,
&settings.server.certificate_key);
op.add<Value<string>>("", "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<std::thread> threads;
threads.reserve(num_threads);
for (int n = 0; n < num_threads; ++n)
threads.emplace_back([&] { io_context.run(); });
io_context.run();

View file

@ -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)

View file

@ -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");
}

View file

@ -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 =
#
###############################################################################

View file

@ -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<std::filesystem::path> 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<User> 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<std::string> bind_to_address{{"::"}};
};
/// Stream settings
struct Stream
{
size_t port{1704};
@ -102,22 +120,28 @@ struct ServerSettings
std::vector<std::string> 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<User> users; ///< User settings
Http http; ///< HTTP settings
Tcp tcp; ///< TCP settings
Stream stream; ///< Stream settings
StreamingClient streamingclient; ///< Client settings
Logging logging; ///< Logging settings
};

View file

@ -86,6 +86,9 @@ int main(int argc, char* argv[])
conf.add<Value<std::filesystem::path>>("", "ssl.certificate_key", "private key file (PEM format)", settings.ssl.certificate_key,
&settings.ssl.certificate_key);
conf.add<Value<string>>("", "ssl.key_password", "key password (for encrypted private key)", settings.ssl.key_password, &settings.ssl.key_password);
conf.add<Value<bool>>("", "ssl.verify_clients", "Verify client certificates", settings.ssl.verify_clients, &settings.ssl.verify_clients);
auto client_cert_opt =
conf.add<Value<std::filesystem::path>>("", "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())
{