尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

低内存STM32上的SM2国密算法实现与优化

发布时间:2026/9/9 0:45:26

资讯中心
01
ARTICLE

低内存STM32上的SM2国密算法实现与优化

低内存STM32上的SM2国密算法实现与优化
简介面向低内存STM32单片机的国密SM2算法实现源码包适合嵌入式与信息安全开发者在资源受限平台上集成国产密码算法。该源码以STM32F10x等低RAM/Flash环境为设计目标重点解决SM2椭圆曲线公钥算法在资源受限MCU上的运行效率与内存占用问题。压缩包共421个文件、约6.88MB以C源文件(.c/.h)为核心包含MDK工程文件、链接脚本、编译过程文件(.o/.crf/.axf)、可烧录hex文件及辅助清理脚本目录结构清晰方便导入工程后直接编译验证。源码经过测试保证能运行覆盖SM2椭圆曲线运算、有限域算术及点乘等关键模块并有MDK产生的过程文件与备份便于逆向理解编译流程或进行二次开发。已有1962人学习下载作者还表示如需Linux版可单独联系并会在此基础上免费赠送适合有跨平台移植需求的读者。 做嵌入式这些年我在不少物联网终端、电力采集设备、车载单元和门禁控制器项目里都碰到过国密算法适配的需求。SM2作为国密公钥密码体系里应用最广的一员签名、验签、密钥交换都离不开它但真正想把它跑进低内存STM32单片机并不轻松256位大数运算、椭圆曲线点乘、模逆模乘随便一算都是“吃内存大户”。这个项目就是我把多年积累整理成的一套基于低内存STM32的单片机版SM2源码在RAM只有20KB左右的芯片上完整实现了签名、验签、加密、解密内存占用控制在8KB内全程无动态内存分配。适合正在做国产化改造、对BOM成本敏感、又不想额外加安全芯片的嵌入式工程师参考。1. 项目背景与整体设计思路1.1 为什么SM2在单片机上这么“难搞”很多人第一次在MCU上跑SM2第一反应是“不就是几百行C代码吗”。实际上SM2的运算逻辑本身不复杂复杂的是它背后的大数运算和曲线点运算。SM2基于256位素数域上的椭圆曲线核心运算包括模加、模减、模乘、模逆以及点加、倍点、多点乘。一次标量乘法 kG 的过程中要反复执行几百次点加和倍点运算而每一次点加或倍点背后又藏着多轮模乘和模逆。模逆又是所有运算里最贵的一次模逆的成本大约相当于几十次模乘如果坐标选得不好总耗时会被拖到完全不可用的水平。更麻烦的是内存。PC上跑OpenSSL随便malloc一块几百KB的内存眼都不眨但STM32F103C8T6这类经典芯片只有20KB RAMSTM32F103RCT6也只有48KB很多项目还得分出一大半给协议栈、通信缓存和业务逻辑。RAM分配稍微不谨慎编译直接挂掉或者跑起来就HardFault。1.2 低内存单片机场景的真实约束在做这套源码前我先列了一张硬约束清单所有设计决策都围绕这张表展开硬件/环境约束典型值对SM2实现的影响RAM20~48KB不允许大缓冲、不允许mallocFlash128~512KB代码体积宽松但也不能无限制膨胀CPU主频72MHzM3~ 168MHzM4运算速度只有PC的几十分之一硬件加速多数无FPU、无除法指令大数运算要靠纯软件循环编译器Keil MDK / IAR / GCC不同优化等级对性能影响巨大基于这些约束我给源码定的设计目标是峰值RAM尽量控制在8KB以内签名耗时在百毫秒到秒级可接受代码完全不用动态内存全部工作区由调用者传入或者用静态/全局数组复用。拿8KB这个数字来说它能比较舒服地塞进STM32F103C8T6这类最小配置芯片同时预留出运行协议栈和业务主循环的空间。2. 方案选型与几个关键优化点2.1 坐标系统与窗口表怎么选SM2点运算的坐标表示直接决定性能天花板。业界常用仿射坐标和雅可比坐标两种方式。仿射坐标x, y最直观但每次点加和倍点都要计算模逆雅可比坐标X, Y, Z则把模逆推迟到整个点乘的最后统一做一次。一次点乘里通常有几百次点加/倍点如果全部用仿射坐标就等于做了几百次昂贵模逆耗时根本压不住改成雅可比坐标后整个点乘过程只需要在终点还原坐标时做一次模逆速度能提升数倍。代价是雅可比坐标下的每个点要多保存一个Z坐标内存占用小幅增加。对低内存场景来说这点增加完全值得。窗口表的设计则是典型的“时间换空间、空间换时间”取舍。点乘 kG 如果逐比特硬算每次循环都做一次倍点加条件点加512比特密钥大约需要几百次点运算太慢。加窗口表后可以把G的1倍、2倍、3倍……15倍点预先算好放在RAM里然后每4比特查一次表做点加速度明显提升。窗口越大速度越快但预计算点的数量按2的幂次增长。实测下来4比特窗口是最适合低内存单片机的档位15个预计算点约占用1KB RAM速度提升已经很明显RAM开销又可控5比特窗口速度快一点但光预计算表就要翻倍到2KB以上得不偿失。2.2 内存复用没有malloc也能跑这套源码里没有任何动态内存分配。256位大整数统一用 uint8_t buf[32] 或 uint32_t buf[8] 表示临时大数区域设计成“多个算法阶段共用同一块工作区”。比如签名过程先算 kG算完后临时点立即释放后续计算 r、s 时复用同一片缓冲。实现上我把几个核心工作区做成全局静态数组函数之间通过宏或参数约定访问这样既避免了栈上开大数组导致溢出又避免了malloc的碎片问题。全局数组在裸机环境里没有任何并发问题不需要加锁简单直接。还有一个容易被忽略的点栈空间。很多MCU工程默认栈只有1KB左右SM2函数如果直接在栈上开几个 uint32_t tmp[8] 和点结构体一个函数嵌套调用下来很容易栈溢出。我把容易爆栈的中间计算改成静态数组或者让调用者显式传入一块缓冲区彻底规避了这类隐患。2.3 SM2、SM4、AES256怎么分工聊国密绕不开“SM2和SM4、AES256的区别”。简单说SM2是非对称公钥算法适合做签名验签、身份认证和协商对称密钥但运算慢、不适合加密大块数据SM4和AES256是对称算法适合做批量数据加密速度快、硬件实现多。在单片机的通信协议里常用的组合是SM2握手 SM4传输先通过SM2完成双方身份认证和主密钥协商再用协商出的SM4密钥加密实际业务报文。如果只用SM2去逐帧加密整个业务流既不现实也没必要——一次SM2加密的耗时是SM4的几百上千倍RAM占用也远高于对称算法。3. 源码结构与核心接口实现3.1 工程目录与模块划分源码按功能拆成几个独立模块方便裁剪和移植sm2_stm32/ ├─ inc/ │ ├─ sm2.h // 对外API签名/验签/加密/解密 │ ├─ sm2_bn.h // 256位大数运算接口 │ ├─ sm2_ec.h // 椭圆曲线点运算接口 │ └─ sm2_rng.h // 随机数适配层接口 ├─ src/ │ ├─ sm2.c // SM2主流程签名、验签、加密、解密 │ ├─ sm2_bn.c // 大数运算模加、模减、模乘、模逆 │ ├─ sm2_ec.c // 点运算点加、倍点、点乘 │ └─ sm2_rng.c // 随机数适配ADC噪声/真随机源接入 └─ test/ ├─ test_sm2.c // 官方测试向量自检 └─ demo_main.c // 裸机调用示例大数运算和点运算是底层基础主流程文件只负责按SM2标准把参数组织好然后调用底层函数。随机数单独抽成一个适配层是因为不同板子的熵源差异巨大后面会专门讲。3.2 对外接口与字节序约定核心接口设计得尽量简单方便裸机工程直接调用/* SM2签名对hash值做数字签名r和s各占32字节 */ int SM2_Sign(const uint8_t *hash, uint32_t hashLen, const SM2_KEY *key, uint8_t r[32], uint8_t s[32]); /* SM2验签验证hash的签名是否由key对应私钥产生 */ int SM2_Verify(const uint8_t *hash, uint32_t hashLen, const SM2_KEY *key, const uint8_t r[32], const uint8_t s[32]); /* SM2加密使用公钥加密一段明文 */ int SM2_Encrypt(const uint8_t *msg, uint32_t msgLen, const SM2_KEY *pubKey, uint8_t *cipher, uint32_t *cipherLen); /* SM2解密使用私钥解密密文 */ int SM2_Decrypt(const uint8_t *cipher, uint32_t cipherLen, const SM2_KEY *privKey, uint8_t *msg, uint32_t *msgLen);这里有个特别容易踩坑的约定所有多字节整数统一采用“大端字节序”。SM2标准文档里的十六进制字符串天然是从高位到低位的内存数组直接按大端顺序存放处理起来最自然。STM32本身是小端但通信协议往往又是大端所以我在API层直接约定好进入函数的字节就是协议里的字节内部计算时保持一致只在必要处做一次转换避免在算法内部反复横跳。3.3 核心代码片段解读大数模乘是整个SM2性能的地基。代码里使用uint32_t数组表示256位大数每个元素保存32位乘法时用64位变量累加中间结果一次循环完成一组乘加操作/* 256位模乘c a * b mod p使用32位无符号数组表示 */ static void bn_mul_mod(uint32_t c[8], const uint32_t a[8], const uint32_t b[8]) { uint64_t t[16] {0}; int i, j; /* 普通小学乘法逐位相乘累加 */ for (i 0; i 8; i) { uint64_t carry 0; for (j 0; j 8; j) { uint64_t uv t[i j] (uint64_t)a[i] * b[j] carry; t[i j] (uint32_t)(uv 0xFFFFFFFFULL); carry uv 32; } t[i 8] carry; } /* 巴雷特约减或蒙哥马利约减取模 */ bn_mod_p(c, t); }我这里展示的是最直观的乘加框架实际源码里取模部分会根据平台选择巴雷特约减或蒙哥马利约减。uint64_t在Cortex-M3/M4上由编译器拆成两条32位乘加指令完成效率足够一定不要用uint16_t数组去模拟大数迭代次数翻倍且容易出错。点乘是SM2耗时大头核心逻辑是把4比特窗口表和雅可比坐标串起来/* 点乘R k * G使用4比特窗口预计算表 */ static void ec_mul_point(EC_POINT *R, const uint32_t k[8], const EC_POINT *G) { EC_POINT T[16]; /* 预计算G的1~15倍点 */ EC_POINT Q; int i; ec_precompute_window(T, G); /* 窗口表建立 */ ec_point_set_infinity(Q); for (i 63; i 0; i--) { ec_point_double(Q); /* 倍点 */ ec_point_double(Q); ec_point_double(Q); ec_point_double(Q); uint32_t nibble (k[i 3] ((i 7) 2)) 0xF; ec_point_add(Q, T[nibble]); /* 窗口查表点加 */ } ec_to_affine(R, Q); /* 统一做一次模逆还原坐标 */ }这里把64个4比特窗口逐层处理每层做4次倍点和1次点加。窗口表T[16]里第一个点其实是无穷远点实际用T[0]表示0查表时映射一下即可。签名流程按SM2标准走先计算 e H(Z_A || M)取随机数k计算 (x1, y1) kG得到 r (e x1) mod n再计算 s ((1 dA)^-1 * (k - r*dA)) mod n。验签则用公钥 P 计算 (x1, y1) sG tP其中 t (r s) mod n最后比较 x1 是否等于 r。整套逻辑在sm2.c里不到300行重点是每个步骤都复用了同一块大数工作区。4. 性能实测与平台适配4.1 内存和耗时实测数据我在STM32F103C8T672MHz20KB RAM和STM32F407VE168MHz192KB RAM上分别跑过这套源码开启Keil MDK最高速度优化后的数据如下操作峰值RAM占用F103耗时72MHzF407耗时168MHz说明SM2签名约6.8KB约450ms约190ms一次点乘SM2验签约7.2KB约860ms约350ms需要两次点乘SM2加密约8.0KB约1.5s约650ms含KDF派生SM2解密约8.0KB约1.2s约520ms内部一次点乘不同优化等级和编译器环境下数据会上下浮动但趋势是一致的验签比签名慢接近一倍因为验签需要计算 sG tP 两个点乘。如果项目对响应时间有硬性要求建议优先把主频拉高、用更高主频的M4/M7芯片或者减少中断嵌套、关掉其他外设对CPU的抢占。4.2 移植到其他MCU的注意点这套源码设计时尽量做了平台抽象理论上不限于STM32GD32、APM32、NXP的LPC系列都能跑但有几个点必须注意第一是随机数熵源。签名用的随机数k必须是不可预测的如果两次签名用了相同的k私钥可以直接被算出来。STM32F4系列有硬件RNG但很多低端MCU没有。我在适配层里留了接口低端平台可以用ADC噪声末位波动混合一个确定性伪随机序列再用定时器捕获的jitter做扰动效果能满足一般商用场景。注意直接用rand()或简单LFSR生成的伪随机数签名是非常危险的后面排查章节会细说。第二是字节序。我已经在API层统一用大端内部运算也按大端存数组所以只要调用方按协议字节序传数据就不会有问题。如果某个平台本身是大端比如部分TCP/IP协议栈内部是网络序反而会更顺。第三是编译优化。Keil MDK里建议开-O2或-O3优化等级过低会导致大数运算时间直接翻倍开了优化后如果出现异常先检查是否用到未初始化的局部变量或数组越界不要盲目降优化等级。5. 常见问题与排查记录5.1 高频问题速查把我在实际项目和社区交流里碰到的问题整理成了速查表基本都是共性痛点问题现象可能原因解决建议编译报RAM溢出/超限全局窗口表或工作区过大把窗口从5比特降到4比特或把部分工作区复用运行时进入HardFault栈溢出函数内局部大数组过大检查栈配置把临时大结构体移到全局/静态区栈至少设置4KB自检向量验证不过字节序不统一数组长度没对齐核对SM2官方向量的十六进制串如何读入内存统一大端签名后验签不通过签名r/s在传输过程中被做了字节序转换确认通信协议层只传输原始字节不额外做大小端变换运算耗时过长优化等级太低或误用了仿射坐标Keil开-O2确认坐标选择是雅可比/射影坐标两次签名结果完全相同随机数k来源固定熵源退化改成ADC噪声定时器jitter混合熵源或者上硬件TRNG5.2 踩坑实录我在移植中遇到的三件事第一件是Keil的MicroLIB坑。MicroLIB里printf不支持浮点这个很多人都知道但如果你在调试SM2时用printf打印大数中间值可能触发奇怪的编译优化错误表现为某些数组数据被“莫名奇妙”改写。实际排查下来是MicroLIB的流式输出占用了栈空间和我的大静态工作区撞车。后来调试信息全部改成先格式化到字符串再输出或者直接用串口逐字节发送十六进制。第二件就是随机数问题。早期原型里我用了一个简单LFSR做随机数源结果连续签同一份数据时出现了大量相同签名特征。虽然最终生成的r/s值不完全一致但k的熵明显不足。后来改成ADC通道悬空采样末位bit 定时器捕获抖动 系统tick混合再用哈希置换生成真正的每个签名随机数k才彻底稳定下来。这个点如果你做量产产品一定不能省。第三件是官方测试向量本身的字节序。SM2国标文档里给的测试向量是一长串十六进制字符串比如私钥、公钥、z值、hash值都是连续字符串很多初学者直接按字符流读入结果内存数组里的顺序全反了。正确做法是“每两个十六进制字符组成一个字节按字符串从前往后的顺序放入数组”这样数组下标0对应最高位字节也就是大端表示。只要记住“内存数组顺序等于文档字符串顺序”这个坑就能避开。6. 最后再分享一个关于调试的小技巧如果你准备把这套源码集成到自己的工程里我强烈建议先把test_sm2.c里的官方测试向量跑通再去接业务逻辑。官方向量是国标文档附录里现成的通过自检说明底层大数和曲线运算是对的之后再排查问题就不用怀疑算法本身了。我自己第一次移植时跳过自检直接接通信协议结果验签老失败排查了大半天才发现是字节序问题白白浪费了时间。另外在Keil工程里建议把编译优化等级从默认的-O0直接调到-O2性能差异非常明显但记得把栈空间配置到4KB以上。这套源码现在在我多个项目里稳定跑了两三年希望也能帮你把国密算法顺利落地到低内存单片机上。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。