数字签名:证明谁发送了消息
Published: Sat Feb 01 2025 | Modified: Sat Feb 07 2026 , 10 minutes reading.
1. 为什么要关心这个问题?
你下载一个软件更新。你怎么知道它真的来自供应商而不是恶意软件?
你收到一封来自银行的邮件。你怎么知道它真的来自你的银行?
你电子签署一份合同。法院怎么知道你真的同意了?
数字签名解决了这些问题。它们是软件安全、安全通信和法律电子文档的基础。
2. 定义
数字签名 是一种密码学方案,它证明:
- 身份验证:消息是由声称的发送者创建的
- 完整性:消息自签名以来没有被修改
- 不可否认性:发送者不能否认签署了该消息
物理签名 vs 数字签名:
物理签名:
- 可以通过复制来伪造
- 不能检测文档修改
- 无论文档内容如何,看起来都一样
数字签名:
- 在数学上与签名者的私钥绑定
- 任何修改都会使签名失效
- 每个文档的签名都不同3. 数字签名如何工作
基本过程
签名:
┌─────────────────────────────────────────────────────────────┐
│ 1. 对消息哈希 hash = SHA256(message) │
│ 2. 签名哈希 signature = Sign(hash, privKey) │
│ 3. 发送消息 + 签名 │
└─────────────────────────────────────────────────────────────┘
验证:
┌─────────────────────────────────────────────────────────────┐
│ 1. 对消息哈希 hash = SHA256(message) │
│ 2. 验证签名 Verify(signature, hash, pubKey) │
│ 3. 如果有效,消息是真实且未被修改的 │
└─────────────────────────────────────────────────────────────┘为什么要先哈希?
为什么不直接签名消息?
1. 性能:
- RSA 只能签名约 256 字节
- 签名 1MB 文件需要数千次操作
- 哈希将任意大小减少到 32 字节
2. 安全性:
- 签名原始数据有数学弱点
- 哈希提供额外的安全层
3. 标准化:
- 签名算法的固定大小输入
- 无论消息大小,行为一致4. 签名算法
RSA 签名 (RSA-PSS)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import rsa, padding
# 生成密钥
private_key = rsa.generate_private_key(
public_exponent=65537,
key_size=2048
)
public_key = private_key.public_key()
message = b"Contract: I agree to pay $1000"
# 使用 PSS 填充签名(推荐)
signature = private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
# 验证
try:
public_key.verify(
signature,
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.MAX_LENGTH
),
hashes.SHA256()
)
print("签名有效:消息是真实的")
except Exception:
print("签名无效:消息可能是伪造或被修改的")ECDSA(椭圆曲线 DSA)
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
# 生成密钥(比 RSA 小得多)
private_key = ec.generate_private_key(ec.SECP256R1())
public_key = private_key.public_key()
message = b"Transfer 1 BTC to address xyz"
# 签名
signature = private_key.sign(message, ec.ECDSA(hashes.SHA256()))
# 验证
try:
public_key.verify(signature, message, ec.ECDSA(hashes.SHA256()))
print("有效的 ECDSA 签名")
except Exception:
print("无效的签名")EdDSA (Ed25519) - 现代选择
from cryptography.hazmat.primitives.asymmetric import ed25519
# 生成密钥
private_key = ed25519.Ed25519PrivateKey.generate()
public_key = private_key.public_key()
message = b"Authenticate this request"
# 签名(算法是内置的,不需要选择)
signature = private_key.sign(message)
# 验证
try:
public_key.verify(signature, message)
print("有效的 Ed25519 签名")
except Exception:
print("无效的签名")对比
算法 | 密钥大小 | 签名大小 | 速度 | 安全性
------------+----------+----------+--------+----------
RSA-2048 | 256 B | 256 B | 慢 | 112 位
RSA-3072 | 384 B | 384 B | 更慢 | 128 位
ECDSA P-256 | 64 B | 64 B | 快 | 128 位
Ed25519 | 32 B | 64 B | 最快 | 128 位
推荐:新项目使用 Ed25519
备选:ECDSA P-256 用于兼容性
遗留:需要时使用 RSA-2048+5. 签名保证什么(和不保证什么)
保证什么
✓ 身份验证
「这是由私钥 X 的持有者签名的」
✓ 完整性
「消息自签名以来没有改变」
✓ 不可否认性
「签名者不能否认签署了这条确切的消息」不保证什么
✗ 身份
「签名者就是他们声称的那个人」
(你需要证书来做到这点——下一篇文章)
✗ 机密性
「消息是保密的」
(签名不加密——消息是公开的)
✗ 时间戳
「这是在时间 X 签名的」
(你需要可信时间戳)
✗ 授权
「签名者被授权签署这个」
(业务逻辑,不是密码学)6. 常见用例
代码签名
为什么重要:
- 证明软件来自声称的发布者
- 检测篡改(恶意软件注入)
- 操作系统可以阻止未签名/未知软件
工作原理:
开发者 用户
│ │
│ 1. 创建软件 │
│ 2. 哈希可执行文件 │
│ 3. 用私钥签名 │
│ (来自 CA) │
│ │
│──── signed.exe ────────>│
│ │
│ 4. 操作系统验证签名 │
│ 5. 检查证书链 │
│ 6. 如果有效则运行 │Git 提交签名
# 配置签名密钥
git config --global user.signingkey YOUR_KEY_ID
git config --global commit.gpgsign true
# 签名提交
git commit -S -m "Signed commit"
# 验证提交
git log --show-signature
# 输出:
# commit abc123
# gpg: Signature made Wed Feb 1 2025 10:00:00
# gpg: using RSA key ABCD1234...
# gpg: Good signature from "Developer Name <[email protected]>"JWT(JSON Web Tokens)
import jwt
import datetime
# 服务器的私钥
PRIVATE_KEY = """-----BEGIN EC PRIVATE KEY-----
...
-----END EC PRIVATE KEY-----"""
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----"""
# 创建签名令牌
payload = {
'user_id': 123,
'role': 'admin',
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}
token = jwt.encode(payload, PRIVATE_KEY, algorithm='ES256')
print(f"令牌: {token}")
# 验证令牌
try:
decoded = jwt.decode(token, PUBLIC_KEY, algorithms=['ES256'])
print(f"有效令牌,用户: {decoded['user_id']}")
except jwt.InvalidSignatureError:
print("令牌签名无效 - 可能被篡改")
except jwt.ExpiredSignatureError:
print("令牌已过期")文档签名 (PDF)
# 使用 pyHanko 进行 PDF 签名(概念示例)
from pyhanko.sign import signers
from pyhanko.pdf_utils.incremental_writer import IncrementalPdfFileWriter
# 加载 PDF
with open('contract.pdf', 'rb') as f:
w = IncrementalPdfFileWriter(f)
# 使用证书签名
signer = signers.SimpleSigner.load(
'signing_key.pem',
'signing_cert.pem',
key_passphrase=b'password'
)
# 应用签名
signers.sign_pdf(
w,
signers.PdfSignatureMetadata(field_name='Signature1'),
signer=signer
)
with open('contract_signed.pdf', 'wb') as out:
w.write(out)7. 安全考虑
绝不签名攻击者控制的数据
# 危险:签名任意用户输入
def vulnerable_sign(user_data):
return private_key.sign(user_data) # 攻击者控制内容!
# 安全:签名你自己的结构化数据
def safe_sign(action, resource_id, timestamp):
# 你控制结构和内容
message = f"{action}:{resource_id}:{timestamp}".encode()
return private_key.sign(message)签名可延展性
某些签名方案是可延展的:
给定消息 M 的有效签名 S,
攻击者可以创建 S',它对 M 也是有效的。
ECDSA 是可延展的:
(r, s) 和 (r, -s mod n) 对同一消息都有效
影响:
- 通常不是问题
- 在某些协议中可能导致问题
- 比特币曾因此有漏洞
防御:
- 使用确定性签名 (Ed25519)
- 规范化签名(只使用低 s)密钥管理
私钥安全就是一切:
如果私钥泄露:
- 攻击者可以以你的身份签名
- 所有过去的签名仍然有效
- 必须撤销并重新签名所有内容
最佳实践:
- 高价值密钥存储在 HSM 中
- 使用密码保护的密钥文件
- 定期轮换签名密钥
- 安全地保存离线备份8. 时间戳
问题
Alice 在 2025 年 2 月 1 日签署了一份文档。
她的密钥在 2025 年 3 月 1 日过期。
在 2025 年 4 月 1 日:
- 签名在数学上仍然有效
- 但它是在过期之前还是之后创建的?
- Alice 可能在撤销后签名!可信时间戳
1. Alice 创建签名
2. Alice 将签名发送到时间戳权威机构 (TSA)
3. TSA 签名:「我在时间 X 看到了这个签名」
4. TSA 返回时间戳令牌
现在我们可以证明:
- 签名在特定时间存在
- 密钥在那个时间是有效的RFC 3161 时间戳
# 概念示例 - 实际实现有所不同
import hashlib
import requests
def get_timestamp(signature_bytes):
# 创建时间戳请求
digest = hashlib.sha256(signature_bytes).digest()
# 发送到 TSA
response = requests.post(
'http://timestamp.example.com/tsa',
data=create_timestamp_request(digest),
headers={'Content-Type': 'application/timestamp-query'}
)
return parse_timestamp_response(response.content)
# 时间戳令牌证明签名的创建时间9. 实践中的签名验证
完整示例
from cryptography.hazmat.primitives.asymmetric import ed25519
from cryptography.hazmat.primitives import serialization
import json
import base64
import time
class SignedMessage:
def __init__(self, private_key=None):
if private_key:
self.private_key = private_key
self.public_key = private_key.public_key()
else:
self.private_key = ed25519.Ed25519PrivateKey.generate()
self.public_key = self.private_key.public_key()
def sign(self, data: dict) -> dict:
"""签名带元数据的消息"""
# 添加元数据
message = {
'data': data,
'timestamp': int(time.time()),
'signer': self._get_key_id()
}
# 确定性序列化
message_bytes = json.dumps(message, sort_keys=True).encode()
# 签名
signature = self.private_key.sign(message_bytes)
return {
'message': message,
'signature': base64.b64encode(signature).decode()
}
def verify(self, signed_message: dict, public_key) -> dict:
"""验证签名消息"""
message = signed_message['message']
signature = base64.b64decode(signed_message['signature'])
# 重建被签名的确切字节
message_bytes = json.dumps(message, sort_keys=True).encode()
# 验证签名
public_key.verify(signature, message_bytes)
return message['data']
def _get_key_id(self) -> str:
"""获取公钥的短标识符"""
pub_bytes = self.public_key.public_bytes(
serialization.Encoding.Raw,
serialization.PublicFormat.Raw
)
return base64.b64encode(pub_bytes[:8]).decode()
# 使用
signer = SignedMessage()
# 签名
signed = signer.sign({
'action': 'transfer',
'amount': 100,
'to': '[email protected]'
})
print("签名消息:")
print(json.dumps(signed, indent=2))
# 验证
try:
data = signer.verify(signed, signer.public_key)
print(f"验证成功!数据: {data}")
except Exception as e:
print(f"验证失败: {e}")10. 常见错误
| 错误 | 后果 | 正确做法 |
|---|---|---|
| 不验证签名 | 接受伪造消息 | 信任前始终验证 |
| 不哈希直接签名 | 性能和安全问题 | 使用标准签名方案 |
| 使用 MD5/SHA1 | 易受碰撞攻击 | 使用 SHA-256 或 SHA-3 |
| 在代码中存储私钥 | 密钥泄露 | 使用 HSM、环境变量或密钥保管库 |
| 不检查密钥有效性 | 接受已撤销密钥的签名 | 检查证书链 |
| 混淆加密和签名 | 错误的安全属性 | 它们解决不同问题 |
11. 本章小结
三点要记住:
数字签名证明真实性和完整性。 它们保证谁签名以及内容没有被修改,但不加密也不证明现实世界的身份。
新项目使用 Ed25519。 它快速、安全且难以误用。为了兼容性回退到 ECDSA P-256,只有必需时才用 RSA。
签名需要上下文。 有效的签名只证明某人签了某些东西——你需要证书(PKI)来知道那个人是谁。
12. 下一步
我们现在可以验证消息来自特定私钥的持有者。但我们如何知道那个密钥属于他们声称的那个人?
在下一篇文章中:证书和 PKI——我们如何在互联网上建立信任链并证明现实世界的身份。
