Luke a Pro

Luke Sun

Developer & Marketer

🇺🇦
EN||

数字签名:证明谁发送了消息

| , 10 minutes reading.

1. 为什么要关心这个问题?

你下载一个软件更新。你怎么知道它真的来自供应商而不是恶意软件?

你收到一封来自银行的邮件。你怎么知道它真的来自你的银行?

你电子签署一份合同。法院怎么知道你真的同意了?

数字签名解决了这些问题。它们是软件安全、安全通信和法律电子文档的基础。

2. 定义

数字签名 是一种密码学方案,它证明:

  1. 身份验证:消息是由声称的发送者创建的
  2. 完整性:消息自签名以来没有被修改
  3. 不可否认性:发送者不能否认签署了该消息
物理签名 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 <dev@example.com>"

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': 'user@example.com'
})

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. 本章小结

三点要记住:

  1. 数字签名证明真实性和完整性。 它们保证谁签名以及内容没有被修改,但不加密也不证明现实世界的身份。

  2. 新项目使用 Ed25519。 它快速、安全且难以误用。为了兼容性回退到 ECDSA P-256,只有必需时才用 RSA。

  3. 签名需要上下文。 有效的签名只证明某人签了某些东西——你需要证书(PKI)来知道那个人是谁。

12. 下一步

我们现在可以验证消息来自特定私钥的持有者。但我们如何知道那个密钥属于他们声称的那个人?

在下一篇文章中:证书和 PKI——我们如何在互联网上建立信任链并证明现实世界的身份。