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

前言

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

国密全家桶

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

算法 公开 类似 主要用途
SM1 AES 智能IC卡、加密卡,加密机等。
SM2 RSA 重要信息的加解密,签名,如密码。
SM3 SHA 密码应用中的数字签名和验证,摘要等。
SM4 AES 分组算法用于无线局域网产品。
SM7 AES 校园一卡通,门禁卡,工作证等。
SM9 SSL 基于身份的密码,用于验证身份。
ZUC AES 4G 网络中的国际标准密码算法。

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 有开源的编译脚本 https://github.com/muzipiao/GMOpenSSL,下载根据说明编译即可。

国密的 Objective-C 封装

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

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

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

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 ⭐️,您的鼓励是我前进的动力。