Files
Havline/internal/certificate/import.go
T
mrlee 278f8267b7 chore: 初始提交 Fonu 1.0.2
- 公网反代:Nginx 状态卡片(版本 / 运行状态 / 配置校验 / 端口监听)
- 部署设置第一、二梯队(HTTPS 开关、安全头、限流、连接数、仅中国大陆 IP)
- 隧道检测兼容 frps v0.68 / v0.71(V2 与旧版端点)
- 恢复被清空的 internal/frp/store.go、internal/api/ddns_handler.go、web/src/views/DdnsView.vue
2026-09-23 09:40:59 +08:00

215 lines
4.8 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 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
}