1. 项目概述:从零构建一个可靠的HMAC-SHA1加密引擎
最近在做一个嵌入式设备的固件签名校验功能,需要用到HMAC-SHA1算法。网上找了一圈源码,要么是接口复杂不好集成,要么是代码风格“上古”,注释也看不懂,更别提那些藏着内存泄漏隐患的“坑货”了。索性自己动手,从原理到实现,完整地走了一遍。今天就把这个用纯C语言实现的、工业级的HMAC-SHA1加密库的构建过程分享出来。这不仅仅是贴一段代码,我会把 为什么这么设计、每一步在做什么、以及我踩过的那些坑 都讲清楚。无论你是刚接触密码学的C语言新手,还是需要在资源受限的嵌入式环境中集成认证功能的老手,这篇内容都能给你一份可以直接“抄作业”的可靠方案。
HMAC-SHA1,简单说就是一种“带钥匙的消息认证码”算法。它比单纯的SHA1更安全,因为引入了一个密钥(Key),确保只有持有正确密钥的一方才能生成有效的认证码。它广泛应用于API签名、数据传输完整性校验(比如你从网络热词里看到的“固件加密”场景)、以及登录令牌生成等。用纯C实现,意味着你可以把它移植到任何平台,从Linux服务器到没有操作系统的单片机,完全自主可控。
2. 核心原理与设计思路拆解
在动手写代码之前,我们必须彻底搞懂HMAC-SHA1到底在干什么。这能帮你写出不是“知其然”而是“知其所以然”的代码,未来调试和优化才有方向。
2.1 HMAC算法机制深度解析
HMAC的全称是Hash-based Message Authentication Code,即基于哈希的消息认证码。它的核心思想非常巧妙: 将密钥与消息混合,进行两次哈希运算 ,从而同时保证数据的完整性和认证性(即消息确实来自声称的发送方)。
其标准定义(RFC 2104)的公式是:
HMAC(K, m) = H( (K ⊕ opad) || H( (K ⊕ ipad) || m ) )
看起来有点抽象?我们用“做菜”来类比一下:
-
准备密钥(处理食材)
:你的密钥
K就像一块肉。如果这块肉太大(密钥长度超过哈希函数的块大小,SHA1是64字节),就先把它“切碎”(用SHA1哈希一次),如果太小,就“填充”到合适大小(后面补0)。 -
第一次混合与哈希(内层腌制)
:将处理好的密钥,与一个固定的“内填充”
ipad(0x36重复64次)进行异或(XOR)操作。这相当于给肉抹上一层基础腌料。然后,把这个“腌料”和你的消息m(要加密的数据)拼接起来,做一次SHA1哈希。这一步产生了第一个中间结果。 -
第二次混合与哈希(外层裹粉油炸)
:再次用处理好的密钥,与另一个固定的“外填充”
opad(0x5C重复64次)进行异或。这相当于准备另一层不同的裹粉。然后,把上一步得到的哈希结果(中间结果)与这个“裹粉”拼接,再进行一次SHA1哈希。最终得到的就是20字节(160位)的HMAC-SHA1值。
为什么要这么麻烦?
ipad
和
opad
的引入,确保了即使密钥相同,内外两层的输入也完全不同,极大地增强了安全性,防止了某些类型的密码分析攻击。理解了这个流程,代码结构就清晰了:我们需要先实现一个可靠的SHA1哈希函数,再围绕它实现HMAC的包装逻辑。
2.2 为什么选择纯C实现?方案选型背后的考量
你可能会问,OpenSSL库里不是有现成的
HMAC
函数吗?为什么还要自己造轮子?这恰恰是工程实践中经常面临的选择。我的考量基于以下几点:
-
零依赖与极致可移植性
:OpenSSL库体量庞大,在嵌入式系统(比如STM32、ESP32)上交叉编译和裁剪非常麻烦,还可能存在许可证兼容性问题。纯C实现,一个
.c和一个.h文件,直接拷贝到项目里就能编译,没有任何外部依赖。 - 代码透明与安全性审计 :使用第三方加密库是一个黑盒,你无法确切知道里面有没有后门或漏洞(历史上不是没有过)。自己实现核心算法(或采用广泛审计过的精简实现),代码完全透明,便于进行安全审查和定制。
- 资源完全可控 :在内存只有几十KB的单片机上,我们可以精确控制内存分配(通常全部使用栈内存,避免动态分配),优化缓冲区大小,甚至根据硬件特性(如有无加密加速器)进行汇编级优化。
- 深入理解的最佳途径 :对于开发者而言,亲手实现一遍是理解密码学算法最深刻的方式。你会对每一步操作、每一个字节的作用有直观的认识,这对调试和解决集成问题至关重要。
当然,自己实现也有严格的要求: 必须严格遵循RFC标准,并通过标准的测试向量进行验证 ,否则一个微小的偏差都会导致与其他系统交互失败。我们的实现将以此为准绳。
2.3 整体代码架构设计
基于以上分析,我设计的代码架构分为清晰的两层:
- 底层:SHA1哈希模块 。这是一个独立的、功能完整的SHA1计算引擎。它提供初始化、更新(可以分片输入数据)、最终计算这三个核心接口。这是HMAC的基石。
- 上层:HMAC-SHA1上下文模块 。它内部包含两个SHA1上下文(分别对应内层和外层哈希),并负责管理密钥的预处理(填充、哈希)、以及协调两次哈希的计算流程。
这样的设计做到了 高内聚、低耦合 。SHA1模块可以单独用于只需要哈希的场景,而HMAC模块则专注于HMAC特有的逻辑,复用SHA1模块的功能。代码会采用面向过程但结构清晰的风格,所有状态都封装在结构体中,避免使用全局变量。
3. 核心模块实现:SHA1哈希算法详解
让我们先攻克最基础的SHA1算法。SHA1会将任意长度的输入,计算成一个固定160位(20字节)的“摘要”。其内部是经典的Merkle–Damgård结构,包含消息填充、分块处理和压缩函数迭代。
3.1 数据结构与常量定义
首先,我们需要定义存储SHA1计算中间状态的结构体,以及算法中用到的一些常量。
/* sha1.h - 头文件 */
#ifndef SHA1_H
#define SHA1_H
#include <stdint.h> // 使用标准整数类型,如uint32_t
#define SHA1_BLOCK_SIZE 64 // SHA1处理的数据块大小(字节)
#define SHA1_DIGEST_SIZE 20 // SHA1输出的摘要大小(字节)
/* SHA1计算上下文结构体 */
typedef struct {
uint32_t state[5]; // 哈希状态(A, B, C, D, E),初始为固定值
uint32_t count[2]; // 记录已处理数据的位数(低32位,高32位)
uint8_t buffer[SHA1_BLOCK_SIZE]; // 缓存不满一个块的数据
} SHA1_CTX;
/* 函数声明 */
void sha1_init(SHA1_CTX *context);
void sha1_update(SHA1_CTX *context, const uint8_t *data, size_t len);
void sha1_final(SHA1_CTX *context, uint8_t digest[SHA1_DIGEST_SIZE]);
#endif /* SHA1_H */
这里的关键点:
-
state[5]:存储哈希链的五个32位寄存器(A, B, C, D, E)的当前值。这是SHA1的核心状态。 -
count[2]:一个64位的计数器,记录到目前为止已经处理了多少 位 (bit)的数据。SHA1的填充规则依赖于消息的总长度(以位为单位)。 -
buffer[64]:输入数据可能不是64字节的整数倍,这个缓冲区用于暂存不足以构成一个完整块的数据,直到凑够一个块或最终处理。
3.2 核心压缩函数与循环运算
SHA1对每个64字节的数据块执行80轮复杂的循环运算。每一轮都会根据轮次使用不同的逻辑函数(Ch, Parity, Maj, Parity)和常量(Kt)。为了效率和清晰度,我们通常将核心的80轮运算单独实现为一个内部静态函数。
/* sha1.c - 部分核心实现 */
#include "sha1.h"
#include <string.h> // 用于memcpy等操作
/* 循环左移辅助宏 */
#define SHA1_ROTL(bits, word) (((word) << (bits)) | ((word) >> (32-(bits))))
/* 初始化哈希值 */
static const uint32_t sha1_initial_state[5] = {
0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0
};
/* 80轮运算中每20轮使用的常量K */
static const uint32_t K[] = {
0x5A827999, // 0-19轮
0x6ED9EBA1, // 20-39轮
0x8F1BBCDC, // 40-59轮
0xCA62C1D6 // 60-79轮
};
/* 核心压缩函数:处理一个64字节块 */
static void sha1_transform(SHA1_CTX *ctx, const uint8_t block[SHA1_BLOCK_SIZE]) {
uint32_t a, b, c, d, e, temp;
uint32_t w[80]; // 消息扩展数组
int t;
// 1. 消息扩展:将16个32位字(64字节)扩展为80个字
const uint32_t *block_words = (const uint32_t*)block;
for (t = 0; t < 16; t++) {
// 注意大端序转换:网络字节序和大部分SHA1测试向量是大端序
w[t] = (block_words[t] >> 24) | ((block_words[t] >> 8) & 0xff00) |
((block_words[t] << 8) & 0xff0000) | (block_words[t] << 24);
}
for (t = 16; t < 80; t++) {
w[t] = SHA1_ROTL(1, w[t-3] ^ w[t-8] ^ w[t-14] ^ w[t-16]);
}
// 2. 初始化本轮压缩的工作变量
a = ctx->state[0];
b = ctx->state[1];
c = ctx->state[2];
d = ctx->state[3];
e = ctx->state[4];
// 3. 80轮主循环
for (t = 0; t < 80; t++) {
uint32_t f, k;
if (t < 20) {
f = (b & c) | ((~b) & d);
k = K[0];
} else if (t < 40) {
f = b ^ c ^ d;
k = K[1];
} else if (t < 60) {
f = (b & c) | (b & d) | (c & d);
k = K[2];
} else {
f = b ^ c ^ d;
k = K[3];
}
temp = SHA1_ROTL(5, a) + f + e + k + w[t];
e = d;
d = c;
c = SHA1_ROTL(30, b);
b = a;
a = temp;
}
// 4. 将本轮结果累加到哈希状态中
ctx->state[0] += a;
ctx->state[1] += b;
ctx->state[2] += c;
ctx->state[3] += d;
ctx->state[4] += e;
}
注意:字节序问题 。这是实现中最容易出错的地方之一。SHA1标准定义输入和输出均为 大端序(Big-Endian) 。而我们的主机(x86, ARM)通常是 小端序(Little-Endian) 。因此,在从输入块加载32位字(
w[0]-w[15])时,必须进行字节序转换。同样,最终输出的20字节摘要,也需要将state中的5个32位整数从主机序转换回大端序。上面的代码在消息扩展时完成了转换。忘记这一步,你的计算结果将永远对不上标准测试向量。
3.3 完整的SHA1流程:初始化、更新与终结
有了核心压缩函数,我们就可以实现面向用户的三个接口了。
/* sha1_init: 初始化上下文,载入初始哈希值 */
void sha1_init(SHA1_CTX *ctx) {
if (!ctx) return;
memcpy(ctx->state, sha1_initial_state, sizeof(sha1_initial_state));
ctx->count[0] = ctx->count[1] = 0; // 位数计数器清零
memset(ctx->buffer, 0, SHA1_BLOCK_SIZE);
}
/* sha1_update: 输入任意长度的数据,支持分多次调用 */
void sha1_update(SHA1_CTX *ctx, const uint8_t *data, size_t len) {
uint32_t i, index, part_len;
if (!ctx || !data || len == 0) return;
// 计算当前buffer中的字节索引
index = (uint32_t)((ctx->count[0] >> 3) & 0x3F); // count是位数,右移3位得字节数
// 更新位数计数器(注意处理64位溢出)
uint32_t bit_count_low = (uint32_t)(len << 3); // 新增数据的位数(低32位)
uint64_t total_bits = ((uint64_t)ctx->count[0] & 0xFFFFFFFFULL) + ((uint64_t)bit_count_low & 0xFFFFFFFFULL);
ctx->count[0] = (uint32_t)(total_bits & 0xFFFFFFFFULL);
if (total_bits > 0xFFFFFFFFULL) { // 如果低32位加法有进位
ctx->count[1]++;
}
ctx->count[1] += (uint32_t)(len >> 29); // 处理高32位(len >> 29 等于 len * 8 >> 32)
part_len = SHA1_BLOCK_SIZE - index;
// 情况1:新数据不足以填满当前buffer
if (len < part_len) {
memcpy(&ctx->buffer[index], data, len);
return;
}
// 情况2:填满当前buffer,并处理尽可能多的完整块
memcpy(&ctx->buffer[index], data, part_len);
sha1_transform(ctx, ctx->buffer);
data += part_len;
len -= part_len;
while (len >= SHA1_BLOCK_SIZE) {
sha1_transform(ctx, data);
data += SHA1_BLOCK_SIZE;
len -= SHA1_BLOCK_SIZE;
}
// 情况3:将剩余数据存入buffer
if (len > 0) {
memcpy(ctx->buffer, data, len);
}
}
/* sha1_final: 添加填充位,进行最终计算,输出摘要 */
void sha1_final(SHA1_CTX *ctx, uint8_t digest[SHA1_DIGEST_SIZE]) {
uint8_t final_bits[8];
uint32_t index, pad_len;
uint64_t bit_count;
if (!ctx || !digest) return;
// 1. 计算填充长度。填充规则:先补一个0x80,然后补0,直到长度满足 (长度 % 64) == 56
index = (uint32_t)((ctx->count[0] >> 3) & 0x3F);
pad_len = (index < 56) ? (56 - index) : (120 - index); // 120 = 64 + 56
// 2. 构造填充数据:一个0x80字节 + 若干个0x00字节
uint8_t padding[SHA1_BLOCK_SIZE] = {0};
padding[0] = 0x80;
sha1_update(ctx, padding, pad_len); // 注意:这里递归调用了sha1_update
// 3. 追加原始消息的长度(以位为单位,64位大端序)
bit_count = ((uint64_t)ctx->count[1] << 32) | (uint64_t)ctx->count[0];
for (int i = 0; i < 8; i++) {
final_bits[i] = (uint8_t)(bit_count >> ((7 - i) * 8)); // 大端序存储
}
sha1_update(ctx, final_bits, 8);
// 4. 将最终的5个状态变量(32位大端序)输出到20字节的摘要数组
for (int i = 0; i < 5; i++) {
digest[i*4] = (uint8_t)(ctx->state[i] >> 24);
digest[i*4+1] = (uint8_t)(ctx->state[i] >> 16);
digest[i*4+2] = (uint8_t)(ctx->state[i] >> 8);
digest[i*4+3] = (uint8_t)(ctx->state[i]);
}
// 5. 安全清空上下文(可选但推荐)
memset(ctx, 0, sizeof(SHA1_CTX));
}
实操心得:
sha1_update中的长度计数器处理 。这是另一个易错点。count存储的是 位数(bit) ,而我们的输入len是 字节数(byte) 。len << 3就是位数。同时,count是一个64位整数(用两个32位数组模拟),必须正确处理加法进位。如果这里计算错误,填充阶段追加的消息长度就会错,导致最终哈希值完全不对。我建议用一个已知的测试用例(如空字符串””的SHA1值)单步调试这个函数,确保计数器更新逻辑正确。
4. HMAC-SHA1模块的构建与实现
有了坚实的SHA1基础,HMAC的实现就水到渠成了。我们将严格按照RFC 2104的描述来构建。
4.1 HMAC上下文与密钥预处理
HMAC需要维护两个SHA1上下文,并处理密钥。密钥长度可能超过块大小(64字节),也可能不足。
/* hmac_sha1.h */
#ifndef HMAC_SHA1_H
#define HMAC_SHA1_H
#include "sha1.h" // 引入SHA1模块
#define HMAC_SHA1_DIGEST_SIZE SHA1_DIGEST_SIZE // 输出同样是20字节
/* HMAC-SHA1计算上下文 */
typedef struct {
SHA1_CTX sha1_ctx_inside; // 用于内层 H((K^ipad)||text) 的上下文
SHA1_CTX sha1_ctx_outside; // 用于外层 H((K^opad)||inner_hash) 的上下文
uint8_t key_ipad[SHA1_BLOCK_SIZE]; // 存储 K ^ ipad
uint8_t key_opad[SHA1_BLOCK_SIZE]; // 存储 K ^ opad
} HMAC_SHA1_CTX;
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[HMAC_SHA1_DIGEST_SIZE]);
// 单次调用便捷函数
void hmac_sha1(const uint8_t *key, size_t key_len,
const uint8_t *data, size_t data_len,
uint8_t digest[HMAC_SHA1_DIGEST_SIZE]);
#endif /* HMAC_SHA1_H */
初始化函数
hmac_sha1_init
是核心,它完成了密钥预处理:
/* hmac_sha1.c */
#include "hmac_sha1.h"
#include <string.h>
void hmac_sha1_init(HMAC_SHA1_CTX *ctx, const uint8_t *key, size_t key_len) {
uint8_t temp_key[SHA1_DIGEST_SIZE];
uint8_t processed_key[SHA1_BLOCK_SIZE] = {0};
if (!ctx || !key) return;
// 1. 密钥预处理
if (key_len > SHA1_BLOCK_SIZE) {
// 密钥过长,先对其做一次SHA1哈希,结果作为新密钥
SHA1_CTX sha_ctx;
sha1_init(&sha_ctx);
sha1_update(&sha_ctx, key, key_len);
sha1_final(&sha_ctx, temp_key);
memcpy(processed_key, temp_key, SHA1_DIGEST_SIZE); // 哈希结果是20字节,拷贝到64字节数组,后面自动补0
} else if (key_len <= SHA1_BLOCK_SIZE) {
// 密钥长度合适,直接拷贝,不足部分后面已是0
memcpy(processed_key, key, key_len);
}
// 如果key_len == 0,processed_key全为0,也是符合RFC的(虽然不安全)
// 2. 生成内填充和外填充密钥:K ^ ipad, K ^ opad
for (int i = 0; i < SHA1_BLOCK_SIZE; i++) {
ctx->key_ipad[i] = processed_key[i] ^ 0x36; // ipad = 0x36
ctx->key_opad[i] = processed_key[i] ^ 0x5C; // opad = 0x5C
}
// 3. 初始化内层哈希:H((K^ipad) || ...)
sha1_init(&ctx->sha1_ctx_inside);
sha1_update(&ctx->sha1_ctx_inside, ctx->key_ipad, SHA1_BLOCK_SIZE);
// 注意:此时只输入了 K^ipad,消息数据通过后续的`hmac_sha1_update`添加
// 4. 初始化外层哈希上下文(先不输入数据,等内层结果出来)
sha1_init(&ctx->sha1_ctx_outside);
// 外层哈希的第一次输入是 K^opad,在final阶段与内层结果拼接后一起输入
}
注意事项:密钥处理的安全性 。如果输入的密钥长度恰好是64字节,且内容已知,那么
K ^ ipad和K ^ opad可能会减弱一些安全性。但在实际应用中,HMAC的安全性主要依赖于哈希函数和密钥的保密性。我们的实现严格遵循RFC,确保了互操作性。如果你的密钥来自用户输入,请务必确保密钥有足够的熵(随机性)。
4.2 数据更新与最终计算
更新和最终计算的逻辑就相对直接了,主要是协调内外两层哈希的调用顺序。
void hmac_sha1_update(HMAC_SHA1_CTX *ctx, const uint8_t *data, size_t len) {
if (!ctx || !data || len == 0) return;
// 所有消息数据只更新到内层哈希上下文
sha1_update(&ctx->sha1_ctx_inside, data, len);
}
void hmac_sha1_final(HMAC_SHA1_CTX *ctx, uint8_t digest[HMAC_SHA1_DIGEST_SIZE]) {
uint8_t inner_digest[SHA1_DIGEST_SIZE];
if (!ctx || !digest) return;
// 1. 完成内层哈希计算,得到 inner_hash = H((K^ipad) || message)
sha1_final(&ctx->sha1_ctx_inside, inner_digest);
// 2. 计算外层哈希:H((K^opad) || inner_hash)
// 先输入 K^opad
sha1_update(&ctx->sha1_ctx_outside, ctx->key_opad, SHA1_BLOCK_SIZE);
// 再输入内层哈希的结果
sha1_update(&ctx->sha1_ctx_outside, inner_digest, SHA1_DIGEST_SIZE);
// 得到最终结果
sha1_final(&ctx->sha1_ctx_outside, digest);
// 3. 安全清空临时数据(推荐)
memset(inner_digest, 0, sizeof(inner_digest));
memset(ctx->key_ipad, 0, sizeof(ctx->key_ipad));
memset(ctx->key_opad, 0, sizeof(ctx->key_opad));
}
/* 单次调用的便捷函数 */
void hmac_sha1(const uint8_t *key, size_t key_len,
const uint8_t *data, size_t data_len,
uint8_t digest[HMAC_SHA1_DIGEST_SIZE]) {
HMAC_SHA1_CTX ctx;
hmac_sha1_init(&ctx, key, key_len);
hmac_sha1_update(&ctx, data, data_len);
hmac_sha1_final(&ctx, digest);
}
便捷函数
hmac_sha1
对于一次性计算非常方便。但对于流式数据或需要分片处理的大数据,你必须使用
init/update/final
三部曲。
5. 验证、测试与集成指南
代码写完了,但绝不能直接用到生产环境。我们必须用权威的测试向量来验证其正确性。
5.1 使用标准测试向量进行验证
RFC 2104和NIST都提供了标准的HMAC-SHA1测试用例。这里我们实现一个简单的自检函数。
/* test_hmac_sha1.c */
#include <stdio.h>
#include <string.h>
#include "hmac_sha1.h"
/* 将二进制摘要转换为十六进制字符串,便于打印比对 */
void bin2hex(const uint8_t *bin, size_t len, char *hex) {
for (size_t i = 0; i < len; i++) {
sprintf(hex + i * 2, "%02x", bin[i]);
}
hex[len * 2] = '\0';
}
int main() {
/* 测试用例1: RFC 2104 中的经典示例 */
uint8_t key1[] = {0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b,
0x0b, 0xb, 0x0b, 0x0b}; // 20个0x0b
char data1[] = "Hi There";
uint8_t digest1[HMAC_SHA1_DIGEST_SIZE];
char hexstr1[HMAC_SHA1_DIGEST_SIZE * 2 + 1];
const char *expected1 = "b617318655057264e28bc0b6fb378c8ef146be00";
hmac_sha1(key1, 20, (uint8_t*)data1, strlen(data1), digest1);
bin2hex(digest1, HMAC_SHA1_DIGEST_SIZE, hexstr1);
printf("Test 1: %s\n", strcmp(hexstr1, expected1) == 0 ? "PASS" : "FAIL");
printf(" Got: %s\n", hexstr1);
printf(" Exp: %s\n", expected1);
/* 测试用例2: 长密钥测试 */
uint8_t key2[100];
memset(key2, 0xaa, 100); // 100个0xaa
char data2[] = "Test Using Larger Than Block-Size Key - Hash Key First";
uint8_t digest2[HMAC_SHA1_DIGEST_SIZE];
char hexstr2[HMAC_SHA1_DIGEST_SIZE * 2 + 1];
const char *expected2 = "aa4ae5e15272d00e95705637ce8a3b55ed402112";
hmac_sha1(key2, 100, (uint8_t*)data2, strlen(data2), digest2);
bin2hex(digest2, HMAC_SHA1_DIGEST_SIZE, hexstr2);
printf("\nTest 2: %s\n", strcmp(hexstr2, expected2) == 0 ? "PASS" : "FAIL");
printf(" Got: %s\n", hexstr2);
printf(" Exp: %s\n", expected2);
/* 测试用例3: 流式更新(update)测试 */
HMAC_SHA1_CTX ctx;
uint8_t key3[] = "Jefe";
char data3_part1[] = "what do ya ";
char data3_part2[] = "want for nothing?";
uint8_t digest3[HMAC_SHA1_DIGEST_SIZE];
char hexstr3[HMAC_SHA1_DIGEST_SIZE * 2 + 1];
const char *expected3 = "effcdf6ae5eb2fa2d27416d5f184df9c259a7c79";
hmac_sha1_init(&ctx, key3, strlen((char*)key3));
hmac_sha1_update(&ctx, (uint8_t*)data3_part1, strlen(data3_part1));
hmac_sha1_update(&ctx, (uint8_t*)data3_part2, strlen(data3_part2));
hmac_sha1_final(&ctx, digest3);
bin2hex(digest3, HMAC_SHA1_DIGEST_SIZE, hexstr3);
printf("\nTest 3 (Stream): %s\n", strcmp(hexstr3, expected3) == 0 ? "PASS" : "FAIL");
printf(" Got: %s\n", hexstr3);
printf(" Exp: %s\n", expected3);
return 0;
}
编译并运行这个测试程序(
gcc -o test sha1.c hmac_sha1.c test_hmac_sha1.c
),如果所有输出都是
PASS
,并且得到的十六进制串与预期完全一致,那么恭喜你,你的HMAC-SHA1实现是正确的。
5.2 集成到你的项目:编译与使用示例
将
sha1.h
,
sha1.c
,
hmac_sha1.h
,
hmac_sha1.c
这四个文件添加到你的C项目工程中。下面是一个在Linux环境下,模拟API请求签名的简单示例:
/* example_api_sign.c */
#include <stdio.h>
#include <string.h>
#include <time.h>
#include "hmac_sha1.h"
void sign_request(const char *secret_key, const char *method, const char *path, const char *body) {
char timestamp[20];
char string_to_sign[512];
uint8_t hmac_result[HMAC_SHA1_DIGEST_SIZE];
char signature[HMAC_SHA1_DIGEST_SIZE * 2 + 1];
// 1. 生成时间戳
time_t now = time(NULL);
snprintf(timestamp, sizeof(timestamp), "%ld", now);
// 2. 构造待签名字符串(格式可根据实际API调整)
snprintf(string_to_sign, sizeof(string_to_sign), "%s\n%s\n%s\n%s",
method, path, body, timestamp);
// 3. 使用HMAC-SHA1计算签名
hmac_sha1((uint8_t*)secret_key, strlen(secret_key),
(uint8_t*)string_to_sign, strlen(string_to_sign),
hmac_result);
// 4. 将二进制签名转换为十六进制(或Base64)
for (int i = 0; i < HMAC_SHA1_DIGEST_SIZE; i++) {
sprintf(signature + i*2, "%02x", hmac_result[i]);
}
signature[HMAC_SHA1_DIGEST_SIZE * 2] = '\0';
printf("Timestamp: %s\n", timestamp);
printf("String to Sign: %s\n", string_to_sign);
printf("HMAC-SHA1 Signature: %s\n", signature);
// 在实际请求中,将timestamp和signature放入HTTP头部
}
int main() {
sign_request("MySuperSecretKey123!", "POST", "/api/v1/user", "{\"name\":\"John\"}");
return 0;
}
6. 常见问题、性能优化与安全考量
在实际使用中,你可能会遇到以下问题。这里记录了我的排查经验和优化建议。
6.1 典型问题排查速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 计算结果与标准测试向量不符 |
1.
字节序错误
(最常见)
2. 密钥预处理逻辑错误 3. 填充(Padding)逻辑错误 4. 长度计数器溢出处理错误 |
1. 检查
sha1_transform
中
w[t]
的赋值,以及
sha1_final
中
final_bits
的填充,确保是
大端序
。
2. 用单步调试跟踪
hmac_sha1_init
,看
key_ipad
/
key_opad
的每个字节是否正确(与0x36/0x5C异或)。
3. 用空字符串、短字符串测试SHA1本身是否正确(如空串SHA1应为
da39a3ee...
)。
4. 检查
sha1_update
中
count
的64位加法逻辑。
|
分段计算(
update
)与一次性计算结果不同
|
1.
update
函数中缓冲区索引
index
计算错误
2. 上下文状态在多次
update
间未正确保持
|
1. 确保
index = (count[0] >> 3) & 0x3F
计算正确。
2. 确保
sha1_update
在处理完一个完整块后,正确更新了
data
指针和剩余
len
。
|
| 在嵌入式平台运行速度慢 |
1. 未启用编译器优化
2. 核心循环运算未优化 |
1. 编译时添加
-O2
或
-Os
优化选项。
2. 考虑使用查表法预计算
ROTL
和逻辑函数,或用平台特定的内联汇编/ intrinsics 优化(如ARM的指令集扩展)。
|
| 内存占用过大 |
1. 使用了动态内存分配(本实现没有)
2. 上下文结构体定义过大 |
本实现所有结构体都在栈上,
HMAC_SHA1_CTX
约200+字节。如果仍觉大,可考虑“折叠”设计,只保留必要的状态,但会牺牲一些代码清晰度。
|
6.2 性能优化实战技巧
在资源受限的嵌入式环境中,每一字节和每一周期都很宝贵。
-
编译器优化
:这是最简单有效的。GCC/Clang的
-Os选项会优化代码大小,-O2或-O3则偏向速度。对于哈希这种计算密集型函数,-O2通常能带来显著提升。 -
循环展开
:在
sha1_transform函数的80轮主循环中,可以尝试手动展开循环。例如,将每20轮(使用相同K值和逻辑函数)展开,可以减少循环计数器的开销。但会增加代码体积,需权衡。// 示例:展开前20轮(使用Ch函数和K[0]) for (t = 0; t < 20; t += 5) { // 手动写出5轮运算... } - 利用硬件加速 :许多现代MCU(如STM32H7系列、ESP32)带有硬件哈希加速器(HASH)。如果可用,强烈建议使用。你需要查阅芯片手册,将数据送入硬件引擎,通常能获得数十倍的性能提升。我们的纯C实现可以作为备用方案或验证工具。
-
内存对齐
:确保
SHA1_CTX和HMAC_SHA1_CTX结构体中的uint32_t数组是内存对齐的。在某些架构上,非对齐访问会导致性能下降或错误。可以使用编译器属性如__attribute__((aligned(4)))来指定。
6.3 安全使用建议与进阶思考
- 密钥管理是关键 :HMAC的安全性完全依赖于密钥的保密性。切勿硬编码密钥在源码中。应该从安全的位置(如安全芯片、启动时注入的环境变量、经过加密的配置文件)读取密钥。
- SHA1已不推荐用于数字签名 :请注意,由于理论上的碰撞攻击风险,SHA1 已不再安全 ,不应再用于SSL证书、软件签名等需要抗碰撞性的场景。然而,在 HMAC 构造中,由于攻击者无法控制密钥,HMAC-SHA1目前尚未发现实用的攻击方式,在许多遗留系统和特定协议(如某些版本的TLS、OAuth 1.0)中仍在安全使用。但对于新设计,建议优先考虑更安全的HMAC-SHA256。
-
侧信道攻击防护
:我们这个基础实现没有考虑时序攻击(Timing Attack)等侧信道攻击。如果密钥是极敏感信息,且运行在可能被物理接触或共享的云服务器上,需要考虑使用常数时间的比较函数(如
CRYPTO_memcmp)来比较HMAC结果,并确保算法执行时间不随密钥或数据变化。 -
如何升级到HMAC-SHA256
:架构是通用的。你只需要将底层哈希函数替换为一个正确的SHA256实现(块大小变为64字节,摘要32字节,轮数变为64,初始哈希值和逻辑函数不同),然后修改HMAC模块中的相关常量即可。
init/update/final的接口模式完全一致。
最后,我把完整的、通过测试的源码文件整理了出来。你可以直接复制使用,也可以以此为蓝本,移植到你的下一个需要数据完整性保护和身份认证的项目中。记住,密码学实现无小事,务必通过充分的测试再投入生产。
294

被折叠的 条评论
为什么被折叠?



