签名校验前必须验证签名算法与密钥一致性:pkcs#1 v1.5和pss互不兼容,须成对使用;签名输入须覆盖全部可信字段(含时间戳、版本号等);验签失败统一返回模糊错误,且强制校验时效性与密钥生命周期。

签名校验前必须验证签名算法与密钥一致性
很多开发者直接拿 rsa.SignPKCS1v15 签名,却用 rsa.VerifyPSS 校验,结果永远返回 crypto.ErrInvalidSignature。这不是代码写错了,而是算法不匹配——PKCS#1 v1.5 和 PSS 是两种互不兼容的填充方案。
实操建议:
- 签名和验签两端必须使用完全相同的算法组合:比如都用
rsa.SignPSS+rsa.VerifyPSS,且hash.Hash类型(如crypto.SHA256)、盐值长度(saltLength)也需一致 - 密钥对必须成对生成;公钥不能从私钥“推导”或手动修改字段,否则验签失败且无明确错误提示
- 若对接外部系统(如政务平台),务必确认其文档中明确标注的算法标识符,例如
"RS256"对应rsa.SignPKCS1v15+crypto.SHA256,而非 PSS
防篡改的关键在于签名覆盖全部可信数据字段
常见错误是只对业务数据结构体做 json.Marshal 后签名,但忽略了时间戳、版本号、签名者 ID 等元信息。攻击者可截获请求,复用旧签名+修改 payload,只要后端没校验这些字段,就无法识别。
实操建议:
- 定义一个不可变的签名输入结构体,例如:
type SignedPayload struct { Version string `json:"v"` Timestamp int64 `json:"t"` SignerID string `json:"id"` Data []byte `json:"d"` // 原始业务数据(已序列化) Signature []byte `json:"s,omitempty"` // 验证时不参与计算 } - 签名前调用
json.Marshal得到字节流,再传给rsa.SignPKCS1v15;验签前先解码 JSON,剥离Signature字段,重新 Marshal 其余字段用于比对 - 避免使用
reflect.DeepEqual或结构体指针比较——JSON 序列化顺序、空字段处理、浮点数精度都会导致哈希不一致
验签失败时不要暴露具体错误原因
Go 的 rsa.VerifyPKCS1v15 在密钥不匹配、哈希不一致、填充错误等场景下都返回同一个 crypto.ErrInvalidSignature,这是设计使然。但若你在日志里打印出 “signature hash mismatch” 或 “wrong public key”,等于帮攻击者做侧信道分析。
实操建议:
- 统一返回模糊错误:仅记录
"signature verification failed"到审计日志,并带上请求 ID 和时间戳 - 禁止在响应体中返回任何与密码学过程相关的提示,HTTP 状态码始终用
401 Unauthorized或403 Forbidden,不区分是签名错还是过期 - 对高频失败请求(如 1 分钟内同一
SignerID失败 ≥5 次),触发临时限流,防止暴力穷举公钥或算法参数
生产环境必须校验签名时效性与密钥生命周期
签名本身防篡改,但不防重放。没有时间窗口校验的模块,等于把“一次有效”的签名变成“永久有效”。更隐蔽的问题是:证书过期、密钥轮转后,旧公钥仍被硬编码在代码里,导致新签名无法通过验证。
实操建议:
- 验签时强制检查
Timestamp字段:当前时间 -payload.Timestamp必须 5 * time.Minute,且payload.Timestamp不得大于当前时间(防时钟回拨) - 公钥不应写死在代码或配置文件中;推荐从可信源(如 KMS、Vault 或带签名的配置服务)动态加载,并缓存带 TTL(如 1 小时),定期刷新
- 每个公钥关联唯一
keyID,放在签名载荷头部(如 JWT 的kid字段),验签时据此选择对应公钥,支持多密钥并行运行与平滑轮转
最常被忽略的一点:JSON 序列化时默认忽略零值字段(如 omitempty),但签名输入必须严格固定字段集——哪怕某个字段为零值,也要显式写出,否则不同语言/版本的序列化结果可能不一致,导致验签失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











