diff --git a/internal/applicant/acme_ca.go b/internal/applicant/acme_ca.go
index 67b7693e..36c0a0a4 100644
--- a/internal/applicant/acme_ca.go
+++ b/internal/applicant/acme_ca.go
@@ -3,25 +3,26 @@ package applicant
import "github.com/usual2970/certimate/internal/domain"
const (
- sslProviderLetsEncrypt = string(domain.CAProviderTypeLetsEncrypt)
- sslProviderLetsEncryptStaging = string(domain.CAProviderTypeLetsEncryptStaging)
- sslProviderBuypass = string(domain.CAProviderTypeBuypass)
- sslProviderGoogleTrustServices = string(domain.CAProviderTypeGoogleTrustServices)
- sslProviderSSLCom = string(domain.CAProviderTypeSSLCom)
- sslProviderZeroSSL = string(domain.CAProviderTypeZeroSSL)
+ caLetsEncrypt = string(domain.CAProviderTypeLetsEncrypt)
+ caLetsEncryptStaging = string(domain.CAProviderTypeLetsEncryptStaging)
+ caBuypass = string(domain.CAProviderTypeBuypass)
+ caGoogleTrustServices = string(domain.CAProviderTypeGoogleTrustServices)
+ caSSLCom = string(domain.CAProviderTypeSSLCom)
+ caZeroSSL = string(domain.CAProviderTypeZeroSSL)
+ caCustom = string(domain.CAProviderTypeACMECA)
- sslProviderDefault = sslProviderLetsEncrypt
+ caDefault = caLetsEncrypt
)
-var sslProviderUrls = map[string]string{
- sslProviderLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory",
- sslProviderLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory",
- sslProviderBuypass: "https://api.buypass.com/acme/directory",
- sslProviderGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory",
- sslProviderSSLCom: "https://acme.ssl.com/sslcom-dv-rsa",
- sslProviderSSLCom + "RSA": "https://acme.ssl.com/sslcom-dv-rsa",
- sslProviderSSLCom + "ECC": "https://acme.ssl.com/sslcom-dv-ecc",
- sslProviderZeroSSL: "https://acme.zerossl.com/v2/DV90",
+var caDirUrls = map[string]string{
+ caLetsEncrypt: "https://acme-v02.api.letsencrypt.org/directory",
+ caLetsEncryptStaging: "https://acme-staging-v02.api.letsencrypt.org/directory",
+ caBuypass: "https://api.buypass.com/acme/directory",
+ caGoogleTrustServices: "https://dv.acme-v02.api.pki.goog/directory",
+ caSSLCom: "https://acme.ssl.com/sslcom-dv-rsa",
+ caSSLCom + "RSA": "https://acme.ssl.com/sslcom-dv-rsa",
+ caSSLCom + "ECC": "https://acme.ssl.com/sslcom-dv-ecc",
+ caZeroSSL: "https://acme.zerossl.com/v2/DV90",
}
type acmeSSLProviderConfig struct {
diff --git a/internal/applicant/acme_user.go b/internal/applicant/acme_user.go
index 29ac80cd..e6e13cb7 100644
--- a/internal/applicant/acme_user.go
+++ b/internal/applicant/acme_user.go
@@ -7,6 +7,7 @@ import (
"crypto/elliptic"
"crypto/rand"
"fmt"
+ "strings"
"github.com/go-acme/lego/v4/lego"
"github.com/go-acme/lego/v4/registration"
@@ -19,22 +20,31 @@ import (
)
type acmeUser struct {
- CA string
- Email string
+ // 证书颁发机构标识。
+ // 通常等同于 [CAProviderType] 的值。
+ // 对于自定义 ACME CA,值为 "custom#{access_id}"。
+ CA string
+ // 邮箱。
+ Email string
+ // 注册信息。
Registration *registration.Resource
+ // CSR 私钥。
privkey string
}
-func newAcmeUser(ca, email string) (*acmeUser, error) {
+func newAcmeUser(ca, caAccessId, email string) (*acmeUser, error) {
repo := repository.NewAcmeAccountRepository()
applyUser := &acmeUser{
CA: ca,
Email: email,
}
+ if ca == caCustom {
+ applyUser.CA = fmt.Sprintf("%s#%s", ca, caAccessId)
+ }
- acmeAccount, err := repo.GetByCAAndEmail(ca, email)
+ acmeAccount, err := repo.GetByCAAndEmail(applyUser.CA, applyUser.Email)
if err != nil {
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
@@ -73,6 +83,10 @@ func (u *acmeUser) hasRegistration() bool {
return u.Registration != nil
}
+func (u *acmeUser) getCAProvider() string {
+ return strings.Split(u.CA, "#")[0]
+}
+
func (u *acmeUser) getPrivateKeyPEM() string {
return u.privkey
}
@@ -94,16 +108,16 @@ func registerAcmeUserWithSingleFlight(client *lego.Client, user *acmeUser, userR
func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) {
var reg *registration.Resource
var err error
- switch user.CA {
- case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging:
+ switch user.getCAProvider() {
+ case caLetsEncrypt, caLetsEncryptStaging:
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
- case sslProviderBuypass:
+ case caBuypass:
{
reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true})
}
- case sslProviderGoogleTrustServices:
+ case caGoogleTrustServices:
{
access := domain.AccessConfigForGoogleTrustServices{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
@@ -117,7 +131,7 @@ func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions m
})
}
- case sslProviderSSLCom:
+ case caSSLCom:
{
access := domain.AccessConfigForSSLCom{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
@@ -131,7 +145,7 @@ func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions m
})
}
- case sslProviderZeroSSL:
+ case caZeroSSL:
{
access := domain.AccessConfigForZeroSSL{}
if err := maputil.Populate(userRegisterOptions, &access); err != nil {
@@ -145,6 +159,26 @@ func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions m
})
}
+ case caCustom:
+ {
+ access := domain.AccessConfigForACMECA{}
+ if err := maputil.Populate(userRegisterOptions, &access); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ if access.EabKid == "" && access.EabHmacKey == "" {
+ reg, err = client.Registration.Register(registration.RegisterOptions{
+ TermsOfServiceAgreed: true,
+ })
+ } else {
+ reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{
+ TermsOfServiceAgreed: true,
+ Kid: access.EabKid,
+ HmacEncoded: access.EabHmacKey,
+ })
+ }
+ }
+
default:
err = fmt.Errorf("unsupported ca provider '%s'", user.CA)
}
diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go
index e9ed4cb1..6d17e940 100644
--- a/internal/applicant/applicant.go
+++ b/internal/applicant/applicant.go
@@ -20,12 +20,13 @@ import (
"golang.org/x/time/rate"
"github.com/usual2970/certimate/internal/domain"
+ maputil "github.com/usual2970/certimate/internal/pkg/utils/map"
sliceutil "github.com/usual2970/certimate/internal/pkg/utils/slice"
"github.com/usual2970/certimate/internal/repository"
)
type ApplyResult struct {
- CertificateFullChain string
+ FullChainCertificate string
IssuerCertificate string
PrivateKey string
ACMEAccountUrl string
@@ -81,6 +82,7 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err
if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil {
return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err)
} else {
+ options.CAProviderAccessId = access.Id
options.CAProviderAccessConfig = access.Config
}
}
@@ -91,13 +93,13 @@ func NewWithWorkflowNode(config ApplicantWithWorkflowNodeConfig) (Applicant, err
sslProviderConfig := &acmeSSLProviderConfig{
Config: make(map[domain.CAProviderType]map[string]any),
- Provider: sslProviderDefault,
+ Provider: caDefault,
}
if settings != nil {
if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil {
return nil, err
} else if sslProviderConfig.Provider == "" {
- sslProviderConfig.Provider = sslProviderDefault
+ sslProviderConfig.Provider = caDefault
}
}
@@ -163,7 +165,7 @@ func getLimiter(key string) *rate.Limiter {
}
func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOptions) (*ApplyResult, error) {
- user, err := newAcmeUser(string(options.CAProvider), options.ContactEmail)
+ user, err := newAcmeUser(string(options.CAProvider), options.CAProviderAccessId, options.ContactEmail)
if err != nil {
return nil, err
}
@@ -175,13 +177,26 @@ func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOpt
// Create an ACME client config
config := lego.NewConfig(user)
config.Certificate.KeyType = parseLegoKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm))
- config.CADirURL = sslProviderUrls[user.CA]
- if user.CA == sslProviderSSLCom {
+ switch user.getCAProvider() {
+ case caSSLCom:
if strings.HasPrefix(options.KeyAlgorithm, "RSA") {
- config.CADirURL = sslProviderUrls[sslProviderSSLCom+"RSA"]
+ config.CADirURL = caDirUrls[caSSLCom+"RSA"]
} else if strings.HasPrefix(options.KeyAlgorithm, "EC") {
- config.CADirURL = sslProviderUrls[sslProviderSSLCom+"ECC"]
+ config.CADirURL = caDirUrls[caSSLCom+"ECC"]
+ } else {
+ config.CADirURL = caDirUrls[caSSLCom]
}
+
+ case caCustom:
+ caDirURL := maputil.GetString(options.CAProviderAccessConfig, "endpoint")
+ if caDirURL != "" {
+ config.CADirURL = caDirURL
+ } else {
+ return nil, fmt.Errorf("invalid ca provider endpoint")
+ }
+
+ default:
+ config.CADirURL = caDirUrls[user.CA]
}
// Create an ACME client
@@ -229,7 +244,7 @@ func applyUseLego(legoProvider challenge.Provider, options *applicantProviderOpt
}
return &ApplyResult{
- CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)),
+ FullChainCertificate: strings.TrimSpace(string(certResource.Certificate)),
IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)),
PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)),
ACMEAccountUrl: user.Registration.URI,
diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go
index 90a3cf72..bf27e7e3 100644
--- a/internal/applicant/providers.go
+++ b/internal/applicant/providers.go
@@ -48,6 +48,7 @@ type applicantProviderOptions struct {
ProviderAccessConfig map[string]any
ProviderExtendedConfig map[string]any
CAProvider domain.CAProviderType
+ CAProviderAccessId string
CAProviderAccessConfig map[string]any
CAProviderExtendedConfig map[string]any
KeyAlgorithm string
diff --git a/internal/domain/access.go b/internal/domain/access.go
index 9ab18b9f..0321cb41 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -22,6 +22,12 @@ type AccessConfigFor1Panel struct {
AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
}
+type AccessConfigForACMECA struct {
+ Endpoint string `json:"endpoint"`
+ EabKid string `json:"eabKid,omitempty"`
+ EabHmacKey string `json:"eabHmacKey,omitempty"`
+}
+
type AccessConfigForACMEHttpReq struct {
Endpoint string `json:"endpoint"`
Mode string `json:"mode,omitempty"`
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 4a0c05dd..d4d9af2e 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -10,7 +10,7 @@ type AccessProviderType string
*/
const (
AccessProviderType1Panel = AccessProviderType("1panel")
- AccessProviderTypeACMECA = AccessProviderType("acmeca") // ACME CA(预留)
+ AccessProviderTypeACMECA = AccessProviderType("acmeca")
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留)
AccessProviderTypeAliyun = AccessProviderType("aliyun")
@@ -91,6 +91,7 @@ type CAProviderType string
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
+ CAProviderTypeACMECA = CAProviderType(AccessProviderTypeACMECA)
CAProviderTypeBuypass = CAProviderType(AccessProviderTypeBuypass)
CAProviderTypeGoogleTrustServices = CAProviderType(AccessProviderTypeGoogleTrustServices)
CAProviderTypeLetsEncrypt = CAProviderType(AccessProviderTypeLetsEncrypt)
diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go
index 97b7575d..ff8c573d 100644
--- a/internal/workflow/node-processor/apply_node.go
+++ b/internal/workflow/node-processor/apply_node.go
@@ -66,14 +66,14 @@ func (n *applyNode) Process(ctx context.Context) error {
}
// 解析证书并生成实体
- certX509, err := certutil.ParseCertificateFromPEM(applyResult.CertificateFullChain)
+ certX509, err := certutil.ParseCertificateFromPEM(applyResult.FullChainCertificate)
if err != nil {
n.logger.Warn("failed to parse certificate, may be the CA responded error")
return err
}
certificate := &domain.Certificate{
Source: domain.CertificateSourceTypeWorkflow,
- Certificate: applyResult.CertificateFullChain,
+ Certificate: applyResult.FullChainCertificate,
PrivateKey: applyResult.PrivateKey,
IssuerCertificate: applyResult.IssuerCertificate,
ACMEAccountUrl: applyResult.ACMEAccountUrl,
diff --git a/ui/public/imgs/providers/acmeca.svg b/ui/public/imgs/providers/acmeca.svg
new file mode 100644
index 00000000..260530c9
--- /dev/null
+++ b/ui/public/imgs/providers/acmeca.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx
index 3e31dad4..4981a045 100644
--- a/ui/src/components/access/AccessForm.tsx
+++ b/ui/src/components/access/AccessForm.tsx
@@ -12,6 +12,7 @@ import { ACCESS_PROVIDERS, ACCESS_USAGES, type AccessProvider } from "@/domain/p
import { useAntdForm, useAntdFormName } from "@/hooks";
import AccessForm1PanelConfig from "./AccessForm1PanelConfig";
+import AccessFormACMECAConfig from "./AccessFormACMECAConfig";
import AccessFormACMEHttpReqConfig from "./AccessFormACMEHttpReqConfig";
import AccessFormAliyunConfig from "./AccessFormAliyunConfig";
import AccessFormAWSConfig from "./AccessFormAWSConfig";
@@ -177,6 +178,8 @@ const AccessForm = forwardRef(({ className,
switch (fieldProvider) {
case ACCESS_PROVIDERS["1PANEL"]:
return ;
+ case ACCESS_PROVIDERS.ACMECA:
+ return ;
case ACCESS_PROVIDERS.ACMEHTTPREQ:
return ;
case ACCESS_PROVIDERS.ALIYUN:
diff --git a/ui/src/components/access/AccessFormACMECAConfig.tsx b/ui/src/components/access/AccessFormACMECAConfig.tsx
new file mode 100644
index 00000000..8c9eb2ac
--- /dev/null
+++ b/ui/src/components/access/AccessFormACMECAConfig.tsx
@@ -0,0 +1,77 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input, Select } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { type AccessConfigForACMECA } from "@/domain/access";
+
+type AccessFormACMECAConfigFieldValues = Nullish;
+
+export type AccessFormACMECAConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: AccessFormACMECAConfigFieldValues;
+ onValuesChange?: (values: AccessFormACMECAConfigFieldValues) => void;
+};
+
+const initFormModel = (): AccessFormACMECAConfigFieldValues => {
+ return {
+ endpoint: "https://example.com/acme/directory",
+ };
+};
+
+const AccessFormACMECAConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormACMECAConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ endpoint: z.string().url(t("common.errmsg.url_invalid")),
+ eabKid: z.string().trim().nullish(),
+ eabHmacKey: z.string().trim().nullish(),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ }
+ >
+
+
+
+ }
+ >
+
+
+
+ );
+};
+
+export default AccessFormACMECAConfig;
diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts
index fe912543..c676b5f7 100644
--- a/ui/src/domain/access.ts
+++ b/ui/src/domain/access.ts
@@ -7,6 +7,7 @@ export interface AccessModel extends BaseModel {
*/ Record &
(
| AccessConfigFor1Panel
+ | AccessConfigForACMECA
| AccessConfigForACMEHttpReq
| AccessConfigForAliyun
| AccessConfigForAWS
@@ -75,6 +76,12 @@ export type AccessConfigFor1Panel = {
allowInsecureConnections?: boolean;
};
+export type AccessConfigForACMECA = {
+ endpoint: string;
+ eabKid?: string;
+ eabHmacKey?: string;
+};
+
export type AccessConfigForACMEHttpReq = {
endpoint: string;
mode?: string;
diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts
index 3d0678ba..2c6f0f0f 100644
--- a/ui/src/domain/provider.ts
+++ b/ui/src/domain/provider.ts
@@ -5,6 +5,7 @@
*/
export const ACCESS_PROVIDERS = Object.freeze({
["1PANEL"]: "1panel",
+ ACMECA: "acmeca",
ACMEHTTPREQ: "acmehttpreq",
ALIYUN: "aliyun",
AWS: "aws",
@@ -153,6 +154,7 @@ export const accessProvidersMap: Map = new
[CA_PROVIDERS.GOOGLETRUSTSERVICES],
[CA_PROVIDERS.SSLCOM],
[CA_PROVIDERS.ZEROSSL],
+ [CA_PROVIDERS.ACMECA],
].map(([type, builtin]) => [
type,
{
diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json
index b3f9167f..4b178ad4 100644
--- a/ui/src/i18n/locales/en/nls.access.json
+++ b/ui/src/i18n/locales/en/nls.access.json
@@ -44,6 +44,13 @@
"access.form.1panel_allow_insecure_conns.label": "Insecure SSL/TLS connections",
"access.form.1panel_allow_insecure_conns.switch.on": "Allow",
"access.form.1panel_allow_insecure_conns.switch.off": "Disallow",
+ "access.form.acmeca_endpoint.label": "Endpoint",
+ "access.form.acmeca_endpoint.placeholder": "Please enter endpoint",
+ "access.form.acmeca_endpoint.tooltip": "For more information, see https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1",
+ "access.form.acmeca_eab_kid.label": "ACME EAB KID (Optional)",
+ "access.form.acmeca_eab_kid.placeholder": "Please enter ACME EAB KID",
+ "access.form.acmeca_eab_hmac_key.label": "ACME EAB HMAC key (Optional)",
+ "access.form.acmeca_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key",
"access.form.acmehttpreq_endpoint.label": "Endpoint",
"access.form.acmehttpreq_endpoint.placeholder": "Please enter endpoint",
"access.form.acmehttpreq_endpoint.tooltip": "For more information, see https://go-acme.github.io/lego/dns/httpreq/",
diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json
index 13571036..dad0ae6f 100644
--- a/ui/src/i18n/locales/en/nls.provider.json
+++ b/ui/src/i18n/locales/en/nls.provider.json
@@ -2,7 +2,8 @@
"provider.1panel": "1Panel",
"provider.1panel.console": "1Panel - Console",
"provider.1panel.site": "1Panel - Website",
- "provider.acmehttpreq": "Http Request (ACME Proxy)",
+ "provider.acmeca": "ACME Custom CA Endpoint",
+ "provider.acmehttpreq": "ACME Custom HTTP Endpoint",
"provider.aliyun": "Alibaba Cloud",
"provider.aliyun.alb": "Alibaba Cloud - ALB (Application Load Balancer)",
"provider.aliyun.apigw": "Alibaba Cloud - API Gateway",
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index 5e3444fe..1ca78d07 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -44,6 +44,13 @@
"access.form.1panel_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误",
"access.form.1panel_allow_insecure_conns.switch.on": "允许",
"access.form.1panel_allow_insecure_conns.switch.off": "不允许",
+ "access.form.acmeca_endpoint.label": "服务端点",
+ "access.form.acmeca_endpoint.placeholder": "请输入服务端点",
+ "access.form.acmeca_endpoint.tooltip": "这是什么?请参阅 https://datatracker.ietf.org/doc/html/rfc8555#section-7.1.1",
+ "access.form.acmeca_eab_kid.label": "ACME EAB KID(可选)",
+ "access.form.acmeca_eab_kid.placeholder": "请输入 ACME EAB KID",
+ "access.form.acmeca_eab_hmac_key.label": "ACME EAB HMAC Key(可选)",
+ "access.form.acmeca_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key",
"access.form.acmehttpreq_endpoint.label": "服务端点",
"access.form.acmehttpreq_endpoint.placeholder": "请输入服务端点",
"access.form.acmehttpreq_endpoint.tooltip": "这是什么?请参阅 https://go-acme.github.io/lego/dns/httpreq/",
diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json
index bd04cc43..e7c6ea31 100644
--- a/ui/src/i18n/locales/zh/nls.provider.json
+++ b/ui/src/i18n/locales/zh/nls.provider.json
@@ -2,7 +2,8 @@
"provider.1panel": "1Panel",
"provider.1panel.console": "1Panel - 面板",
"provider.1panel.site": "1Panel - 网站",
- "provider.acmehttpreq": "Http Request (ACME Proxy)",
+ "provider.acmeca": "ACME 自定义 CA 端点",
+ "provider.acmehttpreq": "ACME 自定义 HTTP 端点",
"provider.aliyun": "阿里云",
"provider.aliyun.alb": "阿里云 - 应用型负载均衡 ALB",
"provider.aliyun.apigw": "阿里云 - API 网关",