Serial-Studio 中的 p256-m 深度解析:面向受限 32 位环境的极简 P-256 ECDH/ECDSA 实现

📅 发布时间:2026/9/18 8:55:08
Serial-Studio 中的 p256-m 深度解析:面向受限 32 位环境的极简 P-256 ECDH/ECDSA 实现
Serial-Studio 中的 p256-m 深度解析面向受限 32 位环境的极简 P-256 ECDH/ECDSA 实现【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio导读p256-m 是一个在 NIST P-256 曲线上实现 ECDH椭圆曲线 Diffie-Hellman 密钥协商与 ECDSA椭圆曲线数字签名算法的极简开源实现专为资源受限的 32 位嵌入式环境设计以正确性与安全优先于体积、体积优先于速度为设计准则最终以不足 3KiB 的代码和不足 768 字节的 RAM 提供了完整的两类密码学原语。本文以本仓库中随 Mbed TLS 一同分发的 p256-m 原始 README 为核心骨架结合仓库内实际存在的源码、头文件与构建配置系统讲解其正确性保障、恒定时间安全设计、体积/内存/性能指标、分层实现架构以及它在 Mbed TLS PSA Crypto 驱动体系中的集成方式。读完本文你将理解一个以安全性换体积的极简密码实现如何被设计、测试、度量和接入上层加密框架。一、p256-m 是什么一个以正确性与体积为先的 P-256 实现p256-m 由 Manuel Pégourié-Gonnard 开发是 NIST P-256 曲线上 ECDH 与 ECDSA 的极简实现特别适合受限的 32 位环境。它使用标准 C 编写并为 Arm Cortex-M 与 Cortex-A 系列 CPU 提供了可选的内联汇编优化。在本仓库中它位于 lib/mbedtls/3rdparty/p256-m/p256-m/作为 Mbed TLS 的第三方组件被分发采用 Apache-2.0 OR GPL-2.0-or-later 双许可证。与大多数速度优先、体积其次的密码实现不同p256-m 的设计目标按如下优先级排序正确性与安全性correctness security低代码体积与低 RAM 占用low code size RAM usage运行时性能runtime performance。作者在 README 中直言大多数密码实现更关心速度而非占用p256-m 的诞生源于我想看看把通常的侧重点颠倒过来会发生什么。结果是在不足 700 行 C 代码含注释超过代码行数内交付了完整的 ECDH 与 ECDSA代码体积小于 3KiBRAM 占用小于 768 字节而性能与主流实现相当详见下文对比章节。注意本仓库内分发的是 Mbed TLS 快照Mbed TLS 侧说明 明确指出这里只包含 p256-m 文件的一个子集README 中提到的部分辅助脚本如coverage.sh、sizes.sh、bench.sh、p256.py等属于上游仓库并不在本仓库中且此快照不会随上游定期更新。二、正确性极简 API、全输入校验与多维测试2.1 API 设计极简接口 具体错误码p256-m 的 API 设计遵循三个原则公开函数数量极少、每个函数完整校验输入并返回具体错误码、所有输入输出均为字节数组。README 声称 API 只有 4 个公开函数密钥对生成、ECDH 共享密钥、ECDSA 签名、ECDSA 验证。在 Mbed TLS 分发的 p256-m.h 中除了这 4 个核心操作还补充了p256_validate_pubkey()、p256_validate_privkey()与p256_public_from_private()三个辅助函数用于在不执行操作的情况下单独校验密钥、或由私钥导出公钥。完整的接口与错误码如下函数作用错误返回p256_gen_keypair(priv[32], pub[64])生成密钥对大端字节序P256_RANDOM_FAILEDp256_ecdh_shared_secret(secret[32], priv[32], pub[64])计算 ECDH 共享密钥P256_INVALID_PRIVKEY/P256_INVALID_PUBKEYp256_ecdsa_sign(sig[64], priv[32], hash, hlen)对给定哈希签名P256_RANDOM_FAILED/P256_INVALID_PRIVKEYp256_ecdsa_verify(sig[64], pub[64], hash, hlen)验证签名P256_INVALID_PUBKEY/P256_INVALID_SIGNATUREp256_validate_pubkey(pub[64])/p256_validate_privkey(priv[32])单独校验公/私钥P256_INVALID_PUBKEY/P256_INVALID_PRIVKEYp256_public_from_private(pub[64], priv[32])由私钥计算公钥P256_INVALID_PRIVKEY错误码定义见 p256-m.hP256_SUCCESS 0P256_RANDOM_FAILED -1P256_INVALID_PUBKEY -2P256_INVALID_PRIVKEY -3P256_INVALID_SIGNATURE -4。私钥/公钥分别以 32 字节与 64 字节大端整数表示。2.2 测试策略向量验证、双套测试与动态分析p256-m 的正确性通过多层测试保障标准测试向量针对多个 RFC 与 NIST 测试向量进行了验证构造性输入使用精心构造的输入进行负向测试并触达边界情况双测试套件一是 closed-box闭箱测试仅通过公开 API 驱动二是 open-box开箱测试直接对内部函数做单元测试并利用对 RNG 使用方式的了解触达更多错误分支分支覆盖闭箱测试覆盖除 4 个分支外的所有分支其中 3 个由开箱测试借助作弊的 RNG 触达最后一个分支只能通过在 P-256 上计算离散对数才能到达这意味着该分支在实践中几乎不可达动态分析valgrind、ASan地址消毒器、MemSan内存消毒器、UBSan未定义行为消毒器。2.3 代码质量与 Short Weierstrass 陷阱代码为纯标准 C99可在clang -Weverything与gcc -Wall -Wextra -pedantic下无警告编译代码小而文档完善内部 API 也有注释注释行数多于代码行数。作者也坦言作为个人项目它尚未经过独立审计。针对 safecurves 社区指出的 Short Weierstrass 曲线NIST P-256 属于此类两类经典陷阱p256-m 做了针对性规避对某些罕见曲线点产生错误结果通过在全代码中仔细检查所用公式的有效性域来避免当输入不是曲线点时泄漏秘密数据通过每次反序列化点时都验证点确实在曲线上来避免。三、安全设计从底层恒定时间到明确的威胁模型3.1 恒定时间属性在正确性之外p256-m 宣称具备两条硬性安全性质不存在哪怕间接依赖秘密数据的分支不存在哪怕间接依赖秘密数据的内存访问。这两条性质通过 valgrind 与 MemSan 配合 ctgrind 的思路进行检查README 中的consttime.sh。源码中对应的机制是CT_POISON/CT_UNPOISON宏见 p256-m.c把秘密数据标记为未初始化任何依赖它的分支或指针解引用都会触发工具告警公开数据则标记回已初始化。此外为避免执行时间依赖操作数取值的指令或库函数p256-m从不使用整数除法默认只使用16x16→32 位无符号乘法在具有恒定时间 32x32→64 位无符号乘法指令的内核上可通过在编译期定义宏MUL64_IS_CONSTANT_TIME启用该指令以改善性能与体积在 Cortex-M / Cortex-A 上配 GCC 或 Clang 时无需此宏因为直接使用了内联汇编。核心实现 中u32_muladd64()的四种实现正是这一策略的体现两条内联汇编路径有 DSP 扩展的UMAAL单周期指令、无 DSP 扩展时拆分为 16 位半字乘法以及两条纯 C 路径MUL64_IS_CONSTANT_TIME时直接用 32x32→64 乘法否则用 16x16→32 乘法展开。3.2 威胁模型能防什么、不防什么由此 p256-m 声明可抵御以下三类攻击者只能操纵输入、观察输出的攻击者还能测量操作总耗时的攻击者计时攻击还能以任意精度观察与操纵微架构特性如缓存、分支预测器的攻击者。同时它明确不抵御能记录 CPU 处理秘密时物理辐射功耗、电磁、声音轨迹的被动物理攻击者能向计算中注入故障的主动物理攻击者。README 还补充p256-m 由于整体恒定流量constant-flow实际上应能抵御 SPA简单功耗分析但预计无法抵御任何其他物理攻击。它也没有 TinyCrypt 那样的坐标随机化因此不针对 DPA 类攻击设防。3.3 关键前提外部 RNG 与密钥擦除p256-m 不自带加密安全随机数发生器CSPRNG。它要求外部提供p256_generate_random()函数声明见 p256-m.h。README 给出明确警告如果该函数不是加密安全的那么 p256-m 的密钥生成与 ECDSA 签名生成也不安全。在本仓库的 Mbed TLS 集成中该函数包装了psa_generate_random()见 p256-m.c复用 PSA 层的 CSPRNG。同时p256-m 遵循返回前安全擦除栈上秘密数据的最佳实践。四、代码体积不足 3KiB 的秘密README 给出的体积数据基于 ARM-GCC 9、-mthumb -Os编译内核代码体积Cortex-M02988 字节Cortex-M42900 字节Cortex-A72924 字节Clang 也被尝试过但生成的代码通常大约大 10%详见上游sizes.sh。体积中包含的部分完整的输入校验与输入/输出字节序列化/反序列化函数返回前从栈上清理秘密值零依赖代码不依赖任何 libc 函数或工具链运行时库如长乘法辅助函数——这一点可由上游deps.sh脚本对 Arm-GCC 工具链检查确认。体积中不包含的部分安全的 RNG 函数需外部提供见上节p256_generate_random()。五、RAM 使用零堆分配仅栈使用p256-m 不使用任何动态内存堆只使用栈。README 给出 4 个公开函数在各内核上的栈占用函数Cortex-M0Cortex-M4Cortex-A7p256_gen_keypair608564564p256_ecdh_shared_secret640596596p256_ecdsa_sign664604604p256_ecdsa_verify752700700单位字节上述数据假设外部 RNG 函数最多使用 384 字节栈空间。p256_ecdsa_verify因为需要处理双标量运算而成为栈消耗最大的操作。六、运行时性能与主流实现相当README 的性能数据在三个平台测得Cortex-M0 48 MHzSTM32F091 开发板运行 Mbed OS 6Cortex-M4 100 MHzSTM32F411 开发板运行 Mbed OS 6Cortex-A7 900 MHzRaspberry Pi 2B运行 Raspbian Buster。各公开函数耗时毫秒函数Cortex-M0Cortex-M4Cortex-A7p256_gen_keypair92114511p256_ecdh_shared_secret92214411p256_ecdsa_sign99015512p256_ecdsa_verify197630924四项合计480975359README 指出这四项操作之和大致对应一次使用 ECDHE-ECDSA、基于裸公钥或直接信任证书做双向认证的 TLS 握手否则对端证书链中每增加一级就需要额外增加一次 verify。此外以上数据用 GCC 编译能使用内联汇编如果没有那 22 行Cortex-M0/1 行Cortex-M4内联汇编这些平台上代码大约慢 2 倍Cortex-A7 上影响小得多。七、与 TinyCrypt 的对比体积换性能的代价与收益README 选取 TinyCrypt 作为对比对象因为它同样是仅面向 P-256、只做 ECDH 与 ECDSA 的独立实现、同样面向受限设备而其他实现往往支持多曲线并基于共享的大数/MPI 模块可能还支持 RSA不利于公平比较。代码体积字节内核p256-mTinyCryptCortex-M029886134Cortex-M429005934Cortex-A729245934RAM 使用Cortex-M0栈字节操作p256-mTinyCrypt密钥生成608824ECDH 共享密钥640728ECDSA 签名664880ECDSA 验证752824RAM 使用Cortex-M4 或 Cortex-A7数值相同操作p256-mTinyCrypt密钥生成564796ECDH 共享密钥596700ECDSA 签名604844ECDSA 验证700808运行时性能毫秒Cortex-M0 48 MHz操作p256-mTinyCrypt密钥生成921979ECDH 共享密钥922975ECDSA 签名9901009ECDSA 验证19761130四项合计48094093运行时性能毫秒Cortex-M4 100 MHz操作p256-mTinyCrypt密钥生成145178ECDH 共享密钥144177ECDSA 签名155188ECDSA 验证309210四项合计753753运行时性能毫秒Cortex-A7 900 MHz操作p256-mTinyCrypt密钥生成1113ECDH 共享密钥1113ECDSA 签名1214ECDSA 验证2415四项合计5955运行时性能微秒64 位 Intel i7-6500U 2.50 GHzUbuntu 20.04操作p256-mTinyCrypt密钥生成10601627ECDH 共享密钥10601611ECDSA 签名11361712ECDSA 验证22791888四项合计55356838其他关键差异README 原文要点输入校验p256-m 全面校验所有输入TinyCrypt 的 ECDH 共享密钥函数不校验对端公钥静态 ECDH 场景下需用户另行校验文献记载了用户忘记校验时存在攻击。安全特性取向p256-m 从底层起即完全恒定时间对强大本地攻击者如不受信 OS 攻击安全 enclave更稳健TinyCrypt 则包含坐标随机化可抵御部分被动物理攻击如 DPA这是 p256-m 完全不考虑的。可扩展性TinyCrypt 的代码看起来容易扩展支持更多曲线p256-m 为压体积做了大量硬编码详见下文其他曲线。归约算法TinyCrypt 用专门例程利用曲线素数是 Solinas 素数这一结构做模归约理论比 p256-m 的通用 Montgomery 归约快但其他因素似乎抵消了这一点。点运算公式TinyCrypt 用 Co-Z Jacobian 公式更快但稍大p256-m 用混合 affine-Jacobian 公式。汇编p256-m 在基准平台上用少量内联汇编做 64 位乘法TinyCrypt 只用 C及编译器运行时库。验证优化TinyCrypt 用基于 Shamir 技巧的专门例程做 ECDSA 验证性能远好于 p256-m 为压体积而采用的通用代码——这正是 p256-m 验证性能偏弱的原因。总体结论p256-m 在代码体积与 RAM 上全面领先约一倍在密钥生成/ECDH/签名上与 TinyCrypt 相当甚至略快但 ECDSA 验证尤其小端 CPU 上明显偏慢这是它坚持通用代码 最小体积路线的必然取舍。八、设计概览单文件、五层架构整个实现集中在单个文件p256-m.c共 1514 行中大部分函数保持static以利于优化。实现按职责分为五层8.1 固定宽度多精度算术层大整数表示为uint32_t数组limb最低有效位在前256 位即 8 个 limb见 p256-m.c。可能产生进位时使用向uint64_t的强制转换引导编译器使用 CPU 的进位标志可能溢出时函数返回进位标志。该层提供加、减、乘加、按 limb 移位服务于 Montgomery 乘法、条件赋值、相等比较、与零比较、大端字节序列化/反序列化。内部函数u32_muladd64()在此层有 4 种实现两种 Cortex-M/A 汇编 两种纯 C见前文第三节。8.2 固定宽度模运算层所有模运算都在Montgomery 域中进行即 x 以x * 2^256 mod m表示计算前需转入该域计算后转回。曲线素数 p 与阶 n 对应的 Montgomery 常量预计算并存储在静态结构中。模逆使用费马小定理计算以获得与所逆值无关的恒定时间行为。该层提供 p/n 常量及 Montgomery 常量、模加减乘逆、小值赋值、Montgomery 域转换、以及带范围检查与 Montgomery 域转换的字节序列化/反序列化。8.3 曲线点运算层曲线点使用仿射affine或 Jacobian 坐标表示仿射坐标扩展用 (0,0) 表示零点。单个坐标始终处于 Montgomery 域。并非所有仿射/Jacobian 公式都是完备的代码对每个函数的前置条件做了细致记录与满足。该层提供曲线常量 b、基点坐标、点有效性检查在曲线上且非零、Jacobian→仿射转换、Jacobian 坐标点倍完备公式、混合 affine-Jacobian 点加P ∉ {0, Q, -Q}、仿射坐标点加或点倍泄漏版仅用于全部数据公开的 ECDSA 验证、以及带有效性检查的字节序列化/反序列化。8.4 标量运算层核心是标量乘法采用有符号二元阶梯signed binary ladder——经典 double-and-add 的变体每一步都执行一次加/减。代码同样保证加法公式的前置条件始终满足。有符号二元阶梯要求标量为奇数必要时通过同时对标量模 n和输入点取负来保证。该层提供标量乘法、带范围检查的字节反序列化、标量及其公钥的生成。8.5 公共 API 层该层构建于以上各层之上与前几层不同所有输入输出都是字节数组。密钥生成与 ECDH 共享密钥计算只是内部函数的薄封装负责格式转换与错误处理ECDSA 函数则有更多非平凡逻辑如签名中的随机数处理、验证中的双标量运算。8.6 测试设计Python 参考实现驱动上游采用先写一个自包含的纯 Python 参考实现p256.py作为热身并辅助核对中间值的路线内嵌多个来源的测试向量。第二个脚本gen-test-data.py基于该参考实现生成更多正/负测试数据输出为 C 头文件供闭箱与开箱测试程序使用。另外p256-m 可编译带额外插桩的版本把秘密数据标记出来让 valgrind 或 MemSan 检查是否有分支/内存访问哪怕间接依赖它——相关宏定义在文件顶部即前文第三节的CT_POISON/CT_UNPOISON。8.7 已测试平台u32_muladd64()有 4 个版本带/不带 DSP 扩展的两种汇编、MUL64_IS_CONSTANT_TIME开/关的两种纯 C对应测试矩阵为x64 上make测试不带MUL64_IS_CONSTANT_TIME的纯 C 版Clangx64 上./consttime.sh测试两种纯 C 版ClangArm v7-ARaspberry Pi 2上make测试 Arm-DSP 汇编版ClangCortex-M0/M4 开发板上的on-target-*box测试两种汇编版GCC。此外sizes.sh用 GCC 与 Clang 为三个 Arm 内核构建以测体积deps.sh用 GCC 检查外部依赖。九、在当前仓库中的集成作为 Mbed TLS 的 PSA 加速驱动p256-m 在本仓库中的价值不仅在于算法本身更在于它被接入了 Mbed TLS 的PSA Crypto 加速驱动体系作为软件加速器software accelerator的示例实现。9.1 目录与构建结构核心源码与头文件p256-m/p256-m.c、p256-m/p256-m.hPSA 驱动入口p256-m_driver_entrypoints.c 与 p256-m_driver_entrypoints.h构建脚本CMakeLists.txt 构建独立的p256m静态库目标lib/mbedtls/3rdparty/CMakeLists.txt 中的add_subdirectory(p256-m)将其纳入整体构建Makefile.inc 提供 Make 构建路径的源文件与头文件清单。9.2 启用宏启用驱动需要在配置头中定义MBEDTLS_PSA_P256M_DRIVER_ENABLED见 mbedtls_config.h默认注释关闭。该宏在驱动入口头文件中还会自动定义PSA_CRYPTO_ACCELERATOR_DRIVER_PRESENT告知 PSA 层存在加速驱动。9.3 驱动入口与格式转换驱动入口实现了 PSA 透明驱动规定的 6 个入口p256_transparent_import_key、p256_transparent_export_public_key、p256_transparent_generate_key、p256_transparent_key_agreement、p256_transparent_sign_hash、p256_transparent_verify_hash。其核心工作之一是密钥格式转换PSA 导出的 SECP256R1 公钥是 65 字节前导0x04 64 字节公钥即未压缩 ECPoint 格式私钥是 32 字节p256-m 内部公钥格式只有 64 字节与 PSA 相同但无前导字节私钥格式与 PSA 一致因此向 p256-m 传入公钥前需去掉前导字节peer_key 1/key_buffer 1导出公钥时则补回0x04p256-m_driver_entrypoints.c共享密钥与签名格式在 PSA 与 p256-m 之间完全一致无需转换。错误码转换由p256_to_psa_error()完成P256_SUCCESS → PSA_SUCCESSP256_INVALID_PUBKEY/PRIVKEY → PSA_ERROR_INVALID_ARGUMENTP256_INVALID_SIGNATURE → PSA_ERROR_INVALID_SIGNATUREP256_RANDOM_FAILED → PSA_ERROR_GENERIC_ERRORp256-m_driver_entrypoints.c。verify_hash入口还通过psa_driver_wrapper_export_public_key()先统一把密钥规范为 65 字节公钥格式再交给 p256-m全程避免动态分配。所有入口均坚持无动态内存原则栈上分配固定缓冲。9.4 快照性质与 RNG 复用如 Mbed TLS 侧说明 所述p256-m 刻意不提供自己的 CSPRNG因此在 Mbed TLS 集成中p256_generate_random()包装了 PSA 的psa_generate_random()p256-m.c随机性安全边界完全由 PSA 层的 RNG 决定。同时要留意本仓库内的 p256-m 文件不会随上游频繁更新可能缺少上游项目的最新修复与改进。十、可移植性与扩展硬编码的边界10.1 扩展其他曲线需要改什么README 明确指出最小代码体积只能通过对特定曲线做专门化实现来达到。要在分层结构上扩展其他曲线各层需要做如下调整多精度算术层limb 数硬编码为 8。对同为 256 位的曲线无需改动不同位宽的曲线改为硬编码其他值若支持多种位宽则需为每个函数增加 limb 数参数数组声明统一用最大 limb 数。模运算层p、n 及其 Montgomery 常量硬编码。换曲线只需替换常量支持多曲线则定义全部常量且只保留已接受mod参数的函数删除m256_xxx_p()便捷函数mod参数可同时携带 limb 数。曲线点运算层曲线常量 b 与基点坐标 gx、gy 硬编码常量 a 利用a -3的标准优化在point_double()第一步省一次乘法隐式硬编码对 a ≠ -3 的曲线需恢复常规计算b ! 0被间接利用——保证 (0,0) 不在曲线上从而可复用该值表示零点目前已标准化的 Short Weierstrass 曲线均满足 b ≠ 0曲线形状假设为 Short Weierstrass对 Montgomery、(twisted) Edwards 等其他形状本层实现与 API 都会大不相同。标量运算层多曲线时所有函数需增加 curve id 参数本层假设曲线阶 n 的位宽与模数 p 相同绝大多数已标准化曲线如此唯一例外是 secp224k1需为 n 与标量表示增加一个 limbscalar_mult()中阶的位宽硬编码该函数还利用了阶 n 的次低有效位为 1 来避免特殊情形不满足此条件的曲线阶需单独处理乘 ±2 的情形单独计算再条件赋值。公共 API 层多曲线时需增加 curve id 参数并处理可变大小 I/OECDH API 在不同曲线形状下仍相似仅公钥尺寸不同但 ECDSA API 不适用于 Montgomery/Edwards 曲线后者需要 EdDSA 风格的接口。10.2 对其他平台的假设p256-m 虽为标准 C99但面向受限 32 位平台做了如下假设uint8_t、uint16_t、uint32_t、uint64_t类型必须存在32 位无符号加减带进位必须是恒定时间的16x16→32 位无符号乘法必须可用且恒定时间。即便平台上存在带进位的 64 位加减、甚至 64x64→128 位乘法p256-m 也不使用它们尽管它们能显著提升性能。README 给出的改进方向是把内部 API 中uint32_t数组替换为自定义类型在 64 位平台上将该类型定义为uint64_t数组并在多精度算术层做相应适配。最后可选的汇编代码在被测 Cortex-M 上带来约 2 倍性能提升同时略微减小代码体积与栈占用目前仅适用于支持GCC 扩展 asm 语法的编译器包括 GCC 与 Clang。结语p256-m 是一份值得仔细研读的极简密码实现范本它在不足 3KiB 的代码与不足 768 字节的 RAM 预算内交付了恒定时间、全输入校验的 P-256 ECDH/ECDSA并通过分层架构多精度算术 → 模运算 → 曲线点 → 标量 → 公共 API让安全与体积目标层层落地。它的取舍同样鲜明——E CDSA 验证用通用代码换体积而牺牲速度、不做物理侧信道防护、不支持多曲线——这些边界在 README 中被坦率记录。在本仓库中它进一步以 Mbed TLS PSA 软件加速驱动的形态接入上层加密框架成为理解算法实现 ↔ PSA 驱动 ↔ 上层 API三层关系的现成案例。相关一手材料均可在本仓库内查阅原始 README、公共头文件、核心实现、PSA 驱动入口 与 Mbed TLS 集成说明。【免费下载链接】Serial-StudioOpen-source telemetry dashboard. Supports UART, BLE, MQTT, Modbus, CAN Bus and more.项目地址: https://gitcode.com/GitHub_Trending/se/Serial-Studio创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考