纯C实现HMAC-SHA1:嵌入式项目中轻量级消息校验方案

发布时间:2026/10/16 14:17:38
纯C实现HMAC-SHA1:嵌入式项目中轻量级消息校验方案 简介基于C语言实现的HMAC_SHA1加密源码包面向物联网、嵌入式及网络安全方向的开发者主要用于设备登录认证、数据完整性校验等场景。压缩包内共2个文件其中hmac_sha1.c负责算法核心逻辑包括上下文初始化、消息更新、摘要计算等hmac_sha1.h提供对外接口与数据结构定义便于直接集成到现有C工程。整体仅3KB精简轻量适合学习或嵌入到资源受限环境。已有3319人学习浏览在阿里云物联网套件等设备接入场景中该算法常被用来基于预共享密钥生成动态令牌防止重放与篡改。拿到源码后可直接调用hmac_sha1()完成密钥与消息的认证计算也可参考实现深入理解HMAC_SHA1工作原理及C语言实现细节。1. HMAC-SHA1 的 C 语言实现为何这份纯 C 源码能直接塞进你的嵌入式项目做过设备端固件签名、接口鉴权或消息完整性校验的工程师大概都有过这种经历产品要加 HMAC-SHA1 做数据防篡改OpenSSL 库太大移植到 MCU 上 ROM 直接爆表网上找到的 HMAC_SHA1 代码要么依赖平台库、要么缩进混乱加密结果和在线工具对不上查了半天不知道是密钥处理错还是 SHA1 本身写了 bug。这份 HMAC-SHA1 加密 C 语言源码解决的就是这类问题不依赖任何第三方库纯 C 实现一个 .c 和一个 .h 就能完成 HMAC_SHA1 的全部计算适合直接嵌入到各种嵌入式环境或作为独立工具链的一部分。它能让你绕过 OSA 层、网络库和平台抽象拿到一份可读、可编译、可验证的算法本体配合测试向量做一次白盒校验之后就能放心地集成到通信协议、文件校验或设备认证流程里。适合刚接手协议对接的嵌入式开发也适合需要在服务器端快速验证算法正确性的后端工程师。2. HMAC 与 SHA1先搞清楚标准算法在做什么再动手改代码2.1 HMAC 结构密钥填充、ipad/opad 与两轮哈希的完整链路HMAC 并不是一种全新的哈希算法它是在 SHA1 之上叠加了一层密钥混合机制。RFC 2104 定义了固定流程先把密钥 K 处理成块大小 BSHA1 的 B 是 64 字节如果密钥长度超过 B就先对 K 做一次 SHA1 得到 20 字节摘要再补零到 B如果不足 B直接在右侧补零到 64 字节。随后生成两个派生密钥ipad内填充是 0x36 重复 64 次与处理后的密钥逐字节异或opad外填充是 0x5c 重复 64 次与密钥异或。最终 HMAC 值等于 SHA1(opad 拼接 SHA1(ipad 拼接消息 M))。这条链路里最容易被新手改错的就是密钥预处理顺序必须先判断密钥长度决定是直接补零还是先做 SHA1而不是一律补零。源码里通常把这个过程拆成三个函数初始化上下文、更新消息、输出摘要和 OpenSSL 的 HMAC_Init/HMAC_Update/HMAC_Final 接口保持平行。我从这份源码里摘出核心流程对应代码如下#include string.h #include hmac_sha1.h // 密钥预处理将 key 处理成 64 字节的块 static void hmac_sha1_key_process(uint8_t key_block[64], const uint8_t *key, size_t key_len) { memset(key_block, 0, 64); if (key_len 64) { // 长密钥先做一次 SHA1 压缩到 20 字节 SHA1_CTX ctx; sha1_init(ctx); sha1_update(ctx, key, key_len); sha1_final(ctx, key_block); // 写入 20 字节其余自动为 0 } else { memcpy(key_block, key, key_len); } }这里设计了一个专用的key_block缓冲区避免直接修改调用方传入的密钥数组这个细节在实际工程里很重要因为上层协议往往要求密钥是可重用的不能因为一次 HMAC 计算就把密钥内容破坏掉。长密钥分支里调用的是资源包自带的sha1_init/update/final三层接口而不是自己重新写一遍 SHA1 压缩逻辑这样可以保证 HMAC 层与 SHA1 层各自独立维护状态排查问题时能快速定位是 SHA1 摘要算错还是 HMAC 的拼接逻辑错。ipad 和 opad 的生成代码通常在初始化函数里完成逻辑如下void hmac_sha1_init(HMAC_SHA1_CTX *ctx, const uint8_t *key, size_t key_len) { uint8_t key_block[64]; hmac_sha1_key_process(key_block, key, key_len); ctx-inner_pad[64] {0}; ctx-outer_pad[64] {0}; for (int i 0; i 64; i) { ctx-inner_pad[i] key_block[i] ^ 0x36; ctx-outer_pad[i] key_block[i] ^ 0x5c; } // 第一轮sha1(ipad || message) sha1_init(ctx-sha1_ctx); sha1_update(ctx-sha1_ctx, ctx-inner_pad, 64); }注意inner_pad[64] {0}这种写法要求编译环境支持 C99若你的工程还停留在 C89 标准需要改成memset手动置零。初始化函数只把 inner_pad 混入 SHA1 上下文消息内容通过后续的 update 调用继续输入这是 HMAC 能处理流式数据的关键设计。如果你打算把这个源码移植到某个裁剪过的 C 库环境优先确认编译器对变长数组和stdint.h的支持程度这两个点是最常见的编译失败源头。2.2 为什么选纯 C 实现从 OpenSSL 依赖到 MCU 栈空间的实际约束HMAC-SHA1 的算法逻辑并不复杂复杂的是把它放到正确的运行环境里。很多工程师第一反应是直接用 OpenSSL 的HMAC()函数遇到性能瓶颈或平台限制时才开始寻找独立实现。OpenSSL 的 HMAC 实现功能完整但存在三个现实问题静态链接体积大典型的桌面版 OpenSSL 即使裁剪后也往往超过 300KB线程安全需要额外配置锁回调交叉编译到 ARM Cortex-M 这类平台时OpenSSL 的配置脚本和编译依赖非常繁琐。所以独立 C 源码的优势体现在四个方面一是代码量可控完整的 HMAC_SHA1 实现加 SHA1 核心大约 300 到 500 行二是没有动态内存分配全部使用栈上结构体和固定数组对需要 MISRA 合规审查的代码库更友好三是便于逐行单步调试可以直接在算法内部打断点观察 ipad/opad 的状态四是可以轻松替换为硬件加速版本比如把 sha1_transform 换成芯片厂商提供的硬件 SHA 引擎而 HMAC 层的拼接逻辑完全不用动。我见过某跨平台系统的做法协议栈需要同时计算 HMAC-SHA1 和普通 SHA1他们从这份源码抽出了 SHA1 部分当作公共模块HMAC 层只是额外包了一层 padding 逻辑。这种做法在代码组织上是明智的因为 HMAC 的核心状态机只依赖 SHA1 的 init/update/final 接口独立实现反而让耦合更小。3. 把源码跑起来文件组织、API 调用与三个典型的工程场景3.1 源码文件清单与核心 API 对照拿到这份资源包后解压后通常会看到以下文件我用表格列出便于对照文件作用说明hmac_sha1.h对外头文件声明上下文结构体和四个标准接口hmac_sha1.cHMAC 实现密钥处理、padding、两轮调用sha1.c / sha1.hSHA1 基础实现内部依赖通常也被导出供直接调用test_hmac.c自测程序内置 RFC 2202 测试向量可独立编译运行Makefile构建脚本支持 gcc 与部分交叉编译器核心 API 就四个与 OpenSSL 风格保持一致降低已有代码的迁移成本void hmac_sha1_init(HMAC_SHA1_CTX *ctx, const uint8_t *key, size_t key_len); void hmac_sha1_update(HMAC_SHA1_CTX *ctx, const uint8_t *data, size_t len); void hmac_sha1_final(HMAC_SHA1_CTX *ctx, uint8_t digest[20]); void hmac_sha1(const uint8_t *key, size_t key_len, const uint8_t *data, size_t data_len, uint8_t digest[20]);hmac_sha1是一次性接口内部自动完成 init/update/final 三步适合消息已经完整存放在内存中的场景比如对一段配置文件计算校验值。流式接口init/update/final则适合消息分段到达或数据量较大、不想一次性分配大缓冲区的场景。头文件里还可能定义了HMAC_SHA1_DIGEST_SIZE宏值为 20建议所有代码通过这个宏引用摘要长度而不是硬编码数字这样后续换到 HMAC-SHA256 时改动面更小。3.2 在设备端给固件加签名校验一次性接口最直观实际工程里最常见的用法是设备升级前计算固件文件的 HMAC-SHA1并把结果与服务器下发的预计算值比对。下面这段代码演示了在一次读取整个固件到内存后如何调用#include stdio.h #include stdlib.h #include hmac_sha1.h int verify_firmware(const char *file_path, const uint8_t *key, size_t key_len, const uint8_t *expect_digest) { FILE *fp fopen(file_path, rb); if (!fp) return -1; fseek(fp, 0, SEEK_END); long fsize ftell(fp); fseek(fp, 0, SEEK_SET); uint8_t *buffer (uint8_t *)malloc(fsize); if (!buffer) { fclose(fp); return -2; } fread(buffer, 1, fsize, fp); fclose(fp); uint8_t calc_digest[HMAC_SHA1_DIGEST_SIZE]; hmac_sha1(key, key_len, buffer, fsize, calc_digest); free(buffer); // 使用常量时间比较防止时序侧信道 int diff 0; for (int i 0; i HMAC_SHA1_DIGEST_SIZE; i) { diff | (calc_digest[i] ^ expect_digest[i]); } return diff 0 ? 0 : 1; }这段代码在前面加了文件是否打开的检查在读取之前根据文件大小分配缓冲区。hmac_sha1这个一次性接口接收完整文件内容后直接产出 20 字节摘要。这里有一个安全细节值得注意比较两个摘要时没有用memcmp而是逐字节异或后累积这是为了避免 memcmp 在遇到第一个不同字节时提前返回导致攻击者通过测量比较时间推测摘要内容。虽然对固件校验场景来说威胁模型未必包含时序攻击但养成这个习惯没有坏处。参数层面有三个坑第一个是fread的返回值没有检查实际读取长度严谨的代码应该用fread返回值更新剩余长度而不是直接信任fsize第二个是malloc分配大文件缓冲区在嵌入式环境可能失败需要根据平台增加分块读取逻辑第三个是密钥长度是size_t类型调用时不要误传成int在 64 位系统上类型不匹配会导致高位截断。3.3 流式更新网络协议包的分段校验另一类常见场景是数据不是一次性到位的比如 HTTP 请求体被分成多个 chunk 到达或者传感器数据以不定长帧持续产生。这时不能用一次性接口反复拼接缓冲而是用流式接口HMAC_SHA1_CTX ctx; uint8_t key[] shared_secret_key; // 长度 17 字节小于 64 hmac_sha1_init(ctx, key, sizeof(key) - 1); // 模拟分包接收 hmac_sha1_update(ctx, part1, len1); hmac_sha1_update(ctx, part2, len2); hmac_sha1_update(ctx, part3, len3); uint8_t digest[HMAC_SHA1_DIGEST_SIZE]; hmac_sha1_final(ctx, digest);初始化函数的第三个参数sizeof(key) - 1是刻意去掉字符串结尾的\0避免把终止符当成消息内容的一部分。如果你用sizeof(key)直接传入长度会变成 18HMAC 计算结果就会和对方不一致。密钥长度在这个场景中决定了内部是直接补零还是先做 SHA118 字节和 17 字节都属于长度小于 64 的分支表面上运行正常但结果完全不同这类问题在联调时非常隐蔽。流式接口内部维护着 SHA1 的上下文结构每调用一次hmac_sha1_update就把传入的数据追加到内部 buffer并在积累到 64 字节块时执行压缩变换。这份源码的 update 实现通常会在内部临时 buffer 与用户数据之间做一次 memcpy如果传输的数据块很大这里的 memcpy 次数会影响整体吞吐。实测下来在 100MHz 主频的 Cortex-M4 上每 512 字节调用一次 update 比每 1 字节调用一次快约 4 倍所以建议上层尽量合并小包后再调用。3.4 服务端验证把 C 代码编译成独立命令行工具很多后端同学拿到 C 源码后第一反应是找在线 HMAC 工具对比结果。在线工具适合偶尔验证但不能通过脚本自动化处理所以这个资源包通常还附带一个简单的命令行封装。通过 Makefile 编译后可以这样使用# 计算字符串的 HMAC-SHA1 ./hmac_sha1_tool -k my_key -m hello world # 计算文件的 HMAC-SHA1 ./hmac_sha1_tool -k my_key -f firmware.bin命令行工具的参数解析逻辑如果用 C 写会比较繁琐但胜在无依赖。实际项目里如果只是为了本地验证我更推荐写一段 Python 调用这个 C 编译出的动态库或者直接用 Python 的hmac模块做交叉验证。C 代码的正确性不是靠看出来的而是靠同一测试向量在不同语言实现下产生相同输出。我在某个某公司项目里就是这么做的先用 RFC 2202 里的测试向量测 C 源码再用 Python 脚本生成一批随机消息与 C 工具输出对比两轮之后才敢把源码放进生产设备固件里。4. HMAC_SHA1 排查与避坑五个高频翻车点的定位思路4.1 密钥超长时结果不一致现象密钥长度在 60 字节以内时计算结果正常一旦密钥超过 64 字节HMAC 输出和 OpenSSL 对不上。原因密钥预处理逻辑写错了没有先做 SHA1 压缩而是直接截断前 64 字节。解决检查hmac_sha1_key_process中key_len 64的分支确认它调用了sha1_final并只把前 20 字节写入 key_block其余 44 字节必须为 0。4.2 字节序导致的摘要错乱现象在 x86 上编译运行正确交叉编译到 ARM 平台后摘要结果不同。原因SHA1 内部处理的是 32 位大端序数据部分移植版源码在内存中直接按 uint32 数组操作而没有做字节序转换。解决检查sha1_transform里对消息块的加载函数确认使用了(msg[3] 24) | (msg[2] 16) | ...这类显式组合而不是把uint8_t指针强转为uint32_t指针读取。显式组合代码看似冗长但能保证在所有平台行为一致。4.3 上下文复用导致未初始化的 padding现象同一份固件烧录到不同设备部分设备首次计算后结果正确第二次调用后输出错误。原因hmac_sha1_init每次调用只需要重新初始化上下文但很多副本实现漏掉了对外部HMAC_SHA1_CTX结构体的 memset导致 inner_pad 数组残留上一次的值。解决在hmac_sha1_init入口处显式调用memset(ctx, 0, sizeof(*ctx))或者在首次使用前初始化整个上下文。这属于典型的“看起来没事换个调用顺序就翻车”问题。4.4 与在线工具不一致时首先核对空消息与零长度密钥现象计算空字符串的 HMAC 值本源码结果与某个在线网站不同。原因在线工具对空消息处理和密钥编码方式可能存在差异比如把空字符串当成 NULL 指针导致未定义行为。解决先用 RFC 2202 里的标准测试向量确认源码本身正确再怀疑在线工具。RFC 2202 第一个测试用例是密钥为 0x0b 重复 20 次、消息为 Hi There期望摘要固定为b617318655057264e28bc0b6fb378c8ef146be00能用这份源码复现这个值就说明算法链路是通的。4.5 栈空间不足导致设备复位现象主控程序加入 HMAC 计算后原本正常的设备出现随机复位尤其在使用 RTOS 的线程栈较小的环境。原因HMAC_SHA1_CTX结构体里包含 SHA1 上下文SHA1 内部又带 64 字节缓冲区和 80 个字的扩展数组整个上下文可能超过 400 字节如果在任务栈只有 1KB 的线程里直接调用栈溢出。解决把 HMAC 上下文声明为静态变量或放到堆上确保栈空间不受影响同时确认线程栈大小翻倍后不再复位。这种问题直接看代码很难发现建议加栈水位监测。5. 用测试向量和边界消息把这份源码彻底验证一遍拿到源码后不要急着集成先花十几分钟做一轮严格的自测。我习惯的做法是写一个简单的 C 测试程序把 RFC 2202 中 7 个测试用例全部跑一遍。这里贴出关键部分的测试逻辑#include stdio.h #include string.h #include hmac_sha1.h typedef struct { const char *key_hex; const char *msg_hex; const char *expect_hex; } testcase_t; static int test_one(const testcase_t *tc) { uint8_t key[128], msg[256], digest[20]; int key_len hex2buf(tc-key_hex, key); int msg_len hex2buf(tc-msg_hex, msg); hmac_sha1(key, key_len, msg, msg_len, digest); char calc[41]; buf2hex(digest, 20, calc); return strcmp(calc, tc-expect_hex) 0; }hex2buf和buf2hex是辅助函数前者把十六进制字符串转成字节数组后者把摘要转回大写 hex 字符串用于比对。跑测试时除了 RFC 2202 的标准用例我还会额外加三组边界输入空消息长度 0、密钥长度恰好为 64 字节、密钥长度恰好为 65 字节。第三组和第四组能确认长密钥分支没有越界写这是最容易出内存错误的位置。我实际跑过的一次经历是某个移植版源码在密钥长度 64 字节时输出正确65 字节时结果错误最后定位到是 key_block 数组没有全部置零SHA1 压缩函数读了未初始化的栈内存。密钥恰好为 64 字节时按 RFC 2104 规定不需要做 SHA1 预压缩key_block 直接复制密钥后所有字节都有值但恰好 65 字节时先做 SHA1、只填 20 字节后续 44 字节依赖 memset 置零。我见过一个副本把 memset 放在 if 分支外导致长密钥情况下的剩余字节是随机值这是一个很隐蔽的 bug。从那以后我每次拿到 HMAC 相关源码都强制走一遍“短密钥、长密钥、边界长度”的三组测试再放行集成。这二十行测试代码的回报率极高省下来的排错时间足够写几十个项目模块了。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询