C++导出ECDSA公钥在Go中解析后Y坐标不符问题排查
ECDSA公钥在Go中解析后Y坐标与同私钥生成公钥不一致的问题分析
我在C中实现了ECDSA函数,签名在C的验证逻辑中完全正常,但移植到Go语言后无法通过全部测试。将C++导出的33字节压缩格式公钥(十六进制字符串)用Go的elliptic.UnmarshalCompressed解析后,发现其X坐标与通过同一私钥调用ScalarBaseMult生成的公钥X坐标一致,但Y坐标完全不同。
Go测试代码
import ( "crypto/ecdsa" "crypto/elliptic" "encoding/hex" "fmt" "math/big" ) func main() { // 从C++导出的压缩公钥十六进制字符串解码 publicKeyBufferFromCplusplus, err := hex.DecodeString("02d36b0e521ca9a28cd6f2ddc56dc0973215702f6f67ed0670b9bc9a98c28d473b") if err != nil { fmt.Println("十六进制转字节失败: ", err) return } pk := new(ecdsa.PublicKey) pk.Curve = elliptic.P256() pk.X, pk.Y = elliptic.UnmarshalCompressed(elliptic.P256(), publicKeyBufferFromCplusplus) // 使用C++中的私钥在Go中生成公钥 expect_sk := new(ecdsa.PrivateKey) expect_sk.D, _ = new(big.Int).SetString("50228957095953179898827503463423289296009712707225507368245266147079499081684", 10) expect_sk.PublicKey.Curve = elliptic.P256() expect_sk.PublicKey.X, expect_sk.PublicKey.Y = expect_sk.PublicKey.Curve.ScalarBaseMult(expect_sk.D.Bytes()) expect_pk := expect_sk.PublicKey // 对比两个公钥 fmt.Printf("pk_x:\t\t%d\n", pk.X) fmt.Printf("expect pk_x:\t%d\n\n", expect_pk.X) fmt.Printf("pk_y:\t\t%d\n", pk.Y) fmt.Printf("expect pk_y:\t%d\n", expect_pk.Y) }
终端输出
pk_x: 95627162525183504786576659676808415919520991299985517290103803735976207796027 expect pk_x: 95627162525183504786576659676808415919520991299985517290103803735976207796027 pk_y: 106312815215663533204607583749797836088594130128596587441436180287153537381066 expect pk_y: 9479273994692715558089863199609737441492013286693726754097451021713560472885
问题原因分析
椭圆曲线算法中,对于给定的X坐标,存在两个满足椭圆曲线方程y² = x³ + ax + b mod p的Y坐标,这两个Y坐标互为模p的相反数(即y和p-y)。压缩格式公钥的第一个字节(02或03)用于指定Y坐标的奇偶性:
- 02表示Y为偶数
- 03表示Y为奇数
针对P256曲线,其模数p为115792089210356248762697446949407573530086143415290314195533631308867097853951。计算p - expect_pk.Y可得:
115792089210356248762697446949407573530086143415290314195533631308867097853951 - 9479273994692715558089863199609737441492013286693726754097451021713560472885 = 106312815215663533204607583749797836088594130128596587441436180287153537381066
该结果与解析得到的pk.Y完全一致,说明这两个Y坐标是互补的。问题出在C++生成公钥时,选择了与Go中私钥推导公钥相反的Y分支,导致解析后的公钥点与签名生成时使用的公钥点不匹配,从而验证失败。
解决方案
- 修正C++公钥导出逻辑:检查C++代码中生成压缩公钥时的前缀字节是否与Y坐标的奇偶性正确对应,确保导出的是私钥对应的那个Y坐标的压缩公钥。
- 在Go中调整公钥Y坐标:如果C++的公钥导出逻辑无法修改,可以在Go中解析公钥后,将Y坐标替换为其模p的补数,使其与私钥生成的公钥匹配:
p := elliptic.P256().Params().P // 计算互补Y坐标 complementY := new(big.Int).Sub(p, pk.Y) // 将解析后的公钥Y替换为互补值 pk.Y = complementY // 此时pk与expect_pk完全一致 fmt.Printf("调整后pk_y:\t%d\n", pk.Y) fmt.Printf("与expect_pk一致: %t\n", pk.Y.Cmp(expect_pk.Y) == 0)
内容的提问来源于stack exchange,提问作者Richard
相关产品推荐
相关产品推荐

