非对称加密与签名:RSA、ECC、SM2
作者:林 | 系列:密码学 | 适合读者:后端开发 / 安全工程师
一、非对称加密原理
对称加密用一把钥匙,非对称加密用一对钥匙:
公钥(Public Key):可以公开,任何人都能拿到
私钥(Private Key):必须保密,只有自己持有
加密:用对方的公钥加密 → 只有对方的私钥能解
签名:用自己的私钥签名 → 任何人用公钥都能验证非对称加密的数学基础是"单向陷门函数",正向计算容易,反向计算极难,但持有特定信息(私钥)可以轻松反向。
二、三大主流算法
2.1 RSA
基于大整数分解难题:给定两个大素数 p、q,计算 n = p × q 容易;但给定 n,分解出 p 和 q 极难。
密钥生成:
1. 选两个大素数 p, q(各 1024 bit)
2. 计算 n = p × q(2048 bit)
3. 计算 φ(n) = (p-1)(q-1)
4. 选择 e(通常 65537),满足 gcd(e, φ(n)) = 1
5. 计算 d = e⁻¹ mod φ(n)
公钥 = (n, e)
私钥 = (n, d)
加密:C = M^e mod n
解密:M = C^d mod nRSA 的问题:密钥太长。2048 bit 才提供 112 bit 安全强度,4096 bit 才提供 128 bit。
2.2 ECC(椭圆曲线)
基于椭圆曲线离散对数难题:给定椭圆曲线上的点 G 和 Q = k×G,由 Q 和 G 反推 k 极难。
椭圆曲线方程:y² = x³ + ax + b (mod p)
密钥生成:
1. 选择标准曲线(如 P-256、sm2p256v1)
2. 随机生成私钥 d(256 bit 整数)
3. 计算公钥 Q = d × G(椭圆曲线点乘)
私钥 = d(一个 256 bit 整数)
公钥 = Q(椭圆曲线上的一个点,x + y 各 256 bit)ECC 256 bit 密钥 = RSA 3072 bit 密钥的安全强度,但密钥短 12 倍。
2.3 SM2(国密椭圆曲线)
SM2 本质就是 ECC,使用国密定义的曲线参数 sm2p256v1,包含三个功能:
| 功能 | 对标 | 标准 |
|---|---|---|
| 数字签名 | ECDSA | GM/T 0003.2 |
| 密钥交换 | ECDH | GM/T 0003.3 |
| 公钥加密 | ECIES | GM/T 0003.4 |
SM2 签名与 ECDSA 的关键区别:SM2 签名过程中引入了 userId(用户标识),参与 Z 值计算,提供了更强的身份绑定。
三、算法对比
3.1 参数与互操作对比
| 指标 | RSA-2048 | ECDSA P-256 | SM2 | Ed25519 |
|---|---|---|---|---|
| 密钥长度 | 2048 bit | 256 bit | 256 bit | 256 bit |
| 安全强度 | 112 bit | 128 bit | 128 bit | 128 bit |
| 签名长度 | 256 bytes | 64 bytes | 64 bytes | 64 bytes |
表中的 ECDSA 与 SM2 长度按固定宽度 r || s 表示,DER 编码长度可变化。Ed25519 签名为固定 64 字节。速度取决于提供者、硬件、密钥缓存和批处理,应在目标库中分别测量密钥生成、签名与验签。
3.2 选型建议
| 场景 | 推荐 | 原因 |
|---|---|---|
| 新项目(通用) | Ed25519 / ECDSA P-256 | 快、密钥短、现代标准 |
| 信创/国密项目 | SM2 | 合规要求 |
| 兼容老系统 | RSA-2048 | 兼容性更稳妥 |
| SSH 密钥 | Ed25519 | OpenSSH 默认推荐 |
| TLS 证书 | ECDSA P-256 | 主流 CA 支持 |
| 区块链 | Ed25519 / secp256k1 | 性能要求高 |
四、密钥格式
4.1 RSA 密钥格式
RSA 公钥(PKCS#1):
-----BEGIN RSA PUBLIC KEY-----
Base64(DER 编码的 RSAPublicKey)
-----END RSA PUBLIC KEY-----
RSA 公钥(X.509/PKCS#8,更通用):
-----BEGIN PUBLIC KEY-----
Base64(DER 编码的 SubjectPublicKeyInfo)
-----END PUBLIC KEY-----
RSA 私钥(PKCS#1):
-----BEGIN RSA PRIVATE KEY-----
Base64(DER 编码的 RSAPrivateKey)
-----END RSA PRIVATE KEY-----
RSA 私钥(PKCS#8,推荐):
-----BEGIN PRIVATE KEY-----
Base64(DER 编码的 PrivateKeyInfo)
-----END PRIVATE KEY-----4.2 SM2/ECC 密钥格式
SM2 私钥:256 bit 整数
Hex 表示:64 字符(32 字节)
示例:aabbccdd11223344aabbccdd11223344aabbccdd11223344aabbccdd11223344
SM2 公钥:椭圆曲线上的点 (x, y)
未压缩格式:04 + x(64字符) + y(64字符) = 130 字符(65 字节)
压缩格式:02/03 + x(64字符) = 66 字符(33 字节)4.3 格式转换(Java)
import org.bouncycastle.jce.ECNamedCurveTable;
import org.bouncycastle.jce.spec.ECParameterSpec;
import org.bouncycastle.math.ec.ECPoint;
import java.math.BigInteger;
import java.security.*;
import java.security.spec.*;
public class KeyFormatUtil {
// hex 私钥 → PrivateKey 对象
public static PrivateKey hexToSm2PrivateKey(String hexKey) throws Exception {
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
BigInteger d = new BigInteger(hexKey, 16);
org.bouncycastle.jce.spec.ECPrivateKeySpec privSpec =
new org.bouncycastle.jce.spec.ECPrivateKeySpec(d, ecSpec);
KeyFactory kf = KeyFactory.getInstance("EC", "BC");
return kf.generatePrivate(privSpec);
}
// hex 公钥 → PublicKey 对象
public static PublicKey hexToSm2PublicKey(String hexKey) throws Exception {
if (!hexKey.startsWith("04")) {
hexKey = "04" + hexKey;
}
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("sm2p256v1");
ECPoint point = ecSpec.getCurve().decodePoint(hexToBytes(hexKey));
org.bouncycastle.jce.spec.ECPublicKeySpec pubSpec =
new org.bouncycastle.jce.spec.ECPublicKeySpec(point, ecSpec);
KeyFactory kf = KeyFactory.getInstance("EC", "BC");
return kf.generatePublic(pubSpec);
}
// PEM 字符串 → RSA PublicKey
public static PublicKey pemToRsaPublicKey(String pem) throws Exception {
String base64 = pem
.replace("-----BEGIN PUBLIC KEY-----", "")
.replace("-----END PUBLIC KEY-----", "")
.replaceAll("\\s", "");
byte[] der = java.util.Base64.getDecoder().decode(base64);
X509EncodedKeySpec spec = new X509EncodedKeySpec(der);
return KeyFactory.getInstance("RSA").generatePublic(spec);
}
private static byte[] hexToBytes(String hex) {
int len = hex.length();
byte[] data = new byte[len / 2];
for (int i = 0; i < len; i += 2) {
data[i / 2] = (byte) ((Character.digit(hex.charAt(i), 16) << 4)
+ Character.digit(hex.charAt(i + 1), 16));
}
return data;
}
}五、数字签名实战
5.1 签名流程
签名方(持有私钥):
1. 对消息 M 计算哈希:h = Hash(M)
2. 用私钥对 h 签名:sig = Sign(privateKey, h)
3. 发送 (M, sig)
验签方(持有公钥):
1. 对消息 M 计算哈希:h' = Hash(M)
2. 用公钥验证签名:Verify(publicKey, h', sig)
3. 通过 → 消息未被篡改且确实由私钥持有者发出5.2 RSA 签名
import java.security.*;
public class RsaSignUtil {
// 生成 RSA 密钥对
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("RSA");
gen.initialize(2048, new SecureRandom());
return gen.generateKeyPair();
}
// SHA256withRSA 签名
public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(data);
return sig.sign();
}
// 验签
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);
}
}5.3 SM2 签名(Hutool)
import cn.hutool.core.util.HexUtil;
import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.SM2;
public class Sm2SignUtil {
private static final byte[] DEFAULT_USER_ID =
"1234567812345678".getBytes();
// SM2 签名
public static String sign(String privateKeyHex, String message) {
SM2 sm2 = SmUtil.sm2(privateKeyHex, null);
sm2.setUserId(DEFAULT_USER_ID);
byte[] signBytes = sm2.sign(message.getBytes());
return HexUtil.encodeHexStr(signBytes);
}
// SM2 验签
public static boolean verify(String publicKeyHex, String message,
String signHex) {
SM2 sm2 = SmUtil.sm2(null, publicKeyHex);
sm2.setUserId(DEFAULT_USER_ID);
return sm2.verify(message.getBytes(), HexUtil.decodeHex(signHex));
}
}5.4 ECDSA 签名
import java.security.*;
import java.security.spec.ECGenParameterSpec;
public class EcdsaSignUtil {
// 生成 ECDSA P-256 密钥对
public static KeyPair generateKeyPair() throws Exception {
KeyPairGenerator gen = KeyPairGenerator.getInstance("EC");
gen.initialize(new ECGenParameterSpec("secp256r1"), new SecureRandom());
return gen.generateKeyPair();
}
// SHA256withECDSA 签名
public static byte[] sign(byte[] data, PrivateKey privateKey) throws Exception {
Signature sig = Signature.getInstance("SHA256withECDSA");
sig.initSign(privateKey);
sig.update(data);
return sig.sign();
}
// 验签
public static boolean verify(byte[] data, byte[] signature,
PublicKey publicKey) throws Exception {
Signature sig = Signature.getInstance("SHA256withECDSA");
sig.initVerify(publicKey);
sig.update(data);
return sig.verify(signature);
}
}六、SM2 签名的特殊性
SM2 签名与 ECDSA 的主要区别在于 Z 值计算:
ECDSA 签名:
e = SHA256(M)
签名 = f(e, privateKey, randomK)
SM2 签名:
Z = SM3(ENTL || userId || a || b || xG || yG || xA || yA)
e = SM3(Z || M)
签名 = f(e, privateKey, randomK)Z 值将用户标识、曲线参数、公钥绑定在一起,使得签名与特定身份关联。
6.1 前后端对接要点
| 参数 | 说明 | 前后端必须一致 |
|---|---|---|
| userId | 用户标识,默认 1234567812345678 | ✅ |
| hash | 是否对消息预哈希 | ✅ |
| 签名格式 | r||s(128 hex) 或 DER | ✅ |
| 公钥格式 | 130 字符 hex(04 开头) | ✅ |
6.2 签名格式:r||s vs DER
r||s 格式(128 hex 字符 = 64 字节):
r(32字节) + s(32字节) 直接拼接
长度固定,简单明了
DER 格式(可变长度,通常 70-72 字节):
30 [len]
02 [len] [r 的字节,可能有前导 00]
02 [len] [s 的字节,可能有前导 00]
为什么长度不固定:ASN.1 INTEGER 是有符号的,
如果 r 或 s 最高位是 1,需要补 0x00 避免被解释为负数// r||s 转 DER
public static byte[] rsToDer(byte[] rs) {
byte[] r = new byte[32];
byte[] s = new byte[32];
System.arraycopy(rs, 0, r, 0, 32);
System.arraycopy(rs, 32, s, 0, 32);
ASN1EncodableVector v = new ASN1EncodableVector();
v.add(new ASN1Integer(new BigInteger(1, r)));
v.add(new ASN1Integer(new BigInteger(1, s)));
return new DERSequence(v).getEncoded();
}
// DER 转 r||s
public static byte[] derToRs(byte[] der) {
ASN1Sequence seq = ASN1Sequence.getInstance(der);
BigInteger r = ASN1Integer.getInstance(seq.getObjectAt(0)).getValue();
BigInteger s = ASN1Integer.getInstance(seq.getObjectAt(1)).getValue();
byte[] result = new byte[64];
byte[] rBytes = toFixedLengthBytes(r, 32);
byte[] sBytes = toFixedLengthBytes(s, 32);
System.arraycopy(rBytes, 0, result, 0, 32);
System.arraycopy(sBytes, 0, result, 32, 32);
return result;
}七、非对称加密(公钥加密)
非对称加密直接加密数据有长度限制,通常只用于加密短数据(如对称密钥):
7.1 RSA 加密
// RSA-OAEP 加密(推荐,比 PKCS1v15 更安全)
public static byte[] rsaEncrypt(byte[] data, PublicKey publicKey) throws Exception {
Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
return cipher.doFinal(data);
}
// RSA 最大明文长度:
// PKCS1v15: keySize/8 - 11 字节(RSA-2048 → 245 字节)
// OAEP-SHA256: keySize/8 - 66 字节(RSA-2048 → 190 字节)7.2 SM2 加密
SM2 加密输出格式为 C1C3C2:
C1: 临时公钥点(65 字节,04+x+y)
C3: SM3 哈希(32 字节)
C2: 加密数据(与明文等长)
密文总长 = 97 + 明文长度import cn.hutool.crypto.SmUtil;
import cn.hutool.crypto.asymmetric.SM2;
// SM2 加密
public static String sm2Encrypt(String plaintext, String publicKeyHex) {
SM2 sm2 = SmUtil.sm2(null, publicKeyHex);
return sm2.encryptHex(plaintext, KeyType.PublicKey);
}
// SM2 解密
public static String sm2Decrypt(String cipherHex, String privateKeyHex) {
SM2 sm2 = SmUtil.sm2(privateKeyHex, null);
return sm2.decryptStr(cipherHex, KeyType.PrivateKey);
}八、证书体系(X.509)
8.1 证书的作用
公钥需要一个可信的方式分发。X.509 证书将公钥与身份绑定,由 CA(证书颁发机构)签名背书:
X.509 证书内容:
├── 版本号(V3)
├── 序列号
├── 签名算法(如 SHA256withRSA、SM3withSM2)
├── 颁发者(CA 信息)
├── 有效期(起止时间)
├── 使用者(证书主体信息)
├── 公钥(SubjectPublicKeyInfo)
├── 扩展字段(用途、SAN 等)
└── CA 的数字签名8.2 证书链验证
根证书(Root CA) ← 自签名,预装在系统/浏览器中
│ 签发
▼
中间证书(Intermediate CA)
│ 签发
▼
终端证书(你的网站证书) ← 包含你的公钥
验证过程:
1. 用中间 CA 的公钥验证终端证书的签名
2. 用根 CA 的公钥验证中间证书的签名
3. 根 CA 在系统信任列表中 → 整条链可信8.3 Java 读取证书
import java.security.cert.*;
import java.io.FileInputStream;
public class CertUtil {
// 从文件加载 X.509 证书
public static X509Certificate loadCert(String path) throws Exception {
CertificateFactory cf = CertificateFactory.getInstance("X.509");
try (FileInputStream fis = new FileInputStream(path)) {
return (X509Certificate) cf.generateCertificate(fis);
}
}
// 打印证书信息
public static void printCertInfo(X509Certificate cert) {
System.out.println("主题: " + cert.getSubjectX500Principal());
System.out.println("颁发者: " + cert.getIssuerX500Principal());
System.out.println("有效期: " + cert.getNotBefore() + " ~ " + cert.getNotAfter());
System.out.println("签名算法: " + cert.getSigAlgName());
System.out.println("公钥算法: " + cert.getPublicKey().getAlgorithm());
System.out.println("序列号: " + cert.getSerialNumber().toString(16));
}
// 验证证书签名(用颁发者公钥)
public static boolean verifyCert(X509Certificate cert,
PublicKey issuerPublicKey) {
try {
cert.verify(issuerPublicKey);
return true;
} catch (Exception e) {
return false;
}
}
}九、常见踩坑
坑 1:RSA 明文超长
// ❌ RSA-2048 加密 300 字节数据 → 报错
cipher.doFinal(new byte[300]); // IllegalBlockSizeException
// ✅ 用数字信封:RSA 加密 AES 密钥,AES 加密数据
byte[] aesKey = generateAesKey();
byte[] encryptedKey = rsaEncrypt(aesKey, publicKey);
byte[] encryptedData = aesEncrypt(data, aesKey);坑 2:SM2 userId 不一致
签名方用 userId = "1234567812345678"
验签方用 userId = "" 或 null
结果:验签永远失败,无报错提示
排查:首先确认两端 userId 完全一致坑 3:SM2 公钥缺少 04 前缀
// ❌ 128 字符公钥(缺少 04)
String pubKey = "aabb...ccdd"; // 128 字符
// ✅ 补齐 04 前缀
if (!pubKey.startsWith("04")) {
pubKey = "04" + pubKey; // 130 字符
}坑 4:RSA 签名用了 PKCS1v15 Padding
// ⚠️ 旧方式,存在 Bleichenbacher 攻击风险
Cipher.getInstance("RSA/ECB/PKCS1Padding");
// ✅ 加密推荐 OAEP
Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");
// 签名推荐 PSS
Signature.getInstance("SHA256withRSA/PSS");坑 5:ECDSA 签名的随机数 k 重复
ECDSA/SM2 签名时需要随机数 k。如果两次签名使用了相同的 k:
已知 sig1 = (r, s1) 和 sig2 = (r, s2)(r 相同说明 k 相同)
可以直接计算出私钥 d
PlayStation 3 的 ECDSA 私钥就是因为 k 固定被破解。
确保使用 SecureRandom,或使用确定性签名(RFC 6979)。十、完整示例:接口签名方案
/**
* 通用接口签名验签方案
* 支持 RSA / SM2 切换
*/
public class ApiSignature {
public enum SignAlgorithm {
RSA_SHA256("SHA256withRSA"),
SM2_SM3("SM3withSM2");
final String jcaName;
SignAlgorithm(String jcaName) { this.jcaName = jcaName; }
}
// 构造签名串
public static String buildSignString(String method, String path,
long timestamp, String body) {
return method.toUpperCase() + "\n"
+ path + "\n"
+ timestamp + "\n"
+ (body == null ? "" : body);
}
// 签名
public static byte[] sign(String signString, PrivateKey privateKey,
SignAlgorithm algorithm) throws Exception {
Signature sig = Signature.getInstance(algorithm.jcaName, "BC");
sig.initSign(privateKey);
sig.update(signString.getBytes(StandardCharsets.UTF_8));
return sig.sign();
}
// 验签
public static boolean verify(String signString, byte[] signature,
PublicKey publicKey,
SignAlgorithm algorithm) throws Exception {
// 时间戳检查应在调用前完成
Signature sig = Signature.getInstance(algorithm.jcaName, "BC");
sig.initVerify(publicKey);
sig.update(signString.getBytes(StandardCharsets.UTF_8));
return sig.verify(signature);
}
}十一、总结
| 要点 | 建议 |
|---|---|
| 算法选型 | 新项目 ECC/Ed25519,信创用 SM2,兼容用 RSA-2048 |
| 签名算法 | RSA 用 PSS,ECC 用 SHA256withECDSA |
| RSA Padding | 加密用 OAEP,不要用 PKCS1v15 |
| SM2 对接 | 统一 userId、hash 参数、签名格式 |
| 密钥格式 | SM2 用 hex,RSA 用 PEM/PKCS#8 |
| 安全随机数 | 签名的 k 值必须随机,使用 SecureRandom |
| 数据加密 | 非对称加密有长度限制,大数据用数字信封 |