登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go crypto/mldsa Options.Context 怎样区分不同业务签名

来源:17golang原创

时间:2026-10-25 16:29:26 500浏览 收藏

线上把一把 ML-DSA 私钥同时交给“发票签名”和“登录挑战签名”后,最容易出现的不是密钥突然失效,而是业务边界没有被写进签名语义:验签方只看到“签名合法”,却不知道它原本属于哪个业务域。Go 的 crypto/mldsa 提供了 mldsa.Options.Context,正好可以把这个域标识带入签名和验签。结论是:为每个业务定义稳定、版本化的 Context,并让签名端和验签端使用完全相同的字符串;只改一端会让合法签名全部验签失败。

Context 不是密钥,也不是把业务名拼到消息里的替代写法。它是 ML-DSA 签名用途的附加区分信息,默认为空,最长 255 字节;同一密钥下只要 Context 不同,验签就不能互相替代。

影响面:同一密钥为什么会出现业务串域

假设服务保留一把密钥,发票服务用它签收款单,登录服务用它签挑战值。原来的调用只传消息和签名,验签端没有业务域字段,于是排查日志时只能回答“这段数据由这把钥匙签过”,不能回答“它是不是为当前业务签的”。这会让协议升级、消息转发和重放防护都变得含糊。

这次问题的触发条件通常有三个:多个业务共享密钥;消息格式在不同服务之间相似;验签函数的参数没有强制携带用途。修复目标不是立刻换钥匙,而是先让签名用途成为签名输入的一部分。

ML-DSA 同一密钥下发票与登录两个业务上下文的静态边界关系图
图1:结构说明图,展示同一把 ML-DSA 密钥、两个业务 Context 与各自验签入口的静态关系;它不是截图或运行证据。

先把 Context 设计成稳定的业务标识

Context 应该能在日志、协议字段和代码常量中被同时识别。建议采用“业务名 + 用途 + 版本”的短字符串,例如 invoice.sign.v1 和 login.challenge.v1。版本放进 Context 后,升级签名语义可以显式切换,而不是让旧验签器悄悄接受新协议。

不要把订单号、用户 ID 或时间戳塞进 Context。它们属于消息内容,Context 更适合表达稳定的用途边界。还要记住长度上限是 255 字节;如果业务标识超长,应在协议层重新命名,而不是截断后继续使用。

签名与验签必须使用同一个上下文

下面的辅助函数假设密钥已经由上层安全地加载,本文只处理业务域绑定。签名端和验签端分别构造同值的 Options;把 invoice.sign.v1 换成 login.challenge.v1 时,即使消息、签名和公钥都不变,也应得到验签错误。

package domainseal

import "crypto/mldsa"

const (
	// 用稳定且可读的标识区分两个业务域,版本变化时显式升级。
	invoiceContext = "invoice.sign.v1"
	loginContext   = "login.challenge.v1"
)

// signForDomain 只负责把签名绑定到一个业务用途。
func signForDomain(sk *mldsa.PrivateKey, message []byte, context string) ([]byte, error) {
	// mldsa.Sign 会忽略 io.Reader;Context 由 Options 传入签名语义。
	opts := &mldsa.Options{Context: context}
	return sk.Sign(nil, message, opts)
}

// verifyForDomain 要求验签方显式声明自己正在验证哪个业务。
func verifyForDomain(pk *mldsa.PublicKey, message, signature []byte, context string) error {
	// 这里的 Context 必须与签名端逐字一致,否则合法签名也会被拒绝。
	return mldsa.Verify(pk, message, signature, &mldsa.Options{Context: context})
}

// verifyInvoice 展示调用方如何固定用途,避免把任意字符串传进来。
func verifyInvoice(pk *mldsa.PublicKey, message, signature []byte) error {
	return verifyForDomain(pk, message, signature, invoiceContext)
}

// verifyLogin 展示另一个业务域使用独立的验签入口。
func verifyLogin(pk *mldsa.PublicKey, message, signature []byte) error {
	return verifyForDomain(pk, message, signature, loginContext)
}

代码中的关键点是“用途在调用边界固定”。如果把 context 直接作为来自请求的任意字符串,调用方仍可能误把发票签名交给登录验签器;更稳妥的做法是让每个业务入口只暴露自己的常量,并在日志中记录该常量名。

ML-DSA Context 在签名端与验签端保持同值并在错误上下文下拒绝的静态关系图
图2:关系说明图,展示同一 Context 在签名与验签两端的配对关系,以及跨业务 Context 不匹配的边界;它不是执行流程截图。

复盘时重点检查三个边界

现象根因处理方式
所有验签突然失败只在签名端加了 Context,旧验签端仍使用空值先发布共同配置,再切换签名流量
不同业务仍能互相验证两个入口都使用默认空 Context为每个用途固定版本化常量
升级后部分消息失败一端使用 v2,另一端还在使用 v1把 Context 版本写入协议和兼容窗口

如果调用方使用 crypto.MLDSAMu 处理预哈希消息,还要把预哈希格式与 Context 的变更一起纳入协议记录;不要把“验签失败”简单归因于公钥损坏。Context 为空也是合法默认值,但它不提供业务域隔离。

最后留下可执行的防复发清单

  • 每个业务域都有独立的 Context 常量,并带用途和版本。
  • 签名、验签、日志和协议字段使用同一套命名。
  • 测试至少覆盖同 Context 成功、跨 Context 失败、版本切换失败三种情况。
  • 密钥轮换与 Context 版本升级分开记录,避免排障时混淆两个变量。

相关问题

Context 可以放动态的订单号吗?

不建议。订单号应属于消息内容;Context 用来区分稳定的签名用途。把高频动态数据放进 Context 会让协议难以追踪,也不利于兼容和日志聚合。

签名端设置了 Context,验签端不设置会怎样?

验签端相当于使用默认空 Context,和签名端的非空 Context 不一致,验签应失败。迁移时要让两端先具备相同配置,再逐步切换新签名。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>