diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index c2136d20..f700b213 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -297,11 +297,12 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { switch options.Provider { case domain.DeployProviderTypeAzureKeyVault: deployer, err := pAzureKeyVault.NewDeployer(&pAzureKeyVault.DeployerConfig{ - TenantId: access.TenantId, - ClientId: access.ClientId, - ClientSecret: access.ClientSecret, - CloudName: access.CloudName, - KeyVaultName: maputil.GetString(options.ProviderDeployConfig, "keyvaultName"), + TenantId: access.TenantId, + ClientId: access.ClientId, + ClientSecret: access.ClientSecret, + CloudName: access.CloudName, + KeyVaultName: maputil.GetString(options.ProviderDeployConfig, "keyvaultName"), + CertificateName: maputil.GetString(options.ProviderDeployConfig, "certificateName"), }) return deployer, err diff --git a/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go b/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go index 4439aa68..c39ed892 100644 --- a/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go +++ b/internal/pkg/core/deployer/providers/azure-keyvault/azure_keyvault.go @@ -22,6 +22,8 @@ type DeployerConfig struct { CloudName string `json:"cloudName,omitempty"` // Key Vault 名称。 KeyVaultName string `json:"keyvaultName"` + // Certificate 名称。 + CertificateName string `json:"certificateName"` } type DeployerProvider struct { @@ -38,11 +40,12 @@ func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { } uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ - TenantId: config.TenantId, - ClientId: config.ClientId, - ClientSecret: config.ClientSecret, - CloudName: config.CloudName, - KeyVaultName: config.KeyVaultName, + TenantId: config.TenantId, + ClientId: config.ClientId, + ClientSecret: config.ClientSecret, + CloudName: config.CloudName, + KeyVaultName: config.KeyVaultName, + CertificateName: config.CertificateName, }) if err != nil { return nil, xerrors.Wrap(err, "failed to create ssl uploader") diff --git a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go index 5f6f998a..308cb5d4 100644 --- a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go +++ b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault.go @@ -3,6 +3,7 @@ import ( "context" "crypto/x509" + "encoding/base64" "fmt" "log/slog" "time" @@ -29,6 +30,8 @@ type UploaderConfig struct { CloudName string `json:"cloudName,omitempty"` // Key Vault 名称。 KeyVaultName string `json:"keyvaultName"` + // Certificate 名称。 + CertificateName string `json:"certificateName,omitempty"` } type UploaderProvider struct { @@ -88,6 +91,11 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe } for _, certItem := range page.Value { + // 如果已经指定了证书名称,则跳过证书名称不匹配的证书 + if u.config.CertificateName != "" && certItem.ID.Name() != u.config.CertificateName { + continue + } + // 先对比证书有效期 if certItem.Attributes == nil { continue @@ -138,16 +146,28 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe } } - // 生成新证书名(需符合 Azure 命名规则) - certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + certName := u.config.CertificateName + if certName == "" { + // 未指定证书名称时,生成包含timestamp的新证书名(需符合 Azure 命名规则) + certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + } + + // Azure Key Vault 不支持导入带有Certificiate Chain的PEM证书。 + // Issue Link: https://github.com/Azure/azure-cli/issues/19017 + // 暂时的解决方法是,将 PEM 证书转换成 PFX 格式,然后再导入。 + pfxCert, err := certutil.TransformCertificateFromPEMToPFX(certPem, privkeyPem, "") + if err != nil { + u.logger.Error("failed to transform certificate from PEM to PFX", slog.String("certPem", certPem), slog.String("privkeyPem", privkeyPem)) + return nil, xerrors.Wrap(err, "failed to transform certificate from PEM to PFX") + } // 导入证书 // REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/import-certificate/import-certificate importCertificateParams := azcertificates.ImportCertificateParameters{ - Base64EncodedCertificate: to.Ptr(certPem), + Base64EncodedCertificate: to.Ptr(base64.StdEncoding.EncodeToString(pfxCert)), CertificatePolicy: &azcertificates.CertificatePolicy{ SecretProperties: &azcertificates.SecretProperties{ - ContentType: to.Ptr("application/x-pem-file"), + ContentType: to.Ptr("application/x-pkcs12"), }, }, Tags: map[string]*string{ diff --git a/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault_test.go b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault_test.go new file mode 100644 index 00000000..3a8ff985 --- /dev/null +++ b/internal/pkg/core/uploader/providers/azure-keyvault/azure_keyvault_test.go @@ -0,0 +1,87 @@ +package azurekeyvault_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/azure-keyvault" +) + +var ( + fInputCertPath string + fInputKeyPath string + fTenantId string + fAccessKeyId string + fSecretAccessKey string + fKeyVaultName string + fCertificateName string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_AZUREKEYVAULT_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fTenantId, argsPrefix+"TENANTID", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") + flag.StringVar(&fKeyVaultName, argsPrefix+"KEYVAULTNAME", "", "") + flag.StringVar(&fCertificateName, argsPrefix+"CERTIFICATENAME", "", "") +} + +/* +Shell command to run this test: + + go test -v ./azure_keyvault_test.go -args \ + --CERTIMATE_UPLOADER_AZUREKEYVAULT_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_AZUREKEYVAULT_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_UPLOADER_AZUREKEYVAULT_TENANTID="your-tenant-id" \ + --CERTIMATE_UPLOADER_AZUREKEYVAULT_ACCESSKEYID="your-app-registration-client-id" \ + --CERTIMATE_UPLOADER_AZUREKEYVAULT_SECRETACCESSKEY="your-app-registration-client-secret" \ + --CERTIMATE_UPLOADER_AZUREKEYVAULT_KEYVAULTNAME="your-keyvault-name" \ + --CERTIMATE_UPLOADER_AZUREKEYVAULT_CERTIFICATENAME="your-certificate-name" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("TENANTID: %v", fTenantId), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), + fmt.Sprintf("KEYVAULTNAME: %v", fKeyVaultName), + fmt.Sprintf("CERTIFICATENAME: %v", fCertificateName), + }, "\n")) + + uploader, err := provider.NewUploader(&provider.UploaderConfig{ + TenantId: fTenantId, + ClientId: fAccessKeyId, + ClientSecret: fSecretAccessKey, + KeyVaultName: fKeyVaultName, + CertificateName: fCertificateName, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +} diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx index 91d48cdf..9518fd25 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx @@ -2,9 +2,11 @@ import { useTranslation } from "react-i18next"; import { Form, type FormInstance, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import { validAzureKeyVaultCertificateName } from "@/utils/validators"; type DeployNodeConfigFormAzureKeyVaultConfigFieldValues = Nullish<{ keyvaultName: string; + certificateName?: string; }>; export type DeployNodeConfigFormAzureKeyVaultConfigProps = { @@ -33,6 +35,13 @@ const DeployNodeConfigFormAzureKeyVaultConfig = ({ .string({ message: t("workflow_node.deploy.form.azure_keyvault_name.placeholder") }) .nonempty(t("workflow_node.deploy.form.azure_keyvault_name.placeholder")) .trim(), + certificateName: z + .string({ message: t("workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder") }) + .nullish() + .refine((v) =>{ + if (!v) return true; + return validAzureKeyVaultCertificateName(v); + }, t("common.errmsg.azure_keyvault_certificate_name_invalid")), }); const formRule = createSchemaFieldRule(formSchema); @@ -57,6 +66,14 @@ const DeployNodeConfigFormAzureKeyVaultConfig = ({ > +