4、数字签名原理:公钥密码学、哈希函数、签名验证

大家好。今天我们来聊聊数字签名这个核心话题。

你想想看,HSM 为什么会存在?说白了,就是得证明一段数据「是谁签的、签完之后有没有被改过」。所以,数字签名原理这块搞不清楚,后面审计日志签名根本没法落地。

我在项目里见过不少团队,直接把私钥读出来,在应用服务器上做签名。嗯……结果大家都懂,私钥被拿走了,签名就毫无意义了。

下面我把这三个东西拆开来讲:公钥密码学、哈希函数、签名验证到底是怎么配合的。

4.1 公钥密码学:公私钥对才是签名的基础

数字签名不是靠对称密钥,而是靠公钥密码学。为什么?因为你得让所有人能验证签名,但只有你才能生成签名。

这就像我有个印章(私钥),任何人都能用我的公开印鉴(公钥)来核验这个印章是不是真的。

  • 私钥(Private Key): 必须存储在 HSM 内部,永不离开;负责对数据生成签名。
  • 公钥(Public Key): 公开分发;负责验证签名;HSM 支持导出公钥。

常用的签名算法有 RSA(2048/4096 位)和 ECDSA(P-256/P-384)。我个人习惯:在密钥审计日志场景下,ECC P-384 已经够用;如果合规要求是 FIPS 140-3 Level 3,那就老老实实上 RSA 4096。

来,看一看这两种算法的简单对比:

特性 RSA 签名 ECDSA 签名
密钥长度 2048 / 4096 位 P-256 / P-384
签名大小 与密钥长度相等(如 256 字节) 很小(64~96 字节)
计算速度 签名较慢,验证较快 签名较快,验证稍慢
HSM 典型支持 非常成熟 现代 HSM 全面支持
审计日志场景 常用,兼容性极好 日志空间敏感时首选
我的一个小建议: 在 HSM 中生成密钥对时,尽量指定 CKA_SENSITIVE 和 CKA_EXTRACTABLE = FALSE。我曾经遇到过开发团队为了调试方便把私钥设成了可导出,结果上线前审计直接打回来了——私钥一旦出 HSM,HSM 的安全边界就失效了。

4.2 哈希函数:给数据打上「数字指纹」

签名的对象不可能是几十 GB 的原始审计日志。你得先给它算个摘要。

哈希函数的作用是:无论输入多大,输出一个固定长度的摘要。而且这个过程是不可逆的。

常用的哈希算法:

  • SHA-256: 输出 32 字节。目前最通用,NIST 推荐。
  • SHA-384: 输出 48 字节。用在 ECDSA P-384 上很合适。
  • SHA-512: 输出 64 字节。计算量大一点,我一般不用在日志签名上。

核心原则:

先哈希,再签名。HSM 内部一般都支持直接输入 CKM_SHA256_RSA_PKCS 这种机制——把哈希和签名一步完成,减少了数据在传输过程中的暴露。

避坑提醒: 我曾经见过有人用 MD5 给日志摘要做签名——说“只是内部用,没关系”。结果审计来了,要求用 SHA-2 以上,整个签名链全得重签,几十万条日志全部作废。所以,只要是新系统,哈希函数请直接上 SHA-256 或更高。

4.3 签名与验证:完整流程

数字签名的完整流程,我给你拆成两步。

签名生成(Sign):

  1. 获取审计日志原文(比如 log_entry.txt)。
  2. 计算哈希:digest = SHA-256(log_entry)。
  3. 送入 HSM,用私钥对 digest 做加密(或 ECC 点乘),生成签名 signature。
  4. 输出 log_entry + signature 一起存储。

签名验证(Verify):

  1. 拿到 log_entry。
  2. 重新计算哈希:digest' = SHA-256(log_entry)。
  3. 用公钥解密签名,得到 digest''。
  4. 比较 digest' 是否等于 digest''。

嗯,这里要注意:上述第三步大多由 HSM 或密码库自动完成,你只需要告诉它:验证签名。

看一下核心的代码片段示意(PKCS#11 接口):

// 生成签名 (HSM 内部计算)
CK_SESSION_HANDLE hSession;
CK_OBJECT_HANDLE hPrivateKey;
CK_BYTE data[] = "audit log line #1001";
CK_BYTE signature[256];
CK_ULONG sigLen = sizeof(signature);

// 设置机制为 SHA256 + RSA PKCS
CK_MECHANISM mech = {CKM_SHA256_RSA_PKCS, NULL_PTR, 0};
C_SignInit(hSession, &mech, hPrivateKey);
C_Sign(hSession, data, sizeof(data)-1, signature, &sigLen);

// 验证签名 (HSM 内部计算)
CK_OBJECT_HANDLE hPublicKey;
C_VerifyInit(hSession, &mech, hPublicKey);
C_Verify(hSession, data, sizeof(data)-1, signature, sigLen);  // 返回 CKR_OK 即通过
经验之谈: 在上面这段代码中,C_Verify 返回 CKR_OK 才是验证通过。我曾经调试时发现返回的是 CKR_SIGNATURE_INVALID,排查了半天发现是日志读取时多了个换行符。所以我的习惯是:在做验证前,确保原文的字节与签名时完全一致——包括空格、换行、编码。

4.4 为什么审计日志需要哈希再签名?

你可能会问:为什么不直接对整条日志签名?

原因有两点:

  • 性能: HSM 的签名运算比较慢,尤其是 RSA 4096。先哈希相当于把大数据压缩成 32 字节,再签。我在参与设计一个要求每秒签 20000 条日志的 HSM 时,如果直接对 Log 原文签名,HSM 根本扛不住;改成先哈希再签名,性能直接翻倍。
  • 安全: 哈希函数让原文没法被篡改。哪怕攻击者改了原文的一个比特,重新算出来的摘要也完全不同,签名的验证就会失败。

说白了,哈希函数和签名就像是双重保险:哈希保证数据没被改,签名保证哈希是来自正确的人。

4.5 整章知识体系一览

这些东西其实不复杂,串起来就一行字:

私钥签名,公钥验证;先哈希,再签名。

下面这张图把整个流程和组件关系画清楚了,你看一眼就能记住。

数字签名原理流程 签名方(HSM 私钥) 原始审计日志 SHA-256 哈希摘要 私钥签名 日志原文 + 签名 验证方 获取日志 & 签名 重算哈希摘要 公钥验证签名 验证通过? Yes: 日志可信 / No: 篡改 最左边是谁在签,最右边是谁在验。中间只流通公钥和签名结果,私钥始终在 HSM 内部.

一句话总结: 私钥签名、公钥验证、哈希压缩、HSM 保护。这四个合在一起,才叫一个完整、可信的数字签名体系。