签名前必须确认三件事:数据是否已规范预处理(如JSON字段顺序、空格)、密钥格式是PKCS#1还是PKCS#8、哈希算法与SignPKCS1v15参数严格匹配(如SHA256需传crypto.SHA256而非字符串)。

签名前必须确认的三件事
Go 语言做数据签名,不是套个 crypto/rsa 或 crypto/ecdsa 就能直接用——密钥格式、哈希算法、填充方式不匹配,签名验签必然失败。最常见的是拿 PEM 解析出的私钥去调 SignPKCS1v15,结果提示 crypto: requested hash function is unavailable,其实只是没显式指定 crypto.SHA256。
- 确认你要签的数据是否已做预处理(比如 JSON 序列化后是否保留空格、字段顺序是否固定)
- 确认密钥是 PKCS#1 还是 PKCS#8 格式:RSA 私钥用
ParsePKCS1PrivateKey,而现代工具(如 OpenSSL 1.3+)默认输出 PKCS#8,得用ParsePKCS8PrivateKey - 确认签名后要不要 Base64 编码:HTTP Header 传签名一般要
base64.StdEncoding.EncodeToString,二进制协议直传则保持原始字节
用 crypto/ecdsa 签名时避免 panic 的写法
ecdsa.Sign 不返回错误,但输入私钥或哈希摘要长度不对会直接 panic。它要求摘要长度必须严格等于曲线字节长度(比如 elliptic.P256() 要 32 字节),不能靠 hash.Hash.Sum(nil) 直接喂。
- 先用
crypto/sha256.New()计算摘要,再用sum := hash.Sum(nil)获取原始字节 - 确保摘要长度匹配:对 P256,用
sha256;对 P384,必须用sha512并取前 48 字节,或直接用crypto/sha512.New384() - 调
ecdsa.Sign后,得到的r, s *big.Int需序列化为固定长度字节:推荐用ecdsa.Marshal,它按标准 ASN.1 DER 格式打包,验签端才通用
验签失败时优先检查这三处
签名成功不代表验签能过。Go 的 ecdsa.Verify 和 rsa.VerifyPKCS1v15 对输入极其敏感,一个字节差异就返回 false,且不报错。
- 确认验签用的公钥和签名时的私钥是同一对:用
pubKey.Equal(&privKey.PublicKey)快速比对 - 确认验签时重新计算的摘要和签名时完全一致:尤其注意字符串是否含 BOM、JSON 是否用了
json.Marshal(会排序键)还是json.MarshalIndent(加空格) - 确认签名字节是原始 DER 格式,不是 Base64 后又误 decode 两次——常见于前端 JS 签名后传到 Go 后端,JS 侧用
Buffer.from(sig, 'base64'),Go 侧却多调了一次base64.DecodeString
生产环境别跳过的安全细节
本地跑通不等于线上可用。Go 的标准库不自动做密钥保护或签名限频,这些得自己兜底。
立即学习“go语言免费学习笔记(深入)”;
- 私钥绝不能硬编码或从文件裸读:用
os.ReadFile读完立刻defer zeroBytes(privateKeyBytes)清零内存 - 验签前加简单限流:对高频接口,用
golang.org/x/time/rate.Limiter控制每秒最多 10 次验签,防暴力重放 - 签名结构体建议嵌入时间戳和随机 nonce:比如
struct{ Data string; Ts int64; Nonce string },避免签名被跨请求复用
真正麻烦的从来不是调哪个函数,而是签名前后数据形态是否严格一致、密钥生命周期怎么管、以及出问题时有没有足够上下文快速定位——比如在签名函数里加一行 log.Printf("signing data hash: %x", hash.Sum(nil)),比反复猜哈希算法有用得多。


















