AuthorN4
2026-03-15

现代密码学中的签名和验签...

签名

假设这是一段我需要发送给另一个用户的密信,而我不希望数据到达接受者的时候被其他人恶意修改,这时我需要证明两件事:

  • 你收到的信息是没有经过修改的
  • 这条信息的来源是我

如何证明上述两点呢?这段时间以来,我了解了一些密码学知识,这是我的一点点个人见解:

  • 密码学的加密倾向于使用数学中难以通过单/部分参数反推原计算式的数学难题(如椭圆曲线等)
  • 密码学的加密与解密的密钥对构建从数学上看就是数学证明问题。

受限于本人近乎孱弱的数学能力,我们只能相信现有的加密算法确实能够实现完整的证明。而我们只关心数据加密的实际过程。先来说说什么是签名

SHA256

SHA256是一种加密算法之一,他能保证相同一致的输入数据经过SHA256加密后输出的是一致的密文(好像长度是256个字符),而原输入如果有任何修改,那么输出就会有天差地别;此外,很难从密文直接反推明文。

回到签名部分。签名的过程是这样的:

假设我们有一段信息需要签名:

message
// 在实际中,这段信息通常包含更多内容,比如我们可以在其中加入身份信息和下一段密钥等等
public key || identity key || info....

首先第一步,我们需要保证信息内容不被修改。这时我们可以用SHA256:

h1(message_hash) = SHA256(message);

接下来,我们需要保证对方能够验证数据来自我,就需要用我的私钥对数据进行运算:

signature = Sign(h1, private_key);

接下来就可以把签名和信息一起发送出去了:

(message, signature)

当任何一个人接收到数据包的时候,他就能够通过验签来核验上面的两个要点,从而保证信息传输的可靠性。

验签

验签的一方需要证明以上的两点才能够信任这份信息。过程是这样的:

对收到的信息生成摘要。此时,如果数据有改动,那么摘要会直接与发送方本地计算的h1不匹配,那么信息校验肯定不通过:

h2 = SHA256(message);

解密签名,核验包裹在签名中的”发送方计算的摘要“是否和信息摘要一致:

Verify(public_key, signature, h2)

这时就会遇到两种情况:如果不是我的私钥签名,那么使用我的公钥会得到错误的结果,自然与本地计算的h2无法匹配。如果信息被篡改,得出的h1一定不能与我再次计算的h2匹配。于是就能够丢弃这个包。

这样的过程就能够保证消息传输的安全性。

在现实中,会有更多复杂的情况。想要实现私密通信,必须采取更多的通信手段和加密方式。这里做一点小小的预告:

Anon-chat(WIP)

ECDH与X3DH

ECDH能够通过让通信的两个对端计算出相同的共享密钥(SharedSecret)来保证首次通信后后续信息的秘密性。其中,X3DH是Signal Protocol中建立通信的核心。使用Signal Protocol进行加密通信的方式是当下最流行的加密通信方式之一。Signal/WhatsAPP/iMessager都采用这样的协议进行通信。

在常规的Diffie-Hallman加密算法中,必须要求通信的两个对端同时在线才有条件交换密钥。X3DH是对这一缺点的改进。它能够借助托管在不可信服务器上的预密钥实现即使对端离线仍能够保证双端都能计算出共享密钥。

对于信息的接收方Bob,他会将这些东西放在服务器上

struct PreKeyBundle {
  pub identity_publickey_B: [u8;32],
  pub signed_prekey_B: String,
  pub signature: String;			// 使用身份私钥对预密钥签名
  pub opk_B: Option<Vec<[u8;32]>>,	// 可选的多个单次预密钥,提供更强的信息前向安全性
}

当Bob离线后,Alice仍然能够从服务器上下载预密钥包,并验证合法性,计算共享密钥:

// Alice从服务器得到...
PrekeyBundle
// 验证签名,防止服务器伪造预密钥
Verify(identity_publickey_B, signature, signed_prekey_B);
// 生成临时密钥 Ephemeral key
// 计算四次Diffie-Hallman加密:
dh1 = DH(identity_key_A, signed_prekey_B);		// A身份认证
dh2 = DH(ephemeral_key_A, identity_key_B); 		// B身份认证
dh3 = DH(ephemeral_key_A, signed_prekey_B); 	// 数据前向安全
dh4 = DH(ephemeral_key_A, Onetime_prekey_B);	// 额外的前向安全
// 生成共享密钥
SharedSecret: [u8;32] = HKDF(dh1, dh2, dh3, dh4);
// 将第一条消息加密
AD = encode(identity_key_A) || encode(identity_key_B)	//绑定身份
ciphertext = AEAD(message)
// 将Alice的数据上传到不可信服务器
pub enum DataFrame {
  PrekeyInit {
    identity_key_A: [u8;32],
    ephemeral_key_A: [u8;32],
    signed_prekey_id: u32,
    ciphertext: Vec<u8>,
  },
  ...
}
// Bob收到消息后:
ikA + ekA + SPKB + OPKB => dh1-4
// 于是就能计算出同样的SecretKey
AD = encode(ika) || encode(ikb);
Decode(ciphertext, AD);
// 这样一来,两个端点就握手成功了。如果用到了opk,那么b会删除本地的opk

握手完成后,两个端点会基于共享密钥使用双棘轮算法通信。这个我还没看,留到后面介绍吧。

大概就是这么多。感谢你能看到这里!希望你看的开心~

留言

加载中...

发布留言

去登录