FASTDDS的安全设计

1.背景

无论是ROS机器人系统还是在车载系统领域,DDS的主要应用场景都是在近场传输方面,近场传输的数据安全性需要一定的保证,SOME/IP,CAN总线,ROSTCP,ROSUDP都无法提供内置的安全插件,为了满足安全方面的需求需要额外的安全软件的开发。早期的车载系统中安全防护方面是非常薄弱的,基本属于裸奔状态,随着这些年车载系统的发展,座舱系统对于安全性方面的要求越来越高。早期的这些消息中间件无法适应这些安全性方面的要求。

DDS提供了一系列的安全机制,用于保护数据的传输和存储。它支持数据加密、身份验证、访问控制等安全特性,可以有效地防止数据泄露和未经授权的访问。FASTDDS提供了符合DDS-Security标准的安全插件,我们在使用的时候,只需要做相关的配置就可以了。

2.FASTDDS的安全插件

FASTDDS提供了一些安全插件,这些插件是符合DDS-SECURITY标准的。

一共有五种插件:

授权插件(Auth plug)。主要是身份认证。

访问权插件(Access plug)。在认证完成后,对什么样的数据进行什么样的加密,由这个插件控制。

加密算法插件(Cryptographic plug)。采用什么样的算法对数据较密,由这个插件控制。

日志插件(Logging plug)。对日志采用什么样的方式加密,由这个插件控制。

数据tag插件(Data tagging plug)。对数据进行区分,给dds消息打上tag,那么对于某些有tag的消息,可以采用加密手段,这样对数据区分得更细。

这里主要介绍 授权插件 和 访问权插件。其他插件用的相对较少,也不是主要流程,这里不做介绍了。这里以一个安全代码的示例做解析。

2.1安全代码示例

发送端Publisher的初始化

scss 复制代码
 bool HelloWorldPublisher::init()
 {
     hello_.index(0);
     hello_.message("HelloWorld");
 ​
     //CREATE THE PARTICIPANT
     DomainParticipantQos pqos;
   //使用内置的PKI-DH 插件
     pqos.properties().properties().emplace_back("dds.sec.auth.plugin",
             "builtin.PKI-DH");
   //加载PKI-DH相关的证书
     pqos.properties().properties().emplace_back("dds.sec.auth.builtin.PKI-DH.identity_ca",
             "file://certs/maincacert.pem");
     pqos.properties().properties().emplace_back("dds.sec.auth.builtin.PKI-DH.identity_certificate",
             "file://certs/mainpubcert.pem");
     pqos.properties().properties().emplace_back("dds.sec.auth.builtin.PKI-DH.private_key",
             "file://certs/mainpubkey.pem");
   //使用内置的Access-Permissions插件
     pqos.properties().properties().emplace_back("dds.sec.access.plugin",
             "builtin.Access-Permissions");
   //加载Access-Permissions相关的证书
     pqos.properties().properties().emplace_back("dds.sec.access.builtin.Access-Permissions.permissions_ca",
             "file://certs/maincacert.pem");
     pqos.properties().properties().emplace_back("dds.sec.access.builtin.Access-Permissions.governance",
             "file://certs/governance.smime");
     pqos.properties().properties().emplace_back("dds.sec.access.builtin.Access-Permissions.permissions",
             "file://certs/permissions.smime");
   //使用内置的AES-GCM-GMAC插件,这个是加密算法插件
     pqos.properties().properties().emplace_back("dds.sec.crypto.plugin",
             "builtin.AES-GCM-GMAC");
 ​
     ------
     return true;
 ​
 }

这是HelloWorldPublisher初始化函数,其他部分和第一章的内容基本一样,这里不再解析。

这些代码主要意思是做Security相关的配置。

1.使用内置的PKI-DH 插件,加载PKI-DH相关证书。这是授权插件(Auth plug)。

加载的是3个东西就是我们2.1节介绍的DomainParticipantQos中被配置的3个文件。

2.使用内置的Access-Permissions插件,加载Access-Permissions相关的证书。这是访问权插件(Access plug)。

3.使用内置的AES-GCM-GMAC插件(这个是加密算法插件)。这是加密算法插件(Cryptographic plug)。

接收端Subscriber的初始化

scss 复制代码
 bool HelloWorldSubscriber::init()
 {
     //CREATE THE PARTICIPANT
     DomainParticipantQos pqos;
 ​
     //使用内置的PKI-DH 插件
     pqos.properties().properties().emplace_back("dds.sec.auth.plugin",
             "builtin.PKI-DH");
     //加载PKI-DH相关的证书
     pqos.properties().properties().emplace_back("dds.sec.auth.builtin.PKI-DH.identity_ca",
             "file://certs/maincacert.pem");
     pqos.properties().properties().emplace_back("dds.sec.auth.builtin.PKI-DH.identity_certificate",
             "file://certs/mainsubcert.pem");
     pqos.properties().properties().emplace_back("dds.sec.auth.builtin.PKI-DH.private_key",
             "file://certs/mainsubkey.pem");
     //使用内置的Access-Permissions插件
     pqos.properties().properties().emplace_back("dds.sec.access.plugin",
             "builtin.Access-Permissions");
     //加载Access-Permissions相关的证书
     pqos.properties().properties().emplace_back(
         "dds.sec.access.builtin.Access-Permissions.permissions_ca",
         "file://certs/maincacert.pem");
     pqos.properties().properties().emplace_back(
         "dds.sec.access.builtin.Access-Permissions.governance",
         "file://certs/governance.smime");
     pqos.properties().properties().emplace_back(
         "dds.sec.access.builtin.Access-Permissions.permissions",
         "file://certs/permissions.smime");
     //使用内置的AES-GCM-GMAC插件,这个是加密算法插件
     pqos.properties().properties().emplace_back("dds.sec.crypto.plugin",
             "builtin.AES-GCM-GMAC");
 ​
     ------
 }

这是HelloWorldSubscriber初始化函数,其他部分和第一章的内容基本一样,这里不再解析。

这些代码主要意思是做Security相关的配置。

1.使用内置的PKI-DH 插件,加载PKI-DH相关证书。这是授权插件(Auth plug)。

2.使用内置的Access-Permissions插件,加载Access-Permissions相关的证书。这是访问权插件(Access plug)。

3.使用内置的AES-GCM-GMAC插件。这是加密算法插件(Cryptographic plug)。

在发送和接收两端都需要进行Security的相关配置,动作基本是相同的,加载的证书内容却并不完全相同。

2.2PKI-DH插件

这是身份认证的插件。PKI是Public Key Infrastructure(公共密钥基础设施)。DH是(Diffie-Hellman密钥交换算法)

这个内置的身份验证插件被称为"DDS:Auth:PKI-DH"。DDS:Auth:PKI-DH插件使用受信任的证书(CA证书)实现身份验证。这个插件在Participant之间使用标准的数字签名算法来实现身份的互相验证。它使用数字签名算法来建立身份信任链并签名

身份验证消息。它使用标准密钥建立算法在Participant之间创建了一个共享密钥,以创建一个对等安全通道。CA可能是一个现有的CA。

或者可以创建一个新的用于部署DDS域上的应用程序。选择CA的性质或方式并不重要。因为它的使用方式强制所有参与的应用程序进行

共享识别。想要使用这个插件,DomainParticipant必须配置三个文件:

  1. CA证书,里面包含公钥。
  2. DomainParticipant的私钥。
  3. 由前面CA证书,签名的CA身份证书,里面包含DomainParticipant相关的公钥。

内置的身份认证插件需要在DomainParticipantQos中被配置,主要的配置参数有这几个:

1.identity_ca 就是CA证书,身份证书。

2.identity_certificate 由identity_ca签发的证书,这个证书包含对应的DomainParticipant的公钥。

3.private_key 就是DomainParticipant的私钥。

这个插件主要完成的功能是身份认证,同时使用DH算法在两端协商一个加密密钥,这个密钥用来数据加密。

sequenceDiagram participant ParticipantA participant ParticipantB ParticipantA ->> ParticipantB: 1.ParticipantA向发ParticipantB送HandshakeRequestMessageToken消息 ParticipantB ->> ParticipantA: 2.ParticipantB收到ParticipantA发送的HandshakeRequestMessageToken消息,解析消息,验证身份,向ParticipantA发送HandshakeReplyMessageToken消息 ParticipantA ->> ParticipantB: 3.ParticipantA收到ParticipantB发送的HandshakeReplyMessageToken消息,解析消息,验证身份,向ParticipantB发送HandshakeFinalMessageToken消息

1.ParticipantA向发ParticipantB送HandshakeRequestMessageToken消息 ,这个消息里面包含了一个ParticipantA的证书,C1证书,一个随机生成的chanllege1,一个通过自身私钥生成DH1。

2.ParticipantB收到ParticipantA发送的HandshakeRequestMessageToken消息,解析消息,拿到C1证书,验明身份,向ParticipantA发送HandshakeReplyMessageToken消息,这个消息里面包含了一个ParticipantB的C2证书,包含一个随机生成的chanllege2,一个通过自身私钥生成的DH2,DH1,chanllege1,还有一个依据这些变量的签名sign2。

3.ParticipantA收到ParticipantB发送的HandshakeReplyMessageToken消息,解析消息,拿到c2证书,验证身份,通过sign2和公钥验证数据可靠性,拿到DH2,通过自身私钥和DH2生成secret,这个secret用于对称的数据加密,之后向ParticipantB发送HandshakeFinalMessageToken消息,这个消息包含chanllege1,DH2,DH1,chanllege2,还有一个依据这些变量的签名sign1。这个消息是对HandshakeReplyMessageToken的回应,内容也是类似。

4.ParticipantB收到ParticipantA发送的HandshakeFinalMessageToken消息,验证签名sign1,通过通过自身私钥和DH1生成secret。这个secret和在ParticipantA生成的secret是一样的。

通过这个些消息的传递,ParticipantB和ParticipantA协商了一个公共的secret,这个secret用户对称加密的AES算法。之后的消息加密解密就是使用这个secert。

PKI-DH 插件完成身份认证之后,产生共同的密钥(sharedsecret)。

整个过程就是DH算法的在DDS上的一个实现。

2.3Access:Permissions 插件

这个内置的访问控制插件被称为"DDS:Access:Permissions"插件。DDS:Access:Permissions通过一个由共享CA签署的文档实现访问控制。共享CA可以是现有的CA(包括用于身份验证的相同CA插件),或者可以创建一个新的插件在DDS域来为应用程序分配权限。 每个DomainParticipant都有一个DDS:Access:Permissions插件的关联实例。

这个DDS:Access:Permissions插件是基于PKI-DH插件实现的。需要配置3个文件:

  1. Permissions的CA 证书
  2. 被 Permissions CA 签名的governance文件
  3. 被 Permissions CA 签名的permissions文件

这些需要在需要在DomainParticipantQos中被配置,主要的配置参数有这几个:

1.permissions_ca 就是Permissions的CA 证书。

2.governance文件,就是签名的管理(governance)文件。

3.permissions文件,就是签名的权限(permissions)文件。

Permissions的CA 证书可以是和PKI-DH插件中的CA证书(identity_ca)一样的证书。

2.3.1governance文件

基本的保护类型有3种:

NONE,ENCRYPT,SIGN

NONE就是没有保护措施,SIGN就是比较比较低的保护措施,加一个校验码

ENCRYPT就是加密,这儿一般是AES加密,解密。

保护类型是2种:

SIGN_WITH_ORIGIN_AUTHENTICATION 就是身份认证之后,使用签名加一个校验码,数据不会被加密。

和ENCRYPT_WITH_ORIGIN_AUTHENTICATION就是身份认证之后,给数据加密。

所以总共有5种。

governance文件示例:

xml 复制代码
 <?xml version="1.0" encoding="utf-8"?>
 <dds xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:noNamespaceSchemaLocation="omg_shared_ca_domain_governance.xsd">
     <domain_access_rules>
         <domain_rule>
             <domains>
                 <id_range>
                     <min>0</min>
                     <max>230</max>
                 </id_range>
             </domains>
             <allow_unauthenticated_participants>false</allow_unauthenticated_participants>
             <enable_join_access_control>true</enable_join_access_control>
             <discovery_protection_kind>ENCRYPT</discovery_protection_kind>
             <liveliness_protection_kind>ENCRYPT</liveliness_protection_kind>
             <rtps_protection_kind>ENCRYPT</rtps_protection_kind>
             <topic_access_rules>
                 <topic_rule>
                     <topic_expression>HelloWorldTopic</topic_expression>
                     <enable_discovery_protection>true</enable_discovery_protection>
                     <enable_liveliness_protection>false</enable_liveliness_protection>
                     <enable_read_access_control>true</enable_read_access_control>
                     <enable_write_access_control>true</enable_write_access_control>
                     <metadata_protection_kind>ENCRYPT</metadata_protection_kind>
                     <data_protection_kind>ENCRYPT</data_protection_kind>
                 </topic_rule>
             </topic_access_rules>
         </domain_rule>
     </domain_access_rules>
 </dds>

governance文件中一般包含这几个设置项:

  1. domains 设置项,表示需要在哪个域(Domain)内应用这些加密设置。

    在上图文件件中设置为domain id 是0-230的domain。

  2. allow_unauthenticated_participants设置项,是否允许未经过授权的 participant加入进来。

    在上图文件中设置为false。

  3. enable_join_access_control设置项,is_access_protected这个变量的值跟随这个值。

  4. discovery_protection_kind设置项,可以有5个选项

    NONE,SIGN,ENCRYPT,SIGN_WITH_ORIGIN_AUTHENTICATION,ENCRYPT_WITH_ORIGIN_AUTHENTICATION

    这是是EDP阶段是否加保护,以及采用什么级别的保护措施

  5. liveliness_protection_kind设置项,

    可以有5个选项NONE,SIGN,ENCRYPT,SIGN_WITH_ORIGIN_AUTHENTICATION,ENCRYPT_WITH_ORIGIN_AUTHENTICATION

    对liveliness消息采用什么级别的保护措施

  6. rtps_protection_kind设置项,

    可以有5个选项NONE,SIGN,ENCRYPT,SIGN_WITH_ORIGIN_AUTHENTICATION,ENCRYPT_WITH_ORIGIN_AUTHENTICATION

  7. rtps_psk_protection_kind设置项,

    对PDP消息采取的保护措施,3个选项NONE, SIGN, or ENCRYPT,ENCRYPT的密钥和其他的密钥不一样是提前设置的密钥。

  8. allowed_crypto_algorithms设置项,

    对于两端签名,加密算法等算法的设置。

  9. topic_rule设置项,

    里面有很多小的设置项。

Publisher的governance文件需要和Subscriber的governance文件保持一致。

2.3.2permissions文件

xml 复制代码
 <permissions>
         <grant name="PublisherPermissions">
             <subject_name>[email protected], CN=Main Publisher, OU=eProsima, O=eProsima, ST=MA, C=ES</subject_name>
             <validity>
                 <not_before>2013-06-01T13:00:00</not_before>
                 <not_after>2038-06-01T13:00:00</not_after>
             </validity>
             <allow_rule>
                 <domains>
                     <id_range>
                         <min>0</min>
                         <max>230</max>
                     </id_range>
                 </domains>
                 <publish>
                     <topics>
                         <topic>HelloWorldTopic</topic>
                     </topics>
                 </publish>
             </allow_rule>
             <default>DENY</default>
         </grant>
         <grant name="SubscriberPermissions">
             <subject_name> [email protected], CN=Main Subscriber, OU=eProsima, O=eProsima, ST=MA, C=ES</subject_name>
             <validity>
                 <not_before>2013-06-01T13:00:00</not_before>
                 <not_after>2038-06-01T13:00:00</not_after>
             </validity>
             <allow_rule>
                 <domains>
                     <id_range>
                         <min>0</min>
                         <max>230</max>
                     </id_range>
                 </domains>
                 <subscribe>
                     <topics>
                         <topic>HelloWorldTopic</topic>
                     </topics>
                 </subscribe>
             </allow_rule>
             <default>DENY</default>
         </grant>
     </permissions>

这个部分分为基本对称的两个部分。

grant 的名字是SubscriberPermissions 和PublisherPermissions

grant内部又分为3个部分

1.subject_name部分

这个名字和CA证书是一一对应的

PublisherPermission的subject_name 是和mainpubcert.pem里面的名字是一致的。

PublisherPermissions的subject_name 是和mainsubcert.pem里面的名字是一致的。

2.validity部分

表示这个规则生效的时间是什么时间段

3.规则部分(allow, deny 和 default 选项)

这个文件中就是allow 和 default。

allow_rule 这里设置了domain和topic名字,就是说在这个domain范围中的topic使用这些规则。

deault这个设置的是DENY,就是默认的情况下不使用这些规则。

3.源码解析

3.1创建安全相关的PDP端点

由于增加了安全插件,PDP的节点数有了一定的增加。

c_EntityId_spdp_reliable_participant_secure_reader和c_EntityId_spdp_reliable_participant_secure_writer

kotlin 复制代码
 RTPSParticipantImpl的构造函数中
 if (!m_security_manager.init(security_attributes_, m_att.properties))
    {
         // Participant will be deleted, no need to allocate buffers or create builtin endpoints
         return;
    }

完成初始化的工作。

在PDPSimple

arduino 复制代码
 bool PDPSimple::createPDPEndpoints()
 {
     ------
 #if HAVE_SECURITY
     fastdds::rtps::SimplePDPEndpointsSecure* secure_endpoints = nullptr;
     bool is_secure = mp_RTPSParticipant->is_secure();
     if (is_secure)
    {
         secure_endpoints = new fastdds::rtps::SimplePDPEndpointsSecure();
         secure_endpoints->secure_reader.listener_.reset(new PDPListener(this));
 ​
         endpoints = secure_endpoints;
         endpoints->reader.listener_.reset(new PDPSecurityInitiatorListener(this,
                [this](const ParticipantProxyData& participant_data)
                {
                     match_pdp_remote_endpoints(participant_data, false, true);
                }));
    }
     else
 #endif  // HAVE_SECURITY
       
     ------
       
 #if HAVE_SECURITY
     if (ret_val && is_secure)
    {
         create_dcps_participant_secure_endpoints();
    }
 #endif  // HAVE_SECURITY
   ------
 }

主要是设置了一些回调,同时创建了PDPSecure的Writer和Reader

ini 复制代码
 bool PDPSimple::create_dcps_participant_secure_endpoints()
 {
     const RTPSParticipantAttributes& pattr = mp_RTPSParticipant->get_attributes();
     const RTPSParticipantAllocationAttributes& allocation = pattr.allocation;
     const BuiltinAttributes& builtin_att = mp_builtin->m_att;
     auto endpoints = dynamic_cast<fastdds::rtps::SimplePDPEndpointsSecure*>(builtin_endpoints_.get());
     assert(nullptr != endpoints);
 ​
     constexpr const char* topic_name = "DCPSParticipantsSecure";
     const EntityId_t reader_entity_id = c_EntityId_spdp_reliable_participant_secure_reader;
     const EntityId_t writer_entity_id = c_EntityId_spdp_reliable_participant_secure_writer;
 ​
     // BUILTIN DCPSParticipantsSecure READER
     auto& reader = endpoints->secure_reader;
     HistoryAttributes hatt;
     hatt = pdp_reader_history_attributes(builtin_att, allocation);
 ​
     PoolConfig reader_pool_cfg = PoolConfig::from_history_attributes(hatt);
     reader.payload_pool_ = TopicPayloadPoolRegistry::get(topic_name, reader_pool_cfg);
     reader.payload_pool_->reserve_history(reader_pool_cfg, true);
     reader.history_.reset(new ReaderHistory(hatt));
 ​
     ReaderAttributes ratt = create_builtin_reader_attributes();
     WriterAttributes watt = create_builtin_writer_attributes();
     add_builtin_security_attributes(ratt, watt);
 ​
     RTPSReader* rtps_reader = nullptr;
     if (mp_RTPSParticipant->createReader(&rtps_reader, ratt, reader.payload_pool_, reader.history_.get(),
             reader.listener_.get(), reader_entity_id, true, false))
    {
         reader.reader_ = dynamic_cast<StatefulReader*>(rtps_reader);
         assert(nullptr != reader.reader_);
    }
     else
    {
         EPROSIMA_LOG_ERROR(RTPS_PDP, "'" << topic_name << "' builtin reader creation failed");
         reader.release();
         return false;
    }
 ​
     // SPDP BUILTIN RTPSParticipant WRITER
     auto& writer = endpoints->secure_writer;
     hatt = pdp_writer_history_attributes(builtin_att);
 ​
     PoolConfig writer_pool_cfg = PoolConfig::from_history_attributes(hatt);
     writer.payload_pool_ = TopicPayloadPoolRegistry::get(topic_name, writer_pool_cfg);
     writer.payload_pool_->reserve_history(writer_pool_cfg, false);
     writer.history_.reset(new WriterHistory(hatt, writer.payload_pool_));
 ​
     RTPSWriter* rtps_writer = nullptr;
     if (mp_RTPSParticipant->createWriter(&rtps_writer, watt, writer.history_.get(),
             nullptr, writer_entity_id, true))
    {
         writer.writer_ = dynamic_cast<StatefulWriter*>(rtps_writer);
         assert(nullptr != writer.writer_);
    }
     else
    {
         EPROSIMA_LOG_ERROR(RTPS_PDP, "'" << topic_name << "' builtin writer creation failed");
         writer.release();
         return false;
    }
     return true;
 }
 ​

主要是创建了PDPSecure的Writer和Reader,创建的详细内容可以参考之前的内容。有两点需要注意,这里创建的Writer和Reader都是reliable和stateful的,就是说一般情况下,消息都是可靠的。而之前创建的普通的PDP Writer和Reader都是stateless的。

这个PDP节点和之前的PDP节点的作用是一样的。还是用于Participant的发现,只是这个发现的消息,是加了密的。

3.2创建SecurityManager的节点(Entity)

scss 复制代码
 bool SecurityManager::create_entities()
 {
     if (create_participant_stateless_message_entities())
    {
         if (crypto_plugin_ == nullptr || create_participant_volatile_message_secure_entities())
        {
             EPROSIMA_LOG_INFO(SECURITY, "Initialized security manager for participant " << participant_->getGuid());
             return true;
        }
 ​
         delete_participant_stateless_message_entities();
    }
 ​
     cancel_init();
     return false;
 }

分别调用create_participant_stateless_message_entities和create_participant_volatile_message_secure_entities

scss 复制代码
 bool SecurityManager::create_participant_stateless_message_entities()
 {
     create_participant_stateless_message_pool();
     if (create_participant_stateless_message_writer())
    {
         if (create_participant_stateless_message_reader())
        {
             return true;
        }
         delete_participant_stateless_message_writer();
    }
 ​
     delete_participant_stateless_message_pool();
     return false;
 }

创建了ENTITYID_P2P_BUILTIN_PARTICIPANT_STATELESS_WRITER 和 ENTITYID_P2P_BUILTIN_PARTICIPANT_STATELESS_WRITER 节点(Entity)

scss 复制代码
 bool SecurityManager::create_participant_volatile_message_secure_entities()
 {
     create_participant_volatile_message_secure_pool();
 ​
     if (create_participant_volatile_message_secure_writer())
    {
         if (create_participant_volatile_message_secure_reader())
        {
             return true;
        }
 ​
         delete_participant_volatile_message_secure_writer();
    }
 ​
     delete_participant_volatile_message_secure_pool();
     return false;
 }

创建了ENTITYID_P2P_BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_WRITER 和ENTITYID_P2P_BUILTIN_PARTICIPANT_VOLATILE_MESSAGE_SECURE_READER的pdp 端点。

这儿有2类的SecurityManager的节点。用处各有不同。

ENTITYID_P2P_BUILTIN_PARTICIPANT_STATELESS_WRITER 和ENTITYID_P2P_BUILTIN_PARTICIPANT_STATELESS_WRITER 节点,

这类节点是为了避免一类攻击(Sequence Number 攻击)。就是针对fastdds协议的攻击。就是伪装成DataWriter发送heartbeat消息,在hearbeat中,将消息的seq number范围提高,这样DataReader 低于seq number的消息将无法收到。

3.3与安全相关的其他Entity

在EDP中也有相应的安全节点,如果discovery_protection_kind设置为需要加密,则需要使用安全节点。

sedp_builtin_publications_secure_writer

sedp_builtin_publications_secure_reader

sedp_builtin_subscriptions_secure_writer

sedp_builtin_subscriptions_secure_reader

WLP也有对应的加密的节点

c_EntityId_WriterLivelinessSecure

c_EntityId_ReaderLivelinessSecure

如果governance的liveliness_protection_kind选项设置为加密选项,则使用这两个加密节点发送和接收liveliness消息。

rust 复制代码
 bool EDPSimple::initEDP(
         BuiltinAttributes& attributes)
 {
     ------
     //create_sedp_secure_endpoints创建了安全节点
     if (mp_RTPSParticipant->is_secure() && !create_sedp_secure_endpoints())
    {
         EPROSIMA_LOG_ERROR(RTPS_EDP, "Problem creation SimpleEDP endpoints");
         return false;
    }
 -----
 ​
     return true;
 }

EDP的安全节点在EDPSimple的initEDP中创建。

rust 复制代码
 bool WLP::initWL(
         RTPSParticipantImpl* p)
 {
    
 ------
     if (retVal && p->is_secure())
    {
         //createSecureEndpoints创建了WLP的安全节点
         retVal = createSecureEndpoints();
    }
 ------
     return retVal;
 }
 ​

WLP的安全节点在WLP的initWL中创建。

3.4Auth认证相关的内容

这部分消息的发送接收,是由BuiltinParticipantStatelessMessageWriter和BuiltinParticipantStatelessMessageReader完成的。

在收到pdp消息之后处理消息的时候会调用到这个process_alive_data函数

scss 复制代码
 void PDPSecurityInitiatorListener::process_alive_data(
         ParticipantProxyData* old_data,
         ParticipantProxyData& new_data,
         GUID_t& writer_guid,
         RTPSReader* reader,
         std::unique_lock<std::recursive_mutex>& lock)
 {
     if (reader->matched_writer_is_matched(writer_guid))
    {
         // Act as the standard PDPListener when the writer is matched.
         // This will be the case for unauthenticated participants when
         // allowed_unathenticated_participants is true
         PDPListener::process_alive_data(old_data, new_data, writer_guid, reader, lock);
         return;
    }
 ​
     if (old_data == nullptr)
    {
         auto callback_data = new_data;
         reader->getMutex().unlock();
         lock.unlock();
 ​
         //! notify security manager in order to start handshake
         bool ret = parent_pdp_->getRTPSParticipant()->security_manager().discovered_participant(callback_data);
         //! Reply to the remote participant
         if (ret)
        {
             response_cb_(callback_data);
        }
 ​
         // Take again the reader lock
         reader->getMutex().lock();
    }
 ​
 }

调用到SecurityManager的discovered_participant函数

ini 复制代码
 bool SecurityManager::discovered_participant(
         const ParticipantProxyData& participant_data)
 {
     ------
     // 省略部分判断一下1.SecurityManager有没有初始化完毕2.两个participant的安全设置是否一致 3.安全插件有没有加载完毕
 ​
     SecurityException exception;
     AuthenticationStatus auth_status = AUTHENTICATION_INIT;
 ​
     // Create or find information
     bool undiscovered = false;
     DiscoveredParticipantInfo::AuthUniquePtr remote_participant_info;
     // Use the information from the collection
     const ParticipantProxyData* remote_participant_data = nullptr;
    {
         std::lock_guard<shared_mutex> _(mutex_);
 ​
         auto map_ret = discovered_participants_.insert(
             std::make_pair(
                 participant_data.guid,
                 std::unique_ptr<DiscoveredParticipantInfo>(
                     new DiscoveredParticipantInfo(
                         auth_status,
                         participant_data))));
 ​
         undiscovered = map_ret.second;
         remote_participant_info = map_ret.first->second->get_auth();
         remote_participant_data = &map_ret.first->second->participant_data();
    }
 ​
     bool notify_part_authorized = false;
     if (undiscovered && remote_participant_info && remote_participant_data != nullptr)
    {
         // Configure the timed event but do not start it
         const GUID_t guid = remote_participant_data->guid;
         //这个设置了一个定时器,如果没有收到reply消息,将会重发消息
         remote_participant_info->event_.reset(new TimedEvent(participant_->getEventResource(),
                [&, guid]() -> bool
                {
                     resend_handshake_message_token(guid);
                     return true;
                },
                 static_cast<double>(auth_handshake_props_.initial_handshake_resend_period_ms_)));
 ​
         IdentityHandle* remote_identity_handle = nullptr;
 ​
         // Validate remote participant.
         ValidationResult_t validation_ret = authentication_plugin_->validate_remote_identity(&remote_identity_handle,
                         *local_identity_handle_,
                         remote_participant_data->identity_token_,
                         remote_participant_data->guid, exception);
 ​
         switch (validation_ret)
        {
             case VALIDATION_OK:
                 assert(remote_identity_handle != nullptr);
                 auth_status = AUTHENTICATION_OK;
                 break;
             case VALIDATION_PENDING_HANDSHAKE_REQUEST:
                 assert(remote_identity_handle != nullptr);
             //将会发送request消息的状态
                 auth_status = AUTHENTICATION_REQUEST_NOT_SEND;
                 break;
             case VALIDATION_PENDING_HANDSHAKE_MESSAGE:
                 assert(remote_identity_handle != nullptr);
             //等待对端发送request消息的状态
                 auth_status = AUTHENTICATION_WAITING_REQUEST;
                 break;
             case VALIDATION_PENDING_RETRY:
             // TODO(Ricardo) Send event.
             default:
 ​
                 on_validation_failed(*remote_participant_data, exception);
 ​
                 std::lock_guard<shared_mutex> _(mutex_);
 ​
                 // Remove created element, because authentication failed.
                 discovered_participants_.erase(remote_participant_data->guid);
 ​
                 //TODO(Ricardo) cryptograhy registration in AUTHENTICAITON_OK
                 return false;
        }
 ​
         EPROSIMA_LOG_INFO(SECURITY, "Discovered participant " << remote_participant_data->guid);
 ​
         // Match entities
         match_builtin_endpoints(*remote_participant_data);
 ​
         // Store new remote handle.
         remote_participant_info->auth_status_ = auth_status;
         remote_participant_info->identity_handle_ = remote_identity_handle;
 ​
         // TODO(Ricardo) Start cryptography if authentication ok in this point.
         // If authentication is successful, inform user about it.
         if (auth_status == AUTHENTICATION_OK)
        {
             //TODO(Ricardo) Shared secret on this case?
             std::shared_ptr<SecretHandle> ss;
             notify_part_authorized = participant_authorized(*remote_participant_data, remote_participant_info, ss);
        }
    }
     else
    {
         // If cannot retrieve the authentication info pointer then return, because
         // it is used in other thread.
         if (!remote_participant_info)
        {
             return false;
        }
 ​
         if (remote_participant_info->auth_status_ == AUTHENTICATION_FAILED)
        {
             remote_participant_info->auth_status_ = AUTHENTICATION_REQUEST_NOT_SEND;
        }
    }
 ​
     bool returnedValue = true;
 ​
     if (remote_participant_info->auth_status_ == AUTHENTICATION_REQUEST_NOT_SEND)
    {
         // Maybe send request.
         // 发送消息
         returnedValue = on_process_handshake(*remote_participant_data, remote_participant_info,
                         MessageIdentity(), HandshakeMessageToken(), notify_part_authorized);
    }
 ​
     restore_discovered_participant_info(remote_participant_data->guid, remote_participant_info);
 ​
     if (notify_part_authorized)
    {
         notify_participant_authorized(*remote_participant_data);
    }
 ​
     return returnedValue;
 }

SecurityManager对于安全的信息,存在了一个队列中

std::map<GUID_t, std::unique_ptr> discovered_participants_,里面存放了远端的participant的信息,包括participant的一些证书,participant所在的状态,比如participant 发送认证的消息之后所处的状态(具体见下面的图)

这个函数主要干了这几件事:

1.将收到的远端participant 信息存储到本地的一个map中。

2.如果这个participant是首次发现,进入认证(AUTH)的状态机。发送或者等待接收消息。

大概的流程见下图:

复制代码
ParticipantAParticipantB1.ParticipantA向ParticipantB发送GMCLASSID_SECURITY_AUTH_HANDSHAKE消息2.ParticipantB收到ParticipantA发送的消息,解析消息,验证身份,向ParticipantA发送reply消息3.ParticipantA收到ParticipantB发送的Reply消息,解析消息,验证身份ParticipantAParticipantB

1.ParticipantA的BuiltinParticipantStatelessMessageWriter向ParticipantB的BuiltinParticipantStatelessMessageReader发送GMCLASSID_SECURITY_AUTH_HANDSHAKE消息。 2.ParticipantB的BuiltinParticipantStatelessMessageReader收到ParticipantA发送的GMCLASSID_SECURITY_AUTH_HANDSHAKE消息,解析消息,验证身份,向ParticipantA发送reply消息。 3.ParticipantA收到ParticipantB发送的Reply消息,解析消息,验证身份。

由于发送消息的是StatelessMessageWriter,接收的消息都是StatelessMessageReader。在第一个步骤中,ParticipantA会启动一个定时器,如果在指定的时间内没有收到ParticipantB的消息,会重新发送一条GMCLASSID_SECURITY_AUTH_HANDSHAKE消息。

如果在步骤3中,验证身份没有通过,则需要重新发送GMCLASSID_SECURITY_AUTH_HANDSHAKE消息。

整个过程是2.2节的一部份,这里面因篇幅所限,只对整个流程做一个简单介绍,至于DH算法部分的内容,这块不详细介绍了。

3.5 密钥交换

密钥交换,主要是在BuiltinParticipantVolatileMessageSecureWriter 和BuiltinParticipantVolatileMessageSecureReader之间进行。

主要发送和接收的是VolatileMessage,由于密钥消息的特殊性,所以这两个Writer和Reader是Stateful的,他们消息传输时可靠的,同时消息是VOLATILE,就是只发给当时在线的participant。

主要包括:

participantcryptotokens

writercryptotokens

readercryptotokens

这3类。

participantcryptotokens主要是为了加密解密,整个rtps消息。

writercryptotokens主要是为了加密解密writer发送的子消息(submessage)。

readercryptotokens主要是为了加密解密reader发送的子消息(比如ack消息)。

这里以participantcryptotokens举例,看一下如何交换participantcryptotokens的。

SecurityManager::discovered_participant 如果验证通过会调用SecurityManager::participant_authorized

scss 复制代码
 bool SecurityManager::participant_authorized(
         const ParticipantProxyData& participant_data,
         const DiscoveredParticipantInfo::AuthUniquePtr& remote_participant_info,
         std::shared_ptr<SecretHandle>& shared_secret_handle)
 {
     ------
      //省略掉部分permission相关代码 
     -------
 ​
     if (access_plugin_ == nullptr || remote_permissions != nullptr)
    {
 ​
        ------
 ​
         if (crypto_plugin_ != nullptr)
        {
             -------
 ​
             // Starts cryptography mechanism
             // 存储远端的认证相关的信息,包括shared_secret
             std::shared_ptr<ParticipantCryptoHandle> participant_crypto_handle =
                     register_and_match_crypto_endpoint(*remote_participant_info->identity_handle_,
                             *shared_secret_handle);
 ​
             // Store cryptography info
             if (participant_crypto_handle && !participant_crypto_handle->nil())
            {
                 std::lock_guard<shared_mutex> _(mutex_);
 ​
                 // Check there is a pending crypto message.
               // 查一下队列中有没有之前收到的信息
                 auto pending = remote_participant_pending_messages_.find(participant_data.guid);
 ​
                 if (pending != remote_participant_pending_messages_.end())
                {//如果原来有,存储原来的信息
                     if (!crypto_plugin_->cryptokeyexchange()->set_remote_participant_crypto_tokens(
                                 *local_participant_crypto_handle_,
                                 *participant_crypto_handle,
                                 pending->second,
                                 exception))
                    {
                         EPROSIMA_LOG_ERROR(SECURITY, "Cannot set remote participant crypto tokens ("
                                 << participant_data.guid << ") - (" << exception.what() << ")");
                    }
 ​
                     remote_participant_pending_messages_.erase(pending);
                }
 ​
                 ------
            }
             ------
 //匹配远端的BuiltinParticipantVolatileMessageSecureWriter 和BuiltinParticipantVolatileMessageSecureReader
             match_builtin_key_exchange_endpoints(participant_data);
           //交换密钥
             exchange_participant_crypto(participant_crypto_handle, participant_data.guid);
        }
         else
        {
            {
                 shared_lock<shared_mutex> _(mutex_);
 ​
                 // Store shared_secret.
                 auto dp_it = discovered_participants_.find(participant_data.guid);
 ​
                 if (dp_it != discovered_participants_.end())
                {
                     dp_it->second->set_shared_secret(shared_secret_handle);
                     dp_it->second->set_permissions_handle(remote_permissions);
                }
            }
 ​
             match_builtin_key_exchange_endpoints(participant_data);
        }
 ​
         ------
 ​
         return true;
    }
 ​
     return false;
 }

这个函数主要是

1.调用了register_and_match_crypto_endpoint存储了远端participant的一些认证信息(SharedSecret等)。

2.调用了exchange_participant_crypto函数,这个函数主要是生成一个local_participant_crypto_tokens,并将这个local_participant_crypto_tokens打包成消息,发送给远端的participant

scss 复制代码
 void SecurityManager::exchange_participant_crypto(
         std::shared_ptr<ParticipantCryptoHandle> remote_participant_crypto,
         const GUID_t& remote_participant_guid)
 {
     SecurityException exception;
 ​
     // Get participant crypto tokens.
     ParticipantCryptoTokenSeq local_participant_crypto_tokens;
     if (crypto_plugin_->cryptokeyexchange()->create_local_participant_crypto_tokens(local_participant_crypto_tokens,
             *local_participant_crypto_handle_, *remote_participant_crypto, exception))
    {
         ParticipantGenericMessage message = generate_participant_crypto_token_message(remote_participant_guid,
                         local_participant_crypto_tokens);
 ​
         CacheChange_t* change = create_change_for_message(
             message,
             participant_volatile_message_secure_writer_history_);
 ​
         if (change != nullptr)
        {
             // Serialize message
           // 初始化aux_msg
             CDRMessage_t aux_msg(0);
             ------
 ​
             if (CDRMessage::addParticipantGenericMessage(&aux_msg, message))
            {
                 change->serializedPayload.length = aux_msg.length;
 ​
                 // 放入消息队列中,发送出去
                 if (!participant_volatile_message_secure_writer_history_->add_change(change))
                {
                     participant_volatile_message_secure_writer_history_->release_change(change);
                     EPROSIMA_LOG_ERROR(SECURITY, "WriterHistory cannot add the CacheChange_t");
                }
            }
             else
            {
                 participant_volatile_message_secure_writer_history_->release_change(change);
                 EPROSIMA_LOG_ERROR(SECURITY, "Cannot serialize ParticipantGenericMessage");
            }
        }
         ------
    }
     ------
 }
 ​

这个函数主要就是将

1.调用create_local_participant_crypto_tokens生成local_participant_crypto_tokens

2.将local_participant_crypto_tokens打包成volatilemessage传给远端的participant

scss 复制代码
 void SecurityManager::process_participant_volatile_message_secure(
         const CacheChange_t* const change)
 {
     ------
 ​
     // Deserialize message
     // 省略部分是对message的解析
     ------
 ​
     CDRMessage::readParticipantGenericMessage(&aux_msg, message);
 ​
     if (message.message_class_id().compare(GMCLASSID_SECURITY_PARTICIPANT_CRYPTO_TOKENS) == 0)
    {
         ------
 ​
         const GUID_t remote_participant_key(message.message_identity().source_guid().guidPrefix,
                 c_EntityId_RTPSParticipant);
         std::shared_ptr<ParticipantCryptoHandle> remote_participant_crypto;
         DiscoveredParticipantInfo::AuthUniquePtr remote_participant_info;
 ​
         // Search remote participant crypto handle.
        {
             shared_lock<shared_mutex> lock(mutex_);
 ​
             auto dp_it = discovered_participants_.find(remote_participant_key);
 ​
             if (dp_it != discovered_participants_.end())
            {
                 if (dp_it->second->get_participant_crypto() == nullptr)
                {
                     return;
                }
 ​
                 remote_participant_crypto = dp_it->second->get_participant_crypto();
                 remote_participant_info = dp_it->second->get_auth();
            }
             ------
        }
 ​
         if (remote_participant_crypto != nullptr)
        {
             SecurityException exception;
 ​
             if (!crypto_plugin_->cryptokeyexchange()->set_remote_participant_crypto_tokens(
                         *local_participant_crypto_handle_,
                         *remote_participant_crypto,
                         message.message_data(),
                         exception))
            {
                 EPROSIMA_LOG_ERROR(SECURITY, "Cannot set remote participant crypto tokens ("
                         << remote_participant_key << ") - (" << exception.what() << ")");
            }
             else
            {
                 // Release the change from the participant_stateless_message_writer_pool_
                 // As both participants have already authorized each other
 ​
                 if (remote_participant_info &&
                         remote_participant_info->change_sequence_number_ != SequenceNumber_t::unknown())
                {
                     participant_stateless_message_writer_history_->remove_change(
                         remote_participant_info->change_sequence_number_);
                     remote_participant_info->change_sequence_number_ = SequenceNumber_t::unknown();
                }
            }
        }
         else
        {
             std::lock_guard<shared_mutex> _(mutex_);
             remote_participant_pending_messages_.emplace(remote_participant_key, std::move(message.message_data()));
        }
 ​
         if (remote_participant_info)
        {
             restore_discovered_participant_info(remote_participant_key, remote_participant_info);
        }
    }
     else if (message.message_class_id().compare(GMCLASSID_SECURITY_READER_CRYPTO_TOKENS) == 0)
    {
        //这部分是对writercryptotokens的处理
        ------
    }
     else if (message.message_class_id().compare(GMCLASSID_SECURITY_WRITER_CRYPTO_TOKENS) == 0)
    {
        //这部分是对readercryptotokens的处理
        ------
    }
     ------
 }
 ​

这个函数就是收到volatile_message,处理这些消息,在statefulreader的change_received 函数中会调用listener的on_new_change_added函数,最终调用到process_participant_volatile_message_secure函数。

这些消息又分为三类:

participantcryptotokens相关的消息

writercryptotokens相关的消息

readercryptotokens相关的消息

这些消息分别处理

最终将会调用set_remote_participant_crypto_tokens,将远端的participant_crypto存储起来。

sequenceDiagram participant SecurityManagerA participant ParticipantA participant ParticipantB ParticipantB ->> ParticipantA: 1.ParticipantA收到ParticipantB发送PDP消息,ParticipantA发现ParticipantB ParticipantA ->> SecurityManagerA: 2.auth插件认证通过(可以参看3.3auth认证部分) ParticipantA ->> SecurityManagerA: 3.ParticipantA调用register_matched_remote_participant存储相关的ParticipantB的信息(SharedSecret等) ParticipantA ->> SecurityManagerA: 4.ParticipantA调用create_local_participant_crypto_tokens,获取participantcryptotokens ParticipantA ->> ParticipantB: 5.ParticipantA将participantcryptotokens 发送给ParticipantB ParticipantB ->> ParticipantB: 6.ParticipantB将participant_crypto_tokens保存到本地。

1.ParticipantA收到ParticipantB发送PDP消息,ParticipantA发现了ParticipantB

2.Auth插件认证通过(可以参看3.3auth认证部分)通过SecurityManager的安全认证其实比较复杂,有多个步骤,可以看一下3.3部分的内容。

3.ParticipantA调用register_matched_remote_participant存储相关的ParticipantB的信息(SharedSecret等)

4.ParticipantA调用exchange_participant_crypto函数,这个函数调用create_local_participant_crypto_tokens,获取participantcryptotokens。将local_participant_crypto_tokens打包成volatilemessage,发送到ParticipantB(也就是第5步)。

5.ParticipantA将participantcryptotokens 发送给ParticipantB

6.ParticipantB调用process_participant_volatile_message_secure,处理这个消息,调用set_remote_participant_crypto_tokens将这个participant_crypto_tokens保存到本地。

3.6消息的加密解密

RTPSMessageGroup的send函数,调用SecurityManager的encode_rtps_message函数来对rtps消息进行加密。

c 复制代码
 bool SecurityManager::encode_rtps_message(
         const CDRMessage_t& input_message,
         CDRMessage_t& output_message,
         const std::vector<GuidPrefix_t>& receiving_list) const
 {
     ------
 ​
     std::vector<std::shared_ptr<ParticipantCryptoHandle>> receiving_crypto_list;
     for (const auto& remote_participant : receiving_list)
    {
         const GUID_t remote_participant_key(remote_participant, c_EntityId_RTPSParticipant);
 ​
         if (remote_participant_key == participant_->getGuid())
        {
             receiving_crypto_list.push_back(local_participant_crypto_handle_);
        }
         else
        {
             auto dp_it = discovered_participants_.find(remote_participant_key);
 ​
             if (dp_it != discovered_participants_.cend() && dp_it->second->get_participant_crypto() != nullptr)
            {
                 receiving_crypto_list.push_back(dp_it->second->get_participant_crypto());
            }
             ------
        }
    }
 ​
     SecurityException exception;
     return crypto_plugin_->cryptotransform()->encode_rtps_message(output_message,
                    input_message, *local_participant_crypto_handle_, receiving_crypto_list,
                    exception);
 }

receiving_crypto_list中存储的就是各个participant对应的participant_crypto,然后调用插件,对消息,根据participant的不同加密。

ini 复制代码
 int SecurityManager::decode_rtps_message(
         const CDRMessage_t& message,
         CDRMessage_t& out_message,
         const GuidPrefix_t& remote_participant) const
 {
     ------
     // Init output buffer
     CDRMessage::initCDRMsg(&out_message);
 ​
     std::shared_ptr<const ParticipantCryptoHandle> remote_participant_crypto_handle;
 ​
     const GUID_t remote_participant_key(remote_participant, c_EntityId_RTPSParticipant);
 ​
     if (remote_participant_key == participant_->getGuid())
    {
         remote_participant_crypto_handle = local_participant_crypto_handle_;
    }
     else
    {
         shared_lock<shared_mutex> _(mutex_);
 ​
         auto dp_it = discovered_participants_.find(remote_participant_key);
 ​
         if (dp_it != discovered_participants_.end())
        {
             remote_participant_crypto_handle = dp_it->second->get_participant_crypto();
        }
    }
 ​
     int returnedValue = -1;
 ​
     if (remote_participant_crypto_handle != nullptr)
    {
         SecurityException exception;
         bool ret = crypto_plugin_->cryptotransform()->decode_rtps_message(out_message,
                         message,
                         *local_participant_crypto_handle_,
                         *remote_participant_crypto_handle,
                         exception);
 ​
         ------
    }
     else
    {
         ------
    }
 ​
     return returnedValue;
 }

找到远端participant的remote_participant_crypto_handle,和本地participant的local_participant_crypto_handle_一起将message解密出来。

这里仅仅是将使用participant_crypto的加密解密rtps消息的情况进行了解读,使用writercryptotokens对writer发送的submessage加密解密的情况和使用readercryptotokens对reader发送的submessage进行加密解密的情况这块不再详细解读了,读者可以根据源码对这块进行详细解析。

相关推荐
禺垣2 小时前
区块链技术概述
大数据·人工智能·分布式·物联网·去中心化·区块链
憧憬一下2 小时前
FreeRTOS同步和互斥
嵌入式·freertos
zhbi984 小时前
RoboDK 自定义机器人
机器人
Tipriest_4 小时前
Pinocchio 库详解及其在足式机器人上的应用
机器人·动力学·足式机器人·运动学·pinocchio
暗影八度4 小时前
Spark流水线+Gravitino+Marquez数据血缘采集
大数据·分布式·spark
q567315235 小时前
IBM官网新闻爬虫代码示例
开发语言·分布式·爬虫
不爱学英文的码字机器6 小时前
数据网格的革命:从集中式到分布式的数据管理新范式
分布式
优秀的颜9 小时前
计算机基础知识(第五篇)
java·开发语言·分布式
曹勖之15 小时前
基于ROS2,撰写python脚本,根据给定的舵-桨动力学模型实现动力学更新
开发语言·python·机器人·ros2