mirror of
https://github.com/badaix/snapcast.git
synced 2025-04-28 17:57:05 +02:00
Mutual SSL authentication
This commit is contained in:
parent
be301c6931
commit
85e8d02e5b
9 changed files with 164 additions and 30 deletions
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
if (settings.server.server_certificate.has_value())
|
||||
{
|
||||
LOG(DEBUG, LOG_TAG) << "Loading server certificate\n";
|
||||
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->empty())
|
||||
{
|
||||
ssl_context_.load_verify_file(settings.server.certificate.value().string(), ec);
|
||||
ssl_context_.load_verify_file(settings.server.server_certificate.value().string(), ec);
|
||||
if (ec.failed())
|
||||
throw SnapException("Failed to load certificate: " + settings.server.certificate.value().string() + ": " + ec.message());
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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())
|
||||
{
|
||||
|
|
Loading…
Add table
Reference in a new issue