跳到主要内容
ecdsa.com
Go

Go crypto/ecdsa (standard library)

Go 标准库中的签名相关包——crypto/ecdsa 及 crypto/x509、ed25519、mlkem 和 mldsa:对你提供的摘要生成 DER 签名,nonce 随机化且忽略你传入的 Reader,仅支持 NIST 曲线。
许可证
BSD-3-Clause
检查的版本
1.27.3 ·
源代码
github.com/golang/go
文档
pkg.go.dev/crypto/ecdsa

兼容性概览

ECDSAEdDSA后量子

ECDSA 曲线
P-224P-256P-384P-521
EdDSA
Ed25519Ed25519phEd25519ctx
后量子算法
ML-KEMML-DSA
公钥格式
spki-derx509sec1-uncompressed
私钥格式
pkcs8-dersec1-derraw-scalar

Go 把密码学功能放在标准库中,从 Go 1.24 起,核心原语位于为 FIPS 140-3 构建的 Go 密码模块中。crypto/ecdsa 负责签名和验证,crypto/x509 读取密钥和证书;除 crypto/ed25519 外,Go 1.27.3 还加入了 crypto/mlkem(ML-KEM-768/1024)和 crypto/mldsa(ML-DSA-44/65/87)。标准库中没有 secp256k1:以太坊和比特币代码使用 go-ethereum 或 decred 的包。

crypto/ecdsa 中的所有操作都针对你计算好的摘要——这个包从不计算哈希。比生成错误的签名更糟的是,过长的输入会被静默截断到曲线长度:对 46 字节输入生成的签名,对另一个前 32 字节相同的输入也验证通过。务必像示例那样先计算哈希。SignASN1、VerifyASN1 和 crypto.Signer 接口使用 ASN.1 DER;WebCrypto、JWS 等使用原始 r‖s 的对端需要用 Sign/Verify,并用 FillBytes 填充整数——Bytes() 会丢掉前导零字节,因此凡是恰好以零字节开头的 r 或 s,输出都会少一个字节。

签名是随机化的,而且从 Go 1.26 起,除非设置了 GODEBUG=cryptocustomrand=1,否则你传入的 io.Reader 会被忽略:用全零 Reader 生成的两个签名并不相同。依赖固定 Reader 获得可复现签名的测试因此失效;文档给出的替代方案是 testing/cryptotest.SetGlobalRandom。s 没有经过规范化,VerifyASN1 对处于高半区和低半区的 s 都接受,因此面向严格要求 low-S 的验证方时,需要把签名中的 s 替换为 n − s。

密钥解析优先面向 DER。把 PEM 文本传给 x509.ParsePKIXPublicKey 会报错 asn1: structure error: tags don't match——应先调用 pem.Decode。ecdsa.ParseUncompressedPublicKey 和 ecdsa.ParseRawPrivateKey 读取裸 SEC1 点和标量;压缩点会被拒绝,也没有读取口令加密 PKCS#8 的函数。格式错误的签名会让 VerifyASN1 返回 false,而绝不会返回错误。

要将 Go 签名与其他技术栈比较,可以先在 DER ⇄ 原始格式转换器中转换,再在签名验证工具中检查;证书解析问题在证书解码器中最容易看出来。

事实及其依据的检查

以下内容均非抄自文档。检查脚本 content/libraries/verify/go-crypto-ecdsa/run.sh 于2026年10月20日在容器镜像 golang:1.27.3-trixie 中运行了 Go crypto/ecdsa (standard library) 1.27.3;本页内容必须与它的输出一致,每条事实都列出了对应的输出行。

  • ECDSA 签名编码

    der

    SignASN1、VerifyASN1 和 PrivateKey.Sign(即 crypto.Signer 路径)使用 ASN.1 DER;Sign 和 Verify 以 *big.Int 表示 r 和 s。

    检查记录
    fact ecdsa-signature-encoding = der
  • ECDSA 的 nonce

    random

    随机化:同一摘要的两个签名不同,与包文档的说明一致(熵与密钥和消息混合)。

    检查记录
    fact ecdsa-nonce = random
    evidence ecdsa-nonce: two signatures with an all-zero Reader are identical: false
  • 随机源参数

    ignored

    从 Go 1.26 起,io.Reader 参数会被忽略:用全零 Reader 生成的两个签名不同。目前可以用 GODEBUG=cryptocustomrand=1 恢复旧行为。

    检查记录
    fact go-rand-reader-argument = ignored
  • 签名时的 low-S

    either

    s 没有经过规范化:64 个签名中出现了 high-S 值。

    检查记录
    fact ecdsa-low-s-sign = either
  • 验证时的 high-S

    accepts-high-s

    VerifyASN1 接受了把 s 替换为 n − s 的签名。

    检查记录
    fact ecdsa-low-s-verify = accepts-high-s
  • 签名调用对哪些内容做哈希

    takes-digest

    所有函数都接收摘要;crypto/ecdsa 中没有任何函数会对消息计算哈希。

    检查记录
    fact ecdsa-prehash = takes-digest
  • 摘要长于曲线的阶

    truncated

    长度超过曲线阶的输入会被截断:对 46 字节输入生成的签名,对另一个前 32 字节相同的输入也验证通过。

    检查记录
    fact ecdsa-long-input = truncated
    evidence ecdsa-long-input: a signature over a 46-byte input verified for a different input with the same first 32 bytes: true
  • 格式错误的 ECDSA 签名

    returns-false

    对于原始 r‖s 签名和 3 字节的数据块,VerifyASN1 都返回 false。

    检查记录
    fact ecdsa-malformed-signature = returns-false
  • ECDSA 曲线

    P-224P-256P-384P-521

    P-224、P-256、P-384 和 P-521;crypto/elliptic 不提供 secp256k1 或 Brainpool 曲线。

    检查记录
    fact ecdsa-curves = P-224, P-256, P-384, P-521
    evidence ecdsa-curves: crypto/elliptic exposes P224, P256, P384 and P521 only
  • EdDSA

    Ed25519Ed25519phEd25519ctx

    Ed25519,以及通过 ed25519.Options 支持的 RFC 8032 变体 Ed25519ph 和 Ed25519ctx。

    检查记录
    fact eddsa = Ed25519, Ed25519ph, Ed25519ctx
  • Ed25519 私钥布局

    expanded-64

    ed25519.PrivateKey 为 64 字节,即种子后接公钥;NewKeyFromSeed 从 32 字节的种子构造它。

    检查记录
    fact ed25519-private-key = expanded-64
  • 后量子算法

    ML-KEMML-DSA

    在 Go 1.27.3 中,ML-KEM-768 和 ML-KEM-1024 的封装与解封装均成功,ML-DSA-44/65/87 的签名与验证也均成功。

    检查记录
    fact pqc = ML-KEM, ML-DSA
    evidence pqc: crypto/mlkem offers ML-KEM-768 and ML-KEM-1024; crypto/mldsa offers ML-DSA-44, ML-DSA-65 and ML-DSA-87
  • 公钥格式

    spki-derx509sec1-uncompressed

    通过 x509.ParsePKIXPublicKey 读取的 SPKI DER、证书,以及通过 ecdsa.ParseUncompressedPublicKey 读取的 SEC1 未压缩点;PEM 需要先经过 pem.Decode,压缩点会被拒绝。

    检查记录
    fact public-key-formats = spki-der, x509, sec1-uncompressed
    evidence public-key-formats: ParsePKIXPublicKey on PEM text: asn1: structure error: tags don't match (16 vs {class:0 tag:13 length:45 isCompound:true}) {optional:false explicit:false application:false private:false defaultValue:<nil> tag:<nil> stringType:0 timeType:0 set:false omitEmpty:false} publicKeyInfo @2
    evidence public-key-formats: ParseUncompressedPublicKey on a compressed point: ecdsa: invalid uncompressed public key
    evidence public-key-formats: ecdh.P256().NewPublicKey on a compressed point: crypto/ecdh: invalid public key
  • 私钥格式

    pkcs8-dersec1-derraw-scalar

    PKCS#8 DER、SEC1 DER,以及通过 ecdsa.ParseRawPrivateKey 读取的原始标量;没有解析口令加密 PKCS#8 的函数。

    检查记录
    fact private-key-formats = pkcs8-der, sec1-der, raw-scalar
    evidence private-key-formats: no parser for passphrase-encrypted PKCS#8 in the standard library

用法

先计算哈希,为 OpenSSL 风格的对端生成 DER 签名,再为 WebCrypto 和 JOSE 构造正确填充的原始 r‖s。
package main

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

func main() {
	key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
	// crypto/ecdsa never hashes: pass a digest, never the message itself.
	digest := sha256.Sum256([]byte("order #1042 approved"))

	der, _ := ecdsa.SignASN1(rand.Reader, key, digest[:]) // ASN.1 DER for OpenSSL, Java, .NET peers
	fmt.Println("DER verified:", ecdsa.VerifyASN1(&key.PublicKey, digest[:], der))

	// Raw r||s for WebCrypto and JOSE: FillBytes pads to 32 bytes; Bytes() would not.
	r, s, _ := ecdsa.Sign(rand.Reader, key, digest[:])
	raw := make([]byte, 64)
	r.FillBytes(raw[:32])
	s.FillBytes(raw[32:])
	ok := ecdsa.Verify(&key.PublicKey, digest[:], new(big.Int).SetBytes(raw[:32]), new(big.Int).SetBytes(raw[32:]))
	fmt.Println("raw verified:", ok, len(raw))
}

代码片段测试工具会在容器镜像 golang:1.27.3-trixie 中原样运行每个示例,只要输出不匹配,运行就会失败。

命令
go run .
输出必须匹配(正则表达式)
DER verified: true\nraw verified: true 64

库