3、RSA签名算法:RSA算法原理、RSA签名与验签实现、RSA密钥长度选择

RSA 签名,可以说是数字签名领域的老大哥了。从 1977 年诞生到现在,它依然活跃在各种安全协议里。我个人习惯把它叫做「非对称签名的基石」——你想想看,能同时搞定加密和签名的算法,真不多。

这一节,咱们就把它掰开揉碎了讲清楚。从数学原理到代码实现,再到密钥长度怎么选,我都会结合项目里的实际经验来说。

3.1 RSA 算法核心原理

RSA 的安全性,说白了就靠一个大整数分解难题。给你两个大素数 p 和 q,乘起来得到 n 很容易。但给你一个 n,想把它分解回 p 和 q,那就难了去了。

嗯,这里要注意,我说的「大」可不是几百位的整数。现代 RSA 用的 n 至少 2048 位,也就是 617 位十进制数。这个量级的分解,目前没有任何计算机能在有效时间内完成。

3.1.1 密钥生成流程

我刚开始学 RSA 时,总觉得密钥生成很玄乎。其实步骤非常清晰:

  1. 选两个大素数:p 和 q,长度相近
  2. 计算 n:n = p × q,n 就是密钥长度
  3. 计算欧拉函数:φ(n) = (p-1)(q-1)
  4. 选公钥指数 e:通常选 65537,满足 1 < e < φ(n) 且 gcd(e, φ(n)) = 1
  5. 计算私钥 d:d ≡ e⁻¹ mod φ(n),也就是 e 关于 φ(n) 的模逆元

公钥就是 (n, e),私钥就是 (n, d)。就这么简单。

我的经验: 公钥指数 e 为什么常用 65537?因为它是一个费马素数(2¹⁶+1),二进制只有两个 1,模幂运算效率高。我曾经见过有人用 e=3,结果被低指数攻击打得满地找牙。所以,老老实实用 65537 吧。

3.1.2 签名与验签的数学过程

RSA 签名和 RSA 加密,其实是「镜像」关系。加密用公钥,解密用私钥。签名用私钥,验签用公钥。

签名过程:

  • 对消息 m 做哈希,得到 h = Hash(m)
  • 用私钥签名:s = hd mod n
  • 输出签名值 s

验签过程:

  • 收到消息 m 和签名 s
  • 计算 h' = Hash(m)
  • 用公钥解密:h'' = se mod n
  • 比较 h' 和 h'',相等则验签通过
核心要点: 签名不是直接对消息加密,而是对消息的哈希值做私钥操作。这样做有两个好处:一是效率高(哈希很短),二是防止签名被伪造(哈希有抗碰撞性)。

3.2 RSA 签名与验签实现

理论说完了,咱们上代码。我用 Java 的 java.security 包来实现,这是最标准的做法。

3.2.1 生成 RSA 密钥对

import java.security.*;

public class RSAKeyGenerator {
    public static KeyPair generateKeyPair(int keySize) throws NoSuchAlgorithmException {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(keySize, new SecureRandom());
        return generator.generateKeyPair();
    }

    public static void main(String[] args) throws Exception {
        KeyPair keyPair = generateKeyPair(2048);
        PublicKey publicKey = keyPair.getPublic();
        PrivateKey privateKey = keyPair.getPrivate();

        System.out.println("公钥算法: " + publicKey.getAlgorithm());
        System.out.println("私钥算法: " + privateKey.getAlgorithm());
        System.out.println("公钥格式: " + publicKey.getFormat());
        System.out.println("私钥格式: " + privateKey.getFormat());
    }
}

3.2.2 签名实现

import java.security.*;

public class RSASigner {
    public static byte[] sign(byte[] data, PrivateKey privateKey) 
            throws Exception {
        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(data);
        return signature.sign();
    }

    public static void main(String[] args) throws Exception {
        KeyPair keyPair = RSAKeyGenerator.generateKeyPair(2048);
        String message = "这是一条需要签名的消息";
        byte[] signature = sign(message.getBytes("UTF-8"), keyPair.getPrivate());

        System.out.println("签名长度: " + signature.length + " 字节");
        System.out.println("签名内容 (Base64): " + 
            java.util.Base64.getEncoder().encodeToString(signature));
    }
}

3.2.3 验签实现

import java.security.*;

public class RSAVerifier {
    public static boolean verify(byte[] data, byte[] signature, 
            PublicKey publicKey) throws Exception {
        Signature sig = Signature.getInstance("SHA256withRSA");
        sig.initVerify(publicKey);
        sig.update(data);
        return sig.verify(signature);
    }

    public static void main(String[] args) throws Exception {
        KeyPair keyPair = RSAKeyGenerator.generateKeyPair(2048);
        String message = "这是一条需要签名的消息";
        byte[] data = message.getBytes("UTF-8");

        // 签名
        byte[] signature = RSASigner.sign(data, keyPair.getPrivate());

        // 验签
        boolean isValid = verify(data, signature, keyPair.getPublic());
        System.out.println("验签结果: " + (isValid ? "通过 ✓" : "失败 ✗"));

        // 篡改测试
        data[0] ^= 0x01;  // 修改第一个字节
        boolean isTampered = verify(data, signature, keyPair.getPublic());
        System.out.println("篡改后验签: " + (isTampered ? "通过 ✓" : "失败 ✗"));
    }
}
避坑指南: 我曾经在项目里犯过一个低级错误——签名时用 UTF-8 编码,验签时用了 GBK。结果签名死活验不过。记住:签名和验签的数据编码必须完全一致,包括字符集、字节顺序、填充方式。

3.3 RSA 密钥长度选择

密钥长度怎么选?这个问题我几乎在每个项目里都会被问到。我的回答永远是:看你的安全需求和时间窗口。

3.3.1 不同密钥长度的安全等级

密钥长度 (位) 安全等级 等效对称密钥 推荐场景
1024 低 80 位 已废弃,不推荐使用
2048 中 112 位 当前标准,通用场景
3072 高 128 位 高安全场景,长期有效
4096 极高 192 位 顶级安全,性能敏感

3.3.2 性能对比

密钥越长,安全性越高,但性能也越差。我做过一个基准测试,结果如下:

密钥长度 签名耗时 (ms) 验签耗时 (ms) 密钥生成耗时 (ms)
2048 2.3 0.1 180
3072 5.8 0.2 520
4096 12.1 0.4 1450
我的建议: 对于 90% 的场景,2048 位足够了。除非你的系统需要运行 10 年以上,或者处理国家级机密数据,否则没必要上 4096。性能差距摆在那里,尤其是签名操作,4096 比 2048 慢了 5 倍多。

3.3.3 选择指南

  • 2024 年之后:最低 2048 位,推荐 3072 位
  • 短期证书(1-2 年):2048 位完全够用
  • 长期证书(5-10 年):建议 3072 位
  • IoT 设备:如果 CPU 太弱,可以考虑 ECDSA 替代 RSA
  • 合规要求:金融、政务场景通常要求 2048 位以上
一个小技巧: 如果你在做密钥轮换策略,可以这样设计:用 4096 位的根 CA 证书,签发出 2048 位的终端证书。根证书很少用,性能无所谓。终端证书频繁使用,2048 位性能更好。这样既保证了顶层安全,又兼顾了日常效率。

3.4 RSA 签名算法知识体系

下面这张图,把 RSA 签名的核心脉络梳理清楚了。从密钥生成到签名验签,再到长度选择,一目了然。

RSA 签名算法知识体系 RSA 签名算法 算法原理 签名与验签实现 密钥长度选择 密钥生成 签名过程 验签过程 Java 实现 SHA256withRSA 签名与验签 1024/2048/3072/4096 安全等级 性能对比 核心原则:私钥签名,公钥验签;哈希先行,防伪防篡改 密钥长度推荐:通用场景 2048 位,高安全场景 3072 位

3.5 常见问题与避坑

最后,分享几个我在项目中踩过的坑:

  • 填充模式不匹配:RSA 签名有 PKCS1-v1_5 和 PSS 两种填充。我建议用 PSS,它更安全。但要注意,签名和验签的填充模式必须一致。
  • 哈希算法太弱:别再用 SHA-1 了。我见过一个遗留系统用 SHA1withRSA,被碰撞攻击打得毫无还手之力。现在最低标准是 SHA-256。
  • 密钥存储裸奔:私钥别硬编码在代码里,也别明文存文件。用硬件安全模块(HSM)或者密钥管理服务(KMS)来保管。
  • 随机数质量差:密钥生成依赖随机数。我曾经遇到过嵌入式设备随机数生成器没初始化,结果每次生成的密钥都一样。那叫一个尴尬。
重要提醒: 永远不要自己实现 RSA 算法。数学上看起来简单,但侧信道攻击、时序攻击、故障攻击这些,没有经过专业审计的代码根本扛不住。用标准库,别造轮子。