feat: support ARI

This commit is contained in:
Fu Diwei
2025-01-20 02:27:59 +08:00
parent 11d654e902
commit fa8ba061fb
9 changed files with 100 additions and 63 deletions

View File

@@ -14,10 +14,11 @@ import (
"github.com/go-acme/lego/v4/challenge"
"github.com/go-acme/lego/v4/challenge/dns01"
"github.com/go-acme/lego/v4/lego"
"golang.org/x/exp/slices"
"golang.org/x/time/rate"
"github.com/usual2970/certimate/internal/domain"
"github.com/usual2970/certimate/internal/pkg/utils/slices"
uslices "github.com/usual2970/certimate/internal/pkg/utils/slices"
"github.com/usual2970/certimate/internal/repository"
)
@@ -45,7 +46,9 @@ type applicantOptions struct {
DnsPropagationTimeout int32
DnsTTL int32
DisableFollowCNAME bool
DisableARI bool
SkipBeforeExpiryDays int32
ReplacedARICertId string
}
func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
@@ -54,32 +57,49 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) {
}
nodeConfig := node.GetConfigForApply()
accessRepo := repository.NewAccessRepository()
access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId)
if err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
}
accessConfig, err := access.UnmarshalConfigToMap()
if err != nil {
return nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
options := &applicantOptions{
Domains: slices.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
Domains: uslices.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }),
ContactEmail: nodeConfig.ContactEmail,
Provider: domain.ApplyDNSProviderType(nodeConfig.Provider),
ProviderAccessConfig: accessConfig,
ProviderApplyConfig: nodeConfig.ProviderConfig,
KeyAlgorithm: nodeConfig.KeyAlgorithm,
Nameservers: slices.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
Nameservers: uslices.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }),
DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout,
DnsTTL: nodeConfig.DnsTTL,
DisableFollowCNAME: nodeConfig.DisableFollowCNAME,
DisableARI: nodeConfig.DisableARI,
SkipBeforeExpiryDays: nodeConfig.SkipBeforeExpiryDays,
}
accessRepo := repository.NewAccessRepository()
if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err)
} else {
accessConfig, err := access.UnmarshalConfigToMap()
if err != nil {
return nil, fmt.Errorf("failed to unmarshal access config: %w", err)
}
options.ProviderAccessConfig = accessConfig
}
certRepo := repository.NewCertificateRepository()
lastCertificate, _ := certRepo.GetByWorkflowNodeId(context.Background(), node.Id)
if lastCertificate != nil {
newCertSan := slices.Clone(options.Domains)
oldCertSan := strings.Split(lastCertificate.SubjectAltNames, ";")
slices.Sort(newCertSan)
slices.Sort(oldCertSan)
if slices.Equal(newCertSan, oldCertSan) {
lastCertX509, _ := certcrypto.ParsePEMCertificate([]byte(lastCertificate.Certificate))
if lastCertX509 != nil {
replacedARICertId, _ := certificate.MakeARICertID(lastCertX509)
options.ReplacedARICertId = replacedARICertId
}
}
}
applicant, err := createApplicant(options)
if err != nil {
return nil, err
@@ -109,7 +129,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
sslProviderConfig.Provider = defaultSSLProvider
}
myUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail)
acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail)
if err != nil {
return nil, err
}
@@ -118,39 +138,42 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
// link: https://github.com/go-acme/lego/issues/1867
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME))
config := lego.NewConfig(myUser)
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM.
// Create an ACME client config
config := lego.NewConfig(acmeUser)
config.CADirURL = sslProviderUrls[sslProviderConfig.Provider]
config.Certificate.KeyType = parseKeyAlgorithm(options.KeyAlgorithm)
// A client facilitates communication with the CA server.
// Create an ACME client
client, err := lego.NewClient(config)
if err != nil {
return nil, err
}
// Set the DNS01 challenge provider
challengeOptions := make([]dns01.ChallengeOption, 0)
if len(options.Nameservers) > 0 {
challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(dns01.ParseNameservers(options.Nameservers)))
challengeOptions = append(challengeOptions, dns01.DisableAuthoritativeNssPropagationRequirement())
}
client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...)
// New users will need to register
if !myUser.hasRegistration() {
reg, err := registerAcmeUser(client, sslProviderConfig, myUser)
// New users need to register first
if !acmeUser.hasRegistration() {
reg, err := registerAcmeUser(client, sslProviderConfig, acmeUser)
if err != nil {
return nil, fmt.Errorf("failed to register: %w", err)
}
myUser.Registration = reg
acmeUser.Registration = reg
}
// Obtain a certificate
certRequest := certificate.ObtainRequest{
Domains: options.Domains,
Bundle: true,
}
if !options.DisableARI {
certRequest.ReplacesCertID = options.ReplacedARICertId
}
certResource, err := client.Certificate.Obtain(certRequest)
if err != nil {
return nil, err
@@ -162,7 +185,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
ACMECertUrl: certResource.CertURL,
ACMECertStableUrl: certResource.CertStableURL,
CSR: string(certResource.CSR),
CSR: strings.TrimSpace(string(certResource.CSR)),
}, nil
}