diff --git a/internal/applicant/acme-ca.go b/internal/applicant/acme-ca.go index ce6e3006..57f24b6e 100644 --- a/internal/applicant/acme-ca.go +++ b/internal/applicant/acme-ca.go @@ -1,22 +1,25 @@ package applicant -const defaultSSLProvider = "letsencrypt" const ( - sslProviderLetsencrypt = "letsencrypt" - sslProviderZeroSSL = "zerossl" - sslProviderGts = "gts" + sslProviderLetsEncrypt = "letsencrypt" + sslProviderLetsEncryptStaging = "letsencrypt_staging" + sslProviderZeroSSL = "zerossl" + sslProviderGoogleTrustServices = "gts" ) +const defaultSSLProvider = sslProviderLetsEncrypt 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" + letsencryptUrl = "https://acme-v02.api.letsencrypt.org/directory" + letsencryptStagingUrl = "https://acme-staging-v02.api.letsencrypt.org/directory" + zerosslUrl = "https://acme.zerossl.com/v2/DV90" + gtsUrl = "https://dv.acme-v02.api.pki.goog/directory" ) var sslProviderUrls = map[string]string{ - sslProviderLetsencrypt: letsencryptUrl, - sslProviderZeroSSL: zerosslUrl, - sslProviderGts: gtsUrl, + sslProviderLetsEncrypt: letsencryptUrl, + sslProviderLetsEncryptStaging: letsencryptStagingUrl, + sslProviderZeroSSL: zerosslUrl, + sslProviderGoogleTrustServices: gtsUrl, } type acmeSSLProviderConfig struct { @@ -25,11 +28,11 @@ type acmeSSLProviderConfig struct { } type acmeSSLProviderConfigContent struct { - Zerossl acmeSSLProviderEab `json:"zerossl"` - Gts acmeSSLProviderEab `json:"gts"` + ZeroSSL acmeSSLProviderEabConfig `json:"zerossl"` + GoogleTrustServices acmeSSLProviderEabConfig `json:"gts"` } -type acmeSSLProviderEab struct { +type acmeSSLProviderEabConfig struct { EabHmacKey string `json:"eabHmacKey"` EabKid string `json:"eabKid"` } diff --git a/internal/applicant/acme-user.go b/internal/applicant/acme-user.go index dd3de89f..11aa1fff 100644 --- a/internal/applicant/acme-user.go +++ b/internal/applicant/acme-user.go @@ -5,7 +5,6 @@ import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" - "errors" "fmt" "github.com/go-acme/lego/v4/lego" @@ -39,12 +38,12 @@ func newAcmeUser(ca, email string) (*acmeUser, error) { return nil, err } - keyStr, err := x509.ConvertECPrivateKeyToPEM(key) + keyPEM, err := x509.ConvertECPrivateKeyToPEM(key) if err != nil { return nil, err } - applyUser.privkey = keyStr + applyUser.privkey = keyPEM return applyUser, nil } @@ -80,30 +79,30 @@ type acmeAccountRepository interface { Save(ca, email, key string, resource *registration.Resource) error } -func registerAcmeUser(client *lego.Client, sslProvider *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { +func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { // TODO: fix 潜在的并发问题 var reg *registration.Resource var err error - switch sslProvider.Provider { + switch sslProviderConfig.Provider { case sslProviderZeroSSL: reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ TermsOfServiceAgreed: true, - Kid: sslProvider.Config.Zerossl.EabKid, - HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey, + Kid: sslProviderConfig.Config.ZeroSSL.EabKid, + HmacEncoded: sslProviderConfig.Config.ZeroSSL.EabHmacKey, }) - case sslProviderGts: + case sslProviderGoogleTrustServices: reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ TermsOfServiceAgreed: true, - Kid: sslProvider.Config.Gts.EabKid, - HmacEncoded: sslProvider.Config.Gts.EabHmacKey, + Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid, + HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey, }) - case sslProviderLetsencrypt: + case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging: reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) default: - err = errors.New("unknown ssl provider") + err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider) } if err != nil { @@ -112,13 +111,13 @@ func registerAcmeUser(client *lego.Client, sslProvider *acmeSSLProviderConfig, u repo := repository.NewAcmeAccountRepository() - resp, err := repo.GetByCAAndEmail(sslProvider.Provider, user.GetEmail()) + resp, err := repo.GetByCAAndEmail(sslProviderConfig.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 { + if err := repo.Save(sslProviderConfig.Provider, user.GetEmail(), user.getPrivateKeyPEM(), reg); err != nil { return nil, fmt.Errorf("failed to save registration: %w", err) } diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 399bfa5f..8f32766e 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -77,29 +77,33 @@ func apply(challengeProvider challenge.Provider, applyConfig *applyConfig) (*App settingsRepo := repository.NewSettingsRepository() settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider") - sslProvider := &acmeSSLProviderConfig{ + sslProviderConfig := &acmeSSLProviderConfig{ Config: acmeSSLProviderConfigContent{}, Provider: defaultSSLProvider, } if settings != nil { - if err := json.Unmarshal([]byte(settings.Content), sslProvider); err != nil { + if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil { return nil, err } } + if sslProviderConfig.Provider == "" { + sslProviderConfig.Provider = defaultSSLProvider + } + + myUser, err := newAcmeUser(sslProviderConfig.Provider, applyConfig.ContactEmail) + if err != nil { + return nil, err + } + // 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(applyConfig.DisableFollowCNAME)) - myUser, err := newAcmeUser(sslProvider.Provider, applyConfig.ContactEmail) - if err != nil { - return nil, err - } - config := lego.NewConfig(myUser) // This CA URL is configured for a local dev instance of Boulder running in Docker in a VM. - config.CADirURL = sslProviderUrls[sslProvider.Provider] + config.CADirURL = sslProviderUrls[sslProviderConfig.Provider] config.Certificate.KeyType = parseKeyAlgorithm(applyConfig.KeyAlgorithm) // A client facilitates communication with the CA server. @@ -118,29 +122,29 @@ func apply(challengeProvider challenge.Provider, applyConfig *applyConfig) (*App // New users will need to register if !myUser.hasRegistration() { - reg, err := registerAcmeUser(client, sslProvider, myUser) + reg, err := registerAcmeUser(client, sslProviderConfig, myUser) if err != nil { return nil, fmt.Errorf("failed to register: %w", err) } myUser.Registration = reg } - request := certificate.ObtainRequest{ + certRequest := certificate.ObtainRequest{ Domains: strings.Split(applyConfig.Domains, ";"), Bundle: true, } - certificates, err := client.Certificate.Obtain(request) + certResource, err := client.Certificate.Obtain(certRequest) if err != nil { return nil, err } return &ApplyCertResult{ - PrivateKey: string(certificates.PrivateKey), - Certificate: string(certificates.Certificate), - IssuerCertificate: string(certificates.IssuerCertificate), - CSR: string(certificates.CSR), - ACMECertUrl: certificates.CertURL, - ACMECertStableUrl: certificates.CertStableURL, + PrivateKey: string(certResource.PrivateKey), + Certificate: string(certResource.Certificate), + IssuerCertificate: string(certResource.IssuerCertificate), + CSR: string(certResource.CSR), + ACMECertUrl: certResource.CertURL, + ACMECertStableUrl: certResource.CertStableURL, }, nil } diff --git a/ui/src/domain/settings.ts b/ui/src/domain/settings.ts index a27e61e1..9df3dc05 100644 --- a/ui/src/domain/settings.ts +++ b/ui/src/domain/settings.ts @@ -133,13 +133,11 @@ export const notifyChannelsMap: Map = new // #endregion // #region Settings: SSLProvider -export const SSLPROVIDER_LETSENCRYPT = "letsencrypt" as const; -export const SSLPROVIDER_ZEROSSL = "zerossl" as const; -export const SSLPROVIDER_GOOGLETRUSTSERVICES = "gts" as const; export const SSLPROVIDERS = Object.freeze({ - LETS_ENCRYPT: SSLPROVIDER_LETSENCRYPT, - ZERO_SSL: SSLPROVIDER_ZEROSSL, - GOOGLE_TRUST_SERVICES: SSLPROVIDER_GOOGLETRUSTSERVICES, + LETS_ENCRYPT: "letsencrypt", + LETS_ENCRYPT_STAGING: "letsencrypt_staging", + ZERO_SSL: "zerossl", + GOOGLE_TRUST_SERVICES: "gts", } as const); export type SSLProviders = (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS]; @@ -148,9 +146,10 @@ export type SSLProviderSettingsContent = { provider: (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS]; config: { [key: string]: Record | undefined; - letsencrypt?: SSLProviderLetsEncryptConfig; - zerossl?: SSLProviderZeroSSLConfig; - gts?: SSLProviderGoogleTrustServicesConfig; + [SSLPROVIDERS.LETS_ENCRYPT]?: SSLProviderLetsEncryptConfig; + [SSLPROVIDERS.LETS_ENCRYPT_STAGING]?: SSLProviderLetsEncryptConfig; + [SSLPROVIDERS.ZERO_SSL]?: SSLProviderZeroSSLConfig; + [SSLPROVIDERS.GOOGLE_TRUST_SERVICES]?: SSLProviderGoogleTrustServicesConfig; }; }; diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index 7778369c..68b93ec5 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -73,6 +73,12 @@ "settings.sslprovider.tab": "Certificate authority", "settings.sslprovider.form.provider.label": "ACME provider", + "settings.sslprovider.form.provider.option.letsencrypt.label": "Let's Encrypt", + "settings.sslprovider.form.provider.option.letsencrypt_staging.label": "Let's Encrypt Staging Environment", + "settings.sslprovider.form.provider.option.zerossl.label": "ZeroSSL", + "settings.sslprovider.form.provider.option.gts.label": "Google Trust Services", + "settings.sslprovider.form.letsencrypt_staging_alert": "The staging environment can reduce the chance of your running up against rate limits.

Learn more:
https://letsencrypt.org/docs/staging-environment/", + "settings.sslprovider.form.letsencrypt_staging_warning": "Attention: Certificates from the staging environment are only for testing purposes.", "settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID", "settings.sslprovider.form.zerossl_eab_kid.placeholder": "Please enter EAB KID", "settings.sslprovider.form.zerossl_eab_kid.tooltip": "For more information, see https://zerossl.com/documentation/acme/", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index 50b871c4..c8d4703a 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -72,7 +72,13 @@ "settings.notification.channel.form.wecom_webhook_url.tooltip": "这是什么?请参阅 https://open.work.weixin.qq.com/help2/pc/18401", "settings.sslprovider.tab": "证书颁发机构(CA)", - "settings.sslprovider.form.provider.label": "ACME 提供商", + "settings.sslprovider.form.provider.label": "ACME 服务商", + "settings.sslprovider.form.provider.option.letsencrypt.label": "Let's Encrypt", + "settings.sslprovider.form.provider.option.letsencrypt_staging.label": "Let's Encrypt 测试环境", + "settings.sslprovider.form.provider.option.zerossl.label": "ZeroSSL", + "settings.sslprovider.form.provider.option.gts.label": "Google Trust Services", + "settings.sslprovider.form.letsencrypt_staging_alert": "测试环境比生产环境有更宽松的速率限制,可进行测试性部署。

点击下方链接了解更多:
https://letsencrypt.org/zh-cn/docs/staging-environment/", + "settings.sslprovider.form.letsencrypt_staging_warning": "警告:测试环境证书仅能用于测试目的。", "settings.sslprovider.form.zerossl_eab_kid.label": "EAB KID", "settings.sslprovider.form.zerossl_eab_kid.placeholder": "请输入 EAB KID", "settings.sslprovider.form.zerossl_eab_kid.tooltip": "这是什么?请参阅 https://zerossl.com/documentation/acme/", diff --git a/ui/src/pages/settings/SettingsSSLProvider.tsx b/ui/src/pages/settings/SettingsSSLProvider.tsx index 1314b086..c3a550f0 100644 --- a/ui/src/pages/settings/SettingsSSLProvider.tsx +++ b/ui/src/pages/settings/SettingsSSLProvider.tsx @@ -1,7 +1,7 @@ import { createContext, useContext, useEffect, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import { CheckCard } from "@ant-design/pro-components"; -import { Button, Form, Input, Skeleton, message, notification } from "antd"; +import { Alert, Button, Form, Input, Skeleton, message, notification } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { produce } from "immer"; import { z } from "zod"; @@ -61,6 +61,55 @@ const SSLProviderEditFormLetsEncryptConfig = () => { ); }; +const SSLProviderEditFormLetsEncryptStagingConfig = () => { + const { t } = useTranslation(); + + const { pending, settings, updateSettings } = useContext(SSLProviderContext); + + const { form: formInst, formProps } = useAntdForm>({ + initialValues: settings?.content?.config?.[SSLPROVIDERS.LETS_ENCRYPT_STAGING], + onSubmit: async (values) => { + const newSettings = produce(settings, (draft) => { + draft.content ??= {} as SSLProviderSettingsContent; + draft.content.provider = SSLPROVIDERS.LETS_ENCRYPT_STAGING; + + draft.content.config ??= {} as SSLProviderSettingsContent["config"]; + draft.content.config[SSLPROVIDERS.LETS_ENCRYPT_STAGING] = values; + }); + await updateSettings(newSettings); + + setFormChanged(false); + }, + }); + + const [formChanged, setFormChanged] = useState(false); + useEffect(() => { + setFormChanged(settings?.content?.provider !== SSLPROVIDERS.LETS_ENCRYPT_STAGING); + }, [settings?.content?.provider]); + + const handleFormChange = () => { + setFormChanged(true); + }; + + return ( +
+ + } /> + + + + } /> + + + + + +
+ ); +}; + const SSLProviderEditFormZeroSSLConfig = () => { const { t } = useTranslation(); @@ -231,6 +280,8 @@ const SettingsSSLProvider = () => { switch (providerType) { case SSLPROVIDERS.LETS_ENCRYPT: return ; + case SSLPROVIDERS.LETS_ENCRYPT_STAGING: + return ; case SSLPROVIDERS.ZERO_SSL: return ; case SSLPROVIDERS.GOOGLE_TRUST_SERVICES: @@ -272,14 +323,25 @@ const SettingsSSLProvider = () => { } size="small" - title="Let's Encrypt" + title={t("settings.sslprovider.form.provider.option.letsencrypt.label")} value={SSLPROVIDERS.LETS_ENCRYPT} /> - } size="small" title="ZeroSSL" value={SSLPROVIDERS.ZERO_SSL} /> + } + size="small" + title={t("settings.sslprovider.form.provider.option.letsencrypt_staging.label")} + value={SSLPROVIDERS.LETS_ENCRYPT_STAGING} + /> + } + size="small" + title={t("settings.sslprovider.form.provider.option.zerossl.label")} + value={SSLPROVIDERS.ZERO_SSL} + /> } size="small" - title="Google Trust Services" + title={t("settings.sslprovider.form.provider.option.gts.label")} value={SSLPROVIDERS.GOOGLE_TRUST_SERVICES} />