Go crypto/ecdsa VerifyASN1 怎样验证 ASN.1 编码签名
来源:17golang原创
时间:2026-10-25 10:01:54 196浏览 收藏
Go 里用 crypto/ecdsa.VerifyASN1 验证签名时,最容易弄错的不是函数名,而是三个输入的形态:公钥必须对应签名私钥,hash 必须是双方约定的原始摘要字节,sig 必须是 ASN.1 编码的签名字节。只要其中一个被换成十六进制文本、另一份消息的摘要,或者固定宽度的 r||s,返回值就会是 false。
官方资料入口:https://pkg.go.dev/crypto/ecdsa;实现源码:https://go.dev/src/crypto/ecdsa/ecdsa.go。本文只讨论 VerifyASN1 的输入契约、签名格式和排错路径,不展开证书链验证或密钥生成。
最稳妥的配对是:同一份原始消息按同一算法计算摘要,签名端用SignASN1产生 ASN.1 字节,验签端把对应公钥、同一摘要和完整签名字节交给VerifyASN1。
先固定 VerifyASN1 的三个输入
VerifyASN1 的签名是 func VerifyASN1(pub *PublicKey, hash, sig []byte) bool。它不接收原文,也不负责替你选择哈希算法。pub 是验证用公钥,hash 是已经算好的摘要,sig 是 ASN.1 编码后的签名。三个参数都来自协议约定,而不是从签名长度临时猜出来。
| 输入 | 应该是什么 | 常见误传 |
|---|---|---|
pub | 与签名私钥匹配的 ECDSA 公钥 | 另一把钥匙或错误曲线的公钥 |
hash | 原始摘要字节,例如 SHA-256 的 32 字节结果 | 消息正文、十六进制文本、Base64 文本 |
sig | SignASN1 产生的 ASN.1 签名字节 | 两个整数直接拼接的固定宽度字节 |

因此,验签失败时先把这三个值分别记录为“来源、编码、长度”,再看密码学结论。长度相同不代表内容相同;一份错误的 32 字节摘要仍然会得到失败结果。
区分原始摘要与十六进制文本
以 SHA-256 为例,sha256.Sum256 返回的是 32 字节数组。把它格式化成十六进制后,通常得到 64 个 ASCII 字符;把字符串直接转换为 []byte,得到的是这 64 个字符的字节,不是原来的 32 字节摘要。Base64 也一样:它适合传输和展示,不能未经解码直接交给验签函数。
正确的边界是:如果协议传来原始摘要,直接使用其字节;如果协议传来十六进制或 Base64,先按协议指定的编码解码,并把解码错误返回给调用方。不要通过截断文本或反复尝试不同解码方式来“凑长度”,那会把输入契约问题变成更难定位的验签失败。
package main
import (
"crypto/sha256"
"encoding/hex"
"fmt"
)
func main() {
message := []byte("order=demo-2026")
digest := sha256.Sum256(message)
// digest[:] 是 32 字节原始摘要,适合交给 VerifyASN1。
raw := digest[:]
// hexText 只是展示或传输格式,长度通常是原始摘要的两倍。
hexText := hex.EncodeToString(raw)
decoded, err := hex.DecodeString(hexText)
if err != nil {
// 解码失败必须中止验签,不能用原文本继续尝试。
panic(err)
}
fmt.Println(len(raw), len(hexText), len(decoded))
}
这个片段只说明字节形态,未把输出当作本机运行结果。实际接入时,raw 和 decoded 才可能作为摘要输入;hexText 不应直接传入。
让 SignASN1 与 VerifyASN1 使用同一约定
签名和验签必须对同一份消息字节计算同一种摘要。JSON 字段顺序、字符编码、换行和空格只要有一个不同,两个摘要就不同。推荐把摘要算法固定在业务函数里,让调用方传入原文或已经明确标注的摘要,不要让调用方同时自由传入算法名和任意字节。
func signAndVerify(key *ecdsa.PrivateKey, payload []byte) (bool, error) {
digest := sha256.Sum256(payload)
// SignASN1 返回与 VerifyASN1 配套的 ASN.1 签名字节。
sig, err := ecdsa.SignASN1(rand.Reader, key, digest[:])
if err != nil {
return false, fmt.Errorf("生成 ASN.1 签名失败: %w", err)
}
// 验签重新计算同一消息的摘要,并使用对应公钥。
valid := ecdsa.VerifyASN1(&key.PublicKey, digest[:], sig)
return valid, nil
}
上面示例中的 ecdsa、sha256、rand 和 fmt 需要按正常 Go 文件导入。这里关注的是参数契约:SignASN1 和 VerifyASN1 共用原始摘要与 ASN.1 输出格式。示例不提供实测输出,也不把静态说明图当作运行证据。
按协议选择签名入口与输出格式
Go 同时提供 ecdsa.Sign、ecdsa.SignASN1 和 VerifyASN1。包级 Sign 返回两个大整数 r、s;SignASN1 返回 ASN.1 编码字节。协议如果规定 ASN.1 DER,就应让签名端和验签端都使用 ASN.1 入口;协议如果规定两个整数或固定宽度拼接,则需要明确的编码适配。

不能把 ASN.1 字节按“签名长度除以二”拆成两个整数,也不能只换验签函数来掩盖上游格式不一致。先确认协议写的是哪一种签名表示,再决定使用哪个 API;换 API 不会自动修复错误摘要。
用参数表缩小验签失败范围
| 现象 | 优先检查 | 修复方向 |
|---|---|---|
| 摘要长度是 SHA-256 的两倍 | 是否把十六进制文本当成字节 | 按协议解码为原始摘要 |
| 摘要长度正确但总是 false | 两端消息字节、哈希算法或公钥是否一致 | 比较序列化结果和密钥来源 |
| 签名来自其他语言库 | 对方返回 DER 还是 r、s 拼接格式 | 按协议转换,避免猜测编码 |
| 只在部分消息失败 | 是否对消息做了不一致的规范化 | 固定编码、换行和字段顺序 |
VerifyASN1 能直接接收原文吗?
不能。它接收的是摘要字节;原文需要由应用按照协议计算摘要。
为什么同一私钥两次签名的字节可能不同?
ECDSA 签名包含随机性。排错时应验证签名能否通过,而不是要求两次签名字节完全相同。
应该记录完整签名和摘要吗?
生产日志通常记录输入类型、编码、长度、算法和错误链即可;不要记录私钥,也不要无必要地打印敏感业务消息和完整摘要。
结论很简单:先统一原始消息和摘要,再统一 ASN.1 或整数签名格式,最后确认公钥确实来自对应私钥。这样 VerifyASN1 的布尔结果才有明确的排错含义。
-
502 收藏
-
502 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
400 收藏
-
212 收藏
-
301 收藏
-
191 收藏
-
436 收藏
-
374 收藏
-
485 收藏
-
434 收藏
-
349 收藏
-
177 收藏
-
416 收藏
-
333 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习