← 返回

非对称加密与签名: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 n

RSA 的问题:密钥太长。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,包含三个功能:

功能对标标准
数字签名ECDSAGM/T 0003.2
密钥交换ECDHGM/T 0003.3
公钥加密ECIESGM/T 0003.4

SM2 签名与 ECDSA 的关键区别:SM2 签名过程中引入了 userId(用户标识),参与 Z 值计算,提供了更强的身份绑定。


三、算法对比

3.1 参数与互操作对比

指标RSA-2048ECDSA P-256SM2Ed25519
密钥长度2048 bit256 bit256 bit256 bit
安全强度112 bit128 bit128 bit128 bit
签名长度256 bytes64 bytes64 bytes64 bytes

表中的 ECDSA 与 SM2 长度按固定宽度 r || s 表示,DER 编码长度可变化。Ed25519 签名为固定 64 字节。速度取决于提供者、硬件、密钥缓存和批处理,应在目标库中分别测量密钥生成、签名与验签。

3.2 选型建议

场景推荐原因
新项目(通用)Ed25519 / ECDSA P-256快、密钥短、现代标准
信创/国密项目SM2合规要求
兼容老系统RSA-2048兼容性更稳妥
SSH 密钥Ed25519OpenSSH 默认推荐
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
数据加密非对称加密有长度限制,大数据用数字信封

上一篇对称加密:AES 与 SM4 模式选择指南

下一篇数字信封与密钥协商:SM2+SM4、ECDH、TLS