diff --git a/Development/cmake/NmosCppLibraries.cmake b/Development/cmake/NmosCppLibraries.cmake index b49f78736..970a8a60a 100644 --- a/Development/cmake/NmosCppLibraries.cmake +++ b/Development/cmake/NmosCppLibraries.cmake @@ -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 @@ -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 diff --git a/Development/nmos-cpp-node/main.cpp b/Development/nmos-cpp-node/main.cpp index 857fac13e..a612ce13a 100644 --- a/Development/nmos-cpp-node/main.cpp +++ b/Development/nmos-cpp-node/main.cpp @@ -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" @@ -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); @@ -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(gate, SLOG_FLF) << "Preparing for connections"; diff --git a/Development/nmos-cpp-registry/main.cpp b/Development/nmos-cpp-registry/main.cpp index a98b253b7..15a0415ea 100644 --- a/Development/nmos-cpp-registry/main.cpp +++ b/Development/nmos-cpp-registry/main.cpp @@ -2,6 +2,7 @@ #include #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" @@ -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); @@ -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(gate, SLOG_FLF) << "Preparing for connections"; diff --git a/Development/nmos/certificate_handlers.cpp b/Development/nmos/certificate_handlers.cpp index ed52a73ab..2d750f19f 100644 --- a/Development/nmos/certificate_handlers.cpp +++ b/Development/nmos/certificate_handlers.cpp @@ -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(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(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(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) { @@ -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(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(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(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(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(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(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); + } } diff --git a/Development/nmos/certificate_handlers.h b/Development/nmos/certificate_handlers.h index 043a50e40..5854314c9 100644 --- a/Development/nmos/certificate_handlers.h +++ b/Development/nmos/certificate_handlers.h @@ -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 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 @@ -68,9 +75,20 @@ namespace nmos // callback to supply a list of RSA private keys typedef std::function()> 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 save_ca_certificates_handler; + + // callback to save server certificate + // this callback should not throw exceptions + typedef std::function 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); @@ -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 diff --git a/Development/nmos/certificate_settings.h b/Development/nmos/certificate_settings.h index a01fc90f0..5f19af546 100644 --- a/Development/nmos/certificate_settings.h +++ b/Development/nmos/certificate_settings.h @@ -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("") }; diff --git a/Development/nmos/client_utils.cpp b/Development/nmos/client_utils.cpp index 681c520da..c29bd3d8a 100644 --- a/Development/nmos/client_utils.cpp +++ b/Development/nmos/client_utils.cpp @@ -37,23 +37,43 @@ namespace nmos // cf. preprocessor conditions in nmos::make_http_client_config and nmos::make_websocket_client_config #if !defined(_WIN32) || !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) template - inline std::function make_client_ssl_context_callback(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + inline std::function make_client_ssl_context_callback(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, slog::base_gate& gate) { if (!load_ca_certificates) { load_ca_certificates = make_load_ca_certificates_handler(settings, gate); } - return [load_ca_certificates](boost::asio::ssl::context& ctx) + return [load_ca_certificates, load_client_certificate](boost::asio::ssl::context& ctx) { try { ctx.set_options(nmos::details::ssl_context_options); + // for server certificate validation const auto cacerts = utility::us2s(load_ca_certificates()); ctx.add_certificate_authority(boost::asio::buffer(cacerts.data(), cacerts.size())); set_cipher_list(ctx, nmos::details::ssl_cipher_list); + + // for client certificate support + // ignore if client certificate handler is not provided + if (load_client_certificate) + { + const auto client_certificate = load_client_certificate(); + const auto key = utility::us2s(client_certificate.private_key); + if (0 == key.size()) + { + throw ExceptionType({}, "Missing client private key"); + } + const auto cert_chain = utility::us2s(client_certificate.certificate_chain); + if (0 == cert_chain.size()) + { + throw ExceptionType({}, "Missing client certificate chain"); + } + ctx.use_private_key(boost::asio::buffer(key.data(), key.size()), boost::asio::ssl::context_base::pem); + ctx.use_certificate_chain(boost::asio::buffer(cert_chain.data(), cert_chain.size())); + } } catch (const boost::system::system_error& e) { @@ -158,25 +178,33 @@ namespace nmos // construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout - web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, slog::base_gate& gate) { web::http::client::http_client_config config; const auto proxy = proxy_uri(settings); if (!proxy.is_empty()) config.set_proxy(proxy); if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); #if !defined(_WIN32) && !defined(__cplusplus_winrt) || defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) - if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, gate)); + if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, load_client_certificate, gate)); config.set_nativehandle_options(details::make_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate)); #endif return config; } + web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) + { + return make_http_client_config(secure, settings, load_ca_certificates, {}, gate); + } // construct client config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, slog::base_gate& gate) + { + return make_http_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, load_client_certificate, gate); + } web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate) { - return make_http_client_config(nmos::experimental::fields::client_secure(settings), settings, load_ca_certificates, gate); + return make_http_client_config(settings, load_ca_certificates, load_client_certificate_handler{}, gate); } // construct oauth2 config with the bearer token @@ -225,7 +253,7 @@ namespace nmos if (!proxy.is_empty()) config.set_proxy(proxy); if (secure) config.set_validate_certificates(nmos::experimental::fields::validate_certificates(settings)); #if !defined(_WIN32) || !defined(__cplusplus_winrt) - if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, gate)); + if (secure) config.set_ssl_context_callback(details::make_client_ssl_context_callback(settings, load_ca_certificates, {}, gate)); #ifdef CPPRESTSDK_ENABLE_BIND_WEBSOCKET_CLIENT config.set_nativehandle_options(details::make_ws_client_nativehandle_options(secure, nmos::experimental::fields::client_address(settings), gate)); #endif diff --git a/Development/nmos/client_utils.h b/Development/nmos/client_utils.h index c1f83e3f7..485152c0a 100644 --- a/Development/nmos/client_utils.h +++ b/Development/nmos/client_utils.h @@ -14,9 +14,11 @@ namespace nmos { // construct client config based on specified secure flag and settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout + web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, slog::base_gate& gate); web::http::client::http_client_config make_http_client_config(bool secure, const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // construct client config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. request timeout + web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, slog::base_gate& gate); web::http::client::http_client_config make_http_client_config(const nmos::settings& settings, load_ca_certificates_handler load_ca_certificates, slog::base_gate& gate); // construct client config including OAuth 2.0 config based on settings, e.g. using the specified proxy and OCSP config // with the remaining options defaulted, e.g. authorization request timeout diff --git a/Development/nmos/est_behaviour.cpp b/Development/nmos/est_behaviour.cpp new file mode 100644 index 000000000..1b1e0341f --- /dev/null +++ b/Development/nmos/est_behaviour.cpp @@ -0,0 +1,1526 @@ +#include "nmos/est_behaviour.h" + +#include // for boost::to_upper_copy +#include // for tm and strptime +#include "pplx/pplx_utils.h" // for pplx::complete_at +#include "cpprest/http_client.h" +#include "cpprest/json_validator.h" +#include "mdns/service_discovery.h" +#include "nmos/api_utils.h" +#include "nmos/client_utils.h" +#include "nmos/est_versions.h" +#include "nmos/est_utils.h" +#include "nmos/json_schema.h" +#include "nmos/mdns.h" +#include "nmos/model.h" +#include "nmos/random.h" +#include "nmos/slog.h" +#include "nmos/thread_utils.h" // for wait_until, reverse_lock_guard +#include "ssl/ssl_utils.h" +#if (defined(_WIN32) || defined(__cplusplus_winrt)) && !defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) +#include +#include +#include +#include "nmos/certificate_settings.h" +#endif + +namespace nmos +{ + namespace experimental + { + namespace fields + { + const web::json::field_as_string_or ver{ U("ver"), {} }; + //const web::json::field_as_integer_or pri{ U("pri"), nmos::service_priorities::no_priority }; already defined in settings.h + const web::json::field_as_string_or uri{ U("uri"), {} }; + } + + namespace details + { + struct est_shared_state + { + load_ca_certificates_handler load_ca_certificates; + load_client_certificate_handler load_client_certificate; + + receive_ca_certificate_handler receive_ca_certificate; + // cacerts data (ca certificate chain) + utility::string_t cacerts; + // is cacerts expired (the shortest expiry CA has reached the renewal time) + bool renewal; + + bool est_service_error; + + nmos::details::seed_generator seeder; + std::default_random_engine engine; + std::unique_ptr client; + + explicit est_shared_state(load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, receive_ca_certificate_handler receive_ca_certificate) + : load_ca_certificates(std::move(load_ca_certificates)), load_client_certificate(std::move(load_client_certificate)), receive_ca_certificate(std::move(receive_ca_certificate)), cacerts({}), renewal(false), est_service_error(false), engine(seeder) + {} + }; + + typedef std::function verify_cert_handler; + + struct cert_shared_state + { + receive_server_certificate_handler received; + // private key data + utility::string_t key; + // certificate signing request data + utility::string_t csr; + // certificate data + utility::string_t cert; + + std::unique_ptr client; + + // how many seconds before next certificate fetch + std::chrono::seconds delay; + // is certificate expired + bool expired; + + // verify certificate + verify_cert_handler verify; + + // mutex to be used to protect the crl_urls from simultaneous access by multiple threads + mutable nmos::mutex mutex; + + // certificate revocation URLs + std::vector crl_urls; + + nmos::read_lock read_lock() const { return nmos::read_lock{ mutex }; } + nmos::write_lock write_lock() const { return nmos::write_lock{ mutex }; } + + explicit cert_shared_state(receive_server_certificate_handler received, utility::string_t private_key = {}, utility::string_t csr = {}) : received(std::move(received)), key(std::move(private_key)), csr(std::move(csr)), cert({}), delay(std::chrono::seconds(0)), expired(false) + {} + }; + } + + namespace details + { + void est_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, receive_ca_certificate_handler ca_certificate_received, receive_server_certificate_handler rsa_server_certificate_received, receive_server_certificate_handler ecdsa_server_certificate_received, mdns::service_discovery& discovery, slog::base_gate& gate); + + bool do_ca_certificates_requests(nmos::model& model, est_shared_state& est_state, slog::base_gate& gate); + bool do_certificates_requests(nmos::model& model, est_shared_state& est_state, cert_shared_state& rsa_state, cert_shared_state& ecdsa_state, slog::base_gate& gate); + void do_renewal_certificates_and_certificates_revocation_requests(nmos::model& model, est_shared_state& est_state, cert_shared_state& rsa_state, cert_shared_state& ecdsa_state, slog::base_gate& gate); + + // background service discovery + void est_services_background_discovery(nmos::model& model, mdns::service_discovery& discovery, slog::base_gate& gate); + + // service discovery + bool discover_est_services(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()); + bool has_discovered_est_services(const nmos::model& model); + } + + // uses the default DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void est_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, receive_ca_certificate_handler ca_certificate_received, receive_server_certificate_handler rsa_server_certificate_received, receive_server_certificate_handler ecdsa_server_certificate_received, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::est_behaviour)); + + mdns::service_discovery discovery(gate); + + details::est_behaviour_thread(model, std::move(load_ca_certificates), std::move(load_client_certificate), std::move(ca_certificate_received), std::move(rsa_server_certificate_received), std::move(ecdsa_server_certificate_received), discovery, gate); + } + + // uses the specified DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void est_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, receive_ca_certificate_handler ca_certificate_received, receive_server_certificate_handler rsa_server_certificate_received, receive_server_certificate_handler ecdsa_server_certificate_received, mdns::service_discovery& discovery, slog::base_gate& gate_) + { + nmos::details::omanip_gate gate(gate_, nmos::stash_category(nmos::categories::est_behaviour)); + + details::est_behaviour_thread(model, std::move(load_ca_certificates), std::move(load_client_certificate), std::move(ca_certificate_received), std::move(rsa_server_certificate_received), std::move(ecdsa_server_certificate_received), discovery, gate); + } + + void details::est_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, receive_ca_certificate_handler ca_certificate_received, receive_server_certificate_handler rsa_server_certificate_received, receive_server_certificate_handler ecdsa_server_certificate_received, mdns::service_discovery& discovery, slog::base_gate& gate) + { + enum + { + initial_discovery, + fetch_cacerts, + fetch_certificates, + renewal_certificates_and_certificates_revocation, + rediscovery, + background_discovery + } mode = initial_discovery; + + // If the chosen EST API does not respond correctly at any time, another EST API should be selected from the discovered list. + + with_write_lock(model.mutex, [&model] { model.settings[nmos::experimental::fields::est_services] = web::json::value::array(); }); + + nmos::details::seed_generator discovery_backoff_seeder; + std::default_random_engine discovery_backoff_engine(discovery_backoff_seeder); + double discovery_backoff = 0; + + est_shared_state est_state{ load_ca_certificates, load_client_certificate, ca_certificate_received }; + cert_shared_state rsa_state{ rsa_server_certificate_received }; + cert_shared_state ecdsa_state{ ecdsa_server_certificate_received }; + + // continue until the server is being shut down + for (;;) + { + if (with_read_lock(model.mutex, [&] { return model.shutdown; })) break; + + switch (mode) + { + case initial_discovery: + case rediscovery: + if (0 != discovery_backoff) + { + auto lock = model.read_lock(); + const auto random_backoff = std::uniform_real_distribution<>(0, discovery_backoff)(discovery_backoff_engine); + slog::log(gate, SLOG_FLF) << "Waiting to retry EST API discovery for about " << std::fixed << std::setprecision(3) << random_backoff << " seconds (current backoff limit: " << discovery_backoff << " seconds)"; + model.wait_for(lock, std::chrono::milliseconds(std::chrono::milliseconds::rep(1000 * random_backoff)), [&] { return model.shutdown; }); + if (model.shutdown) break; + } + + // The Node performs a DNS-SD browse for services of type '_nmos-certs._tcp' as specified. + if (details::discover_est_services(model, discovery, gate)) + { + mode = fetch_cacerts; + + // If the Node is unable to contact the EST API, the Node implements an exponential backoff algorithm + // to avoid overloading the EST API in the event of a system restart. + auto lock = model.read_lock(); + discovery_backoff = (std::min)((std::max)((double)nmos::fields::discovery_backoff_min(model.settings), discovery_backoff * nmos::fields::discovery_backoff_factor(model.settings)), (double)nmos::fields::discovery_backoff_max(model.settings)); + } + else + { + mode = background_discovery; + } + break; + + case fetch_cacerts: + // fetch Root CA certificates + mode = do_ca_certificates_requests(model, est_state, gate) ? fetch_certificates : rediscovery; + break; + + case fetch_certificates: + // generate CSRs then request to sign them + mode = do_certificates_requests(model, est_state, rsa_state, ecdsa_state, gate) ? renewal_certificates_and_certificates_revocation : rediscovery; + break; + + case renewal_certificates_and_certificates_revocation: + // start a background task to renewal CA certificates + // start background tasks to renewal server certificates + // start a background task to do certificates revocation + do_renewal_certificates_and_certificates_revocation_requests(model, est_state, rsa_state, ecdsa_state, gate); + + // reaching here, either Root CA certificate(s) need to be renrewed, server certificate(s) has expired, + // or no further EST APIs be available or TTLs on advertised services expired. + mode = (est_state.renewal || rsa_state.expired || ecdsa_state.expired) ? fetch_cacerts : rediscovery; + break; + + case background_discovery: + details::est_services_background_discovery(model, discovery, gate); + + if (details::has_discovered_est_services(model)) + { + mode = fetch_cacerts; + } + + break; + } + } + } + + // service discovery + namespace details + { + static web::json::value make_service(const resolved_service& service) + { + using web::json::value; + + return web::json::value_of({ + { nmos::experimental::fields::ver, value::string(make_api_version(service.first.first)) }, + { nmos::fields::pri, service.first.second }, + { nmos::experimental::fields::uri, value::string(service.second.to_string()) } + }); + } + + static resolved_service parse_service(const web::json::value& data) + { + return { + { parse_api_version(nmos::experimental::fields::ver(data)), nmos::fields::pri(data) }, + web::uri(nmos::experimental::fields::uri(data)) + }; + } + + // get the fallback EST service from settings (if present) + resolved_service get_est_service(const nmos::settings& settings) + { + if (settings.has_field(nmos::experimental::fields::est_address)) + { + const auto api_selector = nmos::experimental::fields::est_selector(settings); + + return { { {}, 0 }, + web::uri_builder() + .set_scheme(nmos::http_scheme(settings)) + .set_host(nmos::experimental::fields::est_address(settings)) + .set_port(nmos::experimental::fields::est_port(settings)) + .set_path(U("/.well-known/est")).append_path(!api_selector.empty() ? U("/") + api_selector : U("")) + .to_uri() }; + } + return {}; + } + + // query DNS Service Discovery for any EST API based on settings + bool discover_est_services(nmos::base_model& model, mdns::service_discovery& discovery, slog::base_gate& gate, const pplx::cancellation_token& token) + { + slog::log(gate, SLOG_FLF) << "Trying EST API discovery"; + + // lock to read settings, then unlock to wait for the discovery task to complete + auto est_services = with_read_lock(model.mutex, [&] + { + auto& settings = model.settings; + + if (nmos::service_priorities::no_priority != nmos::fields::highest_pri(settings)) + { + slog::log(gate, SLOG_FLF) << "Attempting discovery of a EST API in domain: " << nmos::get_domain(settings); + + return nmos::experimental::resolve_service_(discovery, nmos::service_types::est, settings, token); + } + else + { + return pplx::task_from_result(std::list{}); + } + }).get(); + + with_write_lock(model.mutex, [&] + { + if (!est_services.empty()) + { + slog::log(gate, SLOG_FLF) << "Discovered " << est_services.size() << " EST API(s)"; + } + else + { + slog::log(gate, SLOG_FLF) << "Did not discover a suitable EST API via DNS-SD"; + + auto fallback_authorization_service = get_est_service(model.settings); + if (!fallback_authorization_service.second.is_empty()) + { + est_services.push_back(fallback_authorization_service); + } + } + + if (!est_services.empty()) slog::log(gate, SLOG_FLF) << "Using the EST API(s):" << slog::log_manip([&](slog::log_statement& s) + { + for (auto& est_service : est_services) + { + s << '\n' << est_service.second.to_string(); + } + }); + + model.settings[nmos::experimental::fields::est_services] = web::json::value_from_elements(est_services | boost::adaptors::transformed([](const resolved_service& est_service) { return make_service(est_service); })); + + model.notify(); + }); + + return !est_services.empty(); + } + + bool empty_est_services(const nmos::settings& settings) + { + return web::json::empty(nmos::experimental::fields::est_services(settings)); + } + + bool has_discovered_est_services(const nmos::model& model) + { + return with_read_lock(model.mutex, [&] { return !empty_est_services(model.settings); }); + } + + // "The Node selects a EST API to use based on the priority" + resolved_service top_est_service(const nmos::settings& settings) + { + const auto value = web::json::front(nmos::experimental::fields::est_services(settings)); + return parse_service(value); + } + + // If the chosen EST API does not respond correctly at any time, + // another EST API should be selected from the discovered list. + void pop_est_service(nmos::settings& settings) + { + web::json::pop_front(nmos::experimental::fields::est_services(settings)); + } + } + + // EST operation + namespace details + { +#if defined(_WIN32) + // convert a string representation of time to a time tm structure + // See https://stackoverflow.com/questions/321849/strptime-equivalent-on-windows + char* strptime(const char* s, const char* f, tm* tm) + { + std::istringstream input(s); + input.imbue(std::locale(setlocale(LC_ALL, nullptr))); + input >> std::get_time(tm, f); + if (input.fail()) { + return nullptr; + } + return (char*)(s + input.tellg()); + } +#endif + std::chrono::seconds parse_retry_after(const std::string& retry_after) + { + const bst::regex http_date_regex(R"([a-z|A-Z]{3}, \d{2} [a-z|A-Z]{3} \d{4} \d{2}:\d{2}:\d{2} [a-z|A-Z]{3})"); + + // parse the retry-after header to obtain the retry next value + // See https://tools.ietf.org/html/rfc7231#section-7.1.3 + // e.g. Retry-After: Fri, 31 Dec 1999 23:59:59 GMT + // Retry-After: 120 + if (bst::regex_match(retry_after, http_date_regex)) + { + tm when; + strptime(retry_after.c_str(), "%a, %d %b %Y %H:%M:%S %Z", &when); + + time_t now = time(0); + const auto diff = difftime(mktime(&when), now); + return std::chrono::seconds(diff > 0 ? (int)diff : 0); + } + else + { + int delay{ 0 }; + std::istringstream(retry_after) >> delay; + return std::chrono::seconds(delay); + } + } + + web::http::client::http_client_config make_est_client_config(const nmos::settings& settings_, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, bool disable_validate_server_certificates, slog::base_gate& gate) + { + nmos::settings settings{ settings_ }; + // overrule server certificates validation + if (disable_validate_server_certificates) + { + settings[nmos::experimental::fields::validate_certificates] = web::json::value::boolean(!disable_validate_server_certificates); + } + + auto config = nmos::make_http_client_config(settings, std::move(load_ca_certificates), std::move(load_client_certificate), gate); + config.set_timeout(std::chrono::seconds(nmos::experimental::fields::est_request_max(settings))); + +#if (defined(_WIN32) || defined(__cplusplus_winrt)) && !defined(CPPREST_FORCE_HTTP_CLIENT_ASIO) + config.set_nativehandle_options([=](web::http::client::native_handle hRequest) + { + // the client_certificate_file must be in PKCS #12 format + // storing the certificate chain and the private key in a single encryptable file + const auto client_certificate = nmos::experimental::fields::client_certificate(settings); + const auto client_certificate_file = nmos::experimental::fields::certificate_chain_file(client_certificate); + + if (!client_certificate_file.empty()) + { + std::ifstream stream(client_certificate_file.c_str(), std::ios::in | std::ios::binary); + std::vector pkcs12_data((std::istreambuf_iterator(stream)), std::istreambuf_iterator()); + + CRYPT_DATA_BLOB data; + data.cbData = (DWORD)pkcs12_data.size(); + data.pbData = reinterpret_cast(pkcs12_data.data()); + + auto hCertStore = PFXImportCertStore(&data, {}, 0); + if (hCertStore) + { + PCCERT_CONTEXT pCertContext = NULL; + pCertContext = CertFindCertificateInStore(hCertStore, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, CERT_FIND_ANY, NULL, NULL); + if (pCertContext) + { + WinHttpSetOption(hRequest, WINHTTP_OPTION_CLIENT_CERT_CONTEXT, (LPVOID)pCertContext, sizeof(CERT_CONTEXT)); + + CertFreeCertificateContext(pCertContext); + } + CertCloseStore(hCertStore, 0); + } + } + }); +#endif + return config; + } + + struct est_service_exception {}; + + bool verify_est_response_header(const web::http::http_headers& headers) + { + const auto content_type = headers.find(web::http::header_names::content_type); + const auto content_type_or_empty = headers.end() != content_type ? content_type->second : utility::string_t{}; + const auto content_transfer_encoding = headers.find(U("Content-Transfer-Encoding")); + const auto content_transfer_encoding_or_empty = headers.end() != content_transfer_encoding ? content_transfer_encoding->second : utility::string_t{}; + const utility::string_t application_pkc7{ U("application/pkcs7-mime") }; + const utility::string_t base64{ U("base64") }; + + return (!content_type_or_empty.empty() && application_pkc7 == web::http::details::get_mime_type(content_type_or_empty) && base64 == content_transfer_encoding_or_empty); + } + + bool verify_crl_response_header(const web::http::http_headers& headers) + { + const auto content_type = headers.find(web::http::header_names::content_type); + const auto content_type_or_empty = headers.end() != content_type ? content_type->second : utility::string_t{}; + const utility::string_t text_plain{ U("text/plain") }; + + return (!content_type_or_empty.empty() && text_plain == web::http::details::get_mime_type(content_type_or_empty)); + } + + // extract and verify Certificate Response + // See https://tools.ietf.org/html/rfc7030#section-4.2.3 + pplx::task> extract_and_verify_certificate(const web::http::http_response& response, verify_cert_handler verify_certificate, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + // verify Enroll/Re-enroll response header + if (!verify_est_response_header(response.headers())) + { + throw est_exception("invalid Enroll/Re-enroll response header"); + } + + if (response.body()) + { + return response.extract_string(true).then([=, &gate](utility::string_t body) + { + slog::log(gate, SLOG_FLF) << "Received certificate: " << utility::us2s(body); + + // convert pkcs7 to pem format + auto pem = utility::s2us(make_pem_from_pkcs7(utility::us2s(body))); + + // verify certificate + verify_certificate(pem); + + return std::pair(response, pem); + }, token); + } + else + { + throw est_exception("missing certificate"); + } + } + + // extract and verify CA Certificates Response + // See https://tools.ietf.org/html/rfc7030#section-4.1.3 + pplx::task extract_and_verify_cacerts(const web::http::http_response& response, verify_cert_handler verify_cacerts, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + // verify CA response header + if (!verify_est_response_header(response.headers())) + { + throw est_exception("invalid CA certificates response header"); + } + + // verify CA + if (response.body()) + { + return response.extract_string(true).then([=, &gate](utility::string_t body) + { + slog::log(gate, SLOG_FLF) << "Received CA certificates: " << utility::us2s(body); + + // convert pkcs7 to pem format + auto pem = utility::s2us(make_pem_from_pkcs7(utility::us2s(body))); + + // verify CA certs + verify_cacerts(pem); + + return pem; + }, token); + } + else + { + throw est_exception("missing CA certificates"); + } + } + + // extract and verify Certificate Revocation List Response + pplx::task extract_certificate_revocation_list(const web::http::http_response& response, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + // verify certificate revocation list response header + if (!verify_crl_response_header(response.headers())) + { + throw est_exception("invalid CRL response header"); + } + + // hmm, verify certificate revocation list + if (response.body()) + { + return response.extract_string(true).then([=, &gate](utility::string_t body) + { + slog::log(gate, SLOG_FLF) << "Received CRL: " << utility::us2s(body); + + return body; + }, token); + } + else + { + throw est_exception("missing CRL"); + } + } + + // make an asynchronously GET request on the EST API to fetch Root CA certificates + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#getting-the-root-ca + pplx::task request_ca_certificates(web::http::client::http_client client, verify_cert_handler verify_cacerts, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting CA certificates"; + + using namespace web::http; + + // ://:/.well-known/est[/]/cacerts + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#est-server-api + // see https://tools.ietf.org/html/rfc7030#section-4.1.2 + return nmos::api_request(client, methods::GET, U("cacerts"), gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + // extract CA from the response body + return extract_and_verify_cacerts(response, verify_cacerts, gate, token); + } + else + { + slog::log(gate, SLOG_FLF) << "Request CA certificates error: " << response.status_code() << " " << response.reason_phrase(); + } + throw est_service_exception(); + + }, token); + } + + // make an asynchronously POST request on the EST API to fetch certificate + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#certificate-request + pplx::task> request_certificate(web::http::client::http_client client, const utility::string_t& key, const utility::string_t& csr, verify_cert_handler verify_certificate, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting certificate"; + + using namespace web::http; + + // ://:/.well-known/est[/]/simpleenroll + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#est-server-api + // see https://tools.ietf.org/html/rfc7030#section-4.2.1 + http_request req(methods::POST); + req.headers().add(header_names::content_type, U("application/pkcs10")); + req.set_request_uri(U("simpleenroll")); + req.set_body(csr); + + return nmos::api_request(client, req, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + return extract_and_verify_certificate(response, verify_certificate, gate, token); + } + return pplx::task_from_result(std::pair(response, {})); + + }, token); + } + + // make an asynchronously POST request on the EST API to certificate renewal + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#certificate-renewal + pplx::task> request_certificate_renewal(web::http::client::http_client client, const utility::string_t& key, const utility::string_t& csr, verify_cert_handler verify_certificate, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Requesting certificate renewal"; + + using namespace web::http; + + // ://:/.well-known/est[/]/simplereenroll + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#est-server-api + // see https://tools.ietf.org/html/rfc7030#section-4.2.2 + http_request req(methods::POST); + req.headers().add(header_names::content_type, U("application/pkcs10")); + req.set_request_uri(U("simplereenroll")); + req.set_body(csr); + + return nmos::api_request(client, req, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + return extract_and_verify_certificate(response, verify_certificate, gate, token); + } + return pplx::task_from_result(std::pair(response, {})); + + }, token); + } + + // make an asynchronously GET request on CRL endpoint to obtain the Certificate Revocation List, then check is certificate revoked + pplx::task certificate_revocation(web::http::client::http_client client, const utility::string_t& cacerts, const utility::string_t& cert, slog::base_gate& gate, const pplx::cancellation_token& token = pplx::cancellation_token::none()) + { + slog::log(gate, SLOG_FLF) << "Certificate revocation check at " << client.base_uri().to_string(); + + using namespace web::http; + + // fetch certificate revocation list + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#certificate-revocation + return nmos::api_request(client, methods::GET, gate, token).then([=, &gate](pplx::task response_task) + { + auto response = response_task.get(); // may throw http_exception + + if (status_codes::OK == response.status_code()) + { + // extract certificate revocation list from the response body + return extract_certificate_revocation_list(response, gate, token); + } + return pplx::task_from_result(utility::string_t{}); + + }, token).then([=, &gate](utility::string_t crl) + { + return is_revoked_by_crl(utility::us2s(cert), utility::us2s(cacerts), utility::us2s(crl)); + }); + } + + csr make_rsa_csr(const nmos::settings& settings) + { + // generate RSA CSR + const auto common_name = utility::us2s(get_host_name(settings)); + const auto country = utility::us2s(nmos::experimental::fields::country(settings)); + const auto state = utility::us2s(nmos::experimental::fields::state(settings)); + const auto city = utility::us2s(nmos::experimental::fields::city(settings)); + const auto organization = utility::us2s(get_domain(settings)); + const auto organizational_unit = utility::us2s(nmos::experimental::fields::organizational_unit(settings)); + const auto email_address = utility::us2s(nmos::experimental::fields::email_address(settings)); + + return make_rsa_csr(common_name, country, state, city, organization, organizational_unit, email_address); + } + + csr make_ecdsa_csr(const nmos::settings& settings) + { + // generate ECDSA CSR + const auto common_name = utility::us2s(get_host_name(settings)); + const auto country = utility::us2s(nmos::experimental::fields::country(settings)); + const auto state = utility::us2s(nmos::experimental::fields::state(settings)); + const auto city = utility::us2s(nmos::experimental::fields::city(settings)); + const auto organization = utility::us2s(get_domain(settings)); + const auto organizational_unit = utility::us2s(nmos::experimental::fields::organizational_unit(settings)); + const auto email_address = utility::us2s(nmos::experimental::fields::email_address(settings)); + + return make_ecdsa_csr(common_name, country, state, city, organization, organizational_unit, email_address); + } + + // "Renewal of the Root CA SHOULD be attempted no sooner than 50% of the certificate's expiry time. It is RECOMMENDED that certificate renewal is performed after 80% of the expiry time. + // To renew the Root CA and the EST Client's TLS Certificate, follow the Initial Certificate Provisioning workflow, renewing both the Root CA and server certificates in the process." + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#root-certificate-authority-renewal + pplx::task do_cacerts_renewal_monitor(nmos::model& model, est_shared_state& est_state, slog::base_gate& gate, const pplx::cancellation_token& token) + { + // split cacerts chain to list + auto ca_certs = ssl::experimental::split_certificate_chain(utility::us2s(est_state.cacerts)); + + // find the nearest to expiry time from the list of CA certs + auto expiry = std::chrono::seconds{ -1 }; + for (auto& cert : ca_certs) + { + auto tmp = std::chrono::seconds((int)ssl::experimental::certificate_expiry_from_now(cert, 0.8)); + if (std::chrono::seconds(-1) == expiry || tmp < expiry) + { + expiry = tmp; + } + } + + slog::log(gate, SLOG_FLF) << "Root CA certificates renewal for about " << expiry.count() << " seconds"; + + auto expiry_time = std::chrono::steady_clock::now() + expiry; + return pplx::complete_at(expiry_time, token).then([&]() + { + auto lock = model.write_lock(); // in order to update local state + // now to renew Root CA + slog::log(gate, SLOG_FLF) << "Now to renew Root CA certificates and the EST Client's TLS Certificates"; + est_state.renewal = true; + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try { finally.get(); } + catch (...) {} + + model.notify(); + }); + } + + pplx::task do_certificate_request(const est_shared_state& est_state, cert_shared_state& cert_state, slog::base_gate& gate, const pplx::cancellation_token& token) + { + using namespace web::http; + + auto retry = std::make_shared(false); + + return pplx::do_while([=, &est_state, &cert_state, &gate] + { + auto fetch_time = std::chrono::steady_clock::now(); + return pplx::complete_at(fetch_time + cert_state.delay, token).then([=, &est_state, &cert_state, &gate]() + { + return request_certificate(*cert_state.client, cert_state.key, cert_state.csr, cert_state.verify, gate, token).then([=, &est_state, &cert_state, &gate](std::pair result) + { + cert_state.delay = std::chrono::seconds(0); + + if (status_codes::OK == result.first.status_code()) + { + cert_state.cert = result.second; + + // "Renewal of the TLS Certificate SHOULD be attempted no sooner than 50% of the certificate's expiry time or before the 'Not Before' date on the certificate. + // It is RECOMMENDED that certificate renewal is performed after 80% of the expiry time." + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#certificate-renewal + cert_state.delay = std::chrono::seconds((int)ssl::experimental::certificate_expiry_from_now(utility::us2s(cert_state.cert), 0.8)); + + // get Certificate Revocation List URLs from certificate + with_write_lock(cert_state.mutex, [&cert_state] + { + cert_state.crl_urls = x509_crl_urls(utility::us2s(cert_state.cert)); + }); + + // do callback on certificate received + if (cert_state.received) + { + // callback with server certificate chain + cert_state.received({ cert_state.key, cert_state.cert + est_state.cacerts }); + } + + *retry = false; + } + else if (status_codes::Accepted == result.first.status_code() || status_codes::ServiceUnavailable == result.first.status_code()) + { + // parse the Retry-After header to obtain the retry value + // See https://tools.ietf.org/html/rfc7231#section-7.1.3 + // Retry-After: Fri, 31 Dec 1999 23:59:59 GMT + // Retry-After: 120 + auto& headers = result.first.headers(); + if (headers.end() != headers.find(U("Retry-After"))) + { + *retry = true; + cert_state.delay = parse_retry_after(utility::us2s(headers[U("Retry-After")])); + slog::log(gate, SLOG_FLF) << "Requesting certificate again for about " << cert_state.delay.count() << " seconds"; + } + else + { + slog::log(gate, SLOG_FLF) << "Request certificate error: missing Retry-After header"; + throw est_service_exception(); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Request certificate error: " << result.first.status_code() << " " << result.first.reason_phrase(); + throw est_service_exception(); + } + }); + }).then([retry]() + { + // continous to re-fetch certificate if retry is set + return pplx::task_from_result(*retry); + }); + }, token); + } + + pplx::task do_certificate_renewal_request(nmos::model& model, est_shared_state& est_state, cert_shared_state& cert_state, slog::base_gate& gate, const pplx::cancellation_token& token) + { + using namespace web::http; + + return pplx::do_while([=, &est_state, &cert_state, &gate] + { + slog::log(gate, SLOG_FLF) << "Requesting certificate renewal for about " << cert_state.delay.count() << " seconds"; + + auto now = std::chrono::steady_clock::now(); + return pplx::complete_at(now + cert_state.delay, token).then([=, &est_state, &cert_state, &gate]() mutable + { + return request_certificate_renewal(*cert_state.client, cert_state.key, cert_state.csr, cert_state.verify, gate, token).then([&est_state, &cert_state, &gate](std::pair result) + { + if (status_codes::OK == result.first.status_code()) + { + cert_state.cert = result.second; + + // do callback on certificate received + if (cert_state.received) + { + // callback with server certificate chain + cert_state.received({ cert_state.key, cert_state.cert + est_state.cacerts }); + } + + // "Renewal of the TLS Certificate SHOULD be attempted no sooner than 50% of the certificate's expiry time or before the 'Not Before' date on the certificate. + // It is RECOMMENDED that certificate renewal is performed after 80% of the expiry time." + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#certificate-renewal + cert_state.delay = std::chrono::seconds((int)ssl::experimental::certificate_expiry_from_now(utility::us2s(result.second), 0.8)); + + with_write_lock(cert_state.mutex, [&cert_state] + { + if (cert_state.delay.count() > 0) + { + // cache Certificate Revocation List URLs from certificate + cert_state.crl_urls = x509_crl_urls(utility::us2s(cert_state.cert)); + } + else + { + // certificate has expired + cert_state.expired = true; + } + }); + } + else if (status_codes::Accepted == result.first.status_code() || status_codes::ServiceUnavailable == result.first.status_code()) + { + // parse the retry-after header to obtain the retry next value + // See https://tools.ietf.org/html/rfc7231#section-7.1.3 + // Retry-After: Fri, 31 Dec 1999 23:59:59 GMT + // Retry-After: 120 + auto& headers = result.first.headers(); + if (headers.end() != headers.find(U("Retry-After"))) + { + cert_state.delay = parse_retry_after(utility::us2s(headers[U("Retry-After")])); + slog::log(gate, SLOG_FLF) << "Requesting certificate renewal again for about " << cert_state.delay.count() << " seconds"; + } + else + { + slog::log(gate, SLOG_FLF) << "Request certificate renewal error: missing Retry-After header"; + throw est_service_exception(); + } + } + else + { + slog::log(gate, SLOG_FLF) << "Request certificate renewal error: " << result.first.status_code() << " " << result.first.reason_phrase(); + throw est_service_exception(); + } + }); + }).then([=, &cert_state]() + { + // continous to fetch certificate if retry interval is set + return pplx::task_from_result(cert_state.delay.count() > 0); + }); + }, token).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API certificate renewal request HTTP error: " << e.what() << " [" << e.error_code() << "]"; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API certificate renewal request JSON error: " << e.what(); + } + catch (const est_service_exception&) + { + slog::log(gate, SLOG_FLF) << "EST API certificate renewal request error"; + } + catch (const est_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API certificate renewal request EST error: " << e.what(); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API certificate renewal request unexpected exception: " << e.what(); + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "EST API certificate renewal request unexpected unknown exception"; + } + + // reaching here, there must be something has gone wrong with the EST Server + // let select the next avaliable Authorization Server + if (!est_state.renewal) + { + est_state.est_service_error = true; + } + + model.notify(); + }); + } + + pplx::task do_certificate_revocation(std::vector clients, const utility::string_t& cacerts, const utility::string_t& cert, slog::base_gate& gate, const pplx::cancellation_token& token) + { + slog::log(gate, SLOG_FLF) << "Do certificate revocation check"; + + std::vector> tasks; + for (auto& client : clients) + { + tasks.push_back(certificate_revocation(client, cacerts, cert, gate, token)); + } + + return pplx::when_all(tasks.begin(), tasks.end()).then([&, tasks](pplx::task> finally) + { + // to ensure an exception from one doesn't leave other tasks' exceptions unobserved + for (auto& task : tasks) + { + try + { + task.wait(); + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "Certificate revocation HTTP error: " << e.what() << " [" << e.error_code() << "]"; + } + catch (const est_exception& e) + { + slog::log(gate, SLOG_FLF) << "Certificate revocation error: " << e.what(); + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Certificate revocation unexpected exception: " << e.what(); + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Certificate revocation unexpected unknown exception"; + } + } + + for (const auto& revoked : finally.get()) + { + if (revoked) + { + return true; + } + } + return false; + }); + } + + pplx::task do_certificates_revocation(nmos::model& model, est_shared_state& est_state, cert_shared_state& rsa_state, cert_shared_state& ecdsa_state, slog::base_gate& gate, const pplx::cancellation_token& token) + { + slog::log(gate, SLOG_FLF) << "Starting certificates revocation"; + + const auto certificate_revocation_interval = std::make_shared(nmos::experimental::fields::certificate_revocation_interval(model.settings)); + + auto cacerts = std::make_shared(est_state.cacerts); + auto rsa_cert = std::make_shared(rsa_state.cert); + auto ecdsa_cert = std::make_shared(ecdsa_state.cert); + auto interval = std::make_shared(0); + + auto rsa_crl_urls = std::make_shared>(); + auto ecdsa_crl_urls = std::make_shared>(); + auto rsa_crl_clients = std::make_shared>(); + auto ecdsa_crl_clients = std::make_shared>(); + + return pplx::do_while([=, &model, &est_state, &rsa_state, &ecdsa_state, &gate] + { + slog::log(gate, SLOG_FLF) << "Requesting certificate revocation check for about " << interval->count() << " seconds"; + + auto config = with_read_lock(model.mutex, [&model, &est_state, &gate] {return make_http_client_config(model.settings, est_state.load_ca_certificates, gate); }); + + // buildup a list of clients for RSA revolcation check + with_read_lock(rsa_state.mutex, [=, &config, &rsa_state] { + if (rsa_state.crl_urls != *rsa_crl_urls) + { + *rsa_crl_urls = rsa_state.crl_urls; + rsa_crl_clients->clear(); + for (const auto& crl_url : rsa_state.crl_urls) + { + rsa_crl_clients->push_back({ utility::s2us(crl_url), config }); + } + } + }); + + // buildup a list of clients for ECSDA revolcation check + with_read_lock(ecdsa_state.mutex, [=, &config, &ecdsa_state] { + if (ecdsa_state.crl_urls != *ecdsa_crl_urls) + { + *ecdsa_crl_urls = ecdsa_state.crl_urls; + ecdsa_crl_clients->clear(); + for (const auto& crl_url : ecdsa_state.crl_urls) + { + ecdsa_crl_clients->push_back({ utility::s2us(crl_url), config }); + } + } + }); + + // create tasks for RSA and ECSDA revolcation check + auto now = std::chrono::steady_clock::now(); + return pplx::complete_at(now + *interval, token).then([=, &model, &rsa_state, &ecdsa_state, &gate]() + { + *interval = *certificate_revocation_interval; + + const std::vector> tasks{ + do_certificate_revocation(*rsa_crl_clients, *cacerts, *rsa_cert, gate, token), + do_certificate_revocation(*ecdsa_crl_clients, *cacerts, *ecdsa_cert, gate, token) + }; + + return pplx::when_all(tasks.begin(), tasks.end()).then([&model, &rsa_state, &ecdsa_state, tasks](pplx::task> finally) + { + for (auto& task : tasks) { try { task.wait(); } catch (...) {} } + + bool revoked{ false }; + auto results = finally.get(); + if (results[0]) + { + auto lock = rsa_state.write_lock(); + rsa_state.expired = true; + revoked = true; + } + if (results[1]) + { + auto lock = ecdsa_state.write_lock(); + ecdsa_state.expired = true; + revoked = true; + } + return !revoked; + }); + }); + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try { finally.wait(); } catch (...) {} + + // reaching here, there must be because at least one of the certificates is revoked or shutdown + // lets restart initial certificate provisioning + slog::log(gate, SLOG_FLF) << "Stopping certificates revocation, it could be due to a certificate(s) has been revoked"; + + model.notify(); + }); + } + + // fetch Root CA certificates + bool do_ca_certificates_requests(nmos::model& model, est_shared_state& state, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting Root CA certificates fetch"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + pplx::cancellation_token_source cancellation_source; + auto request = pplx::task_from_result(); + + bool cacerts_received(false); + bool running(false); + + auto verify_cacerts = [](const utility::string_t& cacerts) + { + if (!cacerts.empty()) + { + // split the cacerts chain to list + auto ca_certs = ssl::experimental::split_certificate_chain(utility::us2s(cacerts)); + + std::string issuer_name; + for (auto& cert : ca_certs) + { + // verify certificate's NotBefore, Not After, Common Name, Subject Alternative Name and chain of trust + const auto cert_info = ssl::experimental::get_certificate_info(cert); + + // verify the chain against the certificate issuer name and the issuer certificate common name + if (!issuer_name.empty()) + { + if (boost::to_upper_copy(issuer_name) != boost::to_upper_copy(cert_info.subject_common_name)) { throw est_exception("invalid CA chain of trust"); } + } + issuer_name = cert_info.issuer_common_name; + + // Not Before + const auto now = time(NULL); + if (cert_info.not_before > now) { throw est_exception("CA certificate is not valid yet"); } + + // Not After + if (cert_info.not_after < now) { throw est_exception("CA certificate has expired"); } + } + } + else + { + throw est_exception("missing CA certificates to verify"); + } + }; + + for (;;) + { + // wait for the thread to be interrupted because an error has been encountered with the selected EST service + // or because the server is being shut down + // (or this is the first time through) + condition.wait(lock, [&] { return shutdown || state.est_service_error || cacerts_received || !running; }); + running = true; + if (state.est_service_error) + { + pop_est_service(model.settings); + model.notify(); + state.est_service_error = false; + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + + state.client.reset(); + cancellation_source = pplx::cancellation_token_source(); + } + if (shutdown || empty_est_services(model.settings) || cacerts_received) break; + + // selects a EST API to use based on the priority + if (!state.client) + { + const auto service = top_est_service(model.settings); + + // "If explicit trust of the EST Server is Enabled, the EST Client SHOULD make a HTTPS request to the /cacerts endpoint of the EST Server for the latest Root CA of the current network. + // The EST Client SHOULD explicitly trust the EST Server manually configured or discovered using Unicast DNS and not perform authentication of the EST Server's TLS Certificate during the initial request to the EST Server. + // + // If explicit trust of the EST Server is Disabled, the EST Client SHOULD make a HTTPS request to the /cacerts endpoint of the EST Server for the latest Root CA of the current network. + // The EST Client MUST perform authentication of the EST Server's TLS Certificate, using the list of trusted Certificate Authorities." + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#getting-the-root-ca + const auto trusted = nmos::experimental::fields::explicit_trust_est_enabled(model.settings); + const auto est_uri = service.second; + state.client.reset(new web::http::client::http_client(est_uri, make_est_client_config(model.settings, state.load_ca_certificates, state.load_client_certificate, trusted, gate))); + } + + auto token = cancellation_source.get_token(); + + request = request_ca_certificates(*state.client, verify_cacerts, gate, token).then([&model, &state, &gate, token](utility::string_t cacerts) + { + // record the cacerts (CA chain) for later use + state.cacerts = cacerts; + + // do callback allowing user to store the Root CA + if (state.receive_ca_certificate) + { + // extract the Root CA from chain, Root CA is always presented in the end of the CA chain + const auto ca_certs = ssl::experimental::split_certificate_chain(utility::us2s(cacerts)); + + state.receive_ca_certificate(utility::s2us(ca_certs.back())); + } + + }).then([&](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + try + { + finally.get(); + cacerts_received = true; + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API CA certificates HTTP error: " << e.what() << " [" << e.error_code() << "]"; + state.est_service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API CA certificates JSON error: " << e.what(); + state.est_service_error = true; + } + catch (const est_service_exception&) + { + slog::log(gate, SLOG_FLF) << "EST API CA certificates error"; + state.est_service_error = true; + } + catch (const est_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API CA certificates EST error: " << e.what(); + state.est_service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API CA certificates unexpected exception: " << e.what(); + state.est_service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "EST API CA certificates unexpected unknown exception"; + state.est_service_error = true; + } + + model.notify(); + }); + + // wait for the request because interactions with the EST API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || state.est_service_error || cacerts_received; }); + } + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + request.wait(); + + return !state.est_service_error && cacerts_received; + } + + // fetch RSA and ECDSA server certificates + bool do_certificates_requests(nmos::model& model, est_shared_state& est_state, cert_shared_state& rsa_state, cert_shared_state& ecdsa_state, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting certificates fetch"; + + auto lock = model.write_lock(); + + try + { + const auto service = top_est_service(model.settings); + const auto est_uri = service.second; + + const auto FQDN = nmos::get_host_name(model.settings); + const auto& cacerts = est_state.cacerts; + auto verify_certificate = [FQDN, &cacerts](const utility::string_t& certificate) + { + if (!certificate.empty()) + { + // verify certificate's NotBefore, Not After, Common Name, Subject Alternative Name and chain of trust + const auto cert_info = ssl::experimental::get_certificate_info(utility::us2s(certificate)); + + // Not Before + const auto now = time(NULL); + if (cert_info.not_before > now) { throw est_exception("certificate is not valid yet"); } + + // Not After + if (cert_info.not_after < now) { throw est_exception("certificate has expired"); } + + // Common Name + if (boost::to_upper_copy(utility::us2s(FQDN)) != boost::to_upper_copy(cert_info.subject_common_name)) { throw est_exception("invalid Common Name"); } + + // Subject Alternative Name + auto found_san = std::find_if(cert_info.subject_alternative_names.begin(), cert_info.subject_alternative_names.end(), [&FQDN](const std::string& san) { return boost::to_upper_copy(utility::us2s(FQDN)) == boost::to_upper_copy(san); }); + if (cert_info.subject_alternative_names.end() == found_san) { throw est_exception("invalid Subject Alternative Name"); } + + // chain of trust + const auto cacerts_info = ssl::experimental::get_certificate_info(utility::us2s(cacerts)); + if (cacerts_info.subject_common_name != cert_info.issuer_common_name) { throw est_exception("invalid chain of trust"); } + } + else + { + throw est_exception("missing certificate to verify"); + } + }; + + // generate RSA CSR with RSA key + const auto rsa_csr = make_rsa_csr(model.settings); + with_write_lock(rsa_state.mutex, [&]() { + rsa_state.key = utility::s2us(rsa_csr.first); + rsa_state.csr = utility::s2us(rsa_csr.second); + rsa_state.delay = std::chrono::seconds::zero(); + rsa_state.client.reset(new web::http::client::http_client(est_uri, make_est_client_config(model.settings, est_state.load_ca_certificates, est_state.load_client_certificate, false, gate))); + rsa_state.expired = false; + rsa_state.cert = U(""); + rsa_state.verify = verify_certificate; + }); + // generate ECDSA CSR with ECDSA key + const auto ecdsa_csr = make_ecdsa_csr(model.settings); + with_write_lock(ecdsa_state.mutex, [&]() { + ecdsa_state.key = utility::s2us(ecdsa_csr.first); + ecdsa_state.csr = utility::s2us(ecdsa_csr.second); + ecdsa_state.delay = std::chrono::seconds::zero(); + ecdsa_state.client.reset(new web::http::client::http_client(est_uri, make_est_client_config(model.settings, est_state.load_ca_certificates, est_state.load_client_certificate, false, gate))); + ecdsa_state.expired = false; + ecdsa_state.cert = U(""); + ecdsa_state.verify = verify_certificate; + }); + } + catch (const est_exception& e) + { + slog::log(gate, SLOG_FLF) << "Logic error to generate CSR EST error: " << e.what(); + return false; + } + catch (const ssl::experimental::ssl_exception& e) + { + slog::log(gate, SLOG_FLF) << "Logic error to generate CSR SSL error: " << e.what(); + return false; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "Generate CSR unexpected exception: " << e.what(); + return false; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "Generate CSR unexpected unknown exception"; + return false; + } + + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + auto& service_error = est_state.est_service_error; + + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + + const std::vector> tasks{ + do_certificate_request(est_state, rsa_state, gate, token), + do_certificate_request(est_state, ecdsa_state, gate, token) + }; + + bool all_done(false); + auto completed = pplx::when_all(tasks.begin(), tasks.end()).then([&, tasks](pplx::task finally) + { + auto lock = model.write_lock(); // in order to update local state + + // to ensure an exception from one doesn't leave other tasks' exceptions unobserved + for (auto& task : tasks) + { + try + { + task.wait(); + } + catch (const web::http::http_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API certificate fetch HTTP error: " << e.what() << " [" << e.error_code() << "]"; + service_error = true; + } + catch (const web::json::json_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API certificate fetch JSON error: " << e.what(); + service_error = true; + } + catch (const est_service_exception&) + { + slog::log(gate, SLOG_FLF) << "EST API certificate fetch error"; + service_error = true; + } + catch (const est_exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API certificate fetch EST error: " << e.what(); + service_error = true; + } + catch (const std::exception& e) + { + slog::log(gate, SLOG_FLF) << "EST API certificate fetch unexpected exception: " << e.what(); + service_error = true; + } + catch (...) + { + slog::log(gate, SLOG_FLF) << "EST API certificate fetch unexpected unknown exception"; + service_error = true; + } + } + + try { finally.wait(); } catch (...) {} + + all_done = true; + + model.notify(); + }); + + // wait for the request because interactions with the EST API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || service_error || all_done; }); + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + completed.wait(); + + return !service_error; + } + + // monitor when to renew Root CA certificates, do renewal of RSA and ECDSA server certificates and do certicate revocation check + void do_renewal_certificates_and_certificates_revocation_requests(nmos::model& model, est_shared_state& est_state, cert_shared_state& rsa_state, cert_shared_state& ecdsa_state, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Attempting renewal certificates and certificates revocation operation"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + + auto cacerts_renewal_monitor = pplx::task_from_result(); + auto rsa_request = pplx::task_from_result(); + auto ecdsa_request = pplx::task_from_result(); + auto certificates_revocation = pplx::task_from_result(); + + bool running(false); + + est_state.renewal = false; + + for (;;) + { + // wait for the thread to be interrupted because an error has been encountered with the selected EST service + // or because the server is being shut down + // (or this is the first time through) + condition.wait(lock, [&] { return shutdown || est_state.est_service_error || !running; }); + running = true; + if (est_state.est_service_error) + { + pop_est_service(model.settings); + model.notify(); + est_state.est_service_error = false; + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + rsa_request.wait(); + ecdsa_request.wait(); + + est_state.client.reset(); + rsa_state.client.reset(); + ecdsa_state.client.reset(); + cancellation_source = pplx::cancellation_token_source(); + } + if (shutdown || empty_est_services(model.settings)) break; + + if (!rsa_state.client) + { + const auto service = top_est_service(model.settings); // selects a EST API to use based on the priority + const auto est_uri = service.second; + rsa_state.client.reset(new web::http::client::http_client(est_uri, make_est_client_config(model.settings, est_state.load_ca_certificates, est_state.load_client_certificate, false, gate))); + } + if (!ecdsa_state.client) + { + const auto service = top_est_service(model.settings); // selects a EST API to use based on the priority + const auto est_uri = service.second; + ecdsa_state.client.reset(new web::http::client::http_client(est_uri, make_est_client_config(model.settings, est_state.load_ca_certificates, est_state.load_client_certificate, false, gate))); + } + + auto token = cancellation_source.get_token(); + + // create a background task to monitor when the Root CA certificates are reqiired to renew + cacerts_renewal_monitor = do_cacerts_renewal_monitor(model, est_state, gate, token); + + // create a background task to renew RSA certificate + const auto rsa_csr = make_rsa_csr(model.settings); + rsa_state.key = utility::s2us(rsa_csr.first); + rsa_state.csr = utility::s2us(rsa_csr.second); + rsa_request = do_certificate_renewal_request(model, est_state, rsa_state, gate, token); + + // create a background task to renew ECDSA certificate + const auto ecdsa_csr = make_ecdsa_csr(model.settings); + ecdsa_state.key = utility::s2us(ecdsa_csr.first); + ecdsa_state.csr = utility::s2us(ecdsa_csr.second); + ecdsa_request = do_certificate_renewal_request(model, est_state, ecdsa_state, gate, token); + + // create a background task to check for certificates revocation + certificates_revocation = do_certificates_revocation(model, est_state, rsa_state, ecdsa_state, gate, token); + + // wait for the request because interactions with the EST API endpoint must be sequential + condition.wait(lock, [&] { return shutdown || est_state.est_service_error || est_state.renewal || rsa_state.expired || ecdsa_state.expired; }); + + if (est_state.renewal || rsa_state.expired || ecdsa_state.expired) break; + } + + cancellation_source.cancel(); + nmos::details::reverse_lock_guard unlock{ lock }; + cacerts_renewal_monitor.wait(); + rsa_request.wait(); + ecdsa_request.wait(); + certificates_revocation.wait(); + } + } + + // service discovery operation + namespace details + { + void est_services_background_discovery(nmos::model& model, mdns::service_discovery& discovery, slog::base_gate& gate) + { + slog::log(gate, SLOG_FLF) << "Adopting background discovery of a EST API"; + + auto lock = model.write_lock(); + auto& condition = model.condition; + auto& shutdown = model.shutdown; + + bool est_services_discovered(false); + + // background tasks may read/write the above local state by reference + pplx::cancellation_token_source cancellation_source; + auto token = cancellation_source.get_token(); + pplx::task background_discovery = pplx::do_while([&] + { + // add a short delay since initial discovery or rediscovery must have only just failed + // (this also prevents a tight loop in the case that the underlying DNS-SD implementation is just refusing to co-operate + // though that would be better indicated by an exception from discover_est_services) + return pplx::complete_after(std::chrono::seconds(1), token).then([&] + { + return !discover_est_services(model, discovery, gate, token); + }); + }, token).then([&] + { + auto lock = model.write_lock(); // in order to update local state + + est_services_discovered = true; // since discovery must have succeeded + + model.notify(); + }); + + for (;;) + { + // wait for the thread to be interrupted because a EST API has been discovered + // or because the server is being shut down + condition.wait(lock, [&] { return shutdown || est_services_discovered; }); + if (shutdown || est_services_discovered) break; + } + + cancellation_source.cancel(); + // wait without the lock since it is also used by the background tasks + nmos::details::reverse_lock_guard unlock{ lock }; + background_discovery.wait(); + } + } + } +} diff --git a/Development/nmos/est_behaviour.h b/Development/nmos/est_behaviour.h new file mode 100644 index 000000000..95cffe74a --- /dev/null +++ b/Development/nmos/est_behaviour.h @@ -0,0 +1,35 @@ +#ifndef NMOS_EST_BEHAVIOUR_H +#define NMOS_EST_BEHAVIOUR_H + +#include "nmos/certificate_handlers.h" +#include "nmos/est_certificate_handlers.h" + +namespace slog +{ + class base_gate; +} + +namespace mdns +{ + class service_discovery; +} + +// EST behaviour including fetch CA certificates, request server certificates and certificate revocation operation +// See https://specs.amwa.tv/is-10/ +namespace nmos +{ + struct model; + + namespace experimental + { + // uses the default DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void est_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, receive_ca_certificate_handler ca_certificate_received, receive_server_certificate_handler rsa_server_certificate_received, receive_server_certificate_handler ecdsa_server_certificate_received, slog::base_gate& gate); + + // uses the specified DNS-SD implementation + // callbacks from this function are called with the model locked, and may read or write directly to the model + void est_behaviour_thread(nmos::model& model, load_ca_certificates_handler load_ca_certificates, load_client_certificate_handler load_client_certificate, receive_ca_certificate_handler ca_certificate_received, receive_server_certificate_handler rsa_server_certificate_received, receive_server_certificate_handler ecdsa_server_certificate_received, mdns::service_discovery& discovery, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/est_certificate_handlers.cpp b/Development/nmos/est_certificate_handlers.cpp new file mode 100644 index 000000000..94c3e31b4 --- /dev/null +++ b/Development/nmos/est_certificate_handlers.cpp @@ -0,0 +1,46 @@ +#include "nmos/est_certificate_handlers.h" + +namespace nmos +{ + namespace experimental + { + // construct callback to save certification authorities to file based on settings, see nmos/certificate_settings.h + receive_ca_certificate_handler make_ca_certificate_received_handler(const nmos::settings& settings, slog::base_gate& gate) + { + auto save_ca_certificates = nmos::make_save_ca_certificates_handler(settings, gate); + + return[save_ca_certificates](const utility::string_t& ca_certificate) + { + save_ca_certificates(ca_certificate); + }; + } + + // construct callback to save ECDSA server certificate to file based on settings, see nmos/certificate_settings.h + receive_server_certificate_handler make_ecdsa_server_certificate_received_handler(const nmos::settings& settings, slog::base_gate& gate) + { + auto save_server_certificate = nmos::make_save_ecdsa_server_certificate_handler(settings, gate); + + return[save_server_certificate](const nmos::certificate& server_certificate) + { + if (save_server_certificate) + { + save_server_certificate(server_certificate); + } + }; + } + + // construct callback to save RSA server certificate to file based on settings, see nmos/certificate_settings.h + receive_server_certificate_handler make_rsa_server_certificate_received_handler(const nmos::settings& settings, slog::base_gate& gate) + { + auto save_server_certificate = nmos::make_save_rsa_server_certificate_handler(settings, gate); + + return[save_server_certificate](const nmos::certificate& server_certificate) + { + if (save_server_certificate) + { + save_server_certificate(server_certificate); + } + }; + } + } +} diff --git a/Development/nmos/est_certificate_handlers.h b/Development/nmos/est_certificate_handlers.h new file mode 100644 index 000000000..38f8d2acf --- /dev/null +++ b/Development/nmos/est_certificate_handlers.h @@ -0,0 +1,43 @@ +#ifndef NMOS_EST_CERTIFICATE_HANDLERS_H +#define NMOS_EST_CERTIFICATE_HANDLERS_H + +#include +#include "cpprest/details/basic_types.h" +#include "nmos/certificate_handlers.h" +#include "nmos/settings.h" + +namespace web +{ + class uri; +} + +namespace nmos +{ + namespace experimental + { + // an est_handler is a notification that a EST server has been identified + // est uri should be like http://api.example.com//.well-known/est/{api_selector} + // or empty if errors have been encountered when interacting with all discoverable EST APIs + // this callback should not throw exceptions + typedef std::function est_handler; + + // callback after root ca certificate have been received + // this callback should not throw exceptions + typedef std::function receive_ca_certificate_handler; + + // callback after server certificate has been received + // this callback should not throw exceptions + typedef std::function receive_server_certificate_handler; + + // construct callback to save certification authorities to file based on settings, see nmos/certificate_settings.h + receive_ca_certificate_handler make_ca_certificate_received_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 + receive_server_certificate_handler make_ecdsa_server_certificate_received_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 + receive_server_certificate_handler make_rsa_server_certificate_received_handler(const nmos::settings& settings, slog::base_gate& gate); + } +} + +#endif diff --git a/Development/nmos/est_utils.cpp b/Development/nmos/est_utils.cpp new file mode 100644 index 000000000..7d034064c --- /dev/null +++ b/Development/nmos/est_utils.cpp @@ -0,0 +1,480 @@ +#include "nmos/est_utils.h" + +#include +#include +#include +#include // for boost::split +#include +#include +#include +#include +#include +#include // for X509V3_EXT_conf_nid +#include "ssl/ssl_utils.h" + +namespace nmos +{ + namespace experimental + { + namespace details + { + typedef std::unique_ptr BIGNUM_ptr; + typedef std::unique_ptr EVP_PKEY_ptr; + typedef std::unique_ptr X509_REQ_ptr; + typedef std::unique_ptr EVP_PKEY_CTX_ptr; + + std::shared_ptr make_rsa_key(const std::string& private_key_data = {}, const std::string& password = {}) + { + if (private_key_data.empty()) + { + // create the context for the key generation + EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL), &EVP_PKEY_CTX_free); + if (!ctx) + { + throw est_exception("failed to generate RSA key: EVP_PKEY_CTX_new_id failure: " + ssl::experimental::last_openssl_error()); + } + + // generate the RSA key + if (0 >= EVP_PKEY_keygen_init(ctx.get())) + { + throw est_exception("failed to generate RSA key: EVP_PKEY_keygen_init failure: " + ssl::experimental::last_openssl_error()); + } + + if (0 >= EVP_PKEY_CTX_set_rsa_keygen_bits(ctx.get(), 2048)) + { + throw est_exception("failed to generate RSA key: EVP_PKEY_CTX_set_rsa_keygen_bits failure: " + ssl::experimental::last_openssl_error()); + } + + EVP_PKEY* pkey_temp = NULL; + if (0 >= EVP_PKEY_keygen(ctx.get(), &pkey_temp)) + { + throw est_exception("failed to generate RSA key: EVP_PKEY_keygen failure: " + ssl::experimental::last_openssl_error()); + } + + // create a EVP_PKEY to store key + std::shared_ptr pkey(EVP_PKEY_new(), &EVP_PKEY_free); + + return pkey; + } + else + { + ssl::experimental::BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if ((size_t)BIO_write(bio.get(), private_key_data.data(), (int)private_key_data.size()) != private_key_data.size()) + { + throw est_exception("failed to load RSA key: BIO_write failure: " + ssl::experimental::last_openssl_error()); + } + std::shared_ptr pkey(PEM_read_bio_PrivateKey(bio.get(), NULL, NULL, const_cast(password.c_str())), &EVP_PKEY_free); + if (!pkey) + { + throw est_exception("failed to load RSA key: PEM_read_bio_PrivateKey failure: " + ssl::experimental::last_openssl_error()); + } + return pkey; + } + } + + std::shared_ptr make_ecdsa_key(const std::string& private_key_data = {}, const std::string& password = {}) + { + if (private_key_data.empty()) + { + // create the context for the key generation + EVP_PKEY_CTX_ptr ctx(EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL), &EVP_PKEY_CTX_free); + if (!ctx) + { + throw est_exception("failed to generate ECDSA key: EVP_PKEY_CTX_new_id failure: " + ssl::experimental::last_openssl_error()); + } + + // generate the ECDSA key + if (0 >= EVP_PKEY_keygen_init(ctx.get())) + { + throw est_exception("failed to generate ECDSA key: EVP_PKEY_keygen_init failure: " + ssl::experimental::last_openssl_error()); + } + + // use the ANSI X9.62 Prime 256v1 curve + // NIST P-256 is refered to as secp256r1 and prime256v1. Different names, but they are all the same. + // See https://tools.ietf.org/search/rfc4492#appendix-A + if (0 >= EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx.get(), NID_X9_62_prime256v1)) + { + throw est_exception("failed to generate ECDSA key: EVP_PKEY_CTX_set_ec_paramgen_curve_nid failure: " + ssl::experimental::last_openssl_error()); + } + + EVP_PKEY* pkey_temp = NULL; + if (0 >= EVP_PKEY_keygen(ctx.get(), &pkey_temp)) + { + throw est_exception("failed to generate ECDSA key: EVP_PKEY_keygen failure: " + ssl::experimental::last_openssl_error()); + } + + // create a EVP_PKEY to store key + std::shared_ptr pkey(pkey_temp, EVP_PKEY_free); + + return pkey; + } + else + { + ssl::experimental::BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if ((size_t)BIO_write(bio.get(), private_key_data.data(), (int)private_key_data.size()) != private_key_data.size()) + { + throw est_exception("failed to load ECDSA key: BIO_write failure: " + ssl::experimental::last_openssl_error()); + } + std::shared_ptr pkey(PEM_read_bio_PrivateKey(bio.get(), NULL, NULL, const_cast(password.c_str())), EVP_PKEY_free); + if (!pkey) + { + throw est_exception("failed to load ECDSA key: PEM_read_bio_PrivateKey failure: " + ssl::experimental::last_openssl_error()); + } + return pkey; + } + } + + // convert PKCS7 to pem format + // it is based on openssl example + // See https://github.com/openssl/openssl/blob/master/apps/pkcs7.c + std::string to_pem(std::shared_ptr p7) + { + if (!p7) + { + throw est_exception("failed to convert PKCS7 to pem: no PKCS7"); + } + + if (!p7->d.sign) + { + throw est_exception("failed to convert PKCS7 to pem: no NID_pkcs7_signed"); + } + + auto certs = p7->d.sign->cert; + if (certs) + { + ssl::experimental::BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + + for (auto idx = 0; idx < sk_X509_num(certs); idx++) + { + auto x509 = sk_X509_value(certs, idx); + auto cert_info = [](BIO* out, X509* x) + { + auto p = X509_NAME_oneline(X509_get_subject_name(x), NULL, 0); + BIO_puts(out, "subject="); + BIO_puts(out, p); + OPENSSL_free(p); + + p = X509_NAME_oneline(X509_get_issuer_name(x), NULL, 0); + BIO_puts(out, "\n\nissuer="); + BIO_puts(out, p); + OPENSSL_free(p); + + BIO_puts(out, "\n\nnotBefore="); +#if (OPENSSL_VERSION_NUMBER >= 0x1010100fL) + ASN1_TIME_print(out, X509_get0_notBefore(x)); +#else + ASN1_TIME_print(out, X509_get_notBefore(x)); +#endif + BIO_puts(out, "\n\nnotAfter="); +#if (OPENSSL_VERSION_NUMBER >= 0x1010100fL) + ASN1_TIME_print(out, X509_get0_notAfter(x)); +#else + ASN1_TIME_print(out, X509_get_notAfter(x)); +#endif + BIO_puts(out, "\n\n"); + }; + cert_info(bio.get(), x509); + PEM_write_bio_X509(bio.get(), x509); + BIO_puts(bio.get(), "\n"); + } + + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string pem(size_t(buf->length), 0); + if ((size_t)BIO_read(bio.get(), (void*)pem.data(), (int)pem.length()) != pem.length()) + { + throw est_exception("failed to convert PKCS7 to pem: BIO_read failure: " + ssl::experimental::last_openssl_error()); + } + return pem; + } + else + { + throw est_exception("failed to convert PKCS7 to pem: no certificate found"); + } + } + + // convert Private key to pem format + std::string to_pem(std::shared_ptr pkey) + { + ssl::experimental::BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (PEM_write_bio_PrivateKey(bio.get(), pkey.get(), NULL, NULL, 0, NULL, NULL)) + { + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string pem(size_t(buf->length), 0); + if ((size_t)BIO_read(bio.get(), (void*)pem.data(), (int)pem.length()) != pem.length()) + { + throw est_exception("failed to convert private key to pem: BIO_read failure: " + ssl::experimental::last_openssl_error()); + } + return pem; + } + else + { + throw est_exception("failed to convert private key to pem: PEM_write_bio_PrivateKey failure: " + ssl::experimental::last_openssl_error()); + } + } + + // convert PKCS7 to pem format + std::string make_pem_from_pkcs7(const std::string& pkcs7_data) + { + if (pkcs7_data.empty()) + { + throw est_exception("no pkcs7 to convert to pem"); + } + + const std::string prefix{ "-----BEGIN PKCS7-----\n" }; + const std::string suffix{ '\n' == pkcs7_data.back() ? "-----END PKCS7-----\n" : "\n-----END PKCS7-----\n" }; + + // insert PKCS7 prefix & suffix if missing + auto pkcs7 = (prefix != pkcs7_data.substr(0, prefix.length())) ? prefix + pkcs7_data + suffix : pkcs7_data; + + // convert PKCS7 to pem format + // it is based on openssl example + // See https://github.com/openssl/openssl/blob/master/apps/pkcs7.c + ssl::experimental::BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); // BIO_free_all + if ((size_t)BIO_write(bio.get(), pkcs7.data(), (int)pkcs7.size()) != pkcs7.size()) + { + throw est_exception("failed to load PKCS7: BIO_write failure: " + ssl::experimental::last_openssl_error()); + } + std::shared_ptr p7(PEM_read_bio_PKCS7(bio.get(), NULL, NULL, NULL), PKCS7_free); + if (p7) + { + return to_pem(p7); + } + else + { + throw est_exception("failed to load PKCS7: PEM_read_bio_PKCS7 failure: " + ssl::experimental::last_openssl_error()); + } + } + + // generate X509 certificate request + std::string make_X509_req(const std::shared_ptr& pkey, const std::string& common_name, const std::string& country, const std::string& state, const std::string& city, const std::string& organization, const std::string& organizational_unit, const std::string& email_address) + { + const long version = 0; + + // generate x509 request + X509_REQ_ptr x509_req(X509_REQ_new(), &X509_REQ_free); + if (!x509_req) + { + throw est_exception("failed to create x509 req: X509_REQ_new failure: " + ssl::experimental::last_openssl_error()); + } + // set version of x509 request + if (!X509_REQ_set_version(x509_req.get(), version)) + { + throw est_exception("failed to set version of x509 req: X509_REQ_set_version failure: " + ssl::experimental::last_openssl_error()); + } + + // set subject of x509 request + auto x509_name = X509_REQ_get_subject_name(x509_req.get()); + auto add_X509_REQ_subject = [&x509_name](const std::string& subject, const std::string& value) + { + if (!value.empty()) + { + if (!X509_NAME_add_entry_by_txt(x509_name, subject.c_str(), MBSTRING_ASC, (const unsigned char*)value.c_str(), -1, -1, 0)) + { + std::stringstream ss; + ss << "failed to set '" << subject << "' of x509 req: X509_NAME_add_entry_by_txt failure: " << ssl::experimental::last_openssl_error(); + throw est_exception(ss.str()); + } + } + }; + // set common name of x509 request, common name MUST be presented + if (common_name.empty()) + { + throw est_exception("missing common name for x509 req"); + } + add_X509_REQ_subject("CN", common_name); + // set country of x509 request + add_X509_REQ_subject("C", country); + // set state of x509 request + add_X509_REQ_subject("ST", state); + // set city of x509 request + add_X509_REQ_subject("L", city); + // set organization of x509 request + add_X509_REQ_subject("O", organization); + // set organizational unit of x509 request + add_X509_REQ_subject("OU", organizational_unit); + // set email address of x509 request + add_X509_REQ_subject("emailAddress", email_address); + + // set x509 extensions + auto extensions = sk_X509_EXTENSION_new_null(); + auto add_extension = [&extensions](int nid, const std::string& value) + { +#if OPENSSL_VERSION_NUMBER < 0x30000000L + auto extension = X509V3_EXT_conf_nid(NULL, NULL, nid, const_cast(value.c_str())); +#else + auto extension = X509V3_EXT_conf_nid(NULL, NULL, nid, value.c_str()); +#endif + if (!extension) + { + std::stringstream ss; + ss << "failed to create '" << nid << "' extension: X509V3_EXT_conf_nid failure: " << ssl::experimental::last_openssl_error(); + + // release all previous added extension + sk_X509_EXTENSION_pop_free(extensions, X509_EXTENSION_free); + + throw est_exception(ss.str()); + } + sk_X509_EXTENSION_push(extensions, extension); + }; + // set subjectAltName of x509 request + add_extension(NID_subject_alt_name, "DNS:" + common_name); + + // add extensions to x509 request + if (!X509_REQ_add_extensions(x509_req.get(), extensions)) + { + throw est_exception("failed to add extnsions to x509 req: X509_REQ_add_extensions failure: " + ssl::experimental::last_openssl_error()); + } + // release x509 extensions + sk_X509_EXTENSION_pop_free(extensions, X509_EXTENSION_free); + + // set public key of x509 req + if (!X509_REQ_set_pubkey(x509_req.get(), pkey.get())) + { + throw est_exception("failed to set public key of x509 req: X509_REQ_set_pubkey failure: " + ssl::experimental::last_openssl_error()); + } + + // set sign key of x509 req + if (0 >= X509_REQ_sign(x509_req.get(), pkey.get(), EVP_sha256())) + { + throw est_exception("failed to set sign key of x509 req: X509_REQ_sign failure: " + ssl::experimental::last_openssl_error()); + } + + // generate x509 req in pem format + ssl::experimental::BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if (PEM_write_bio_X509_REQ(bio.get(), x509_req.get())) + { + BUF_MEM* buf; + BIO_get_mem_ptr(bio.get(), &buf); + std::string pem(size_t(buf->length), 0); + if ((size_t)BIO_read(bio.get(), (void*)pem.data(), (int)pem.length()) != pem.length()) + { + throw est_exception("failed to generate CSR: BIO_read failure: " + ssl::experimental::last_openssl_error()); + } + return pem; + } + else + { + throw est_exception("failed to generate CSR: PEM_write_bio_X509_REQ failure: " + ssl::experimental::last_openssl_error()); + } + } + + // generate RSA certificate request + csr make_rsa_csr(const std::string& common_name, const std::string& country, const std::string& state, const std::string& city, const std::string& organization, const std::string& organizational_unit, const std::string& email_address, const std::string& private_key_data, const std::string& password) + { + auto pkey = make_rsa_key(private_key_data, password); + return{ to_pem(pkey), make_X509_req(pkey, common_name, country, state, city, organization, organizational_unit, email_address) }; + } + + // generate ECDSA certificate request + csr make_ecdsa_csr(const std::string& common_name, const std::string& country, const std::string& state, const std::string& city, const std::string& organization, const std::string& organizational_unit, const std::string& email_address, const std::string& private_key_data, const std::string& password) + { + auto pkey = make_ecdsa_key(private_key_data, password); + return{ to_pem(pkey), make_X509_req(pkey, common_name, country, state, city, organization, organizational_unit, email_address) }; + } + + std::vector x509_crl_urls(const std::string& certificate) + { + ssl::experimental::BIO_ptr bio(BIO_new(BIO_s_mem()), &BIO_free); + if ((size_t)BIO_write(bio.get(), certificate.data(), (int)certificate.size()) != certificate.size()) + { + throw est_exception("failed to load cert to bio: BIO_write failure: " + ssl::experimental::last_openssl_error()); + } + + ssl::experimental::X509_ptr x509(PEM_read_bio_X509_AUX(bio.get(), NULL, NULL, NULL), &X509_free); + if (!x509) + { + throw est_exception("failed to load cert: PEM_read_bio_X509_AUX failure: " + ssl::experimental::last_openssl_error()); + } + + std::vector list; + auto dist_points = (STACK_OF(DIST_POINT)*)X509_get_ext_d2i(x509.get(), NID_crl_distribution_points, NULL, NULL); + for (auto idx = 0; idx < sk_DIST_POINT_num(dist_points); idx++) + { + auto dp = sk_DIST_POINT_value(dist_points, idx); + auto distpoint = dp->distpoint; + if (distpoint->type == 0) //fullname GENERALIZEDNAME + { + for (int i = 0; i < sk_GENERAL_NAME_num(distpoint->name.fullname); i++) + { + auto gen = sk_GENERAL_NAME_value(distpoint->name.fullname, i); + auto asn1_str = gen->d.uniformResourceIdentifier; +#if (OPENSSL_VERSION_NUMBER >= 0x1010100fL) + list.push_back(std::string((const char*)ASN1_STRING_get0_data(asn1_str), ASN1_STRING_length(asn1_str))); +#else + list.push_back(std::string((const char*)ASN1_STRING_data(asn1_str), ASN1_STRING_length(asn1_str))); +#endif + } + } + else if (distpoint->type == 1) //relativename X509NAME + { + auto sk_relname = distpoint->name.relativename; + for (int i = 0; i < sk_X509_NAME_ENTRY_num(sk_relname); i++) + { + auto e = sk_X509_NAME_ENTRY_value(sk_relname, i); + auto d = X509_NAME_ENTRY_get_data(e); +#if (OPENSSL_VERSION_NUMBER >= 0x1010100fL) + list.push_back(std::string((const char*)ASN1_STRING_get0_data(d), ASN1_STRING_length(d))); +#else + list.push_back(std::string((const char*)ASN1_STRING_data(d), ASN1_STRING_length(d))); +#endif + } + } + } + return list; + } + + // revocation check + // it is based on zedwood.com example + // See http://www.zedwood.com/article/cpp-check-crl-for-revocation + bool is_revoked_by_crl(X509* x509, X509* issuer, X509_CRL* crl) + { + if (!x509) { throw est_exception("invalid cert"); } + if (!issuer) { throw est_exception("invalid cert issuer"); } + if (!crl) { throw est_exception("invalid CRL"); } + + auto ikey = X509_get_pubkey(issuer); + auto cert_serial_number = X509_get_serialNumber(x509); + + if (!ikey) { throw est_exception("invalid cert public key"); } + + if (!X509_CRL_verify(crl, ikey)) { throw est_exception("failed to verify CRL signature"); } + + auto revoked_list = X509_CRL_get_REVOKED(crl); + for (auto idx = 0; idx < sk_X509_REVOKED_num(revoked_list); idx++) + { + auto entry = sk_X509_REVOKED_value(revoked_list, idx); +#if (OPENSSL_VERSION_NUMBER >= 0x1010100fL) + auto serial_number = X509_REVOKED_get0_serialNumber(entry); +#else + auto serial_number = entry->serialNumber; +#endif + if (serial_number->length == cert_serial_number->length) + { + if (memcmp(serial_number->data, cert_serial_number->data, cert_serial_number->length) == 0) + { + return true; + } + } + } + return false; + } + + bool is_revoked_by_crl(const std::string& certificate, const std::string& cert_issuer_data, const std::string& crl_data) + { + ssl::experimental::BIO_ptr bio_cert(BIO_new(BIO_s_mem()), &BIO_free); + BIO_puts(bio_cert.get(), certificate.c_str()); + ssl::experimental::BIO_ptr bio_cert_issuer(BIO_new(BIO_s_mem()), &BIO_free); + BIO_puts(bio_cert_issuer.get(), cert_issuer_data.c_str()); + ssl::experimental::BIO_ptr bio_crl(BIO_new(BIO_s_mem()), &BIO_free); + BIO_puts(bio_crl.get(), crl_data.c_str()); + + return is_revoked_by_crl( + PEM_read_bio_X509(bio_cert.get(), NULL, NULL, NULL), + PEM_read_bio_X509(bio_cert_issuer.get(), NULL, NULL, NULL), + PEM_read_bio_X509_CRL(bio_crl.get(), NULL, NULL, NULL)); + } + } + } +} diff --git a/Development/nmos/est_utils.h b/Development/nmos/est_utils.h new file mode 100644 index 000000000..454a7927d --- /dev/null +++ b/Development/nmos/est_utils.h @@ -0,0 +1,34 @@ +#ifndef NMOS_EST_UTILS_H +#define NMOS_EST_UTILS_H + +#include +#include +#include + +namespace nmos +{ + namespace experimental + { + struct est_exception : std::runtime_error + { + est_exception(const std::string& message) : std::runtime_error(message) {} + }; + + namespace details + { + std::string make_pem_from_pkcs7(const std::string& pkcs7); + + typedef std::pair csr; // csr represented private key pem and csr pem + + csr make_rsa_csr(const std::string& common_name, const std::string& country, const std::string& state, const std::string& city, const std::string& organization, const std::string& organizational_unit, const std::string& email_address, const std::string& private_key_data = {}, const std::string& password = {}); + + csr make_ecdsa_csr(const std::string& common_name, const std::string& country, const std::string& state, const std::string& city, const std::string& organization, const std::string& organizational_unit, const std::string& email_address, const std::string& private_key_data = {}, const std::string& password = {}); + + std::vector x509_crl_urls(const std::string& certificate); + + bool is_revoked_by_crl(const std::string& certificate, const std::string& cert_issuer_data, const std::string& crl_data); + } + } +} + +#endif diff --git a/Development/nmos/est_versions.h b/Development/nmos/est_versions.h new file mode 100644 index 000000000..1e6249144 --- /dev/null +++ b/Development/nmos/est_versions.h @@ -0,0 +1,26 @@ +#ifndef NMOS_EST_VERSIONS_H +#define NMOS_EST_VERSIONS_H + +#include +#include +#include "nmos/api_version.h" +#include "nmos/settings.h" + +namespace nmos +{ + namespace est_versions + { + const api_version v1_0{ 1, 0 }; + + const std::set all{ nmos::est_versions::v1_0 }; + + inline std::set from_settings(const nmos::settings& settings) + { + return settings.has_field(nmos::fields::est_versions) + ? boost::copy_range>(nmos::fields::est_versions(settings) | boost::adaptors::transformed([](const web::json::value& v) { return nmos::parse_api_version(v.as_string()); })) + : nmos::est_versions::all; + } + } +} + +#endif diff --git a/Development/nmos/mdns.cpp b/Development/nmos/mdns.cpp index 2549e5f43..42230fa27 100644 --- a/Development/nmos/mdns.cpp +++ b/Development/nmos/mdns.cpp @@ -12,6 +12,7 @@ #include "cpprest/uri_builder.h" #include "mdns/service_advertiser.h" #include "mdns/service_discovery.h" +#include "nmos/est_versions.h" #include "nmos/is09_versions.h" #include "nmos/is10_versions.h" #include "nmos/random.h" @@ -262,6 +263,16 @@ namespace nmos { txt_record_keys::api_auth, details::make_api_auth_value(api_auth) } }; } + else if (service == nmos::service_types::est) + { + // see https://specs.amwa.tv/bcp-003-03/releases/v1.0.0/docs/1.0._Certificate_Provisioning.html#dns-sd-txt-records + // EST API does not use authorization + return + { + { txt_record_keys::pri, details::make_pri_value(pri) }, + { txt_record_keys::api_selector, details::make_api_selector_value(selector) } + }; + } return {}; } @@ -311,6 +322,7 @@ namespace nmos if (nmos::service_types::register_ == service) return nmos::fields::registration_port(settings); if (nmos::service_types::system == service) return nmos::fields::system_port(settings); if (nmos::service_types::authorization == service) return nmos::experimental::fields::authorization_port(settings); + if (nmos::service_types::est == service) return nmos::experimental::fields::est_port(settings); return 0; } @@ -322,6 +334,7 @@ namespace nmos if (nmos::service_types::register_ == service) return "registration"; if (nmos::service_types::system == service) return "system"; if (nmos::service_types::authorization == service) return "auth"; + if (nmos::service_types::est == service) return "est"; return{}; } @@ -336,6 +349,8 @@ namespace nmos if (nmos::service_types::system == service) return nmos::is09_versions::from_settings(settings); // the Authorization API is defined by IS-10 if (nmos::service_types::authorization == service) return nmos::is10_versions::from_settings(settings); + // the EST API is defined by BCP-003-03 + if (nmos::service_types::est == service) return nmos::est_versions::from_settings(settings); // all the other APIs are defined by IS-04, and should advertise consistent versions return nmos::is04_versions::from_settings(settings); } @@ -524,17 +539,28 @@ namespace nmos } // check advertisement has a matching 'api_proto' value - auto resolved_proto = nmos::parse_api_proto_record(records); - if (api_proto.end() == api_proto.find(resolved_proto)) return true; + // ignore for EST service, no 'api_proto' in txt records (see nmos::make_txt_records) + auto resolved_proto = service_protocols::https; + if (service != nmos::service_types::est) + { + resolved_proto = nmos::parse_api_proto_record(records); + if (api_proto.end() == api_proto.find(resolved_proto)) return true; + } // check advertisement has a matching 'api_auth' value auto resolved_auth = nmos::parse_api_auth_record(records); if (api_auth.end() == api_auth.find(resolved_auth)) return true; // check the advertisement includes a version we support - auto resolved_vers = nmos::parse_api_ver_record(records); - auto resolved_ver = std::find_first_of(resolved_vers.rbegin(), resolved_vers.rend(), api_ver.rbegin(), api_ver.rend()); - if (resolved_vers.rend() == resolved_ver) return true; + // note: for the EST service, no 'api_ver' in the txt records, so just ignore it (see nmos::make_txt_records) + api_version resolved_ver{}; + if (service != nmos::service_types::est) + { + auto resolved_vers = nmos::parse_api_ver_record(records); + auto resolved_ver_ = std::find_first_of(resolved_vers.rbegin(), resolved_vers.rend(), api_ver.rbegin(), api_ver.rend()); + if (resolved_vers.rend() == resolved_ver_) return true; + resolved_ver = *resolved_ver_; + } // hmm, maybe in the future check for the matching 'api_selector' value auto resolved_selector = nmos::parse_api_selector_record(records); @@ -547,6 +573,13 @@ namespace nmos .set_port(resolved.port) .set_path(U("/.well-known/oauth-authorization-server")).append_path(!resolved_selector.empty() ? U("/") + resolved_selector : U("")); } + else if (service == nmos::service_types::est) + { + resolved_uri + .set_scheme(utility::s2us(resolved_proto)) + .set_port(resolved.port) + .set_path(U("/.well-known/est")).append_path(!resolved_selector.empty() ? U("/") + resolved_selector : U("")); + } else { resolved_uri @@ -563,7 +596,7 @@ namespace nmos { // sneakily stash the host name for the Host header in user info // cf. nmos::details::make_http_client - results->push_back({ { *resolved_ver, resolved_pri }, resolved_uri + results->push_back({ { resolved_ver, resolved_pri }, resolved_uri .set_user_info(host_name) .set_host(utility::s2us(ip_address)) .to_uri() diff --git a/Development/nmos/mdns.h b/Development/nmos/mdns.h index 8267ab850..12890be1a 100644 --- a/Development/nmos/mdns.h +++ b/Development/nmos/mdns.h @@ -45,6 +45,9 @@ namespace nmos // MQTT Broker // See https://specs.amwa.tv/is-07/releases/v1.0.1/docs/5.1._Transport_-_MQTT.html#7-broker-discovery const service_type mqtt{ "_nmos-mqtt._tcp" }; + + // EST API + const service_type est{ "_nmos-certs._tcp" }; } // "The DNS-SD advertisement MUST be accompanied by a TXT record of name 'api_proto' with a value diff --git a/Development/nmos/node_resource.cpp b/Development/nmos/node_resource.cpp index 8e5af4744..8ba909d86 100644 --- a/Development/nmos/node_resource.cpp +++ b/Development/nmos/node_resource.cpp @@ -4,6 +4,7 @@ #include "nmos/api_utils.h" // for nmos::http_scheme #include "nmos/clock_name.h" #include "nmos/clock_ref_type.h" +#include "nmos/est_versions.h" #include "nmos/is04_versions.h" #include "nmos/resource.h" @@ -48,6 +49,9 @@ namespace nmos data[U("interfaces")] = !web::json::empty(interfaces) ? interfaces : value::array(); + // See https://specs.amwa.tv/nmos-parameter-registers/branches/main/tags/certprov.html#certificate-provisioning-urn + data[U("tags")][U("urn:x-nmos:tag:certprov")] = nmos::experimental::fields::est_enabled(settings) ? value_from_elements(nmos::est_versions::from_settings(settings) | boost::adaptors::transformed(make_api_version)) : value::array(); + return{ is04_versions::v1_3, types::node, std::move(data), false }; } diff --git a/Development/nmos/node_server.cpp b/Development/nmos/node_server.cpp index 00258389d..3e592273e 100644 --- a/Development/nmos/node_server.cpp +++ b/Development/nmos/node_server.cpp @@ -21,7 +21,7 @@ namespace nmos { namespace experimental { - // Construct a server instance for an NMOS Node, implementing the IS-04 Node API, IS-05 Connection API, IS-07 Events API, the IS-10 Authorization API + // Construct a server instance for an NMOS Node, implementing the IS-04 Node API, IS-05 Connection API, IS-07 Events API, IS-10 Authorization API, BCP-003-03 EST API // and the experimental Logging API and Settings API, according to the specified data models and callbacks nmos::server make_node_server(nmos::node_model& node_model, nmos::experimental::node_implementation node_implementation, nmos::experimental::log_model& log_model, slog::base_gate& gate) { diff --git a/Development/nmos/node_server.h b/Development/nmos/node_server.h index 25a15d4b7..952cfebfe 100644 --- a/Development/nmos/node_server.h +++ b/Development/nmos/node_server.h @@ -8,6 +8,7 @@ #include "nmos/connection_api.h" #include "nmos/connection_activation.h" #include "nmos/control_protocol_handlers.h" +#include "nmos/est_certificate_handlers.h" #include "nmos/node_behaviour.h" #include "nmos/node_system_behaviour.h" #include "nmos/ocsp_response_handler.h" @@ -27,10 +28,14 @@ namespace nmos // underlying implementation into the server instance for the NMOS Node struct node_implementation { - node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response, get_authorization_bearer_token_handler get_authorization_bearer_token, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor, nmos::control_protocol_property_changed_handler control_protocol_property_changed) + node_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_client_certificate_handler load_client_certificate, nmos::experimental::receive_ca_certificate_handler ca_certificate_received, nmos::experimental::receive_server_certificate_handler rsa_server_certificate_received, nmos::experimental::receive_server_certificate_handler ecdsa_server_certificate_received, nmos::system_global_handler system_changed, nmos::registration_handler registration_changed, nmos::transport_file_parser parse_transport_file, nmos::details::connection_resource_patch_validator validate_staged, nmos::connection_resource_auto_resolver resolve_auto, nmos::connection_sender_transportfile_setter set_transportfile, nmos::connection_activation_handler connection_activated, nmos::ocsp_response_handler get_ocsp_response, get_authorization_bearer_token_handler get_authorization_bearer_token, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization, nmos::load_rsa_private_keys_handler load_rsa_private_keys, load_authorization_clients_handler load_authorization_clients, save_authorization_client_handler save_authorization_client, request_authorization_code_handler request_authorization_code, nmos::get_control_protocol_class_descriptor_handler get_control_protocol_class_descriptor, nmos::get_control_protocol_datatype_descriptor_handler get_control_protocol_datatype_descriptor, nmos::get_control_protocol_method_descriptor_handler get_control_protocol_method_descriptor, nmos::control_protocol_property_changed_handler control_protocol_property_changed) : load_server_certificates(std::move(load_server_certificates)) , load_dh_param(std::move(load_dh_param)) , load_ca_certificates(std::move(load_ca_certificates)) + , load_client_certificate(std::move(load_client_certificate)) + , ca_certificate_received(std::move(ca_certificate_received)) + , rsa_server_certificate_received(std::move(rsa_server_certificate_received)) + , ecdsa_server_certificate_received(std::move(ecdsa_server_certificate_received)) , system_changed(std::move(system_changed)) , registration_changed(std::move(registration_changed)) , parse_transport_file(std::move(parse_transport_file)) @@ -61,6 +66,7 @@ namespace nmos node_implementation& on_load_server_certificates(nmos::load_server_certificates_handler load_server_certificates) { this->load_server_certificates = std::move(load_server_certificates); return *this; } node_implementation& on_load_dh_param(nmos::load_dh_param_handler load_dh_param) { this->load_dh_param = std::move(load_dh_param); return *this; } node_implementation& on_load_ca_certificates(nmos::load_ca_certificates_handler load_ca_certificates) { this->load_ca_certificates = std::move(load_ca_certificates); return *this; } + node_implementation& on_load_client_certificate(nmos::load_client_certificate_handler load_client_certificate) { this->load_client_certificate = std::move(load_client_certificate); return *this; } node_implementation& on_system_changed(nmos::system_global_handler system_changed) { this->system_changed = std::move(system_changed); return *this; } node_implementation& on_registration_changed(nmos::registration_handler registration_changed) { this->registration_changed = std::move(registration_changed); return *this; } node_implementation& on_parse_transport_file(nmos::transport_file_parser parse_transport_file) { this->parse_transport_file = std::move(parse_transport_file); return *this; } @@ -73,6 +79,9 @@ namespace nmos node_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } node_implementation& on_get_authorization_bearer_token(get_authorization_bearer_token_handler get_authorization_bearer_token) { this->get_authorization_bearer_token = std::move(get_authorization_bearer_token); return *this; } node_implementation& on_validate_authorization(validate_authorization_handler validate_authorization) { this->validate_authorization = std::move(validate_authorization); return *this; } + node_implementation& on_ca_certificate_received(nmos::experimental::receive_ca_certificate_handler ca_certificate_received) { this->ca_certificate_received = std::move(ca_certificate_received); return *this; } + node_implementation& on_rsa_server_certificate_received(nmos::experimental::receive_server_certificate_handler rsa_server_certificate_received) { this->rsa_server_certificate_received = std::move(rsa_server_certificate_received); return *this; } + node_implementation& on_ecdsa_server_certificate_received(nmos::experimental::receive_server_certificate_handler ecdsa_server_certificate_received) { this->ecdsa_server_certificate_received = std::move(ecdsa_server_certificate_received); return *this; } node_implementation& on_ws_validate_authorization(ws_validate_authorization_handler ws_validate_authorization) { this->ws_validate_authorization = std::move(ws_validate_authorization); return *this; } node_implementation& on_load_rsa_private_keys(nmos::load_rsa_private_keys_handler load_rsa_private_keys) { this->load_rsa_private_keys = std::move(load_rsa_private_keys); return *this; } node_implementation& on_load_authorization_clients(load_authorization_clients_handler load_authorization_clients) { this->load_authorization_clients = std::move(load_authorization_clients); return *this; } @@ -95,6 +104,11 @@ namespace nmos nmos::load_server_certificates_handler load_server_certificates; nmos::load_dh_param_handler load_dh_param; nmos::load_ca_certificates_handler load_ca_certificates; + nmos::load_client_certificate_handler load_client_certificate; + + nmos::experimental::receive_ca_certificate_handler ca_certificate_received; + nmos::experimental::receive_server_certificate_handler rsa_server_certificate_received; + nmos::experimental::receive_server_certificate_handler ecdsa_server_certificate_received; nmos::system_global_handler system_changed; nmos::registration_handler registration_changed; diff --git a/Development/nmos/registry_server.cpp b/Development/nmos/registry_server.cpp index 540581215..af9a865d8 100644 --- a/Development/nmos/registry_server.cpp +++ b/Development/nmos/registry_server.cpp @@ -27,7 +27,7 @@ namespace nmos namespace experimental { - // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API, the IS-10 Authorization API + // Construct a server instance for an NMOS Registry instance, implementing the IS-04 Registration and Query APIs, the Node API, the IS-09 System API, the IS-10 Authorization API, the BCP-003-03 EST API // and the experimental DNS-SD Browsing API, Logging API and Settings API, according to the specified data models nmos::server make_registry_server(nmos::registry_model& registry_model, nmos::experimental::registry_implementation registry_implementation, nmos::experimental::log_model& log_model, slog::base_gate& gate) { diff --git a/Development/nmos/registry_server.h b/Development/nmos/registry_server.h index 82dce35ce..65917e279 100644 --- a/Development/nmos/registry_server.h +++ b/Development/nmos/registry_server.h @@ -3,6 +3,7 @@ #include "nmos/authorization_handlers.h" #include "nmos/certificate_handlers.h" +#include "nmos/est_certificate_handlers.h" #include "nmos/ocsp_response_handler.h" #include "nmos/ws_api_utils.h" @@ -25,10 +26,14 @@ namespace nmos // underlying implementation into the server instance for the NMOS Registry struct registry_implementation { - registry_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::ocsp_response_handler get_ocsp_response, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization) + registry_implementation(nmos::load_server_certificates_handler load_server_certificates, nmos::load_dh_param_handler load_dh_param, nmos::load_ca_certificates_handler load_ca_certificates, nmos::load_client_certificate_handler load_client_certificate, nmos::experimental::receive_ca_certificate_handler ca_certificate_received, nmos::experimental::receive_server_certificate_handler rsa_server_certificate_received, nmos::experimental::receive_server_certificate_handler ecdsa_server_certificate_received, nmos::ocsp_response_handler get_ocsp_response, validate_authorization_handler validate_authorization, ws_validate_authorization_handler ws_validate_authorization) : load_server_certificates(std::move(load_server_certificates)) , load_dh_param(std::move(load_dh_param)) , load_ca_certificates(std::move(load_ca_certificates)) + , load_client_certificate(std::move(load_client_certificate)) + , ca_certificate_received(std::move(ca_certificate_received)) + , rsa_server_certificate_received(std::move(rsa_server_certificate_received)) + , ecdsa_server_certificate_received(std::move(ecdsa_server_certificate_received)) , get_ocsp_response(std::move(get_ocsp_response)) , validate_authorization(std::move(validate_authorization)) , ws_validate_authorization(std::move(ws_validate_authorization)) @@ -45,6 +50,10 @@ namespace nmos registry_implementation& on_get_ocsp_response(nmos::ocsp_response_handler get_ocsp_response) { this->get_ocsp_response = std::move(get_ocsp_response); return *this; } registry_implementation& on_validate_authorization(validate_authorization_handler validate_authorization) { this->validate_authorization = std::move(validate_authorization); return* this; } registry_implementation& on_ws_validate_authorization(ws_validate_authorization_handler ws_validate_authorization) { this->ws_validate_authorization = std::move(ws_validate_authorization); return *this; } + registry_implementation& on_load_client_certificate(nmos::load_client_certificate_handler load_client_certificate) { this->load_client_certificate = std::move(load_client_certificate); return *this; } + registry_implementation& on_ca_certificate_received(nmos::experimental::receive_ca_certificate_handler ca_certificate_received) { this->ca_certificate_received = std::move(ca_certificate_received); return *this; } + registry_implementation& on_rsa_server_certificate_received(nmos::experimental::receive_server_certificate_handler rsa_server_certificate_received) { this->rsa_server_certificate_received = std::move(rsa_server_certificate_received); return *this; } + registry_implementation& on_ecdsa_server_certificate_received(nmos::experimental::receive_server_certificate_handler ecdsa_server_certificate_received) { this->ecdsa_server_certificate_received = std::move(ecdsa_server_certificate_received); return *this; } // determine if the required callbacks have been specified bool valid() const @@ -55,6 +64,11 @@ namespace nmos nmos::load_server_certificates_handler load_server_certificates; nmos::load_dh_param_handler load_dh_param; nmos::load_ca_certificates_handler load_ca_certificates; + nmos::load_client_certificate_handler load_client_certificate; + + nmos::experimental::receive_ca_certificate_handler ca_certificate_received; + nmos::experimental::receive_server_certificate_handler rsa_server_certificate_received; + nmos::experimental::receive_server_certificate_handler ecdsa_server_certificate_received; nmos::ocsp_response_handler get_ocsp_response; diff --git a/Development/nmos/server_utils.cpp b/Development/nmos/server_utils.cpp index 464e1fc6f..0db6655e9 100644 --- a/Development/nmos/server_utils.cpp +++ b/Development/nmos/server_utils.cpp @@ -55,12 +55,12 @@ namespace nmos const auto key = utility::us2s(server_certificate.private_key); if (0 == key.size()) { - throw ExceptionType({}, "Missing private key"); + throw ExceptionType({}, "Missing server private key"); } const auto cert_chain = utility::us2s(server_certificate.certificate_chain); if (0 == cert_chain.size()) { - throw ExceptionType({}, "Missing certificate chain"); + throw ExceptionType({}, "Missing server certificate chain"); } ctx.use_private_key(boost::asio::buffer(key.data(), key.size()), boost::asio::ssl::context_base::pem); ctx.use_certificate_chain(boost::asio::buffer(cert_chain.data(), cert_chain.size())); diff --git a/Development/nmos/settings.h b/Development/nmos/settings.h index c0981f8bf..9c7c8b047 100644 --- a/Development/nmos/settings.h +++ b/Development/nmos/settings.h @@ -107,6 +107,9 @@ namespace nmos // is12_versions [node]: used to specify the enabled API versions for a version-locked configuration const web::json::field_as_array is12_versions{ U("is12_versions") }; // when omitted, nmos::is12_versions::all is used + // est_versions [registry, node]: used to specify the enabled API versions for a version-locked configuration + const web::json::field_as_array est_versions{ U("est_versions") }; // when omitted, nmos::est_versions::all is used + // pri [registry, node]: used for the 'pri' TXT record; specifying nmos::service_priorities::no_priority (maximum value) disables advertisement completely const web::json::field_as_integer_or pri{ U("pri"), 100 }; // default to highest_development_priority @@ -464,6 +467,46 @@ namespace nmos // serial_number [node]: the serial number of the NcDeviceManager used for NMOS Control Protocol // See https://specs.amwa.tv/ms-05-02/branches/v1.0.x/docs/Framework.html#ncdevicemanager const web::json::field_as_string_or serial_number{ U("serial_number"), U("") }; + + // est_services [registry, node]: the discovered list of EST APIs, in the order they should be used + // this list is created and maintained by nmos::est_behaviour_thread; each entry is a uri like http://example.api.com/x-nmos/est/{version} + const web::json::field_as_value est_services{ U("est_services") }; + + // est_address [registry, node]: IP address or host name used to construct request URLs for the EST API (if not discovered via DNS-SD) + const web::json::field_as_string est_address{ U("est_address") }; + + // est_port [registry, node]: used to construct request URLs for the EST server's EST API (if not discovered via DNS-SD) + const web::json::field_as_integer_or est_port{ U("est_port"), 443 }; + + // est_selector [registry, node]: used to construct request URLs for the EST API (if not discovered via DNS-SD) + const web::json::field_as_string_or est_selector{ U("est_selector"), U("") }; + + // est_request_max [registry, node]: timeout for interactions with the EST API + const web::json::field_as_integer_or est_request_max{ U("est_request_max"), 30 }; + + // est_enabled [registry, node]: allow EST to be disabled, preventing from being automatically provisioned with a TLS Certificate if required by the network's security policy + const web::json::field_as_bool_or est_enabled{ U("est_enabled"), true }; + + // explicit_trust_est_enabled [registry, node]: allow explicit trust of EST server to be disable, to prevent the EST Client from requesting a TLS Certificate from a rogue server + const web::json::field_as_bool_or explicit_trust_est_enabled{ U("explicit_trust_est_enabled"), true }; + + // country [registry, node]: two-character abbreviation of country in which organization resides(e.g.GB) for CSR generation + const web::json::field_as_string_or country{ U("country"), U("") }; + + // state [registry, node]: the full name of your state or province for CSR generation + const web::json::field_as_string_or state{ U("state"), U("") }; + + // city [registry, node]: the city of your organization's main office, or a main office for your organization for CSR generation + const web::json::field_as_string_or city{ U("city"), U("") }; + + // organizational_unit [registry, node]: the name of the department or organization unit making the CSR request + const web::json::field_as_string_or organizational_unit{ U("organizational_unit"), U("") }; + + // email_address [registry, node]: the organization contact, usually of the certificate administrator or IT department for CSR generation + const web::json::field_as_string_or email_address{ U("email_address"), U("") }; + + // certificate_revocation_interval [registry, node]: the interval to check the revocation status of the TLS Certificates using CRL + const web::json::field_as_integer_or certificate_revocation_interval{ U("certificate_revocation_interval"), 600 }; } } } diff --git a/Development/nmos/slog.h b/Development/nmos/slog.h index 13a1a67b8..93f66ac97 100644 --- a/Development/nmos/slog.h +++ b/Development/nmos/slog.h @@ -44,6 +44,7 @@ namespace nmos const category send_events_ws_commands{ "send_events_ws_commands" }; const category node_system_behaviour{ "node_system_behaviour" }; const category ocsp_behaviour{ "ocsp_behaviour" }; + const category est_behaviour{ "est_behaviour" }; const category authorization_behaviour{ "authorization_behaviour" }; const category send_control_protocol_ws_messages{ "send_control_protocol_ws_messages" }; diff --git a/Development/ssl/ssl_utils.cpp b/Development/ssl/ssl_utils.cpp index 9dd4cdb90..0d10be459 100644 --- a/Development/ssl/ssl_utils.cpp +++ b/Development/ssl/ssl_utils.cpp @@ -204,7 +204,7 @@ namespace ssl } // calculate the number of seconds until expiry of the specified certificate - // 0 is returned if certificate has already expired + // if the certificate has expired, the value of 0 will be returned double certificate_expiry_from_now(const std::string& certificate) { const auto certificate_info = get_certificate_info(certificate); @@ -212,5 +212,12 @@ namespace ssl const auto from_now = difftime(certificate_info.not_after, now); return (std::max)(0.0, from_now); } + + // calculate the number of the factor of seconds until expiry of the specified certificate + // if the certificate has expired, the value of 0 will be returned + double certificate_expiry_from_now(const std::string& certificate, double ratio) + { + return certificate_expiry_from_now(certificate) * ratio; + } } } diff --git a/Development/ssl/ssl_utils.h b/Development/ssl/ssl_utils.h index 2cf2853c6..9e8286470 100644 --- a/Development/ssl/ssl_utils.h +++ b/Development/ssl/ssl_utils.h @@ -44,8 +44,12 @@ namespace ssl std::vector split_certificate_chain(const std::string& certificate_chain); // calculate the number of seconds until expiry of the specified certificate - // 0 is returned if certificate has already expired + // if the certificate has expired, the value of 0 will be returned double certificate_expiry_from_now(const std::string& certificate); + + // calculate the number of the factor of seconds until expiry of the specified certificate + // if the certificate has expired, the value of 0 will be returned + double certificate_expiry_from_now(const std::string& certificate, double ratio); } }