diff --git a/CMakeLists.txt b/CMakeLists.txt index c99ef885..01d120b8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -271,6 +271,8 @@ endif() # Compilation ############################################################################### +option(SECURITY "Whether to build with security features and test-suite" OFF) + # Create the target and its dependencies, note that variable PROJECT_NAME is created by the closest project() # called in the current directory scope or above. # We must populate this variables as project sources grow @@ -358,6 +360,7 @@ target_compile_definitions(${PROJECT_NAME} PRIVATE ASIO_STANDALONE $<$,$>:_WIN32_WINNT=0x0603> $<$,$>>:_WIN32_WINNT=0x0601> + $<$:SECURITY> ) #if (WIN32) diff --git a/include/DiscoveryServerManager.h b/include/DiscoveryServerManager.h index 1ade3df9..0ac86a1c 100644 --- a/include/DiscoveryServerManager.h +++ b/include/DiscoveryServerManager.h @@ -128,6 +128,9 @@ class DiscoveryServerManager // Snapshops container snapshots_list snapshots; + // Participant PropertiesPolicy + PropertyPolicy properties_; + // Topic description profiles std::map topic_description_profiles_map; @@ -135,6 +138,12 @@ class DiscoveryServerManager bool auto_shutdown; // close when event processing is finished? bool enable_prefix_validation; // allow multiple servers share the same prefix? (only for testing purposes) bool correctly_created_; // store false if the DiscoveryServerManager has not been successfully created + bool security_enabled_; // security enabled + +#ifdef SECURITY + // Ceritficate path + std::string auth_cert_path_; +#endif // SECURITY void loadProfiles( tinyxml2::XMLElement* profiles); @@ -169,6 +178,13 @@ class DiscoveryServerManager void saveSnapshots( const std::string& file) const; + /** + * @brief: This method loads a set of properties into attributes + * @param [in] props_n: element containig the XML properties + */ + bool load_properties( + tinyxml2::XMLElement* props_n); + // File where to save snapshots std::string snapshots_output_file; // validation required @@ -185,6 +201,7 @@ class DiscoveryServerManager DiscoveryServerManager( const std::string& xml_file_path, + const std::string& security_props_file_path, const bool shared_memory_off); ~DiscoveryServerManager(); diff --git a/src/DiscoveryServerManager.cpp b/src/DiscoveryServerManager.cpp index c244be40..5eebaa9f 100644 --- a/src/DiscoveryServerManager.cpp +++ b/src/DiscoveryServerManager.cpp @@ -31,6 +31,10 @@ #include "LateJoiner.h" #include "log/DSLog.h" +#ifdef SECURITY +#include "security_helpers.hpp" +#endif // ifdef SECURITY + using namespace eprosima::fastdds; using namespace eprosima::discovery_server; @@ -49,6 +53,9 @@ const char* TYPES = "types"; const char* PUBLISHER = "publisher"; const char* SUBSCRIBER = "subscriber"; const char* TOPIC = "topic"; +const char* PROPERTIES = "properties"; +const char* PROPERTY = "property"; +const char* VALUE = "value"; } // namespace DSxmlparser } // namespace fastdds } // namespace eprosima @@ -60,11 +67,13 @@ const std::chrono::seconds DiscoveryServerManager::last_snapshot_delay_ = std::c DiscoveryServerManager::DiscoveryServerManager( const std::string& xml_file_path, + const std::string& security_props_file_path, const bool shared_memory_off) : no_callbacks(false) , auto_shutdown(true) , enable_prefix_validation(true) , correctly_created_(false) + , security_enabled_(false) , last_PDP_callback_(Snapshot::_steady_clock) , last_EDP_callback_(Snapshot::_steady_clock) , shared_memory_off_(shared_memory_off) @@ -101,6 +110,51 @@ DiscoveryServerManager::DiscoveryServerManager( // try load the enable_prefix_validation attribute enable_prefix_validation = root->BoolAttribute(s_sPrefixValidation.c_str(), enable_prefix_validation); +#ifdef SECURITY + //try load security properties if any + if (!security_props_file_path.empty()) + { + tinyxml2::XMLDocument props_doc; + + if (tinyxml2::XMLError::XML_SUCCESS == props_doc.LoadFile(security_props_file_path.c_str())) + { + tinyxml2::XMLElement* root = props_doc.FirstChildElement(eprosima::fastdds::DSxmlparser::PROPERTIES); + if (root != nullptr) + { + if (!load_properties(root)) + { + LOG_ERROR("Error loading PropertiesPolicy from properties file"); + return; + } + else + { + security_enabled_ = true; + auto auth_property = PropertyPolicyHelper::find_property(properties_, "dds.sec.auth.builtin.PKI-DH.identity_certificate"); + if (auth_property == nullptr) + { + LOG_ERROR("No certificate path found in properties file"); + return; + } + else + { + auth_cert_path_ = *auth_property; + } + } + } + else + { + LOG_ERROR("Error retrieving properties element from properties file"); + return; + } + } + else + { + LOG_ERROR("Could not load properties file"); + return; + } + } +#endif // ifdef SECURITY + for (auto child = doc.FirstChildElement(s_sDS.c_str()); child != nullptr; child = child->NextSiblingElement(s_sDS.c_str())) { @@ -969,6 +1023,23 @@ void DiscoveryServerManager::loadServer( (void)b; assert(b.discoveryProtocol == eprosima::fastdds::rtps::DiscoveryProtocol::SERVER || b.discoveryProtocol == eprosima::fastdds::rtps::DiscoveryProtocol::BACKUP); + // Extend Participant properties if applies + if (!properties_.properties().empty()) + { + dpQOS.properties(properties_); + } + +#ifdef SECURITY + // mangle GUID if security is enabled + if (security_enabled_) + { + GUID_t original_guid = GUID_t(guid.guidPrefix, c_EntityId_RTPSParticipant); + // If security is enabled, mangle GUID + mangle_guid(auth_cert_path_, original_guid, + guid); + } +#endif // SECURITY + // Create the participant or the associated events DelayedParticipantCreation event(creation_time, std::move(dpQOS), &DiscoveryServerManager::addServer); if (creation_time == getTime()) @@ -1176,6 +1247,12 @@ void DiscoveryServerManager::loadClient( dpQOS.transport().user_transports.push_back(udp_transport); } + // Extend Participant properties if applies + if (!properties_.properties().empty()) + { + dpQOS.properties(properties_); + } + GUID_t guid(dpQOS.wire_protocol().prefix, c_EntityId_RTPSParticipant); DelayedParticipantDestruction* destruction_event = nullptr; DelayedParticipantCreation* creation_event = nullptr; @@ -1281,6 +1358,12 @@ void DiscoveryServerManager::loadSimple( dpQOS.name() = name; } + // Extend Participant properties if applies + if (!properties_.properties().empty()) + { + dpQOS.properties(properties_); + } + GUID_t guid(dpQOS.wire_protocol().prefix, c_EntityId_RTPSParticipant); DelayedParticipantDestruction* destruction_event = nullptr; DelayedParticipantCreation* creation_event = nullptr; @@ -2172,3 +2255,27 @@ void DiscoveryServerManager::saveSnapshots( LOG("Snapshot file saved " << file << "."); } } + +bool DiscoveryServerManager::load_properties(tinyxml2::XMLElement* props_n) +{ + bool ret = true; + tinyxml2::XMLElement* prop = props_n->FirstChildElement(eprosima::fastdds::DSxmlparser::PROPERTY); + + for (;prop != nullptr; prop = prop->NextSiblingElement(eprosima::fastdds::DSxmlparser::PROPERTY)) + { + tinyxml2::XMLElement* name = prop->FirstChildElement(eprosima::fastdds::DSxmlparser::NAME); + tinyxml2::XMLElement* value = prop->FirstChildElement(eprosima::fastdds::DSxmlparser::VALUE); + + if (nullptr != name && nullptr != value) + { + properties_.properties().push_back({name->GetText(), value->GetText()}); + } + else + { + LOG_ERROR("Missing name/value for property " << prop->GetText()); + ret = false; + } + } + + return ret; +} diff --git a/src/arguments.h b/src/arguments.h index 70875703..27218144 100644 --- a/src/arguments.h +++ b/src/arguments.h @@ -23,6 +23,7 @@ enum optionIndex UNKNOWN, HELP, CONFIG_FILE, + PROPERTIES_FILE, OUTPUT_FILE, SHM }; @@ -46,6 +47,9 @@ const option::Descriptor usage[] = { { CONFIG_FILE, 0, "c", "config-file", Arg::check_inp, " -c \t--config-file Mandatory configuration file path\n"}, + { PROPERTIES_FILE, 0, "p", "props-file", Arg::check_inp, + " -p \t--props-file Optional participant properties configuration file path\n"}, + { OUTPUT_FILE, 0, "o", "output-file", Arg::check_inp, " -o \t--output-file File to write result snapshots. If not specified" " snapshots will be written in the file specified in the snapshot\n"}, diff --git a/src/main.cpp b/src/main.cpp index 589c98a6..6c6bd18c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -120,8 +120,24 @@ int main( // Load Default XML files DomainParticipantFactory::get_instance()->load_profiles(); + // Load properties file path from arg + pOp = options[PROPERTIES_FILE]; + + std::string path_to_properties; + + if ( nullptr != pOp ) + { + if ( pOp->count() != 1) + { + cout << "Only one properties file can be specified." << endl; + return 1; + } + + path_to_properties = pOp->arg; + } + // Create DiscoveryServerManager - DiscoveryServerManager manager(path_to_config, options[SHM]); + DiscoveryServerManager manager(path_to_config, path_to_properties, options[SHM]); if (!manager.correctly_created()) { return_code = 1; diff --git a/src/security_helpers.hpp b/src/security_helpers.hpp new file mode 100644 index 00000000..efa361f9 --- /dev/null +++ b/src/security_helpers.hpp @@ -0,0 +1,113 @@ +// Copyright 2025 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifdef SECURITY + +#include + +#include +#include +#include + +#include + +using namespace eprosima::fastdds::rtps; + +inline bool mangle_guid( + const std::string& cert_path, + const GUID_t& candidate_participant_key, + GUID_t& adjusted_participant_key) +{ + BIO* bio = BIO_new(BIO_s_file()); + X509* cert; + + if (bio != nullptr) + { + if (BIO_read_filename(bio, cert_path.substr(7).c_str()) > 0) + { + cert = PEM_read_bio_X509_AUX(bio, NULL, NULL, NULL); + } + else + { + std::cerr << "OpenSSL library cannot read file "<< cert_path.substr(7) << std::endl; + } + + BIO_free(bio); + } + else + { + std::cerr << "OpenSSL library cannot allocate file" << std::endl; + } + + X509_NAME* cert_sn = X509_get_subject_name(cert); + + unsigned char md[SHA256_DIGEST_LENGTH]; + unsigned int length = 0; + + if (!X509_NAME_digest(cert_sn, EVP_sha256(), md, &length) || length != SHA256_DIGEST_LENGTH) + { + std::cerr << "OpenSSL library cannot hash sha256" << std::endl; + return false; + } + + adjusted_participant_key.guidPrefix.value[0] = 0x80 | (md[0] >> 1); + adjusted_participant_key.guidPrefix.value[1] = (md[0] << 7) | (md[1] >> 1); + adjusted_participant_key.guidPrefix.value[2] = (md[1] << 7) | (md[2] >> 1); + adjusted_participant_key.guidPrefix.value[3] = (md[2] << 7) | (md[3] >> 1); + adjusted_participant_key.guidPrefix.value[4] = (md[3] << 7) | (md[4] >> 1); + adjusted_participant_key.guidPrefix.value[5] = (md[4] << 7) | (md[5] >> 1); + + unsigned char key[16] = { + candidate_participant_key.guidPrefix.value[0], + candidate_participant_key.guidPrefix.value[1], + candidate_participant_key.guidPrefix.value[2], + candidate_participant_key.guidPrefix.value[3], + candidate_participant_key.guidPrefix.value[4], + candidate_participant_key.guidPrefix.value[5], + candidate_participant_key.guidPrefix.value[6], + candidate_participant_key.guidPrefix.value[7], + candidate_participant_key.guidPrefix.value[8], + candidate_participant_key.guidPrefix.value[9], + candidate_participant_key.guidPrefix.value[10], + candidate_participant_key.guidPrefix.value[11], + candidate_participant_key.entityId.value[0], + candidate_participant_key.entityId.value[1], + candidate_participant_key.entityId.value[2], + candidate_participant_key.entityId.value[3] + }; + + if (!EVP_Digest(&key, 16, md, NULL, EVP_sha256(), NULL)) + { + std::cerr << "OpenSSL library cannot hash sha256" << std::endl; + return false; + } + + adjusted_participant_key.guidPrefix.value[6] = md[0]; + adjusted_participant_key.guidPrefix.value[7] = md[1]; + adjusted_participant_key.guidPrefix.value[8] = md[2]; + adjusted_participant_key.guidPrefix.value[9] = md[3]; + adjusted_participant_key.guidPrefix.value[10] = md[4]; + adjusted_participant_key.guidPrefix.value[11] = md[5]; + + adjusted_participant_key.entityId.value[0] = candidate_participant_key.entityId.value[0]; + adjusted_participant_key.entityId.value[1] = candidate_participant_key.entityId.value[1]; + adjusted_participant_key.entityId.value[2] = candidate_participant_key.entityId.value[2]; + adjusted_participant_key.entityId.value[3] = candidate_participant_key.entityId.value[3]; + + EVP_cleanup(); + + return true; +} + +#endif //SECURITY diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index f977efec..bcef4953 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -205,6 +205,33 @@ foreach(TEST IN LISTS TEST_LIST) set_property(TEST ${TEST_NAME} PROPERTY LABELS xfail) endif() + if(SECURITY) + unset(TEST_NAME) + set(TEST_NAME "discovery_server_test.${TEST}.SECURITY") + list(APPEND TEST_CASE_LIST ${TEST_NAME}) + + # Test with security + # No shared memory, no intraprocess + add_test(NAME ${TEST_NAME} + COMMAND ${PYTHON_EXECUTABLE} ${RUN_TEST} + -e $ + -p ${TESTS_PARAMS} + -f $<$:$> + -t ${TEST} + -s false + -i false + -S true + -C ${PROJECT_SOURCE_DIR}/test/certs) + + set_tests_properties(${TEST_NAME} PROPERTIES + REQUIRED_FILES ${RUN_TEST} + REQUIRED_FILES ${TESTS_PARAMS}) + + if("${TEST}" IN_LIST FAIL_TEST_CASES) + set_property(TEST ${TEST_NAME} PROPERTY LABELS xfail) + endif() + endif() + endforeach() # Windows requires an special treatment of environmental variables diff --git a/test/certs/governance_all_enable.smime b/test/certs/governance_all_enable.smime new file mode 100644 index 00000000..7b234a67 --- /dev/null +++ b/test/certs/governance_all_enable.smime @@ -0,0 +1,72 @@ +MIME-Version: 1.0 +Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha-256"; boundary="----205663DC800FC27263B797AC629F745C" + +This is an S/MIME signed message + +------205663DC800FC27263B797AC629F745C +Content-Type: text/plain + + + + + + + + 0 + 230 + + + false + true + ENCRYPT + ENCRYPT + ENCRYPT + + + * + true + true + true + true + ENCRYPT + ENCRYPT + + + + + + + +------205663DC800FC27263B797AC629F745C +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MIIEeQYJKoZIhvcNAQcCoIIEajCCBGYCAQExDzANBglghkgBZQMEAgEFADALBgkq +hkiG9w0BBwGgggJAMIICPDCCAeOgAwIBAgIJALZwpgo2sxthMAoGCCqGSM49BAMC +MIGaMQswCQYDVQQGEwJFUzELMAkGA1UECAwCTUExFDASBgNVBAcMC1RyZXMgQ2Fu +dG9zMREwDwYDVQQKDAhlUHJvc2ltYTERMA8GA1UECwwIZVByb3NpbWExHjAcBgNV +BAMMFWVQcm9zaW1hIE1haW4gVGVzdCBDQTEiMCAGCSqGSIb3DQEJARYTbWFpbmNh +QGVwcm9zaW1hLmNvbTAeFw0xNzA5MDYwOTAzMDNaFw0yNzA5MDQwOTAzMDNaMIGa +MQswCQYDVQQGEwJFUzELMAkGA1UECAwCTUExFDASBgNVBAcMC1RyZXMgQ2FudG9z +MREwDwYDVQQKDAhlUHJvc2ltYTERMA8GA1UECwwIZVByb3NpbWExHjAcBgNVBAMM +FWVQcm9zaW1hIE1haW4gVGVzdCBDQTEiMCAGCSqGSIb3DQEJARYTbWFpbmNhQGVw +cm9zaW1hLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGLlhB3WQ8l1fpUE +3DfOoulA/de38Zfj7hmpKtOnxiH2q6RJbwhxvJeA7R7mkmAKaJKmzx695BjyiXVS +7bE7vgejEDAOMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgVTY1BEvT +4pw3GyBMzaUqmp69wi0kBkyOgq04OhyJ13UCICR125vvt0fUhXsXaxOAx28E4Ac9 +SVxpI+3UYs2kV5n0MYIB/TCCAfkCAQEwgagwgZoxCzAJBgNVBAYTAkVTMQswCQYD +VQQIDAJNQTEUMBIGA1UEBwwLVHJlcyBDYW50b3MxETAPBgNVBAoMCGVQcm9zaW1h +MREwDwYDVQQLDAhlUHJvc2ltYTEeMBwGA1UEAwwVZVByb3NpbWEgTWFpbiBUZXN0 +IENBMSIwIAYJKoZIhvcNAQkBFhNtYWluY2FAZXByb3NpbWEuY29tAgkAtnCmCjaz +G2EwDQYJYIZIAWUDBAIBBQCggeQwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAc +BgkqhkiG9w0BCQUxDxcNMjMwMjIxMDk1MjM0WjAvBgkqhkiG9w0BCQQxIgQguOmE +ipH9WhFwZt05wsDRKD9aInelvQO2SQANMKbmGV8weQYJKoZIhvcNAQkPMWwwajAL +BglghkgBZQMEASowCwYJYIZIAWUDBAEWMAsGCWCGSAFlAwQBAjAKBggqhkiG9w0D +BzAOBggqhkiG9w0DAgICAIAwDQYIKoZIhvcNAwICAUAwBwYFKw4DAgcwDQYIKoZI +hvcNAwICASgwCgYIKoZIzj0EAwIERzBFAiBZyyAcaQ7KB5qI/oF276mxSspzFkCI +HH1qbSPHKfjueQIhAIJQDdUCbimYKFGJYYYBZ/JBrdzMaB6Ordmomvkgjcx0 + +------205663DC800FC27263B797AC629F745C-- + diff --git a/test/certs/governance_all_enable.xml b/test/certs/governance_all_enable.xml new file mode 100644 index 00000000..b800591b --- /dev/null +++ b/test/certs/governance_all_enable.xml @@ -0,0 +1,31 @@ + + + + + + + 0 + 230 + + + false + true + ENCRYPT + ENCRYPT + ENCRYPT + + + * + true + true + true + true + ENCRYPT + ENCRYPT + + + + + + diff --git a/test/certs/maincacert.pem b/test/certs/maincacert.pem new file mode 100644 index 00000000..b6d2da28 --- /dev/null +++ b/test/certs/maincacert.pem @@ -0,0 +1,14 @@ +-----BEGIN CERTIFICATE----- +MIICPDCCAeOgAwIBAgIJALZwpgo2sxthMAoGCCqGSM49BAMCMIGaMQswCQYDVQQG +EwJFUzELMAkGA1UECAwCTUExFDASBgNVBAcMC1RyZXMgQ2FudG9zMREwDwYDVQQK +DAhlUHJvc2ltYTERMA8GA1UECwwIZVByb3NpbWExHjAcBgNVBAMMFWVQcm9zaW1h +IE1haW4gVGVzdCBDQTEiMCAGCSqGSIb3DQEJARYTbWFpbmNhQGVwcm9zaW1hLmNv +bTAeFw0xNzA5MDYwOTAzMDNaFw0yNzA5MDQwOTAzMDNaMIGaMQswCQYDVQQGEwJF +UzELMAkGA1UECAwCTUExFDASBgNVBAcMC1RyZXMgQ2FudG9zMREwDwYDVQQKDAhl +UHJvc2ltYTERMA8GA1UECwwIZVByb3NpbWExHjAcBgNVBAMMFWVQcm9zaW1hIE1h +aW4gVGVzdCBDQTEiMCAGCSqGSIb3DQEJARYTbWFpbmNhQGVwcm9zaW1hLmNvbTBZ +MBMGByqGSM49AgEGCCqGSM49AwEHA0IABGLlhB3WQ8l1fpUE3DfOoulA/de38Zfj +7hmpKtOnxiH2q6RJbwhxvJeA7R7mkmAKaJKmzx695BjyiXVS7bE7vgejEDAOMAwG +A1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgVTY1BEvT4pw3GyBMzaUqmp69 +wi0kBkyOgq04OhyJ13UCICR125vvt0fUhXsXaxOAx28E4Ac9SVxpI+3UYs2kV5n0 +-----END CERTIFICATE----- diff --git a/test/certs/maincakey.pem b/test/certs/maincakey.pem new file mode 100644 index 00000000..bd7d89f4 --- /dev/null +++ b/test/certs/maincakey.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgRaipe1KYZNzj+35E +N2jvtzjRsQ7n9Me/vm35UKGuVI6hRANCAARi5YQd1kPJdX6VBNw3zqLpQP3Xt/GX +4+4ZqSrTp8Yh9qukSW8IcbyXgO0e5pJgCmiSps8eveQY8ol1Uu2xO74H +-----END PRIVATE KEY----- diff --git a/test/certs/mainpartcert.pem b/test/certs/mainpartcert.pem new file mode 100644 index 00000000..ab99f06c --- /dev/null +++ b/test/certs/mainpartcert.pem @@ -0,0 +1,44 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + ae:7b:ad:8c:07:5a:ea:f3 + Signature Algorithm: ecdsa-with-SHA256 + Issuer: C=ES, ST=MA, L=Tres Cantos, O=eProsima, OU=eProsima, CN=eProsima Main Test CA/emailAddress=mainca@eprosima.com + Validity + Not Before: Sep 6 09:04:05 2017 GMT + Not After : Sep 4 09:04:05 2027 GMT + Subject: C=ES, ST=MA, O=eProsima, OU=eProsima, CN=Main Publisher/emailAddress=mainpub@eprosima.com + Subject Public Key Info: + Public Key Algorithm: id-ecPublicKey + Public-Key: (256 bit) + pub: + 04:55:95:f0:0b:1f:56:3f:80:4e:97:7e:1b:69:9c: + 7b:54:53:22:c4:a3:96:e9:99:2c:3d:c7:a8:8c:ec: + 1c:fd:d1:35:e7:ba:7d:63:01:9b:42:82:00:73:2c: + 52:e2:e1:0b:db:53:d9:45:a0:f8:64:1c:be:c5:0d: + 51:18:14:9f:90 + ASN1 OID: prime256v1 + NIST CURVE: P-256 + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + Signature Algorithm: ecdsa-with-SHA256 + 30:45:02:21:00:9c:e7:46:44:78:0c:95:eb:a7:38:9a:a7:af: + 4b:6c:bd:84:3b:bb:85:09:25:3d:49:b1:79:9e:e2:7e:dc:99: + 42:02:20:60:78:bd:d0:1e:cd:bc:4b:e3:25:2f:dd:56:6d:c8: + 29:78:3d:df:72:bc:bd:90:de:c6:19:b0:48:44:31:c7:46 +-----BEGIN CERTIFICATE----- +MIICHTCCAcOgAwIBAgIJAK57rYwHWurzMAoGCCqGSM49BAMCMIGaMQswCQYDVQQG +EwJFUzELMAkGA1UECAwCTUExFDASBgNVBAcMC1RyZXMgQ2FudG9zMREwDwYDVQQK +DAhlUHJvc2ltYTERMA8GA1UECwwIZVByb3NpbWExHjAcBgNVBAMMFWVQcm9zaW1h +IE1haW4gVGVzdCBDQTEiMCAGCSqGSIb3DQEJARYTbWFpbmNhQGVwcm9zaW1hLmNv +bTAeFw0xNzA5MDYwOTA0MDVaFw0yNzA5MDQwOTA0MDVaMH4xCzAJBgNVBAYTAkVT +MQswCQYDVQQIDAJNQTERMA8GA1UECgwIZVByb3NpbWExETAPBgNVBAsMCGVQcm9z +aW1hMRcwFQYDVQQDDA5NYWluIFB1Ymxpc2hlcjEjMCEGCSqGSIb3DQEJARYUbWFp +bnB1YkBlcHJvc2ltYS5jb20wWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAARVlfAL +H1Y/gE6XfhtpnHtUUyLEo5bpmSw9x6iM7Bz90TXnun1jAZtCggBzLFLi4QvbU9lF +oPhkHL7FDVEYFJ+Qow0wCzAJBgNVHRMEAjAAMAoGCCqGSM49BAMCA0gAMEUCIQCc +50ZEeAyV66c4mqevS2y9hDu7hQklPUmxeZ7iftyZQgIgYHi90B7NvEvjJS/dVm3I +KXg933K8vZDexhmwSEQxx0Y= +-----END CERTIFICATE----- diff --git a/test/certs/mainpartkey.pem b/test/certs/mainpartkey.pem new file mode 100644 index 00000000..cfb610b5 --- /dev/null +++ b/test/certs/mainpartkey.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgY5T1nA3Wpo8/JegF +k9vz0eTeboO2NB5LqoIDCICa8YChRANCAARVlfALH1Y/gE6XfhtpnHtUUyLEo5bp +mSw9x6iM7Bz90TXnun1jAZtCggBzLFLi4QvbU9lFoPhkHL7FDVEYFJ+Q +-----END PRIVATE KEY----- diff --git a/test/certs/permissions.smime b/test/certs/permissions.smime new file mode 100644 index 00000000..8f943a3d --- /dev/null +++ b/test/certs/permissions.smime @@ -0,0 +1,72 @@ +MIME-Version: 1.0 +Content-Type: multipart/signed; protocol="application/x-pkcs7-signature"; micalg="sha-256"; boundary="----F7C423515E88F48AE4A6D4674403FDFE" + +This is an S/MIME signed message + +------F7C423515E88F48AE4A6D4674403FDFE +Content-Type: text/plain + + + + + + emailAddress=mainpub@eprosima.com, CN=Main Publisher, OU=eProsima, O=eProsima, ST=MA, C=ES + + 2013-06-01T13:00:00 + 2038-06-01T13:00:00 + + + + + 0 + 230 + + + + + * + + + + + * + + + + DENY + + + + +------F7C423515E88F48AE4A6D4674403FDFE +Content-Type: application/x-pkcs7-signature; name="smime.p7s" +Content-Transfer-Encoding: base64 +Content-Disposition: attachment; filename="smime.p7s" + +MIIEeQYJKoZIhvcNAQcCoIIEajCCBGYCAQExDzANBglghkgBZQMEAgEFADALBgkq +hkiG9w0BBwGgggJAMIICPDCCAeOgAwIBAgIJALZwpgo2sxthMAoGCCqGSM49BAMC +MIGaMQswCQYDVQQGEwJFUzELMAkGA1UECAwCTUExFDASBgNVBAcMC1RyZXMgQ2Fu +dG9zMREwDwYDVQQKDAhlUHJvc2ltYTERMA8GA1UECwwIZVByb3NpbWExHjAcBgNV +BAMMFWVQcm9zaW1hIE1haW4gVGVzdCBDQTEiMCAGCSqGSIb3DQEJARYTbWFpbmNh +QGVwcm9zaW1hLmNvbTAeFw0xNzA5MDYwOTAzMDNaFw0yNzA5MDQwOTAzMDNaMIGa +MQswCQYDVQQGEwJFUzELMAkGA1UECAwCTUExFDASBgNVBAcMC1RyZXMgQ2FudG9z +MREwDwYDVQQKDAhlUHJvc2ltYTERMA8GA1UECwwIZVByb3NpbWExHjAcBgNVBAMM +FWVQcm9zaW1hIE1haW4gVGVzdCBDQTEiMCAGCSqGSIb3DQEJARYTbWFpbmNhQGVw +cm9zaW1hLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGLlhB3WQ8l1fpUE +3DfOoulA/de38Zfj7hmpKtOnxiH2q6RJbwhxvJeA7R7mkmAKaJKmzx695BjyiXVS +7bE7vgejEDAOMAwGA1UdEwQFMAMBAf8wCgYIKoZIzj0EAwIDRwAwRAIgVTY1BEvT +4pw3GyBMzaUqmp69wi0kBkyOgq04OhyJ13UCICR125vvt0fUhXsXaxOAx28E4Ac9 +SVxpI+3UYs2kV5n0MYIB/TCCAfkCAQEwgagwgZoxCzAJBgNVBAYTAkVTMQswCQYD +VQQIDAJNQTEUMBIGA1UEBwwLVHJlcyBDYW50b3MxETAPBgNVBAoMCGVQcm9zaW1h +MREwDwYDVQQLDAhlUHJvc2ltYTEeMBwGA1UEAwwVZVByb3NpbWEgTWFpbiBUZXN0 +IENBMSIwIAYJKoZIhvcNAQkBFhNtYWluY2FAZXByb3NpbWEuY29tAgkAtnCmCjaz +G2EwDQYJYIZIAWUDBAIBBQCggeQwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAc +BgkqhkiG9w0BCQUxDxcNMjMwMjE2MTQwMjU5WjAvBgkqhkiG9w0BCQQxIgQgTNU9 +XvIPHn/lsXX4n0MFp6YijDkDkx7/yP8x/7IKOgMweQYJKoZIhvcNAQkPMWwwajAL +BglghkgBZQMEASowCwYJYIZIAWUDBAEWMAsGCWCGSAFlAwQBAjAKBggqhkiG9w0D +BzAOBggqhkiG9w0DAgICAIAwDQYIKoZIhvcNAwICAUAwBwYFKw4DAgcwDQYIKoZI +hvcNAwICASgwCgYIKoZIzj0EAwIERzBFAiALAu7jtqdmKZtGyfo0LSDccaZtpffa +bCqItPVlT2+9kAIhALxRDkvkB4uPOZbOdViYew0sFOoUr+wwHOKpEM8J4HkR + +------F7C423515E88F48AE4A6D4674403FDFE-- + diff --git a/test/certs/permissions.xml b/test/certs/permissions.xml new file mode 100644 index 00000000..2e99421e --- /dev/null +++ b/test/certs/permissions.xml @@ -0,0 +1,31 @@ + + + + + emailAddress=mainpub@eprosima.com, CN=Main Publisher, OU=eProsima, O=eProsima, ST=MA, C=ES + + 2013-06-01T13:00:00 + 2038-06-01T13:00:00 + + + + + 0 + 230 + + + + + * + + + + + * + + + + DENY + + + diff --git a/test/configuration/security_properties.xml b/test/configuration/security_properties.xml new file mode 100644 index 00000000..7b9299a1 --- /dev/null +++ b/test/configuration/security_properties.xml @@ -0,0 +1,44 @@ + + + + + dds.sec.auth.plugin + builtin.PKI-DH + + + + dds.sec.auth.builtin.PKI-DH.identity_ca + file:///maincacert.pem + + + dds.sec.auth.builtin.PKI-DH.identity_certificate + file:///mainpartcert.pem + + + dds.sec.auth.builtin.PKI-DH.private_key + file:///mainpartkey.pem + + + + dds.sec.access.plugin + builtin.Access-Permissions + + + + dds.sec.access.builtin.Access-Permissions.permissions_ca + file:///maincacert.pem + + + dds.sec.access.builtin.Access-Permissions.governance + file:///governance_all_enable.smime + + + dds.sec.access.builtin.Access-Permissions.permissions + file:///permissions.smime + + + + dds.sec.crypto.plugin + builtin.AES-GCM-GMAC + + diff --git a/test/configuration/tests_params.json b/test/configuration/tests_params.json index 6eacde4b..868fb231 100644 --- a/test/configuration/tests_params.json +++ b/test/configuration/tests_params.json @@ -1,6 +1,10 @@ { "configurations": { + "properties": + { + "SECURITY": "/security_properties.xml" + }, "configuration_files": { "INTRAPROCESS_OFF": "/intraprocess_off.xml", diff --git a/test/run_test.py b/test/run_test.py index cb76e715..61c4d219 100644 --- a/test/run_test.py +++ b/test/run_test.py @@ -169,6 +169,20 @@ def parse_options(): required=False, help='Use intraprocess transport. Default one test with each.' ) + parser.add_argument( + '-S', + '--security', + type=str, + required=False, + help='Enable security in tests.' + ) + parser.add_argument( + '-C', + '--certs-path', + type=str, + required=False, + help='Path to certs path directory' + ) return parser.parse_args() @@ -305,6 +319,7 @@ def execute_validate_thread_test( ds_tool_path, process_params, config_file, + properties_file, flags_in, fds_path=None, clear=True, @@ -325,6 +340,7 @@ def execute_validate_thread_test( :param ds_tool_path: path to Discovery-Server tool. :param process_params: process parameters. :param config_file: xml file for default configuration of FastDDS. + :param properties_file: path to extra properties file to use with the DS tool. :param flags_in: auxiliary flags to use with the DS tool. :param fds_path: path to fastdds tool. This arg is not needed unless test must execute fastdds tool, in which case it will raise an error @@ -431,9 +447,14 @@ def execute_validate_thread_test( stop_domain = None # Be sure that no stop command is called if not needed # Launch if xml_config_file is not None: - # Create args with config file and outputfile - process_args = \ - [ds_tool_path, '-c', xml_config_file, '-o', result_file] + flags + if properties_file is not None: + # Create args with properties, config and output files + process_args = \ + [ds_tool_path, '-c', xml_config_file, '-p', properties_file, '-o', result_file] + flags + else: + # Create args with config file and outputfile + process_args = \ + [ds_tool_path, '-c', xml_config_file, '-o', result_file] + flags else: # Fastdds tool @@ -499,6 +520,12 @@ def execute_validate_thread_test( # Validation needed, pass to validate_test function logger.debug(f'Executing validation for process {process_name}') validation_params = process_params['validation'] + + # Add security to all the entries in validation_params + if properties_file is not None: + for key in validation_params: + validation_params[key]["security"] = str(args.certs_path + '/mainpartcert.pem') + validator_input = val.ValidatorInput( process_ret, lines, @@ -545,6 +572,7 @@ def execute_validate_test( ds_tool_path, test_params, config_file, + properties_file, flags, fds_path=None, clear=True, @@ -596,6 +624,7 @@ def execute_validate_test( ds_tool_path, process_config, config_file, + properties_file, flags, fds_path, clear, @@ -628,7 +657,7 @@ def execute_validate_test( return result -def get_configurations(config_params, intraprocess, shm): +def get_configurations(config_params, intraprocess, shm, security): """ Extract configurations from json. @@ -642,6 +671,7 @@ def get_configurations(config_params, intraprocess, shm): :param config_params: dictionary with configurations. :param intraprocess: only use intra-process as configuration file. :param shm: only use shared memory as default transport. + :param security: enable security. :return: tuple of two arrays. First array is an array of tuples where first @@ -691,6 +721,24 @@ def get_configurations(config_params, intraprocess, shm): if shm is not None and shm: flags = [f for f in flags if f[0] != 'SHM_OFF'] + if security is not None: + if os.path.isfile(config_params['properties']['SECURITY']): + if os.path.exists(args.certs_path): + properties_file = open(config_params['properties']['SECURITY'], "r+") + data = properties_file.read() + #Replace all occurrences of the required label + data = data.replace('', args.certs_path) + properties_file.seek(0) + properties_file.write(data) + properties_file.truncate() + properties_file.close() + else: + logger.error('Certs path not found at ' + args.certs_path) + exit(1) + else: + logger.error('Properties file not found at ' + config_params['properties']['SECURITY']) + exit(1) + flags_combinatory = [] for i in range(1, 1+len(flags)): for combination in itertools.combinations(flags, i): @@ -716,6 +764,7 @@ def create_tests( tests=None, intraprocess=None, shm=None, + security=None, clear=True, fds_path=None, debug=False, @@ -737,6 +786,7 @@ def create_tests( :param shm: if set it specifies if shared memory is used or not. If None, both with and without shared memory will be executed. (Default: None) + :param security: Whether to load properties file to enable security. :param clear: if true remove generated files if test passes. :param fds_path: path to fastdds tool. This arg is not needed unless test must execute fastdds tool, in which case it will raise an error @@ -767,11 +817,17 @@ def create_tests( config_files, flags_combinatory = get_configurations( config_params, intraprocess, - shm + shm, + security ) test_results = True + if security is not None and security: + properties_file = config_params['properties']['SECURITY'] + else: + properties_file = None + # iterate over parameters for test_name, test in tests: for config_name, config_file in config_files: @@ -804,12 +860,18 @@ def create_tests( f' with config file <{config_file}>' f' and flags {flags}') + if properties_file is not None: + logger.info(f'Using properties of file <{properties_file}>') + for property in config_params['properties']: + test_id += '.' + property + test_results &= execute_validate_test( test_name, test_id, discovery_server_tool_path, params_file[test], config_file, + properties_file, flags, fds_path, clear, @@ -955,6 +1017,10 @@ def load_test_params(tests_params_path): if shm is not None: shm = shared.boolean_from_string(shm) + security = args.security + if security is not None and security: + security = shared.boolean_from_string(security) + result = create_tests( test_params, config_params, @@ -962,6 +1028,7 @@ def load_test_params(tests_params_path): tests=args.test, intraprocess=intraprocess, shm=shm, + security=security, clear=not args.not_remove, fds_path=(args.fds if args.fds else None), debug=args.debug, diff --git a/test/validation/GroundTruthValidator.py b/test/validation/GroundTruthValidator.py index 842fba28..19855c47 100644 --- a/test/validation/GroundTruthValidator.py +++ b/test/validation/GroundTruthValidator.py @@ -26,6 +26,14 @@ import validation.Validator as validator +from cryptography import x509 + +import hashlib + +class SecurityException(Exception): + pass + +spdp_entity_id = "00.00.01.C1" class GroundTruthValidator(validator.Validator): """ @@ -45,6 +53,9 @@ def _validate(self): # Get parameters from test params try: self.guidless = self.validation_params_['guidless'] + self.security_props_file = "" + if 'security' in self.validation_params_: + self.security_props_file = self.validation_params_['security'] self.val_snapshot = \ self.parse_xml_snapshot(self.validation_params_['file_path']) self.gt_snapshot = \ @@ -65,7 +76,7 @@ def _validate(self): self.__trim_snapshot_dict_guidless(self.gt_snapshot, self.gt_dict) self.__trim_snapshot_dict_guidless(self.val_snapshot, self.val_dict) else: - self.__trim_snapshot_dict(self.gt_snapshot, self.gt_dict) + self.__trim_snapshot_dict(self.gt_snapshot, self.gt_dict, True) self.__trim_snapshot_dict(self.val_snapshot, self.val_dict) n_tests = 0 @@ -116,6 +127,78 @@ def save_generated_json_files( if ground_truth_json: self.__write_json_file(self.copy_dict, ground_truth_json_file) + def mangle_guid(self, + guid_prefix : str, + force_no_mangling : bool = False): + """ If security is enabled, mangle the participant GUID based on the certificate subject name. + :param guid_prefix: The participant GUID prefix. + :param entity_id: The participant GUID entity. + :param force_no_mangling: If True, do not mangle the GUID even if security is enabled. + """ + + if self.security_props_file and not force_no_mangling: + + guid = ".".join([guid_prefix, spdp_entity_id]) + + # Load certificate from PEM file + with open(self.security_props_file, "rb") as f: + cert_data = f.read() + cert = x509.load_pem_x509_certificate(cert_data) + + if cert is None: + raise SecurityException("Certificate is None") + + # Parse the input string to bytes + parts = guid.strip().split('.') + if len(parts) != 16: + raise ValueError("GUID string must have 16 byte components separated by '.'") + + try: + # Extract subject name bytes + subject_name_bytes = cert.subject.public_bytes() + except Exception: + raise SecurityException("Cannot get subject name from certificate") + + # Digest with SHA256 + sha256_digest = hashlib.sha256(subject_name_bytes).digest() + if len(sha256_digest) != 32: + raise SecurityException("SHA256 digest length mismatch") + + bytes_in = bytearray(int(b, 16) for b in parts) + guidPrefix = bytes_in[:12] + entityId = bytes_in[12:] + + adjusted_guidPrefix = bytearray(12) + + # Set guidPrefix first 6 bytes with bit manipulation + adjusted_guidPrefix[0] = 0x80 | (sha256_digest[0] >> 1) + adjusted_guidPrefix[1] = ((sha256_digest[0] << 7) & 0xFF) | (sha256_digest[1] >> 1) + adjusted_guidPrefix[2] = ((sha256_digest[1] << 7) & 0xFF) | (sha256_digest[2] >> 1) + adjusted_guidPrefix[3] = ((sha256_digest[2] << 7) & 0xFF) | (sha256_digest[3] >> 1) + adjusted_guidPrefix[4] = ((sha256_digest[3] << 7) & 0xFF) | (sha256_digest[4] >> 1) + adjusted_guidPrefix[5] = ((sha256_digest[4] << 7) & 0xFF) | (sha256_digest[5] >> 1) + + # Build 16-byte key from guid_prefix + key = guidPrefix + entityId + + # Hash the key with SHA256 + sha256_digest_2 = hashlib.sha256(bytes(key)).digest() + + # Update remaining guidPrefix and entityId + adjusted_guidPrefix[6] = sha256_digest_2[0] + adjusted_guidPrefix[7] = sha256_digest_2[1] + adjusted_guidPrefix[8] = sha256_digest_2[2] + adjusted_guidPrefix[9] = sha256_digest_2[3] + adjusted_guidPrefix[10] = sha256_digest_2[4] + adjusted_guidPrefix[11] = sha256_digest_2[5] + + # Return adjusted GUIDPrefix string + # EntityId is not included since it remains the same + return '.'.join(f'{b:02x}' for b in adjusted_guidPrefix) + else: + # If security is not enabled, return the original guid_prefix + return guid_prefix + def process_servers(self): """Generate a list with the servers guid_prefix from the snapshot.""" servers = [] @@ -123,21 +206,22 @@ def process_servers(self): for snapshot in self.__dict2list( self.gt_snapshot['DS_Snapshots']['DS_Snapshot']): for ptdb in self.__dict2list(snapshot['ptdb']): - [servers.append(ptdi['@guid_prefix']) + [servers.append(self.mangle_guid(ptdi['@guid_prefix'], True)) for ptdi in self.__dict2list(ptdb['ptdi']) if (ptdi['@server'] == 'true' and - ptdi['@guid_prefix'] not in servers)] + self.mangle_guid(ptdi['@guid_prefix'], True) not in servers)] except KeyError as e: self.logger.debug(e) return servers - def __trim_snapshot_dict(self, original_dict, trimmed_dict): + def __trim_snapshot_dict(self, original_dict, trimmed_dict, no_guid_mangling=False): """ Create the ground truth and validation dicts parsing the snapshots. :param original_dict: The original dictionary. - :param trimmed_dict: The resulting dictionary after trim. + :param trimmed_dict: The resulting dictionary after trim. + :param no_guid_mangling: If True, do not mangle the GUIDs. """ for ds_snapshot in self.__dict2list( original_dict['DS_Snapshots']['DS_Snapshot']): @@ -155,14 +239,14 @@ def __trim_snapshot_dict(self, original_dict, trimmed_dict): trimmed_dict[ 'DS_Snapshots'][ f"{ds_snapshot['description']}"][ - f"ptdb_{ptdb['@guid_prefix']}"] = { - 'guid_prefix': ptdb['@guid_prefix']} + f"ptdb_{self.mangle_guid(ptdb['@guid_prefix'], no_guid_mangling)}"] = { + 'guid_prefix': self.mangle_guid(ptdb['@guid_prefix'], no_guid_mangling)} try: ptdi_l = self.__dict2list(ptdb['ptdi']) except KeyError: self.logger.debug( - f"Participant {ptdb['@guid_prefix']} does not " + f"Participant {self.mangle_guid(ptdb['@guid_prefix'], no_guid_mangling)} does not " 'match any remote participant.') continue @@ -170,19 +254,19 @@ def __trim_snapshot_dict(self, original_dict, trimmed_dict): trimmed_dict[ 'DS_Snapshots'][ f"{ds_snapshot['description']}"][ - f"ptdb_{ptdb['@guid_prefix']}"][ - f"ptdi_{ptdi['@guid_prefix']}"] = { - 'guid_prefix': ptdi['@guid_prefix']} + f"ptdb_{self.mangle_guid(ptdb['@guid_prefix'], no_guid_mangling)}"][ + f"ptdi_{self.mangle_guid(ptdi['@guid_prefix'], no_guid_mangling)}"] = { + 'guid_prefix': self.mangle_guid(ptdi['@guid_prefix'], no_guid_mangling)} if 'publisher' in (x.lower() for x in ptdi.keys()): for pub in self.__dict2list(ptdi['publisher']): publisher_guid = '{}.{}'.format( - pub['@guid_prefix'], pub['@guid_entity']) + self.mangle_guid(pub['@guid_prefix'], no_guid_mangling), pub['@guid_entity']) trimmed_dict[ 'DS_Snapshots'][ f"{ds_snapshot['description']}"][ - f"ptdb_{ptdb['@guid_prefix']}"][ - f"ptdi_{ptdi['@guid_prefix']}"][ + f"ptdb_{self.mangle_guid(ptdb['@guid_prefix'], no_guid_mangling)}"][ + f"ptdi_{self.mangle_guid(ptdi['@guid_prefix'], no_guid_mangling)}"][ f'publisher_{publisher_guid}'] = { 'topic': pub['@topic'], 'guid': publisher_guid @@ -191,12 +275,12 @@ def __trim_snapshot_dict(self, original_dict, trimmed_dict): if 'subscriber' in (x.lower() for x in ptdi.keys()): for sub in self.__dict2list(ptdi['subscriber']): subscriber_guid = '{}.{}'.format( - sub['@guid_prefix'], sub['@guid_entity']) + self.mangle_guid(sub['@guid_prefix'], no_guid_mangling), sub['@guid_entity']) trimmed_dict[ 'DS_Snapshots'][ f"{ds_snapshot['description']}"][ - f"ptdb_{ptdb['@guid_prefix']}"][ - f"ptdi_{ptdi['@guid_prefix']}"][ + f"ptdb_{self.mangle_guid(ptdb['@guid_prefix'], no_guid_mangling)}"][ + f"ptdi_{self.mangle_guid(ptdi['@guid_prefix'], no_guid_mangling)}"][ f'subscriber_{subscriber_guid}'] = { 'topic': sub['@topic'], 'guid': subscriber_guid