JAVA实现国密算法SM2/SM3/SM4签名与验签(基于 BouncyCastle)

内容分享4天前发布
0 0 0

密码学算法分类指南[
国密算法全解析
Java实现国密算法 SM2 /SM3 /SM4加解密(基于 BouncyCastle)

环境要求:
JDK 17Maven 3BouncyCastle(1.78 )

先上个结论, SM2 是三种算法中唯一真正适用于数字签名的算法

一、加解密与签名验签的区别

贴代码之前,先说一下加解密与签名验签的区别:

加解密:用于保密通信,使用公钥加密,私钥解密签名验签:用于身份验证和数据完整性校验, 强调真实性、完整性以及不可否认性。使用私钥签名,公钥验签

举个栗子:

1. 加解密示例

A 要给 B 寄一份重要的文件快递,但怕被别人偷看,便使用了一个只能用 B 钥匙打开的快递箱。 如图:

JAVA实现国密算法SM2/SM3/SM4签名与验签(基于 BouncyCastle)

B 提前公开了开箱钥匙(公钥);A 把文件放进快递箱里,并用这把钥匙锁住(公钥加密);A 寄出这个快递箱(密文);B 收到后,用私钥打开;只有 B 能看到内容,别人即使拦截也无法打开。

2. 签名验签示例

A 寄文件快递时,在包裹外贴上了专属的封条(签名)。B 收到包裹后,使用 A 的公开印章验签,判断这个包括是否是 A 寄出的,内容是否被修改过。

JAVA实现国密算法SM2/SM3/SM4签名与验签(基于 BouncyCastle)

A 写好信件后,用自己专属的签名封条(私钥)封住包裹;B 接收包裹, 从公开平台获取A 的印章样式(公钥);B 比对签名是否一致;如果验证通过,B 可以确定包裹是 A 发出的,内容也没被改过。

二、 Maven 依赖配置


pom.xml
中添加如下依赖:


<!-- 1.78 -->
<dependency>
  <groupId>org.bouncycastle</groupId>
  <artifactId>bcprov-jdk18on</artifactId>
  <version>1.78</version>
</dependency>

三. 签名工具类

工具类说明:

工具类基于 Bouncy Castle 实现;SM2 用于非对称签名验签;SM3 用于哈希校验(非数字签名);SM4 用于对称加密,可用于实现“加密认证”。

Bouncy Castle 中,SM4Engine 默认使用 ECB 模式 。类
SmBaseUtils

SmKeyGenUtils
和密钥对生成在上一篇文章中,这里就不贴了。

代码如下:


import org.bouncycastle.crypto.digests.SM3Digest;

import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Arrays;
import java.util.Base64;

/***
 * 国密算法签名工具类.
 *
 * @author shijiangyong
 * @date 2025/11/5 10:23
 **/
public class SmAlgorithmSignUtils extends SmBaseUtils{

    /**
     * SM2 签名(非对称,基于公钥/私钥).
     *
     * @param privateKey key
     * @param valueToDigest 数据
     * @return 结果
     */
    public static String sm2Sign(final String privateKey, final String valueToDigest) {
        try {
            byte[] privateKeyBytes = Base64.getDecoder().decode(privateKey);
            PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
            PrivateKey key = keyFactory.generatePrivate(keySpec);

            Signature signature = Signature.getInstance("SM3withSM2", "BC");
            signature.initSign(key);
            signature.update(valueToDigest.getBytes(StandardCharsets.UTF_8));
            byte[] signed = signature.sign();
            return Base64.getEncoder().encodeToString(signed);
        } catch (Exception e) {
            throw new RuntimeException("SM2 sign error", e);
        }
    }

    /**
     * SM2 验签.
     *
     * @param publicKey publicKey
     * @param value 验签数据
     * @param signatureStr 签名值
     * @return 结果
     */
    public static boolean sm2Verify(final String publicKey, final String value, final String signatureStr) {
        try {
            byte[] publicKeyBytes = Base64.getDecoder().decode(publicKey);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(publicKeyBytes);
            KeyFactory keyFactory = KeyFactory.getInstance("EC", "BC");
            PublicKey key = keyFactory.generatePublic(keySpec);

            Signature signature = Signature.getInstance("SM3withSM2", "BC");
            signature.initVerify(key);
            signature.update(value.getBytes(StandardCharsets.UTF_8));
            return signature.verify(Base64.getDecoder().decode(signatureStr));
        } catch (Exception e) {
            throw new RuntimeException("SM2 verify error", e);
        }
    }


    /**
     * sm3签名.sm3签名不等于数字签名,非标准签名算法。
     * 哈希+密钥的方式(类似简化版的 HMAC)
     *
     * @param key
     * @param valueToDigest 数据
     * @return 加密结果
     */
    public static String sm3WithKeyHash(final String key, final String valueToDigest) {
        SM3Digest digest = new SM3Digest();
        byte[] input = (valueToDigest + key).getBytes(StandardCharsets.UTF_8);
        digest.update(input, 0, input.length);
        byte[] hash = new byte[digest.getDigestSize()];
        digest.doFinal(hash, 0);
        return Base64.getEncoder().encodeToString(hash);
    }

    /**
     *  sm3 签名认证.
     *
     * @param key
     * @param data
     * @param expectedHash
     * @return 结果
     */
    public static boolean sm3SimpleVerify(final String key, final String data, final String expectedHash) {
        String actualHash = sm3WithKeyHash(key, data);
        return actualHash.equalsIgnoreCase(expectedHash);
    }

    // ========== 通用加密/解密方法 ==========

    /**
     * 通用加密方法.
     *
     * @param mode  模式
     * @param key 密钥
     * @param iv 初始向量
     * @param data
     * @return 加密结果
     */
    private static String sm4Encrypt(String mode, String key, String iv, String data) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(key);
            String transformation = "SM4/" + mode + "/PKCS5Padding";
            Cipher cipher = Cipher.getInstance(transformation, "BC");
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "SM4");

            if ("CBC".equalsIgnoreCase(mode)) {
                byte[] ivBytes = Base64.getDecoder().decode(iv);
                IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
                cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
            } else {
                cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            }

            byte[] encrypted = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            throw new RuntimeException("SM4 " + mode + " encrypt error", e);
        }
    }

    /**
     * 解密、验签比较.
     *
     * @param mode
     * @param key
     * @param iv
     * @param data
     * @param encryptedBase64
     * @return 结果
     */
    private static boolean sm4Decrypt(String mode, String key, String iv, String data, String encryptedBase64) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(key);
            String transformation = "SM4/" + mode + "/PKCS5Padding";
            Cipher cipher = Cipher.getInstance(transformation, "BC");
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "SM4");

            if ("CBC".equalsIgnoreCase(mode)) {
                byte[] ivBytes = Base64.getDecoder().decode(iv);
                IvParameterSpec ivSpec = new IvParameterSpec(ivBytes);
                cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec);
            } else {
                cipher.init(Cipher.DECRYPT_MODE, keySpec);
            }

            byte[] decrypted = cipher.doFinal(Base64.getDecoder().decode(encryptedBase64));
            return data.equals(new String(decrypted, StandardCharsets.UTF_8));
        } catch (Exception e) {
            return false;
        }
    }

    // ===================== SM4 通用签名与验签接口 =====================
    /**
     * SM4 通用签名(支持 ECB / CBC / GCM).
     * @param mode
     * @param key 私钥
     * @param iv
     * @param data
     * @return 签名结果
     */
    public static String sm4EncryptAndAuthenticate(String mode, String key, String iv, String data) {
        validateSm4Key(key);
        switch (mode.toUpperCase()) {
            case "ECB":
                return sm4Encrypt("ECB", key, null, data);
            case "CBC":
                validateIv(iv);
                return sm4Encrypt("CBC", key, iv, data);
            case "GCM":
                return sm4GcmEncrypt(key, data);
            default:
                throw new IllegalArgumentException("Unsupported SM4 mode: " + mode);
        }
    }

    /**
     * SM4 通用验签(支持 ECB / CBC / GCM).
     * 对称加密不具备不可否认性
     *
     * @param mode
     * @param key 公钥
     * @param iv
     * @param data
     * @param encryptedBase64
     * @return 验签结果
     */
    public static boolean sm4CheckIntegrity(String mode, String key, String iv, String data, String encryptedBase64) {
        validateSm4Key(key);
        switch (mode.toUpperCase()) {
            case "ECB":
                return sm4Decrypt("ECB", key, null, data, encryptedBase64);
            case "CBC":
                validateIv(iv);
                return sm4Decrypt("CBC", key, iv, data, encryptedBase64);
            case "GCM":
                return sm4GcmDecrypt(key, data, encryptedBase64);
            default:
                throw new IllegalArgumentException("Unsupported SM4 mode: " + mode);
        }
    }

    /**
     * 校验 SM4 密钥长度是否为 16 字节(128 位)
     *
     * @param key
     */
    private static void validateSm4Key(String key) {
        if (!SmKeyGenUtils.isValidSM4Key(key,SmBaseUtils.KEY_LENGTH)) {
            throw new IllegalArgumentException("SM4 key must be 16 bytes (128 bits)");
        }
    }

    /**
     * 校验 CBC 模式 IV 长度是否为 16 字节
     *
     * @param iv
     */
    private static void validateIv(String iv) {
        if (!SmKeyGenUtils.isValidSM4Key(iv,SmBaseUtils.KEY_LENGTH)) {
            throw new IllegalArgumentException("SM4 CBC IV must be 16 bytes");
        }
    }

    // ===================== SM4 GCM =====================

    /**
     * gcm 加密.
     *
     * @param key
     * @param data
     * @return
     */
    private static String sm4GcmEncrypt(String key, String data) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(key);
            byte[] iv = new byte[12];
            SecureRandom random = new SecureRandom();
            random.nextBytes(iv);

            Cipher cipher = Cipher.getInstance("SM4/GCM/NoPadding", "BC");
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "SM4");
            GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);

            byte[] cipherText = cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));

            // 拼接 IV + 密文
            byte[] combined = new byte[iv.length + cipherText.length];
            System.arraycopy(iv, 0, combined, 0, iv.length);
            System.arraycopy(cipherText, 0, combined, iv.length, cipherText.length);

            return Base64.getEncoder().encodeToString(combined);
        } catch (Exception e) {
            throw new RuntimeException("SM4 GCM encrypt error", e);
        }
    }

    /**
     * gcm解密.
     *
     * @param key
     * @param data
     * @param encryptedBase64
     * @return
     */
    private static boolean sm4GcmDecrypt(String key, String data, String encryptedBase64) {
        try {
            byte[] input = Base64.getDecoder().decode(encryptedBase64);
            byte[] keyBytes = Base64.getDecoder().decode(key);

            byte[] iv = Arrays.copyOfRange(input, 0, 12);
            byte[] cipherText = Arrays.copyOfRange(input, 12, input.length);

            Cipher cipher = Cipher.getInstance("SM4/GCM/NoPadding", "BC");
            SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "SM4");
            GCMParameterSpec gcmSpec = new GCMParameterSpec(128, iv);
            cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);

            byte[] decrypted = cipher.doFinal(cipherText);
            return data.equals(new String(decrypted, StandardCharsets.UTF_8));
        } catch (Exception e) {
            return false;
        }
    }
}

一般生产环境不使用 ECB 模式处理敏感数据,这个模式无法隐藏明文结构;CBC 模式需确保 IV 唯一且不可预测,推荐每次随机生成,IV 长度为 16 字节;GCM 模式推荐用于实际场景,提供认证加密(AEAD)能力,IV 长度为 12 字节;SM2 签名具备不可否认性,适用于对外接口、金融级别系统;安全性高的场景中,密钥一般使用 KMS 或者安全的配置中心管理;SM3 签名不等于数字签名,非标准签名算法。

四、测试工具类

代码说明:

SM2 测试使用预设公私钥;SM3 使用自定义 key 进行哈希校验;SM4 支持 ECB / CBC / GCM 三种模式;


import com.example.sm.utils.SmAlgorithmSignUtils;
import com.example.sm.utils.SmBaseUtils;
import com.example.sm.utils.SmKeyGenUtils;

/***
 * @title
 * @author shijiangyong
 * @date 2025/11/6 20:42
 **/
public class SignTest {
    public static void main(String[] args) {
        String str = "这个是一个signature test~!";
        // sm2
        System.out.println("=== SM2 Test ===");
        String publicKey = "MFkwEwYHKoZIzj0CAQYIKoEcz1UBgi0DQgAED4a2WmtVnR3lwae4pcX507BL5bTL9BSyFuU3Ij+zc+ppYu8DgoOsUbIvV2ROaDa6+J6FTYEtmhmchsYD/edOzw==";
        String privateKey = "MIGTAgEAMBMGByqGSM49AgEGCCqBHM9VAYItBHkwdwIBAQQg1IsF13XAUdqyUWqegCKEOTaVaCPwFrWXF0gIB5d6dMWgCgYIKoEcz1UBgi2hRANCAAQPhrZaa1WdHeXBp7ilxfnTsEvltMv0FLIW5TciP7Nz6mli7wOCg6xRsi9XZE5oNrr4noVNgS2aGZyGxgP9507P";
        String sm2Sign = SmAlgorithmSignUtils.sm2Sign(privateKey, str);
        boolean sm2Verify = SmAlgorithmSignUtils.sm2Verify(publicKey, str, sm2Sign);
        System.out.println("sm2Verify: " + sm2Verify);
        // sm3
        System.out.println("=== SM3 Test ===");
        // 定义自己的字符串即可
        String sm3Key = "testkey";
        String sm3Sign = SmAlgorithmSignUtils.sm3WithKeyHash(sm3Key, str);
        boolean sm3Verify = SmAlgorithmSignUtils.sm3SimpleVerify(sm3Key, str, sm3Sign);
        System.out.println("sm3Verify: " + sm3Verify);
        //sm4 ecb
        System.out.println("=== SM4 Test ===");
        String key = SmKeyGenUtils.generateSM4Key(SmBaseUtils.KEY_LENGTH);
        String ecbSign = SmAlgorithmSignUtils.sm4EncryptAndAuthenticate("ecb", key, null, str);
        boolean ecbVerify = SmAlgorithmSignUtils.sm4CheckIntegrity("ecb", key, null, str, ecbSign);
        System.out.println("ecbVerify: " + ecbVerify);
        // cbc
        String iv = SmKeyGenUtils.generateSM4Key(SmBaseUtils.KEY_LENGTH);
        String cbcSign = SmAlgorithmSignUtils.sm4EncryptAndAuthenticate("cbc", key, iv, str);
        boolean cbcVerify = SmAlgorithmSignUtils.sm4CheckIntegrity("cbc", key, iv, str, cbcSign);
        System.out.println("cbcVerify: " + cbcVerify);
        // gcm,实际内部生成 IV,忽略传入参数,这里只是示例gcm需要使用字节IV
        // String gcmIv = SmKeyGenUtils.generateSM4Key(SmBaseUtils.IV_LENGTH);
        String gcmSign = SmAlgorithmSignUtils.sm4EncryptAndAuthenticate("gcm", key, null, str);
        boolean gcmVerify = SmAlgorithmSignUtils.sm4CheckIntegrity("gcm", key, null, str, gcmSign);
        System.out.println("gcmVerify: " + gcmVerify);

    }
}

测试结果:

JAVA实现国密算法SM2/SM3/SM4签名与验签(基于 BouncyCastle)

五、总结

SM2:真正的数字签名算法, 适用于涉密、金融、司法等需要不可否认性的场景 ;SM3:适合用于数据完整性校验,非签名算法;SM4:用于对称加密,可配合解密比对实现简单认证,非签名算法 。

在实际业务中根据安全等级和合规要求,选用合适的算法吧。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...