diff --git a/internal/applicant/acme-ca.go b/internal/applicant/acme-ca.go new file mode 100644 index 00000000..ce6e3006 --- /dev/null +++ b/internal/applicant/acme-ca.go @@ -0,0 +1,35 @@ +package applicant + +const defaultSSLProvider = "letsencrypt" +const ( + sslProviderLetsencrypt = "letsencrypt" + sslProviderZeroSSL = "zerossl" + sslProviderGts = "gts" +) + +const ( + zerosslUrl = "https://acme.zerossl.com/v2/DV90" + letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory" + gtsUrl = "https://dv.acme-v02.api.pki.goog/directory" +) + +var sslProviderUrls = map[string]string{ + sslProviderLetsencrypt: letsencryptUrl, + sslProviderZeroSSL: zerosslUrl, + sslProviderGts: gtsUrl, +} + +type acmeSSLProviderConfig struct { + Config acmeSSLProviderConfigContent `json:"config"` + Provider string `json:"provider"` +} + +type acmeSSLProviderConfigContent struct { + Zerossl acmeSSLProviderEab `json:"zerossl"` + Gts acmeSSLProviderEab `json:"gts"` +} + +type acmeSSLProviderEab struct { + EabHmacKey string `json:"eabHmacKey"` + EabKid string `json:"eabKid"` +} diff --git a/internal/applicant/acme-user.go b/internal/applicant/acme-user.go new file mode 100644 index 00000000..7d3e5d10 --- /dev/null +++ b/internal/applicant/acme-user.go @@ -0,0 +1,126 @@ +package applicant + +import ( + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "errors" + "fmt" + + "github.com/go-acme/lego/v4/lego" + "github.com/go-acme/lego/v4/registration" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/utils/x509" + "github.com/usual2970/certimate/internal/repository" +) + +type acmeUser struct { + CA string + Email string + Registration *registration.Resource + + privkey string +} + +func newAcmeUser(ca, email string) (*acmeUser, error) { + repo := repository.NewAcmeAccountRepository() + + applyUser := &acmeUser{ + CA: ca, + Email: email, + } + + acmeAccount, err := repo.GetByCAAndEmail(ca, email) + if err != nil { + key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, err + } + + keyStr, err := x509.ConvertECPrivateKeyToPEM(key) + if err != nil { + return nil, err + } + + applyUser.privkey = keyStr + return applyUser, nil + } + + applyUser.Registration = acmeAccount.Resource + applyUser.privkey = acmeAccount.Key + + return applyUser, nil +} + +func (u *acmeUser) GetEmail() string { + return u.Email +} + +func (u acmeUser) GetRegistration() *registration.Resource { + return u.Registration +} + +func (u *acmeUser) GetPrivateKey() crypto.PrivateKey { + rs, _ := x509.ParseECPrivateKeyFromPEM(u.privkey) + return rs +} + +func (u *acmeUser) hasRegistration() bool { + return u.Registration != nil +} + +func (u *acmeUser) getPrivateKeyPEM() string { + return u.privkey +} + +type AcmeAccountRepository interface { + GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error) + Save(ca, email, key string, resource *registration.Resource) error +} + +func registerAcmeUser(client *lego.Client, sslProvider *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { + // TODO: fix 潜在的并发问题 + + var reg *registration.Resource + var err error + switch sslProvider.Provider { + case sslProviderZeroSSL: + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: sslProvider.Config.Zerossl.EabKid, + HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey, + }) + case sslProviderGts: + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: sslProvider.Config.Gts.EabKid, + HmacEncoded: sslProvider.Config.Gts.EabHmacKey, + }) + + case sslProviderLetsencrypt: + reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + + default: + err = errors.New("unknown ssl provider") + } + + if err != nil { + return nil, err + } + + repo := repository.NewAcmeAccountRepository() + + resp, err := repo.GetByCAAndEmail(sslProvider.Provider, user.GetEmail()) + if err == nil { + user.privkey = resp.Key + return resp.Resource, nil + } + + if err := repo.Save(sslProvider.Provider, user.GetEmail(), user.getPrivateKeyPEM(), reg); err != nil { + return nil, fmt.Errorf("failed to save registration: %w", err) + } + + return reg, nil +} diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index ed02a9f3..be6db02d 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -2,11 +2,6 @@ package applicant import ( "context" - "crypto" - "crypto/ecdsa" - "crypto/elliptic" - "crypto/rand" - "errors" "fmt" "os" "strconv" @@ -17,126 +12,36 @@ import ( "github.com/go-acme/lego/v4/challenge" "github.com/go-acme/lego/v4/challenge/dns01" "github.com/go-acme/lego/v4/lego" - "github.com/go-acme/lego/v4/registration" "github.com/usual2970/certimate/internal/app" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/pkg/utils/x509" "github.com/usual2970/certimate/internal/repository" ) -const defaultSSLProvider = "letsencrypt" -const ( - sslProviderLetsencrypt = "letsencrypt" - sslProviderZeroSSL = "zerossl" - sslProviderGts = "gts" -) - -const ( - zerosslUrl = "https://acme.zerossl.com/v2/DV90" - letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory" - gtsUrl = "https://dv.acme-v02.api.pki.goog/directory" -) - -var sslProviderUrls = map[string]string{ - sslProviderLetsencrypt: letsencryptUrl, - sslProviderZeroSSL: zerosslUrl, - sslProviderGts: gtsUrl, -} - -type Certificate struct { - CertUrl string `json:"certUrl"` - CertStableUrl string `json:"certStableUrl"` - PrivateKey string `json:"privateKey"` - Certificate string `json:"certificate"` - IssuerCertificate string `json:"issuerCertificate"` - CSR string `json:"csr"` -} - type applyConfig struct { - Domains string `json:"domains"` - ContactEmail string `json:"contactEmail"` - AccessConfig string `json:"accessConfig"` - KeyAlgorithm string `json:"keyAlgorithm"` - Nameservers string `json:"nameservers"` - PropagationTimeout int32 `json:"propagationTimeout"` - DisableFollowCNAME bool `json:"disableFollowCNAME"` + Domains string + ContactEmail string + AccessConfig string + KeyAlgorithm string + Nameservers string + PropagationTimeout int32 + DisableFollowCNAME bool } -type applyUser struct { - CA string - Email string - Registration *registration.Resource - - privkey string +type ApplyResult struct { + PrivateKey string + Certificate string + IssuerCertificate string + ACMECertUrl string + ACMECertStableUrl string + CSR string } -func newApplyUser(ca, email string) (*applyUser, error) { - repo := getAcmeAccountRepository() - - applyUser := &applyUser{ - CA: ca, - Email: email, - } - - acmeAccount, err := repo.GetByCAAndEmail(ca, email) - if err != nil { - key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - return nil, err - } - - keyStr, err := x509.ConvertECPrivateKeyToPEM(key) - if err != nil { - return nil, err - } - - applyUser.privkey = keyStr - return applyUser, nil - } - - applyUser.Registration = acmeAccount.Resource - applyUser.privkey = acmeAccount.Key - - return applyUser, nil +type applicant interface { + Apply() (*ApplyResult, error) } -func (u *applyUser) GetEmail() string { - return u.Email -} - -func (u applyUser) GetRegistration() *registration.Resource { - return u.Registration -} - -func (u *applyUser) GetPrivateKey() crypto.PrivateKey { - rs, _ := x509.ParseECPrivateKeyFromPEM(u.privkey) - return rs -} - -func (u *applyUser) hasRegistration() bool { - return u.Registration != nil -} - -func (u *applyUser) getPrivateKeyString() string { - return u.privkey -} - -type Applicant interface { - Apply() (*Certificate, error) -} - -// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑 -type proxyApplicant struct { - applyConfig *applyConfig - applicant challenge.Provider -} - -func (d *proxyApplicant) Apply() (*Certificate, error) { - return apply(d.applyConfig, d.applicant) -} - -func GetWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { +func NewWithApplyNode(node *domain.WorkflowNode) (applicant, error) { // 获取授权配置 accessRepo := repository.NewAccessRepository() @@ -161,31 +66,16 @@ func GetWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { } return &proxyApplicant{ - applyConfig: applyConfig, applicant: challengeProvider, + applyConfig: applyConfig, }, nil } -type SSLProviderConfig struct { - Config SSLProviderConfigContent `json:"config"` - Provider string `json:"provider"` -} - -type SSLProviderConfigContent struct { - Zerossl SSLProviderEab `json:"zerossl"` - Gts SSLProviderEab `json:"gts"` -} - -type SSLProviderEab struct { - EabHmacKey string `json:"eabHmacKey"` - EabKid string `json:"eabKid"` -} - -func apply(option *applyConfig, provider challenge.Provider) (*Certificate, error) { +func apply(challengeProvider challenge.Provider, applyConfig *applyConfig) (*ApplyResult, error) { record, _ := app.GetApp().Dao().FindFirstRecordByFilter("settings", "name='sslProvider'") - sslProvider := &SSLProviderConfig{ - Config: SSLProviderConfigContent{}, + sslProvider := &acmeSSLProviderConfig{ + Config: acmeSSLProviderConfigContent{}, Provider: defaultSSLProvider, } if record != nil { @@ -196,9 +86,9 @@ func apply(option *applyConfig, provider challenge.Provider) (*Certificate, erro // Some unified lego environment variables are configured here. // link: https://github.com/go-acme/lego/issues/1867 - os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(option.DisableFollowCNAME)) + os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(applyConfig.DisableFollowCNAME)) - myUser, err := newApplyUser(sslProvider.Provider, option.ContactEmail) + myUser, err := newAcmeUser(sslProvider.Provider, applyConfig.ContactEmail) if err != nil { return nil, err } @@ -207,7 +97,7 @@ func apply(option *applyConfig, provider challenge.Provider) (*Certificate, erro // This CA URL is configured for a local dev instance of Boulder running in Docker in a VM. config.CADirURL = sslProviderUrls[sslProvider.Provider] - config.Certificate.KeyType = parseKeyAlgorithm(option.KeyAlgorithm) + config.Certificate.KeyType = parseKeyAlgorithm(applyConfig.KeyAlgorithm) // A client facilitates communication with the CA server. client, err := lego.NewClient(config) @@ -216,25 +106,24 @@ func apply(option *applyConfig, provider challenge.Provider) (*Certificate, erro } challengeOptions := make([]dns01.ChallengeOption, 0) - if len(option.Nameservers) > 0 { - challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(dns01.ParseNameservers(strings.Split(option.Nameservers, ";")))) + if len(applyConfig.Nameservers) > 0 { + challengeOptions = append(challengeOptions, dns01.AddRecursiveNameservers(dns01.ParseNameservers(strings.Split(applyConfig.Nameservers, ";")))) challengeOptions = append(challengeOptions, dns01.DisableAuthoritativeNssPropagationRequirement()) } - client.Challenge.SetDNS01Provider(provider, challengeOptions...) + client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...) // New users will need to register if !myUser.hasRegistration() { - reg, err := getReg(client, sslProvider, myUser) + reg, err := registerAcmeUser(client, sslProvider, myUser) if err != nil { return nil, fmt.Errorf("failed to register: %w", err) } myUser.Registration = reg } - domains := strings.Split(option.Domains, ";") request := certificate.ObtainRequest{ - Domains: domains, + Domains: strings.Split(applyConfig.Domains, ";"), Bundle: true, } certificates, err := client.Certificate.Obtain(request) @@ -242,70 +131,16 @@ func apply(option *applyConfig, provider challenge.Provider) (*Certificate, erro return nil, err } - return &Certificate{ - CertUrl: certificates.CertURL, - CertStableUrl: certificates.CertStableURL, + return &ApplyResult{ PrivateKey: string(certificates.PrivateKey), Certificate: string(certificates.Certificate), IssuerCertificate: string(certificates.IssuerCertificate), CSR: string(certificates.CSR), + ACMECertUrl: certificates.CertURL, + ACMECertStableUrl: certificates.CertStableURL, }, nil } -type AcmeAccountRepository interface { - GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error) - Save(ca, email, key string, resource *registration.Resource) error -} - -func getAcmeAccountRepository() AcmeAccountRepository { - return repository.NewAcmeAccountRepository() -} - -func getReg(client *lego.Client, sslProvider *SSLProviderConfig, user *applyUser) (*registration.Resource, error) { - // TODO: fix 潜在的并发问题 - - var reg *registration.Resource - var err error - switch sslProvider.Provider { - case sslProviderZeroSSL: - reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ - TermsOfServiceAgreed: true, - Kid: sslProvider.Config.Zerossl.EabKid, - HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey, - }) - case sslProviderGts: - reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ - TermsOfServiceAgreed: true, - Kid: sslProvider.Config.Gts.EabKid, - HmacEncoded: sslProvider.Config.Gts.EabHmacKey, - }) - - case sslProviderLetsencrypt: - reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) - - default: - err = errors.New("unknown ssl provider") - } - - if err != nil { - return nil, err - } - - repo := getAcmeAccountRepository() - - resp, err := repo.GetByCAAndEmail(sslProvider.Provider, user.GetEmail()) - if err == nil { - user.privkey = resp.Key - return resp.Resource, nil - } - - if err := repo.Save(sslProvider.Provider, user.GetEmail(), user.getPrivateKeyString(), reg); err != nil { - return nil, fmt.Errorf("failed to save registration: %w", err) - } - - return reg, nil -} - func parseKeyAlgorithm(algo string) certcrypto.KeyType { switch algo { case "RSA2048": @@ -324,3 +159,13 @@ func parseKeyAlgorithm(algo string) certcrypto.KeyType { return certcrypto.RSA2048 } } + +// TODO: 暂时使用代理模式以兼容之前版本代码,后续重新实现此处逻辑 +type proxyApplicant struct { + applicant challenge.Provider + applyConfig *applyConfig +} + +func (d *proxyApplicant) Apply() (*ApplyResult, error) { + return apply(d.applicant, d.applyConfig) +} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index c62e892f..dfa29dfc 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -15,14 +15,14 @@ type DeployerOption struct { AccessConfig string `json:"accessConfig"` AccessRecord *domain.Access `json:"-"` DeployConfig domain.DeployConfig `json:"deployConfig"` - Certificate applicant.Certificate `json:"certificate"` + Certificate applicant.ApplyResult `json:"certificate"` } type Deployer interface { Deploy(ctx context.Context) error } -func GetWithProviderAndOption(provider string, option *DeployerOption) (Deployer, error) { +func NewWithProviderAndOption(provider string, option *DeployerOption) (Deployer, error) { deployer, logger, err := createDeployer(domain.DeployProviderType(provider), option.AccessRecord.Config, option.DeployConfig.NodeConfig) if err != nil { return nil, err diff --git a/internal/domain/domains.go b/internal/domain/domains.go index 4794d338..ab2d5b58 100644 --- a/internal/domain/domains.go +++ b/internal/domain/domains.go @@ -1,15 +1,5 @@ package domain -// Deprecated: TODO: 即将废弃 -type ApplyConfig struct { - ContactEmail string `json:"contactEmail"` - ProviderAccessId string `json:"providerAccessId"` - KeyAlgorithm string `json:"keyAlgorithm"` - Nameservers string `json:"nameservers"` - PropagationTimeout int32 `json:"propagationTimeout"` - DisableFollowCNAME bool `json:"disableFollowCNAME"` -} - // Deprecated: TODO: 即将废弃 type DeployConfig struct { NodeId string `json:"nodeId"` diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 63d361c4..d214ca63 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -60,7 +60,7 @@ func (a *applyNode) Run(ctx context.Context) error { } // 获取Applicant - applicant, err := applicant.GetWithApplyNode(a.node) + applicant, err := applicant.NewWithApplyNode(a.node) if err != nil { a.AddOutput(ctx, a.node.Name, "获取申请对象失败", err.Error()) return err @@ -101,8 +101,8 @@ func (a *applyNode) Run(ctx context.Context) error { Certificate: applyResult.Certificate, PrivateKey: applyResult.PrivateKey, IssuerCertificate: applyResult.IssuerCertificate, - ACMECertUrl: applyResult.CertUrl, - ACMECertStableUrl: applyResult.CertStableUrl, + ACMECertUrl: applyResult.ACMECertUrl, + ACMECertStableUrl: applyResult.ACMECertStableUrl, EffectAt: certX509.NotBefore, ExpireAt: certX509.NotAfter, WorkflowId: GetWorkflowId(ctx), diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index e43c767e..b2be4ea8 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -70,9 +70,9 @@ func (d *deployNode) Run(ctx context.Context) error { Domains: cert.SubjectAltNames, AccessConfig: access.Config, AccessRecord: access, - Certificate: applicant.Certificate{ - CertUrl: cert.ACMECertUrl, - CertStableUrl: cert.ACMECertStableUrl, + Certificate: applicant.ApplyResult{ + ACMECertUrl: cert.ACMECertUrl, + ACMECertStableUrl: cert.ACMECertStableUrl, PrivateKey: cert.PrivateKey, Certificate: cert.Certificate, IssuerCertificate: cert.IssuerCertificate, @@ -85,7 +85,7 @@ func (d *deployNode) Run(ctx context.Context) error { }, } - deploy, err := deployer.GetWithProviderAndOption(d.node.GetConfigString("provider"), option) + deploy, err := deployer.NewWithProviderAndOption(d.node.GetConfigString("provider"), option) if err != nil { d.AddOutput(ctx, d.node.Name, "获取部署对象失败", err.Error()) return err diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx index ccb94e65..9705c6db 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -2,7 +2,7 @@ import { forwardRef, useImperativeHandle, useMemo } from "react"; import { Form, type FormInstance } from "antd"; import { NOTIFY_CHANNELS, type NotifyChannelsSettingsContent } from "@/domain/settings"; -import { useAntdForm, useAntdFormName } from "@/hooks"; +import { useAntdForm } from "@/hooks"; import NotifyChannelEditFormBarkFields from "./NotifyChannelEditFormBarkFields"; import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalkFields"; diff --git a/ui/src/repository/workflow.ts b/ui/src/repository/workflow.ts index a1ae7bd3..dc16b2ea 100644 --- a/ui/src/repository/workflow.ts +++ b/ui/src/repository/workflow.ts @@ -1,6 +1,6 @@ import { type RecordListOptions } from "pocketbase"; -import { type WorkflowModel, type WorkflowNode } from "@/domain/workflow"; +import { type WorkflowModel } from "@/domain/workflow"; import { getPocketBase } from "./pocketbase"; const COLLECTION_NAME = "workflow";