HMAC 签名与密钥派生

HMAC 用来给一条消息算认证码,PBKDF2 用来把口令变慢变硬,HKDF 用来把已有的随机材料展开成多个子密钥。三者都基于哈希,但用途完全不同,不能互相替代。

给消息算认证码,用来验证消息没被改过。

5 个容易踩的点

HMAC 不是「哈希加个密钥」。很多人第一反应是 H(密钥 + 消息),这被称为前缀密钥构造,在 SHA-1、SHA-256 这类 Merkle–Damgård 结构上会被长度扩展攻击打穿:攻击者不需要知道密钥,就能凭 H(密钥 + 消息) 算出 H(密钥 + 消息 + 附加内容) 的合法值。HMAC 用 ipad / opad 两次嵌套绕开了这个问题,这也是它看起来啰嗦的原因。另一个常见写法 H(消息 + 密钥) 则受碰撞影响,同样不推荐。

认证不等于加密。HMAC 只保证消息没被改动、且来自持有密钥的一方,它完全不会隐藏消息内容——签名值本身也不包含原文。想保密要另外加密。做接口签名时通常是「对参数排序拼串,加时间戳和随机串,再算 HMAC」,时间戳和随机串的作用是防重放,缺了它们,攻击者可以把旧请求原样重发。

PBKDF2 和 HKDF 别互相顶替。PBKDF2 慢,这是它的目的:口令通常只有几十比特熵,只能靠迭代次数把每次猜测的成本拉高。HKDF 快,且假设输入本身已是高熵随机材料,用它处理口令会得到一个「一次猜测就很快」的结果,等于没有保护。反过来,拿 PBKDF2 去展开一个 256 位随机主密钥只是白白浪费 CPU。一句话:口令用 PBKDF2(或 bcrypt、Argon2),高熵材料用 HKDF。

盐要随机、要唯一、不用保密。盐的作用是让相同口令得到不同结果,从而杜绝彩虹表、也避免一个用户被攻破牵连其他人。所以盐必须每次生成都取新的随机值,至少 16 字节,并且和哈希一起明文存进数据库——盐不是秘密,它的价值在于唯一。旧格式常见的固定盐或短盐(比如 8 位十六进制)现在都算不合格。

迭代次数要跟着硬件涨。迭代次数没有一个一劳永逸的数值:今天够用的次数,几年后就是便宜的攻击成本。实践上按「单次校验耗时 100 到 500 毫秒」来定,然后定期重新评估。OWASP 目前给的参考值是 SHA-256 六十万次、SHA-512 二十一万次,只是下限。改迭代次数时注意存量数据——通常做法是校验通过后顺手用新参数重新哈希一遍。

接口签名该怎么做

一个能防住常见攻击的签名方案大概是:把参数按字典序排好拼成 k=v&k=v,加上时间戳和一次性随机串,整体作为消息,用 HMAC-SHA256 算出签名,把签名和随机串一起发给对方。对方收到后先检查时间戳是否在允许窗口内(比如 5 分钟)、随机串是否用过,然后自己重算一遍签名再比对。少了时间戳和随机串,攻击者可以把抓到的旧请求原样重放。

比对时一定要用定长比较(本页内置的比对就是),不要写 if (a === b)。字符串比较会在第一个不同的字符处提前返回,攻击者能通过统计响应时间一个字符一个字符地试出正确签名。同理,不要先把两边的 hex 转成小写再拼字符串比较,直接按字节比。

这套算法在真实系统里的位置

TLS 1.3 的密钥调度就是一连串 HKDF:握手协商出的共享密钥先当 IKM,用 ClientHello 的哈希当盐做 Extract,再按 c hs traffic、s ap traffic 这类标签当 info 展开出各个方向的密钥。JWT 的 HS256 是 HMAC-SHA256,TLS 的 PRF 也大量用到 HMAC。而 PBKDF2 则常出现在「从用户口令生成加密密钥」的场合,比如全盘加密、密码管理器的主密钥派生。