Files
havline-native/app-src/internal/certificate/import.go
T

215 lines
4.8 KiB
Go
Raw Normal View History

package certificate
import (
"crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"net"
"os"
"strings"
"time"
)
type ImportInfo struct {
Primary string
Domains []string
Wildcard bool
ExpiresAt time.Time
}
func InspectImport(certPEM, keyPEM []byte) (ImportInfo, error) {
cert, err := parseCertificate(certPEM)
if err != nil {
return ImportInfo{}, err
}
key, err := parsePrivateKey(keyPEM)
if err != nil {
return ImportInfo{}, err
}
if !publicKeysMatch(cert.PublicKey, key) {
return ImportInfo{}, fmt.Errorf("证书与私钥不匹配")
}
domains, err := ExtractCertDomains(cert)
if err != nil {
return ImportInfo{}, err
}
return ImportInfo{
Primary: primaryDomain(domains),
Domains: domains,
Wildcard: hasWildcardDomain(domains),
ExpiresAt: cert.NotAfter.UTC(),
}, nil
}
func ValidatePEM(certPEM, keyPEM []byte) (time.Time, error) {
info, err := InspectImport(certPEM, keyPEM)
if err != nil {
return time.Time{}, err
}
return info.ExpiresAt, nil
}
func ExtractCertDomains(cert *x509.Certificate) ([]string, error) {
seen := map[string]bool{}
var domains []string
add := func(name string) error {
name = strings.TrimSuffix(strings.ToLower(strings.TrimSpace(name)), ".")
if name == "" || net.ParseIP(name) != nil {
return nil
}
if seen[name] {
return nil
}
if strings.HasPrefix(name, "*.") {
if err := validateDomainLabel(strings.TrimPrefix(name, "*.")); err != nil {
return err
}
} else if err := validateDomainLabel(name); err != nil {
return err
}
seen[name] = true
domains = append(domains, name)
return nil
}
for _, name := range cert.DNSNames {
if err := add(name); err != nil {
return nil, err
}
}
if err := add(cert.Subject.CommonName); err != nil {
return nil, err
}
if len(domains) == 0 {
return nil, fmt.Errorf("证书中未包含有效域名")
}
return domains, nil
}
func primaryDomain(domains []string) string {
for _, domain := range domains {
if !strings.HasPrefix(domain, "*.") {
return domain
}
}
return strings.TrimPrefix(domains[0], "*.")
}
func hasWildcardDomain(domains []string) bool {
for _, domain := range domains {
if strings.HasPrefix(domain, "*.") {
return true
}
}
return false
}
func validateDomainLabel(domain string) error {
if domain == "" {
return fmt.Errorf("域名格式无效")
}
parts := strings.Split(domain, ".")
for _, part := range parts {
if part == "" || len(part) > 63 {
return fmt.Errorf("域名格式无效")
}
}
return nil
}
func parseCertificate(certPEM []byte) (*x509.Certificate, error) {
rest := certPEM
for {
block, remaining := pem.Decode(rest)
if block == nil {
return nil, fmt.Errorf("证书 PEM 格式无效")
}
if block.Type == "CERTIFICATE" {
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("解析证书失败:%w", err)
}
return cert, nil
}
rest = remaining
if len(rest) == 0 {
break
}
}
return nil, fmt.Errorf("证书 PEM 中未找到有效证书")
}
func parsePrivateKey(keyPEM []byte) (crypto.PrivateKey, error) {
block, _ := pem.Decode(keyPEM)
if block == nil {
return nil, fmt.Errorf("私钥 PEM 格式无效")
}
switch block.Type {
case "RSA PRIVATE KEY":
return x509.ParsePKCS1PrivateKey(block.Bytes)
case "EC PRIVATE KEY":
return x509.ParseECPrivateKey(block.Bytes)
case "PRIVATE KEY":
return x509.ParsePKCS8PrivateKey(block.Bytes)
default:
return nil, fmt.Errorf("不支持的私钥类型:%s", block.Type)
}
}
func publicKeysMatch(certKey crypto.PublicKey, privateKey crypto.PrivateKey) bool {
switch pub := certKey.(type) {
case *rsa.PublicKey:
priv, ok := privateKey.(*rsa.PrivateKey)
return ok && pub.N.Cmp(priv.N) == 0
case *ecdsa.PublicKey:
priv, ok := privateKey.(*ecdsa.PrivateKey)
return ok && pub.X.Cmp(priv.X) == 0 && pub.Y.Cmp(priv.Y) == 0
default:
return false
}
}
func NormalizePEM(v string) []byte {
return []byte(strings.TrimSpace(v))
}
func ResolvePEMSource(content, path string) ([]byte, error) {
path = strings.TrimSpace(path)
if path != "" {
return ReadPEMFile(path)
}
data := NormalizePEM(content)
if len(data) == 0 {
return nil, fmt.Errorf("内容不能为空")
}
return data, nil
}
func ReadPEMFile(path string) ([]byte, error) {
path = strings.TrimSpace(path)
if path == "" {
return nil, fmt.Errorf("文件路径不能为空")
}
info, err := os.Stat(path)
if err != nil {
return nil, fmt.Errorf("无法读取文件 %s:%w", path, err)
}
if info.IsDir() {
return nil, fmt.Errorf("%s 是目录,不是文件", path)
}
if info.Size() > 1<<20 {
return nil, fmt.Errorf("文件过大(最大 1MB)")
}
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("读取文件失败:%w", err)
}
if len(strings.TrimSpace(string(data))) == 0 {
return nil, fmt.Errorf("文件内容为空")
}
return data, nil
}