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

Go crypto/ecdsa 怎样处理 Sign 对摘要长度的检查

来源:17golang原创

时间:2026-10-24 11:11:05 319浏览 收藏

调用 Go 的 crypto/ecdsa 签名时,最容易误解的一点是:Sign 并不是把摘要字节数强制要求成“必须等于曲线阶的字节数”。官方文档明确说明,摘要超过私钥曲线阶的位长度时,会被截断;PrivateKey.Sign 最终返回 ASN.1 编码签名。

官方文档:https://pkg.go.dev/crypto/ecdsa@go1.27.3#PrivateKey.Sign

因此,排查“Sign 对摘要长度的检查”时要先分清两层策略:库层面允许过长摘要按规则截断,业务层面是否允许这种隐式处理则由你决定。下面用一个小型签名器把这个边界固定下来。

先把摘要长度的结论固定下来

ECDSA 的输入不是原文,而是原文经过哈希得到的摘要。以 P-256 为例,曲线阶 N 的有效位长度是 256 位。SHA-256 产生 256 位摘要,长度正好对齐;SHA-512 产生 512 位摘要,交给 ecdsa.Sign 或 PrivateKey.Sign 时,超过曲线阶位长度的部分会按照实现规则被截断。

这意味着“摘要太长”通常不会直接变成 Sign 的错误。若协议要求摘要算法必须与曲线边界严格匹配,就要在进入签名函数前自己拒绝;若协议遵循 ECDSA 的摘要到整数规则,则可以接受库的截断语义。

原文、SHA-256摘要、SHA-512摘要与P-256曲线阶位长度比较的静态结构说明图
图1:摘要长度与曲线阶位数的静态关系说明图,不是运行截图。

先记住三个判断:

  • 等长:SHA-256 与 P-256 是常见的直观组合,摘要和曲线阶位长度一致。
  • 过长:摘要位数超过曲线阶位数时,标准库会截断,而不是自动换曲线。
  • 短摘要:库文档没有要求把每个摘要补到曲线阶长度;但业务协议仍应明确允许哪些哈希算法。

准备一个摘要长度策略

小工具的第一步不是直接调用 Sign,而是把“是否接受过长摘要”写成可见的策略。下面的示例选择严格模式:摘要长度不是对应哈希函数的标准长度,或者超过曲线阶位长度,就返回错误。这样可以避免调用方误以为自己签的是完整 SHA-512 摘要。

package main

import (
	"crypto"
	"crypto/ecdsa"
	"crypto/elliptic"
	"crypto/rand"
	"crypto/sha256"
	"fmt"
)

// signDigest 只接受与声明的哈希函数等长、且不超过曲线阶位长度的摘要。
func signDigest(priv *ecdsa.PrivateKey, digest []byte, hashFunc crypto.Hash) ([]byte, error) {
	if priv == nil || priv.Curve == nil {
		return nil, fmt.Errorf("私钥或曲线为空")
	}
	if hashFunc == 0 || !hashFunc.Available() {
		return nil, fmt.Errorf("哈希函数不可用")
	}

	orderBits := priv.Curve.Params().N.BitLen()
	if len(digest)*8 > orderBits {
		return nil, fmt.Errorf("摘要过长:%d 位,大于曲线阶的 %d 位", len(digest)*8, orderBits)
	}
	if len(digest) != hashFunc.Size() {
		return nil, fmt.Errorf("摘要长度为 %d 字节,但声明的哈希函数要求 %d 字节", len(digest), hashFunc.Size())
	}

	// 传入非 nil 的随机源,保留普通随机签名语义;签名结果是 ASN.1 编码。
	return priv.Sign(rand.Reader, digest, hashFunc)
}

func main() {
	// 生成 P-256 私钥,实际项目应妥善保存或从受保护的密钥来源加载。
	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	if err != nil {
		panic(err)
	}

	message := []byte("order-aware signing")
	digest := sha256.Sum256(message)
	signature, err := signDigest(priv, digest[:], crypto.SHA256)
	if err != nil {
		panic(err)
	}

	// 验收时使用同一份摘要和公钥,确认签名编码可以被解析和验证。
	ok := ecdsa.VerifyASN1(&priv.PublicKey, digest[:], signature)
	fmt.Println("signature verified:", ok)
}

这个策略函数做了两件容易被忽略的事。第一,orderBits 取的是曲线阶 N 的位长度,不是公钥坐标的字节长度。第二,hashFunc.Size() 用来保证摘要确实由声明的哈希函数产生,避免把一段随意的字节串伪装成规范摘要。

把 Sign 放进小型签名器

现在把流程串起来:输入消息先生成摘要,策略函数检查摘要,再调用 PrivateKey.Sign,最后用 VerifyASN1 验收。签名函数只负责签名,不应同时猜测调用方到底使用了哪一种哈希算法。

摘要策略、crypto.Hash、ecdsa.PrivateKey、PrivateKey.Sign、ASN.1签名和VerifyASN1之间的静态结构说明图
图2:小型签名器的输入、签名与验收关系说明图,不是运行截图。

如果代码需要实现 crypto.Signer,crypto.Hash 可以作为 crypto.SignerOpts 传给 Sign。这里的 opts.HashFunc() 应该与生成 digest 时使用的算法一致,否则确定性签名场景下会违反方法的输入约定。

另一个边界是随机源:官方文档说明,random 非空时使用随机签名语义;从 Go 1.26 起,安全随机源由实现控制,传入的 reader 通常会被忽略,除非通过 GODEBUG=cryptocustomrand=1 选择旧兼容行为。若 random 为 nil,方法进入 RFC 6979 确定性签名路径,并要求 opts.HashFunc() 与摘要生成函数一致。

接入和验收边界

把这个小工具接入接口或证书流程时,可以按下面的顺序确认:

检查点应该确认什么常见误区
摘要来源原文先哈希,传入的是摘要字节把原文直接传给 Sign
曲线边界用 priv.Curve.Params().N.BitLen() 判断位长度用 P-256 名称推断所有曲线都一样
过长摘要选择接受 ECDSA 截断,或在包装层显式拒绝以为 Sign 一定返回长度错误
验收使用同一摘要、公钥和 VerifyASN1拿原文或另一种哈希结果验证

如果只是需要常规 ASN.1 签名,也可以直接使用 ecdsa.SignASN1;如果需要通过通用接口调用私钥,使用 PrivateKey.Sign 更适合表达 crypto.Signer 语义。两种入口都共享“过长摘要按曲线阶位数截断”的核心边界。

最终,标题里的“检查”不应理解成标准库会替你执行一个严格的等长断言。正确做法是:先决定协议要不要允许截断,再把这个决定写进包装层,最后用同一摘要完成验证。

常见问题

SHA-512 配 P-256 一定签名失败吗?

不一定。直接调用 Sign 时,过长摘要会按曲线阶位长度截断;是否接受这种语义取决于协议。若你的接口要求“摘要算法和曲线严格匹配”,应像示例那样在包装层提前报错。

为什么示例还要传 crypto.SHA256?

它让签名器知道摘要来自哪个哈希函数,并满足 crypto.SignerOpts 的约定。尤其在 random 为 nil 的确定性签名路径中,声明的哈希函数必须和实际生成摘要的函数一致。

验证失败时先查什么?

先确认验证使用的摘要字节与签名时完全相同,再确认公钥来自同一私钥,最后检查 ASN.1 签名没有被字符串编码或传输层改写。摘要长度本身只是一层输入边界,不是全部排查项。

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