数字签名基础:非对称加密、哈希与签名验签
说到数字签名,我得先讲个真实教训。几年前我参与一个固件升级项目,团队觉得「只要加密了数据就安全」,结果签名机制没做严谨。后来被攻击者伪造了升级包,差点导致大批设备变砖。从那以后,我对签名这件事就特别较真。
数字签名不是加密,它解决的是另一个问题:你怎么证明这段数据确实来自声称的发送方,而且没被篡改过? 说白了,就是「身份认证 + 完整性校验」的组合拳。
核心结论:数字签名 = 非对称加密 + 哈希函数。前者解决身份问题,后者解决完整性问题。两者缺一不可。
非对称加密原理
非对称加密,也叫公钥密码体制。它有一对密钥:公钥和私钥。公钥可以公开,私钥必须保密。
我习惯用一个比喻来解释:公钥就像一把挂锁,谁都能拿到;私钥是唯一能打开这把锁的钥匙,只有你有。别人用你的公钥加密数据,只有你能解密。反过来,你用私钥加密数据,别人用你的公钥解密——这就是数字签名的核心思路。
| 特性 | 对称加密 | 非对称加密 |
|---|---|---|
| 密钥数量 | 1 个(共享密钥) | 2 个(公钥 + 私钥) |
| 密钥分发 | 困难,需要安全通道 | 公钥可公开,私钥本地保存 |
| 计算速度 | 快(适合大数据量) | 慢(适合小数据量) |
| 典型算法 | AES、SM4 | RSA、ECC、SM2 |
| 主要用途 | 数据加密 | 密钥交换、数字签名 |
实际项目中,我们不会用非对称加密去加密大文件。为什么?太慢了。RSA 加密 1KB 数据都要几毫秒,加密一个 100MB 的固件包?不现实。所以真实场景是:非对称加密只处理哈希值或对称密钥,数据本身用对称加密。
哈希函数:数据的「数字指纹」
哈希函数,你想想看,它就像一个单向的粉碎机。你扔进去任意长度的数据,它吐出来固定长度的摘要。而且这个摘要几乎不可能反向还原出原始数据。
哈希函数有三个关键特性:
- 单向性:从哈希值无法反推出原始数据。这是数学上保证的。
- 抗碰撞性:很难找到两个不同的输入,产生相同的哈希值。注意我说的是「很难」,不是「不可能」——理论上碰撞一定存在,但计算上不可行。
- 雪崩效应:输入改一个比特,输出完全变样。我测试过,改一个字节,SHA-256 的输出能差 50% 以上的比特位。
常见的哈希算法:
| 算法 | 输出长度 | 安全性 | 我的建议 |
|---|---|---|---|
| MD5 | 128 位 | 已破解,不推荐 | 别用了,碰撞攻击成本极低 |
| SHA-1 | 160 位 | 理论不安全 | 新项目不要选 |
| SHA-256 | 256 位 | 安全(推荐) | 当前主流选择 |
| SM3 | 256 位 | 国标,安全 | 国内合规项目首选 |
注意:哈希函数不是加密。加密可以解密,哈希不行。我见过有人把「哈希加密」挂在嘴边,这是概念混淆。哈希是摘要,不是加密。
签名与验签流程
好了,把非对称加密和哈希函数组合起来,就是数字签名。流程其实不复杂,我拆成两步讲。
签名过程(发送方)
- 对原始数据计算哈希值(比如 SHA-256)
- 用自己的私钥对哈希值进行加密,得到签名
- 将「原始数据 + 签名」一起发送给接收方
注意:签名是对哈希值加密,不是对原始数据加密。这样做的好处是速度快——哈希值只有 32 字节(SHA-256),加密开销很小。
验签过程(接收方)
- 收到「原始数据 + 签名」
- 对原始数据计算哈希值(用同样的哈希算法)
- 用发送方的公钥解密签名,得到解密后的哈希值
- 比较两个哈希值:如果一致,验签通过;否则失败
为什么会这样?因为只有持有私钥的人才能生成有效的签名。公钥解密成功,就证明签名确实来自私钥持有者。哈希值一致,就证明数据没有被篡改。
一个小技巧:我在项目中经常把签名和验签的日志打出来,对比两个哈希值。如果验签失败,先看哈希值是否一致——很多时候是数据在传输过程中被意外修改了,而不是密钥问题。
下面这张图把整个流程串起来了:
代码示例:用 Python 演示签名与验签
光讲理论不够,我写个简单的 Python 示例。实际项目中你可能用 OpenSSL 或硬件安全模块,但原理是一样的。
import hashlib
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization
# 1. 生成密钥对
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048,
)
public_key = private_key.public_key()
# 2. 原始数据
data = b"这是需要签名的固件镜像数据"
# 3. 签名过程
signature = private_key.sign(
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print(f"签名长度: {len(signature)} 字节")
# 4. 验签过程
try:
public_key.verify(
signature,
data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("验签通过:数据完整,来源可信")
except Exception as e:
print(f"验签失败: {e}")
# 5. 模拟篡改数据
tampered_data = b"这是被篡改的固件镜像数据"
try:
public_key.verify(
signature,
tampered_data,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("验签通过(不应该发生)")
except Exception as e:
print(f"验签失败(预期行为):数据已被篡改")
避坑指南:我曾经在项目中遇到一个坑——签名时用的哈希算法和验签时不一致。比如签名用 SHA-256,验签代码里写成了 SHA-1。结果验签永远失败,排查了半天才发现是算法不匹配。所以,签名和验签的算法参数必须完全一致,包括哈希算法、填充方式等。
常见问题与误区
- 「签名就是加密」:不对。签名是用私钥加密哈希值,目的是证明身份和完整性。加密是用公钥加密数据,目的是保密。两者用途不同。
- 「公钥可以随便给」:理论上可以,但实际中要防止中间人攻击。如果攻击者替换了你的公钥,他就能伪造签名。所以公钥的分发需要可信渠道,比如证书颁发机构(CA)。
- 「哈希值越长越安全」:不一定。SHA-512 比 SHA-256 长,但安全性提升有限,计算开销却大很多。对于大多数场景,SHA-256 已经足够。
重要提醒:永远不要用 MD5 或 SHA-1 做数字签名。我在 2017 年参与的一个遗留系统就用了 SHA-1,后来被证明存在碰撞攻击风险。升级到 SHA-256 花了团队两周时间,因为要兼容旧设备。选算法时眼光放长远一点。
数字签名是启动校验的基石。没有它,你无法确认固件是不是来自官方,也无法知道它有没有被篡改。下一层我们要讨论的是:这些密钥怎么安全地存储和管理?嗯,那是密钥生命周期管理的话题了。