feat: letsencrypt staging environment

This commit is contained in:
Fu Diwei 2025-01-06 19:45:26 +08:00
parent 87e1749553
commit 155371cdd0
7 changed files with 137 additions and 58 deletions

View File

@ -1,22 +1,25 @@
package applicant package applicant
const defaultSSLProvider = "letsencrypt"
const ( const (
sslProviderLetsencrypt = "letsencrypt" sslProviderLetsEncrypt = "letsencrypt"
sslProviderLetsEncryptStaging = "letsencrypt_staging"
sslProviderZeroSSL = "zerossl" sslProviderZeroSSL = "zerossl"
sslProviderGts = "gts" sslProviderGoogleTrustServices = "gts"
) )
const defaultSSLProvider = sslProviderLetsEncrypt
const ( const (
zerosslUrl = "https://acme.zerossl.com/v2/DV90"
letsencryptUrl = "https://acme-v02.api.letsencrypt.org/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" gtsUrl = "https://dv.acme-v02.api.pki.goog/directory"
) )
var sslProviderUrls = map[string]string{ var sslProviderUrls = map[string]string{
sslProviderLetsencrypt: letsencryptUrl, sslProviderLetsEncrypt: letsencryptUrl,
sslProviderLetsEncryptStaging: letsencryptStagingUrl,
sslProviderZeroSSL: zerosslUrl, sslProviderZeroSSL: zerosslUrl,
sslProviderGts: gtsUrl, sslProviderGoogleTrustServices: gtsUrl,
} }
type acmeSSLProviderConfig struct { type acmeSSLProviderConfig struct {
@ -25,11 +28,11 @@ type acmeSSLProviderConfig struct {
} }
type acmeSSLProviderConfigContent struct { type acmeSSLProviderConfigContent struct {
Zerossl acmeSSLProviderEab `json:"zerossl"` ZeroSSL acmeSSLProviderEabConfig `json:"zerossl"`
Gts acmeSSLProviderEab `json:"gts"` GoogleTrustServices acmeSSLProviderEabConfig `json:"gts"`
} }
type acmeSSLProviderEab struct { type acmeSSLProviderEabConfig struct {
EabHmacKey string `json:"eabHmacKey"` EabHmacKey string `json:"eabHmacKey"`
EabKid string `json:"eabKid"` EabKid string `json:"eabKid"`
} }

View File

@ -5,7 +5,6 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand" "crypto/rand"
"errors"
"fmt" "fmt"
"github.com/go-acme/lego/v4/lego" "github.com/go-acme/lego/v4/lego"
@ -39,12 +38,12 @@ func newAcmeUser(ca, email string) (*acmeUser, error) {
return nil, err return nil, err
} }
keyStr, err := x509.ConvertECPrivateKeyToPEM(key) keyPEM, err := x509.ConvertECPrivateKeyToPEM(key)
if err != nil { if err != nil {
return nil, err return nil, err
} }
applyUser.privkey = keyStr applyUser.privkey = keyPEM
return applyUser, nil return applyUser, nil
} }
@ -80,30 +79,30 @@ type acmeAccountRepository interface {
Save(ca, email, key string, resource *registration.Resource) error 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 潜在的并发问题 // TODO: fix 潜在的并发问题
var reg *registration.Resource var reg *registration.Resource
var err error var err error
switch sslProvider.Provider { switch sslProviderConfig.Provider {
case sslProviderZeroSSL: case sslProviderZeroSSL:
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true, TermsOfServiceAgreed: true,
Kid: sslProvider.Config.Zerossl.EabKid, Kid: sslProviderConfig.Config.ZeroSSL.EabKid,
HmacEncoded: sslProvider.Config.Zerossl.EabHmacKey, HmacEncoded: sslProviderConfig.Config.ZeroSSL.EabHmacKey,
}) })
case sslProviderGts: case sslProviderGoogleTrustServices:
reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
TermsOfServiceAgreed: true, TermsOfServiceAgreed: true,
Kid: sslProvider.Config.Gts.EabKid, Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid,
HmacEncoded: sslProvider.Config.Gts.EabHmacKey, HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey,
}) })
case sslProviderLetsencrypt: case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
default: default:
err = errors.New("unknown ssl provider") err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider)
} }
if err != nil { if err != nil {
@ -112,13 +111,13 @@ func registerAcmeUser(client *lego.Client, sslProvider *acmeSSLProviderConfig, u
repo := repository.NewAcmeAccountRepository() repo := repository.NewAcmeAccountRepository()
resp, err := repo.GetByCAAndEmail(sslProvider.Provider, user.GetEmail()) resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail())
if err == nil { if err == nil {
user.privkey = resp.Key user.privkey = resp.Key
return resp.Resource, nil 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) return nil, fmt.Errorf("failed to save registration: %w", err)
} }

View File

@ -77,29 +77,33 @@ func apply(challengeProvider challenge.Provider, applyConfig *applyConfig) (*App
settingsRepo := repository.NewSettingsRepository() settingsRepo := repository.NewSettingsRepository()
settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider") settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider")
sslProvider := &acmeSSLProviderConfig{ sslProviderConfig := &acmeSSLProviderConfig{
Config: acmeSSLProviderConfigContent{}, Config: acmeSSLProviderConfigContent{},
Provider: defaultSSLProvider, Provider: defaultSSLProvider,
} }
if settings != nil { 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 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. // Some unified lego environment variables are configured here.
// link: https://github.com/go-acme/lego/issues/1867 // link: https://github.com/go-acme/lego/issues/1867
os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(applyConfig.DisableFollowCNAME)) 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) config := lego.NewConfig(myUser)
// This CA URL is configured for a local dev instance of Boulder running in Docker in a VM. // 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) config.Certificate.KeyType = parseKeyAlgorithm(applyConfig.KeyAlgorithm)
// A client facilitates communication with the CA server. // 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 // New users will need to register
if !myUser.hasRegistration() { if !myUser.hasRegistration() {
reg, err := registerAcmeUser(client, sslProvider, myUser) reg, err := registerAcmeUser(client, sslProviderConfig, myUser)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to register: %w", err) return nil, fmt.Errorf("failed to register: %w", err)
} }
myUser.Registration = reg myUser.Registration = reg
} }
request := certificate.ObtainRequest{ certRequest := certificate.ObtainRequest{
Domains: strings.Split(applyConfig.Domains, ";"), Domains: strings.Split(applyConfig.Domains, ";"),
Bundle: true, Bundle: true,
} }
certificates, err := client.Certificate.Obtain(request) certResource, err := client.Certificate.Obtain(certRequest)
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &ApplyCertResult{ return &ApplyCertResult{
PrivateKey: string(certificates.PrivateKey), PrivateKey: string(certResource.PrivateKey),
Certificate: string(certificates.Certificate), Certificate: string(certResource.Certificate),
IssuerCertificate: string(certificates.IssuerCertificate), IssuerCertificate: string(certResource.IssuerCertificate),
CSR: string(certificates.CSR), CSR: string(certResource.CSR),
ACMECertUrl: certificates.CertURL, ACMECertUrl: certResource.CertURL,
ACMECertStableUrl: certificates.CertStableURL, ACMECertStableUrl: certResource.CertStableURL,
}, nil }, nil
} }

View File

@ -133,13 +133,11 @@ export const notifyChannelsMap: Map<NotifyChannel["type"], NotifyChannel> = new
// #endregion // #endregion
// #region Settings: SSLProvider // #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({ export const SSLPROVIDERS = Object.freeze({
LETS_ENCRYPT: SSLPROVIDER_LETSENCRYPT, LETS_ENCRYPT: "letsencrypt",
ZERO_SSL: SSLPROVIDER_ZEROSSL, LETS_ENCRYPT_STAGING: "letsencrypt_staging",
GOOGLE_TRUST_SERVICES: SSLPROVIDER_GOOGLETRUSTSERVICES, ZERO_SSL: "zerossl",
GOOGLE_TRUST_SERVICES: "gts",
} as const); } as const);
export type SSLProviders = (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS]; export type SSLProviders = (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS];
@ -148,9 +146,10 @@ export type SSLProviderSettingsContent = {
provider: (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS]; provider: (typeof SSLPROVIDERS)[keyof typeof SSLPROVIDERS];
config: { config: {
[key: string]: Record<string, unknown> | undefined; [key: string]: Record<string, unknown> | undefined;
letsencrypt?: SSLProviderLetsEncryptConfig; [SSLPROVIDERS.LETS_ENCRYPT]?: SSLProviderLetsEncryptConfig;
zerossl?: SSLProviderZeroSSLConfig; [SSLPROVIDERS.LETS_ENCRYPT_STAGING]?: SSLProviderLetsEncryptConfig;
gts?: SSLProviderGoogleTrustServicesConfig; [SSLPROVIDERS.ZERO_SSL]?: SSLProviderZeroSSLConfig;
[SSLPROVIDERS.GOOGLE_TRUST_SERVICES]?: SSLProviderGoogleTrustServicesConfig;
}; };
}; };

View File

@ -73,6 +73,12 @@
"settings.sslprovider.tab": "Certificate authority", "settings.sslprovider.tab": "Certificate authority",
"settings.sslprovider.form.provider.label": "ACME provider", "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.label": "EAB KID",
"settings.sslprovider.form.zerossl_eab_kid.placeholder": "Please enter 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>", "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>",

View File

@ -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.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.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.label": "EAB KID",
"settings.sslprovider.form.zerossl_eab_kid.placeholder": "请输入 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>", "settings.sslprovider.form.zerossl_eab_kid.tooltip": "这是什么?请参阅 <a href=\"https://zerossl.com/documentation/acme/\" target=\"_blank\">https://zerossl.com/documentation/acme/</a>",

View File

@ -1,7 +1,7 @@
import { createContext, useContext, useEffect, useMemo, useState } from "react"; import { createContext, useContext, useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { CheckCard } from "@ant-design/pro-components"; 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 { createSchemaFieldRule } from "antd-zod";
import { produce } from "immer"; import { produce } from "immer";
import { z } from "zod"; 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 SSLProviderEditFormZeroSSLConfig = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -231,6 +280,8 @@ const SettingsSSLProvider = () => {
switch (providerType) { switch (providerType) {
case SSLPROVIDERS.LETS_ENCRYPT: case SSLPROVIDERS.LETS_ENCRYPT:
return <SSLProviderEditFormLetsEncryptConfig />; return <SSLProviderEditFormLetsEncryptConfig />;
case SSLPROVIDERS.LETS_ENCRYPT_STAGING:
return <SSLProviderEditFormLetsEncryptStagingConfig />;
case SSLPROVIDERS.ZERO_SSL: case SSLPROVIDERS.ZERO_SSL:
return <SSLProviderEditFormZeroSSLConfig />; return <SSLProviderEditFormZeroSSLConfig />;
case SSLPROVIDERS.GOOGLE_TRUST_SERVICES: case SSLPROVIDERS.GOOGLE_TRUST_SERVICES:
@ -272,14 +323,25 @@ const SettingsSSLProvider = () => {
<CheckCard <CheckCard
avatar={<img src={"/imgs/acme/letsencrypt.svg"} className="size-8" />} avatar={<img src={"/imgs/acme/letsencrypt.svg"} className="size-8" />}
size="small" size="small"
title="Let's Encrypt" title={t("settings.sslprovider.form.provider.option.letsencrypt.label")}
value={SSLPROVIDERS.LETS_ENCRYPT} 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 <CheckCard
avatar={<img src={"/imgs/acme/google.svg"} className="size-8" />} avatar={<img src={"/imgs/acme/google.svg"} className="size-8" />}
size="small" size="small"
title="Google Trust Services" title={t("settings.sslprovider.form.provider.option.gts.label")}
value={SSLPROVIDERS.GOOGLE_TRUST_SERVICES} value={SSLPROVIDERS.GOOGLE_TRUST_SERVICES}
/> />
</CheckCard.Group> </CheckCard.Group>