ssl_certificate_create_task.go 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package ssl_certificate
  15. import (
  16. "context"
  17. "crypto/ecdsa"
  18. "crypto/elliptic"
  19. "crypto/rand"
  20. "crypto/x509"
  21. "crypto/x509/pkix"
  22. "encoding/pem"
  23. "fmt"
  24. "net"
  25. "strings"
  26. "time"
  27. "github.com/eggsampler/acme/v3"
  28. "yunion.io/x/cloudmux/pkg/cloudprovider"
  29. "yunion.io/x/jsonutils"
  30. "yunion.io/x/log"
  31. "yunion.io/x/pkg/errors"
  32. "yunion.io/x/onecloud/pkg/apis"
  33. api "yunion.io/x/onecloud/pkg/apis/compute"
  34. "yunion.io/x/onecloud/pkg/appsrv"
  35. "yunion.io/x/onecloud/pkg/cloudcommon/db"
  36. "yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
  37. "yunion.io/x/onecloud/pkg/compute/models"
  38. "yunion.io/x/onecloud/pkg/compute/options"
  39. "yunion.io/x/onecloud/pkg/util/logclient"
  40. )
  41. var (
  42. SSLCertificateCreateWorkerManager *appsrv.SWorkerManager
  43. )
  44. func init() {
  45. SSLCertificateCreateWorkerManager = appsrv.NewWorkerManager("SSLCertificateCreateWorkerManager", 8, 1024, false)
  46. taskman.RegisterTaskAndWorker(SSLCertificateCreateTask{}, SSLCertificateCreateWorkerManager)
  47. }
  48. type SSLCertificateCreateTask struct {
  49. taskman.STask
  50. }
  51. func (self *SSLCertificateCreateTask) taskFailed(ctx context.Context, sc *models.SSSLCertificate, err error) {
  52. sc.SetStatus(ctx, self.UserCred, apis.STATUS_CREATE_FAILED, err.Error())
  53. db.OpsLog.LogEvent(sc, db.ACT_ALLOCATE_FAIL, err, self.UserCred)
  54. logclient.AddActionLogWithStartable(self, sc, logclient.ACT_ALLOCATE, err, self.UserCred, false)
  55. self.SetStageFailed(ctx, jsonutils.NewString(err.Error()))
  56. }
  57. func key2pem(certKey *ecdsa.PrivateKey) ([]byte, error) {
  58. certKeyEnc, err := x509.MarshalECPrivateKey(certKey)
  59. if err != nil {
  60. return nil, errors.Wrapf(err, "MarshalECPrivateKey")
  61. }
  62. return pem.EncodeToMemory(&pem.Block{
  63. Type: "EC PRIVATE KEY",
  64. Bytes: certKeyEnc,
  65. }), nil
  66. }
  67. func (self *SSLCertificateCreateTask) OnInit(ctx context.Context, obj db.IStandaloneModel, body jsonutils.JSONObject) {
  68. sc := obj.(*models.SSSLCertificate)
  69. zone, err := sc.GetDnsZone()
  70. if err != nil {
  71. self.taskFailed(ctx, sc, errors.Wrapf(err, "GetDnsZone"))
  72. return
  73. }
  74. iZone, err := zone.GetICloudDnsZone(ctx)
  75. if err != nil {
  76. self.taskFailed(ctx, sc, errors.Wrapf(err, "GetProvider"))
  77. return
  78. }
  79. if len(sc.Issuer) == 0 {
  80. self.taskFailed(ctx, sc, errors.Wrapf(err, "Issuer is required"))
  81. return
  82. }
  83. addr := ""
  84. switch sc.Issuer {
  85. case api.SSL_ISSUER_LETSENCRYPT:
  86. addr = acme.LetsEncryptProduction
  87. case api.SSL_ISSUER_ZEROSSL:
  88. addr = acme.ZeroSSLProduction
  89. }
  90. client, err := acme.NewClient(addr)
  91. if err != nil {
  92. self.taskFailed(ctx, sc, errors.Wrapf(err, "NewClient"))
  93. return
  94. }
  95. client.PollInterval = 10 * time.Second
  96. client.PollTimeout = 6 * time.Minute
  97. privKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  98. if err != nil {
  99. self.taskFailed(ctx, sc, errors.Wrapf(err, "GenerateKey"))
  100. return
  101. }
  102. emails := []string{}
  103. for _, email := range options.Options.SSLAccounts {
  104. emails = append(emails, "mailto:"+email)
  105. }
  106. account, err := client.NewAccount(privKey, false, true, emails...)
  107. if err != nil {
  108. self.taskFailed(ctx, sc, errors.Wrapf(err, "NewAccount"))
  109. return
  110. }
  111. domainList := strings.Split(sc.Sans, ",")
  112. var ids []acme.Identifier
  113. for _, domain := range domainList {
  114. ids = append(ids, acme.Identifier{Type: "dns", Value: domain})
  115. }
  116. order, err := client.NewOrder(account, ids)
  117. if err != nil {
  118. self.taskFailed(ctx, sc, errors.Wrapf(err, "NewOrder"))
  119. return
  120. }
  121. for _, authUrl := range order.Authorizations {
  122. auth, err := client.FetchAuthorization(account, authUrl)
  123. if err != nil {
  124. self.taskFailed(ctx, sc, errors.Wrapf(err, "FetchAuthorization"))
  125. return
  126. }
  127. chal, ok := auth.ChallengeMap[acme.ChallengeTypeDNS01]
  128. if !ok {
  129. self.taskFailed(ctx, sc, fmt.Errorf("ChallengeTypeDNS01 not found"))
  130. return
  131. }
  132. txt := acme.EncodeDNS01KeyAuthorization(chal.KeyAuthorization)
  133. info := strings.Split(auth.Identifier.Value, ".")
  134. subDomain := strings.Join(info[:len(info)-2], ".")
  135. subDomain = strings.ReplaceAll(subDomain, "*", "")
  136. dnsName := "_acme-challenge"
  137. if len(subDomain) > 0 {
  138. dnsName = dnsName + "." + subDomain
  139. }
  140. log.Debugf("add dns record for %s: label: %s, txt: %s", auth.Identifier.Value, dnsName, txt)
  141. err = func() error {
  142. opts := &cloudprovider.DnsRecord{
  143. DnsName: dnsName,
  144. DnsValue: txt,
  145. DnsType: cloudprovider.DnsTypeTXT,
  146. Enabled: true,
  147. Ttl: 60,
  148. }
  149. recordId, err := iZone.AddDnsRecord(opts)
  150. if err != nil {
  151. return errors.Wrapf(err, "AddDnsRecord")
  152. }
  153. cloudprovider.Wait(10*time.Second, 3*time.Minute, func() (bool, error) {
  154. v, err := net.LookupTXT("_acme-challenge." + auth.Identifier.Value)
  155. log.Debugf("lookup txt for %s: %s, error: %v", "_acme-challenge."+auth.Identifier.Value, v, err)
  156. if len(v) > 0 {
  157. return true, nil
  158. }
  159. return false, nil
  160. })
  161. defer func() {
  162. record, err := iZone.GetIDnsRecordById(recordId)
  163. if err != nil {
  164. logclient.AddActionLogWithStartable(self, sc, logclient.ACT_UPDATE, errors.Wrapf(err, "GetIDnsRecordById"), self.UserCred, false)
  165. return
  166. }
  167. err = record.Delete()
  168. if err != nil {
  169. logclient.AddActionLogWithStartable(self, sc, logclient.ACT_UPDATE, errors.Wrapf(err, "Delete"), self.UserCred, false)
  170. return
  171. }
  172. }()
  173. chal, err = client.UpdateChallenge(account, chal)
  174. if err != nil {
  175. return errors.Wrapf(err, "UpdateChallenge")
  176. }
  177. return nil
  178. }()
  179. if err != nil {
  180. self.taskFailed(ctx, sc, errors.Wrapf(err, "AddDnsRecord"))
  181. return
  182. }
  183. }
  184. certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
  185. if err != nil {
  186. self.taskFailed(ctx, sc, errors.Wrapf(err, "GenerateKey"))
  187. return
  188. }
  189. b, err := key2pem(certKey)
  190. if err != nil {
  191. self.taskFailed(ctx, sc, errors.Wrapf(err, "key2pem"))
  192. return
  193. }
  194. tpl := &x509.CertificateRequest{
  195. SignatureAlgorithm: x509.ECDSAWithSHA256,
  196. PublicKeyAlgorithm: x509.ECDSA,
  197. PublicKey: certKey.Public(),
  198. Subject: pkix.Name{CommonName: domainList[0]},
  199. DNSNames: domainList,
  200. }
  201. csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey)
  202. if err != nil {
  203. self.taskFailed(ctx, sc, errors.Wrapf(err, "CreateCertificateRequest"))
  204. return
  205. }
  206. csr, err := x509.ParseCertificateRequest(csrDer)
  207. if err != nil {
  208. self.taskFailed(ctx, sc, errors.Wrapf(err, "ParseCertificateRequest"))
  209. return
  210. }
  211. order, err = client.FinalizeOrder(account, order, csr)
  212. if err != nil {
  213. self.taskFailed(ctx, sc, errors.Wrapf(err, "FinalizeOrder"))
  214. return
  215. }
  216. certs, err := client.FetchCertificates(account, order.Certificate)
  217. if err != nil {
  218. self.taskFailed(ctx, sc, errors.Wrapf(err, "FetchCertificates"))
  219. return
  220. }
  221. start, end, country, province, city := time.Time{}, time.Time{}, "", "", ""
  222. var pemData []string
  223. for i, c := range certs {
  224. if i == 0 {
  225. start = c.NotBefore
  226. end = c.NotAfter
  227. if len(c.Subject.Country) > 0 {
  228. country = c.Subject.Country[0]
  229. }
  230. if len(c.Subject.Province) > 0 {
  231. province = c.Subject.Province[0]
  232. }
  233. if len(c.Subject.Locality) > 0 {
  234. city = c.Subject.Locality[0]
  235. }
  236. }
  237. pemData = append(pemData, strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{
  238. Type: "CERTIFICATE",
  239. Bytes: c.Raw,
  240. }))))
  241. }
  242. _, err = db.Update(sc, func() error {
  243. sc.Certificate = strings.Join(pemData, "\n")
  244. sc.PrivateKey = string(b)
  245. sc.EndDate = end
  246. sc.StartDate = start
  247. sc.Country = country
  248. sc.Province = province
  249. sc.City = city
  250. sc.Status = apis.STATUS_AVAILABLE
  251. return nil
  252. })
  253. if err != nil {
  254. self.taskFailed(ctx, sc, errors.Wrapf(err, "Update"))
  255. return
  256. }
  257. err = func() error {
  258. provider, err := zone.GetProvider(ctx)
  259. if err != nil {
  260. return errors.Wrapf(err, "GetProvider")
  261. }
  262. opts := &cloudprovider.SSLCertificateCreateOptions{
  263. Name: sc.Name,
  264. DnsZoneId: zone.ExternalId,
  265. Certificate: sc.Certificate,
  266. PrivateKey: sc.PrivateKey,
  267. }
  268. _, err = provider.CreateISSLCertificate(opts)
  269. if err != nil {
  270. if errors.Cause(err) == cloudprovider.ErrNotImplemented || errors.Cause(err) == cloudprovider.ErrNotSupported {
  271. return nil
  272. }
  273. return errors.Wrapf(err, "CreateISSLCertificate")
  274. }
  275. return nil
  276. }()
  277. if err != nil {
  278. logclient.AddActionLogWithStartable(self, sc, logclient.ACT_CREATE, err, self.UserCred, false)
  279. }
  280. self.SetStageComplete(ctx, nil)
  281. }