Files
encryptx/ecdsax/ecdsax_test.go
T
2026-09-30 15:49:12 +08:00

778 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package ecdsax_test
import (
"bytes"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"math/big"
mrand "math/rand"
"os"
"path/filepath"
"runtime"
"testing"
"code.yun.ink/pkg/encryptx/ecdsax"
)
// curveCase 描述一条受支持的曲线,便于表驱动覆盖 P-256/P-384/P-521。
type curveCase struct {
name string
curve elliptic.Curve
}
func supportedCurves() []curveCase {
return []curveCase{
{"P-256", elliptic.P256()},
{"P-384", elliptic.P384()},
{"P-521", elliptic.P521()},
}
}
// requireError 断言操作失败返回了 error(而非 panic 或静默成功)。
func requireError(t *testing.T, name string, err error) {
t.Helper()
if err == nil {
t.Errorf("%s: 期望返回错误, 实际返回 nil", name)
}
}
// mustGenerateKey 生成指定曲线的密钥,供其余用例复用。
func mustGenerateKey(t *testing.T, curve elliptic.Curve) *ecdsa.PrivateKey {
t.Helper()
priv, err := ecdsax.GenerateKey(curve)
if err != nil {
t.Fatalf("GenerateKey 返回错误: %v", err)
}
if priv == nil {
t.Fatal("GenerateKey 返回 nil 私钥且无错误")
}
if priv.Curve != curve {
t.Fatalf("生成的私钥曲线 = %v, 期望 %v", priv.Curve, curve)
}
if priv.D == nil || priv.X == nil || priv.Y == nil {
t.Fatal("生成的私钥缺少 D 或 X/Y 字段")
}
return priv
}
// TestGenerateKeyRejectsUnsupportedCurves 覆盖:nil 与 P-224 必须被拒绝。
func TestGenerateKeyRejectsUnsupportedCurves(t *testing.T) {
cases := []struct {
name string
curve elliptic.Curve
}{
{"nil 曲线", nil},
{"P-224 曲线", elliptic.P224()},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
priv, err := ecdsax.GenerateKey(tc.curve)
requireError(t, "GenerateKey("+tc.name+")", err)
if priv != nil {
t.Error("GenerateKey 失败时应返回 nil 私钥")
}
privatePEM, publicPEM, err := ecdsax.GenerateKeyPEM(tc.curve)
requireError(t, "GenerateKeyPEM("+tc.name+")", err)
if privatePEM != nil || publicPEM != nil {
t.Error("GenerateKeyPEM 失败时应返回 nil PEM")
}
dir := t.TempDir()
requireError(t, "SaveKeyPair("+tc.name+")", ecdsax.SaveKeyPair(
filepath.Join(dir, "priv.pem"), filepath.Join(dir, "pub.pem"), tc.curve))
})
}
}
// TestSignVerifyRoundTrip 覆盖:P-256/P-384/P-521 三条曲线均能完成签名/验签往返。
func TestSignVerifyRoundTrip(t *testing.T) {
for _, tc := range supportedCurves() {
t.Run(tc.name, func(t *testing.T) {
priv := mustGenerateKey(t, tc.curve)
pub := &priv.PublicKey
message := []byte("encryptx ecdsax 往返测试消息 / " + tc.name)
signature, err := ecdsax.Sign(priv, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
if len(signature) == 0 {
t.Fatal("Sign 返回空签名")
}
if err := ecdsax.Verify(pub, message, signature); err != nil {
t.Fatalf("Verify 应通过, 实际返回错误: %v", err)
}
// ECDSA 每次签名使用新的随机 nonce,两次结果都应能通过校验。
second, err := ecdsax.Sign(priv, message)
if err != nil {
t.Fatalf("第二次 Sign 返回错误: %v", err)
}
if err := ecdsax.Verify(pub, message, second); err != nil {
t.Fatalf("第二次签名的 Verify 应通过, 实际返回错误: %v", err)
}
if bytes.Equal(signature, second) {
t.Log("提示: 两次 ECDSA 签名完全相同, 概率极低, 请确认随机源正常")
}
// 空消息与较长消息同样应能往返。
for _, msg := range [][]byte{nil, {}, bytes.Repeat([]byte("B"), 1<<16)} {
sig, err := ecdsax.Sign(priv, msg)
if err != nil {
t.Fatalf("Sign(len=%d) 返回错误: %v", len(msg), err)
}
if err := ecdsax.Verify(pub, msg, sig); err != nil {
t.Fatalf("Verify(len=%d) 应通过, 实际返回错误: %v", len(msg), err)
}
}
})
}
}
// TestPEMRoundTrip 覆盖:GenerateKeyPEM -> 解析 -> 用公钥验签成功,且曲线保持不变。
func TestPEMRoundTrip(t *testing.T) {
for _, tc := range supportedCurves() {
t.Run(tc.name, func(t *testing.T) {
privatePEM, publicPEM, err := ecdsax.GenerateKeyPEM(tc.curve)
if err != nil {
t.Fatalf("GenerateKeyPEM 返回错误: %v", err)
}
privateBlock, _ := pem.Decode(privatePEM)
if privateBlock == nil {
t.Fatal("私钥 PEM 无法解码")
}
if privateBlock.Type != "PRIVATE KEY" {
t.Errorf("私钥 PEM 块类型 = %q, 期望 %q", privateBlock.Type, "PRIVATE KEY")
}
publicBlock, _ := pem.Decode(publicPEM)
if publicBlock == nil {
t.Fatal("公钥 PEM 无法解码")
}
if publicBlock.Type != "PUBLIC KEY" {
t.Errorf("公钥 PEM 块类型 = %q, 期望 %q", publicBlock.Type, "PUBLIC KEY")
}
priv, err := ecdsax.ParsePrivateKeyPEM(privatePEM)
if err != nil {
t.Fatalf("ParsePrivateKeyPEM 返回错误: %v", err)
}
pub, err := ecdsax.ParsePublicKeyPEM(publicPEM)
if err != nil {
t.Fatalf("ParsePublicKeyPEM 返回错误: %v", err)
}
if priv.Curve != tc.curve {
t.Errorf("解析出的私钥曲线 = %v, 期望 %v", priv.Curve, tc.curve)
}
if pub.Curve != tc.curve {
t.Errorf("解析出的公钥曲线 = %v, 期望 %v", pub.Curve, tc.curve)
}
if !priv.PublicKey.Equal(pub) {
t.Error("私钥内嵌公钥与解析出的公钥不一致")
}
message := []byte("PEM 往返后验签 / " + tc.name)
signature, err := ecdsax.Sign(priv, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
if err := ecdsax.Verify(pub, message, signature); err != nil {
t.Fatalf("Verify 应通过, 实际返回错误: %v", err)
}
})
}
}
// TestMarshalParsePEMRoundTrip 覆盖:Marshal -> Parse 后私钥标量与公钥坐标完全一致。
func TestMarshalParsePEMRoundTrip(t *testing.T) {
for _, tc := range supportedCurves() {
t.Run(tc.name, func(t *testing.T) {
priv := mustGenerateKey(t, tc.curve)
privatePEM, err := ecdsax.MarshalPrivateKeyPEM(priv)
if err != nil {
t.Fatalf("MarshalPrivateKeyPEM 返回错误: %v", err)
}
parsedPriv, err := ecdsax.ParsePrivateKeyPEM(privatePEM)
if err != nil {
t.Fatalf("ParsePrivateKeyPEM 返回错误: %v", err)
}
if parsedPriv.D.Cmp(priv.D) != 0 {
t.Error("私钥 PEM 往返后 D 不一致")
}
if !parsedPriv.PublicKey.Equal(&priv.PublicKey) {
t.Error("私钥 PEM 往返后公钥不一致")
}
publicPEM, err := ecdsax.MarshalPublicKeyPEM(&priv.PublicKey)
if err != nil {
t.Fatalf("MarshalPublicKeyPEM 返回错误: %v", err)
}
parsedPub, err := ecdsax.ParsePublicKeyPEM(publicPEM)
if err != nil {
t.Fatalf("ParsePublicKeyPEM 返回错误: %v", err)
}
if !parsedPub.Equal(&priv.PublicKey) {
t.Error("公钥 PEM 往返后不一致")
}
})
}
}
// TestBase64RoundTrip 覆盖:Base64 便利函数的正常往返。
func TestBase64RoundTrip(t *testing.T) {
for _, tc := range supportedCurves() {
t.Run(tc.name, func(t *testing.T) {
priv := mustGenerateKey(t, tc.curve)
message := []byte("Base64 签名 / " + tc.name)
encoded, err := ecdsax.SignToBase64(priv, message)
if err != nil {
t.Fatalf("SignToBase64 返回错误: %v", err)
}
if err := ecdsax.VerifyFromBase64(&priv.PublicKey, message, encoded); err != nil {
t.Fatalf("VerifyFromBase64 应通过, 实际返回错误: %v", err)
}
raw, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
t.Fatalf("Base64 解码失败: %v", err)
}
if len(raw) == 0 {
t.Fatal("Base64 解码后签名为空")
}
})
}
}
// TestTamperedMessageFails 覆盖:篡改消息后验签必须失败。
func TestTamperedMessageFails(t *testing.T) {
for _, tc := range supportedCurves() {
t.Run(tc.name, func(t *testing.T) {
priv := mustGenerateKey(t, tc.curve)
pub := &priv.PublicKey
message := []byte("原始消息 payload")
signature, err := ecdsax.Sign(priv, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
tampered := [][]byte{
[]byte("原始消息 payloa"), // 少一个字节
[]byte("原始消息 payload "), // 多一个空格
[]byte("篡改消息 payload"), // 同长度改写
bytes.Repeat([]byte{0x00}, len(message)),
}
for _, msg := range tampered {
if bytes.Equal(msg, message) {
t.Fatal("用例构造错误: 篡改消息与原始消息相同")
}
requireError(t, "篡改消息后 Verify", ecdsax.Verify(pub, msg, signature))
requireError(t, "篡改消息后 VerifyFromBase64",
ecdsax.VerifyFromBase64(pub, msg, base64.StdEncoding.EncodeToString(signature)))
}
// 空消息的签名不能通过非空消息的校验。
emptySignature, err := ecdsax.Sign(priv, nil)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
requireError(t, "空消息签名用于非空消息", ecdsax.Verify(pub, message, emptySignature))
})
}
}
// TestTamperedSignatureFails 覆盖:翻转签名字节、截断、全零签名后验签必须失败。
func TestTamperedSignatureFails(t *testing.T) {
for _, tc := range supportedCurves() {
t.Run(tc.name, func(t *testing.T) {
priv := mustGenerateKey(t, tc.curve)
pub := &priv.PublicKey
message := []byte("签名篡改测试")
signature, err := ecdsax.Sign(priv, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
for _, pos := range []int{0, 1, len(signature) / 2, len(signature) - 1} {
flipped := append([]byte{}, signature...)
flipped[pos] ^= 0x01
if bytes.Equal(flipped, signature) {
t.Fatalf("位置 %d 翻转后签名未变化", pos)
}
requireError(t, "翻转签名字节后 Verify", ecdsax.Verify(pub, message, flipped))
}
requireError(t, "截断签名", ecdsax.Verify(pub, message, signature[:len(signature)-1]))
requireError(t, "全零签名", ecdsax.Verify(pub, message, make([]byte, len(signature))))
requireError(t, "全 0xFF 签名", ecdsax.Verify(pub, message, bytes.Repeat([]byte{0xFF}, len(signature))))
// 合法签名的 Base64 文本被破坏后必须失败。
encoded := []byte(base64.StdEncoding.EncodeToString(signature))
if encoded[0] == 'A' {
encoded[0] = 'B'
} else {
encoded[0] = 'A'
}
requireError(t, "Base64 破坏后 VerifyFromBase64",
ecdsax.VerifyFromBase64(pub, message, string(encoded)))
})
}
}
// TestWrongPublicKeyFails 覆盖:用另一对密钥的公钥验签必须失败。
func TestWrongPublicKeyFails(t *testing.T) {
for _, tc := range supportedCurves() {
t.Run(tc.name, func(t *testing.T) {
privA := mustGenerateKey(t, tc.curve)
privB := mustGenerateKey(t, tc.curve)
if privA.PublicKey.Equal(&privB.PublicKey) {
t.Fatal("两次生成的密钥不应相同")
}
message := []byte("错误公钥测试 / " + tc.name)
signature, err := ecdsax.Sign(privA, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
if err := ecdsax.Verify(&privA.PublicKey, message, signature); err != nil {
t.Fatalf("使用正确公钥应当通过, 实际返回错误: %v", err)
}
requireError(t, "使用错误公钥 Verify", ecdsax.Verify(&privB.PublicKey, message, signature))
requireError(t, "使用错误公钥 VerifyFromBase64",
ecdsax.VerifyFromBase64(&privB.PublicKey, message, base64.StdEncoding.EncodeToString(signature)))
})
}
}
// TestCrossCurveVerifyFails 覆盖:跨曲线的公钥不能验证另一条曲线产生的签名。
func TestCrossCurveVerifyFails(t *testing.T) {
signer := mustGenerateKey(t, elliptic.P256())
message := []byte("跨曲线验签测试")
signature, err := ecdsax.Sign(signer, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
for _, tc := range supportedCurves() {
if tc.curve == elliptic.P256() {
continue
}
t.Run(tc.name, func(t *testing.T) {
other := mustGenerateKey(t, tc.curve)
requireError(t, "跨曲线 Verify", ecdsax.Verify(&other.PublicKey, message, signature))
})
}
}
// TestSEC1PrivateKeyPEM 覆盖:兼容 OpenSSL 的 SEC1("EC PRIVATE KEY")私钥解析。
func TestSEC1PrivateKeyPEM(t *testing.T) {
priv := mustGenerateKey(t, elliptic.P256())
der, err := x509.MarshalECPrivateKey(priv)
if err != nil {
t.Fatalf("MarshalECPrivateKey 返回错误: %v", err)
}
sec1PEM := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: der})
parsed, err := ecdsax.ParsePrivateKeyPEM(sec1PEM)
if err != nil {
t.Fatalf("ParsePrivateKeyPEM(SEC1) 返回错误: %v", err)
}
if parsed.D.Cmp(priv.D) != 0 {
t.Error("SEC1 私钥往返后 D 不一致")
}
message := []byte("SEC1 私钥签名")
signature, err := ecdsax.Sign(parsed, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
if err := ecdsax.Verify(&priv.PublicKey, message, signature); err != nil {
t.Fatalf("Verify 应通过, 实际返回错误: %v", err)
}
}
// TestInvalidPEMInputs 覆盖:空 PEM、垃圾字节、其他算法、P-224 与错误块类型都必须返回 error。
func TestInvalidPEMInputs(t *testing.T) {
priv := mustGenerateKey(t, elliptic.P256())
validPrivatePEM, err := ecdsax.MarshalPrivateKeyPEM(priv)
if err != nil {
t.Fatalf("MarshalPrivateKeyPEM 返回错误: %v", err)
}
validPublicPEM, err := ecdsax.MarshalPublicKeyPEM(&priv.PublicKey)
if err != nil {
t.Fatalf("MarshalPublicKeyPEM 返回错误: %v", err)
}
privateBlock, _ := pem.Decode(validPrivatePEM)
publicBlock, _ := pem.Decode(validPublicPEM)
if privateBlock == nil || publicBlock == nil {
t.Fatal("自产 PEM 无法解码, 测试前提不成立")
}
// 其他算法:Ed25519 与 RSA 的 PKCS#8 私钥、PKIX 公钥。
edPub, edPriv, err := ed25519.GenerateKey(rand.Reader)
if err != nil {
t.Fatalf("生成 Ed25519 测试密钥失败: %v", err)
}
edPrivateDER, err := x509.MarshalPKCS8PrivateKey(edPriv)
if err != nil {
t.Fatalf("编码 Ed25519 私钥失败: %v", err)
}
edPublicDER, err := x509.MarshalPKIXPublicKey(edPub)
if err != nil {
t.Fatalf("编码 Ed25519 公钥失败: %v", err)
}
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
if err != nil {
t.Fatalf("生成 RSA 测试密钥失败: %v", err)
}
rsaPrivateDER, err := x509.MarshalPKCS8PrivateKey(rsaKey)
if err != nil {
t.Fatalf("编码 RSA 私钥失败: %v", err)
}
rsaPublicDER, err := x509.MarshalPKIXPublicKey(&rsaKey.PublicKey)
if err != nil {
t.Fatalf("编码 RSA 公钥失败: %v", err)
}
// P-224 密钥:标准库仍能解析,但本包必须因曲线不受支持而拒绝。
p224Priv, err := ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
if err != nil {
t.Fatalf("生成 P-224 测试密钥失败: %v", err)
}
p224PrivateDER, err := x509.MarshalPKCS8PrivateKey(p224Priv)
if err != nil {
t.Fatalf("编码 P-224 私钥失败: %v", err)
}
p224PublicDER, err := x509.MarshalPKIXPublicKey(&p224Priv.PublicKey)
if err != nil {
t.Fatalf("编码 P-224 公钥失败: %v", err)
}
cases := []struct {
name string
fn func() error
}{
{"空 PEM 私钥(nil)", func() error { _, err := ecdsax.ParsePrivateKeyPEM(nil); return err }},
{"空 PEM 私钥(空切片)", func() error { _, err := ecdsax.ParsePrivateKeyPEM([]byte{}); return err }},
{"空 PEM 私钥(仅空白)", func() error { _, err := ecdsax.ParsePrivateKeyPEM([]byte(" \n\t")); return err }},
{"垃圾字节私钥", func() error { _, err := ecdsax.ParsePrivateKeyPEM([]byte("这不是 PEM 数据")); return err }},
{"随机字节私钥", func() error {
_, err := ecdsax.ParsePrivateKeyPEM(bytes.Repeat([]byte{0xCD}, 256))
return err
}},
{"无 PEM 头尾的裸 DER 私钥", func() error { _, err := ecdsax.ParsePrivateKeyPEM(privateBlock.Bytes); return err }},
{"块类型为 CERTIFICATE 的私钥", func() error {
_, err := ecdsax.ParsePrivateKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: privateBlock.Bytes}))
return err
}},
{"块内容为垃圾的 PKCS#8 私钥", func() error {
_, err := ecdsax.ParsePrivateKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: []byte("garbage")}))
return err
}},
{"块内容为垃圾的 SEC1 私钥", func() error {
_, err := ecdsax.ParsePrivateKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: []byte("garbage")}))
return err
}},
{"Ed25519 私钥 PEM", func() error {
_, err := ecdsax.ParsePrivateKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: edPrivateDER}))
return err
}},
{"RSA 私钥 PEM", func() error {
_, err := ecdsax.ParsePrivateKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: rsaPrivateDER}))
return err
}},
{"P-224 私钥 PEM", func() error {
_, err := ecdsax.ParsePrivateKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: p224PrivateDER}))
return err
}},
{"公钥 PEM 传给 ParsePrivateKeyPEM", func() error { _, err := ecdsax.ParsePrivateKeyPEM(validPublicPEM); return err }},
{"空 PEM 公钥(nil)", func() error { _, err := ecdsax.ParsePublicKeyPEM(nil); return err }},
{"垃圾字节公钥", func() error {
_, err := ecdsax.ParsePublicKeyPEM([]byte("-----BEGIN PUBLIC KEY-----\n不是Base64\n-----END PUBLIC KEY-----"))
return err
}},
{"块类型为 PRIVATE KEY 的公钥", func() error {
_, err := ecdsax.ParsePublicKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: publicBlock.Bytes}))
return err
}},
{"块内容为垃圾的 PKIX 公钥", func() error {
_, err := ecdsax.ParsePublicKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: []byte("garbage")}))
return err
}},
{"Ed25519 公钥 PEM", func() error {
_, err := ecdsax.ParsePublicKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: edPublicDER}))
return err
}},
{"RSA 公钥 PEM", func() error {
_, err := ecdsax.ParsePublicKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: rsaPublicDER}))
return err
}},
{"P-224 公钥 PEM", func() error {
_, err := ecdsax.ParsePublicKeyPEM(pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: p224PublicDER}))
return err
}},
{"私钥 PEM 传给 ParsePublicKeyPEM", func() error { _, err := ecdsax.ParsePublicKeyPEM(validPrivatePEM); return err }},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
requireError(t, tc.name, tc.fn())
})
}
}
// TestInvalidKeyAndSignatureInputs 覆盖:nil/残缺密钥、空或畸形签名、非法 Base64 都必须返回 error 且不 panic。
func TestInvalidKeyAndSignatureInputs(t *testing.T) {
priv := mustGenerateKey(t, elliptic.P256())
message := []byte("非法输入测试")
validSignature, err := ecdsax.Sign(priv, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
// 手工构造的残缺密钥:用于证明入口处的校验能挡住会让标准库 panic 的输入。
nilCurveKey := &ecdsa.PrivateKey{}
nilScalarKey := &ecdsa.PrivateKey{PublicKey: ecdsa.PublicKey{Curve: elliptic.P256()}}
missingPointKey := &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{Curve: elliptic.P256()},
D: big.NewInt(1),
}
offCurveKey := &ecdsa.PrivateKey{
PublicKey: ecdsa.PublicKey{Curve: elliptic.P256(), X: big.NewInt(1), Y: big.NewInt(1)},
D: big.NewInt(1),
}
nilCurvePub := &ecdsa.PublicKey{}
missingPointPub := &ecdsa.PublicKey{Curve: elliptic.P256()}
offCurvePub := &ecdsa.PublicKey{Curve: elliptic.P256(), X: big.NewInt(1), Y: big.NewInt(1)}
infinityPub := &ecdsa.PublicKey{Curve: elliptic.P256(), X: big.NewInt(0), Y: big.NewInt(0)}
cases := []struct {
name string
fn func() error
}{
{"Sign 私钥为 nil", func() error { _, err := ecdsax.Sign(nil, message); return err }},
{"Sign 私钥曲线为 nil", func() error { _, err := ecdsax.Sign(nilCurveKey, message); return err }},
{"Sign 私钥缺少标量 D", func() error { _, err := ecdsax.Sign(nilScalarKey, message); return err }},
{"Sign 私钥缺少坐标", func() error { _, err := ecdsax.Sign(missingPointKey, message); return err }},
{"Sign 私钥公钥点不在曲线上", func() error { _, err := ecdsax.Sign(offCurveKey, message); return err }},
{"SignToBase64 私钥为 nil", func() error { _, err := ecdsax.SignToBase64(nil, message); return err }},
{"MarshalPrivateKeyPEM 私钥为 nil", func() error { _, err := ecdsax.MarshalPrivateKeyPEM(nil); return err }},
{"MarshalPrivateKeyPEM 私钥缺少标量 D", func() error { _, err := ecdsax.MarshalPrivateKeyPEM(nilScalarKey); return err }},
{"Verify 公钥为 nil", func() error { return ecdsax.Verify(nil, message, validSignature) }},
{"Verify 公钥曲线为 nil", func() error { return ecdsax.Verify(nilCurvePub, message, validSignature) }},
{"Verify 公钥缺少坐标", func() error { return ecdsax.Verify(missingPointPub, message, validSignature) }},
{"Verify 公钥点不在曲线上", func() error { return ecdsax.Verify(offCurvePub, message, validSignature) }},
{"Verify 公钥为无穷远点", func() error { return ecdsax.Verify(infinityPub, message, validSignature) }},
{"MarshalPublicKeyPEM 公钥为 nil", func() error { _, err := ecdsax.MarshalPublicKeyPEM(nil); return err }},
{"MarshalPublicKeyPEM 公钥缺少坐标", func() error { _, err := ecdsax.MarshalPublicKeyPEM(missingPointPub); return err }},
{"Verify 签名为 nil", func() error { return ecdsax.Verify(&priv.PublicKey, message, nil) }},
{"Verify 签名为空切片", func() error { return ecdsax.Verify(&priv.PublicKey, message, []byte{}) }},
{"Verify 签名被截断", func() error {
return ecdsax.Verify(&priv.PublicKey, message, validSignature[:len(validSignature)-1])
}},
{"Verify 签名非 DER", func() error { return ecdsax.Verify(&priv.PublicKey, message, []byte("not-a-der-signature")) }},
{"VerifyFromBase64 非法 Base64", func() error {
return ecdsax.VerifyFromBase64(&priv.PublicKey, message, "!!!not-base64!!!")
}},
{"VerifyFromBase64 空字符串", func() error { return ecdsax.VerifyFromBase64(&priv.PublicKey, message, "") }},
{"VerifyFromBase64 公钥为 nil", func() error {
return ecdsax.VerifyFromBase64(nil, message, base64.StdEncoding.EncodeToString(validSignature))
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
requireError(t, tc.name, tc.fn())
})
}
}
// TestSaveLoadKeyPair 覆盖:三条曲线的 SaveKeyPair -> LoadPrivateKey/LoadPublicKey 往返。
func TestSaveLoadKeyPair(t *testing.T) {
for _, tc := range supportedCurves() {
t.Run(tc.name, func(t *testing.T) {
dir := t.TempDir()
privatePath := filepath.Join(dir, "ec_private.pem")
publicPath := filepath.Join(dir, "ec_public.pem")
if err := ecdsax.SaveKeyPair(privatePath, publicPath, tc.curve); err != nil {
t.Fatalf("SaveKeyPair 返回错误: %v", err)
}
priv, err := ecdsax.LoadPrivateKey(privatePath)
if err != nil {
t.Fatalf("LoadPrivateKey 返回错误: %v", err)
}
pub, err := ecdsax.LoadPublicKey(publicPath)
if err != nil {
t.Fatalf("LoadPublicKey 返回错误: %v", err)
}
if priv.Curve != tc.curve {
t.Errorf("加载的私钥曲线 = %v, 期望 %v", priv.Curve, tc.curve)
}
if !priv.PublicKey.Equal(pub) {
t.Error("加载的私钥与公钥不匹配")
}
message := []byte("落盘往返测试 / " + tc.name)
signature, err := ecdsax.Sign(priv, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
if err := ecdsax.Verify(pub, message, signature); err != nil {
t.Fatalf("Verify 应通过, 实际返回错误: %v", err)
}
// 权限位只在类 Unix 系统上有意义,Windows 只用 mode 表示只读属性。
if runtime.GOOS != "windows" {
privateInfo, err := os.Stat(privatePath)
if err != nil {
t.Fatalf("Stat 私钥文件失败: %v", err)
}
if perm := privateInfo.Mode().Perm(); perm != 0o600 {
t.Errorf("私钥文件权限 = %o, 期望 600", perm)
}
publicInfo, err := os.Stat(publicPath)
if err != nil {
t.Fatalf("Stat 公钥文件失败: %v", err)
}
if perm := publicInfo.Mode().Perm(); perm != 0o644 {
t.Errorf("公钥文件权限 = %o, 期望 644", perm)
}
}
})
}
}
// TestLoadKeyErrors 覆盖:路径不存在、路径是目录、文件内容非法时都必须返回 error。
func TestLoadKeyErrors(t *testing.T) {
dir := t.TempDir()
requireError(t, "LoadPrivateKey 路径不存在", func() error {
_, err := ecdsax.LoadPrivateKey(filepath.Join(dir, "missing.pem"))
return err
}())
requireError(t, "LoadPublicKey 路径不存在", func() error {
_, err := ecdsax.LoadPublicKey(filepath.Join(dir, "missing.pem"))
return err
}())
requireError(t, "LoadPrivateKey 路径为目录", func() error {
_, err := ecdsax.LoadPrivateKey(dir)
return err
}())
junkPath := filepath.Join(dir, "junk.pem")
if err := os.WriteFile(junkPath, []byte("这不是 PEM"), 0o600); err != nil {
t.Fatalf("写入测试文件失败: %v", err)
}
requireError(t, "LoadPrivateKey 内容非法", func() error {
_, err := ecdsax.LoadPrivateKey(junkPath)
return err
}())
requireError(t, "LoadPublicKey 内容非法", func() error {
_, err := ecdsax.LoadPublicKey(junkPath)
return err
}())
}
// flipRandomBits 复制一份数据并随机翻转若干位,用于构造逼近真实攻击面的畸形输入。
func flipRandomBits(rng *mrand.Rand, data []byte) []byte {
out := append([]byte{}, data...)
for i := 0; i < 1+rng.Intn(4); i++ {
out[rng.Intn(len(out))] ^= byte(1 << rng.Intn(8))
}
return out
}
// randomBytes 生成指定长度的随机字节串。
func randomBytes(rng *mrand.Rand, size int) []byte {
out := make([]byte, size)
for i := range out {
out[i] = byte(rng.Intn(256))
}
return out
}
// TestMalformedInputsDoNotPanic 覆盖:随机字节、随机位翻转的 PEM/签名,以及坐标随机的公钥,
// 都不会触发 panic。这是对"任何情况下不得 panic"这一硬性要求的直接验证,
// 因此只要求不 panic,不约束具体错误内容。
func TestMalformedInputsDoNotPanic(t *testing.T) {
privatePEM, publicPEM, err := ecdsax.GenerateKeyPEM(elliptic.P256())
if err != nil {
t.Fatalf("GenerateKeyPEM 返回错误: %v", err)
}
priv, err := ecdsax.ParsePrivateKeyPEM(privatePEM)
if err != nil {
t.Fatalf("ParsePrivateKeyPEM 返回错误: %v", err)
}
pub, err := ecdsax.ParsePublicKeyPEM(publicPEM)
if err != nil {
t.Fatalf("ParsePublicKeyPEM 返回错误: %v", err)
}
message := []byte("畸形输入测试")
signature, err := ecdsax.Sign(priv, message)
if err != nil {
t.Fatalf("Sign 返回错误: %v", err)
}
rng := mrand.New(mrand.NewSource(20240517))
for i := 0; i < 200; i++ {
// 完全随机的字节串。
raw := make([]byte, rng.Intn(256))
for j := range raw {
raw[j] = byte(rng.Intn(256))
}
_, _ = ecdsax.ParsePrivateKeyPEM(raw)
_, _ = ecdsax.ParsePublicKeyPEM(raw)
_ = ecdsax.Verify(pub, message, raw)
_ = ecdsax.VerifyFromBase64(pub, message, string(raw))
// 在合法 PEM 上随机翻转比特位,更容易命中解析器的深层分支。
_, _ = ecdsax.ParsePrivateKeyPEM(flipRandomBits(rng, privatePEM))
_, _ = ecdsax.ParsePublicKeyPEM(flipRandomBits(rng, publicPEM))
// 在合法签名上随机翻转比特位。
_ = ecdsax.Verify(pub, message, flipRandomBits(rng, signature))
_ = ecdsax.VerifyFromBase64(pub, message, base64.StdEncoding.EncodeToString(flipRandomBits(rng, signature)))
// 坐标与标量完全是随机数的密钥:标准库对畸形点会报错,
// 本包必须在进入标准库之前就拦住,不能 panic。
randomPub := &ecdsa.PublicKey{
Curve: elliptic.P256(),
X: new(big.Int).SetBytes(randomBytes(rng, 32)),
Y: new(big.Int).SetBytes(randomBytes(rng, 32)),
}
randomPriv := &ecdsa.PrivateKey{
PublicKey: *randomPub,
D: new(big.Int).SetBytes(randomBytes(rng, 32)),
}
_ = ecdsax.Verify(randomPub, message, signature)
_, _ = ecdsax.Sign(randomPriv, message)
_, _ = ecdsax.MarshalPublicKeyPEM(randomPub)
_, _ = ecdsax.MarshalPrivateKeyPEM(randomPriv)
}
}
// TestSaveKeyPairErrors 覆盖:写入路径非法时返回 error 而不是 panic。
func TestSaveKeyPairErrors(t *testing.T) {
dir := t.TempDir()
requireError(t, "SaveKeyPair 私钥路径为目录",
ecdsax.SaveKeyPair(dir, filepath.Join(dir, "pub.pem"), elliptic.P256()))
privatePath := filepath.Join(dir, "priv.pem")
requireError(t, "SaveKeyPair 公钥路径目录不存在",
ecdsax.SaveKeyPair(privatePath, filepath.Join(dir, "no-such-dir", "pub.pem"), elliptic.P256()))
if _, err := os.Stat(privatePath); err == nil {
t.Error("公钥写入失败后应删除已写入的私钥文件")
}
}