From f120ab24eda3e84bbf7a1219a70bf24218c893ff Mon Sep 17 00:00:00 2001 From: 4566704 <4566704@qq.com> Date: Sun, 20 Sep 2026 12:37:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(certx):=20go-hua/ssl=20=E8=BF=81=E5=85=A5?= =?UTF-8?q?=E5=B9=B6=E6=9B=B4=E5=90=8D=20certx(=E8=AF=81=E4=B9=A6/?= =?UTF-8?q?=E7=A7=81=E9=92=A5=E5=B7=A5=E5=85=B7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 原包名 ssl 与协议名混淆, 实为 x509 证书解析校验与自签生成, 更名 certx - 附测试、examples/certx 例程与包 README --- certx/README.md | 31 ++++++++ certx/certx.go | 156 +++++++++++++++++++++++++++++++++++++++++ certx/certx_test.go | 56 +++++++++++++++ examples/certx/main.go | 40 +++++++++++ 4 files changed, 283 insertions(+) create mode 100644 certx/README.md create mode 100644 certx/certx.go create mode 100644 certx/certx_test.go create mode 100644 examples/certx/main.go diff --git a/certx/README.md b/certx/README.md new file mode 100644 index 0000000..839bb92 --- /dev/null +++ b/certx/README.md @@ -0,0 +1,31 @@ +# certx + +证书/私钥工具:PEM 解析校验(证书、公私钥匹配验证)与自签证书生成。 + +> 迁移自 go-hua/ssl 并更名:原包名 `ssl` 与协议名混淆,实际是 x509 证书操作, +> 故更名 `certx`。 + +## 用法 + +```go +import "git.zeroonesoft.cn/golib/zogo/certx" + +// 自签证书(返回证书 PEM 与私钥 PEM) +certPEM, keyPEM, err := certx.CreateCertificate( + "localhost", // CommonName + []string{"localhost", "example.com"}, // SAN DNS 列表 +) + +// 解析校验 +cert, err := certx.ParseCertificate(certPEM) +key, err := certx.ParsePrivateKey(keyPEM) +err = certx.VerifyPrivateKey(cert, keyPEM) // 证书与私钥是否配对 +``` + +完整可运行例程:[examples/certx/main.go](../examples/certx/main.go) + +## 注意 + +- `CreateCertificate` 生成 RSA 2048 自签证书,仅适合内网/测试或做 CA 签发的种子; + 生产对外服务请用正规 CA 证书。 +- `ParsePrivateKey` 支持 PKCS#1/PKCS#8/EC 三种 PEM 形态,返回 `any`(断言后使用)。 diff --git a/certx/certx.go b/certx/certx.go new file mode 100644 index 0000000..2077c10 --- /dev/null +++ b/certx/certx.go @@ -0,0 +1,156 @@ +// Package certx 提供证书/私钥的解析校验与自签证书生成。 +package certx + +import ( + "crypto/ecdh" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "errors" + "math/big" + "time" +) + +// ParseCertificate 解析 PEM 格式证书 +func ParseCertificate(data []byte) (*x509.Certificate, error) { + block, _ := pem.Decode(data) + if block == nil { + return nil, errors.New("证书M解码失败") + } + //调用x509的接口 + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + return cert, err +} + +// ParsePrivateKey 解析 PEM 格式私钥(自动尝试 PKCS8/PKCS1/EC 格式) +func ParsePrivateKey(data []byte) (any, error) { + block, _ := pem.Decode(data) + if block == nil { + return nil, errors.New("证书M解码失败") + } + //调用x509的接口 + PKCS8, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err == nil { + return PKCS8, nil + } + + PKCS1, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err == nil { + return PKCS1, nil + } + + ECP, err := x509.ParseECPrivateKey(block.Bytes) + if err == nil { + return ECP, nil + } + + return nil, err +} + +// VerifyPrivateKey 校验私钥与证书是否匹配 +// der 为 PEM 格式私钥,支持 RSA/ECDSA/Ed25519/ECDH 类型 +func VerifyPrivateKey(cert *x509.Certificate, der []byte) error { + block, _ := pem.Decode(der) + if block == nil { + return errors.New("证书M解码失败") + } + //调用x509的接口 + + PKCS1, err := x509.ParsePKCS1PrivateKey(block.Bytes) + if err == nil { + if PKCS1.PublicKey.Equal(cert.PublicKey) == false { + return errors.New("私钥与证书不匹配") + } + return nil + } + + ECP, err := x509.ParseECPrivateKey(block.Bytes) + if err == nil { + if ECP.PublicKey.Equal(cert.PublicKey) == false { + return errors.New("私钥与证书不匹配") + } + return nil + } + + PKCS8, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err == nil { + + // 根据实际类型使用私钥 + switch key := PKCS8.(type) { + case *rsa.PrivateKey: + // 在这里使用 RSA 私钥 + if key.PublicKey.Equal(cert.PublicKey) == false { + return errors.New("私钥与证书不匹配") + } + return nil + case *ecdsa.PrivateKey: + // 在这里使用 ECDSA 私钥 + if key.PublicKey.Equal(cert.PublicKey) == false { + return errors.New("私钥与证书不匹配") + } + return nil + case ed25519.PrivateKey: + // 在这里使用 Ed25519 私钥 + if key.Equal(cert.PublicKey) == false { + return errors.New("私钥与证书不匹配") + } + return nil + case ecdh.PrivateKey: + // 在这里使用 Ed25519 私钥 + if key.Equal(cert.PublicKey) == false { + return errors.New("私钥与证书不匹配") + } + return nil + default: + return errors.New("不支持的私钥类型") + } + } + + return err +} + +// CreateCertificate 生成自签名证书 +// commonName 为证书主题,dnsName 为 SAN 域名列表 +// 返回 PEM 格式的证书与 RSA 2048 私钥,有效期 10 年 +func CreateCertificate(commonName string, dnsName []string) ([]byte, []byte, error) { + maxValue := new(big.Int).Lsh(big.NewInt(1), 128) + serialNumber, _ := rand.Int(rand.Reader, maxValue) + + // 定义:引用IETF的安全领域的公钥基础实施(PKIX)工作组的标准实例化内容 + subject := pkix.Name{ + CommonName: commonName, + } + + // 设置 SSL证书的属性用途 + certificate509 := x509.Certificate{ + SerialNumber: serialNumber, + Subject: subject, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + //IPAddresses: []net.IP{net.ParseIP("127.0.0.1")}, + DNSNames: dnsName, + } + + // 生成指定位数密匙 + pk, _ := rsa.GenerateKey(rand.Reader, 2048) + + // 生成 SSL公匙 + derBytes, err := x509.CreateCertificate(rand.Reader, &certificate509, &certificate509, &pk.PublicKey, pk) + if err != nil { + return nil, nil, err + } + certBuf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: derBytes}) + + // 生成 SSL私匙 + keyBuf := pem.EncodeToMemory(&pem.Block{Type: "RAS PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(pk)}) + return certBuf, keyBuf, err +} diff --git a/certx/certx_test.go b/certx/certx_test.go new file mode 100644 index 0000000..3b2e333 --- /dev/null +++ b/certx/certx_test.go @@ -0,0 +1,56 @@ +package certx + +import ( + "strings" + "testing" + "time" +) + +func TestCreateAndParseCertificate(t *testing.T) { + certPEM, keyPEM, err := CreateCertificate("test.example.com", []string{"test.example.com", "localhost"}) + if err != nil { + t.Fatalf("CreateCertificate: %v", err) + } + + // PEM 格式校验 + if !strings.Contains(string(certPEM), "BEGIN CERTIFICATE") { + t.Error("证书 PEM 格式错误") + } + if !strings.Contains(string(keyPEM), "PRIVATE KEY") { + t.Error("私钥 PEM 格式错误") + } + + // 解析证书 + cert, err := ParseCertificate(certPEM) + if err != nil { + t.Fatalf("ParseCertificate: %v", err) + } + if cert.Subject.CommonName != "test.example.com" { + t.Errorf("CommonName = %q", cert.Subject.CommonName) + } + if len(cert.DNSNames) != 2 { + t.Errorf("DNSNames = %v, want 2 项", cert.DNSNames) + } + if !time.Now().Before(cert.NotAfter) { + t.Error("证书应未过期") + } + + // 解析私钥 + if _, err := ParsePrivateKey(keyPEM); err != nil { + t.Fatalf("ParsePrivateKey: %v", err) + } + + // 证书与私钥匹配 + if err := VerifyPrivateKey(cert, keyPEM); err != nil { + t.Errorf("证书私钥应匹配: %v", err) + } +} + +func TestParseInvalidPem(t *testing.T) { + if _, err := ParseCertificate([]byte("not a pem")); err == nil { + t.Error("非法 PEM 应返回错误") + } + if _, err := ParsePrivateKey([]byte("not a pem")); err == nil { + t.Error("非法 PEM 应返回错误") + } +} diff --git a/examples/certx/main.go b/examples/certx/main.go new file mode 100644 index 0000000..32644d4 --- /dev/null +++ b/examples/certx/main.go @@ -0,0 +1,40 @@ +// certx 包示例:自签证书生成与解析、私钥解析、证书私钥匹配校验(全程内存操作) +package main + +import ( + "fmt" + + "git.zeroonesoft.cn/golib/zogo/certx" +) + +func main() { + // 生成自签名证书(RSA 2048,有效期 10 年) + certPEM, keyPEM, err := certx.CreateCertificate("go-hua.example.com", []string{"go-hua.example.com", "localhost"}) + if err != nil { + panic(err) + } + fmt.Printf("证书 PEM: %d 字节, 私钥 PEM: %d 字节\n", len(certPEM), len(keyPEM)) + + // 解析证书 + cert, err := certx.ParseCertificate(certPEM) + if err != nil { + panic(err) + } + fmt.Println("主题:", cert.Subject.CommonName) + fmt.Println("DNS 名称:", cert.DNSNames) + fmt.Println("有效期至:", cert.NotAfter.Format("2006-01-02")) + + // 解析私钥(自动尝试 PKCS1 / PKCS8 / EC) + key, err := certx.ParsePrivateKey(keyPEM) + if err != nil { + panic(err) + } + fmt.Printf("私钥类型: %T\n", key) + + // 校验证书与私钥是否匹配 + fmt.Println("证书私钥匹配:", certx.VerifyPrivateKey(cert, keyPEM) == nil) + + // 实际 TLS 服务中的用法: + // certPair, _ := tls.X509KeyPair(certPEM, keyPEM) + // tls.NewListener(listener, &tls.Config{Certificates: []tls.Certificate{certPair}}) +}