feat: add azure keyvault uploader
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user