在上一章 节点 (Node) 中,我们将 Node
比作是您在 ZeroTier 世界里的"数字分身"或"个人助理"。我们知道,每个 Node
都是一个独立的实体,代表着您的一台设备。
那么,这里就出现了一个根本性的问题:在一个由成千上万个节点组成的全球网络中,我们如何确信正在通信的对方就是它所声称的那个人,而不是一个冒名顶替者?当你的笔记本电脑(节点A)想要和公司服务器(节点B)通信时,服务器如何知道它确实是在和你的笔记本电脑说话,而不是一个试图窃取数据的黑客?
这就是 身份 (Identity)
概念要解决的核心问题。它是 ZeroTier 安全体系的基石。
什么是身份 (Identity)?
身份 (Identity)
是每个节点 (Node)都拥有的一个独一无二的加密凭证。你可以把它想象成一本数字护照。
这本"护照"包含几个关键部分,让它变得无法伪造且易于验证:
- 独一无二的护照号 : 每个身份都有一个全球唯一的 ZeroTier 地址 (一个10个字符的十六进制字符串,如
a1b2c3d4e5
)。这个地址就像你的护照号码,是你在 ZeroTier 网络中的唯一标识。 - 生物特征信息 (防伪) : 这本护照的核心是一种叫做公钥/私钥对 的加密技术。
- 私钥:是你绝对保密的个人信息,就像你的指纹。只有你自己拥有。
- 公钥:是你可以公开给他人的信息,就像你的照片和姓名。 任何人都可以通过你的公钥来验证信息是否真的来自于你,但只有你才能用私钥来"签名"信息。
- 官方印章 : 当你的节点发送消息时,它会用自己的私钥 对消息进行"盖章"(这个过程叫数字签名 )。其他任何节点收到消息后,都可以用你公开的公钥来检验这个"印章"的真伪。如果验证通过,就说明这条消息确实是你发的,并且在传输过程中没有被篡改。
没有一个有效的身份,节点 (Node) 就像一个没有护照的旅行者,无法进入任何虚拟网络 (Network),也无法与其他节点进行安全的通信。
Identity
的核心组成部分
Identity
的核心是密码学。让我们看看它的三个关键部分是如何关联的:
秘密保管"] -->|数学计算| B["公钥 (Public Key)
可以公开"] B -->|"通过复杂的"工作量证明"哈希计算"| C["ZeroTier 地址
例如 a1b2c3d4e5"]
- 私钥 (Private Key): 一个随机生成的、高度保密的数据串。它是所有安全的源头。
- 公钥 (Public Key): 通过一个单向的数学函数从私钥计算得出。这意味着从私钥可以轻松得到公钥,但反过来,从公钥几乎不可能推算出私钥。
- 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);
}
这个过程非常直观:
- 程序首先检查本地是否已经存有名为
identity.secret
的文件。 - 如果有,就读取文件内容,恢复现有的身份。这样可以确保你的设备每次启动时都使用同一个"数字护照"。
- 如果没有 ,程序就会调用
identity.generate()
来进行那个"昂贵"的计算,创建一个全新的身份,并将其保存起来供未来使用。
Identity
的工作流程:数字签名
现在,让我们通过一个例子来看看 Identity
是如何在实践中确保通信安全的。
假设节点 A (10.0.0.1
) 要给节点 B (10.0.0.2
) 发送一条消息 "Hello"。
- 签名 : 节点 A 准备好要发送的数据 "Hello"。它调用自己
Identity
对象的sign()
方法,并传入 "Hello" 作为参数。sign()
方法内部会使用节点 A 的私钥生成一个独特的数字签名。 - 发送 : 节点 A 将原始数据 "Hello" 和刚刚生成的签名打包在一起,通过网络发送给节点 B。
- 验证 : 节点 B 收到数据包后,将其拆分出原始数据 "Hello" 和签名。它会查找自己地址簿里记录的节点 A 的公钥。
- 节点 B 调用
verify()
方法,传入三个参数:收到的原始数据 "Hello"、收到的签名、以及它所知道的节点 A 的公钥。 verify()
函数会进行一次数学计算。如果计算结果表明签名确实是由与该公钥配对的私钥生成的,函数就返回true
。否则,返回false
。
通过这个过程,节点 B 可以百分之百地确定,它收到的 "Hello" 消息确实是节点 A 发送的,并且在传输途中没有被任何人修改过。
深入幕后:一个"昂贵"的诞生过程
我们之前提到,Identity::generate()
是一个计算成本高昂的操作。为什么不简单地生成一个随机的密钥对和地址呢?
这是为了防止"地址冲突攻击"。如果生成身份非常容易,攻击者就可以不断地生成海量的新身份,直到碰巧有一个身份的 ZeroTier 地址与某个合法用户的地址相同。虽然公钥不同,但这可能会在某些情况下引起混乱或安全问题。
为了解决这个问题,ZeroTier 使用了一种类似比特币挖矿的**"工作量证明"(Proof-of-Work)**机制。
在 Identity.cpp
文件中,核心逻辑在一个名为 _computeMemoryHardHash
的函数里。这个函数做的事情可以简化理解为:
- 拿出一个公钥。
- 对这个公钥进行一系列非常复杂的、需要大量内存和 CPU 运算的哈希计算。
- 检查计算出的哈希结果是否满足一个特定的"幸运条件"(例如,哈希值的前几个字节必须小于某个数)。
- 如果不满足,就对公钥稍作修改,然后重复第 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)。