第九章 数字签名:RSA签名与验签、DSA算法、C语言实现
数字签名,说白了就是现实世界中签名的电子版。你手写签名,别人能认出是你写的;数字签名也一样,它能证明消息确实是你发的,而且没被篡改过。
我刚开始接触密码学时,总觉得签名和加密是一回事。后来踩了坑才明白——加密是为了保密,签名是为了防伪。这两个目标完全不同。
这一章,我们就来聊聊两种主流的数字签名算法:RSA签名和DSA算法。我会结合C语言实现,把原理和坑都讲清楚。
9.1 数字签名的基础概念
先理清几个核心概念:
- 签名者:用自己的私钥对消息摘要进行加密,生成签名
- 验证者:用签名者的公钥解密签名,比对摘要是否一致
- 消息摘要:对原始消息做哈希,固定长度,防篡改
你想想看,如果我把消息直接签名,那签名长度会跟消息一样长,太浪费了。所以实际做法是:先哈希,再签名。
核心流程:
签名:消息 → 哈希 → 私钥加密 → 签名
验签:消息 + 签名 → 哈希 → 公钥解密 → 比对哈希值
我在项目中遇到过有人直接用RSA加密整个文件当签名,结果文件大了几倍,性能惨不忍睹。嗯,这就是没理解摘要的作用。
9.2 RSA签名与验签
RSA签名,其实就是把RSA加密算法反过来用。私钥签名,公钥验签。
9.2.1 签名过程
- 对消息M做哈希,得到摘要H
- 用私钥对H做RSA加密,得到签名S
- 将(M, S)一起发送给接收方
9.2.2 验签过程
- 接收方对消息M做同样的哈希,得到H'
- 用公钥对签名S做RSA解密,得到H
- 比较H和H'是否相等
这里有个关键点:RSA签名必须使用PKCS#1 v1.5或PSS填充模式。直接裸加密是不安全的。
避坑指南:我曾经在项目中直接用了裸RSA加密做签名,结果被安全审计打回来。原因是裸RSA存在选择密文攻击的风险。一定要用标准填充模式。
9.2.3 C语言实现RSA签名
下面是一个使用OpenSSL库的RSA签名示例。我个人习惯用OpenSSL,因为它成熟且跨平台。
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include <openssl/err.h>
#include <openssl/sha.h>
// RSA签名函数
int rsa_sign(const char *message, RSA *rsa_private, unsigned char **sig, unsigned int *sig_len) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char*)message, strlen(message), hash);
*sig = (unsigned char*)malloc(RSA_size(rsa_private));
if (!*sig) return -1;
if (RSA_sign(NID_sha256, hash, SHA256_DIGEST_LENGTH, *sig, sig_len, rsa_private) != 1) {
ERR_print_errors_fp(stderr);
free(*sig);
return -1;
}
return 0;
}
// RSA验签函数
int rsa_verify(const char *message, RSA *rsa_public, unsigned char *sig, unsigned int sig_len) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char*)message, strlen(message), hash);
int ret = RSA_verify(NID_sha256, hash, SHA256_DIGEST_LENGTH, sig, sig_len, rsa_public);
return ret; // 1表示验证成功,0表示失败
}
代码看起来简单,但有几个坑要注意:
- 哈希算法要一致:签名和验签必须用同一种哈希,否则比对失败
- 内存管理:签名缓冲区要足够大,RSA_size()返回的就是最大长度
- 错误处理:RSA_sign和RSA_verify返回1才表示成功
小技巧:调试时可以用ERR_print_errors_fp(stderr)打印OpenSSL错误信息,能省不少排查时间。
9.3 DSA算法
DSA(Digital Signature Algorithm)是另一种签名算法。它和RSA最大的区别是:DSA只能签名,不能加密。
为什么会这样?因为DSA基于离散对数问题,它的数学结构决定了它不适合做加密。我刚开始学的时候也纳闷,后来看了论文才明白——设计目标不同。
9.3.1 DSA签名过程
- 生成随机数k(每次签名都要不同!)
- 计算r = (g^k mod p) mod q
- 计算s = k^(-1) * (H(m) + x*r) mod q
- 签名就是(r, s)对
9.3.2 DSA验签过程
- 检查r和s是否在(0, q)范围内
- 计算w = s^(-1) mod q
- 计算u1 = H(m) * w mod q
- 计算u2 = r * w mod q
- 计算v = (g^u1 * y^u2 mod p) mod q
- 如果v == r,验签成功
关键点:DSA的随机数k绝对不能重复使用!如果两次签名用了同一个k,私钥就会被算出来。这不是理论问题,是真有人因此翻车。
9.3.3 C语言实现DSA签名
#include <openssl/dsa.h>
#include <openssl/err.h>
#include <openssl/sha.h>
// DSA签名函数
int dsa_sign(const char *message, DSA *dsa, unsigned char **sig, unsigned int *sig_len) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char*)message, strlen(message), hash);
*sig = (unsigned char*)malloc(DSA_size(dsa));
if (!*sig) return -1;
if (DSA_sign(0, hash, SHA256_DIGEST_LENGTH, *sig, sig_len, dsa) != 1) {
ERR_print_errors_fp(stderr);
free(*sig);
return -1;
}
return 0;
}
// DSA验签函数
int dsa_verify(const char *message, DSA *dsa, unsigned char *sig, unsigned int sig_len) {
unsigned char hash[SHA256_DIGEST_LENGTH];
SHA256((unsigned char*)message, strlen(message), hash);
int ret = DSA_verify(0, hash, SHA256_DIGEST_LENGTH, sig, sig_len, dsa);
return ret;
}
代码结构和RSA签名很像,但底层算法完全不同。DSA的签名长度是固定的,而RSA的签名长度取决于密钥长度。
9.4 RSA vs DSA:怎么选?
我经常被问到这个问题。直接给结论:
| 特性 | RSA签名 | DSA |
|---|---|---|
| 签名速度 | 较慢(私钥操作) | 较快 |
| 验签速度 | 较快(公钥操作) | 较慢 |
| 密钥长度 | 2048位起 | 1024位起(推荐2048) |
| 安全性基础 | 大整数分解 | 离散对数 |
| 能否加密 | 能 | 不能 |
| 随机数要求 | 无特殊要求 | 每次签名必须不同 |
我个人建议:如果系统已经用了RSA做加密,那就统一用RSA签名,减少代码复杂度。如果是从零开始,且对性能有要求,可以考虑DSA或ECDSA(椭圆曲线DSA)。
注意:DSA的随机数生成一定要用密码学安全的随机数生成器(CSPRNG)。用rand()或者time(NULL)做种子,那就是在给自己挖坑。
9.5 数字签名知识体系
下面这张图帮你理清本章的核心逻辑:
9.6 实战中的避坑指南
最后,分享几个我踩过的坑:
- 密钥管理:私钥一定要加密存储。我曾经见过有人把私钥硬编码在代码里,结果代码泄露,整个系统沦陷。
- 签名长度:RSA签名长度等于密钥长度(比如2048位就是256字节)。DSA签名长度固定,但不同参数集长度不同。接收方要做好长度检查。
- 哈希算法选择:别再用了SHA-1了,已经被攻破。至少用SHA-256。
- 随机数质量:DSA的随机数k必须真随机。用/dev/urandom或者OpenSSL的RAND_bytes()。
我的习惯:每次签名前,我都会先打印一下哈希值,确认输入没问题。这个小习惯帮我抓到了好几次数据传输出错的bug。
数字签名是安全体系的基石。RSA和DSA各有优劣,选哪个取决于你的场景。但无论选哪个,密钥保护和随机数质量永远是第一位的。
好了,这一章就到这里。代码示例可以直接拿去用,但记得改改参数和错误处理——毕竟生产环境和demo不一样。
公众号:蓝海资料掘金营,微信deep3321