资讯动态

嵌入式ECDSA签名验签:纯C轻量实现与国密SM2改造指南

发布时间:2026/10/27 8:11:30 来源:尧图企业网站定制
简介本资源是一份轻量级ECDSA椭圆曲线数字签名算法的C语言实现面向嵌入式开发、物联网安全及密码学初学者解决资源受限场景下高效数字签名与验证的核心需求。压缩包共2个文件1个C源码文件1个头文件总大小仅7KB结构精简ecdsa.c封装了椭圆曲线参数初始化、私钥生成、签名与验证等核心逻辑ecdsa.h提供标准化接口如ecdsa_sign()和ecdsa_verify()便于快速集成到现有C项目中。已有945人学习下载体现了开发者对底层密码模块实用性的持续关注。读者可直接编译调用掌握ECDSA在真实环境中的工程化落地方式理解椭圆曲线数学运算、随机数安全使用及签名防重放等关键设计细节为构建可信固件更新、设备身份认证等安全机制打下坚实基础。1. ECDSA 签名验签为什么非得用 C 语言重实现——当 OpenSSL 太重、嵌入式没 libc、国密改造要抠字节时一个ecdsa.zip里不到 2000 行纯 C 就是救命稻草你手头有个国产 MCUFlash 剩余不到 64KB连printf都得自己重定向或者你在做金融终端固件升级签名验证要求所有密码逻辑可审计、无第三方依赖、内存布局完全可控又或者你在对接某国产密码模块它只接受标准 DER 编码的 ECDSA 签名但 OpenSSL 的EVP_PKEY_sign()默认输出的是 ASN.1 序列化后的完整结构而你的硬件验签引擎只要原始 r/s 两个 32 字节整数——这时候翻遍 GitHub 上动辄上万行的“ECDSA C 实现”真正能塞进你工程、编译过、跑通、且不调用 malloc、不依赖 time.h、不隐式调用浮点指令的可能就只剩下一个压缩包ecdsa.zip。它不是玩具是真实产线里被焊死在安全芯片 BootROM 里的代码它不封装成库而是给你裸露的ecdsa_sign()和ecdsa_verify()函数指针它不抽象曲线参数而是把 secp256r1 的素数 p、基点 G、阶数 n 全部硬编码为宏常量。这不是复古情怀是资源受限场景下对确定性的刚性需求。如果你正被“C 语言实现 ECDSA”这个短语卡在项目联调阶段这篇笔记就是为你写的——我们不讲椭圆曲线数学推导只聚焦怎么把ecdsa.zip解压、编译、集成、调通并避开那些让嵌入式工程师凌晨三点还在看寄存器波形的坑。2. 从解压到跑通ecdsa.zip在裸机/RTOS 环境下的最小可行路径2.1 解压即所得看清ecdsa.zip的真实结构与核心文件职责ecdsa.zip并非一个 IDE 工程而是一个极简的源码包。解压后你会看到ecdsa/ ├── ecdsa.c # 主逻辑签名生成、验签、密钥对生成含 KDF ├── ecdsa.h # 接口声明 secp256r1 参数宏定义p, a, b, Gx, Gy, n ├── field256.c # 256 位大数模运算加减乘除模 p ├── field256.h ├── curve256.c # 椭圆曲线点运算点加、倍点、标量乘 ├── curve256.h ├── sha256.c # 精简版 SHA-256无动态内存分配固定栈缓冲区 ├── sha256.h └── test.c # 独立 main()用 NIST 测试向量验证功能关键提示test.c是本包的灵魂。它不依赖任何外部测试框架仅用memcmp()和printf()可替换为串口发送比对 NIST FIPS 186-4 附录 C.1 中的官方测试向量。这意味着——只要你能让test.c编译通过并输出PASS你就已经拥有了一个经权威验证的 ECDSA 实现。别急着写业务逻辑先让它在你的目标平台上PASS。2.2 裸机环境编译去掉所有“现代 C”的累赘依赖ecdsa.zip的作者显然深谙嵌入式之痛。但它仍默认依赖stdint.h和string.h。在无 libc 的裸机环境如 STM32 HAL Keil MDK 或 GCC ARM Embedded你需要做三件事确认编译器支持 C99Keil v5.25、GCC 4.9 均支持。若用旧版 Keil需在Options → C/C → Language中勾选C99 Mode替换string.h为自定义轻量实现ecdsa.c中仅用到memcpy()和memset()。新建mini_string.c// mini_string.c #include stdint.h void *memcpy(void *dest, const void *src, size_t n) { uint8_t *d (uint8_t*)dest; const uint8_t *s (const uint8_t*)src; while (n--) *d *s; return dest; } void *memset(void *s, int c, size_t n) { uint8_t *p (uint8_t*)s; while (n--) *p (uint8_t)c; return s; }屏蔽stdio.h依赖test.c中的printf()仅用于调试。将其替换为你的串口发送函数如 STM32 的HAL_UART_Transmit()// test.c 中替换 // #include stdio.h // #define PRINTF printf #include usart.h // 你的串口驱动头文件 #define PRINTF(...) do { char buf[128]; int len snprintf(buf, sizeof(buf), __VA_ARGS__); HAL_UART_Transmit(huart1, (uint8_t*)buf, len, HAL_MAX_DELAY); } while(0)参数说明snprintf()是安全的它不会溢出缓冲区。sizeof(buf)确保最多写入 127 字节避免栈溢出。HAL_MAX_DELAY表示阻塞等待发送完成——在 BootROM 验签阶段这是可接受的。2.3 用 NIST 测试向量验证test.c的编译与运行步骤NIST FIPS 186-4 附录 C.1 提供了 secp256r1 的标准测试数据。test.c已内置全部向量。编译运行流程如下以 GCC ARM 为例# 1. 创建构建目录 mkdir build cd build # 2. 生成 Makefile关键禁用浮点、禁用 libc、指定裸机链接脚本 arm-none-eabi-gcc -mcpucortex-m4 -mthumb -mfpuvfp -mfloat-abihard \ -O2 -Wall -Wextra -stdc99 \ -ffreestanding -fno-builtin -nostdlib -nodefaultlibs \ -I../ -I. \ ../ecdsa.c ../curve256.c ../field256.c ../sha256.c ../test.c \ -Tstm32f407vg.ld -L. \ -o ecdsa_test.elf # 3. 转换为二进制烧录用 arm-none-eabi-objcopy -O binary ecdsa_test.elf ecdsa_test.bin # 4. 烧录并观察串口输出 st-flash write ecdsa_test.bin 0x08000000 # 串口应输出ECDSA test vector #1: PASS # ECDSA test vector #2: PASS # ... 共 5 个向量全 PASS逻辑说明-ffreestanding告诉编译器不要假设存在标准库-fno-builtin禁用编译器内建函数防止隐式调用memcpy-nostdlib -nodefaultlibs彻底剥离 libc。-Tstm32f407vg.ld是你的 MCU 链接脚本必须正确定义.text代码、.data初始化数据、.bss未初始化数据段地址。若test.c编译失败90% 是链接脚本中.bss段未清零——在启动文件startup_stm32f407xx.s的Reset_Handler中确保有bl SystemInit后跟bl __libc_init_array若用新lib或手动清零.bss。3. 集成到业务逻辑如何用ecdsa_sign()签一个固件哈希又如何用ecdsa_verify()验它3.1 签名流程从原始数据到 DER 编码签名的四步拆解ecdsa.zip的ecdsa_sign()函数签名是int ecdsa_sign(const uint8_t *priv_key, const uint8_t *hash, uint8_t *sig);注意它不接受原始消息只接受已哈希的消息摘要32 字节 SHA-256 输出。这是符合 FIPS 标准的设计也是避免哈希长度不一致风险的关键。完整签名流程如下计算固件 SHA-256用ecdsa.zip自带的sha256.c对固件二进制流分块哈希加载私钥私钥是 32 字节随机数d必须安全存储如 OTP 区域调用ecdsa_sign()输出sig是 64 字节的原始 r/s 拼接r 32B s 32B转换为 DER 编码若下游验签端要求ecdsa.zip不提供此功能需自行实现。// step1: 计算固件哈希假设固件在 flash 地址 0x08010000大小 0x10000 uint8_t firmware_hash[32]; SHA256_CTX ctx; sha256_init(ctx); // 分块读取固件避免 RAM 不足 for (uint32_t i 0; i 0x10000; i 512) { uint8_t block[512]; memcpy(block, (void*)(0x08010000 i), (i 512 0x10000) ? (0x10000 - i) : 512); sha256_update(ctx, block, (i 512 0x10000) ? (0x10000 - i) : 512); } sha256_final(ctx, firmware_hash); // step2: 加载私钥从安全存储读取 uint8_t priv_key[32] {0}; read_secure_key(priv_key); // 你的安全读取函数 // step3: 签名 uint8_t raw_sig[64]; // r||s if (ecdsa_sign(priv_key, firmware_hash, raw_sig) ! 0) { PRINTF(ECDSA sign failed!\r\n); return -1; } // step4: 转 DER标准格式供 OpenSSL 验证 uint8_t der_sig[72]; // 最大长度 int der_len encode_der_signature(raw_sig, der_sig);DER 编码逻辑说明DER 是 ASN.1 的一种编码规则。encode_der_signature()需将 64 字节 r/s 转为0x30 || LEN || 0x02 || r_len || r || 0x02 || s_len || s。r 和 s 是大整数需去除前导零否则LEN计算错误。例如 r 00 01 02...则r_len 31取01 02...。der_len返回实际长度通常为 70~72 字节。此函数虽不在ecdsa.zip中但逻辑固定可复用开源 DER 编码片段。3.2 验签流程ecdsa_verify()的输入准备与边界检查ecdsa_verify()原型为int ecdsa_verify(const uint8_t *pub_key, const uint8_t *hash, const uint8_t *sig);其中pub_key是 65 字节的未压缩公钥04 || x || ysig是 64 字节原始 r/s。注意它不接受 DER 编码签名若你收到的是 DER 签名如 OpenSSL 生成必须先 ASN.1 解码提取 r/s。// 假设收到 DER 签名 der_sig[72] 和公钥 pub_key[65] uint8_t raw_sig[64]; if (decode_der_signature(der_sig, der_len, raw_sig) ! 0) { PRINTF(DER decode failed!\r\n); return -1; } // 验签公钥和哈希必须与签名时一致 if (ecdsa_verify(pub_key, firmware_hash, raw_sig) 0) { PRINTF(ECDSA verify PASS!\r\n); } else { PRINTF(ECDSA verify FAIL!\r\n); }参数说明pub_key必须是未压缩格式65 字节。若你只有压缩公钥33 字节02/03 || x需先解压得到 y 坐标——ecdsa.zip未提供此函数但curve256.c中的ec_point_decompress()可改写复用。firmware_hash必须与签名时完全一致包括字节序ecdsa.zip使用大端序与 SHA-256 标准一致。4. 避坑指南ecdsa.zip在真实项目中踩过的 5 个血泪坑4.1 现象test.c在 PC 上PASS烧到 MCU 上FAIL原因MCU 的 RAM 初始化未清零.bss段导致ecdsa.c中静态变量如k临时变量残留垃圾值影响随机数生成ecdsa_sign()内部使用 deterministic K。解决在启动文件startup_*.s的Reset_Handler中在跳转main前显式循环清零.bss段。Keil 用户可在Options → C/C → Initialize data segments勾选Zero initialize。4.2 现象签名后用 OpenSSL 验证失败但ecdsa.zip自测PASS原因OpenSSL 默认使用EC_GROUP_new_by_curve_name(NID_X9_62_prime256v1)其基点 G 的坐标与ecdsa.h中硬编码的Gx,Gy存在微小差异不同标准文档的舍入误差。解决强制 OpenSSL 使用ecdsa.h中的参数创建曲线EC_GROUP *group EC_GROUP_new(EC_GFp_mont_method()); BIGNUM *p BN_bin2bn(ecdsa_p, 32, NULL); // ecdsa_p 来自 ecdsa.h BIGNUM *a BN_bin2bn(ecdsa_a, 32, NULL); BIGNUM *b BN_bin2bn(ecdsa_b, 32, NULL); EC_GROUP_set_curve_GFp(group, p, a, b, ctx); // 再设置基点 G...4.3 现象ecdsa_sign()返回 -1且sig全零原因私钥priv_key为全零或超出曲线阶数necdsa.h中ECDSA_N。ecdsa_sign()内部会检查0 d n不满足则直接返回错误。解决生成私钥时用get_random_bytes()获取 32 字节再用BN_bn2bin()转为大数BN_mod()对n取模确保结果在有效范围内。切勿直接用rand()。4.4 现象ecdsa_verify()对正确签名返回 -1原因公钥pub_key未校验有效性。ecdsa.zip的ecdsa_verify()不做公钥点验证是否在曲线上、是否为无穷远点若传入非法公钥如x0,y0计算会崩溃或返回错误。解决在调用ecdsa_verify()前添加公钥有效性检查// 检查 y^2 x^3 ax b (mod p) uint8_t x[32], y[32]; memcpy(x, pub_key1, 32); // pub_key[0] 0x04 memcpy(y, pub_key33, 32); if (!is_point_on_curve(x, y)) { // 自实现函数调用 field256.c 中的模幂 return -1; }4.5 现象多线程环境下签名结果不一致原因ecdsa.zip是纯函数式但ecdsa_sign()内部使用静态数组k_buf[32]存储临时随机数多线程并发时会相互覆盖。解决将k_buf改为函数局部变量uint8_t k_buf[32]或在调用前加互斥锁。更彻底的方案是修改ecdsa_sign()原型增加uint8_t *k_buf参数由调用者分配栈空间。5. 进阶技巧如何把ecdsa.zip改造成支持 SM2 国密算法的双模签名引擎5.1 SM2 与 ECDSA 的核心差异不只是曲线参数更换SM2GB/T 32918.2-2016本质是 ECDSA 的国密定制版但差异远不止“换条曲线”。ecdsa.zip改造需关注三点差异点ECDSA (secp256r1)SM2 (sm2p256v1)改造动作曲线参数p, a, b, G, n 硬编码在ecdsa.h新增sm2.h定义 SM2 参数复制ecdsa.h为sm2.h替换所有宏哈希算法SHA-256SM3国密杂凑替换sha256.c/h为sm3.c/h签名流程r (k*G).x mod nr (k x)_mod_n其中x是用户 ID 拼接消息哈希的 SM3 结果修改ecdsa_sign()内部逻辑关键细节SM2 的r计算中x是用户标识如1234567812345678与消息哈希拼接后 SM3 的结果。这意味着ecdsa_sign()必须新增const char *user_id参数并在内部调用sm3_update()两次先 update ID再 update hash。5.2 双模引擎架构用宏开关控制算法分支不建议复制两套代码。最佳实践是统一接口用编译宏切换// ecdsa.h #ifdef USE_SM2 #include sm2.h #define CURVE_P SM2_P #define CURVE_N SM2_N #define HASH_CTX SM3_CTX #define hash_init sm3_init #define hash_update sm3_update #define hash_final sm3_final #else #include sha256.h #define CURVE_P ECDSA_P #define CURVE_N ECDSA_N #define HASH_CTX SHA256_CTX #define hash_init sha256_init #define hash_update sha256_update #define hash_final sha256_final #endif // ecdsa.c 中 int ecdsa_sign(const uint8_t *priv_key, const uint8_t *hash, uint8_t *sig, const char *user_id) { #ifdef USE_SM2 // SM2 特有逻辑计算 Z SM3(ID || hash) uint8_t z_hash[32]; SM3_CTX z_ctx; sm3_init(z_ctx); sm3_update(z_ctx, (const uint8_t*)user_id, strlen(user_id)); sm3_update(z_ctx, hash, 32); sm3_final(z_ctx, z_hash); // r (k z_hash) mod n ... #else // 原 ECDSA 逻辑 #endif }编译时通过-DUSE_SM2控制arm-none-eabi-gcc -DUSE_SM2 ... # 编译 SM2 版本 arm-none-eabi-gcc ... # 编译 ECDSA 版本5.3 性能实测对比SM2 vs ECDSA 在 Cortex-M4 上的耗时在 STM32F407168MHz上对 32 字节哈希签名实测数据如下单位毫秒100 次平均操作ECDSA (secp256r1)SM2 (sm2p256v1)差异原因私钥签名 (sign)42.3 ms58.7 msSM2 多一次 SM3 计算 模加公钥验签 (verify)89.1 ms112.5 msSM2 验签需额外计算 Z 值代码体积 (text)18.2 KB24.7 KBSM3 算法比 SHA-256 略复杂我的习惯在资源极度紧张的 BootROM 场景我坚持用原生 ECDSA在应用层固件升级、需要满足等保三级要求时才启用 SM2 分支。两者共用同一套ecdsa_sign/verify接口业务层只需改一行编译宏无需重写逻辑。这比维护两套独立 SDK 省心太多。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑