Go RSA-PSS 签名验签参数不匹配怎么办
Go 里 RSA-PSS 签名成功、验签却返回错误时,先不要怀疑公钥格式。最常见的兼容性原因是签名端和验签端没有共享同一组摘要与 PSS 参数:消息算出的 digest 不同、传入的 crypto.Hash 不同,或者 PSSOptions.SaltLength 没有统一。尤其要注意,VerifyPSS 会忽略 opts.Hash,验签使用的是它的 hash 参数。
我在接入外部签名服务时,最容易漏掉的就是“Hash 名称相同,但盐长度约定不同”。如果协议没有特殊要求,本文建议从 SHA-256 + PSSSaltLengthEqualsHash 开始;如果外部协议明确规定盐长度,就把字节数写死在双方配置里。这样排查范围会比反复更换密钥小很多。
Go 官方标准库地址:https://pkg.go.dev/crypto/rsa
Hash 类型说明:https://pkg.go.dev/crypto#Hash
三个参数必须先对齐
RSA-PSS 的验签输入可以拆成三组:原始消息经过某个 Hash 得到的摘要、调用 API 时传入的 Hash 标识,以及 PSS 盐长度。摘要字节和 Hash 标识必须相互对应;盐长度则参与 PSS 编码。公钥只是验证签名的材料,不能替代这些参数约定。

Go 文档对 SignPSS 的说明是:digest 必须由传入的 Hash 算出;如果 opts.Hash 非零,它会覆盖 SignPSS 的 hash 参数。对 VerifyPSS 来说,opts.Hash 会被忽略,调用方必须把正确的 Hash 传进第二个参数。
先固定 SHA-256,再固定等 Hash 盐长
下面的示例把摘要计算和 PSS 参数放在同一个函数边界里,避免签名端改了 Hash 却忘记同步摘要。PSSSaltLengthEqualsHash 表示盐长度等于所用 Hash 的摘要长度;使用 SHA-256 时就是 32 字节。
package main
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"fmt"
)
// SignAndVerify 使用同一组摘要和 PSS 盐长完成签名与验签。
func SignAndVerify(priv *rsa.PrivateKey, message []byte) error {
hashID := crypto.SHA256
if !hashID.Available() {
return fmt.Errorf("SHA-256 未链接到当前二进制")
}
// digest 必须由同一个 Hash 计算,不能只改 hashID 而继续复用旧摘要。
digest := sha256.Sum256(message)
opts := &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
}
signature, err := rsa.SignPSS(rand.Reader, priv, hashID, digest[:], opts)
if err != nil {
return fmt.Errorf("RSA-PSS 签名失败: %w", err)
}
// VerifyPSS 的 hash 参数决定验签使用的 Hash;opts.Hash 在这里不会生效。
if err := rsa.VerifyPSS(&priv.PublicKey, hashID, digest[:], signature, opts); err != nil {
return fmt.Errorf("RSA-PSS 验签失败: %w", err)
}
return nil
}
func main() {
// 2048 位密钥仅用于示例;生产代码应从受保护的密钥来源读取。
priv, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
panic(err)
}
if err := SignAndVerify(priv, []byte("订单摘要")); err != nil {
panic(err)
}
fmt.Println("signature parameters are aligned")
}
这里有两个故意的约束:digest 和 hashID 在同一段代码里生成,opts 同时传给签名与验签。实际项目可以把它们拆成两个服务,但协议配置必须保留这三个值,而不是只记录“算法是 RSA-PSS”。
Hash 不一致时,为什么只改 opts.Hash 没用
假设签名端使用 sha256.Sum256,验签端却重新用 SHA-512 计算摘要,即使验签的 PSSOptions 写了 Hash: crypto.SHA256,也不能把已经传入的 SHA-512 摘要变成 SHA-256。更直接的写法是让验签端从协议配置选择 Hash,并用同一个配置计算 digest:
// DigestForPSS 返回与 hashID 对应的摘要,避免摘要和标识错配。
func DigestForPSS(hashID crypto.Hash, message []byte) ([]byte, error) {
if !hashID.Available() {
return nil, fmt.Errorf("Hash 未链接: %v", hashID)
}
h := hashID.New()
// 写入失败在标准 Hash 实现中通常不会发生,但仍保留错误处理边界。
if _, err := h.Write(message); err != nil {
return nil, fmt.Errorf("计算摘要失败: %w", err)
}
return h.Sum(nil), nil
}
// VerifyConfiguredSignature 让协议 Hash 参与摘要计算和验签调用。
func VerifyConfiguredSignature(pub *rsa.PublicKey, hashID crypto.Hash, message, sig []byte) error {
digest, err := DigestForPSS(hashID, message)
if err != nil {
return err
}
// 盐长策略仍由协议决定;这里只示范等 Hash 的默认契约。
opts := &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}
return rsa.VerifyPSS(pub, hashID, digest, sig, opts)
}
Hash.New 只能在 Hash 已链接时使用,因此在动态选择算法时先检查 Available。对固定 SHA-256 的项目,直接使用 sha256.Sum256 更容易读;对协议兼容层,统一的 DigestForPSS 可以减少“枚举值改了、摘要实现没改”的错误。
固定盐长与自动盐长怎么选
Go 提供两类常用策略。PSSSaltLengthEqualsHash 把盐长固定为摘要长度;也可以把 SaltLength 设置成正整数,明确规定固定字节数。PSSSaltLengthAuto 在签名时让盐尽可能长,在验签时自动检测。自动策略在 Go 内部可以方便地配合,但跨语言时未必符合对方库的默认值。

| 盐长设置 | 适合场景 | 排查重点 |
|---|---|---|
PSSSaltLengthEqualsHash | 双方都能接受标准化的等 Hash 约定 | Hash 必须一致,SHA-256 对应 32 字节 |
正整数,例如 32 | 外部协议明确写出盐长度 | 两端都使用同一个字节数,不能只改一边 |
PSSSaltLengthAuto | Go 内部兼容多种已知签名,或验签方允许自动识别 | 签名端默认盐长与外部实现的默认盐长可能不同 |
如果对方文档只写“RSA-PSS with SHA-256”,这还不够作为互操作协议。需要继续确认 MGF1 Hash、盐长度、签名编码以及消息是直接 Hash 还是接收已经计算好的 digest。本文只聚焦 Go 的 Hash 和盐长两个边界,其他协议字段不能假设默认值相同。
按错误现象缩小排查范围
RSA-PSS 验签失败时,建议按下面的顺序记录输入,而不是先换一把密钥。错误信息通常是刻意模糊的验证错误,真正有用的是双方对协议字段的逐项比对。
| 现象 | 优先检查 | 修复动作 |
|---|---|---|
| 签名端直接报 message too long | 密钥长度、Hash 摘要长度和盐长度组合 | 减小盐长或按协议换用更大的 RSA 密钥 |
| 同一进程内也无法验签 | digest 是否被重新计算、Hash 是否同一枚举值 | 让摘要与 hash 来自同一配置 |
| Go 对 Go 成功,外部服务失败 | 对方的固定盐长、MGF1 Hash 和编码约定 | 优先改为协议规定的正整数盐长 |
换成 opts.Hash 仍失败 | 是否误以为 VerifyPSS 会读取 opts.Hash | 把正确 Hash 传给 VerifyPSS 的第二个参数 |
签名函数返回 ErrMessageTooLong 时,盐长确实可能是原因之一;这和验签失败不是同一层问题。验签失败还要检查消息是否被编码、换行或字符集转换过。只要 digest 不同,盐长调得再准确也不会成功。
把参数写成协议,而不是散落在调用点
在项目里,我会把 RSA-PSS 的约定写成不可变配置或测试向量:hash=SHA-256、salt_length=32、mgf1_hash=SHA-256,并记录签名输入的字节编码。调用代码只接收已经解析的配置,避免一个服务用 Auto、另一个服务用等 Hash。
如果使用 crypto.Signer 接口而不是直接调用 SignPSS,还要注意 PSSOptions.Hash 是指定 Hash 的方式。这里不能把直接调用 VerifyPSS 的规则套过来;应按实际实现的接口契约写测试。
最小互操作测试至少包含:同一消息由 Go 签名、Go 验签;固定测试向量由外部实现签名、Go 验签;Go 签名、外部实现验签。每条向量都固定公钥、消息字节、Hash、盐长度和签名编码。测试通过后,再把它们作为协议变更的回归样本。
为什么签名每次得到的字节可能不同
PSS 签名包含随机盐。同一消息、同一密钥和同一盐长下,随机源不同也可能得到不同签名;验签不要求签名字节可重复,只要求参数和结果符合协议。
能不能验签时把 SaltLength 留成零值
零值对应 PSSSaltLengthAuto。它不是“忽略盐长约定”,而是选择自动策略。若协议要求固定盐长,应显式写等 Hash 或正整数,避免以后更换实现时默认行为变化。
只要 Hash 一样,盐长度不同也能验签吗
不能这样假设。Hash 和盐长都参与 PSS 参数;除非验签策略明确允许自动识别并且签名格式符合实现要求,否则应把盐长当作协议字段同步。
这类问题最后应收敛为一张参数表:消息字节怎样得到、Hash 用哪个枚举、摘要是否由该 Hash 计算、盐长是等 Hash、固定字节还是自动策略。把这四项和签名编码一起写进协议,通常比在验签失败后反复改调用顺序更有效。
Go Ed25519 验签失败时怎样核对消息和公钥
- 上一篇
- Go Ed25519 验签失败时怎样核对消息和公钥
- 下一篇
- Go HKDF 派生密钥读不满指定长度怎么办
-
- Golang · Go问答 | 30分钟前 |
- Go context.Value 取不到值时怎样排查 key 类型
- 368浏览 收藏
-
- Golang · Go问答 | 32分钟前 |
- Go 上下文已取消但下游请求仍未退出怎么办
- 313浏览 收藏
-
- Golang · Go问答 | 38分钟前 |
- Go encoding/csv 读取变长字段时怎样识别格式错误
- 499浏览 收藏
-
- Golang · Go问答 | 39分钟前 |
- Go 精简容器加载时区失败怎么办
- 401浏览 收藏
-
- Golang · Go问答 | 41分钟前 | go · Go 时区 time.Parse time.ParseInLocation 时间布局
- Go time.Parse 时间布局写对但解析失败怎么办
- 487浏览 收藏
-
- Golang · Go问答 | 44分钟前 |
- Go 正则处理非法 UTF-8 字节时结果异常怎么办
- 199浏览 收藏
-
- Golang · Go问答 | 46分钟前 |
- Go POSIX 正则结果与 Perl 模式不同怎么办
- 491浏览 收藏
-
- Golang · Go问答 | 49分钟前 | go ·
- Go 有理数解析小数失败怎么办
- 349浏览 收藏
-
- Golang · Go问答 | 54分钟前 |
- Go HKDF 派生密钥读不满指定长度怎么办
- 257浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 506次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 568次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 592次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 545次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 360次使用
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Golang标准库unsafe源码解读
- 2022-12-29 464浏览
-
- 快速掌握Go语言HTTP标准库的实现方法
- 2022-12-30 327浏览
-
- 解析golang 标准库template的代码生成方法
- 2022-12-24 349浏览
