乐于分享
好东西不私藏

Fast DDS Security 源码解析:PKI-DH 双向认证与密钥协商实现

Fast DDS Security 源码解析:PKI-DH 双向认证与密钥协商实现

文章目录

  • 0 前言
  • 2 DDS security标准
    • 2.1  DDS Security Plugins
    • 2.2 DDS:Auth:PKI-DH
  • 3 fastdds代码分析
    • 3.1认证过程
      • 3.1.1 本地身份初始化(加载自己的证书)
      • 3.1.2 A→B:发送证书+DH公钥
      • 3.1.3 B→A:验证A证书 + 回复证书+签名
        • 3.1.3.1 阶段一:校验
        • 3.1.3.2 阶段二:构造 Reply 消息
      • 3.1.4 A处理Reply:验证B证书 + 发送Final
        • 3.1.4.1 阶段一 验证 Replier 身份
        • 3.1.4.2 阶段二 验证 Reply 的完整性和签名
        • 3.1.4.3 阶段三 构造 HandshakeFinal 并签名
        • 3.1.4.4 阶段四 生成 SharedSecret
      • 3.1.5 B处理Final:验证A签名 + 完成认证
        • 3.1.5.1 阶段一:验证 Final 消息字段
        • 3.1.5.2 阶段二:验证 Initiator 的签名
        • 3.1.5.3 阶段三:生成 SharedSecret
    • 3.2 DH协商密钥
      • 3.2.1 密钥协商算法选择
      • 3.2.2 生成临时 DH 密钥对
      • 3.2.3 序列化公钥(用于网络传输)
      • 3.2.4 从字节流重构对方公钥
      • 3.2.5 DH 密钥派生 + SHA-256
  • 4.参考文献

0 前言

遇到以下场景:A同学:最近业务中使用到了DDS协议,如何做通信安全呢B:同学:这个简单,TLS可以做通信安全A同学:我使用的是fast DDS ,业务定义是UDP端口,不是TCP端口B同学:我查阅了以下fast DDS,官网只介绍了TCP→TLS的配置方法,确实没有UDP→DTLS的配置方法A同学:是的呀!如果没有现场的配置,让我们再去研究协议栈,改造协议栈成本太高了。

DDS官网自己定义了一套安全机制。分别是Authentication Service Plugin、AccessControl Service Plugin、Cryptographic Service Plugin.、Logging Service Plugin、Data Tagging Service Plugin.。 本文将针对Authentication Plugin PKI-DH 双向认证与密钥协商实现进行介绍、讨论。

2 DDS security标准

2.1  DDS Security Plugins

DDS Security 规范(OMG DDS Security)采用插件(Plugin)架构设计,将安全功能解耦为多个独立模块。每个插件负责不同的安全能力,开发者可以根据实际需求进行配置或替换。 整体可以分为 身份认证、访问控制、数据加密、日志审计和数据标签 五大类。

插件
标准名称
主要作用
是否必选
Authentication Plugin
DDS:Auth:PKI-DH
身份认证、密钥协商
✔ 必须
Access Control Plugin
DDS:Access:Permissions
权限控制
✔ 必须
Cryptographic Plugin
DDS:Crypto:AES-GCM-GMAC
数据加密与完整性保护
✔ 必须
Logging Plugin
DDS:Logging:DDS_LogTopic
安全日志记录
可选
Data Tagging Plugin
DDS:Tagging:DDS_Discovery
数据安全标签
可选

五个插件并不是独立工作的,而是形成了一条完整的安全链路:

2.2 DDS:Auth:PKI-DH

整个 DDS:Auth:PKI-DH 的认证过程实际上包含两个阶段:

  • 身份认证阶段(Authentication)
     所有参与者都信任同一个 CA。 每个 DomainParticipant 持有自己的私钥和由该 CA 签发的 X.509 证书。 双方通过 **ECDSA(或其他数字签名算法)**验证对方证书并对认证消息进行签名,实现双向身份认证,建立身份信任链。
  • 密钥协商阶段(Key Establishment)
     在身份认证成功后,双方利用 ECDH(或其他密钥协商算法计算得到相同的 Shared Secret(共享密钥)。 该共享密钥随后用于派生会话密钥(Session Key),建立安全的点对点通信信道,并供后续的 AES-GCM 等加密算法保护 RTPS 消息的机密性和完整性。

3 fastdds代码分析

敲黑板:目前fast-dds没有实现 DDS:Auth:PSK,仅仅实现了DDS:Auth:PKI-DH,有就是说必须依赖CA证书的方案进行认证和协商密钥

3.1认证过程

3.1.1 本地身份初始化(加载自己的证书)

  • PKI-DH 认证流程的 Phase 0(本地初始化)——在握手开始前,加载并验证本端的 PKI 材料(CA、证书、私钥),构建 IdentityHandle,生成 IdentityToken。双方各自独立执行,无网络交互。
ValidationResult_t PKIDH::validate_local_identity(        IdentityHandle** local_identity_handle,        GUID_t& adjusted_participant_key,        const uint32_t /*domain_id*/,        const PropertyPolicy& part_props,        const GUID_t& candidate_participant_key,        SecurityException& exception)

3.1.2 A→B:发送证书+DH公钥

敲黑板:此时Request 不携带签名

  • Initiator(主动方)构造并发送 HandshakeRequest 消息。这是三次握手的第一条消息,携带 Initiator 的身份信息、DH 公钥和随机数。
ValidationResult_t PKIDH::begin_handshake_request(        HandshakeHandle** handshake_handle,        HandshakeMessageToken** handshake_message,        const IdentityHandle& initiator_identity_handle,        IdentityHandle& replier_identity_handle,        const CDRMessage_t& cdr_participant_data,        SecurityException& exception)
  • 构造的 Request 消息内容。函数按顺序向 HandshakeMessageToken 中添加以下字段。
HandshakeRequest (DDS:Auth:PKI-DH:1.0+Req)├── c.id          ← Initiator 证书(DER 编码字节)├── c.perm        ← Initiator 权限凭证(Permissions 文件内容)├── c.pdata       ← ParticipantProxyData(含 GUID├── c.dsign_algo  ← 签名算法("RSA-SHA256" 或 "ECDSA-SHA256"├── c.kagree_algo ← 密钥协商算法("DH+MODP-2048-256" 或 "ECDH+prime256v1-CEUM"├── hash_c1       ← 上述 5 个 "c." 字段的 SHA-256 摘要├── dh1           ← Initiator 的临时 DH 公钥(序列化字节)└── challenge1    ← 256 位随机数(防重放)

3.1.3 B→A:验证A证书 + 回复证书+签名

敲黑板:在这个阶段首次出现了签名

  • Replier(应答方)收到 Request 后,验证 Initiator 身份,生成自身的 DH 密钥对,构造并发送 HandshakeReply 消息(含数字签名)。这是握手协议中信息量最大、验证逻辑最重的一步。

3.1.3.1 阶段一:校验

阶段一:验证 Request 消息逐项校验 Request 中的每个字段 ① 验证消息类型 class_id == “DDS:Auth:PKI-DH:1.0+Req” ② 加载并验证 Initiator 证书 ├── load_certificate(c.id) ├── 校验 SubjectName └── verify_certificate() — CA 链 + CRL 验证 ③ 提取权限凭证 c.perm ④ 验证 c.pdata 中的 GUID 与证书绑定 └── SubjectName 的 SHA-256 前 47 位 == GUID 前缀的特定位 ⑤ 验证签名算法 c.dsign_algo(RSA-SHA256 / ECDSA-SHA256) ⑥ 验证密钥协商算法 c.kagree_algo(DH-2048-256 / ECDH-prime256v1) ⑦ 重算 hash_c1 并与 Request 中的 hash_c1 比对 ⑧ 提取 dh1 和 challenge1(供后续 Reply 消息回传)

3.1.3.2 阶段二:构造 Reply 消息

HandshakeReply (DDS:Auth:PKI-DH:1.0+Reply)│── 身份字段(Replier 自身)├── c.id          ← Replier 证书├── c.perm        ← Replier 权限凭证├── c.pdata       ← Replier ParticipantProxyData├── c.dsign_algo  ← Replier 签名算法├── c.kagree_algo ← Replier 密钥协商算法├── hash_c2       ← 上述 5 个 "c." 字段的 SHA-256│── 密钥交换字段├── dh2           ← Replier 的临时 DH 公钥 ★│── 回传字段(来自 Request├── hash_c1       ← Request 的身份摘要├── dh1           ← Initiator 的 DH 公钥├── challenge1    ← Initiator 的随机数│── Replier 新增├── challenge2    ← Replier 的随机数│── 签名(核心)└── signature     ← Sign(Replier私钥,                         hash_c2 + challenge2 + dh2 +                         challenge1 + dh1 + hash_c1)
ValidationResult_t PKIDH::begin_handshake_reply(        HandshakeHandle** handshake_handle,        HandshakeMessageToken** handshake_message_out,        HandshakeMessageToken&& handshake_message_in,        IdentityHandle& initiator_identity_handle,        const IdentityHandle& replier_identity_handle,        const CDRMessage_t& cdr_participant_data,        SecurityException& exception)

3.1.4 A处理Reply:验证B证书 + 发送Final

  • Initiator(主动方)收到 Reply 后,验证 Replier 的身份和签名,构造 HandshakeFinal 消息并签名,最后生成 SharedSecret。这是 Initiator 侧的最终处理步骤,也是握手三次消息中逻辑最复杂的一个函数
ValidationResult_t PKIDH::process_handshake_request(        HandshakeMessageToken** handshake_message_out,        HandshakeMessageToken&& handshake_message_in,        PKIHandshakeHandle& handshake_handle,        SecurityException& exception)

3.1.4.1 阶段一 验证 Replier 身份

① 验证消息类型 class_id == “DDS:Auth:PKI-DH:1.0+Reply” ② 加载 Replier 证书(c.id) ├── load_certificate() ├── 校验 SubjectName └── verify_certificate() — CA 链 + CRL 验证 ③ 提取权限凭证 c.perm ④ 验证 c.pdata 中的 GUID 与证书绑定 ⑤ 验证签名算法 c.dsign_algo ⑥ 验证密钥协商算法 c.kagree_algo(必须与本端一致)

3.1.4.2 阶段二 验证 Reply 的完整性和签名

⑦ 验证 hash_c2 — Replier 身份字段摘要 └── 重新序列化所有 “c.” 字段 → SHA-256 → 比对 hash_c2

⑧ 提取 dh2 → generate_dh_peer_key() 存为 peer key └── handshake_handle->peerkeys_ = generate_dh_peer_key(dh2, …)

⑨ 回传字段逐一比对(防篡改) ├── hash_c1: Reply中的 == Request原始发出的 ├── dh1:     Reply中的 == Request原始发出的 └── challenge1: Reply中的 == Request原始发出的

⑩ 验证签名(核心安全检查) └── 用 Replier 证书公钥验证 Reply 的 signature

3.1.4.3 阶段三 构造 HandshakeFinal 并签名

HandshakeFinal (DDS:Auth:PKI-DH:1.0+Final) │ ├── hash_c1       ← 回传 ├── hash_c2       ← 回传 ├── dh1           ← 回传 ├── dh2           ← 回传 ├── challenge1    ← 回传 ├── challenge2    ← 回传 └── signature     ← Sign(Initiator私钥, hash_c1 + challenge1 + dh1 + challenge2 + dh2 + hash_c2)

3.1.4.4 阶段四 生成 SharedSecret

handshake_handle->sharedsecret_ = generate_sharedsecret(    handshake_handle->dhkeys_,   // 本端完整密钥对(pub_A + priv_A)    handshake_handle->peerkeys_, // 对端公钥(pub_B)    exception);// 将 challenge1 和 challenge2 存入 SharedSecret(*handle)->data_.emplace_back("Challenge1", challenge1);(*handle)->data_.emplace_back("Challenge2", challenge2);

3.1.5 B处理Final:验证A签名 + 完成认证

  • Replier(应答方)收到 Final 消息后,验证 Initiator 的签名,确认全部握手参数一致性,最后生成 SharedSecret。这是 Replier 侧的收官函数。
ValidationResult_t PKIDH::process_handshake_reply(        HandshakeMessageToken** /*handshake_message_out*/,        HandshakeMessageToken&& handshake_message_in,        PKIHandshakeHandle& handshake_handle,        SecurityException& exception)

3.1.5.1 阶段一:验证 Final 消息字段

① 验证消息类型 class_id == “DDS:Auth:PKI-DH:1.0+Final”

② challenge1 比对 └── Final.challenge1 == Reply 中发出的 challenge1

③ challenge2 比对 └── Final.challenge2 == Reply 中发出的 challenge2

④ hash_c1 比对(可选字段) └── 若存在,Final.hash_c1 == Reply 中的 hash_c1

⑤ hash_c2 比对(可选字段) └── 若存在,Final.hash_c2 == Reply 中的 hash_c2

⑥ dh1 比对(可选字段) └── 若存在,Final.dh1 == Reply 中的 dh1

⑦ dh2 比对(可选字段) └── 若存在,Final.dh2 == Reply 中的 dh2

3.1.5.2 阶段二:验证 Initiator 的签名

// 重组 6 个字段(Initiator 侧的签名顺序)CDRMessage::addUInt32(&cdrmessage, 6);CDRMessage::addBinaryProperty(&cdrmessage, *hash_c1);CDRMessage::addBinaryProperty(&cdrmessage, *challenge1);CDRMessage::addBinaryProperty(&cdrmessage, *dh1);CDRMessage::addBinaryProperty(&cdrmessage, *challenge2);CDRMessage::addBinaryProperty(&cdrmessage, *dh2);CDRMessage::addBinaryProperty(&cdrmessage, *hash_c2);// 用 Initiator 证书公钥验证签名check_sign_sha256(rih->cert_, cdrmessage.buffer, cdrmessage.length, *signature, exception)

3.1.5.3 阶段三:生成 SharedSecret

handshake_handle->sharedsecret_ = generate_sharedsecret(    handshake_handle->dhkeys_,   // 本端密钥对(pub_B + priv_B)    handshake_handle->peerkeys_, // 对端公钥(pub_A,Reply 阶段已存储)    exception);// 存入 challenge(*handle)->data_.emplace_back("Challenge1", challenge1->value());(*handle)->data_.emplace_back("Challenge2", challenge2->value());

3.2 DH协商密钥

3.2.1 密钥协商算法选择

  • 密钥协商算法选择 
    • DH+MODP-2048-256
    • ECDH+prime256v1-CEUM
staticintget_dh_type(        const std::string& algorithm){    auto raw_alg = algorithm.c_str();    if (strcmp(DH_2048_256, raw_alg) == 0)    {        return EVP_PKEY_DH;    }    else if (strcmp(ECDH_prime256v1, raw_alg) == 0)    {        return EVP_PKEY_EC;    }    return 0;}
static const charconst DH_2048_256 = "DH+MODP-2048-256";static const charconst ECDH_prime256v1 = "ECDH+prime256v1-CEUM";

3.2.2 生成临时 DH 密钥对

  • 生成密钥对
  • 依赖openssl库
static EVP_PKEY* generate_dh_key(int type, SecurityException& exception){    // ── 分支1: ECDH (椭圆曲线) ──    if (type == EVP_PKEY_EC) {        pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);        EVP_PKEY_CTX_set_ec_paramgen_curve_nid(pctx, NID_X9_62_prime256v1);  // P-256 曲线        EVP_PKEY_paramgen(pctx, &params);    }    // ── 分支2: DH (有限域) ──    else if (type == EVP_PKEY_DH) {        params = EVP_PKEY_new();        DH* dh = DH_get_2048_256();          // RFC 5114 2048-bit 素数 + 256-bit 子群        EVP_PKEY_assign(params, dh_type, dh);    }    // ── 生成密钥对 ──    EVP_PKEY* keys = nullptr;    EVP_PKEY_CTX* kctx = EVP_PKEY_CTX_new(params, NULL);    EVP_PKEY_keygen_init(kctx);    EVP_PKEY_keygen(kctx, &keys);           // ← 核心:生成 (私钥, 公钥) 对    return keys;}

3.2.3 序列化公钥(用于网络传输)

  • 生成的临时密钥对中的公钥提取出来,序列化为字节流,以便通过网络发送给对端。这是握手流程中"公钥交换"步骤的实现。
staticboolstore_dh_public_key(        EVP_PKEY* dhkey,        int type,        std::vector<uint8_t>& buffer,        SecurityException& exception)

3.2.4 从字节流重构对方公钥

  • 从对端发来的字节流中反序列化重建对方的 DH/ECDH 公钥,构造出一个仅含公钥的 EVP_PKEY 对象,供后续 generate_sharedsecret() 进行密钥派生使用。
static EVP_PKEY* generate_dh_peer_key(        const std::vector<uint8_t>& buffer,        SecurityException& exception,        int alg_kind)
// DH: 字节数组 → BIGNUM → EVP_PKEYBN_deserialize_raw(&pub_key, buffer, buffer.size(), exception);DH_set0_key(dh, pub_key, NULL);EVP_PKEY_assign(key, type, dh);// ECDH: 字节数组 → EC_KEY → EVP_PKEYEC_KEY_oct2key(ec, pointer, buffer.size(), NULL);EVP_PKEY_assign_EC_KEY(key, ec);

3.2.5 DH 密钥派生 + SHA-256

  • 用本端私钥和对端公钥执行 DH 密钥派生,再经 SHA-256 哈希生成固定 32 字节的 SharedSecret,供后续 AES-GCM-GMAC 加密插件使用。
std::shared_ptr<SecretHandle> PKIDH::generate_sharedsecret(        EVP_PKEY* private_key,    // 自己的 DH 私钥        EVP_PKEY* public_key,     // 对方的 DH 公钥        SecurityException& exception) const{    EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new(private_key, NULL);    EVP_PKEY_derive_init(ctx);                    // 初始化密钥派生    EVP_PKEY_derive_set_peer(ctx, public_key);    // 设置对方公钥    EVP_PKEY_derive(ctx, NULL, &length);          // 获取派生密钥长度    EVP_PKEY_derive(ctx, raw_data, &length);      // ← 核心:计算 DH 共享密钥                                                  //   (数学: g^(a*b) mod p)    EVP_Digest(raw_data, length, md,              // ← SHA-256 哈希               NULLEVP_sha256(), NULL);         //   得到 32 字节 SharedSecret    data.value().assign(md, md + 32);    // 存入 SharedSecretHandle}

4.参考文献

  1. fast dds https://fast-dds.docs.eprosima.com/en/latest/02-formalia/titlepage.html

  2. DDS-SECURITY https://www.omg.org/spec/DDS-SECURITY/1.2/About-DDS-SECURITY

  3. fast dds git https://github.com/eProsima/Fast-DDS/tree/master