Files
cloudpods/pkg/compute/tasks/ssl_certificate/ssl_certificate_create_task.go
2025-07-14 16:06:11 +08:00

315 lines
8.5 KiB
Go

// Copyright 2019 Yunion
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package ssl_certificate
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"net"
"strings"
"time"
"github.com/eggsampler/acme/v3"
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/onecloud/pkg/apis"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/appsrv"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/compute/models"
"yunion.io/x/onecloud/pkg/compute/options"
"yunion.io/x/onecloud/pkg/util/logclient"
)
var (
SSLCertificateCreateWorkerManager *appsrv.SWorkerManager
)
func init() {
SSLCertificateCreateWorkerManager = appsrv.NewWorkerManager("SSLCertificateCreateWorkerManager", 8, 1024, false)
taskman.RegisterTaskAndWorker(SSLCertificateCreateTask{}, SSLCertificateCreateWorkerManager)
}
type SSLCertificateCreateTask struct {
taskman.STask
}
func (self *SSLCertificateCreateTask) taskFailed(ctx context.Context, sc *models.SSSLCertificate, err error) {
sc.SetStatus(ctx, self.UserCred, apis.STATUS_CREATE_FAILED, err.Error())
db.OpsLog.LogEvent(sc, db.ACT_ALLOCATE_FAIL, err, self.UserCred)
logclient.AddActionLogWithStartable(self, sc, logclient.ACT_ALLOCATE, err, self.UserCred, false)
self.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
}
func key2pem(certKey *ecdsa.PrivateKey) ([]byte, error) {
certKeyEnc, err := x509.MarshalECPrivateKey(certKey)
if err != nil {
return nil, errors.Wrapf(err, "MarshalECPrivateKey")
}
return pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: certKeyEnc,
}), nil
}
func (self *SSLCertificateCreateTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
sc := obj.(*models.SSSLCertificate)
zone, err := sc.GetDnsZone()
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "GetDnsZone"))
return
}
iZone, err := zone.GetICloudDnsZone(ctx)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "GetProvider"))
return
}
if len(sc.Issuer) == 0 {
self.taskFailed(ctx, sc, errors.Wrapf(err, "Issuer is required"))
return
}
addr := ""
switch sc.Issuer {
case api.SSL_ISSUER_LETSENCRYPT:
addr = acme.LetsEncryptStaging
case api.SSL_ISSUER_ZEROSSL:
addr = acme.ZeroSSLProduction
}
client, err := acme.NewClient(addr)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "NewClient"))
return
}
client.PollInterval = 10 * time.Second
client.PollTimeout = 6 * time.Minute
privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "GenerateKey"))
return
}
emails := []string{}
for _, email := range options.Options.SSLAccounts {
emails = append(emails, "mailto:"+email)
}
account, err := client.NewAccount(privKey, false, true, emails...)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "NewAccount"))
return
}
domainList := strings.Split(sc.Sans, ",")
var ids []acme.Identifier
for _, domain := range domainList {
ids = append(ids, acme.Identifier{Type: "dns", Value: domain})
}
order, err := client.NewOrder(account, ids)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "NewOrder"))
return
}
for _, authUrl := range order.Authorizations {
auth, err := client.FetchAuthorization(account, authUrl)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "FetchAuthorization"))
return
}
chal, ok := auth.ChallengeMap[acme.ChallengeTypeDNS01]
if !ok {
self.taskFailed(ctx, sc, fmt.Errorf("ChallengeTypeDNS01 not found"))
return
}
txt := acme.EncodeDNS01KeyAuthorization(chal.KeyAuthorization)
info := strings.Split(auth.Identifier.Value, ".")
subDomain := strings.Join(info[:len(info)-2], ".")
subDomain = strings.ReplaceAll(subDomain, "*", "")
dnsName := "_acme-challenge"
if len(subDomain) > 0 {
dnsName = dnsName + "." + subDomain
}
log.Debugf("add dns record for %s: label: %s, txt: %s", auth.Identifier.Value, dnsName, txt)
err = func() error {
opts := &cloudprovider.DnsRecord{
DnsName: dnsName,
DnsValue: txt,
DnsType: cloudprovider.DnsTypeTXT,
Enabled: true,
Ttl: 60,
}
recordId, err := iZone.AddDnsRecord(opts)
if err != nil {
return errors.Wrapf(err, "AddDnsRecord")
}
cloudprovider.Wait(10*time.Second, 3*time.Minute, func() (bool, error) {
v, err := net.LookupTXT("_acme-challenge." + auth.Identifier.Value)
log.Debugf("lookup txt for %s: %s, error: %v", "_acme-challenge."+auth.Identifier.Value, v, err)
if len(v) > 0 {
return true, nil
}
return false, nil
})
defer func() {
record, err := iZone.GetIDnsRecordById(recordId)
if err != nil {
logclient.AddActionLogWithStartable(self, sc, logclient.ACT_UPDATE, errors.Wrapf(err, "GetIDnsRecordById"), self.UserCred, false)
return
}
err = record.Delete()
if err != nil {
logclient.AddActionLogWithStartable(self, sc, logclient.ACT_UPDATE, errors.Wrapf(err, "Delete"), self.UserCred, false)
return
}
}()
chal, err = client.UpdateChallenge(account, chal)
if err != nil {
return errors.Wrapf(err, "UpdateChallenge")
}
return nil
}()
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "AddDnsRecord"))
return
}
}
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "GenerateKey"))
return
}
b, err := key2pem(certKey)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "key2pem"))
return
}
tpl := &x509.CertificateRequest{
SignatureAlgorithm: x509.ECDSAWithSHA256,
PublicKeyAlgorithm: x509.ECDSA,
PublicKey: certKey.Public(),
Subject: pkix.Name{CommonName: domainList[0]},
DNSNames: domainList,
}
csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "CreateCertificateRequest"))
return
}
csr, err := x509.ParseCertificateRequest(csrDer)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "ParseCertificateRequest"))
return
}
order, err = client.FinalizeOrder(account, order, csr)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "FinalizeOrder"))
return
}
certs, err := client.FetchCertificates(account, order.Certificate)
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "FetchCertificates"))
return
}
start, end, country, province, city := time.Time{}, time.Time{}, "", "", ""
var pemData []string
for _, c := range certs {
start = c.NotBefore
end = c.NotAfter
if len(c.Subject.Country) > 0 {
country = c.Subject.Country[0]
}
if len(c.Subject.Province) > 0 {
province = c.Subject.Province[0]
}
if len(c.Subject.Locality) > 0 {
city = c.Subject.Locality[0]
}
pemData = append(pemData, strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
}))))
}
_, err = db.Update(sc, func() error {
sc.Certificate = strings.Join(pemData, "\n")
sc.PrivateKey = string(b)
sc.EndDate = end
sc.StartDate = start
sc.Country = country
sc.Province = province
sc.City = city
sc.Status = apis.STATUS_AVAILABLE
return nil
})
if err != nil {
self.taskFailed(ctx, sc, errors.Wrapf(err, "Update"))
return
}
err = func() error {
provider, err := zone.GetProvider(ctx)
if err != nil {
return errors.Wrapf(err, "GetProvider")
}
opts := &cloudprovider.SSLCertificateCreateOptions{
Certificate: sc.Certificate,
PrivateKey: sc.PrivateKey,
}
_, err = provider.CreateISSLCertificate(opts)
if err != nil {
if errors.Cause(err) == cloudprovider.ErrNotImplemented || errors.Cause(err) == cloudprovider.ErrNotSupported {
return nil
}
return errors.Wrapf(err, "CreateISSLCertificate")
}
return nil
}()
if err != nil {
logclient.AddActionLogWithStartable(self, sc, logclient.ACT_CREATE, err, self.UserCred, false)
}
self.SetStageComplete(ctx, nil)
}