SM2椭圆曲线密码算法原理与纯C语言实现详解
目录
- 1. SM2算法概述
- 2. SM2加密算法原理
- 2.1 数学基础
- 2.2 加密过程
- 3. SM2解密算法原理
- 3.1 解密过程
- 4. 关键数学原理
- 4.1 椭圆曲线点乘
- 4.2 密钥派生函数KDF
- 4.3 异或运算
- 5. 安全性分析
- 6. 实现要点
- 6.1 内存管理
- 6.2 坐标转换
- 6.3 点运算验证
- 7. 性能优化建议
- 8.源码实现
- 9.测试验证
- 10. 总结
1. SM2算法概述
SM2是中国国家密码管理局发布的椭圆曲线公钥密码算法,基于椭圆曲线密码学(ECC)原理。它使用256位素数域上的椭圆曲线,具有安全性高、密钥长度短等优点。
2. SM2加密算法原理
2.1 数学基础
SM2基于椭圆曲线方程:y² = x³ + ax + b
(在有限域Fp上)
核心参数:
- 素数p:256位
- 椭圆曲线参数a、b
- 基点G的阶n
- 私钥d(随机数)
- 公钥P = dG
2.2 加密过程
int sims_sm2_encrypt(unsigned char *msg, int msglen, unsigned char *wx, int wxlen, unsigned char *wy, int wylen, unsigned char *outmsg)
加密步骤:
-
生成随机数k
uint8_t fixed_k[32] = {0x4B, 0x62, 0xEE, 0xFD, ...}; str_to_bn(k, fixed_k);
-
计算C1 = kG
eccpoint_mult_G_jacobian(P1, k); // P1 = kG bn_to_str(x1, P1); // 提取x坐标 bn_to_str(y1, P1 + array_len); // 提取y坐标 memcpy(C1, x1, 32); // C1 = (x1, y1) memcpy(C1 + 32, y1, 32);
-
计算C2 = kP
eccpoint_t PB; str_to_bn(PB, wx); // 公钥P的x坐标 str_to_bn(PB + array_len, wy); // 公钥P的y坐标 eccpoint_mult_jacobian(P2, PB, k); // P2 = kP bn_to_str(x2, P2); // 提取x2, y2 bn_to_str(y2, P2 + array_len);
-
密钥派生函数KDF
kdf(x2, y2, msglen, outmsg+64); // 从(x2,y2)派生密钥t
-
计算密文C2
str_xor(C2, msg, outmsg+64, msglen); // C2 = M ⊕ t
-
计算哈希值C3
memcpy(H3, x2, 32); // H3 = x2 || M || y2 memcpy(H3 + 32, msg, msglen); memcpy(H3 + 32 + msglen, y2, 32); sm3(H3, msglen + 64, C3); // C3 = SM3(H3)
-
组合密文
memcpy(outmsg, C1, 64); // 输出 = C1 || C2 || C3 memcpy(outmsg + 64, C2, msglen); memcpy(outmsg + 64 + msglen, C3, 32);
3. SM2解密算法原理
3.1 解密过程
int sims_sm2_decrypt(unsigned char *msg, int msglen, unsigned char *privkey, int privkeylen, unsigned char *outmsg)
解密步骤:
-
解析密文
memcpy(C1, msg, 64); // 提取C1 memcpy(C2, msg + 64, msglen); // 提取C2 memcpy(C3, msg + 64 + msglen, 32); // 提取C3
-
验证C1的有效性
str_to_bn(x1, c1); // 解析C1的x,y坐标 str_to_bn(y1, c1 + 32); bn_set(p1, x1); bn_set(p1 + array_len, y1);if (!is_eccpoint(p1)) return -2; // 验证是否为有效椭圆曲线点 if (eccpoint_is_zero(p1)) return -3; // 验证是否为零点
-
计算dC1
str_to_bn(d, privkey); // 私钥d eccpoint_mult_jacobian(p2, p1, d); // p2 = dC1 bn_to_str(x2, p2); // 提取x2, y2 bn_to_str(y2, p2 + array_len);
-
密钥派生
kdf(x2, y2, msglen, outmsg); // 从(x2,y2)派生密钥t
-
恢复明文
str_xor(outmsg, C2, outmsg, msglen); // M = C2 ⊕ t
-
验证哈希值
memcpy(H3, x2, 32); // 重新计算H3 memcpy(H3 + 32, outmsg, msglen); memcpy(H3 + 32 + msglen, y2, 32); sm3(H3, 64 + msglen, u); // u = SM3(H3)if (0 != memcmp(u, C3, 32)) return -5; // 验证C3
4. 关键数学原理
4.1 椭圆曲线点乘
核心操作是椭圆曲线点乘:Q = kP
- 加密时:
C1 = kG
,P2 = kP
- 解密时:
P2 = dC1 = d(kG) = k(dG) = kP
4.2 密钥派生函数KDF
KDF将椭圆曲线点的坐标转换为对称密钥:
kdf(x2, y2, msglen, t); // 从(x2,y2)派生msglen长度的密钥t
4.3 异或运算
明文与派生密钥进行异或运算:
C2 = M ⊕ t // 加密
M = C2 ⊕ t // 解密
5. 安全性分析
- 离散对数问题:基于椭圆曲线离散对数问题的困难性
- 随机数k:每次加密使用不同的随机数k
- 哈希验证:C3用于验证密文完整性
- 密钥派生:KDF确保密钥的随机性和均匀分布
6. 实现要点
6.1 内存管理
uint8_t *t = malloc(sizeof(uint8_t) * msglen);
uint8_t *C1 = malloc(sizeof(uint8_t) * 64);
uint8_t *C2 = malloc(sizeof(uint8_t) * msglen);
// 使用完毕后释放
free(t); free(C1); free(C2);
6.2 坐标转换
bn_to_str(x1, P1); // 大数转字符串
str_to_bn(PB, wx); // 字符串转大数
6.3 点运算验证
if (!is_eccpoint(p1)) return -2; // 验证椭圆曲线点
if (eccpoint_is_zero(p1)) return -3; // 验证非零点
7. 性能优化建议
- 使用雅可比坐标:
eccpoint_mult_jacobian
避免模逆运算 - 预计算:可以预计算一些常用的椭圆曲线点
- 并行化:KDF和哈希计算可以并行进行
- 内存池:避免频繁的内存分配和释放
8.源码实现
以下代码为纯c语言的实现,无任何三方库的依赖,代码量极小。方便移植到各种系统包括嵌入式单片机环境中。需要完整测试代码的可以联系博主。
/*** SM2加密算法实现* @param msg: 待加密的明文* @param msglen: 明文长度* @param wx: 公钥x坐标* @param wxlen: 公钥x坐标长度* @param wy: 公钥y坐标* @param wylen: 公钥y坐标长度* @param outmsg: 输出密文* @return: 0成功,-1失败*/
int sims_sm2_encrypt(unsigned char *msg, int msglen, unsigned char *wx, int wxlen, unsigned char *wy, int wylen, unsigned char *outmsg)
{// 验证公钥坐标长度是否为32字节if (wxlen != 32 || wylen != 32) {return -1;}// 声明变量:椭圆曲线点的坐标uint8_t x1[32]; // C1点的x坐标uint8_t y1[32]; // C1点的y坐标uint8_t x2[32]; // P2点的x坐标uint8_t y2[32]; // P2点的y坐标// 声明变量:密文组成部分uint8_t *t; // 密钥派生函数生成的密钥uint8_t *C1; // 密文第一部分:kGuint8_t *C2; // 密文第二部分:明文⊕密钥uint8_t C3[32]; // 密文第三部分:哈希值// 哈希计算缓冲区,大小根据明文长度调整uint8_t H3[1028]; // 用于计算C3的哈希输入// 分配内存:密钥tif ((t = (uint8_t *)malloc(sizeof(uint8_t) * msglen)) == NULL)return -1;// 分配内存:C1(64字节:32字节x坐标 + 32字节y坐标)if ((C1 = (uint8_t *)malloc(sizeof(uint8_t) * 64)) == NULL)return -1;// 分配内存:C2(与明文等长)if ((C2 = (uint8_t *)malloc(sizeof(uint8_t) * msglen)) == NULL)return -1;printf("ok1\n");// 使用do-while(0)结构,便于错误处理和资源清理do{bn_t k; // 大整数类型,用于存储随机数k// 使用固定的随机数k(实际应用中应该使用真随机数)uint8_t fixed_k[32] = {0x4B, 0x62, 0xEE, 0xFD, 0x6E, 0xCF, 0xC2, 0xB9, 0x5B, 0x92, 0xFD, 0x6C, 0x3D, 0x95, 0x75, 0x14, 0x8A, 0xFA, 0x17, 0x42, 0x55, 0x46, 0xD4, 0x90, 0x18, 0xE5, 0x38, 0x8D, 0x49, 0xDD, 0x7B, 0x4F};str_to_bn(k, fixed_k); // 将字节数组转换为大整数eccpoint_t P1, P2; // 椭圆曲线点类型// 计算C1 = kG(基点G的k倍)// 根据编译选项选择不同的点乘函数
#ifdef NIST_CURVE// 使用通用的椭圆曲线点乘函数eccpoint_mult_jacobian(P1, G, k);
#else// 使用针对SM2基点G优化的专用点乘函数eccpoint_mult_G_jacobian(P1, k);
#endif// 将P1点的坐标转换为字节数组bn_to_str(x1, P1); // x坐标bn_to_str(y1, P1 + array_len, y1); // y坐标// 构造C1:将x1和y1坐标复制到C1中// 注意:这里没有添加0x04前缀(标准格式通常包含)memcpy(C1, x1, 32); // 复制x坐标memcpy(C1 + 32, y1, 32); // 复制y坐标// 构造公钥点PB:将输入的x,y坐标转换为椭圆曲线点eccpoint_t PB;str_to_bn(PB, wx); // 公钥x坐标str_to_bn(PB + array_len, wy); // 公钥y坐标// 计算P2 = kPB(公钥的k倍)eccpoint_mult_jacobian(P2, PB, k);// 将P2点的坐标转换为字节数组bn_to_str(x2, P2); // x坐标bn_to_str(y2, P2 + array_len); // y坐标// 构造xy2:将P2的x,y坐标合并为64字节uint8_t xy2[64];memcpy(xy2, x2, 32); // 复制x坐标memcpy(xy2 + 32, y2, 32); // 复制y坐标printf("ok2\n");// 使用密钥派生函数KDF从(x2,y2)生成密钥t// 生成的密钥存储在outmsg+64位置if (kdf(x2, y2, msglen, outmsg+64) == 0) {return -1; // KDF失败}printf("ok3\n");} while (0);// 计算C2:明文与派生密钥进行异或运算str_xor(C2, msg, outmsg+64, msglen);// 构造H3用于计算C3:H3 = x2 || msg || y2memcpy(H3, x2, 32); // 复制x2memcpy(H3 + 32, msg, msglen); // 复制明文memcpy(H3 + 32 + msglen, y2, 32); // 复制y2// 计算C3:对H3进行SM3哈希运算sm3(H3, msglen + 64, C3);// 构造最终密文:C1 || C2 || C3memcpy(outmsg, C1, 64); // 复制C1memcpy(outmsg + 64, C2, msglen); // 复制C2memcpy(outmsg + 64 + msglen, C3, 32); // 复制C3// 释放分配的内存free(t);free(C1);free(C2);return 0; // 成功
}/*** SM2解密算法实现* @param msg: 待解密的密文* @param msglen: 密文长度* @param privkey: 私钥* @param privkeylen: 私钥长度* @param outmsg: 输出明文* @return: 0成功,负数表示错误码*/
int sims_sm2_decrypt(unsigned char *msg, int msglen, unsigned char *privkey, int privkeylen, unsigned char *outmsg)
{// 验证私钥长度是否为32字节if (privkeylen != 32) {return -1;}// 验证密文长度是否足够(至少96字节:64字节C1 + 明文长度 + 32字节C3)if(msglen < 96)return 0;// 减去C1和C3的长度,得到实际明文长度msglen -= 96;// 声明变量uint8_t *t; // 密钥派生函数生成的密钥uint8_t *C2; // 密文第二部分uint8_t x2[32]; // 计算得到的x坐标uint8_t y2[32]; // 计算得到的y坐标uint8_t u[32]; // 重新计算的哈希值uint8_t C3[32]; // 密文第三部分(哈希值)uint8_t C1[64]; // 密文第一部分// 分配内存if ((t = (uint8_t *)malloc(sizeof(uint8_t) * msglen)) == NULL)return -1;if ((C2 = (uint8_t *)malloc(sizeof(uint8_t) * msglen)) == NULL)return -1;// 提取C1:密文的前64字节memcpy(C1, msg, 64);// 将私钥转换为大整数类型bn_t d;str_to_bn(d, privkey);// 解析C1的x,y坐标bn_t x1, y1;uint8_t *c1 = C1;str_to_bn(x1, c1); // 提取x坐标c1 += 32;str_to_bn(y1, c1); // 提取y坐标// 构造椭圆曲线点p1eccpoint_t p1;bn_set(p1, x1); // 设置x坐标bn_set(p1 + array_len, y1); // 设置y坐标// 验证p1是否为有效的椭圆曲线点if (!is_eccpoint(p1)) {free(t);free(C2);return -2; // 无效的椭圆曲线点}printf("ok1\n");// 验证p1是否为零点if (eccpoint_is_zero(p1)) {free(t);free(C2);return -3; // 零点错误}printf("ok2\n");// 计算p2 = d*p1(私钥与C1的点乘)eccpoint_t p2;eccpoint_mult_jacobian(p2, p1, d);// 将p2的坐标转换为字节数组bn_to_str(x2, p2); // x坐标bn_to_str(y2, p2 + array_len); // y坐标// 构造xy2:将x2,y2合并为64字节uint8_t xy2[64];uint8_t *pxy2 = xy2;memcpy(pxy2, x2, 32); // 复制x坐标memcpy(pxy2 + 32, y2, 32); // 复制y坐标// 使用密钥派生函数KDF从(x2,y2)生成密钥tif (kdf(x2, y2, msglen, outmsg) == 0) {return -1; // KDF失败}printf("ok3\n");printf("ok4\n");// 提取C2:密文的中间部分memcpy(C2, msg + 64, msglen);// 恢复明文:C2与派生密钥进行异或运算str_xor(outmsg, C2, outmsg, msglen);printf("ok5\n");// 打印解密结果(调试用)PrintBuf(outmsg, msglen);// 重新计算哈希值用于验证uint8_t *H3;if ((H3 = (uint8_t *)malloc(sizeof(uint8_t) * (msglen + 64))) == NULL)return -1;// 构造H3:x2 || 明文 || y2memcpy(H3, x2, 32); // 复制x2memcpy(H3 + 32, outmsg, msglen); // 复制明文memcpy(H3 + 32 + msglen, y2, 32); // 复制y2printf("ok5-1,msglen=%d\n",msglen);// 计算哈希值usm3(H3, 64 + msglen, u);printf("ok6\n");// 提取C3:密文的最后32字节memcpy(C3, msg + 64 + msglen, 32);// 验证哈希值:比较重新计算的哈希值u与C3if (0 != memcmp(u, C3, 32)) {free(t);free(C2);free(H3);return -5; // 哈希验证失败}// 释放分配的内存free(t);free(C2);free(H3);return 0; // 解密成功
}
9.测试验证
下面写一个main函数,对上述实现进行加解密测试:
#include <stdio.h>
#include <string.h>void PrintBuf(unsigned char *buf, int buflen)
{int i;printf("\n");printf("len = %d\n", buflen);for(i=0; i<buflen; i++) {if (i % 32 != 31)printf("%02x", buf[i]);elseprintf("%02x\n", buf[i]);}printf("\n");return;
}void Printch(unsigned char *buf, int buflen)
{int i;for (i = 0; i < buflen; i++) {if (i % 32 != 31)printf("%c", buf[i]);elseprintf("%c\n", buf[i]);}printf("\n");//return 0;
}extern int sims_sm2_encrypt(unsigned char *msg, int msglen, unsigned char *wx, int wxlen, unsigned char *wy, int wylen, unsigned char *outmsg);
extern int sims_sm2_decrypt(unsigned char *msg, int msglen, unsigned char *privkey, int privkeylen, unsigned char *outmsg);int sm2_test()
{printf("sm2 test 0....\n");unsigned char dB[] = { 0x13,0xd5,0xa9,0x1d,0x02,0x5a,0xe5,0xfb,0xf4,0x7b,0x10,0xa5,0xfc,0x98,0xb9,0x13,0x49,0x4d,0xf6,0x6a,0x6f,0xeb,0x1c,0xf8,0xa7,0x26,0x81,0xee,0xf6,0xc3,0x3d,0xed };unsigned char xB[] = { 0xf7,0x9e,0x06,0x71,0xb8,0xe3,0xbc,0x54,0x2a,0xab,0x81,0xcf,0xa1,0xd1,0xa6,0xc1,0x58,0xb9,0xbd,0xca,0xad,0x46,0x4b,0xbf,0x9e,0x8a,0x79,0x6f,0x8e,0xc9,0x2e,0x27 };unsigned char yB[] = {0xb2,0x2f,0x1f,0x3d,0x72,0xad,0x1e,0x99,0x23,0x51,0xf7,0xce,0xcf,0xdc,0xd5,0x03,0xaa,0x6d,0xf9,0x3b,0xef,0xdd,0xb8,0xdd,0x92,0x14,0x7e,0x74,0x57,0xdf,0x4d,0x44 };unsigned char tx[256]={0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31,0x31};unsigned char etx[256];unsigned char mtx[256];unsigned char hexOut[256] = {0x52,0xB4,0x38,0xC6,0xAB,0xF7,0x68,0x27,0x13,0x7C,0x55,0x9D,0xEF,0x13,0xC0,0xE0,0x09,0x45,0x29,0x44,0x7C,0xFC,0xBD,0x6E,0xFB,0x42,0x66,0x73,0xF2,0x7B,0xCF,0x2B,0x29,0xBF,0x1A,0x67,0xBB,0x8B,0x19,0x53,0x12,0x5C,0x13,0x9B,0x52,0xB8,0x81,0x0F,0x20,0xCC,0x16,0xEA,0x42,0x1B,0x44,0xD0,0x10,0xBF,0x42,0xF2,0x3B,0x95,0xE8,0xB7,0x52,0x5E,0x77,0xC7,0xE4,0xBD,0x40,0xFC,0xA5,0xA6,0xD4,0x87,0x83,0xBD,0xFB,0x19,0x91,0x9A,0x0A,0x3C,0x7B,0x2F,0x87,0x0A,0xAD,0x74,0x13,0xBC,0x94,0x3F,0x84,0x50,0x4B,0x7B,0x9A,0x1F,0x7C,0xFE,0x7E,0xBC,0x46,0xCB,0x90,0x98,0x15,0x22,0x82,0x23,0x30,0x10,0x96,0xB8,0x65,0xFC,0x97,0x4C,0x92,0xB1,0x07,0x54,0x4D,0xB0,0xFB,0x91,0xD5,0xEB,0x5A,0x07,0xFC,0x48,0x2F,0x14,0x91,0xFC,0x2D,0x3A,0x6C,0x50,0x01,0xB9,0x7E,0x1C,0xE7,0x40,0x6A,0x21,0x0F,0x9A,0x62,0xE7,0xC3,0x0B,0xB4,0x7A,0xAD,0x93,0x04,0x35,0x5F,0x87,0xA0,0xDD,0x70,0x22,0x5F,0x65,0xB1,0x95,0x31,0xC3,0x3E,0xA8,0xEF,0xE1,0x74,0x4D,0x16,0x5A,0x6F,0x07,0xB2,0x72,0x38,0xEA,0x2A,0xFD,0x28,0xD8,0xCF,0x5E,0x1A,0x15,0x4A,0xA0,0x1F,0x87,0x2B,0xE8,0xA0,0xED,0xED,0xAD,0xB6,0x14,0xA4,0x83,0x16,0x0A,0xAB,0xAA,0x43,0xF2,0x2E,0xDB,0xE6,0x95,0x23,0xC9,0xF4,0x4F,0xEC,0x94,0x7C,0x32,0x7A,0x6C,0x95,0x5A,0x33,0x8B,0xCB,0x7B,0x05,0xD3,0x5B,0x54,0x9C,0x4F,0xAC,0x9D,0x14,0xA3,0x51,0xA5,0xEB,0x91};//FILE *fp=0;int wxlen, wylen, privkeylen,len;//fopen(&fp, "5.txt", "r");//len=fread_s(tx, 256,sizeof(unsigned char), 256, fp);//fp = fopen("5.txt","r");//len=fread(tx,1,256,fp);memset(tx,0x31,154);len = 154;tx[len] = 0;PrintBuf(tx, len);//sm2_keygen(xB, &wxlen, yB, &wylen, dB, &privkeylen);printf("dB: ");PrintBuf(dB, 32);printf("xB: ");PrintBuf(xB, 32);printf("yB: ");PrintBuf(yB, 32);// mode =1,ΪC1C3C2ģʽ�� 0ΪC1C2C3ģʽsims_sm2_encrypt(tx,len,xB,32,yB,32,etx);printf("\n``````````````````this is encrypt```````````````````\n");PrintBuf(etx, 64 +len + 32);printf("\n``````````````````this is decrypt```````````````````\n");//sm2_decrypt(etx,64+len+32,dB,32,mtx);//int ret = sims_sm2_decrypt(etx,64+len+32,dB,32,mtx);if( ret < 0){printf("sm2_decrypt errors!,code=%d\n",ret);}else{PrintBuf(mtx, len);Printch(mtx, len);}printf("\n``````````````````this is end```````````````````````\n");return 0;
}int main(){sm2_test();return 0;
}
测试结果如下图所示:
分别在x86和x64及stm32单片机上进行测试,测试结果符合正确。且占用资源极低。
10. 总结
SM2算法通过椭圆曲线密码学提供了高安全性的公钥加密方案。其核心在于:
- 加密:使用随机数k和公钥P生成密文C1、C2、C3
- 解密:使用私钥d从C1恢复出相同的中间值,进而恢复明文
- 验证:通过哈希值C3确保密文完整性
纯C语言实现需要处理大数运算、椭圆曲线点运算、密钥派生等复杂操作,但通过模块化设计可以构建出高效、安全的SM2加解密系统。