package cert_manager_wdd import ( "agent-wdd/cloudflare" "agent-wdd/log" "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "crypto/x509/pkix" "fmt" "os" "path/filepath" "strings" "time" "encoding/pem" "golang.org/x/crypto/acme" ) // ACMEClient ACME协议客户端 type ACMEClient struct { client *acme.Client account *acme.Account accountKey crypto.Signer directory string email string cfAPI *cloudflare.CloudflareClient } // NewACMEClient 创建新的ACME客户端 // // 参数: // - directory: ACME服务器目录URL // - email: 注册账户的邮箱 // - cfAPI: Cloudflare API客户端 // // 返回: // - *ACMEClient: ACME客户端 // - error: 错误信息 func NewACMEClient(directory, email string, cfAPI *cloudflare.CloudflareClient) (*ACMEClient, error) { log.Debug("创建ACME客户端,目录: %s, 邮箱: %s", directory, email) // 创建客户端 client := &acme.Client{ DirectoryURL: directory, } // 生成账户密钥 accountKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { log.Error("生成ACME账户密钥失败: %v", err) return nil, fmt.Errorf("生成ACME账户密钥失败: %w", err) } acmeClient := &ACMEClient{ client: client, accountKey: accountKey, directory: directory, email: email, cfAPI: cfAPI, } // 注册账户 if err := acmeClient.registerAccount(); err != nil { log.Error("ACME账户注册失败: %v", err) return nil, fmt.Errorf("ACME账户注册失败: %w", err) } return acmeClient, nil } // registerAccount 注册ACME账户 func (ac *ACMEClient) registerAccount() error { log.Debug("注册ACME账户") // 创建上下文 ctx := context.Background() // 设置账户密钥 ac.client.Key = ac.accountKey // 注册新账户 account := &acme.Account{ Contact: []string{"mailto:" + ac.email}, } account, err := ac.client.Register(ctx, account, acme.AcceptTOS) if err != nil { // 错误处理,特别处理已存在账户的情况 if err.(*acme.Error).StatusCode == 409 { log.Info("ACME账户已存在,已重新获取") // 获取现有账户 account, err = ac.client.GetReg(ctx, "") if err != nil { return fmt.Errorf("获取现有ACME账户失败: %w", err) } } else { return fmt.Errorf("注册ACME账户失败: %w", err) } } ac.account = account log.Info("ACME账户注册/获取成功") return nil } // ObtainCertificate 获取证书 // // 参数: // - domain: 需要申请证书的域名 // - privateKey: 证书私钥 // - certDir: 证书保存目录 // // 返回: // - *CertInfo: 证书信息 // - error: 错误信息 func (ac *ACMEClient) ObtainCertificate(domain string, privateKey crypto.Signer, certDir string) (*CertInfo, error) { log.Info("开始获取域名 %s 的证书", domain) // 创建上下文 ctx := context.Background() // 创建新订单 order, err := ac.client.AuthorizeOrder(ctx, []acme.AuthzID{ {Type: "dns", Value: domain}, }) if err != nil { log.Error("创建ACME订单失败: %v", err) return nil, fmt.Errorf("创建ACME订单失败: %w", err) } // 处理授权 for _, authzURL := range order.AuthzURLs { authz, err := ac.client.GetAuthorization(ctx, authzURL) if err != nil { log.Error("获取授权信息失败: %v", err) return nil, fmt.Errorf("获取授权信息失败: %w", err) } // 查找DNS挑战 var challenge *acme.Challenge for _, c := range authz.Challenges { if c.Type == "dns-01" { challenge = c break } } if challenge == nil { log.Error("未找到DNS-01挑战") return nil, fmt.Errorf("未找到DNS-01挑战") } // 获取TXT记录值 txtValue, err := ac.client.DNS01ChallengeRecord(challenge.Token) if err != nil { log.Error("获取DNS-01挑战记录失败: %v", err) return nil, fmt.Errorf("获取DNS-01挑战记录失败: %w", err) } // 使用Cloudflare API设置DNS TXT记录 if err := ac.setDNSChallengeTXT(domain, txtValue); err != nil { log.Error("设置DNS TXT记录失败: %v", err) return nil, fmt.Errorf("设置DNS TXT记录失败: %w", err) } // 等待DNS传播 log.Info("等待DNS记录传播...") time.Sleep(30 * time.Second) // 通知ACME服务器完成挑战 if _, err := ac.client.Accept(ctx, challenge); err != nil { log.Error("接受挑战失败: %v", err) return nil, fmt.Errorf("接受挑战失败: %w", err) } // 等待授权完成 _, err = ac.client.WaitAuthorization(ctx, authzURL) if err != nil { log.Error("等待授权完成失败: %v", err) return nil, fmt.Errorf("等待授权完成失败: %w", err) } // 清理DNS记录 if err := ac.cleanupDNSChallenge(domain); err != nil { log.Warning("清理DNS TXT记录失败: %v", err) // 不中断流程,继续申请证书 } } // 创建CSR csr, err := createCSR(privateKey, domain) if err != nil { log.Error("创建CSR失败: %v", err) return nil, fmt.Errorf("创建CSR失败: %w", err) } // 完成订单 certDERs, _, err := ac.client.CreateOrderCert(ctx, order.FinalizeURL, csr, true) if err != nil { log.Error("完成订单失败: %v", err) return nil, fmt.Errorf("完成订单失败: %w", err) } // 处理证书 var pemData []byte for _, der := range certDERs { b := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der}) pemData = append(pemData, b...) } // 保存证书 certPath := filepath.Join(certDir, "cert.pem") if err := os.WriteFile(certPath, pemData, 0644); err != nil { log.Error("保存证书失败: %v", err) return nil, fmt.Errorf("保存证书失败: %w", err) } // 解析证书以获取详细信息 cert, err := parseCertificate(certPath) if err != nil { log.Error("解析保存的证书失败: %v", err) return nil, fmt.Errorf("解析保存的证书失败: %w", err) } // 构建证书信息 keyPath := filepath.Join(certDir, "private.key") certInfo := &CertInfo{ Domain: domain, RegisteredAt: cert.NotBefore, ExpiresAt: cert.NotAfter, CertPath: certPath, KeyPath: keyPath, CAName: getCertCAName(cert), NeedsRenewal: false, WildcardCert: strings.HasPrefix(domain, "*."), DaysRemaining: int(cert.NotAfter.Sub(time.Now()).Hours() / 24), } log.Info("证书获取成功: %s", domain) return certInfo, nil } // setDNSChallengeTXT 设置DNS TXT记录用于ACME验证 func (ac *ACMEClient) setDNSChallengeTXT(domain, txtValue string) error { log.Debug("设置DNS TXT记录: %s", domain) // 处理域名,提取主域名和记录名 recordName := fmt.Sprintf("_acme-challenge.%s", domain) if strings.HasPrefix(domain, "*.") { // 通配符域名处理 recordName = fmt.Sprintf("_acme-challenge.%s", domain[2:]) } // 获取域名的zone信息 zones, err := ac.cfAPI.ListZones(nil) if err != nil { return fmt.Errorf("获取Cloudflare zones失败: %w", err) } var zoneID string for _, zone := range zones { if strings.HasSuffix(domain, zone.Name) || strings.HasSuffix(domain[2:], zone.Name) { zoneID = zone.ID break } } if zoneID == "" { return fmt.Errorf("未找到域名 %s 的Cloudflare Zone", domain) } // 创建TXT记录 record := cloudflare.DNSRecord{ Type: "TXT", Name: recordName, Content: txtValue, TTL: 120, } _, err = ac.cfAPI.CreateDNSRecord(zoneID, record) if err != nil { return fmt.Errorf("创建DNS TXT记录失败: %w", err) } log.Info("DNS TXT记录已创建: %s", recordName) return nil } // cleanupDNSChallenge 清理DNS验证记录 func (ac *ACMEClient) cleanupDNSChallenge(domain string) error { log.Debug("清理DNS TXT记录: %s", domain) // 处理域名,提取主域名和记录名 recordName := fmt.Sprintf("_acme-challenge.%s", domain) if strings.HasPrefix(domain, "*.") { // 通配符域名处理 recordName = fmt.Sprintf("_acme-challenge.%s", domain[2:]) } // 获取域名的zone信息 zones, err := ac.cfAPI.ListZones(nil) if err != nil { return fmt.Errorf("获取Cloudflare zones失败: %w", err) } var zoneID string for _, zone := range zones { if strings.HasSuffix(domain, zone.Name) || strings.HasSuffix(domain[2:], zone.Name) { zoneID = zone.ID break } } if zoneID == "" { return fmt.Errorf("未找到域名 %s 的Cloudflare Zone", domain) } // 查找TXT记录 filter := &cloudflare.DNSRecordFilter{ Type: "TXT", Name: recordName, } records, err := ac.cfAPI.ListDNSRecords(zoneID, filter) if err != nil { return fmt.Errorf("查找DNS TXT记录失败: %w", err) } // 删除匹配的记录 for _, record := range records { if record.Name == recordName && record.Type == "TXT" { _, err = ac.cfAPI.DeleteDNSRecord(zoneID, record.ID) if err != nil { return fmt.Errorf("删除DNS TXT记录失败: %w", err) } log.Info("DNS TXT记录已删除: %s", recordName) } } return nil } // createCSR 创建证书签名请求 func createCSR(privateKey crypto.Signer, domain string) ([]byte, error) { template := &x509.CertificateRequest{ Subject: pkix.Name{ CommonName: domain, }, } // 处理通配符域名 if strings.HasPrefix(domain, "*.") { template.DNSNames = []string{domain, domain[2:]} } else { template.DNSNames = []string{domain} } csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, privateKey) if err != nil { return nil, err } return csrDER, nil }