MiServe DevTools

我的贴纸

把工具固定在这里,会保存在此浏览器中。

HMAC 生成器

用 HMAC-SHA256 或其他哈希算法以密钥对消息签名,并验证收到的签名。

HMAC(hex)
HMAC(Base64)

通过 WebAssembly 在浏览器中运行。粘贴的内容不会被上传。

关于此工具

HMAC 能证明什么

HMAC(RFC 2104)通过哈希函数把消息与密钥混合,输出一个简短的认证标签(tag)。持有同一密钥的人可以重新算出这个标签。如果两个标签相同,就说明消息在传输中没有被改动(完整性),而且是由知道密钥的人生成的(真实性)。

普通哈希做不到这一点,因为篡改消息的人同样可以重新计算它的哈希。HMAC 不会隐藏消息,所以它不是加密。而且由于双方持有相同的密钥,它无法证明消息是双方中的哪一方写的,这需要使用私钥的数字签名。

Webhook 签名

最常遇到 HMAC 的地方是 Webhook。提供方用与你共享的密钥对请求体签名,并把标签放在请求头中发送;你的服务器在信任该请求之前,先算出同样的 HMAC。GitHub 以 hex 形式发送 X-Hub-Signature-256: sha256=…,Shopify 以 Base64 形式发送 X-Shopify-Hmac-SHA256,Stripe 则对时间戳、一个点号和请求体签名,再把结果以 v1=… 的形式放在 Stripe-Signature 中发送。

要对请求体到达时的原始字节原样签名。如果框架先解析了 JSON,你又重新序列化,空白和键的顺序就会改变,HMAC 也就对不上了。如果这里的签名验证失败,先检查密钥编码(文本、hex 还是 Base64),再检查消息末尾是否有换行。

为什么服务器要用常量时间比较

普通的字符串比较在遇到第一个不同的字符时就会停止。在服务器上,这会让伪造签名的前几个字节错误时响应稍快一些、正确时稍慢一些,能测量出这一差异的攻击者就可以逐字节还原出有效的标签。常量时间比较无论结果如何都会读完每个字节。请使用你所用平台提供的函数:Node.js 中的 crypto.timingSafeEqual、Go 中的 hmac.Equal、Python 中的 hmac.compare_digest、PHP 中的 hash_equals。

本页的比对框也是这样工作的:它把两个值都解码为字节并全部读完。在已经同时持有两个值的浏览器标签页里,并没有需要隐藏的时间差,所以在这里它只用来告诉你,你自己的代码是否算出了正确的标签。

运行方式

标签由 Go 的 crypto/hmac 包计算,编译为 WebAssembly 后在浏览器的后台线程中运行。消息按 UTF-8 处理,不添加也不删减任何内容。只有一点会变化:浏览器的文本框会把每个换行都保存为单个换行符(LF),因此以 CRLF 换行的消息在这里会按 LF 签名。密钥按 UTF-8 文本处理,或在你指定时从 hex 或 Base64 解码。两者都不会被发送到任何地方。不过,生产环境的密钥最好还是不要放进网页:能用测试密钥时,就用测试密钥。

常见问题

为什么我的 HMAC 和服务器算出来的不一样?

常见原因有:密钥按错误的编码读取(把 hex 或 Base64 密钥当作纯文本)、消息末尾有换行或换行符不同、请求体经过解析后又被重新序列化,或者哈希算法不同。还要确认对方输出的是 hex 还是 Base64。

HMAC 就是对密钥加消息计算哈希吗?

不是。对于 SHA-256、SHA-512、SHA-1 或 MD5,对密钥加消息计算出的哈希可以被扩展:看到这个哈希的人无需知道密钥,就能追加数据并算出有效的哈希。HMAC 用两个派生密钥计算两次哈希,堵住了这个漏洞。

HMAC 密钥应该多长?

使用至少与哈希输出等长的随机密钥:SHA-256 为 32 字节,SHA-512 为 64 字节。更长的密钥作用不大,而超过哈希分组长度的密钥会先被哈希压缩。过短或可猜测的密钥,可以用一组已知的消息和标签逐个尝试候选值找出来。

HMAC-SHA1 和 HMAC-MD5 已经被攻破了吗?

攻破 SHA-1 和 MD5(作为普通哈希)的碰撞攻击,并不能转化为对其 HMAC 的实际伪造。它们仍出现在一些较旧的协议中,但新设计请选择 HMAC-SHA256 或更强的算法。

哈希分类的其他工具