Go crypto/mldsa Options.Context 怎样区分不同业务签名
线上把一把 ML-DSA 私钥同时交给“发票签名”和“登录挑战签名”后,最容易出现的不是密钥突然失效,而是业务边界没有被写进签名语义:验签方只看到“签名合法”,却不知道它原本属于哪个业务域。Go 的 crypto/mldsa 提供了 mldsa.Options.Context,正好可以把这个域标识带入签名和验签。结论是:为每个业务定义稳定、版本化的 Context,并让签名端和验签端使用完全相同的字符串;只改一端会让合法签名全部验签失败。
Context 不是密钥,也不是把业务名拼到消息里的替代写法。它是 ML-DSA 签名用途的附加区分信息,默认为空,最长 255 字节;同一密钥下只要 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 直接作为来自请求的任意字符串,调用方仍可能误把发票签名交给登录验签器;更稳妥的做法是让每个业务入口只暴露自己的常量,并在日志中记录该常量名。

复盘时重点检查三个边界
| 现象 | 根因 | 处理方式 |
|---|---|---|
| 所有验签突然失败 | 只在签名端加了 Context,旧验签端仍使用空值 | 先发布共同配置,再切换签名流量 |
| 不同业务仍能互相验证 | 两个入口都使用默认空 Context | 为每个用途固定版本化常量 |
| 升级后部分消息失败 | 一端使用 v2,另一端还在使用 v1 | 把 Context 版本写入协议和兼容窗口 |
如果调用方使用 crypto.MLDSAMu 处理预哈希消息,还要把预哈希格式与 Context 的变更一起纳入协议记录;不要把“验签失败”简单归因于公钥损坏。Context 为空也是合法默认值,但它不提供业务域隔离。
最后留下可执行的防复发清单
- 每个业务域都有独立的 Context 常量,并带用途和版本。
- 签名、验签、日志和协议字段使用同一套命名。
- 测试至少覆盖同 Context 成功、跨 Context 失败、版本切换失败三种情况。
- 密钥轮换与 Context 版本升级分开记录,避免排障时混淆两个变量。
相关问题
Context 可以放动态的订单号吗?
不建议。订单号应属于消息内容;Context 用来区分稳定的签名用途。把高频动态数据放进 Context 会让协议难以追踪,也不利于兼容和日志聚合。
签名端设置了 Context,验签端不设置会怎样?
验签端相当于使用默认空 Context,和签名端的非空 Context 不一致,验签应失败。迁移时要让两端先具备相同配置,再逐步切换新签名。
Go humanize 怎么给大整数增加千位分隔符
- 上一篇
- Go humanize 怎么给大整数增加千位分隔符
- 下一篇
- Go net/http Cookie.Partitioned 怎样设置分区 Cookie
-
- Golang · Go教程 | 1小时前 |
- Go humanize 怎么给大整数增加千位分隔符
- 319浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go filepath.VolumeName 怎样提取 Windows 盘符和卷名
- 236浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go 标准库 uuid 怎样解析多种字符串格式并处理错误
- 322浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go multipart.Writer.SetBoundary 怎样固定上传请求的边界串
- 294浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go os.Readlink 怎样判断符号链接目标是相对路径还是绝对路径
- 363浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go netip.Addr.AppendBinary 将 IP 追加为二进制地址的处理方法
- 415浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go bytes.Buffer.Available 怎样判断缓冲区还能直接读取多少数据
- 293浏览 收藏
-
- Golang · Go教程 | 11小时前 |
- Go crypto/x509 ParsePKIXPublicKey 怎样读取证书公钥类型
- 232浏览 收藏
-
- Golang · Go教程 | 11小时前 |
- Go crypto/ecdh PrivateKey.ECDH 怎样生成共享密钥
- 266浏览 收藏
-
- Golang · Go教程 | 11小时前 |
- Go text/scanner.Scanner 怎样逐个读取源码词元
- 393浏览 收藏
-
- Golang · Go教程 | 11小时前 |
- Go JMESPath 怎么提前编译并复用字段查询
- 309浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 686次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 702次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 758次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 721次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 561次使用
-
- Java 性能优化上线清单:从定位、改造到灰度发布
- 2026-06-11 860浏览
-
- Spring Boot 压测验证:Gatling、JMeter 与性能回归门禁
- 2026-06-11 843浏览
-
- Java NMT 非堆内存排查:Direct Buffer、线程栈与 Metaspace 分析
- 2026-06-11 826浏览
-
- Spring Boot 容器内存优化:JVM 堆、非堆与 MaxRAMPercentage
- 2026-06-11 809浏览
-
- Tomcat 连接与线程参数调优:maxThreads、acceptCount 与 KeepAlive
- 2026-06-11 792浏览

