ZeroTier源码解析 (3) 身份 (Identity)

在上一章 节点 (Node) 中,我们将 Node 比作是您在 ZeroTier 世界里的"数字分身"或"个人助理"。我们知道,每个 Node 都是一个独立的实体,代表着您的一台设备。

那么,这里就出现了一个根本性的问题:在一个由成千上万个节点组成的全球网络中,我们如何确信正在通信的对方就是它所声称的那个人,而不是一个冒名顶替者?当你的笔记本电脑(节点A)想要和公司服务器(节点B)通信时,服务器如何知道它确实是在和你的笔记本电脑说话,而不是一个试图窃取数据的黑客?

这就是 身份 (Identity) 概念要解决的核心问题。它是 ZeroTier 安全体系的基石。

什么是身份 (Identity)?

身份 (Identity) 是每个节点 (Node)都拥有的一个独一无二的加密凭证。你可以把它想象成一本数字护照

这本"护照"包含几个关键部分,让它变得无法伪造且易于验证:

  • 独一无二的护照号 : 每个身份都有一个全球唯一的 ZeroTier 地址 (一个10个字符的十六进制字符串,如 a1b2c3d4e5)。这个地址就像你的护照号码,是你在 ZeroTier 网络中的唯一标识。
  • 生物特征信息 (防伪) : 这本护照的核心是一种叫做公钥/私钥对 的加密技术。
    • 私钥:是你绝对保密的个人信息,就像你的指纹。只有你自己拥有。
    • 公钥:是你可以公开给他人的信息,就像你的照片和姓名。 任何人都可以通过你的公钥来验证信息是否真的来自于你,但只有你才能用私钥来"签名"信息。
  • 官方印章 : 当你的节点发送消息时,它会用自己的私钥 对消息进行"盖章"(这个过程叫数字签名 )。其他任何节点收到消息后,都可以用你公开的公钥来检验这个"印章"的真伪。如果验证通过,就说明这条消息确实是你发的,并且在传输过程中没有被篡改。

没有一个有效的身份,节点 (Node) 就像一个没有护照的旅行者,无法进入任何虚拟网络 (Network),也无法与其他节点进行安全的通信。

Identity 的核心组成部分

Identity 的核心是密码学。让我们看看它的三个关键部分是如何关联的:

graph TD A["私钥 (Private Key)
秘密保管"] -->|数学计算| B["公钥 (Public Key)
可以公开"] B -->|"通过复杂的"工作量证明"哈希计算"| C["ZeroTier 地址
例如 a1b2c3d4e5"]
  1. 私钥 (Private Key): 一个随机生成的、高度保密的数据串。它是所有安全的源头。
  2. 公钥 (Public Key): 通过一个单向的数学函数从私钥计算得出。这意味着从私钥可以轻松得到公钥,但反过来,从公钥几乎不可能推算出私钥。
  3. ZeroTier 地址 (Address) : 它是通过对公钥进行一个非常复杂的、计算成本高昂的哈希算法得出的。这个过程被设计成一种"工作量证明",需要消耗一定的计算时间(通常是几秒到几分钟)。这大大增加了恶意制造大量虚假身份的难度。

这种设计确保了:

  • 唯一性: 你的地址和你的公钥/私钥对是牢牢绑定的。
  • 防伪造: 别人无法在不知道你私钥的情况下伪造你的签名。
  • 防冲突: 由于生成地址的计算非常复杂,其他人几乎不可能"碰巧"生成一个和你公钥不同但地址相同的身份。

代码中的 Identity

Identity 类封装了所有与身份相关的数据和操作。你可以在 node/Identity.hpp 文件中找到它的定义。

cpp 复制代码
// 文件: node/Identity.hpp

class Identity
{
private:
    Address _address;               // 40位的 ZeroTier 地址
    C25519::Public _publicKey;      // 公钥
    C25519::Private *_privateKey;   // 指向私钥的指针 (保密)

public:
    // 生成一个全新的身份
    void generate();

    // 验证此身份是否有效 (地址是否真的由公钥生成)
    bool locallyValidate() const;

    // 用私钥对数据进行签名
    C25519::Signature sign(const void *data, unsigned int len) const;

    // 用公钥验证签名
    bool verify(const void *data, unsigned int len, const C25519::Signature &signature) const;

    // ... 其他辅助函数
};
  • _address: 存储那个独一无二的10位十六进制 ZeroTier 地址。
  • _publicKey_privateKey: 存储了密码学的核心------公钥和私钥。私钥被存储为指针,以强调其敏感性。
  • generate(): 这是一个计算密集型函数,用于创建全新的身份。
  • sign()verify(): 这两个函数是身份在日常通信中的主要应用,用于"盖章"和"验章"。

Identity 的诞生

当你在设备上第一次运行 ZeroTier 时,它需要创建一个节点 (Node)。而 Node 的第一要务就是拥有一个 Identity

让我们看看 Node 的构造函数(位于 node/Node.cpp)是如何处理这件事的:

cpp 复制代码
// 文件: node/Node.cpp (Node 构造函数中的简化逻辑)

// 1. 尝试从本地存储加载身份信息 (identity.secret)
int n = stateObjectGet(tptr, ZT_STATE_OBJECT_IDENTITY_SECRET, ...);

// 2. 如果加载失败 (n <= 0),说明是第一次运行
if (n <= 0) {
    // 调用 generate() 创建一个全新的身份
    RR->identity.generate(); 
    
    // 将新生成的身份保存到本地,以便下次使用
    RR->identity.toString(true, RR->secretIdentityStr); // 转换为字符串
    stateObjectPut(tptr, ZT_STATE_OBJECT_IDENTITY_SECRET, ..., RR->secretIdentityStr, ...);
} else {
    // 如果加载成功,则从字符串中解析出现有的身份
    RR->identity.fromString(loadedSecretString);
}

这个过程非常直观:

  1. 程序首先检查本地是否已经存有名为 identity.secret 的文件。
  2. 如果,就读取文件内容,恢复现有的身份。这样可以确保你的设备每次启动时都使用同一个"数字护照"。
  3. 如果没有 ,程序就会调用 identity.generate() 来进行那个"昂贵"的计算,创建一个全新的身份,并将其保存起来供未来使用。

Identity 的工作流程:数字签名

现在,让我们通过一个例子来看看 Identity 是如何在实践中确保通信安全的。

假设节点 A (10.0.0.1) 要给节点 B (10.0.0.2) 发送一条消息 "Hello"。

sequenceDiagram participant Node_A as 节点 A (发送方) participant Node_B as 节点 B (接收方) Node_A->>Node_A: 消息: "Hello" Node_A->>Node_A: 用 A 的私钥对消息进行 sign() 操作,生成"签名" Node_A->>Node_B: 将 "Hello" + "签名" 一起发送 Node_B->>Node_B: 收到消息和签名 Node_B->>Node_B: 获取已知的 A 的公钥 Node_B->>Node_B: 调用 verify(消息, 签名, A的公钥) 进行验证 alt 验证成功 Node_B->>Node_B: 确认消息来自 A 且未被篡改 else 验证失败 Node_B->>Node_B: 丢弃消息,因为它不可信 end
  1. 签名 : 节点 A 准备好要发送的数据 "Hello"。它调用自己 Identity 对象的 sign() 方法,并传入 "Hello" 作为参数。sign() 方法内部会使用节点 A 的私钥生成一个独特的数字签名。
  2. 发送 : 节点 A 将原始数据 "Hello" 和刚刚生成的签名打包在一起,通过网络发送给节点 B。
  3. 验证 : 节点 B 收到数据包后,将其拆分出原始数据 "Hello" 和签名。它会查找自己地址簿里记录的节点 A 的公钥
  4. 节点 B 调用 verify() 方法,传入三个参数:收到的原始数据 "Hello"、收到的签名、以及它所知道的节点 A 的公钥。
  5. verify() 函数会进行一次数学计算。如果计算结果表明签名确实是由与该公钥配对的私钥生成的,函数就返回 true。否则,返回 false

通过这个过程,节点 B 可以百分之百地确定,它收到的 "Hello" 消息确实是节点 A 发送的,并且在传输途中没有被任何人修改过。

深入幕后:一个"昂贵"的诞生过程

我们之前提到,Identity::generate() 是一个计算成本高昂的操作。为什么不简单地生成一个随机的密钥对和地址呢?

这是为了防止"地址冲突攻击"。如果生成身份非常容易,攻击者就可以不断地生成海量的新身份,直到碰巧有一个身份的 ZeroTier 地址与某个合法用户的地址相同。虽然公钥不同,但这可能会在某些情况下引起混乱或安全问题。

为了解决这个问题,ZeroTier 使用了一种类似比特币挖矿的**"工作量证明"(Proof-of-Work)**机制。

Identity.cpp 文件中,核心逻辑在一个名为 _computeMemoryHardHash 的函数里。这个函数做的事情可以简化理解为:

  1. 拿出一个公钥。
  2. 对这个公钥进行一系列非常复杂的、需要大量内存和 CPU 运算的哈希计算。
  3. 检查计算出的哈希结果是否满足一个特定的"幸运条件"(例如,哈希值的前几个字节必须小于某个数)。
  4. 如果不满足,就对公钥稍作修改,然后重复第 2 步,直到找到一个满足条件的"幸运"哈希值。

这个"幸运"的哈希值的一部分,最终就成为了你的 ZeroTier 地址。

cpp 复制代码
// 文件: Identity.cpp (generate() 的简化概念)

void Identity::generate()
{
    // ... 初始化 ...
    char *genmem = new char[ZT_IDENTITY_GEN_MEMORY]; // 分配大量内存
    
    C25519::Pair key_pair;
    do {
        // 不断生成新的密钥对,直到满足下面的条件
        key_pair = C25519::generateSatisfying([&](const C25519::Pair &kp) {
            // 进行复杂的哈希计算
            _computeMemoryHardHash(kp.pub.data, ..., digest, genmem);
            // 检查哈希结果是否 "足够幸运"
            return (digest[0] < ZT_IDENTITY_GEN_HASHCASH_FIRST_BYTE_LESS_THAN);
        });
        
        // 从 "幸运" 的哈希结果中提取地址
        _address.setTo(digest + 59, ZT_ADDRESS_LENGTH);
        
    } while (_address.isReserved()); // 确保地址不是保留地址

    // ... 保存最终的密钥对和地址 ...
}

这个过程就像是买彩票。你必须不断地尝试,直到中奖为止。因为"中奖"需要付出时间和计算成本,所以每个生成的身份都来之不易,这有效地阻止了身份的滥用和伪造。

而验证一个身份是否合法(通过 locallyValidate() 函数)则非常快,就像核对中奖彩票号码一样简单。这体现了密码学中一个重要的非对称特性:创造很难,验证很容易

总结

在本章中,我们揭开了 ZeroTier 安全模型的基石------身份 (Identity)

  • Identity 就像每个节点 (Node) 的数字护照 ,它由一个公钥/私钥对 和一个根据公钥生成的唯一 ZeroTier 地址组成。
  • 它的核心功能是数字签名 :使用私钥"盖章"消息,使用公钥"验章",从而保证了通信的真实性完整性
  • 生成一个新身份是一个计算成本高昂的过程,这是一种安全设计,可以有效防止身份的滥用和伪造。

现在我们知道了,网络中的每个成员都有一个可靠的身份。那么,当两个拥有合法身份的节点决定开始通信时,它们之间会建立起怎样的关系呢?它们是如何找到彼此,并维持通信的呢?

在下一章中,我们将探讨两个节点之间建立的直接连接关系------对等节点 (Peer)。

相关推荐
网硕互联的小客服2 分钟前
IIS7.5下的https无法绑定主机头,显示灰色如何处理?
网络协议·http·https
R-G-B1 小时前
【04】OpenCV C++实战篇——实战:发票精准定位,提取指定单元格数据。(倾角计算、旋转矫正、产品定位、目标定位、OCR文字提取)
c++·opencv·ocr·发票精准定位·提取指定单元格数据·倾角计算·旋转矫正
稚肩2 小时前
如何在linux中使用Makefile构建一个C++工程?
linux·运维·c++
啊森要自信3 小时前
【QT】常⽤控件详解(七)容器类控件 GroupBox && TabWidget && 布局管理器 && Spacer
linux·开发语言·c++·qt·adb
源代码•宸3 小时前
C++高频知识点(二十)
开发语言·c++·经验分享·epoll·拆包封包·名称修饰
重启的码农3 小时前
ZeroTier 源码解析 (2) 节点 (Node)
c++·网络协议
遇见你的雩风4 小时前
C++结构体的赋形之记
c++
DemonAvenger4 小时前
边缘计算场景下Go网络编程:优势、实践与踩坑经验
网络协议·架构·go
郝学胜-神的一滴4 小时前
Horse3D引擎研发笔记(一):从使用Qt的OpenGL库绘制三角形开始
c++·qt·3d·unity·图形渲染·unreal engine