778 lines
28 KiB
Go
778 lines
28 KiB
Go
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("公钥写入失败后应删除已写入的私钥文件")
|
||
}
|
||
}
|