mirror of https://github.com/usual2970/certimate
				
				
				
			feat: letsencrypt staging environment
							parent
							
								
									87e1749553
								
							
						
					
					
						commit
						155371cdd0
					
				|  | @ -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"` | ||||
| } | ||||
|  |  | |||
|  | @ -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) | ||||
| 	} | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -133,13 +133,11 @@ export const notifyChannelsMap: Map<NotifyChannel["type"], NotifyChannel> = 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<string, unknown> | 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; | ||||
|   }; | ||||
| }; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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.<br><br>Learn more:<br><a href=\"https://letsencrypt.org/docs/staging-environment/\" target=\"_blank\">https://letsencrypt.org/docs/staging-environment/</a>", | ||||
|   "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 <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>", | ||||
|  |  | |||
|  | @ -72,7 +72,13 @@ | |||
|   "settings.notification.channel.form.wecom_webhook_url.tooltip": "这是什么?请参阅 <a href=\"https://open.work.weixin.qq.com/help2/pc/18401#%E5%85%AD%E3%80%81%E7%BE%A4%E6%9C%BA%E5%99%A8%E4%BA%BAWebhook%E5%9C%B0%E5%9D%80\" target=\"_blank\">https://open.work.weixin.qq.com/help2/pc/18401</a>", | ||||
| 
 | ||||
|   "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": "测试环境比生产环境有更宽松的速率限制,可进行测试性部署。<br><br>点击下方链接了解更多:<br><a href=\"https://letsencrypt.org/zh-cn/docs/staging-environment/\" target=\"_blank\">https://letsencrypt.org/zh-cn/docs/staging-environment/</a>", | ||||
|   "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": "这是什么?请参阅 <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>", | ||||
|  |  | |||
|  | @ -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<NonNullable<unknown>>({ | ||||
|     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 ( | ||||
|     <Form {...formProps} form={formInst} disabled={pending} layout="vertical" onValuesChange={handleFormChange}> | ||||
|       <Form.Item> | ||||
|         <Alert type="info" message={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.letsencrypt_staging_alert") }}></span>} /> | ||||
|       </Form.Item> | ||||
| 
 | ||||
|       <Form.Item> | ||||
|         <Alert type="warning" message={<span dangerouslySetInnerHTML={{ __html: t("settings.sslprovider.form.letsencrypt_staging_warning") }}></span>} /> | ||||
|       </Form.Item> | ||||
| 
 | ||||
|       <Form.Item> | ||||
|         <Button type="primary" htmlType="submit" disabled={!formChanged} loading={pending}> | ||||
|           {t("common.button.save")} | ||||
|         </Button> | ||||
|       </Form.Item> | ||||
|     </Form> | ||||
|   ); | ||||
| }; | ||||
| 
 | ||||
| const SSLProviderEditFormZeroSSLConfig = () => { | ||||
|   const { t } = useTranslation(); | ||||
| 
 | ||||
|  | @ -231,6 +280,8 @@ const SettingsSSLProvider = () => { | |||
|     switch (providerType) { | ||||
|       case SSLPROVIDERS.LETS_ENCRYPT: | ||||
|         return <SSLProviderEditFormLetsEncryptConfig />; | ||||
|       case SSLPROVIDERS.LETS_ENCRYPT_STAGING: | ||||
|         return <SSLProviderEditFormLetsEncryptStagingConfig />; | ||||
|       case SSLPROVIDERS.ZERO_SSL: | ||||
|         return <SSLProviderEditFormZeroSSLConfig />; | ||||
|       case SSLPROVIDERS.GOOGLE_TRUST_SERVICES: | ||||
|  | @ -272,14 +323,25 @@ const SettingsSSLProvider = () => { | |||
|               <CheckCard | ||||
|                 avatar={<img src={"/imgs/acme/letsencrypt.svg"} className="size-8" />} | ||||
|                 size="small" | ||||
|                 title="Let's Encrypt" | ||||
|                 title={t("settings.sslprovider.form.provider.option.letsencrypt.label")} | ||||
|                 value={SSLPROVIDERS.LETS_ENCRYPT} | ||||
|               /> | ||||
|               <CheckCard avatar={<img src={"/imgs/acme/zerossl.svg"} className="size-8" />} size="small" title="ZeroSSL" value={SSLPROVIDERS.ZERO_SSL} /> | ||||
|               <CheckCard | ||||
|                 avatar={<img src={"/imgs/acme/letsencrypt.svg"} className="size-8" />} | ||||
|                 size="small" | ||||
|                 title={t("settings.sslprovider.form.provider.option.letsencrypt_staging.label")} | ||||
|                 value={SSLPROVIDERS.LETS_ENCRYPT_STAGING} | ||||
|               /> | ||||
|               <CheckCard | ||||
|                 avatar={<img src={"/imgs/acme/zerossl.svg"} className="size-8" />} | ||||
|                 size="small" | ||||
|                 title={t("settings.sslprovider.form.provider.option.zerossl.label")} | ||||
|                 value={SSLPROVIDERS.ZERO_SSL} | ||||
|               /> | ||||
|               <CheckCard | ||||
|                 avatar={<img src={"/imgs/acme/google.svg"} className="size-8" />} | ||||
|                 size="small" | ||||
|                 title="Google Trust Services" | ||||
|                 title={t("settings.sslprovider.form.provider.option.gts.label")} | ||||
|                 value={SSLPROVIDERS.GOOGLE_TRUST_SERVICES} | ||||
|               /> | ||||
|             </CheckCard.Group> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Fu Diwei
						Fu Diwei