Scert/backend/internal/certificate/lego.go

105 lines
3.1 KiB
Go

package certificate
import (
"context"
"crypto"
"errors"
"fmt"
"net/http"
"strings"
"time"
acme "github.com/go-acme/lego/v5/acme"
"github.com/go-acme/lego/v5/certcrypto"
legoCertificate "github.com/go-acme/lego/v5/certificate"
"github.com/go-acme/lego/v5/challenge"
lego "github.com/go-acme/lego/v5/lego"
"github.com/go-acme/lego/v5/registration"
)
type AccountUser struct {
Email string
PrivateKey crypto.Signer
Registration *acme.ExtendedAccount
}
func (u *AccountUser) GetEmail() string { return u.Email }
func (u *AccountUser) GetRegistration() *acme.ExtendedAccount { return u.Registration }
func (u *AccountUser) GetPrivateKey() crypto.Signer { return u.PrivateKey }
type LegoIssuer struct {
User *AccountUser
Provider challenge.Provider
Environment string
HTTPClient *http.Client
Now func() time.Time
}
func (i *LegoIssuer) Obtain(ctx context.Context, request ObtainRequest) (Resource, error) {
if i == nil || i.User == nil || i.User.PrivateKey == nil {
return Resource{}, errors.New("ACME 账户未配置")
}
if i.Provider == nil {
return Resource{}, errors.New("DNS 验证服务商未配置")
}
domains, err := NormalizeDomains(request.Domains)
if err != nil {
return Resource{}, err
}
environment := strings.ToLower(strings.TrimSpace(request.Environment))
if environment == "" {
environment = "production"
}
caURL := lego.DirectoryURLLetsEncrypt
if environment == "staging" {
caURL = lego.DirectoryURLLetsEncryptStaging
} else if environment != "production" {
return Resource{}, fmt.Errorf("ACME 环境无效:%s", environment)
}
clientConfig := lego.NewConfig(i.User)
clientConfig.CADirURL = caURL
clientConfig.UserAgent = "Scert/0.1"
if i.HTTPClient != nil {
clientConfig.HTTPClient = i.HTTPClient
}
client, err := lego.NewClient(clientConfig)
if err != nil {
return Resource{}, fmt.Errorf("创建 ACME 客户端失败:%w", err)
}
if err := client.Challenge.SetDNS01Provider(i.Provider); err != nil {
return Resource{}, fmt.Errorf("配置 DNS-01 验证失败:%w", err)
}
if i.User.Registration == nil {
account, err := client.Registration.Register(ctx, registration.RegisterOptions{TermsOfServiceAgreed: true})
if err != nil {
return Resource{}, fmt.Errorf("注册 ACME 账户失败:%w", err)
}
i.User.Registration = account
}
issued, err := client.Certificate.Obtain(ctx, legoCertificate.ObtainRequest{
Domains: domains, EmailAddresses: nonEmptyEmail(request.AccountEmail, i.User.Email),
KeyType: certcrypto.RSA2048, Bundle: true,
PreferredChain: request.PreferredChain,
})
if err != nil {
return Resource{}, fmt.Errorf("申请证书失败:%w", err)
}
resource := Resource{
Domains: issued.Domains, Certificate: issued.Certificate,
IssuerCertificate: issued.IssuerCertificate, PrivateKey: issued.PrivateKey,
}
if i.Now != nil {
_ = i.Now()
}
return ValidateResource(resource)
}
func nonEmptyEmail(values ...string) []string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return []string{strings.TrimSpace(value)}
}
}
return nil
}