计算标签是容易的部分
正确地产生一个 已被充分理解。错误集中在验证一侧,那里有两个问题会绊倒那些做对了密码学却做错了周边逻辑的人:你如何比较标签,以及一个有效标签究竟证明了什么。
以恒定时间比较
为了验证,接收方在消息上重新计算 HMAC 并将其与发送方提供的标签比较。比较两个字符串的自然方式,一个普通的 ==,一旦发现第一个不同的字节就返回。这种提前退出会泄露时间:一个首字节正确的标签,被拒绝所花的时间会比一个立即就错的标签略长。通过在多次尝试中测量响应时间,攻击者可以逐字节地恢复出正确的标签,这是一次计时攻击。
防御是一种恒定时间比较,它始终检查所有字节,无论第一处差异在哪里,从而所花的时间不会泄露一次猜测有多接近。大多数语言都提供了一个:
- Node.js:
crypto.timingSafeEqual(a, b) - Python:
hmac.compare_digest(a, b) - Go:
hmac.Equal(a, b)
绝不要用一个普通的字符串比较来验证签名。
一个有效签名不是一个新鲜请求
即使是一个完美验证的 HMAC,也只证明消息由持有密钥的某人产生且未被篡改。它对何时只字不提。一个捕获了合法已签名请求的攻击者可以再次发送完全相同的字节,而签名仍会通过验证。这是一次重放攻击,对任何引发效果的事物都重要:一笔支付、一次状态变更、一个命令。
阻止重放需要在已签名数据中放入某种让每个请求都唯一且可核验的东西:
- 已签名负载中的一个时间戳,服务器拒绝短窗口之外的请求(并考虑时钟漂移)。
- 一个nonce,每个请求唯一的值,服务器会记录它并拒绝接受两次。
两者都必须位于已签名内容之内,这样攻击者就无法在不破坏签名的情况下更改它们。
两部分规则
一个请求只有在其 HMAC 以恒定时间验证且它是新鲜的之时才可信。在任一处出错,签名都会带来虚假的安全感。这是与签名请求相同纪律的验证一侧。
HMAC 工具 允许你直接在浏览器中为一条消息和一个密钥计算并比较标签,不发送任何内容,因此你可以在开发或调试时手动核对一个签名。