Files
WddSuperAgent/agent-wdd/cert_manager_wdd/AcmeClient.go

369 lines
9.2 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 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
}