feat: add azure keyvault uploader

This commit is contained in:
Fu Diwei 2025-02-25 17:12:55 +08:00
parent 3c91f29a91
commit a2ac836629
8 changed files with 276 additions and 20 deletions

8
go.mod
View File

@ -51,12 +51,16 @@ require (
)
require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1 // indirect
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect
github.com/G-Core/gcorelabscdn-go v1.0.26 // indirect
github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect
github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect

13
go.sum
View File

@ -54,10 +54,17 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WW
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 h1:F0gBpfdPLGsw+nsgk6aqqkZS1jiixa5WwFe3fk/T3Ys=
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2/go.mod h1:SqINnQ9lVVdRlyC8cd1lCI0SdX4n2paeABd2K8ggfnE=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU=
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY=
github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0 h1:btEsytNrA4TG3edZnnUnzOz8W2MjOd6Bu3/7xyOXSOY=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0/go.mod h1:5SlTxxL1U4LLipEr7pAbnu6Ck5y3aIEu4L/tVbGmpsY=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1 h1:FbH3BbSb4bvGluTesZZ+ttN/MDsnMmQP36OSnDuSXqw=
github.com/Azure/azure-sdk-for-go/sdk/keyvault/internal v0.7.1/go.mod h1:9V2j0jn9jDEkCkv8w/bKTNppX/d0FVA1ud77xCIP4KA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0 h1:lpOxwrQ919lCZoNCd69rVt8u1eLZuMORrGXqy8sNf3c=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/dns/armdns v1.2.0/go.mod h1:fSvRkb8d26z9dbL40Uf/OO6Vo9iExtZK3D0ulRV+8M0=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v3 v3.1.0 h1:2qsIIvxVT+uE6yrNldntJKlLRgxGbZ85kgtz5SNBhMw=
@ -68,10 +75,16 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourceg
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0/go.mod h1:wVEOJfGTj0oPAUGA1JuRAvz/lxXQsWW16axmHPP47Bk=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0 h1:Dd+RhdJn0OTtVGaeDLZpcumkIVCtA/3/Fo42+eoYvVM=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0/go.mod h1:5kakwfW5CjC9KK+Q4wjXAg+ShuIm2mBMua0ZFj2C8PE=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1 h1:HUJQzFYTv7t3V1dxPms52eEgl0l9xCNqutDrY45Lvmw=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.3.1/go.mod h1:ig/8nSkzmfxm5QGeIy5JYIEj8JEFy5JxvY3OB1YNRC4=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI=
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 h1:H5xDQaE3XowWfhZRUpnfC+rGZMEVoSiji+b+/HFAPU4=
github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=

View File

@ -28,7 +28,7 @@ func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider,
providerConfig.ClientID = config.ClientId
providerConfig.ClientSecret = config.ClientSecret
if config.CloudName != "" {
env, err := azcommon.GetEnvironmentConfiguration(config.CloudName)
env, err := azcommon.GetCloudEnvironmentConfiguration(config.CloudName)
if err != nil {
return nil, err
}

View File

@ -2,14 +2,13 @@
import (
"context"
"fmt"
"time"
aws "github.com/aws/aws-sdk-go-v2/aws"
awsCfg "github.com/aws/aws-sdk-go-v2/config"
awsCred "github.com/aws/aws-sdk-go-v2/credentials"
awsAcm "github.com/aws/aws-sdk-go-v2/service/acm"
xerrors "github.com/pkg/errors"
"golang.org/x/exp/slices"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/certs"
@ -54,13 +53,74 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
return nil, err
}
// 生成 AWS 所需的服务端证书和证书链参数
// 生成 AWS 业务参数
scertPem, _ := certs.ConvertCertificateToPEM(certX509)
bcertPem := certPem
// 生成新证书名(需符合 AWS 命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate_%d", time.Now().UnixMilli())
// 获取证书列表,避免重复上传
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListCertificates.html
listCertificatesNextToken := new(string)
listCertificatesMaxItems := int32(1000)
for {
listCertificatesReq := &awsAcm.ListCertificatesInput{
NextToken: listCertificatesNextToken,
MaxItems: aws.Int32(listCertificatesMaxItems),
}
listCertificatesResp, err := u.sdkClient.ListCertificates(context.TODO(), listCertificatesReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'acm.ListCertificates'")
}
for _, certSummary := range listCertificatesResp.CertificateSummaryList {
// 先对比证书有效期
if certSummary.NotBefore == nil || !certSummary.NotBefore.Equal(certX509.NotBefore) {
continue
}
if certSummary.NotAfter == nil || !certSummary.NotAfter.Equal(certX509.NotAfter) {
continue
}
// 再对比证书多域名
if !slices.Equal(certX509.DNSNames, certSummary.SubjectAlternativeNameSummaries) {
continue
}
// 最后对比证书内容
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ListTagsForCertificate.html
getCertificateReq := &awsAcm.GetCertificateInput{
CertificateArn: certSummary.CertificateArn,
}
getCertificateResp, err := u.sdkClient.GetCertificate(context.TODO(), getCertificateReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'acm.GetCertificate'")
} else {
oldCertPem := aws.ToString(getCertificateResp.CertificateChain)
if oldCertPem == "" {
oldCertPem = aws.ToString(getCertificateResp.Certificate)
}
oldCertX509, err := certs.ParseCertificateFromPEM(oldCertPem)
if err != nil {
continue
}
if !certs.EqualCertificate(certX509, oldCertX509) {
continue
}
}
// 如果以上信息都一致,则视为已存在相同证书,直接返回
return &uploader.UploadResult{
CertId: *certSummary.CertificateArn,
}, nil
}
if listCertificatesResp.NextToken == nil || len(listCertificatesResp.CertificateSummaryList) < int(listCertificatesMaxItems) {
break
} else {
listCertificatesNextToken = listCertificatesResp.NextToken
}
}
// 导入证书
// REF: https://docs.aws.amazon.com/en_us/acm/latest/APIReference/API_ImportCertificate.html
@ -74,10 +134,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
return nil, xerrors.Wrap(err, "failed to execute sdk request 'acm.ImportCertificate'")
}
certId = *importCertificateResp.CertificateArn
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
CertId: *importCertificateResp.CertificateArn,
}, nil
}

View File

@ -0,0 +1,181 @@
package azurekeyvault
import (
"context"
"crypto/x509"
"fmt"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azcore/to"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates"
xerrors "github.com/pkg/errors"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
"github.com/usual2970/certimate/internal/pkg/utils/certs"
azcommon "github.com/usual2970/certimate/internal/pkg/vendors/azure-sdk/common"
)
type UploaderConfig struct {
// Azure TenantId。
TenantId string `json:"tenantId"`
// Azure ClientId。
ClientId string `json:"clientId"`
// Azure ClientSecret。
ClientSecret string `json:"clientSecret"`
// Azure 主权云环境。
CloudName string `json:"cloudName,omitempty"`
// Key Vault 名称。
KeyVaultName string `json:"keyvaultName"`
}
type UploaderProvider struct {
config *UploaderConfig
sdkClient *azcertificates.Client
}
var _ uploader.Uploader = (*UploaderProvider)(nil)
func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
if config == nil {
panic("config is nil")
}
client, err := createSdkClient(config.TenantId, config.ClientId, config.ClientSecret, config.CloudName, config.KeyVaultName)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &UploaderProvider{
config: config,
sdkClient: client,
}, nil
}
func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := certs.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 生成 Azure 业务参数
const TAG_CERTCN = "certimate/cert-cn"
const TAG_CERTSN = "certimate/cert-sn"
certCN := certX509.Subject.CommonName
certSN := certX509.SerialNumber.Text(16)
// 获取证书列表,避免重复上传
// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificates/get-certificates
listCertificatesPager := u.sdkClient.NewListCertificatesPager(nil)
for listCertificatesPager.More() {
page, err := listCertificatesPager.NextPage(context.TODO())
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'keyvault.GetCertificates'")
}
for _, certItem := range page.Value {
// 先对比证书有效期
if certItem.Attributes == nil {
continue
}
if certItem.Attributes.NotBefore == nil || !certItem.Attributes.NotBefore.Equal(certX509.NotBefore) {
continue
}
if certItem.Attributes.Expires == nil || !certItem.Attributes.Expires.Equal(certX509.NotAfter) {
continue
}
// 再对比 Tag 中的通用名称
if v, ok := certItem.Tags[TAG_CERTCN]; !ok || v == nil {
continue
} else if *v != certCN {
continue
}
// 再对比 Tag 中的序列号
if v, ok := certItem.Tags[TAG_CERTSN]; !ok || v == nil {
continue
} else if *v != certSN {
continue
}
// 最后对比证书内容
getCertificateResp, err := u.sdkClient.GetCertificate(context.TODO(), certItem.ID.Name(), certItem.ID.Version(), nil)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'keyvault.GetCertificate'")
} else {
oldCertX509, err := x509.ParseCertificate(getCertificateResp.CER)
if err != nil {
continue
}
if !certs.EqualCertificate(certX509, oldCertX509) {
continue
}
}
// 如果以上信息都一致,则视为已存在相同证书,直接返回
return &uploader.UploadResult{
CertId: string(*certItem.ID),
CertName: certItem.ID.Name(),
}, nil
}
}
// 生成新证书名(需符合 Azure 命名规则)
certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 导入证书
// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/import-certificate/import-certificate
importCertificateParams := azcertificates.ImportCertificateParameters{
Base64EncodedCertificate: to.Ptr(certPem),
CertificatePolicy: &azcertificates.CertificatePolicy{
SecretProperties: &azcertificates.SecretProperties{
ContentType: to.Ptr("application/x-pem-file"),
},
},
Tags: map[string]*string{
TAG_CERTCN: to.Ptr(certCN),
TAG_CERTSN: to.Ptr(certSN),
},
}
importCertificateResp, err := u.sdkClient.ImportCertificate(context.TODO(), certName, importCertificateParams, nil)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'keyvault.ImportCertificate'")
}
return &uploader.UploadResult{
CertId: string(*importCertificateResp.ID),
CertName: certName,
}, nil
}
func createSdkClient(tenantId, clientId, clientSecret, cloudName, keyvaultName string) (*azcertificates.Client, error) {
env, err := azcommon.GetCloudEnvironmentConfiguration(cloudName)
if err != nil {
return nil, err
}
clientOptions := azcore.ClientOptions{Cloud: env}
credential, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret,
&azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions})
if err != nil {
return nil, err
}
endpoint := fmt.Sprintf("https://%s.vault.azure.net", keyvaultName)
if azcommon.IsEnvironmentGovernment(cloudName) {
endpoint = fmt.Sprintf("https://%s.vault.usgovcloudapi.net", keyvaultName)
} else if azcommon.IsEnvironmentChina(cloudName) {
endpoint = fmt.Sprintf("https://%s.vault.azure.cn", keyvaultName)
}
client, err := azcertificates.NewClient(endpoint, credential, nil)
if err != nil {
return nil, err
}
return client, nil
}

View File

@ -76,31 +76,31 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe
}
for _, certDetail := range describeCertsResp.Result.CertListDetails {
// 先尝试匹配 CN
// 先对比证书通用名称
if !strings.EqualFold(certX509.Subject.CommonName, certDetail.CommonName) {
continue
}
// 再尝试匹配 SAN
// 再对比证书多域名
if !slices.Equal(certX509.DNSNames, certDetail.DnsNames) {
continue
}
// 再尝试匹配证书有效期
// 再对比证书有效期
oldCertNotBefore, _ := time.Parse(time.RFC3339, certDetail.StartTime)
oldCertNotAfter, _ := time.Parse(time.RFC3339, certDetail.EndTime)
if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) {
continue
}
// 最后尝试匹配私钥摘要
// 最后对比私钥摘要
newKeyDigest := sha256.Sum256([]byte(privkeyPem))
newKeyDigestHex := hex.EncodeToString(newKeyDigest[:])
if !strings.EqualFold(newKeyDigestHex, certDetail.Digest) {
continue
}
// 如果以上都匹配,则视为已存在相同证书,直接返回已有的证书信息
// 如果以上信息都一致,则视为已存在相同证书,直接返回
return &uploader.UploadResult{
CertId: certDetail.CertId,
CertName: certDetail.CertName,

View File

@ -121,8 +121,8 @@ func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string) (re
if getCertificateListResp.CertificateList != nil {
for _, certInfo := range getCertificateListResp.CertificateList {
// 优刻得未提供可唯一标识证书的字段,只能通过多个字段尝试匹配来判断是否为同一证书
// 先分别匹配证书的域名、品牌、有效期,再匹配签名算法
// 优刻得未提供可唯一标识证书的字段,只能通过多个字段尝试对比来判断是否为同一证书
// 先分别对比证书的多域名、品牌、有效期,再对比签名算法
if len(certX509.DNSNames) == 0 || certInfo.Domains != strings.Join(certX509.DNSNames, ",") {
continue

View File

@ -34,7 +34,7 @@ func IsEnvironmentChina(env string) bool {
}
}
func GetEnvironmentConfiguration(env string) (cloud.Configuration, error) {
func GetCloudEnvironmentConfiguration(env string) (cloud.Configuration, error) {
if IsEnvironmentPublic(env) {
return cloud.AzurePublic, nil
} else if IsEnvironmentGovernment(env) {