217 lines
5.5 KiB
Go
217 lines
5.5 KiB
Go
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
|
|
}
|