从零实现HMAC-SHA1:纯C语言嵌入式加密引擎实战

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 ) ) 看起来有点抽象?我们用“做菜”来类比一下:

  1. 准备密钥(处理食材) :你的密钥 K 就像一块肉。如果这块肉太大(密钥长度超过哈希函数的块大小,SHA1是64字节),就先把它“切碎”(用SHA1哈希一次),如果太小,就“填充”到合适大小(后面补0)。
  2. 第一次混合与哈希(内层腌制) :将处理好的密钥,与一个固定的“内填充” ipad (0x36重复64次)进行异或(XOR)操作。这相当于给肉抹上一层基础腌料。然后,把这个“腌料”和你的消息 m (要加密的数据)拼接起来,做一次SHA1哈希。这一步产生了第一个中间结果。
  3. 第二次混合与哈希(外层裹粉油炸) :再次用处理好的密钥,与另一个固定的“外填充” opad (0x5C重复64次)进行异或。这相当于准备另一层不同的裹粉。然后,把上一步得到的哈希结果(中间结果)与这个“裹粉”拼接,再进行一次SHA1哈希。最终得到的就是20字节(160位)的HMAC-SHA1值。

为什么要这么麻烦? ipad opad 的引入,确保了即使密钥相同,内外两层的输入也完全不同,极大地增强了安全性,防止了某些类型的密码分析攻击。理解了这个流程,代码结构就清晰了:我们需要先实现一个可靠的SHA1哈希函数,再围绕它实现HMAC的包装逻辑。

2.2 为什么选择纯C实现?方案选型背后的考量

你可能会问,OpenSSL库里不是有现成的 HMAC 函数吗?为什么还要自己造轮子?这恰恰是工程实践中经常面临的选择。我的考量基于以下几点:

  1. 零依赖与极致可移植性 :OpenSSL库体量庞大,在嵌入式系统(比如STM32、ESP32)上交叉编译和裁剪非常麻烦,还可能存在许可证兼容性问题。纯C实现,一个 .c 和一个 .h 文件,直接拷贝到项目里就能编译,没有任何外部依赖。
  2. 代码透明与安全性审计 :使用第三方加密库是一个黑盒,你无法确切知道里面有没有后门或漏洞(历史上不是没有过)。自己实现核心算法(或采用广泛审计过的精简实现),代码完全透明,便于进行安全审查和定制。
  3. 资源完全可控 :在内存只有几十KB的单片机上,我们可以精确控制内存分配(通常全部使用栈内存,避免动态分配),优化缓冲区大小,甚至根据硬件特性(如有无加密加速器)进行汇编级优化。
  4. 深入理解的最佳途径 :对于开发者而言,亲手实现一遍是理解密码学算法最深刻的方式。你会对每一步操作、每一个字节的作用有直观的认识,这对调试和解决集成问题至关重要。

当然,自己实现也有严格的要求: 必须严格遵循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 性能优化实战技巧

在资源受限的嵌入式环境中,每一字节和每一周期都很宝贵。

  1. 编译器优化 :这是最简单有效的。GCC/Clang的 -Os 选项会优化代码大小, -O2 -O3 则偏向速度。对于哈希这种计算密集型函数, -O2 通常能带来显著提升。
  2. 循环展开 :在 sha1_transform 函数的80轮主循环中,可以尝试手动展开循环。例如,将每20轮(使用相同K值和逻辑函数)展开,可以减少循环计数器的开销。但会增加代码体积,需权衡。
    // 示例:展开前20轮(使用Ch函数和K[0])
    for (t = 0; t < 20; t += 5) {
        // 手动写出5轮运算...
    }
    
  3. 利用硬件加速 :许多现代MCU(如STM32H7系列、ESP32)带有硬件哈希加速器(HASH)。如果可用,强烈建议使用。你需要查阅芯片手册,将数据送入硬件引擎,通常能获得数十倍的性能提升。我们的纯C实现可以作为备用方案或验证工具。
  4. 内存对齐 :确保 SHA1_CTX HMAC_SHA1_CTX 结构体中的 uint32_t 数组是内存对齐的。在某些架构上,非对齐访问会导致性能下降或错误。可以使用编译器属性如 __attribute__((aligned(4))) 来指定。

6.3 安全使用建议与进阶思考

  1. 密钥管理是关键 :HMAC的安全性完全依赖于密钥的保密性。切勿硬编码密钥在源码中。应该从安全的位置(如安全芯片、启动时注入的环境变量、经过加密的配置文件)读取密钥。
  2. SHA1已不推荐用于数字签名 :请注意,由于理论上的碰撞攻击风险,SHA1 已不再安全 ,不应再用于SSL证书、软件签名等需要抗碰撞性的场景。然而,在 HMAC 构造中,由于攻击者无法控制密钥,HMAC-SHA1目前尚未发现实用的攻击方式,在许多遗留系统和特定协议(如某些版本的TLS、OAuth 1.0)中仍在安全使用。但对于新设计,建议优先考虑更安全的HMAC-SHA256。
  3. 侧信道攻击防护 :我们这个基础实现没有考虑时序攻击(Timing Attack)等侧信道攻击。如果密钥是极敏感信息,且运行在可能被物理接触或共享的云服务器上,需要考虑使用常数时间的比较函数(如 CRYPTO_memcmp )来比较HMAC结果,并确保算法执行时间不随密钥或数据变化。
  4. 如何升级到HMAC-SHA256 :架构是通用的。你只需要将底层哈希函数替换为一个正确的SHA256实现(块大小变为64字节,摘要32字节,轮数变为64,初始哈希值和逻辑函数不同),然后修改HMAC模块中的相关常量即可。 init/update/final 的接口模式完全一致。

最后,我把完整的、通过测试的源码文件整理了出来。你可以直接复制使用,也可以以此为蓝本,移植到你的下一个需要数据完整性保护和身份认证的项目中。记住,密码学实现无小事,务必通过充分的测试再投入生产。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值