feat: add ucloud ussl uploader

This commit is contained in:
Fu Diwei 2025-01-14 21:02:08 +08:00
parent 493f2a508b
commit e430109228
8 changed files with 545 additions and 6 deletions

2
go.mod
View File

@ -80,7 +80,9 @@ require (
github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/technoweenie/multipartstreamer v1.0.1 // indirect
github.com/ucloud/ucloud-sdk-go v0.22.31 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.mongodb.org/mongo-driver v1.12.0 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect

5
go.sum
View File

@ -783,10 +783,13 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.3.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo=
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
@ -847,6 +850,8 @@ github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaO
github.com/uber/jaeger-client-go v2.30.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk=
github.com/uber/jaeger-lib v2.4.1+incompatible h1:td4jdvLcExb4cBISKIpHuGoVXh+dVKhn2Um6rjCsSsg=
github.com/uber/jaeger-lib v2.4.1+incompatible/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U=
github.com/ucloud/ucloud-sdk-go v0.22.31 h1:izZK+Re9ZkJAd1fHSVpFzgh8uKda4f5G6++iUw4n/mE=
github.com/ucloud/ucloud-sdk-go v0.22.31/go.mod h1:dyLmFHmUfgb4RZKYQP9IArlvQ2pxzFthfhwxRzOEPIw=
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=

View File

@ -0,0 +1,220 @@
package ucloudussl
import (
"context"
"crypto/md5"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
"time"
xerrors "github.com/pkg/errors"
usdk "github.com/ucloud/ucloud-sdk-go/ucloud"
uAuth "github.com/ucloud/ucloud-sdk-go/ucloud/auth"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
x509util "github.com/usual2970/certimate/internal/pkg/utils/x509"
usdkSsl "github.com/usual2970/certimate/internal/pkg/vendors/ucloud-sdk/ussl"
)
type UCloudUSSLUploaderConfig struct {
// 优刻得 API 私钥。
PrivateKey string `json:"privateKey"`
// 优刻得 API 公钥。
PublicKey string `json:"publicKey"`
// 优刻得项目 ID。
ProjectId string `json:"projectId,omitempty"`
}
type UCloudUSSLUploader struct {
config *UCloudUSSLUploaderConfig
sdkClient *usdkSsl.USSLClient
}
var _ uploader.Uploader = (*UCloudUSSLUploader)(nil)
func New(config *UCloudUSSLUploaderConfig) (*UCloudUSSLUploader, error) {
if config == nil {
return nil, errors.New("config is nil")
}
client, err := createSdkClient(config.PrivateKey, config.PublicKey)
if err != nil {
return nil, xerrors.Wrap(err, "failed to create sdk client")
}
return &UCloudUSSLUploader{
config: config,
sdkClient: client,
}, nil
}
func (u *UCloudUSSLUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 生成新证书名(需符合优刻得命名规则)
var certId, certName string
certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
// 生成优刻得所需的证书参数
certPemBase64 := base64.StdEncoding.EncodeToString([]byte(certPem))
privkeyPemBase64 := base64.StdEncoding.EncodeToString([]byte(privkeyPem))
certMd5 := md5.Sum([]byte(certPemBase64 + privkeyPemBase64))
certMd5Hex := hex.EncodeToString(certMd5[:])
// 上传托管证书
// REF: https://docs.ucloud.cn/api/usslcertificate-api/upload_normal_certificate
uploadNormalCertificateReq := u.sdkClient.NewUploadNormalCertificateRequest()
uploadNormalCertificateReq.CertificateName = usdk.String(certName)
uploadNormalCertificateReq.SslPublicKey = usdk.String(certPemBase64)
uploadNormalCertificateReq.SslPrivateKey = usdk.String(privkeyPemBase64)
uploadNormalCertificateReq.SslMD5 = usdk.String(certMd5Hex)
if u.config.ProjectId != "" {
uploadNormalCertificateReq.ProjectId = usdk.String(u.config.ProjectId)
}
uploadNormalCertificateResp, err := u.sdkClient.UploadNormalCertificate(uploadNormalCertificateReq)
if err != nil {
if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 {
return u.getExistCert(ctx, certPem, privkeyPem)
}
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ussl.UploadNormalCertificate'")
}
certId = fmt.Sprintf("%d", uploadNormalCertificateResp.CertificateID)
return &uploader.UploadResult{
CertId: certId,
CertName: certName,
ExtendedData: map[string]interface{}{
"resourceId": uploadNormalCertificateResp.LongResourceID,
},
}, nil
}
func (u *UCloudUSSLUploader) getExistCert(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 解析证书内容
certX509, err := x509util.ParseCertificateFromPEM(certPem)
if err != nil {
return nil, err
}
// 遍历获取用户证书列表,避免重复上传
// REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list
// REF: https://docs.ucloud.cn/api/usslcertificate-api/download_certificate
getCertificateListPage := int(1)
getCertificateListLimit := int(1000)
for {
getCertificateListReq := u.sdkClient.NewGetCertificateListRequest()
getCertificateListReq.Mode = usdk.String("trust")
getCertificateListReq.Domain = usdk.String(certX509.Subject.CommonName)
getCertificateListReq.Sort = usdk.String("2")
getCertificateListReq.Page = usdk.Int(getCertificateListPage)
getCertificateListReq.PageSize = usdk.Int(getCertificateListLimit)
if u.config.ProjectId != "" {
getCertificateListReq.ProjectId = usdk.String(u.config.ProjectId)
}
getCertificateListResp, err := u.sdkClient.GetCertificateList(getCertificateListReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ussl.GetCertificateList'")
}
if getCertificateListResp.CertificateList != nil {
for _, certInfo := range getCertificateListResp.CertificateList {
// 优刻得未提供可唯一标识证书的字段,只能通过多个字段尝试匹配来判断是否为同一证书
// 先分别匹配证书的域名、品牌、有效期,再匹配签名算法
if len(certX509.DNSNames) == 0 || certInfo.Domains != strings.Join(certX509.DNSNames, ",") {
continue
}
if len(certX509.Issuer.Organization) == 0 || certInfo.Brand != certX509.Issuer.Organization[0] {
continue
}
if int64(certInfo.NotBefore) != certX509.NotBefore.UnixMilli() || int64(certInfo.NotAfter) != certX509.NotAfter.UnixMilli() {
continue
}
getCertificateDetailInfoReq := u.sdkClient.NewGetCertificateDetailInfoRequest()
getCertificateDetailInfoReq.CertificateID = usdk.Int(certInfo.CertificateID)
if u.config.ProjectId != "" {
getCertificateDetailInfoReq.ProjectId = usdk.String(u.config.ProjectId)
}
getCertificateDetailInfoResp, err := u.sdkClient.GetCertificateDetailInfo(getCertificateDetailInfoReq)
if err != nil {
return nil, xerrors.Wrap(err, "failed to execute sdk request 'ussl.GetCertificateDetailInfo'")
}
switch certX509.SignatureAlgorithm {
case x509.SHA256WithRSA:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA256-RSA") {
continue
}
case x509.SHA384WithRSA:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA384-RSA") {
continue
}
case x509.SHA512WithRSA:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA512-RSA") {
continue
}
case x509.SHA256WithRSAPSS:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA256-RSAPSS") {
continue
}
case x509.SHA384WithRSAPSS:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA384-RSAPSS") {
continue
}
case x509.SHA512WithRSAPSS:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "SHA512-RSAPSS") {
continue
}
case x509.ECDSAWithSHA256:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "ECDSA-SHA256") {
continue
}
case x509.ECDSAWithSHA384:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "ECDSA-SHA384") {
continue
}
case x509.ECDSAWithSHA512:
if !strings.EqualFold(getCertificateDetailInfoResp.CertificateInfo.Algorithm, "ECDSA-SHA512") {
continue
}
default:
// 未知签名算法,跳过
continue
}
return &uploader.UploadResult{
CertId: fmt.Sprintf("%d", certInfo.CertificateID),
CertName: certInfo.Name,
ExtendedData: map[string]interface{}{
"resourceId": certInfo.CertificateSN,
},
}, nil
}
}
if getCertificateListResp.CertificateList == nil || len(getCertificateListResp.CertificateList) < int(getCertificateListLimit) {
break
} else {
getCertificateListPage++
}
}
return nil, errors.New("no certificate found")
}
func createSdkClient(privateKey, publicKey string) (*usdkSsl.USSLClient, error) {
cfg := usdk.NewConfig()
credential := uAuth.NewCredential()
credential.PrivateKey = privateKey
credential.PublicKey = publicKey
client := usdkSsl.NewClient(&cfg, &credential)
return client, nil
}

View File

@ -0,0 +1,72 @@
package ucloudussl_test
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"testing"
provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ucloud-ussl"
)
var (
fInputCertPath string
fInputKeyPath string
fPrivateKey string
fPublicKey string
)
func init() {
argsPrefix := "CERTIMATE_UPLOADER_UCLOUDUSSL_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
flag.StringVar(&fPrivateKey, argsPrefix+"PRIVATEKEY", "", "")
flag.StringVar(&fPublicKey, argsPrefix+"PUBLICKEY", "", "")
}
/*
Shell command to run this test:
go test -v ./ucloud_ussl_test.go -args \
--CERTIMATE_UPLOADER_UCLOUDUSSL_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_UCLOUDUSSL_INPUTKEYPATH="/path/to/your-input-key.pem" \
--CERTIMATE_UPLOADER_UCLOUDUSSL_PRIVATEKEY="your-private-key" \
--CERTIMATE_UPLOADER_UCLOUDUSSL_PUBLICKEY="your-public-key"
*/
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("PRIVATEKEY: %v", fPrivateKey),
fmt.Sprintf("PUBLICKEY: %v", fPublicKey),
}, "\n"))
uploader, err := provider.New(&provider.UCloudUSSLUploaderConfig{
PrivateKey: fPrivateKey,
PublicKey: fPublicKey,
})
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))
})
}

View File

@ -9,7 +9,7 @@ import (
veSession "github.com/volcengine/volcengine-go-sdk/volcengine/session"
"github.com/usual2970/certimate/internal/pkg/core/uploader"
veCertCenter "github.com/usual2970/certimate/internal/pkg/vendors/volcengine-sdk/certcenter"
vesdkCc "github.com/usual2970/certimate/internal/pkg/vendors/volcengine-sdk/certcenter"
)
type VolcEngineCertCenterUploaderConfig struct {
@ -23,7 +23,7 @@ type VolcEngineCertCenterUploaderConfig struct {
type VolcEngineCertCenterUploader struct {
config *VolcEngineCertCenterUploaderConfig
sdkClient *veCertCenter.CertCenter
sdkClient *vesdkCc.CertCenter
}
var _ uploader.Uploader = (*VolcEngineCertCenterUploader)(nil)
@ -47,8 +47,8 @@ func New(config *VolcEngineCertCenterUploaderConfig) (*VolcEngineCertCenterUploa
func (u *VolcEngineCertCenterUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) {
// 上传证书
// REF: https://www.volcengine.com/docs/6638/1365580
importCertificateReq := &veCertCenter.ImportCertificateInput{
CertificateInfo: &veCertCenter.ImportCertificateInputCertificateInfo{
importCertificateReq := &vesdkCc.ImportCertificateInput{
CertificateInfo: &vesdkCc.ImportCertificateInputCertificateInfo{
CertificateChain: ve.String(certPem),
PrivateKey: ve.String(privkeyPem),
},
@ -71,7 +71,7 @@ func (u *VolcEngineCertCenterUploader) Upload(ctx context.Context, certPem strin
}, nil
}
func createSdkClient(accessKeyId, accessKeySecret, region string) (*veCertCenter.CertCenter, error) {
func createSdkClient(accessKeyId, accessKeySecret, region string) (*vesdkCc.CertCenter, error) {
if region == "" {
region = "cn-beijing" // 证书中心默认区域:北京
}
@ -83,6 +83,6 @@ func createSdkClient(accessKeyId, accessKeySecret, region string) (*veCertCenter
return nil, err
}
client := veCertCenter.New(session)
client := vesdkCc.New(session)
return client, nil
}

View File

@ -0,0 +1,161 @@
package ussl
import (
"github.com/ucloud/ucloud-sdk-go/ucloud/request"
"github.com/ucloud/ucloud-sdk-go/ucloud/response"
)
type UploadNormalCertificateRequest struct {
request.CommonBase
CertificateName *string `required:"true"`
SslPublicKey *string `required:"true"`
SslPrivateKey *string `required:"true"`
SslMD5 *string `required:"true"`
SslCaKey *string `required:"false"`
}
type UploadNormalCertificateResponse struct {
response.CommonBase
CertificateID int
LongResourceID string
}
func (c *USSLClient) NewUploadNormalCertificateRequest() *UploadNormalCertificateRequest {
req := &UploadNormalCertificateRequest{}
c.Client.SetupRequest(req)
req.SetRetryable(false)
return req
}
func (c *USSLClient) UploadNormalCertificate(req *UploadNormalCertificateRequest) (*UploadNormalCertificateResponse, error) {
var err error
var res UploadNormalCertificateResponse
reqCopier := *req
err = c.Client.InvokeAction("UploadNormalCertificate", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
type GetCertificateListRequest struct {
request.CommonBase
Mode *string `required:"true"`
StateCode *string `required:"false"`
Brand *string `required:"false"`
CaOrganization *string `required:"false"`
Domain *string `required:"false"`
Sort *string `required:"false"`
Page *int `required:"false"`
PageSize *int `required:"false"`
}
type GetCertificateListResponse struct {
response.CommonBase
CertificateList []*CertificateListItem
TotalCount int
}
func (c *USSLClient) NewGetCertificateListRequest() *GetCertificateListRequest {
req := &GetCertificateListRequest{}
c.Client.SetupRequest(req)
req.SetRetryable(false)
return req
}
func (c *USSLClient) GetCertificateList(req *GetCertificateListRequest) (*GetCertificateListResponse, error) {
var err error
var res GetCertificateListResponse
reqCopier := *req
err = c.Client.InvokeAction("GetCertificateList", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
type GetCertificateDetailInfoRequest struct {
request.CommonBase
CertificateID *int `required:"true"`
}
type GetCertificateDetailInfoResponse struct {
response.CommonBase
CertificateInfo *CertificateInfo
}
func (c *USSLClient) NewGetCertificateDetailInfoRequest() *GetCertificateDetailInfoRequest {
req := &GetCertificateDetailInfoRequest{}
c.Client.SetupRequest(req)
req.SetRetryable(false)
return req
}
func (c *USSLClient) GetCertificateDetailInfo(req *GetCertificateDetailInfoRequest) (*GetCertificateDetailInfoResponse, error) {
var err error
var res GetCertificateDetailInfoResponse
reqCopier := *req
err = c.Client.InvokeAction("GetCertificateDetailInfo", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
type DownloadCertificateRequest struct {
request.CommonBase
CertificateID *int `required:"true"`
}
type DownloadCertificateResponse struct {
response.CommonBase
CertificateUrl string
CertCA *CertificateDownloadInfo
Certificate *CertificateDownloadInfo
}
func (c *USSLClient) NewDownloadCertificateRequest() *DownloadCertificateRequest {
req := &DownloadCertificateRequest{}
c.Client.SetupRequest(req)
req.SetRetryable(false)
return req
}
func (c *USSLClient) DownloadCertificate(req *DownloadCertificateRequest) (*DownloadCertificateResponse, error) {
var err error
var res DownloadCertificateResponse
reqCopier := *req
err = c.Client.InvokeAction("DownloadCertificate", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}

View File

@ -0,0 +1,18 @@
package ussl
import (
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
)
type USSLClient struct {
*ucloud.Client
}
func NewClient(config *ucloud.Config, credential *auth.Credential) *USSLClient {
meta := ucloud.ClientMeta{Product: "USSL"}
client := ucloud.NewClientWithMeta(config, credential, meta)
return &USSLClient{
client,
}
}

View File

@ -0,0 +1,61 @@
package ussl
type CertificateListItem struct {
CertificateID int
CertificateSN string
CertificateCat string
Mode string
Domains string
Brand string
ValidityPeriod int
Type string
NotBefore int
NotAfter int
AlarmState int
State string
StateCode string
Name string
MaxDomainsCount int
DomainsCount int
CaChannel string
CSRAlgorithms []CSRAlgorithmInfo
TopOrganizationID int
OrganizationID int
IsFree int
YearOfValidity int
Channel int
CreateTime int
CertificateUrl string
}
type CSRAlgorithmInfo struct {
Algorithm string
AlgorithmOption []string
}
type CertificateInfo struct {
Type string
CertificateID int
CertificateType string
CaOrganization string
Algorithm string
ValidityPeriod int
State string
StateCode string
Name string
Brand string
Domains string
DomainsCount int
Mode string
CSROnline int
CSR string
CSRKeyParameter string
CSREncryptAlgo string
IssuedDate int
ExpiredDate int
}
type CertificateDownloadInfo struct {
FileData string
FileName string
}