摘要

  • HTTP 消息签名保护的不是“整个请求”这个抽象概念,而是 Signature-Input 明列的有序字段、派生组件与签名参数。未列入的方法、authority、目标 URI、凭据或正文可以改变,而密码学验证仍然成功。
  • 验证者才是信任边界的所有者。它必须挑中正确的签名,解析获准密钥,执行时效与防重放规则,重算正文摘要,再把签名人映射到当前资源与动作权限。
  • 代理验证后再改写并签名,会产生一份新的托管声明。它可以证明代理看见和转交了什么,却不能倒过来声称客户端曾签过代理后来添加的内容。

一条每一项检查都“绿色”的错误命令

设想一套基础设施 API 收到一条 POST。签名算法符合策略,keyid 能查到密钥,created 仍在五分钟窗口内,Content-Digest 也与收到的 JSON 相同。仪表板给出“签名有效”。

真正危险的信息藏在覆盖清单里:它只有 date 和 content-digest,没有 @method、@authority、@target-uri,也没有一次性挑战。那枚签名至多说明某个密钥持有人在某个时间认可过这段正文。它没有说明这段正文应在这个主机、这条路径、这个账户下,以这种方法执行一次。

同一正文可以从预演接口移到执行接口;抓到的请求可以在窗口内再送一次;网关也可以把外部 authority 改成另一个内部服务。只要被改的内容从未进入签名基底,所有密码学步骤都可能继续正确。

算法没有失灵。失灵的是组织给“验证通过”附加了签名本身并未表达的权力。

RFC 9421 证明的是重建后的语义对象

HTTP 天生允许中间环节。代理会合并字段行、转换协议版本、添加转发信息、改写内容编码。若直接对某一跳的原始字节签名,正常代理也会被当作攻击者。

RFC 9421 因而让双方各自构造确定性的 signature base。签名人选择普通 HTTP 字段,或 @method、@authority、@target-uri、@status 等派生组件。顺序写在 Signature-Input 里,最后的 @signature-params 又把清单以及 created、expires、keyid、nonce、tag 等参数一并纳入。

验证者从自己实际收到的消息重建同一基底。运算成功后,规范给出的结论十分克制:收到的消息只在已覆盖组件范围内,与当初签名的消息语义等价。

这句话里的“范围内”就是整条边界。签两个组件与签十个组件,不是强弱不同的同一声明,而是关于两个不同对象的声明。

覆盖清单才是产品政策

通用标准无法替每个应用判断什么会改变命令含义。只读查询可能必须绑定方法、authority 与目标;资金或路由策略变更还需要正文摘要、账户上下文、资源版本、幂等键或挑战值;签名回执则可能需要状态码、响应摘要以及原请求组件。

所以验证者必须按动作匹配覆盖配置,而不是检测消息里有没有 Signature。DELETE 没有覆盖 @method,就可能挪到另一种方法;多租户操作没有覆盖 authority 与目标,便可能跨服务或账户边界;身份凭据未签,攻击者就能让一枚完好的证明与另一份身份声明同行。

“所有字段都签”同样不是答案。Via、Forwarded 本来就会沿途变化。盲目固定它们,正常链路也会失败。正确做法是覆盖一切会改变授权或结果的组件,并明确写出哪些转换属于哪一个受信代理的职责。

Heng Lu 的 Minimum Initial Specification 在这里很有用:共同层把构造和验证写得严格、可本地复现;将来每个应用仍保留对必签组件、可接受密钥、时效与动作权限的本地决定权。薄规范不是模糊规范,而是拒绝把格式标准膨胀成业务主权。

正文要经过摘要这座桥

RFC 9421 不直接把任意正文放入 signature base。通常需要 RFC 9530 的 Content-Digest:先算摘要,把摘要字段列为覆盖组件,验证签名,然后对实际收到的正文重算并比较。

少一步都会留下空洞。摘要未签时,正文和摘要可一起替换;摘要虽签却不重算时,攻击者可保留受保护的摘要值,同时换掉正文。有效签名只证明“这串摘要值被签过”,验证者还要证明“它确实描述眼前的正文”。

解释正文的元数据也可能重要。若 Content-Type 或 Content-Encoding 没有进入策略,相同字节可能交给不同解析器,或在不同表示层上计算摘要。密码学对字节极其精确,业务错误却常发生在“这些字节是什么”的问题上。

Trailer 还带来时间边界。代理可以丢弃 trailer;流式应用若在摘要到达前已经执行业务,就等于先产生后果、后完成证明。可撤销的缓存也许承受得起,永久删除则未必。

BTW 现有 Digest Fields 文章讨论摘要究竟命名哪些字节。本篇讨论另一件事:谁把摘要字段与哪些控制组件绑在一起,验证者如何据此授权。两套机制互补,不能相互冒充。

keyid 只是索引,不是身份证

keyid 是签名人提供的定位信息,自身没有认证能力。RFC 9421 特意不规定全球密钥发现或统一身份体系,也不替应用选算法。

这项克制避免了格式标准成为中央身份机构,却把风险交给本地密钥解析。若服务接受请求方临时附带的任何公钥,攻击者用自己的密钥给自己的命令签名,也能得到“验证成功”。运算证明的是私钥持有,不是被授权身份。

生产证据至少要记录:实际解析到的密钥版本、信任源与政策版本、算法、启用和退役区间、对应主体、角色以及允许的动作类别。只保留 keyid 字符串,轮换后就无法重建当时用的是哪把钥匙。

迁移期可以有双密钥或双算法,但必须有公开的开始、比较和退出时点。若旧钥匙因为“兼容性”无限保留,轮换只增加入口,没有减少风险。

IANA 登记让不同实现认识同一个参数或算法名,不代表该算法适合一条不可逆命令。企业目录能证明的范围,也只到其自身被授权维护的关系。

时间窗口不会自动消耗一次使用

created 说明签名生成时间,expires 表达签名人愿意负责到何时。允许多旧、容忍多少时钟偏差,由验证应用决定。密码学仍对,并不妨碍策略认为它已经过期。

短窗口也不是单次窗口。三十秒内可以重放很多次,除非消息中有可识别的一次性对象,而验证系统又真正消费它。

nonce 提供承载位置,却不提供分布式状态。在多地区部署中,两处都可能先读到“未使用”,随后各自执行。防重放需要原子写入、明确作用域和足够长的保留期。

tag 可以帮助验证者从多枚签名里挑候选,但它是公开值,任何人都能复制。标签只缩小搜索范围,仍然不能替代覆盖、密钥、时间和权限判断。

代理签名不是客户端授权的续写

RFC 9110 将代理与网关视为 HTTP 的正常组成。一个边缘网关可以验证客户端签名,删除隐私字段,重建内部目标,并用自己的密钥给新消息签名。

这时有两份证词。客户端签名描述外部请求中它实际覆盖的部分;代理签名描述代理验证和改写后的内部形态。第二份不会把后来添加的字段追认成客户端原意。

源服务必须明确代理拥有什么授权。代理也许只能证明“我验证过一份符合 A 配置的客户端请求”,也许被委托替内部调用者发令。两种模式不能靠同一个 signature_valid=true 区分。

审计记录应保存外部上下文、被选中的客户端签名、覆盖配置、改写类型、代理签名与内部上下文。若只留下最后一跳,系统就无法回答客户端到底有没有签过决定权限的目标与方法。

HTTP/1.1 到 HTTP/2 的转换是典型例子。authority 在前者常见为 Host,在后者表现为 :authority。@authority 旨在表达跨版本的语义值。若签的是某一版本的线格式,合法转换会失败;更坏的情况是验证层和授权层各取了不同的 authority。

多枚有效签名仍可能没有一枚适用

一条消息可以同时带客户端、网关和审批者的签名,也可以在算法迁移期间带新旧两枚。它们各有 label、key、组件清单与用途。

“找到第一枚能验过的签名”会把数学正确误当相关性。用于日志托管的签名不是支付授权,网关回执也不必然等于终端用户指令。验证者要按角色、label、tag、覆盖配置和密钥政策选中目标签名。

响应签名还能通过 req 参数覆盖原请求组件。这项能力只有在验证者握有准确关联请求时才有意义。将签名回执与请求分开存放,再凭“两边都有签名”硬凑关系,正好丢掉了它试图保护的上下文。

TLS、缓存与业务判断仍是其他层

TLS 1.3 保护传输通道,提供机密性并按配置认证对端。HTTP 消息签名能让选定语义跨连接或跨代理继续验证。前者不自动保留端到端消息托管,后者不加密,也不替代 TLS。

HTTP 认证可以提交凭据,应用仍要根据当前主体、资源、动作和状态做授权。签名响应也不会自动获得缓存许可;RFC 9111 负责那套判断。

这正是 reality layers 的纪律:signature base 是协调对象,验证成功是运行结果,密钥映射是身份声明,授权是本地决定,commit 与下游效果才是业务现实。前一层的清晰,不构成对后一层的统治权。

来源与证据边界

这些来源定义格式、语义和安全限制,不证明任何具名产品的现实行为或市场采用率,也不证明人的身份、法律意图、不可否认性或商业正确。那些结论需要独立证据。