登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

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 文本
sigSignASN1 产生的 ASN.1 签名字节两个整数直接拼接的固定宽度字节
原始消息、摘要、公钥和 ASN.1 签名进入 VerifyASN1 的静态契约说明图
图1:VerifyASN1 输入契约说明图,展示公钥、原始摘要和 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 入口;协议如果规定两个整数或固定宽度拼接,则需要明确的编码适配。

SignASN1 与 VerifyASN1 的 ASN.1 域和 ecdsa.Sign 整数域静态边界结构图
图2:签名格式边界结构图,对比 ASN.1 DER 与 r、s 整数两种静态输出关系,不是运行截图或性能证据。

不能把 ASN.1 字节按“签名长度除以二”拆成两个整数,也不能只换验签函数来掩盖上游格式不一致。先确认协议写的是哪一种签名表示,再决定使用哪个 API;换 API 不会自动修复错误摘要。

用参数表缩小验签失败范围

现象优先检查修复方向
摘要长度是 SHA-256 的两倍是否把十六进制文本当成字节按协议解码为原始摘要
摘要长度正确但总是 false两端消息字节、哈希算法或公钥是否一致比较序列化结果和密钥来源
签名来自其他语言库对方返回 DER 还是 r、s 拼接格式按协议转换,避免猜测编码
只在部分消息失败是否对消息做了不一致的规范化固定编码、换行和字段顺序

VerifyASN1 能直接接收原文吗?

不能。它接收的是摘要字节;原文需要由应用按照协议计算摘要。

为什么同一私钥两次签名的字节可能不同?

ECDSA 签名包含随机性。排错时应验证签名能否通过,而不是要求两次签名字节完全相同。

应该记录完整签名和摘要吗?

生产日志通常记录输入类型、编码、长度、算法和错误链即可;不要记录私钥,也不要无必要地打印敏感业务消息和完整摘要。

结论很简单:先统一原始消息和摘要,再统一 ASN.1 或整数签名格式,最后确认公钥确实来自对应私钥。这样 VerifyASN1 的布尔结果才有明确的排错含义。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>