package letsencrypt import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/x509" "encoding/json" "encoding/pem" "fmt" "os" "path/filepath" "time" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/registration" ) type Client struct { user *User client *lego.Client certDir string email string dnsProvider DNSProvider } type User struct { Email string Registration *registration.Resource key crypto.PrivateKey } func (u *User) GetEmail() string { return u.Email } func (u *User) GetRegistration() *registration.Resource { return u.Registration } func (u *User) GetPrivateKey() crypto.PrivateKey { return u.key } type DNSProvider interface { Present(domain, token, keyAuth string) error CleanUp(domain, token, keyAuth string) error } type CertificateInfo struct { CertificatePath string PrivateKeyPath string FullChainPath string Domain string ExpiresAt time.Time } func NewClient(email, certDir string, dnsProvider DNSProvider) (*Client, error) { if err := os.MkdirAll(certDir, 0700); err != nil { return nil, fmt.Errorf("failed to create cert directory: %w", err) } privateKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, fmt.Errorf("failed to generate private key: %w", err) } user := &User{ Email: email, key: privateKey, } config := lego.NewConfig(user) config.CADirURL = lego.LEDirectoryProduction config.Certificate.KeyType = certcrypto.RSA2048 config.HTTPClient.Timeout = 30 * time.Second client, err := lego.NewClient(config) if err != nil { return nil, fmt.Errorf("failed to create lego client: %w", err) } provider := &namecheapDNSProvider{dnsProvider: dnsProvider} client.Challenge.SetDNS01Provider(provider) accountPath := filepath.Join(certDir, "account.json") var reg *registration.Resource if _, err := os.Stat(accountPath); err == nil { accountData, err := os.ReadFile(accountPath) if err == nil { if err := json.Unmarshal(accountData, ®); err == nil && reg != nil { user.Registration = reg } } } if user.Registration == nil { reg, err := client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) if err != nil { return nil, fmt.Errorf("failed to register with Let's Encrypt: %w", err) } user.Registration = reg accountData, _ := json.Marshal(reg) os.WriteFile(accountPath, accountData, 0600) } return &Client{ user: user, client: client, certDir: certDir, email: email, dnsProvider: dnsProvider, }, nil } type namecheapDNSProvider struct { dnsProvider DNSProvider } func (p *namecheapDNSProvider) Present(domain, token, keyAuth string) error { return p.dnsProvider.Present(domain, token, keyAuth) } func (p *namecheapDNSProvider) CleanUp(domain, token, keyAuth string) error { return p.dnsProvider.CleanUp(domain, token, keyAuth) } func (c *Client) ObtainCertificate(domain string, sanDomains []string) (*CertificateInfo, error) { domains := append([]string{domain}, sanDomains...) request := certificate.ObtainRequest{ Domains: domains, Bundle: true, } certificates, err := c.client.Certificate.Obtain(request) if err != nil { return nil, fmt.Errorf("failed to obtain certificate: %w", err) } certPath := filepath.Join(c.certDir, fmt.Sprintf("%s.crt", domain)) keyPath := filepath.Join(c.certDir, fmt.Sprintf("%s.key", domain)) fullChainPath := filepath.Join(c.certDir, fmt.Sprintf("%s-fullchain.crt", domain)) if err := os.WriteFile(certPath, certificates.Certificate, 0644); err != nil { return nil, fmt.Errorf("failed to write certificate: %w", err) } if err := os.WriteFile(keyPath, certificates.PrivateKey, 0600); err != nil { return nil, fmt.Errorf("failed to write private key: %w", err) } fullChain := append(certificates.Certificate, certificates.IssuerCertificate...) if err := os.WriteFile(fullChainPath, fullChain, 0644); err != nil { return nil, fmt.Errorf("failed to write full chain: %w", err) } block, _ := pem.Decode(certificates.Certificate) if block == nil { return nil, fmt.Errorf("failed to decode certificate") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %w", err) } return &CertificateInfo{ CertificatePath: certPath, PrivateKeyPath: keyPath, FullChainPath: fullChainPath, Domain: domain, ExpiresAt: cert.NotAfter, }, nil } func (c *Client) RevokeCertificate(certPath string) error { certBytes, err := os.ReadFile(certPath) if err != nil { return fmt.Errorf("failed to read certificate: %w", err) } reason := uint(4) return c.client.Certificate.RevokeWithReason(certBytes, &reason) } func GetCertificateInfo(certPath string) (*CertificateInfo, error) { certBytes, err := os.ReadFile(certPath) if err != nil { return nil, fmt.Errorf("failed to read certificate: %w", err) } block, _ := pem.Decode(certBytes) if block == nil { return nil, fmt.Errorf("failed to decode certificate") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %w", err) } keyPath := certPath[:len(certPath)-4] + ".key" fullChainPath := certPath[:len(certPath)-4] + "-fullchain.crt" return &CertificateInfo{ CertificatePath: certPath, PrivateKeyPath: keyPath, FullChainPath: fullChainPath, Domain: cert.Subject.CommonName, ExpiresAt: cert.NotAfter, }, nil }