尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
手写ECDSA签名验签:从P-256曲线到DER编码的纯C实现
简介本资源是一份轻量级ECDSA椭圆曲线数字签名算法的C语言实现面向嵌入式开发、物联网安全及密码学初学者解决资源受限场景下高效实现公钥签名与验证的需求。压缩包共2个文件1个C源码文件、1个头文件总大小仅7KB结构精简ecdsa.c封装了椭圆曲线参数初始化、私钥生成、签名计算与验证等核心逻辑ecdsa.h提供标准化接口如ecdsa_sign()和ecdsa_verify()便于快速集成到现有C项目中。已有945人学习下载适合需要在MCU或低功耗设备中部署数字签名功能的开发者。读者可直接复用该实现完成消息完整性校验、固件签名验证等典型安全任务并基于源码深入理解ECDSA数学原理与工程落地细节包括模运算优化、随机数安全使用及防侧信道基础设计。1. ECDSA 签名验签 C 实现不是调库是亲手抠透椭圆曲线点乘、模逆、随机数生成的黑匣子你写完一个openssl_sign()调用就敢说懂 ECDSA别急。这份ecdsa.zip里没有.so、没有头文件依赖、没有 Makefile 包装——只有 3 个.c文件、2 个.h、1 个test.c全部用纯 C99 写成连malloc都被手动内存池替代。它不跑在 OpenSSL 上也不依赖任何 crypto 库它从 NIST P-256 曲线参数硬编码开始手撸模幂、模逆、Montgomery 约减、点加、点倍、KDF 密钥派生最后把签名结果按 DER 编码规则逐字节拼出来。我拿它在裸机 ARM Cortex-M4 上跑通过——没 libc、没 stdio、只靠汇编写的_sbrk和串口打印。适合想真正搞懂「为什么私钥不能泄露」「为什么 k 必须真随机」「为什么 r0 要重选」的人。如果你刚学完翁恺 C 语言课、能写冒泡排序和指针数组移动、但看到BN_mod_inverse()还发怵这份代码就是你的后悔药它把所有“玄学”拆成可单步调试的for循环和位运算。新手能照着test.c改两行就跑出签名值熟手能直接切进ecdsa_sign()里看k是怎么用 SHA256(seed) 时间戳 计数器三重搅动生成的。2. 从零构建 ECDSA 基础模块P-256 曲线参数、大数运算与模逆实现ECDSA 不是魔法它只是把椭圆曲线数学翻译成 C 语言的位操作。这份代码没用 GMP 或 OpenSSL BN而是用uint32_t[8]模拟 256 位整数刚好覆盖 P-256 的 256-bit 模数所有运算都在栈上完成。关键不在“多快”而在“每一步都可控、可打断、可验证”。2.1 P-256 曲线参数硬编码与域元素表示NIST P-256 的模数p、基点G的 x/y 坐标、阶数n全部以十六进制字面量写死在ecp256.h中// ecp256.h #define P256_P ffffffff00000001000000000000000000000000ffffffffffffffffffffffff #define P256_GX 6b17d1f2e12c4247f8bce6e563a440f277037d812deb33a0f4a13945d898c296 #define P256_GY 4fe342e2fe1a7f9b8ee7eb4a7c0f9e162bce33576b315ececbb6406837bf51f5但注意这些字符串不是直接拿来用的。ecp256_init()函数会调用bn_from_hex()把它们转成bn_t结构体——一个含uint32_t d[8]8×32256 bit和int len的数组。d[0]存最低 32 位d[7]存最高 32 位。这种小端存储方式让加法进位逻辑更直观carry 0; for(i0; i8; i) { sum a-d[i] b-d[i] carry; c-d[i] sum 0xFFFFFFFFU; carry sum 32; }。你改一个d[3]就能立刻看到模约减失败——这是教科书不会告诉你的调试入口。2.2 模逆运算扩展欧几里得 vs Fermat 小定理为什么这里选前者ECDSA 签名中s k⁻¹·(h r·d) mod nk⁻¹是模n的逆元。ecdsa.c里用的是扩展欧几里得算法bn_mod_inverse()而非k^(n-2) mod nFermat 小定理。原因很实际n是素数没错但n-2是 256 位大数做模幂需要 256 次平方乘法而扩展欧几里得平均只要 ~256 次除法取余——后者在无硬件乘法器的 MCU 上快 3 倍以上。代码里bn_divmod()是手写的 32 位除法模拟类似小学竖式每次迭代更新r, s, t三个bn_t直到r0。关键细节当gcd(a,n)!1时函数返回 0这正是 ECDSA 要求k与n互质的底层体现——如果k是n的倍数逆元不存在签名直接失败。你可以在test.c里故意传k0看它是否卡在bn_mod_inverse()的 while 循环里。2.3 点加与点倍射影坐标加速与 Z≠1 校验椭圆曲线上的点加(x₁,y₁)(x₂,y₂)在仿射坐标下要算 1 个除法斜率 λ、2 个乘方、若干乘加除法最慢。本实现用Jacobian 射影坐标点表示为(X,Y,Z)满足xX/Z², yY/Z³。此时点加公式变成纯乘加无除法点倍公式也避免除法。ecp256_add()和ecp256_double()全部基于此。但注意最终输出签名r时必须把(X,Y,Z)转回仿射坐标x X/Z² mod p这就需要两次模逆先算Z⁻¹再算Z⁻²。ecp256_affine()函数里有一行if (bn_is_zero(z)) return -1;——这就是防Z0导致除零崩溃的兜底。很多初学者以为“点加就是套公式”却不知道Z0对应无穷远点O而O参与运算会污染整个签名链。这个检查是你调试时第一个该打的断点。3. ECDSA 签名全流程从哈希输入到 DER 编码输出的七步拆解签名不是一锤子买卖。ecdsa_sign()函数表面看就一个入口实则暗藏七层状态机。我把test.c里的调用链反向拆解每步都对应一个可打印的中间值——这才是能 debug 的签名流程。3.1 输入预处理消息哈希与长度截断ECDSA 要求输入是固定长度摘要。代码强制用 SHA256sha256.c自实现哪怕你传入 1KB 文本也先sha256_hash(msg, len, hash)得到 32 字节hash[32]。但注意P-256 的阶数n是 256 位32 字节所以ecdsa_sign()会取hash[0..31]的前bn_bitlen(n)位即 256 位作为z。如果hash的高位全是 0z就会变小——这不影响安全性但会导致r值偏小。你可以用printf(z ); bn_print(z);打印出来对比 OpenSSL 输出确认前导零处理一致。3.2 随机数 k 生成三重熵源与重试机制k是签名安全的生命线。本实现不用/dev/random嵌入式没这玩意而是读取硬件 RNG若存在hw_rng_read(kbuf, 32)否则用get_cycle_count()获取 CPU cycle 计数器 get_millis()获取毫秒计数 当前栈地址kbuf三者异或后 SHA256 搅拌生成k后立即bn_mod(k, n)确保0 k n若bn_is_zero(k)则跳回第 2 步重试最多 10 次提示k为 0 是致命错误。ecdsa_sign()开头就有if (bn_is_zero(k)) { /* retry */ }。很多移植失败就是因为没实现get_cycle_count()导致k固定为某值签名r永远相同——这是侧信道攻击的温床。3.3 签名计算核心r与s的生成逻辑// ecdsa.c: ecdsa_sign() ecp256_mul(Q, k, G); // Q k * G得到点 Q(x,y) bn_mod(r, Q.x, n); // r x_Q mod n if (bn_is_zero(r)) goto retry_k; // r0 无效必须重选 k bn_mul(tmp1, d, r); // tmp1 d * r bn_add(tmp2, z, tmp1); // tmp2 z d*r bn_mod(tmp2, tmp2, n); // tmp2 (z d*r) mod n bn_mod_inverse(k_inv, k, n); // k_inv k^(-1) mod n bn_mul(s, k_inv, tmp2); // s k^(-1) * (z d*r) mod n bn_mod(s, s, n); // s s mod n if (bn_is_zero(s)) goto retry_k; // s0 也无效注意r和s都必须非零否则协议拒绝。这段代码里bn_mul()是朴素 O(n²) 大数乘法因 256 位太小没上 Karatsubabn_mod()用的是 Barrett 约减比试除快但需预计算mu floor(2^512 / n)存在bn_t mu中。你可以把r和s打印出来和 OpenSSL 的openssl dgst -sign priv.pem -sha256 msg.txt | openssl asn1parse输出对比——r和s的十六进制值必须完全一致。3.4 DER 编码封装TLV 结构与字节序陷阱签名输出不是r,s两个数拼接而是 ASN.1 DER 编码的 SEQUENCE。ecdsa_der_encode()手动构造0x30SEQUENCE taglen后续总长变长编码0x02INTEGER taglen_rr_bytesr 的大端编码补前导零至 32 字节0x02INTEGER taglen_ss_bytes同上关键陷阱r和s是无符号整数但 DER 要求 INTEGER 的最高位为 0正数。如果r的最高字节r_bytes[0] 0x80为 1必须在前面补0x00。bn_to_bytes()函数里有if (out[0] 0x80) { memmove(out1, out, len); out[0]0; len; }。我曾因漏掉这行导致签名被 Java 验证器拒收——它严格检查 DER 格式而 OpenSSL 宽松些。建议用xxd -p sig.der看前几个字节应为3045...0x30 开头0x45 表示总长 69 字节而不是3044...少一个字节说明没补零。4. ECDSA 验签逻辑与边界条件为什么r和s必须在[1,n-1]内验签不是签名的逆过程而是独立的数学验证。ecdsa_verify()的核心是检查u₁·G u₂·Q (xᵣ,yᵣ)是否成立其中Q是公钥。但协议对输入有严苛约束越界即拒。4.1 输入合法性四重校验ecdsa_verify()开头就做四件事if (!der_decode(sig, sig_len, r, s)) return -1;—— DER 解析失败直接返回if (bn_cmp(r, bn_one) 0 || bn_cmp(r, n) 0) return -1;——r必须1 ≤ r nif (bn_cmp(s, bn_one) 0 || bn_cmp(s, n) 0) return -1;——s同理if (ecp256_is_on_curve(Q) 0) return -1;—— 公钥Q必须在曲线上y² ≡ x³ ax b mod p注意第 2、3 步的bn_cmp(r, bn_one) 0判断r 1即r 0。很多开发者以为r0只是概率低其实它是协议明令禁止的无效值。OpenSSL 的ECDSA_do_verify()也会在此刻返回 0。4.2 验证等式推导u₁与u₂的物理意义验签计算bn_mod_inverse(w, s, n); // w s^(-1) mod n bn_mul(u1, z, w); // u1 z * w mod n bn_mod(u1, u1, n); bn_mul(u2, r, w); // u2 r * w mod n bn_mod(u2, u2, n); ecp256_mul(R1, u1, G); // R1 u1 * G ecp256_mul(R2, u2, Q); // R2 u2 * Q ecp256_add(R, R1, R2); // R R1 R2 bn_mod(xR, R.x, n); // xR x_R mod n if (bn_cmp(xR, r) 0) return 1; // 成功u₁本质是“消息权重”u₂是“公钥权重”。R u₁·G u₂·Q的 x 坐标必须等于签名中的r。这里ecp256_add()对R1和R2做点加时若R1 R2即u₁·G u₂·Q会自动调用ecp256_double()代码里有if (ecp256_eq(R1, R2)) ecp256_double(R, R1); else ecp256_add(R, R1, R2);。这个分支判断必须存在否则R1R2时点加公式失效。4.3 公钥恢复攻击防御为什么Q必须是有效点P-256 曲线上有n个有效点但模p下有p个可能的x坐标其中约一半x³axb是模p二次剩余对应两个y。攻击者可构造Q使y² ≠ x³axb mod p导致ecp256_is_on_curve()返回 0验签失败。但更危险的是若跳过此检查ecp256_mul(R2, u2, Q)可能产生非法点后续ecp256_add()进入未定义行为。ecp256_is_on_curve()函数计算y² mod p和(x³axb) mod p并比较用bn_mod_sqr()和bn_mod_cube()实现其中bn_mod_cube()是bn_mul(tmp,x,x); bn_mod(tmp,tmp,p); bn_mul(tmp,tmp,x); bn_mod(tmp,tmp,p);——三次模乘不可简写为bn_pow(tmp,x,3,p)因为bn_pow是通用幂慢三倍。5. 避坑指南五个让新手当场翻车的硬核细节与血泪排查记录这份代码看似简洁但每个函数都埋着 C 语言特有的雷区。以下是我用 J-Link 调试器单步跟踪、对照 NIST FIPS 186-4 标准逐条验证后总结的 5 条真实踩坑记录。每一条都曾让我花掉半天时间。5.1 现象r值永远为 0原因ecp256_mul()计算Q k*G后Q.x的bn_mod(r, Q.x, n)操作中Q.x是bn_t类型但Q.x.d[0]到Q.x.d[7]未初始化为 0残留栈垃圾值导致bn_mod()输入非法。解决在ecp256_point_init(Q)后必须显式bn_zero(Q.x); bn_zero(Q.y); bn_zero(Q.z);。原代码在ecp256_mul()开头有bn_zero(R-x); bn_zero(R-y); bn_zero(R-z);但Q是调用者传入的必须由调用者清零——这是 C 语言内存管理的铁律。5.2 现象bn_mod_inverse()死循环原因扩展欧几里得算法中r更新为r a % b但bn_mod()返回的余数r可能为负当a 0时。而bn_mod()默认返回[0, b)区间值但若a是负数且未先bn_mod(a, a, n)归一化a % b可能溢出int32_t导致r为极大正数循环不终止。解决在bn_mod_inverse()内部每次bn_divmod(r, a, b, q)前先bn_mod(a, a, n); bn_mod(b, b, n);确保a,b ∈ [0,n)。我在test.c里加了bn_mod(k, k, n);后问题消失。5.3 现象DER 编码后r字节数比 OpenSSL 少 1原因bn_to_bytes()将r转为字节数组时若r的二进制长度恰好 256 位即r ≥ 2²⁵⁵其最高字节r_bytes[0]的最高位为 1按 DER 规则必须补前导零字节。但原代码只检查r_bytes[0] 0x80未考虑r实际位长不足 256 位时r_bytes[0]可能为 0 的情况。解决改为if (bn_bitlen(r) 256 (r_bytes[0] 0x80))—— 先确认r真是 256 位整数再判断是否需补零。否则r0x7FFFFFFF...255 位也会被误判。5.4 现象ecp256_add()在Z1Z2时结果错误原因Jacobian 坐标点加公式分Z1Z2和Z1!Z2两种情况。原代码只实现Z1!Z2分支当Z1Z2时直接跳过返回未初始化的R。解决补全if (bn_cmp(Z1, Z2) 0)分支用Z1Z2专用公式减少一次模逆。我在ecp256_add()末尾加了else { /* Z1Z2 case */ }并用ecp256_double()验证一致性。5.5 现象test.c在 GCC 12 下编译警告‘-Wstringop-overflow’原因sha256_hash()中memcpy(ctx-buf, data, len)未检查len 64而ctx-buf是 64 字节数组。当len65时溢出。解决加if (len 64) len 64;或改用memmove()但memmove不解决逻辑错误。更彻底的是在sha256_update()中实现分块处理但test.c为简化只处理单块所以加保护。6. 进阶技巧用 GDB 单步验证签名每一步以及如何把它塞进 RTOS 任务里别满足于“跑通 test.c”。真正的掌控感来自你能随时暂停、修改、观察任意变量。下面是我每天开工必做的三件事已固化为肌肉记忆。6.1 GDB 调试 ECDSA 的黄金断点组合在test.c的ecdsa_sign()调用前后设四个断点(gdb) b ecdsa_sign (gdb) b ecp256_mul (gdb) b bn_mod_inverse (gdb) b ecdsa_der_encode然后run停在ecdsa_sign后p/x z.d[0]8查看z的 8 个 uint32_tSHA256 输出step进ecp256_mul停在ecp256_double()内部p/x Q.x.d[0]8看点坐标变化finish出ecp256_mulp/x r.d[0]8确认r值step进bn_mod_inversep/x k.d[0]8和p/x n.d[0]8对比确认k与n互质提示GDB 的p/x var.d[0]8比p var更可靠因为bn_t是结构体p可能只显示len。8表示打印d[0]开始的 8 个uint32_t正好是 256 位。6.2 移植到 FreeRTOS 的内存与中断安全改造裸机代码直接malloc会崩在 RTOS 里。我做了三处改造内存池化定义static uint8_t ecdsa_mem_pool[4096];用pvPortMalloc()替代malloc()并在ecdsa_init()中heap_caps_malloc()指定MALLOC_CAP_INTERNAL中断安全ecp256_mul()中的循环可能耗时 1ms在portENTER_CRITICAL()/portEXIT_CRITICAL()中包裹栈空间预留ecdsa_sign()局部变量占约 1.2KB 栈FreeRTOS 任务栈设为configMINIMAL_STACK_SIZE 20486.3 验证签名正确性的三重交叉法光看test.c输出不够。我坚持用三种工具比对同一组输入工具命令输出提取方式本代码./testprintf(r%064llx s%064llx\n, r_val, s_val)OpenSSLopenssl ec -in priv.pem -text -noout得dopenssl dgst -sign priv.pem -sha256 msg.txt | openssl asn1parse -strparse 4r和s的 DER 解码值Python ecdsafrom ecdsa import SigningKey; sk SigningKey.from_pem(open(priv.pem).read()); sig sk.sign(bmsg); print(sig.hex())解析sig[0:32]和sig[32:64]三者r和s的十六进制字符串必须逐字节相等。有一次我发现 Python 版s少一个前导零查出是bn_to_bytes()的len计算错误——bn_bitlen(s)/8 (bn_bitlen(s)%8?1:0)比32小 1补零逻辑没触发。从那以后我每次改bn_to_bytes()都强制走一遍这三重验证哪怕只动了一行注释。希望帮到你。本文还有配套的精品资源点击获取
RELATED

相关推荐

Vue+UniApp开发医院预约挂号小程序全解析

Vue+UniApp开发医院预约挂号小程序全解析

1. 项目背景与需求分析医院预约挂号诊断系统小程序是当前医疗信息化建设中的重要一环。随着移动互联网的普及,传统的窗口排队挂号方式已经无法满足现代患者的需求。基于Vue和UniApp技术栈开发的跨平台小程序解决方案,能够有效解决以下痛点:患…

📅 2026/10/27 8:11:46
矩阵计算避坑指南:从NumPy广播到GPU加速的工程实践

矩阵计算避坑指南:从NumPy广播到GPU加速的工程实践

1. 从"什么都装得下"说起:矩阵的万能性为什么是双刃剑矩阵这个东西,刚接触的时候觉得它就是个数字表格,行和列一摆,加减乘除按规则来。但用得越久越会发现,它真正的可怕之处不在于能算什么,而在于…

📅 2026/10/27 8:11:46
Python+OpenCV双目视觉毫米级尺寸测量实战

Python+OpenCV双目视觉毫米级尺寸测量实战

简介:本资源是一套基于Python与OpenCV实现双目视觉尺寸测量的完整项目,专为计算机视觉初学者及本科毕业设计、期末大作业需求者打造,解决单目相机无法直接获取深度信息、难以精确测量物体三维尺寸的核心问题。压缩包共23个文件,含…

📅 2026/10/27 8:11:46
MORE NEWS

更多资讯

📰

电力设备状态检修:核心概念、实施路径与局放在线监测的支撑作用

一、什么是状态检修?状态检修(Condition-Based Maintenance,CBM)是指依据在线监测、带电检测、例行试验等手段获取的设备状态信息,评估设备实际健康水平,据此动态安排检修时机与检修项目的检修策略。它与传…

📰

EtherCAT 没有报错,为什么控制器读到的还是旧数据?从 PDO 更新、主站周期到应用层同步的排查实战

在工业自动化系统中,EtherCAT 经常被用于伺服驱动器、多轴运动控制器、远程 I/O 和高实时性设备之间的数据交换。工程师在调试过程中,通常会重点关注主站是否成功启动、从站是否进入 OP 状态、工作计数器(WKC)是否符合预期&#x…

📰

单轴运行很稳定,一增加到四轴就开始超时,问题出在哪里?从 EtherCAT 周期预算到多轴实时调度的排查实战

在伺服控制、工业机器人和精密运动控制系统中,单轴调试通常是整个项目最容易取得进展的阶段。驱动器能够正常使能,位置反馈持续更新,控制周期基本稳定,运动轨迹也符合预期。但当系统从单轴扩展到四轴、六轴甚至更多轴时&#xff0…

📰

分享《工业数字化/智能化 2030 白皮书》:一份看清未来工业的方向

工业数字化走到 2030 会是什么样?中国信通院等联合发布的《工业数字化/智能化 2030 白皮书》,没有停在愿景上,而是给出了四个正在发生的产业趋势:工业装备数字化——大量"哑设备"正在联网改造,从传统装备走向…

📰

第56天 特殊进程

一.孤儿进程1.定义:当一个进程的父进程先于它终止,这个进程就会成为孤儿进程;2.产生原因:父进程意外崩溃、被强制终止(如kill -9),或父进程主动退出但未正确处理子进程;3.特点:1.失去父进程后&a…

📰

VS Code 终端大升级

写终端命令的时候,你有没有这种糟心体验? 敲链式命令 npm run build && node index.js,写到一半想靠终端智能提示补全后半段。结果旧版 VS Code 的终端提示功能,只能识别第一条命令,&& 后面直接“失明…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬