Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add bcp-003-03 Certificate Provisioning support #377

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
7 changes: 7 additions & 0 deletions Development/cmake/NmosCppLibraries.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,9 @@ set(NMOS_CPP_NMOS_SOURCES
nmos/control_protocol_utils.cpp
nmos/control_protocol_ws_api.cpp
nmos/did_sdid.cpp
nmos/est_behaviour.cpp
nmos/est_certificate_handlers.cpp
nmos/est_utils.cpp
nmos/events_api.cpp
nmos/events_resources.cpp
nmos/events_ws_api.cpp
Expand Down Expand Up @@ -1030,6 +1033,10 @@ set(NMOS_CPP_NMOS_HEADERS
nmos/control_protocol_ws_api.h
nmos/device_type.h
nmos/did_sdid.h
nmos/est_behaviour.h
nmos/est_certificate_handlers.h
nmos/est_utils.h
nmos/est_versions.h
nmos/event_type.h
nmos/events_api.h
nmos/events_resources.h
Expand Down
21 changes: 21 additions & 0 deletions Development/nmos-cpp-node/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "nmos/authorization_redirect_api.h"
#include "nmos/authorization_state.h"
#include "nmos/control_protocol_state.h"
#include "nmos/est_behaviour.h"
#include "nmos/jwks_uri_api.h"
#include "nmos/log_gate.h"
#include "nmos/model.h"
Expand Down Expand Up @@ -147,6 +148,15 @@ int main(int argc, char* argv[])
.on_get_control_protocol_method_descriptor(nmos::make_get_control_protocol_method_descriptor_handler(control_protocol_state));
}

// only configure communication with EST server if BCP-003-03 is required
if (nmos::experimental::fields::est_enabled(node_model.settings))
{
node_implementation
.on_ca_certificate_received(nmos::experimental::make_ca_certificate_received_handler(node_model.settings, gate))
.on_rsa_server_certificate_received(nmos::experimental::make_rsa_server_certificate_received_handler(node_model.settings, gate))
.on_ecdsa_server_certificate_received(nmos::experimental::make_ecdsa_server_certificate_received_handler(node_model.settings, gate));
}

// Set up the node server

auto node_server = nmos::experimental::make_node_server(node_model, node_implementation, log_model, gate);
Expand Down Expand Up @@ -250,6 +260,17 @@ int main(int argc, char* argv[])
}
}

// only configure communication with EST server if BCP-003-03 is required
if (nmos::experimental::fields::est_enabled(node_model.settings))
{
auto load_ca_certificates = node_implementation.load_ca_certificates;
auto load_client_certificate = node_implementation.load_client_certificate;
auto ca_certificate_received = node_implementation.ca_certificate_received;
auto rsa_server_certificate_received = node_implementation.rsa_server_certificate_received;
auto ecdsa_server_certificate_received = node_implementation.ecdsa_server_certificate_received;
node_server.thread_functions.push_back([&, load_ca_certificates, load_client_certificate, ca_certificate_received, rsa_server_certificate_received, ecdsa_server_certificate_received] { nmos::experimental::est_behaviour_thread(node_model, load_ca_certificates, load_client_certificate, ca_certificate_received, rsa_server_certificate_received, ecdsa_server_certificate_received, gate); });
}

// Open the API ports and start up node operation (including the DNS-SD advertisements)

slog::log<slog::severities::info>(gate, SLOG_FLF) << "Preparing for connections";
Expand Down
21 changes: 21 additions & 0 deletions Development/nmos-cpp-registry/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include <iostream>
#include "nmos/authorization_behaviour.h"
#include "nmos/authorization_state.h"
#include "nmos/est_behaviour.h"
#include "nmos/log_gate.h"
#include "nmos/model.h"
#include "nmos/ocsp_behaviour.h"
Expand Down Expand Up @@ -123,6 +124,15 @@ int main(int argc, char* argv[])
.on_ws_validate_authorization(nmos::experimental::make_ws_validate_authorization_handler(registry_model, authorization_state, nmos::experimental::make_validate_authorization_token_handler(authorization_state, gate), gate));
}

// only configure communication with EST server if BCP-003-03 is required
if (nmos::experimental::fields::est_enabled(registry_model.settings))
{
registry_implementation
.on_ca_certificate_received(nmos::experimental::make_ca_certificate_received_handler(registry_model.settings, gate))
.on_rsa_server_certificate_received(nmos::experimental::make_rsa_server_certificate_received_handler(registry_model.settings, gate))
.on_ecdsa_server_certificate_received(nmos::experimental::make_ecdsa_server_certificate_received_handler(registry_model.settings, gate));
}

// Set up the registry server

auto registry_server = nmos::experimental::make_registry_server(registry_model, registry_implementation, log_model, gate);
Expand Down Expand Up @@ -158,6 +168,17 @@ int main(int argc, char* argv[])
registry_server.thread_functions.push_back([&, load_ca_certificates] { authorization_token_issuer_thread(registry_model, authorization_state, load_ca_certificates, gate); });
}

// only configure communication with EST server if BCP-003-03 is required
if (nmos::experimental::fields::est_enabled(registry_model.settings))
{
auto load_ca_certificates = registry_implementation.load_ca_certificates;
auto load_client_certificate = registry_implementation.load_client_certificate;
auto ca_certificate_received = registry_implementation.ca_certificate_received;
auto rsa_server_certificate_received = registry_implementation.rsa_server_certificate_received;
auto ecdsa_server_certificate_received = registry_implementation.ecdsa_server_certificate_received;
registry_server.thread_functions.push_back([&, load_ca_certificates, load_client_certificate, ca_certificate_received, rsa_server_certificate_received, ecdsa_server_certificate_received] { nmos::experimental::est_behaviour_thread(registry_model, load_ca_certificates, load_client_certificate, ca_certificate_received, rsa_server_certificate_received, ecdsa_server_certificate_received, gate); });
}

// Open the API ports and start up registry management

slog::log<slog::severities::info>(gate, SLOG_FLF) << "Preparing for connections";
Expand Down
134 changes: 134 additions & 0 deletions Development/nmos/certificate_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,45 @@ namespace nmos
};
}

// construct callback to load client certificate from files based on settings, see nmos/certificate_settings.h
load_client_certificate_handler make_load_client_certificate_handler(const nmos::settings& settings, slog::base_gate& gate)
{
// load the client private key and certificate chain from files
const auto client_certificate = nmos::experimental::fields::client_certificate(settings);

return [&, client_certificate]()
{
slog::log<slog::severities::info>(gate, SLOG_FLF) << "Load client private key and certificate chain";

const auto private_key_file = nmos::experimental::fields::private_key_file(client_certificate);
const auto certificate_chain_file = nmos::experimental::fields::certificate_chain_file(client_certificate);

utility::stringstream_t pkey;
if (private_key_file.empty())
{
slog::log<slog::severities::warning>(gate, SLOG_FLF) << "Missing client private key file";
}
else
{
utility::ifstream_t pkey_file(private_key_file);
pkey << pkey_file.rdbuf();
}

utility::stringstream_t cert_chain;
if (certificate_chain_file.empty())
{
slog::log<slog::severities::warning>(gate, SLOG_FLF) << "Missing client certificate chain file";
}
else
{
utility::ifstream_t cert_chain_file(certificate_chain_file);
cert_chain << cert_chain_file.rdbuf();
}

return (nmos::certificate(pkey.str(), cert_chain.str()));
};
}

// construct callback to load server certificates from files based on settings, see nmos/certificate_settings.h
load_server_certificates_handler make_load_server_certificates_handler(const nmos::settings& settings, slog::base_gate& gate)
{
Expand Down Expand Up @@ -178,4 +217,99 @@ namespace nmos
return private_keys;
};
}

// construct callback to save certification authorities to file based on settings, see nmos/certificate_settings.h
save_ca_certificates_handler make_save_ca_certificates_handler(const nmos::settings& settings, slog::base_gate& gate)
{
const auto ca_certificate_file = nmos::experimental::fields::ca_certificate_file(settings);

return [&, ca_certificate_file](const utility::string_t& cacert)
{
// The Root CA is in pem format
slog::log<slog::severities::info>(gate, SLOG_FLF) << "Save certification authorities file";

utility::ofstream_t file(ca_certificate_file, std::ios::out | std::ios::trunc);
if (file.is_open())
{
file << cacert;
file.close();
}
};
}

// construct callback to save server certificates to files based on settings, see nmos/certificate_settings.h
save_server_certificate_handler make_save_server_certificate_handler(const nmos::key_algorithm& key_algorithm, const nmos::settings& settings, slog::base_gate& gate)
{
const auto server_certificates = nmos::experimental::fields::server_certificates(settings);
if (0 == server_certificates.size())
{
slog::log<slog::severities::warning>(gate, SLOG_FLF) << "Missing server certificates";
return nullptr;
}

const auto server_certificates_value = server_certificates.as_array();

auto found = std::find_if(server_certificates_value.begin(), server_certificates_value.end(), [&key_algorithm](const web::json::value& server_certificate)
{
return nmos::experimental::fields::key_algorithm(server_certificate) == key_algorithm.name;
});
if (server_certificates_value.end() == found)
{
slog::log<slog::severities::warning>(gate, SLOG_FLF) << "Missing " << utility::us2s(key_algorithm.name) << " server certificate";
return nullptr;
}
const auto server_certificate = *found;

return [&, key_algorithm, server_certificate](const nmos::certificate& certificate)
{
// The key and the certificate are in pem format

slog::log<slog::severities::info>(gate, SLOG_FLF) << "Save server " << utility::us2s(key_algorithm.name) << " private keys and certificate chains";

const auto private_key_file = nmos::experimental::fields::private_key_file(server_certificate);
const auto certificate_chain_file = nmos::experimental::fields::certificate_chain_file(server_certificate);

utility::stringstream_t pkey;
if (private_key_file.empty())
{
slog::log<slog::severities::warning>(gate, SLOG_FLF) << "Missing server private key file";
}
else
{
utility::ofstream_t key_file(private_key_file, std::ios::out | std::ios::trunc);
if (key_file.is_open())
{
key_file << certificate.private_key;
key_file.close();
}
}

std::stringstream cert_chain;
if (certificate_chain_file.empty())
{
slog::log<slog::severities::warning>(gate, SLOG_FLF) << "Missing server certificate chain file";
}
else
{
utility::ofstream_t cert_chain_file(certificate_chain_file, std::ios::out | std::ios::trunc);
if (cert_chain_file.is_open())
{
cert_chain_file << certificate.certificate_chain;
cert_chain_file.close();
}
}
};
}

// construct callback to save ECDSA server certificate to file based on settings, see nmos/certificate_settings.h
save_server_certificate_handler make_save_ecdsa_server_certificate_handler(const nmos::settings& settings, slog::base_gate& gate)
{
return make_save_server_certificate_handler(nmos::key_algorithms::ECDSA, settings, gate);
}

// construct callback to save RSA server certificate to file based on settings, see nmos/certificate_settings.h
save_server_certificate_handler make_save_rsa_server_certificate_handler(const nmos::settings& settings, slog::base_gate& gate)
{
return make_save_server_certificate_handler(nmos::key_algorithms::RSA, settings, gate);
}
}
27 changes: 27 additions & 0 deletions Development/nmos/certificate_handlers.h
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,16 @@ namespace nmos
nmos::key_algorithm key_algorithm;
utility::string_t private_key;
// the chain should be sorted starting with the end entity's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA
// see https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.6 for client certificate
// see https://datatracker.ietf.org/doc/html/rfc5246#section-7.4.2 for server certificate
utility::string_t certificate_chain;
};

// callback to supply a client certificate
// this callback is executed when opening the HTTP or WebSocket client
// this callback should not throw exceptions
typedef std::function<certificate()> load_client_certificate_handler;

// callback to supply a list of server certificates
// this callback is executed when a connection is accepted by the HTTP or WebSocket listener
// this callback should not throw exceptions
Expand All @@ -68,9 +75,20 @@ namespace nmos
// callback to supply a list of RSA private keys
typedef std::function<std::vector<utility::string_t>()> load_rsa_private_keys_handler;

// callback to save trusted root CA certificate(s) in PEM format
// this callback should not throw exceptions
typedef std::function<void(const utility::string_t& ca_certificates)> save_ca_certificates_handler;

// callback to save server certificate
// this callback should not throw exceptions
typedef std::function<void(const certificate& certificate)> save_server_certificate_handler;

// construct callback to load certification authorities from file based on settings, see nmos/certificate_settings.h
load_ca_certificates_handler make_load_ca_certificates_handler(const nmos::settings& settings, slog::base_gate& gate);

// construct callback to load client certificate from files based on settings, see nmos/certificate_settings.h
load_client_certificate_handler make_load_client_certificate_handler(const nmos::settings& settings, slog::base_gate& gate);

// construct callback to load server certificates from files based on settings, see nmos/certificate_settings.h
load_server_certificates_handler make_load_server_certificates_handler(const nmos::settings& settings, slog::base_gate& gate);

Expand All @@ -79,6 +97,15 @@ namespace nmos

// construct callback to load server RSA private key files based on settings, see nmos/certificate_settings.h
load_rsa_private_keys_handler make_load_rsa_private_keys_handler(const nmos::settings& settings, slog::base_gate& gate);

// construct callback to save certification authorities to file based on settings, see nmos/certificate_settings.h
save_ca_certificates_handler make_save_ca_certificates_handler(const nmos::settings& settings, slog::base_gate& gate);

// construct callback to save ECDSA server certificate to file based on settings, see nmos/certificate_settings.h
save_server_certificate_handler make_save_ecdsa_server_certificate_handler(const nmos::settings& settings, slog::base_gate& gate);

// construct callback to save RSA server certificate to file based on settings, see nmos/certificate_settings.h
save_server_certificate_handler make_save_rsa_server_certificate_handler(const nmos::settings& settings, slog::base_gate& gate);
}

#endif
9 changes: 8 additions & 1 deletion Development/nmos/certificate_settings.h
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,19 @@ namespace nmos
// private_key_file (attribute of server_certificates objects): full path of private key file in PEM format
const web::json::field_as_string_or private_key_file{ U("private_key_file"), U("") };

// certificate_chain_file (attribute of server_certificates objects): full path of certificate chain file in PEM format, which must be sorted
// certificate_chain_file (attribute of server_certificates and client_certificate objects): full path of certificate chain file in PEM format, which must be sorted
// starting with the server's certificate, followed by any intermediate CA certificates, and ending with the highest level (root) CA
// on Windows, if C++ REST SDK is built with CPPREST_HTTP_LISTENER_IMPL=httpsys (reported as "listener=httpsys" by nmos::get_build_settings_info)
// one of the certificates must also be bound to each port e.g. using 'netsh add sslcert'
const web::json::field_as_string_or certificate_chain_file{ U("certificate_chain_file"), U("") };

// client_certificate [registry, node]: a client certificate object, which has the full paths of private key file and certificate chain file
// the value must be an object like { "private_key_file": "client-key.pem, "certificate_chain_file": "client-chain.pem" }
// see private_key_file and certificate_chain_file above
// note: on windows, if C++ REST SDK is built with CPPREST_HTTP_CLIENT_IMPL=winhttp (reported as "client=winhttp" by nmos::get_build_settings_info)
// the certificate_chain_file must be in PKCS#12 format, storing the certificate chain and the private key
const web::json::field_as_value_or client_certificate{ U("client_certificate"), web::json::value_of({ { private_key_file, U("") }, { certificate_chain_file, U("") } }) };

// dh_param_file [registry, node]: Diffie-Hellman parameters file in PEM format for ephemeral key exchange support, or empty string for no support
const web::json::field_as_string_or dh_param_file{ U("dh_param_file"), U("") };

Expand Down
Loading
Loading