在本机给 HTTP 请求签名
AWS SigV4、阿里云 ACS3、腾讯云 TC3、OAuth 1.0a 和 webhook HMAC 到底签了什么,以及签名对不上时怎样排查,而不必把密钥贴到别人的网站。
· 5 分钟阅读
签名错误很难查。服务端往往只回一句「我们算出的签名和你提供的不一致」,然后就停了。它不会告诉你日期格式错了、漏签了 host,还是你哈希的正文并不是发出去的那一份。下一步如果是把访问密钥贴进一个「在线计算 SigV4」的网页,那个网页就拿到了密钥。
HTTP 签名计算器 在当前标签页里做同一套运算。这篇说明的是这套运算到底覆盖了什么,好让对不上的时候有地方看。如果失败的调用本来是从开发者工具复制出来的 curl,curl 转换器 是另一半:它把方法、URL、头和正文摊成代码,同样不上传。
签名签的是哪一段字符串
签名不是含糊地说「把整个请求哈希一下」。它是对一段双方必须用同一种方法拼出来的字符串做哈希。这段字符串里改一个字节,签名就变了,哪怕人眼觉得请求还是同一个。
AWS Signature Version 4 分四层:
- 规范请求:方法、规范化的路径、规范化的查询、按名字排序的已签名头及其取值,再加上正文的 SHA-256 十六进制。
- 待签字符串:算法名、时间戳、凭证范围(
日期/地域/服务/aws4_request),以及规范请求的哈希。 - 签名密钥:用秘密访问密钥做一串 HMAC,依次是日期、地域、服务,最后是字面量
aws4_request。 - 用这个派生密钥对待签字符串再做一次 HMAC。结果放进
Authorization,旁边是访问密钥 ID、范围,以及参与签名的头名字列表。
阿里云 ACS3 和腾讯云 TC3 是同一形状,头的名字和范围字符串不同。OAuth 1.0a 更老:它排序参数,而不是拼规范请求,然后用 HMAC-SHA1。Webhook 通常更短:对原始正文做 HMAC,有时是 时间戳 + "." + 正文,再和对方发来的头比较。
两边不一致,就是某一层的输入不一致。计算器把中间字符串打出来,就是为了看见是哪一层动了。
看起来像密码学问题、其实不是
大多数对不上并不是 HMAC 实现错了。Web Crypto 和服务器上的库算 HMAC-SHA256 的方式是一样的。不一样的是喂进去的材料。
正文哈希针对的是线上的字节。 JSON 库如果重排了键、多了一个空格,或把 \u0026 写成 &,SHA-256 就不同。先美化再签名、发出去的却是压缩形式,一定会失败。签你真正要发送的那一份字节。空正文也要哈希:SigV4 用的是空字符串的哈希,那是一个固定常数,不是 {} 的哈希,也不是把字段省掉。
参与签名的头是一个集合,这个集合本身也在签名里。 你签了 host 和 x-amz-date,服务器还期望 content-type,那么服务器上的规范请求就比你的长。头名字要小写并排序。取值有规定的裁剪方式。列表里写了却没发送的头,或被代理改了大小写的头,都会让结果对不上。Authorization 里列着这些名字。先比这份名单,再比十六进制。
时钟和凭证范围。 x-amz-date 的形式是 YYYYMMDDTHHMMSSZ,UTC,中间没有冒号。范围只用日期那一段,然后是地域,然后是服务名(s3、execute-api 等)。用 us-east-1 签名、请求却发到 eu-west-1 的端点,签出来的是另一个范围。服务器按它收到的端点计算范围,不按你心里想的地域。
路径和查询要按同一套规则编码。 查询参数没排序,或者一边把 + 当成空格、另一边保留 %2B,规范请求就变了。这和 Base64 不是一回事。百分号编码处理的是 URI 里的保留字符。签名关心的是两边是否对同样的字符做了同样的编码。
临时凭证必须带上令牌头。 来自 STS 或角色会话的密钥不够。会话令牌是一个头(AWS 上是 x-amz-security-token),而且必须出现在已签名的集合里。漏掉它,签名在本地看起来自洽,服务器仍然拒绝。
Webhook 是同一类陷阱的缩小版
GitHub、Stripe 和 Slack 不用 SigV4。它们对原始正文签名,让你能区分真实投递和伪造的 POST。失败的方式更少,但一样常见。
Stripe 签的是 时间戳.正文,并要求你拒绝过旧的时间戳,因为截获的投递可以重放。如果你只哈希正文、忘了时间戳前缀,十六进制永远对不上。框架如果已经把 JSON 解析过,你再序列化回去,字节在哈希之前就变了。要读原始正文。
GitHub 的 X-Hub-Signature-256 是 sha256= 加上用 webhook 密钥对原始正文做的 HMAC 十六进制。Slack 在签名密钥和更老的验证令牌之间换过方案。计算器的用途是:按文档写的那一种方案,对着你已经拿到的载荷试一次,密钥留在自己的机器上,而不是贴进一张表单。
对得上只说明字节和密钥一致。它不表示你可以不做时间戳检查,也不表示正文里的 JSON 就是你信任的结构。
OAuth 1.0a 还在
少数旧 API 仍然要 OAuth 1.0a 的 HMAC-SHA1。签名基串是方法、基础 URL 和参数,百分号编码后再排序。nonce 和时间戳也是参数,所以它们在签名里面。签名密钥是 consumer_secret&token_secret;没有令牌时,令牌密钥那一侧是空的。一个库先排序再编码,另一个把空格编成 + 而不是 %20,两边就不会一致。把基串打出来。错的很少是 HMAC 本身。
两个工具怎么一起用
把失败的调用复制成 curl。若你要在改客户端的同时把它看成 fetch 或 Python,用 curl 转换器,把头从反斜杠续行里摊开。然后打开 签名计算器,填同一套方法、URL、头、正文、地域、服务和密钥。
先比规范请求,不要先比最终签名。规范请求已经和厂商示例一致、签名仍不对,那是密钥或范围错了。规范请求就不一致,停下来。HMAC 修不好一段不同的字符串。
这套步骤不需要离开浏览器。页面也不会替你把请求发出去。签名正确只说明两边用同一个密钥拼出了同一段字符串,不说明这次调用在业务上被允许。