iOS 使用 SM2 SM4 加解密,SM2 签名验签及 SM3 生成摘要

5,404 阅读12分钟

前言

对于开发人员,开发中加解密是经常用到的,常见的密码算法 MD5、SHA、AES、DES,RSA 等等,这些无一例外都是国外的加密算法。基于安全和宏观战略考虑,我国从 2010 年先后推出了 SM1(SCB2)、SM2、SM3、SM4、SM7、SM9、ZUC(祖冲之密码算法)等密码算法,本文主要讨论 SM2 算法原理,iOS 端如何使用 SM2、SM4 加解密,SM2 签名验签及使用 SM3 生成 Hash 值。

国密全家桶

国密算法中,SM1、SM4、SM7、ZUC 是对称算法;SM2、SM9是非对称算法;SM3是哈希算法。其中 SM1 和 SM7 分组密码算法不公开,SM1 主要用于加密芯片等重要领域,例如 智能 IC 卡,加密机等;SM7 主要用于常规非接触式 IC 卡,例如门禁卡,工作证等。

算法公开类似主要用途
SM1AES智能IC卡、加密卡,加密机等。
SM2RSA重要信息的加解密,如密码。
SM3SHA密码应用中的数字签名和验证摘要算法。
SM4AES分组算法用于无线局域网产品。
SM7AES校园一卡通,门禁卡,工作证等。
SM9SSL基于身份的密码,用于验证身份。
ZUCAES4G 网络中的国际标准密码算法。

SM2 算法原理

SM2 算法是国密标准的非对称算法标准,基于ecc(Elliptic Curves Cryptography,椭圆曲线密码编码学)的扩展。提起非对称加密,自然想到了 RSA,对极大整数做因数分解的难度决定了RSA算法的可靠性(RSA 算法理解),这是 RSA 安全的基础。那国密加解密的算法基础是什么?首先我们先理解一下椭圆曲线。

SM2 椭圆曲线

国密 SM2 的算法基础是椭圆曲线,公式:

y^2 = x^3 + ax + b(4a^3 + 27b^2 ≠ 0)

那椭圆曲线长什么样子呢,百闻不如一见,图片能直观感受。

椭圆曲线

为什么需要满足呢?

4a^3 + 27b^2 ≠ 0

因为当这个公式等于 0 时,它不是椭圆曲线。

ab取值条件

SM2 算法理解

倍点运算

结合上面这张图,我们了解一下 SM2 的几何意义。

SM2 公私钥

算法是基于数学的,SM2 定义曲线上的群运算加减乘,通过公私钥的生成理解。

  1. 首选一条椭圆曲线,即固定 a、b 的值,假设选择的是上图所示曲线。
  2. 随机选择一个点 P 为基点,曲线做切线,经过 Q 点,切点 R1。
  3. 基于 x 轴做 R1 的对称点 R,则 SM2 定义加法为 P + Q = R,这就是椭圆曲线加法。
  4. 求 2 倍点,当 P = Q 时,即 P + P = R = 2P,则 R 是 P 的 2 倍点。
  5. 求 3 倍点,3P = P + 2P = P + R,经过 P、R 做直线,交于椭圆曲线点 M1, 基于 x 轴对称点 M 则是 3 倍点,依次类推。
  6. 求 d 倍点,假设我们同样次数为 d,运算倍点为 Q。
  7. d 为私钥,Q 为公钥。所以私钥是一个大整数,公钥是一个点坐标。

上面的几何推理是为了方便理解,实际取值都是在质数有限域上。密码专家们经过推理和运算,已经为我们选择了质数有限域上的最优椭圆曲线,除非有特殊需要,否则不需要自定义曲线。

推荐曲线

p:椭圆曲线在质数 p 的有限域 Fp 上的点集合; a:椭圆曲线参数 a 的值; b:椭圆曲线参数 b 的值; n:取值范围,随机整数 d 的取值范围 [1,n-2]; Gx:基点的 x 坐标值,类似于点 P 的 x 坐标值; Gy:基点的 y 坐标值,类似于点 P 的 y 坐标值。

SM2 加密

SM2 加密结果长度是固定的,例如密码为 123456 的 6 位数字,加密结果长度 = 64 + 32 + 6 = 102 字节,转为 16 进制字符串结果为 204 个字符。原文长度为 n,则加密结果长度 r = 96 + n。

加密过程:

sm2加密

设椭圆曲线为推荐曲线,公钥 Q,原文比特串 M,klen 为 M 的比特长度;

  1. 计算随机椭圆曲线点 C1 = [k]G=(x1, y1),k 是随机数,G为基点,计算出的倍点 C1 为 64 字节;
  2. 校验公钥 Q,计算椭圆曲线点 S=[h]Q,h为余因子,若S 为无穷点,退出;
  3. 计算椭圆曲线点 [k]PB=(x2, y2),获取 x2,y2;
  4. 计算 t = KDF(x2||y2,klen),若 t 为全 0 比特串,则返回步骤 1,KDF是 SM2 的密钥派生函数;
  5. 计算 C2= M⊕t,对明文加密,C2 是真正的密文,长度和原文相同;
  6. 计算 C3= Hash (x2||M|| y2),生成杂凑值,用来效验数据,长度 32 字节;
  7. 输出密文 C=C1||C3||C2,C 为密文结果。

注意:OpenSSL 加密结果是经过 ASN1 格式化编码的,加密结果长度会不固定。加过过程中使用了随机数,所以每次加密结果都不一样。

SM2 解密

sm2解密

SM2 解密就是逆流程走一遍,注意 OpenSSL 解密要求传入的密文是 ASN1 编码的。

设椭圆曲线为推荐曲线 私钥 d,密文 C(C=C1||C3||C2),klen 为密文中 C2 的比特长度。

  1. 从 C 中取出比特串 C1(密文 C 的前 64 字节),将 C1 的数据类型转换为椭圆曲线上的点,验证 C1 是否满足椭圆曲线方程,若不满足则报错并退出;
  2. 计算椭圆曲线点 S= [h]C1,若 S 是无穷远点,则报错并退出;
  3. 计算[d]C1=(x2, y2),将坐标 x2、y2 的数据类型转换为比特串;
  4. 计算 t = KDF(x2||y2,klen),若 t 为全 0 比特串,则报错并退出;
  5. 从 C 中取出比特串 C2,计算 M'=C2⊕t;
  6. 计算 u = Hash (x2||M’|| y2),从 C 中取出比特串 C3(密文 C 的后 32 字节),若 u≠C3,则报错并退出;
  7. 输出明文 M',M' 就是解密后的明文。

集成 OpenSSL

OpenSSL 1.1.1 以上版本增加了对 SM2/SM3/SM4 密码算法的支持,我们可以直接使用 OpenSSL 实现国密加解密。需要注意的是,OpenSSL 没有官方版本的 cocoapods 版本,我们需要自行将 OpenSSL 编译为 framework。然而,当检查打包完成的静态库时,发现并未暴露国密的头文件,解决办法很简单,打开下载的 OpenSSL 源码,将 crypto/include/internal 路径下的 sm2.h、sm3.h,sm4.h 都拖到 openssl.framework/Headers 文件夹下即可。

如果想通过 cocoapods 集成 OpenSSL,或者不会编译,我已经将编译完成的 OpenSSL.framework 上传至 cocoapods,编辑 Podfile 文件,添加 pod 'GMObjC',保存执行 pod install 即可。

若想自行编译,在 GitHub 有开源的编译脚本 github.com/muzipiao/GM…,下载根据说明编译即可。

国密的 Objective-C 封装

OpenSSL 实现了 SM2/SM3/SM4 密码算法,但没有注释说明,且纯 C 的 API 用起来不方便。所以,对 SM2/SM3/SM4 进行了 Objective-C 封装,方便在 iOS 端使用。

具体封装过程不再详解,开源项目,可自行查看源码。实现过程有点坎坷,尤其 SM2 加解密,后台是对 C1||C3||C2 拼接的原始密文进行操作,而 OpenSSL 加解密都是 ASN1 编码格式,还好 OpenSSL 是开源项目,查看源码找到了原因。

查看具体实现过程,请至开源项目地址github.com/muzipiao/GM…

sm2 加解密

sm2 加解密,加密传入待加密字符串和公钥,解密传入密文和私钥即可,代码:

// 公钥
NSString *gPubkey = @"0408E3FFF9505BCFAF9307E665E9229F4E1B3936437A870407EA3D97886BAFBC9C624537215DE9507BC0E2DD276CF74695C99DF42424F28E9004CDE4678F63D698";
// 私钥
NSString *gPrikey = @"90F3A42B9FE24AB196305FD92EC82E647616C3A3694441FB3422E7838E24DEAE"
// 待加密的字符串
NSString *pwd = @"123456";
// 加密
NSString *ctext = [GMSm2Utils encrypt:pwd PublicKey:gPubkey];
// 解密
NSString *plainText = [GMSm2Utils decrypt:encodeCtext PrivateKey:gPrikey];

注意:

  1. OpenSSL 所用公钥是 04 开头的,后台返回公钥可能是不带 04 的,需要手动拼接。
  2. 后台返回的解密结果可能是没有标准编码的原始密文,而 OpenSSL 的加解密都是需要 ASN1 编码格式,所以与后台交互过程中,可能需要 ASN1 编码解码。

sm2 签名验签

sm2 私钥签名,公钥验签,可防篡改或验证身份。签名时传入明文、私钥和用户 ID;验签时传入明文、签名、公钥和用户 ID,代码:

// 公钥
NSString *gPubkey = @"0408E3FFF9505BCFAF9307E665E9229F4E1B3936437A870407EA3D97886BAFBC9C624537215DE9507BC0E2DD276CF74695C99DF42424F28E9004CDE4678F63D698";
// 私钥
NSString *gPrikey = @"90F3A42B9FE24AB196305FD92EC82E647616C3A3694441FB3422E7838E24DEAE"

// 待签名的原文
NSString *pwd = @"123456";
// 这里传入自定义 ID,和服务器保持两端一致即可。
NSString *userID = @"lifei_zdjl@126.com";
// 签名结果(r+s)拼接的 16 进制字符
NSString *signStr = [GMSm2Utils sign:pwd PrivateKey:gPrikey UserID:userID];
// 验签,isOK 为 YES 验签通过,NO 为未通过
BOOL isOK = [GMSm2Utils verify:pwd Sign:signStr PublicKey:self.gPubkey UserID:userID];
// 对签名结果 Der 编码
NSString *derSign = [GMSm2Utils encodeWithDer:signStr];
// 对 Der 编码解码
NSString *originStr = [GMSm2Utils decodeWithDer:derSign];

注意:

  1. 用户 ID 可传空值,当传空值时使用 OpenSSL 默认用户 ID,OpenSSL 中默认用户定义为#define SM2_DEFAULT_USERID "1234567812345678" ,客户端和服务端用户 ID 要保持一致。
  2. 客户端和后台交互的过程中,假设后台签名,客户端验签,后台返回的签名是 DER 编码格式,就需要先对签名进行 DER 解码,然后再进行验签。同理,若客户端签名,后台验签,根据后台是需要 (r, s) 拼接格式签名,还是 DER 格式,进行编码解码。

sm4 加解密

sm4 加解密都很简单,加密传入待加密字符串和密钥,解密传入密文和密钥即可,代码:

// 待加密字符串
NSString *pwd = @"123456";
// 生产 sm4 密钥,注意为 16 字节字母数字符号混合的字符串
NSString *sm4Key = [GMSm4Utils createSm4Key]; // 生成16位密钥
// sm4 加密
NSString *sm4Ctext = [GMSm4Utils encrypt:pwd Key:sm4Key];
// sm4 解密
NSString *sm4Ptext = [GMSm4Utils decrypt:sm4Ctext Key:sm4Key];

sm3 摘要

类似于 hash、md5,sm3 摘要算法可对文本文件进行摘要计算,摘要长度为 64 个字符的字符串格式。

// 待提取摘要的字符串
NSString *pwd = @"123456";
// 字符串的摘要
NSString *pwdDigest = [GMSm3Utils hashWithString:plainText];

// 对文件进行摘要计算,传入 NSData 即可
NSString *txtPath = [[NSBundle mainBundle] pathForResource:@"sm4TestFile.txt" ofType:nil];
NSData *fileData = [NSData dataWithContentsOfFile:txtPath];
// 文件的摘要值
NSString *fileDigest = [GMSm3Utils hashWithData:self.fileData];

ASN1 编码解码

OpenSSL 对 sm2 加密结果进行了 ASN1 编码,解密时也是要求密文编码格式为 ASN1 格式,其他平台加解密可能需要 C1C3C2 拼接的原始密文,所以需要编码解码,代码:

// ASN1 编码的密文
NSString *ctext = @"30:6F:02:21:00:D4:F1:B3:2E:29:50:1E:94:44:46:7F:9E:2E:51:36:1E:91:F5:EC:0B:96:F3:34:94:E5:50:82:9F:00:CC:B5:B7:02:20:04:42:83:DF:76:21:B2:9C:EB:7F:64:8B:B4:7A:3C:BF:FE:97:47:E4:D2:BD:47:44:C9:DA:1D:68:12:23:43:D6:04:20:45:F6:AB:54:22:71:63:93:95:3B:58:E3:8D:90:32:B7:A1:D8:76:2B:B8:16:F2:6A:83:51:77:44:2D:28:2C:D2:04:06:62:9F:38:6A:77:76";
// 对 ASN1 编码的密文解码
NSString *decodeStr = [GMSm2Utils decodeWithASN1:ctext];

// 原始密文(C1C3C2 直接拼接)
NSString *dCtext = @"D4F1B32E29501E9444467F9E2E51361E91F5EC0B96F33494E550829F00CCB5B7044283DF7621B29CEB7F648BB47A3CBFFE9747E4D2BD4744C9DA1D68122343D645F6AB5422716393953B58E38D9032B7A1D8762BB816F26A835177442D282CD2629F386A7776";
// 对 C1C3C2 直接拼接的原始密文 ASN1 编码
NSString *encodeStr = [GMSm2Utils encodeWithASN1:dCtext];

生成公私钥

基于 sm2 推荐曲线(素数域 256 位椭圆曲线),生成公私钥。

// 生成公私钥对,数组元素 1 为公钥,2 为私钥
NSArray *newKey = [GMSm2Utils createPublicAndPrivateKey];
// 公钥
NSString *pubKey = newKey[0];
// 私钥
NSString *priKey = newKey[1];

参考

SM2椭圆曲线公钥密码算法

SM2椭圆曲线公钥密码算法推荐曲线参数

椭圆曲线加密原理

椭圆曲线密码学

其他

如果您觉得有所帮助,请在 GitHub GMObjC 上赏个Star ⭐️,您的鼓励是我前进的动力。