SM 國密算法踩坑指南
各位,好久不見~
最近接手網(wǎng)聯(lián)的國密改造項(xiàng)目,由于對國密算法比較陌生,前期碰到了一系列國密算法加解密的問題。
所以這次總結(jié)一下,分享這個過程遇到的問題,希望幫到大家。
國密
什么是國密算法?
國密就是一個口頭上簡稱,官方名稱是國家商用密碼,使用拼音縮寫 SM,它是用于商用的、不涉及國家秘密的密碼技術(shù)。
那說起密碼技術(shù),大家一定很熟悉 MD5,AES,RSA 等算法,這些都是通用國際標(biāo)準(zhǔn)算法。
而國密其實(shí)就是這些國際算法國產(chǎn)化的代替方案,與國際算法對應(yīng)關(guān)系如下:

這次國密改造項(xiàng)目使用的就是 SM2 國密算法。
SM2算法
SM2 ?國密算法是一種非對稱加密算法,基于 ECC(橢圓加密算法), SM2 算法對標(biāo)我們常用的國際算法 RSA。
但是 SM2 算法由于基于 ECC,簽名速度與秘鑰速度都快于 RSA。另外 SM2 采用 ECC 256 位,安全強(qiáng)度比 RSA 2048 位更高,且運(yùn)算速度同樣也高于 ESA。
熟悉 RSA 算法同學(xué)應(yīng)該知道,非對稱加密算法,會有一對公私鑰。
私鑰可以用于加簽,公鑰可以用于驗(yàn)簽。
公鑰可以用于加密,私鑰可以用于解密
同樣 SM2 算法也有一對公私鑰,它們的長度遠(yuǎn)遠(yuǎn)小于 RSA 公私鑰。
SM2 私鑰,一個大于等于 1 且小于 n-1的整數(shù)(n 為 sm2 算法的階),長度為 256 位,即 32 個字節(jié),通常會用 16 進(jìn)制表示。
SM2 私鑰:B17EACC0BB629AB92C591287F2FA4589D10CD1E13BD4BDFDC9589A940F937C7C
SM2 公鑰,SM2 橢圓曲線上的一個點(diǎn),由橫坐標(biāo)與縱坐標(biāo)兩個分量構(gòu)成,每個長度分量長度為 256 位,通常也用 16 進(jìn)制表示。
SM2 公鑰一般有兩種表示方法:
- X|Y,即 X與 Y兩個分量拼接在一起,總共 64 個字節(jié)。
????- 04|X|Y,有些給出公鑰與上面格式一樣,只不過前面增加 04,代表非壓縮,整個公鑰長度變成 65 字節(jié)。
- 分開展示,公鑰 X,公鑰 Y
公鑰 X|Y:53B97D723AA4CEAC97A13B8C50AA53D40DE36960CFC3A3D7929FD54F39F824ED5A4A27AF871AD62C25C75C9D75C75A0907C565A78B805E9502E616C4E77F3B42
公鑰?X:53B97D723AA4CEAC97A13B8C50AA53D40DE36960CFC3A3D7929FD54F39F824ED
公鑰?Y:5A4A27AF871AD62C25C75C9D75C75A0907C565A78B805E9502E616C4E77F3B42
SM2 算法與 RSA 算法一樣,可以用于數(shù)字簽名,也可以用于加密場景,下面我們來看下數(shù)字簽名場景下 SM2 算法原理。
SM2 數(shù)字簽名算法
SM2 簽名算法還是比較復(fù)雜,這里只截取數(shù)字簽名的生成、驗(yàn)證算法原理。
詳細(xì)文檔可以搜索:『GB/T32918.2—2016 信息安全技術(shù) SM2橢圓曲線公鑰 密碼算法 第2部分:數(shù)字簽名算法』
sm2 加簽
數(shù)字簽名生成算法,即加簽流程:

加簽流程圖如下:

sm2 驗(yàn)簽
數(shù)字簽名驗(yàn)證算法,即驗(yàn)簽流程:

驗(yàn)簽流程圖:

SM2 簽名數(shù)據(jù)
上面加簽流程我們可以看到,SM2 加簽之后產(chǎn)生的簽名為(R,S),這一點(diǎn)與 RSA算法不同,RSA 算法加簽之后簽名就是一個值。
SM2 簽名一般有兩種數(shù)據(jù)格式,國標(biāo)(GM/T 0009-2012 SM2 密碼算法使用規(guī)范)規(guī)定簽名數(shù)據(jù)格式,使用** ASN.1** 格式定義,具體格式如下:

通常使用硬件加密機(jī)加簽產(chǎn)生的數(shù)字?jǐn)?shù)字簽名將會使用這種格式。
SM2 數(shù)字簽名另外一種方式就比較簡單,格式為R|S,即直接將兩者拼接在一起表示。
通常使用軟件加密產(chǎn)生數(shù)字簽名將會使用這種數(shù)據(jù)格式。
SM2 公鑰加密算法
SM2 加密算法也是比較復(fù)雜,這里只截取加密、解密原理
詳細(xì)文檔可以搜索:『GB/T 32918.4—2016 信息安全技術(shù) SM2橢圓曲線公鑰 密碼算法 第4部分:公鑰加密算法』
sm2 加密算法


SM2解密算法


SM2 加密數(shù)據(jù)
SM2 加密數(shù)據(jù)將會產(chǎn)生三個值:
C1 為隨機(jī)產(chǎn)生的公鑰
C2 為密文,與明文長度等長
C3 為 SM3 算法對明文數(shù)計算得到消息摘要,長度固定為 256 位
SM2 加密數(shù)據(jù)一般有兩種數(shù)據(jù)格式,國標(biāo)(GM/T 0009-2012 SM2 密碼算法使用規(guī)范)規(guī)定加密數(shù)據(jù)格式,使用 ASN.1格式定義,具體格式如下:

通常使用硬件加密機(jī)加簽產(chǎn)生的加密數(shù)據(jù)將會使用這種格式。
SM2 加密數(shù)據(jù)另外一種方式就比較簡單,格式為 C1|C3|C2,即直接將三者拼接在一起表示。
通常使用軟件加密產(chǎn)生數(shù)字簽名將會使用這種數(shù)據(jù)格式。
這里需要注意一點(diǎn),有些加密數(shù)據(jù)格式也會使用 C1|C2|C3,加解密之間需要注意格式。
SM2 相關(guān)問題
SM2 合規(guī)上通常需要使用硬件加密機(jī),這種方案直接調(diào)用廠商的提供加密接口就好了,安全又比較簡單。
但是這個方案需要采購相關(guān)硬件,成本比較高。
SM2 算法也可以使用軟加密的方案,底層主要依賴 Bouncy Castle 庫。
軟加密的方案在于開箱即用,開發(fā)成本較低。
軟件加密方案,Bouncy Castle 庫封裝的工具類,已經(jīng)大大降低國密開發(fā)的難度。
如果不想開發(fā)可以直接使用 HuTool 工具類:
https://hutool.cn/docs/#/crypto/國密算法工具-SmUtil?id=介紹
如果想自己封裝的話,可以參考下面文章
https://blog.csdn.net/pridas/article/details/86118774
https://github.com/xjfuuu/SM2_SM3_SM4Encrypt
不同的加密方案,加簽、加密輸出的結(jié)果格式不同。如果直接拿硬件加密方案生成加密結(jié)果,然后直接使用軟件加密方案去解密,就會導(dǎo)致解密失敗。
SM2 算法聯(lián)調(diào)測試的時候,這一點(diǎn)比較頭疼,下面講下這次國密改造中碰到一些問題。
SM2 公私鑰讀取
SM2 如果用到數(shù)字簽名,也用到加密的話,這個情況下我們就需要向 CA 機(jī)構(gòu),例如 CFCA,申請國密雙證書。
CFCA 申請結(jié)果如下:

SM2 雙證書,分為簽名證書,加密證書。我們申請獲取兩個證書需要給到對手方,同樣對手方也需要把他們雙證書給我們。
這個過程簽名需要使用自身簽名證書對應(yīng)的私鑰,驗(yàn)簽使用對手方簽名證書包含的公鑰。
加密使用對手方的加密證書包含的公鑰,解密需要使用自身加密證書的對應(yīng)的私鑰。
這個流程比 RSA 單證書的情況復(fù)雜了很多。
我們拿到數(shù)字證書之后,如果需要從里面提取公鑰,擴(kuò)在下面的網(wǎng)站在線解析。
https://www.gmssl.cn/gmssl/index.jsp
下圖選中就是證書中包含的公鑰

SM2 數(shù)字簽名問題
SM2 國標(biāo)規(guī)定的加簽數(shù)據(jù)格式使用 ASN.1,所以部分硬件廠商加簽輸出格式就是這種。

但是如果我們使用 BC 庫加簽輸出格式直接使用 R|S。
如果是這種情況,我們就需要在明文 R|S 與 ASN.1 之間做相互的轉(zhuǎn)換。
最新版本的 BC 庫,已經(jīng)提供轉(zhuǎn)換的換方式。
????????<dependency>
????????????<groupId>org.bouncycastlegroupId>
????????????<artifactId>bcprov-jdk15to18artifactId>
????????????<version>1.69version>
????????dependency>
可以使用下面的方式,輸出簽名結(jié)果為 ASN.1 格式
new?SM2Signer(StandardDSAEncoding.INSTANCE,?new?SM3Digest());
也可以使用下面這種方式嗎,輸出簽名結(jié)果為 R|S
????????//?前面輸出?R|S
????????new?SM2Signer(PlainDSAEncoding.INSTANCE,?new?SM3Digest());
如果是低版本,只能通過自己寫代碼轉(zhuǎn)換了。代碼就不貼了,參考下面這篇文章:
https://blog.csdn.net/pridas/article/details/86118774
SM2 加密問題
SM2 加密結(jié)果,國標(biāo)規(guī)定使用 ASN.1 格式,所以部分硬件廠商加密結(jié)果使用這種格式。

但是 BC 庫加密的結(jié)果是 C1|C3|C2,所以我們需要做一層轉(zhuǎn)換。
轉(zhuǎn)換代碼如下:
將ASN1格式轉(zhuǎn)成c1c3c2
????/**
?????*?將ASN1格式轉(zhuǎn)成c1c3c2
?????*
?????*?@param?asn1
?????*?@return
?????*?@throws?IOException
?????*/
????public?static?byte[]?changeAsn1ToC1C3C2(byte[]?asn1)?throws?IOException?{
????????ASN1InputStream?aIn?=?new?ASN1InputStream(asn1);
????????ASN1Sequence?seq?=?(ASN1Sequence)?aIn.readObject();
????????BigInteger?x?=?ASN1Integer.getInstance(seq.getObjectAt(0)).getValue();
????????BigInteger?y?=?ASN1Integer.getInstance(seq.getObjectAt(1)).getValue();
????????byte[]?c3?=?ASN1OctetString.getInstance(seq.getObjectAt(2)).getOctets();
????????byte[]?c2?=?ASN1OctetString.getInstance(seq.getObjectAt(3)).getOctets();
????????//?不壓縮
????????ECPoint?c1Point?=??GMNamedCurves.getByName("sm2p256v1").getCurve().createPoint(x,?y);
????????byte[]?c1?=?c1Point.getEncoded(false);
????????return?ArrayUtil.addAll(c1,?c3,?c2);
????}
將 c1c3c2格式轉(zhuǎn)成ASN1
private?static?final?int?C1_LEN?=?65;
????private?static?final?int?C3_LEN?=?32;
????/**
?????*?將c1c3c2轉(zhuǎn)成標(biāo)準(zhǔn)的ASN1格式
?????*
?????*?@param?c1c3c2
?????*?@return
?????*?@throws?IOException
?????*/
????public?static?byte[]?changeC1C3C2ToAsn1(byte[]?c1c3c2)?throws?IOException?{
????????byte[]?c1?=?Arrays.copyOfRange(c1c3c2,?0,?C1_LEN);
????????byte[]?c3?=?Arrays.copyOfRange(c1c3c2,?C1_LEN,?C1_LEN?+?C3_LEN);
????????byte[]?c2?=?Arrays.copyOfRange(c1c3c2,?C1_LEN?+?C3_LEN,?c1c3c2.length);
????????byte[]?c1X?=?Arrays.copyOfRange(c1,?1,?33);
????????byte[]?c1Y?=?Arrays.copyOfRange(c1,?33,?65);
????????BigInteger?r?=?new?BigInteger(1,?c1X);
????????BigInteger?s?=?new?BigInteger(1,?c1Y);
????????ASN1Integer?x?=?new?ASN1Integer(r);
????????ASN1Integer?y?=?new?ASN1Integer(s);
????????DEROctetString?derDig?=?new?DEROctetString(c3);
????????DEROctetString?derEnc?=?new?DEROctetString(c2);
????????ASN1EncodableVector?v?=?new?ASN1EncodableVector();
????????v.add(x);
????????v.add(y);
????????v.add(derDig);
????????v.add(derEnc);
????????DERSequence?seq?=?new?DERSequence(v);
????????return?seq.getEncoded(ASN1Encoding.DER);
????}
這里需要注意一下,低版本的 BC 庫加密結(jié)果是 C1|C2|C3,這就很坑了,現(xiàn)在很多都是 C1|C3|C2,這就又需要做轉(zhuǎn)換。
轉(zhuǎn)換代碼參考這篇文章:
https://blog.csdn.net/pridas/article/details/86118774

總結(jié)
SM2 國密算法屬于非對稱加密算法,理解起來不是很難。
但是由于普及程度較低,現(xiàn)有資料太少,所以開發(fā)來還是比較復(fù)雜,碰到的問題也比較多。
建議大家開發(fā)之前可以先了解一下國密 SM2 相關(guān)國標(biāo)規(guī)范,不需要很深入了解整個原理,但是需要知道國密 SM2 與 RSA 的區(qū)別點(diǎn)。
國密算法實(shí)現(xiàn)上,軟加密我們可以直接用 BC 庫,硬加密直接使用廠商提供的相關(guān)接口,這一點(diǎn)難度還好。
國密最大難度是,各個硬件與軟加密,使用規(guī)范不一致,輸出格式不一致,這就導(dǎo)致我們聯(lián)調(diào)過程,加簽/驗(yàn)簽,加密/解密失敗。
這就比較蛋疼,所以調(diào)試雙方國密算法一致性過程中,建議先確認(rèn)加簽、加密輸出格式,搞清楚這個,聯(lián)調(diào)就比較簡單了。
最后,祝大家對接國密算法順利~
幫助資料
https://www.cnblogs.com/xinzhao/p/8963724.html
https://blog.csdn.net/pridas/article/details/86118774
https://github.com/xjfuuu/SM2_SM3_SM4Encrypt
