package cert_manager_wdd import ( "agent-wdd/cloudflare" "agent-wdd/log" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/pem" "fmt" "os" "path/filepath" "strings" "time" ) const ( // DefaultCertDir 默认证书保存目录 DefaultCertDir = "certs" // DefaultDaysBeforeRenewal 默认更新阈值天数 DefaultDaysBeforeRenewal = 30 // DefaultKeyType 默认密钥类型 DefaultKeyType = "ec-256" ) // CACertInfo CA证书服务器信息 type CACertInfo struct { Name string // 证书颁发机构名称 Directory string // ACME目录URL } // 预定义的CA服务器 var ( ZeroSSLCA = CACertInfo{ Name: "ZeroSSL", Directory: "https://acme.zerossl.com/v2/DV90", } LetsEncryptCA = CACertInfo{ Name: "Let's Encrypt", Directory: "https://acme-v02.api.letsencrypt.org/directory", } ) // CertInfo 存储证书信息 type CertInfo struct { Domain string // 证书域名 RegisteredAt time.Time // 注册时间 ExpiresAt time.Time // 过期时间 CertPath string // 证书路径 KeyPath string // 密钥路径 CAName string // CA名称 NeedsRenewal bool // 是否需要更新 WildcardCert bool // 是否为通配符证书 DaysRemaining int // 剩余有效天数 } // CertManager 证书管理类 type CertManager struct { CertDir string // 证书保存目录 CloudflareAPI *cloudflare.CloudflareClient // Cloudflare API客户端 CAServer CACertInfo // 使用的CA服务器 DaysBeforeRenewal int // 证书更新阈值天数 EmailAddr string // 申请证书使用的邮箱 } // NewCertManager 创建一个新的证书管理器 // // 参数: // - certDir: 证书保存目录,若为空则使用默认目录 // - cfToken: Cloudflare API令牌 // - emailAddr: 申请证书使用的邮箱 // // 返回: // - *CertManager: 初始化的证书管理器 func NewCertManager(certDir string, cfToken string, emailAddr string) *CertManager { if certDir == "" { homeDir, err := os.UserHomeDir() if err != nil { log.Warning("无法获取用户主目录: %v,使用当前目录", err) homeDir = "." } certDir = filepath.Join(homeDir, DefaultCertDir) } // 确保证书目录存在 if err := os.MkdirAll(certDir, 0755); err != nil { log.Error("创建证书目录失败: %v", err) } return &CertManager{ CertDir: certDir, CloudflareAPI: cloudflare.GetInstance(cfToken), CAServer: LetsEncryptCA, // 默认使用Let's Encrypt DaysBeforeRenewal: DefaultDaysBeforeRenewal, EmailAddr: emailAddr, } } // SetCAServer 设置CA服务器 // // 参数: // - caServer: CA服务器信息 func (cm *CertManager) SetCAServer(caServer CACertInfo) { cm.CAServer = caServer log.Info("已将CA服务器设置为 %s", caServer.Name) } // ApplyCertificate 申请证书 // // 参数: // - domain: 需要申请证书的域名 // // 返回: // - *CertInfo: 证书信息 // - error: 错误信息 func (cm *CertManager) ApplyCertificate(domain string) (*CertInfo, error) { log.Info("开始为域名 %s 申请证书", domain) // 处理通配符域名文件夹命名 folderName := domain if strings.HasPrefix(domain, "*.") { folderName = "x" + domain[1:] log.Debug("通配符域名文件夹命名为: %s", folderName) } // 创建域名对应的证书目录 certFolder := filepath.Join(cm.CertDir, folderName) if err := os.MkdirAll(certFolder, 0755); err != nil { log.Error("创建域名证书目录失败: %v", err) return nil, fmt.Errorf("创建域名证书目录失败: %w", err) } // 生成私钥 privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Error("生成私钥失败: %v", err) return nil, fmt.Errorf("生成私钥失败: %w", err) } keyPath := filepath.Join(certFolder, "private.key") if err := savePrivateKey(privateKey, keyPath); err != nil { log.Error("保存私钥失败: %v", err) return nil, fmt.Errorf("保存私钥失败: %w", err) } // 使用自定义的DNS验证方法 dnsSolver := &DNSSolver{ CloudflareAPI: cm.CloudflareAPI, } // 使用自定义方法申请证书 issuedCert, err := IssueCertificate(domain, cm.EmailAddr, cm.CAServer.Directory, privateKey, certFolder, dnsSolver) if err != nil { log.Error("证书申请失败: %v", err) return nil, fmt.Errorf("证书申请失败: %w", err) } log.Info("证书申请成功: %s", domain) return issuedCert, nil } // GetAllCerts 获取所有证书信息 // // 返回: // - []CertInfo: 证书信息列表 // - error: 错误信息 func (cm *CertManager) GetAllCerts() ([]CertInfo, error) { log.Info("获取所有证书信息") var certs []CertInfo // 遍历证书目录 err := filepath.Walk(cm.CertDir, func(path string, info os.FileInfo, err error) error { if err != nil { return err } // 只处理目录 if !info.IsDir() || path == cm.CertDir { return nil } // 尝试解析证书 certPath := filepath.Join(path, "cert.pem") keyPath := filepath.Join(path, "private.key") if !fileExists(certPath) || !fileExists(keyPath) { log.Debug("目录 %s 中未找到完整的证书文件", path) return nil } cert, err := parseCertificate(certPath) if err != nil { log.Warning("解析证书 %s 失败: %v", certPath, err) return nil } // 获取域名 folderName := filepath.Base(path) domain := folderName if strings.HasPrefix(folderName, "x") { domain = "*" + domain[1:] } // 计算剩余天数 daysRemaining := int(cert.NotAfter.Sub(time.Now()).Hours() / 24) needsRenewal := daysRemaining <= cm.DaysBeforeRenewal certInfo := CertInfo{ Domain: domain, RegisteredAt: cert.NotBefore, ExpiresAt: cert.NotAfter, CertPath: certPath, KeyPath: keyPath, CAName: getCertCAName(cert), NeedsRenewal: needsRenewal, WildcardCert: strings.HasPrefix(domain, "*."), DaysRemaining: daysRemaining, } certs = append(certs, certInfo) return nil }) if err != nil { log.Error("遍历证书目录失败: %v", err) return nil, fmt.Errorf("遍历证书目录失败: %w", err) } log.Info("找到 %d 个证书", len(certs)) return certs, nil } // RenewCertificate 更新证书 // // 参数: // - domain: 需要更新证书的域名 // // 返回: // - *CertInfo: 更新后的证书信息 // - error: 错误信息 func (cm *CertManager) RenewCertificate(domain string) (*CertInfo, error) { log.Info("开始更新域名 %s 的证书", domain) // 获取当前证书信息 allCerts, err := cm.GetAllCerts() if err != nil { return nil, err } var certToRenew *CertInfo for _, cert := range allCerts { if cert.Domain == domain { certToRenew = &cert break } } if certToRenew == nil { log.Error("未找到域名 %s 的证书", domain) return nil, fmt.Errorf("未找到域名 %s 的证书", domain) } // 调用申请证书的方法重新申请 return cm.ApplyCertificate(domain) } // 工具函数 // savePrivateKey 保存私钥到文件 func savePrivateKey(privateKey *ecdsa.PrivateKey, filePath string) error { // 将私钥编码为PKCS8格式 privateKeyBytes, err := x509.MarshalPKCS8PrivateKey(privateKey) if err != nil { return err } // 将私钥编码为PEM格式 privateKeyPEM := pem.EncodeToMemory(&pem.Block{ Type: "PRIVATE KEY", Bytes: privateKeyBytes, }) // 写入文件 return os.WriteFile(filePath, privateKeyPEM, 0600) } // parseCertificate 解析证书文件 func parseCertificate(certPath string) (*x509.Certificate, error) { certPEM, err := os.ReadFile(certPath) if err != nil { return nil, err } block, _ := pem.Decode(certPEM) if block == nil || block.Type != "CERTIFICATE" { return nil, fmt.Errorf("无法解码证书PEM数据") } return x509.ParseCertificate(block.Bytes) } // getCertCAName 获取证书颁发机构名称 func getCertCAName(cert *x509.Certificate) string { // 尝试从证书中提取颁发者信息 if cert.Issuer.CommonName != "" { if strings.Contains(cert.Issuer.CommonName, "Let's Encrypt") { return "Let's Encrypt" } else if strings.Contains(cert.Issuer.CommonName, "ZeroSSL") { return "ZeroSSL" } } return cert.Issuer.CommonName } // fileExists 检查文件是否存在 func fileExists(filePath string) bool { _, err := os.Stat(filePath) return err == nil }