前言
SM2是中國國家密碼管理局發布的橢圓曲線公鑰密碼算法標準(GB/T 32918),屬于國密算法體系。與RSA和ECDSA相比,SM2在相同安全強度下密鑰更短、計算效率更高。本文將介紹如何在Java中實現SM2的密鑰生成、數字簽名、驗簽、加密及解密功能。
一、結果驗證
1.代碼運行結果
1.1 不帶id簽名驗簽代碼運行結果
1.2 帶id簽名驗簽代碼運行結果
1.3 SM2加密解密代碼運行結果
2.工具驗證結果
2.1 不帶id簽名驗簽工具運行結果
2.2 帶id簽名驗簽工具運行結果
2.3 SM2加密解密工具運行結果
二、SM2簽名原理
SM2簽名過程的核心是利用私鑰對消息進行簽名,生成簽名值 (r, s)
。具體步驟如下:
-
計算消息的哈希值
使用SM3哈希算法對消息M
進行哈希處理,得到哈希值e
。 -
生成隨機數
選擇一個隨機數k
,滿足1 < k < n
,其中n
是橢圓曲線的階。 -
計算橢圓曲線點
使用隨機數k
計算橢圓曲線上的點Q = kG
,其中G
是橢圓曲線的基點。取點Q
的x
坐標x1
。 -
計算簽名值
r
計算r = (e + x1) mod n
。如果r = 0
或r + k = n
,則重新選擇隨機數k
。 -
計算簽名值
s
計算s = (1 + d)^{-1} * (k - r * d) mod n
,其中d
是私鑰。 -
輸出簽名結果
簽名結果為(r, s)
,通常以字節數組的形式存儲和傳輸。
三、SM2驗簽原理
SM2驗簽過程的核心是利用公鑰驗證簽名的有效性。具體步驟如下:
-
計算消息的哈希值
使用SM3哈希算法對消息M
進行哈希處理,得到哈希值e
。 -
計算值
t
計算t = (r + s) mod n
,其中r
和s
是簽名值。 -
計算橢圓曲線點
計算點R = sG + tP
,其中G
是橢圓曲線的基點,P
是簽名者的公鑰。取點R
的x
坐標x1
。 -
驗證簽名
驗證等式r = (e + x1) mod n
是否成立。如果成立,則簽名有效;否則,簽名無效。
四、SM2簽名與驗簽的Java實現
1. 添加依賴
在pom.xml
中添加Bouncy Castle依賴:
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.70</version>
</dependency>
2. 生成密鑰對
/*** 生成SM2密鑰對。** @return 生成的密鑰對(包含公鑰和私鑰)* @throws Exception 如果密鑰生成過程中發生錯誤*/public static KeyPair generateKeyPair() throws Exception {// 添加Bouncy Castle安全提供者Security.addProvider(new BouncyCastleProvider());// 獲取SM2橢圓曲線參數(使用sm2p256v1曲線)ECParameterSpec sm2Spec = ECNamedCurveTable.getParameterSpec("sm2p256v1");// 創建EC密鑰對生成器實例KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC", "BC");// 初始化密鑰對生成器,指定橢圓曲線參數和隨機數生成器kpg.initialize(sm2Spec, new SecureRandom());// 生成密鑰對并返回return kpg.generateKeyPair();}
3. 簽名不帶ID
/*** 使用SM2算法進行簽名(不使用用戶ID)。** @param data 待簽名的數據(字節數組)* @param privateKey 簽名使用的私鑰* @return 簽名結果(字節數組)* @throws Exception 如果簽名過程中發生錯誤*/public static String signNoId(byte[] data, PrivateKey privateKey) throws Exception {// 創建SM2簽名實例,指定使用SM3哈希算法Signature signature = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME);// 初始化簽名器,使用私鑰signature.initSign(privateKey);// 更新待簽名的數據signature.update(data);// 生成簽名byte[] signatureBytes = signature.sign();// 解析 DER 編碼的簽名結果ASN1Sequence sequence = ASN1Sequence.getInstance(signatureBytes);BigInteger r = ASN1Integer.getInstance(sequence.getObjectAt(0)).getValue();BigInteger s = ASN1Integer.getInstance(sequence.getObjectAt(1)).getValue();// 打印 r 和 s 的值System.out.println("r 的十六進制值: " + r.toString(16));System.out.println("s 的十六進制值: " + s.toString(16));// 將 r 和 s 拼接為 64 字節的簽名結果byte[] rBytes = to32Bytes(r);byte[] sBytes = to32Bytes(s);byte[] rawSignature = new byte[64];System.arraycopy(rBytes, 0, rawSignature, 0, 32);System.arraycopy(sBytes, 0, rawSignature, 32, 32);// 生成簽名并返回return Hex.toHexString(rawSignature);}
4. 驗簽不帶ID
/*** 驗證SM2簽名(不使用用戶ID)** @param data 待驗證的數據(明文)* @param signature 簽名數據(字節數組)* @param publicKey 公鑰* @return 驗簽結果(true表示成功,false表示失敗)* @throws Exception 如果驗簽過程中發生錯誤*/public static boolean verifyNoId(byte[] data, byte[] signature, PublicKey publicKey) throws Exception {// 初始化SM2簽名算法(使用SM3哈希算法)Signature verifier = Signature.getInstance(GMObjectIdentifiers.sm2sign_with_sm3.toString(), BouncyCastleProvider.PROVIDER_NAME);// 初始化驗證器,使用公鑰verifier.initVerify(publicKey);// 更新待驗證的數據verifier.update(data);// 將 r 和 s 拼接格式的簽名結果轉換為 DER 編碼格式byte[] derSignature = convertRawSignatureToDER(signature);// 驗證簽名return verifier.verify(derSignature);}
5. 測試代碼
public static void main(String[] args) throws Exception {// 生成密鑰對KeyPair keyPair = generateKeyPair();PublicKey publicKey = keyPair.getPublic();PrivateKey privateKey = keyPair.getPrivate();// 提取公鑰的 x 和 y 坐標String publicKeyX = ((ECPublicKey) publicKey).getQ().getAffineXCoord().toBigInteger().toString(16);String publicKeyY = ((ECPublicKey) publicKey).getQ().getAffineYCoord().toBigInteger().toString(16);// 拼接 x 和 y 坐標String publicKeyXY = publicKeyX + publicKeyY;System.out.println("X: " + publicKeyX);System.out.println("Y: " + publicKeyY);//System.out.println("公鑰: " + publicKeyXY);// 打印私鑰的十六進制表示BigInteger privateKeyD = ((ECPrivateKey) privateKey).getD();System.out.println("私鑰HEX: " + privateKeyD.toString(16));// 待簽名數據String data = "12345";String newData = "1234567";byte[] dataBytes = data.getBytes();//System.out.printf("原文: "+data);byte[] newDat = newData.getBytes();//System.out.printf("原文修改: "+newData);// 簽名String signature = signNoId(dataBytes, privateKey);System.out.println("簽名結果: " + signature);// 驗簽boolean isValid = verifyNoId(dataBytes, Hex.decode(signature), publicKey);System.out.println("驗簽值: " + isValid);// 修改原文驗簽boolean isVa = verifyNoId(newDat, Hex.decode(signature), publicKey);System.out.println("修改原文驗簽結果: " + isVa);System.out.printf("==========================================================: ");// 簽名帶idString dataID = "12345";String dataNew = "123456";String userId ="1234567812345678";String signatureId = signWithID(privateKey, publicKey, dataID, userId);System.out.println("帶id簽名結果: " + signatureId);// 驗簽帶idboolean isValidId = verifyWithID(publicKey, dataID, userId, Hex.decode(signatureId));System.out.println("帶id驗簽值: " + isValidId);// 驗簽帶id原文修改驗證boolean isValidIdNew = verifyWithID(publicKey, dataNew, userId, Hex.decode(signatureId));System.out.println("帶id驗簽值原文修改: " + isValidIdNew);}
五 、SM2帶ID簽名與驗簽Java實現
SM2簽名標準要求計算哈希值時包含用戶身份標識(ID),默認ID為空字符串。但在實際應用中(如金融場景),需明確指定用戶ID(如身份證號、手機號等)。以下是Java實現方法:
1.算法原理解析
SM2簽名算法中,用戶ID(即userId
)被用于生成一個關鍵值 ZA,其目的是將用戶身份與密鑰綁定,增強安全性。具體步驟如下:
-
ZA值計算
ZA通過哈希函數(SM3)生成,計算公式為:復制
ZA = HASH( ENTLA || ID || a || b || xG || yG || xA || yA )
- ENTLA:用戶ID的比特長度(占2字節,如ID長度256比特則值為0x0100)
- ID:用戶自定義標識(如身份證號、手機號)
- a, b:橢圓曲線方程參數
- (xG, yG):橢圓曲線基點坐標
- (xA, yA):簽名方的公鑰坐標
-
簽名過程
- 輸入:私鑰、待簽名數據
M
、用戶ID - 輸出:簽名結果
(r, s)
1. 計算 ZA(如上) 2. 計算 e = HASH(ZA || M) 3. 生成隨機數k,計算橢圓曲線點(x1, y1) = [k]G 4. 計算 r = (e + x1) mod n 5. 若r=0或r+k=n,則重新生成k 6. 計算 s = ((1 + d)^?1 * (k ? r * d)) mod n(d為私鑰) 7. 返回(r, s)
- 輸入:私鑰、待簽名數據
-
驗簽過程
- 輸入:公鑰、簽名
(r, s)
、原始數據M
、用戶ID - 輸出:驗簽結果(true/false)
1. 校驗r和s是否在[1, n-1]范圍內 2. 計算 ZA(與簽名方相同ID) 3. 計算 e = HASH(ZA || M) 4. 計算 t = (r + s) mod n 5. 計算橢圓曲線點(x1, y1) = [s]G + [t]P(P為公鑰) 6. 驗證 R = (e + x1) mod n 是否等于r
- 輸入:公鑰、簽名
2.代碼實現
- 帶ID的簽名
/*** 使用 SM2 算法進行帶用戶 ID 的簽名,并返回 r 和 s 的拼接結果** @param privateKey 私鑰* @param publicKey 公鑰* @param data 待簽名的數據* @param userId 用戶 ID(如企業編號、用戶身份證等)* @return 簽名結果(Hex 編碼的字符串,64 字節)* @throws Exception 如果簽名過程中發生錯誤*/public static String signWithID(PrivateKey privateKey, PublicKey publicKey, String data, String userId) throws Exception {// 將私鑰轉換為 ECPrivateKeyParametersECPrivateKeyParameters ecPrivateKey = convertPrivateKey(privateKey);// 將公鑰轉換為 ECPublicKeyParametersECPublicKeyParameters ecPublicKey = convertPublicKey(publicKey);// 創建 SM2 簽名器SM2Signer signer = new SM2Signer(new SM3Digest());// 初始化簽名器,傳入私鑰和用戶 IDsigner.init(true, new ParametersWithID(ecPrivateKey, userId.getBytes(StandardCharsets.UTF_8)));// 更新待簽名的數據signer.update(data.getBytes(StandardCharsets.UTF_8), 0, data.length());// 生成簽名byte[] signResult = signer.generateSignature();// 解析 DER 編碼的簽名結果ASN1Sequence sequence = ASN1Sequence.getInstance(signResult);BigInteger r = ASN1Integer.getInstance(sequence.getObjectAt(0)).getValue();BigInteger s = ASN1Integer.getInstance(sequence.getObjectAt(1)).getValue();// 打印 r 和 s 的值System.out.println("r 的十六進制值: " + r.toString(16));System.out.println("s 的十六進制值: " + s.toString(16));// 將 r 和 s 拼接為 64 字節的簽名結果byte[] rBytes = to32Bytes(r);byte[] sBytes = to32Bytes(s);byte[] rawSignature = new byte[64];System.arraycopy(rBytes, 0, rawSignature, 0, 32);System.arraycopy(sBytes, 0, rawSignature, 32, 32);// 返回 Hex 編碼的簽名結果return Hex.toHexString(rawSignature);}
- 帶ID的驗簽
/*** 使用 SM2 算法進行帶用戶 ID 的驗簽** @param publicKey 公鑰* @param data 待驗簽的數據* @param userId 用戶 ID(必須與簽名時一致)* @param signature 簽名結果(字節數組,r 和 s 的拼接格式)* @return 驗簽結果(true 表示驗簽成功,false 表示驗簽失敗)* @throws Exception 如果驗簽過程中發生錯誤*/public static boolean verifyWithID(PublicKey publicKey, String data, String userId, byte[] signature) throws Exception {// 將公鑰轉換為 ECPublicKeyParametersECPublicKeyParameters ecPublicKey = convertPublicKey(publicKey);// 創建 SM2 驗簽器SM2Signer verifier = new SM2Signer(new SM3Digest());// 初始化驗簽器,傳入公鑰和用戶 IDverifier.init(false, new ParametersWithID(ecPublicKey, userId.getBytes(StandardCharsets.UTF_8)));// 更新待驗簽的數據verifier.update(data.getBytes(StandardCharsets.UTF_8), 0, data.length());// 將 r 和 s 拼接格式的簽名結果轉換為 DER 編碼格式byte[] derSignature = convertRawSignatureToDER(signature);// 驗簽return verifier.verifySignature(derSignature);}
六、SM2加密與解密Java實現
1.SM2加密原理
-
SM2加密過程主要基于橢圓曲線的數學特性,通過公鑰對明文數據進行加密。具體步驟如下:
- 選擇橢圓曲線參數
- 使用橢圓曲線參數(如
sm2p256v1
),這些參數包括橢圓曲線方程的系數、基點G
以及基點的階n
。
- 使用橢圓曲線參數(如
- 生成隨機數
k
- 選擇一個隨機數
k
(1 < k < n
),用于生成橢圓曲線上的一個點R = [k]G
。
- 選擇一個隨機數
- 計算密文
- 使用公鑰
P
(簽名方的公鑰)和隨機點R
,根據SM2的加密公式計算密文。SM2支持兩種加密模式:- C1C3C2模式:密文格式為
C1 || C3 || C2
。 - C1C2C3模式:密文格式為
C1 || C2 || C3
。
- C1C3C2模式:密文格式為
- 其中:
C1
是隨機點R
的編碼。C2
是經過加密的明文數據。C3
是消息的哈希值,用于驗證數據完整性。
- 使用公鑰
- 輸出密文
- 將計算得到的
C1
、C2
和C3
拼接成最終的密文。
- 將計算得到的
- 選擇橢圓曲線參數
2.SM2解密原理
解密過程是加密的逆操作,使用私鑰對密文進行解密,還原出原始明文。具體步驟如下:
- 解析密文
- 將密文拆分為
C1
、C2
和C3
。
- 將密文拆分為
- 計算橢圓曲線點
- 使用私鑰
d
和C1
中的點R
,根據SM2的解密公式計算橢圓曲線上的一個點。
- 使用私鑰
- 還原明文
- 利用橢圓曲線的數學特性,結合
C1
、C2
和C3
,通過解密公式還原出原始明文。
- 利用橢圓曲線的數學特性,結合
- 驗證數據完整性
- 使用
C3
驗證解密后的數據是否被篡改。
- 使用
3.代碼實現
- 添加依賴
在pom.xml
中添加Bouncy Castle依賴:
<dependency><groupId>org.bouncycastle</groupId><artifactId>bcprov-jdk15on</artifactId><version>1.70</version>
</dependency>
- 生成密鑰對
/*** 生成SM2密鑰對*/public static KeyPair generateSM2KeyPair() throws Exception {// 獲取SM2橢圓曲線參數X9ECParameters ecParameters = GMNamedCurves.getByName("sm2p256v1");ECParameterSpec ecSpec = new ECParameterSpec(ecParameters.getCurve(),ecParameters.getG(),ecParameters.getN(),ecParameters.getH());// 創建密鑰對生成器KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC", "BC");keyPairGenerator.initialize(ecSpec, new SecureRandom());return keyPairGenerator.generateKeyPair();}
- 公鑰加密
/*** SM2加密(C1C3C2模式)* @param publicKey 公鑰* @param data 待加密數據* @return 加密后的字節數組(C1C3C2格式)*/public static byte[] encrypt(BCECPublicKey publicKey, byte[] data) throws Exception {// 獲取橢圓曲線參數ECDomainParameters domainParams = new ECDomainParameters(publicKey.getParameters().getCurve(),publicKey.getParameters().getG(),publicKey.getParameters().getN());// 創建加密引擎(默認輸出C1C3C2格式)SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2);// 初始化加密引擎ECPublicKeyParameters pubKeyParams = new ECPublicKeyParameters(publicKey.getQ(),domainParams);engine.init(true, new ParametersWithRandom(pubKeyParams, new SecureRandom()));return engine.processBlock(data, 0, data.length);}
- 私鑰解密
/*** SM2解密(C1C3C2模式)* @param privateKey 私鑰* @param cipherData 密文數據(C1C3C2格式)* @return 解密后的字節數組*/public static byte[] decrypt(BCECPrivateKey privateKey, byte[] cipherData) throws Exception {// 獲取橢圓曲線參數ECDomainParameters domainParams = new ECDomainParameters(privateKey.getParameters().getCurve(),privateKey.getParameters().getG(),privateKey.getParameters().getN());// 創建解密引擎(設置為C1C3C2模式)SM2Engine engine = new SM2Engine(SM2Engine.Mode.C1C3C2);// 初始化解密引擎ECPrivateKeyParameters priKeyParams = new ECPrivateKeyParameters(privateKey.getD(),domainParams);engine.init(false, priKeyParams);return engine.processBlock(cipherData, 0, cipherData.length);}
注意事項
- 密鑰管理:私鑰需安全存儲(如密碼機或云密碼機等)
- 性能優化:加解密大數據時建議使用SM4對稱加密配合SM2密鑰交換
- ID編碼:
userId.getBytes()
需與業務方約定編碼格式(如UTF-8、HEX等) - 長度限制:ID長度建議不超過65535字節(規范限制)
- 跨系統交互:與其他系統(如C++、Go)對接時需確認ID處理邏輯一致性
總結
希望這篇文章對你有所幫助!如果覺得不錯,別忘了關注哦!