SM 国密算法踩坑指南
共 5953字,需浏览 12分钟
·
2021-12-16 15:14
各位,好久不见~
最近接手网联的国密改造项目,由于对国密算法比较陌生,前期碰到了一系列国密算法加解密的问题。
所以这次总结一下,分享这个过程遇到的问题,希望帮到大家。
国密
什么是国密算法?
国密就是一个口头上简称,官方名称是国家商用密码,使用拼音缩写 SM,它是用于商用的、不涉及国家秘密的密码技术。
那说起密码技术,大家一定很熟悉 MD5,AES,RSA 等算法,这些都是通用国际标准算法。
而国密其实就是这些国际算法国产化的代替方案,与国际算法对应关系如下:
这次国密改造项目使用的就是 SM2 国密算法。
SM2算法
SM2 国密算法是一种非对称加密算法,基于 ECC(椭圆加密算法), SM2 算法对标我们常用的国际算法 RSA。
但是 SM2 算法由于基于 ECC,签名速度与秘钥速度都快于 RSA。另外 SM2 采用 ECC 256 位,安全强度比 RSA 2048 位更高,且运算速度同样也高于 ESA。
熟悉 RSA 算法同学应该知道,非对称加密算法,会有一对公私钥。
私钥可以用于加签,公钥可以用于验签。
公钥可以用于加密,私钥可以用于解密
同样 SM2 算法也有一对公私钥,它们的长度远远小于 RSA 公私钥。
SM2 私钥,一个大于等于 1 且小于 n-1的整数(n 为 sm2 算法的阶),长度为 256 位,即 32 个字节,通常会用 16 进制表示。
SM2 私钥:B17EACC0BB629AB92C591287F2FA4589D10CD1E13BD4BDFDC9589A940F937C7C
SM2 公钥,SM2 椭圆曲线上的一个点,由横坐标与纵坐标两个分量构成,每个长度分量长度为 256 位,通常也用 16 进制表示。
SM2 公钥一般有两种表示方法:
- X|Y,即 X与 Y两个分量拼接在一起,总共 64 个字节。
- 04|X|Y,有些给出公钥与上面格式一样,只不过前面增加 04,代表非压缩,整个公钥长度变成 65 字节。
- 分开展示,公钥 X,公钥 Y
公钥 X|Y:53B97D723AA4CEAC97A13B8C50AA53D40DE36960CFC3A3D7929FD54F39F824ED5A4A27AF871AD62C25C75C9D75C75A0907C565A78B805E9502E616C4E77F3B42
公钥 X:53B97D723AA4CEAC97A13B8C50AA53D40DE36960CFC3A3D7929FD54F39F824ED
公钥 Y:5A4A27AF871AD62C25C75C9D75C75A0907C565A78B805E9502E616C4E77F3B42
SM2 算法与 RSA 算法一样,可以用于数字签名,也可以用于加密场景,下面我们来看下数字签名场景下 SM2 算法原理。
SM2 数字签名算法
SM2 签名算法还是比较复杂,这里只截取数字签名的生成、验证算法原理。
详细文档可以搜索:『GB/T32918.2—2016 信息安全技术 SM2椭圆曲线公钥 密码算法 第2部分:数字签名算法』
sm2 加签
数字签名生成算法,即加签流程:
加签流程图如下:
sm2 验签
数字签名验证算法,即验签流程:
验签流程图:
SM2 签名数据
上面加签流程我们可以看到,SM2 加签之后产生的签名为(R,S),这一点与 RSA算法不同,RSA 算法加签之后签名就是一个值。
SM2 签名一般有两种数据格式,国标(GM/T 0009-2012 SM2 密码算法使用规范)规定签名数据格式,使用** ASN.1** 格式定义,具体格式如下:
通常使用硬件加密机加签产生的数字数字签名将会使用这种格式。
SM2 数字签名另外一种方式就比较简单,格式为R|S,即直接将两者拼接在一起表示。
通常使用软件加密产生数字签名将会使用这种数据格式。
SM2 公钥加密算法
SM2 加密算法也是比较复杂,这里只截取加密、解密原理
详细文档可以搜索:『GB/T 32918.4—2016 信息安全技术 SM2椭圆曲线公钥 密码算法 第4部分:公钥加密算法』
sm2 加密算法
SM2解密算法
SM2 加密数据
SM2 加密数据将会产生三个值:
C1 为随机产生的公钥
C2 为密文,与明文长度等长
C3 为 SM3 算法对明文数计算得到消息摘要,长度固定为 256 位
SM2 加密数据一般有两种数据格式,国标(GM/T 0009-2012 SM2 密码算法使用规范)规定加密数据格式,使用 ASN.1格式定义,具体格式如下:
通常使用硬件加密机加签产生的加密数据将会使用这种格式。
SM2 加密数据另外一种方式就比较简单,格式为 C1|C3|C2,即直接将三者拼接在一起表示。
通常使用软件加密产生数字签名将会使用这种数据格式。
这里需要注意一点,有些加密数据格式也会使用 C1|C2|C3,加解密之间需要注意格式。
SM2 相关问题
SM2 合规上通常需要使用硬件加密机,这种方案直接调用厂商的提供加密接口就好了,安全又比较简单。
但是这个方案需要采购相关硬件,成本比较高。
SM2 算法也可以使用软加密的方案,底层主要依赖 Bouncy Castle
库。
软加密的方案在于开箱即用,开发成本较低。
软件加密方案,Bouncy Castle
库封装的工具类,已经大大降低国密开发的难度。
如果不想开发可以直接使用 HuTool
工具类:
https://hutool.cn/docs/#/crypto/国密算法工具-SmUtil?id=介绍
如果想自己封装的话,可以参考下面文章
https://blog.csdn.net/pridas/article/details/86118774
https://github.com/xjfuuu/SM2_SM3_SM4Encrypt
不同的加密方案,加签、加密输出的结果格式不同。如果直接拿硬件加密方案生成加密结果,然后直接使用软件加密方案去解密,就会导致解密失败。
SM2 算法联调测试的时候,这一点比较头疼,下面讲下这次国密改造中碰到一些问题。
SM2 公私钥读取
SM2 如果用到数字签名,也用到加密的话,这个情况下我们就需要向 CA 机构,例如 CFCA,申请国密双证书。
CFCA 申请结果如下:
SM2 双证书,分为签名证书,加密证书。我们申请获取两个证书需要给到对手方,同样对手方也需要把他们双证书给我们。
这个过程签名需要使用自身签名证书对应的私钥,验签使用对手方签名证书包含的公钥。
加密使用对手方的加密证书包含的公钥,解密需要使用自身加密证书的对应的私钥。
这个流程比 RSA 单证书的情况复杂了很多。
我们拿到数字证书之后,如果需要从里面提取公钥,扩在下面的网站在线解析。
https://www.gmssl.cn/gmssl/index.jsp
下图选中就是证书中包含的公钥
SM2 数字签名问题
SM2 国标规定的加签数据格式使用 ASN.1,所以部分硬件厂商加签输出格式就是这种。
但是如果我们使用 BC 库加签输出格式直接使用 R|S。
如果是这种情况,我们就需要在明文 R|S 与 ASN.1 之间做相互的转换。
最新版本的 BC 库,已经提供转换的换方式。
<dependency>
<groupId>org.bouncycastlegroupId>
<artifactId>bcprov-jdk15to18artifactId>
<version>1.69version>
dependency>
可以使用下面的方式,输出签名结果为 ASN.1 格式
new SM2Signer(StandardDSAEncoding.INSTANCE, new SM3Digest());
也可以使用下面这种方式吗,输出签名结果为 R|S
// 前面输出 R|S
new SM2Signer(PlainDSAEncoding.INSTANCE, new SM3Digest());
如果是低版本,只能通过自己写代码转换了。代码就不贴了,参考下面这篇文章:
https://blog.csdn.net/pridas/article/details/86118774
SM2 加密问题
SM2 加密结果,国标规定使用 ASN.1 格式,所以部分硬件厂商加密结果使用这种格式。
但是 BC 库加密的结果是 C1|C3|C2,所以我们需要做一层转换。
转换代码如下:
将ASN1格式转成c1c3c2
/**
* 将ASN1格式转成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格式转成ASN1
private static final int C1_LEN = 65;
private static final int C3_LEN = 32;
/**
* 将c1c3c2转成标准的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 库加密结果是 C1|C2|C3,这就很坑了,现在很多都是 C1|C3|C2,这就又需要做转换。
转换代码参考这篇文章:
https://blog.csdn.net/pridas/article/details/86118774
总结
SM2 国密算法属于非对称加密算法,理解起来不是很难。
但是由于普及程度较低,现有资料太少,所以开发来还是比较复杂,碰到的问题也比较多。
建议大家开发之前可以先了解一下国密 SM2 相关国标规范,不需要很深入了解整个原理,但是需要知道国密 SM2 与 RSA 的区别点。
国密算法实现上,软加密我们可以直接用 BC 库,硬加密直接使用厂商提供的相关接口,这一点难度还好。
国密最大难度是,各个硬件与软加密,使用规范不一致,输出格式不一致,这就导致我们联调过程,加签/验签,加密/解密失败。
这就比较蛋疼,所以调试双方国密算法一致性过程中,建议先确认加签、加密输出格式,搞清楚这个,联调就比较简单了。
最后,祝大家对接国密算法顺利~
帮助资料
https://www.cnblogs.com/xinzhao/p/8963724.html
https://blog.csdn.net/pridas/article/details/86118774
https://github.com/xjfuuu/SM2_SM3_SM4Encrypt