fix(region): ssl certificate create (#22885)

This commit is contained in:
屈轩
2025-07-14 16:06:11 +08:00
committed by GitHub
parent 8fdf415ff0
commit 16f7c98393
42 changed files with 2980 additions and 76 deletions

View File

@@ -24,6 +24,7 @@ import (
func init() {
cmd := shell.NewResourceCmd(&modules.SslCerticicate).WithKeyword("ssl-certificate")
cmd.List(&compute.SslCertificateListOptions{})
cmd.Create(&compute.SslCertificateCreateOptions{})
cmd.Show(&options.BaseShowOptions{})
cmd.Delete(&options.BaseIdOptions{})
}

5
go.mod
View File

@@ -18,6 +18,7 @@ require (
github.com/creack/pty v1.1.18
github.com/docker/docker v17.12.0-ce-rc1.0.20200916142827-bd33bbf0497b+incompatible
github.com/docker/spdystream v0.0.0-20160310174837-449fdfce4d96
github.com/eggsampler/acme/v3 v3.6.2
github.com/fernet/fernet-go v0.0.0-20180830025343-9eac43b88a5e
github.com/fsnotify/fsnotify v1.4.9
github.com/ghodss/yaml v1.0.0
@@ -95,12 +96,12 @@ require (
k8s.io/cri-api v0.22.17
k8s.io/klog/v2 v2.20.0
moul.io/http2curl/v2 v2.3.0
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250704072708-d4757966c574
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250714064001-1e1f50030bed
yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0
yunion.io/x/jsonutils v1.0.1-0.20250507052344-1abcf4f443b1
yunion.io/x/log v1.0.1-0.20240305175729-7cf2d6cd5a91
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900
yunion.io/x/pkg v1.10.4-0.20250618071935-840c253075b0
yunion.io/x/pkg v1.10.4-0.20250712095850-7797c1f3ffa6
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1
yunion.io/x/sqlchemy v1.1.3-0.20250531010554-ce98f840b833
yunion.io/x/structarg v0.0.0-20231017124457-df4d5009457c

10
go.sum
View File

@@ -295,6 +295,8 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw=
github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M=
github.com/eggsampler/acme/v3 v3.6.2 h1:gvyZbQ92wNQLDASVftGpHEdFwPSfg0+17P0lLt09Tp8=
github.com/eggsampler/acme/v3 v3.6.2/go.mod h1:/qh0rKC/Dh7Jj+p4So7DbWmFNzC4dpcpK53r226Fhuo=
github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc=
github.com/elgatito/upnp v0.0.0-20180711183757-2f244d205f9a h1:2Zw3pxDRTs4nX1WCLAEm27UN0hvjZSge7EaUUQexRZw=
github.com/elgatito/upnp v0.0.0-20180711183757-2f244d205f9a/go.mod h1:afkYpY8JAIL4341N7Zj9xJ5yTovsg6BkWfBFlCzIoF4=
@@ -1406,8 +1408,8 @@ sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q=
sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250704072708-d4757966c574 h1:kglRbPc1P9I7rt3DP7O4pc8ji2JqVvZ4GSD+j52fwSI=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250704072708-d4757966c574/go.mod h1:FXxAEbdNfWXX9gjME3K2nJhkydHY5EKEUZb+RLEzVwQ=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250714064001-1e1f50030bed h1:L34xL83eV2RzpwCjfzOE7ls/5u5EvEjqAL2ToTaD6Tc=
yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250714064001-1e1f50030bed/go.mod h1:FXxAEbdNfWXX9gjME3K2nJhkydHY5EKEUZb+RLEzVwQ=
yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0 h1:msG4SiDSVU7CrXH06WuHlNEZXIooTcmNbfrIGHuIHBU=
yunion.io/x/executor v0.0.0-20250518005516-5402e9e0bed0/go.mod h1:Uxuou9WQIeJXNpy7t2fPLL0BYLvLiMvGQwY7Qc6aSws=
yunion.io/x/jsonutils v0.0.0-20190625054549-a964e1e8a051/go.mod h1:4N0/RVzsYL3kH3WE/H1BjUQdFiWu50JGCFQuuy+Z634=
@@ -1421,8 +1423,8 @@ yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900 h1:Hu/4ERvoWaN6aiFs4h4/yvVB
yunion.io/x/ovsdb v0.0.0-20230306173834-f164f413a900/go.mod h1:0vLkNEhlmA64HViPBAnSTUMrx5QP1CLsxXmxDKQ80tc=
yunion.io/x/pkg v0.0.0-20190620104149-945c25821dbf/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v0.0.0-20190628082551-f4033ba2ea30/go.mod h1:t6rEGG2sQ4J7DhFxSZVOTjNd0YO/KlfWQyK1W4tog+E=
yunion.io/x/pkg v1.10.4-0.20250618071935-840c253075b0 h1:diTsDq4t6jntGdAKtHZYx9eRWkLg0KyMTp0WFdueT+0=
yunion.io/x/pkg v1.10.4-0.20250618071935-840c253075b0/go.mod h1:0Bwxqd9MA3ACi119/l02FprY/o9gHahmYC2bsSbnVpM=
yunion.io/x/pkg v1.10.4-0.20250712095850-7797c1f3ffa6 h1:CzEjNUur33HGmDkW7uKw1eU97gwojNsXlB2AMEzItn8=
yunion.io/x/pkg v1.10.4-0.20250712095850-7797c1f3ffa6/go.mod h1:0Bwxqd9MA3ACi119/l02FprY/o9gHahmYC2bsSbnVpM=
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1 h1:1KJ3YYinydPHpDEQRXdr/T8SYcKZ5Er+m489H+PnaQ4=
yunion.io/x/s3cli v0.0.0-20241221171442-1c11599d28e1/go.mod h1:0iFKpOs1y4lbCxeOmq3Xx/0AcQoewVPwj62eRluioEo=
yunion.io/x/sqlchemy v1.1.3-0.20250531010554-ce98f840b833 h1:XTFC1naKYkciCQDLm9izpzHXfTenmmtYsTpVKrsN5hE=

View File

@@ -14,10 +14,31 @@
package compute
import "yunion.io/x/onecloud/pkg/apis"
import (
"yunion.io/x/cloudmux/pkg/cloudprovider"
"yunion.io/x/onecloud/pkg/apis"
)
const (
SSL_ISSUER_LETSENCRYPT = cloudprovider.SSL_ISSUER_LETSENCRYPT
SSL_ISSUER_ZEROSSL = cloudprovider.SSL_ISSUER_ZEROSSL
)
// 资源创建参数, 目前仅占位
type SSLCertificateCreateInput struct {
apis.VirtualResourceCreateInput
Issuer string `json:"issuer"`
DnsZoneId string `json:"dns_zone_id"`
Sans string `json:"sans"`
Province string
Common string
Country string
City string
OrgName string
Certificate string
PrivateKey string
}
// 资源返回详情

View File

@@ -24,10 +24,13 @@ import (
"yunion.io/x/pkg/util/compare"
"yunion.io/x/sqlchemy"
"yunion.io/x/onecloud/pkg/apis"
api "yunion.io/x/onecloud/pkg/apis/compute"
"yunion.io/x/onecloud/pkg/cloudcommon/db"
"yunion.io/x/onecloud/pkg/cloudcommon/db/lockman"
"yunion.io/x/onecloud/pkg/cloudcommon/db/taskman"
"yunion.io/x/onecloud/pkg/cloudcommon/notifyclient"
"yunion.io/x/onecloud/pkg/cloudcommon/validators"
"yunion.io/x/onecloud/pkg/httperrors"
"yunion.io/x/onecloud/pkg/mcclient"
"yunion.io/x/onecloud/pkg/util/stringutils2"
@@ -67,19 +70,18 @@ type SSSLCertificate struct {
SDeletePreventableResourceBase
Sans string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"`
StartDate time.Time `nullable:"false" list:"user" json:"start_date"`
Province string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"`
Common string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"`
Country string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"`
Issuer string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"`
Expired bool `charset:"utf8" nullable:"false" list:"user" create:"required"`
IsUpload bool `charset:"utf8" nullable:"false" list:"user" create:"required"`
EndDate time.Time `nullable:"false" list:"user" json:"end_date"`
Fingerprint string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"required"`
City string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"`
OrgName string `width:"2048" charset:"utf8" nullable:"false" list:"user" create:"required"`
Certificate string `charset:"utf8" nullable:"true" list:"user" create:"required"`
PrivateKey string `charset:"utf8" nullable:"true" list:"user" create:"required"`
StartDate time.Time `list:"user"`
Province string `width:"64" charset:"utf8" nullable:"false" list:"user" create:"optional"`
Common string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"optional"`
Country string `width:"32" charset:"utf8" nullable:"false" list:"user" create:"optional"`
Issuer string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"required"`
IsUpload bool `list:"user" create:"optional"`
EndDate time.Time `list:"user" create:"optional"`
Fingerprint string `width:"128" charset:"utf8" nullable:"false" list:"user" create:"optional"`
City string `width:"64" charset:"utf8" nullable:"false" list:"user" create:"optional"`
OrgName string `width:"256" charset:"utf8" nullable:"false" list:"user" create:"optional"`
Certificate string `charset:"utf8" nullable:"true" list:"user" create:"optional"`
PrivateKey string `charset:"utf8" nullable:"true" list:"user" create:"optional"`
}
func (s SSSLCertificate) GetExternalId() string {
@@ -188,8 +190,53 @@ func (man *SSSLCertificateManager) QueryDistinctExtraField(q *sqlchemy.SQuery, f
return q, httperrors.ErrNotFound
}
func (man *SSSLCertificateManager) ValidateCreateData(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, input api.SSLCertificateCreateInput) (api.SSLCertificateCreateInput, error) {
return input, httperrors.NewNotImplementedError("Not Implemented")
func (man *SSSLCertificateManager) ValidateCreateData(
ctx context.Context,
userCred mcclient.TokenCredential,
ownerId mcclient.IIdentityProvider,
query jsonutils.JSONObject,
input *api.SSLCertificateCreateInput,
) (*api.SSLCertificateCreateInput, error) {
if len(input.DnsZoneId) > 0 {
obj, err := validators.ValidateModel(ctx, userCred, DnsZoneManager, &input.DnsZoneId)
if err != nil {
return nil, err
}
input.DnsZoneId = obj.GetId()
}
var err error
input.VirtualResourceCreateInput, err = man.SVirtualResourceBaseManager.ValidateCreateData(ctx, userCred, ownerId, query, input.VirtualResourceCreateInput)
if err != nil {
return nil, err
}
return input, nil
}
func (self *SSSLCertificate) PostCreate(ctx context.Context, userCred mcclient.TokenCredential, ownerId mcclient.IIdentityProvider, query jsonutils.JSONObject, data jsonutils.JSONObject) {
self.SVirtualResourceBase.PostCreate(ctx, userCred, ownerId, query, data)
self.StartSSLCertificateCreateTask(ctx, userCred, "")
}
func (self *SSSLCertificate) StartSSLCertificateCreateTask(ctx context.Context, userCred mcclient.TokenCredential, parentTaskId string) error {
params := jsonutils.NewDict()
task, err := taskman.TaskManager.NewTask(ctx, "SSLCertificateCreateTask", self, userCred, params, parentTaskId, "", nil)
if err != nil {
return errors.Wrap(err, "NewTask")
}
self.SetStatus(ctx, userCred, apis.STATUS_CREATING, "")
return task.ScheduleRun(nil)
}
func (self *SSSLCertificate) GetDnsZone() (*SDnsZone, error) {
if len(self.DnsZoneId) == 0 {
return nil, errors.Wrapf(cloudprovider.ErrNotFound, "DnsZoneId is empty")
}
zone, err := DnsZoneManager.FetchById(self.DnsZoneId)
if err != nil {
return nil, errors.Wrapf(err, "DnsZoneManager.FetchById")
}
return zone.(*SDnsZone), nil
}
func (r *SCloudprovider) GetSSLCertificates() ([]SSSLCertificate, error) {
@@ -290,7 +337,7 @@ func (s *SSSLCertificate) SyncWithCloudSSLCertificate(ctx context.Context, userC
s.Common = ext.GetCommon()
s.Country = ext.GetCountry()
s.Issuer = ext.GetIssuer()
s.Expired = ext.GetExpired()
//s.Expired = ext.GetExpired()
s.IsUpload = ext.GetIsUpload()
s.EndDate = ext.GetEndDate()
s.Fingerprint = ext.GetFingerprint()
@@ -349,7 +396,7 @@ func (r *SCloudprovider) newFromCloudSSLCertificate(
s.Common = ext.GetCommon()
s.Country = ext.GetCountry()
s.Issuer = ext.GetIssuer()
s.Expired = ext.GetExpired()
//s.Expired = ext.GetExpired()
s.IsUpload = ext.GetIsUpload()
s.EndDate = ext.GetEndDate()
s.Fingerprint = ext.GetFingerprint()

View File

@@ -235,6 +235,8 @@ type ComputeOptions struct {
NetworkAlwaysManualConfig bool `help:"always manually configure network settings" default:"false"`
SSLAccounts []string `help:"SSL account"`
ComputeEEOptions
}

View File

@@ -48,6 +48,7 @@ import (
_ "yunion.io/x/onecloud/pkg/compute/tasks/server_sku"
_ "yunion.io/x/onecloud/pkg/compute/tasks/snapshot"
_ "yunion.io/x/onecloud/pkg/compute/tasks/snapshotpolicy"
_ "yunion.io/x/onecloud/pkg/compute/tasks/ssl_certificate"
_ "yunion.io/x/onecloud/pkg/compute/tasks/storage"
_ "yunion.io/x/onecloud/pkg/compute/tasks/vpc"
_ "yunion.io/x/onecloud/pkg/compute/tasks/vpc_peering"

View File

@@ -0,0 +1 @@
package ssl_certificate // import "yunion.io/x/onecloud/pkg/compute/tasks/ssl_certificate"

View File

@@ -0,0 +1,314 @@
// 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)
}

View File

@@ -27,3 +27,23 @@ type SslCertificateListOptions struct {
func (opts *SslCertificateListOptions) Params() (jsonutils.JSONObject, error) {
return options.ListStructToParams(opts)
}
type SslCertificateCreateOptions struct {
options.BaseCreateOptions
Issuer string `json:"issuer"`
DnsZoneId string `json:"dns_zone_id"`
Sans string `json:"sans"`
Province string
Common string
Country string
City string
OrgName string
Certificate string
PrivateKey string
}
func (opts *SslCertificateCreateOptions) Params() (jsonutils.JSONObject, error) {
return jsonutils.Marshal(opts), nil
}

3
vendor/github.com/eggsampler/acme/v3/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea/
*.out
coverage*

21
vendor/github.com/eggsampler/acme/v3/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Isaac
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

75
vendor/github.com/eggsampler/acme/v3/Makefile generated vendored Normal file
View File

@@ -0,0 +1,75 @@
.PHONY: test examples clean test_full pebble pebble_setup pebble_start pebble_wait pebble_stop boulder boulder_setup boulder_start boulder_stop
# some variables for path injection, if already set will not override
GOPATH ?= $(HOME)/go
BOULDER_PATH ?= $(GOPATH)/src/github.com/letsencrypt/boulder
PEBBLE_PATH ?= $(GOPATH)/src/github.com/letsencrypt/pebble
TEST_PATH ?= github.com/eggsampler/acme/v3
CLIENT ?= unknown
# tests the code against an already running ca instance
# to actually do a test against pebble or boulder, including , see the 'pebble' or 'boulder' targets
test:
-go clean -testcache
CGO_ENABLED=1 go test -v -race -coverprofile=coverage-$(CLIENT).out -covermode=atomic $(TEST_PATH)
examples:
go build -o /dev/null examples/certbot/certbot.go
go build -o /dev/null examples/autocert/autocert.go
go build -o /dev/null examples/zerossl/zerossl.go
go build -o /dev/null examples/ari/renewalinfo.go
clean:
rm -f coverage*.out
test_full: clean examples pebble pebble_stop boulder boulder_stop
# sets up & runs pebble (in docker), tests, then stops pebble
pebble: CLIENT = pebble
pebble: pebble_setup pebble_start pebble_wait test pebble_stop
pebble_setup:
CLIENT=pebble
mkdir -p $(PEBBLE_PATH)
-git clone --depth 1 https://github.com/letsencrypt/pebble.git $(PEBBLE_PATH)
(cd $(PEBBLE_PATH); git checkout -f main && git reset --hard HEAD && git pull -q)
make pebble_stop
# runs an instance of pebble using docker
pebble_start:
docker-compose -f $(PEBBLE_PATH)/docker-compose.yml up -d
# waits until pebble responds
pebble_wait:
while ! wget --delete-after -q --no-check-certificate "https://localhost:14000/dir" ; do sleep 1 ; done
# stops the running pebble instance
pebble_stop:
docker-compose -f $(PEBBLE_PATH)/docker-compose.yml down
# sets up & runs boulder (in docker), tests, then stops boulder
boulder: CLIENT = boulder
boulder: boulder_setup boulder_start boulder_wait test boulder_stop
# NB: this edits docker-compose.yml
boulder_setup:
mkdir -p $(BOULDER_PATH)
-git clone --depth 1 https://github.com/letsencrypt/boulder.git $(BOULDER_PATH)
(cd $(BOULDER_PATH); git checkout -f main && git reset --hard HEAD && git pull -q)
make boulder_stop
(cd $(BOULDER_PATH); docker compose run --rm bsetup)
# runs an instance of boulder
boulder_start:
docker-compose -f $(BOULDER_PATH)/docker-compose.yml -f $(BOULDER_PATH)/docker-compose.next.yml -f docker-compose.boulder-temp.yml up -d
# waits until boulder responds
boulder_wait:
while ! wget --delete-after -q --no-check-certificate "http://localhost:4001/directory" ; do sleep 1 ; done
# stops the running docker instance
boulder_stop:
docker-compose -f $(BOULDER_PATH)/docker-compose.yml -f $(BOULDER_PATH)/docker-compose.next.yml -f docker-compose.boulder-temp.yml down

43
vendor/github.com/eggsampler/acme/v3/README.md generated vendored Normal file
View File

@@ -0,0 +1,43 @@
# eggsampler/acme
[![GoDoc](https://godoc.org/github.com/eggsampler/acme?status.svg)](https://godoc.org/github.com/eggsampler/acme)
[![Build Status](https://github.com/eggsampler/acme/actions/workflows/go.yml/badge.svg)](https://github.com/eggsampler/acme/actions)
[![Coverage Status](https://coveralls.io/repos/github/eggsampler/acme/badge.svg)](https://coveralls.io/github/eggsampler/acme)
## About
`eggsampler/acme` is a Go client library implementation for [RFC8555](https://tools.ietf.org/html/rfc8555) (previously ACME v2). This library can be used with the [Let's Encrypt](https://letsencrypt.org/) Certificate Authority (CA), but also other ACME compliant CA's such as [ZeroSSL](https://zerossl.com/).
The library is designed to provide a zero external dependency wrapper over exposed directory endpoints and provide objects in easy to use structures.
## Requirements
A Go version of at least 1.11 is required as this repository is designed to be imported as a Go module.
## Usage
Simply import the module into a project,
```go
import "github.com/eggsampler/acme/v3"
```
Note the `/v3` major version at the end. Due to the way modules function, this is the major version as represented in the `go.mod` file and latest git repo [semver](https://semver.org/) tag.
All functions are still exported and called using the `acme` package name.
## Examples
A simple [certbot](https://certbot.eff.org/)-like example is provided in the examples/certbot directory.
This code demonstrates account registration, new order submission, fulfilling challenges, finalising an order and fetching the issued certificate chain.
An example of how to use the autocert package is also provided in examples/autocert.
## Tests
The tests can be run against an instance of [boulder](https://github.com/letsencrypt/boulder) or [pebble](https://github.com/letsencrypt/pebble).
Challenge fulfilment is designed to use the new `challtestsrv` server present inside boulder and pebble which responds to dns queries and challenges as required.
To run tests against an already running instance of boulder or pebble, use the `test` target in the Makefile.
Some convenience targets for launching pebble/boulder using their respective docker compose files have also been included in the Makefile.

35
vendor/github.com/eggsampler/acme/v3/THIRD-PARTY generated vendored Normal file
View File

@@ -0,0 +1,35 @@
This document contains Third Party Software Notices and/or Additional Terms and Conditions for licensed third party software components included within this product.
==
https://github.com/golang/crypto/blob/master/acme/jws.go
https://github.com/golang/crypto/blob/master/acme/jws_test.go
(with modifications)
Copyright (c) 2009 The Go Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following disclaimer
in the documentation and/or other materials provided with the
distribution.
* Neither the name of Google Inc. nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

144
vendor/github.com/eggsampler/acme/v3/account.go generated vendored Normal file
View File

@@ -0,0 +1,144 @@
package acme
import (
"crypto"
"encoding/json"
"errors"
"fmt"
"net/http"
"reflect"
)
// NewAccount registers a new account with the acme service
// Note this function is essentially deprecated and only present for backwards compatibility.
// New programs should implement NewAccountOptions instead.
func (c Client) NewAccount(privateKey crypto.Signer, onlyReturnExisting, termsOfServiceAgreed bool, contact ...string) (Account, error) {
var opts []NewAccountOptionFunc
if onlyReturnExisting {
opts = append(opts, NewAcctOptOnlyReturnExisting())
}
if termsOfServiceAgreed {
opts = append(opts, NewAcctOptAgreeTOS())
}
if len(contact) > 0 {
opts = append(opts, NewAcctOptWithContacts(contact...))
}
return c.NewAccountOptions(privateKey, opts...)
}
// NewAccountOptions registers an account with an acme server with the provided options.
func (c Client) NewAccountOptions(privateKey crypto.Signer, options ...NewAccountOptionFunc) (Account, error) {
newAccountReq := NewAccountRequest{}
account := Account{}
for _, opt := range options {
if err := opt(privateKey, &account, &newAccountReq, c); err != nil {
return account, err
}
}
resp, err := c.post(c.dir.NewAccount, "", privateKey, newAccountReq, &account, http.StatusOK, http.StatusCreated)
if err != nil {
return account, err
}
account.URL = resp.Header.Get("Location")
account.PrivateKey = privateKey
if account.Thumbprint == "" {
account.Thumbprint, err = JWKThumbprint(account.PrivateKey.Public())
if err != nil {
return account, fmt.Errorf("acme: error computing account thumbprint: %v", err)
}
}
return account, nil
}
// UpdateAccount updates an existing account with the acme service.
func (c Client) UpdateAccount(account Account, contact ...string) (Account, error) {
var updateAccountReq interface{}
if !reflect.DeepEqual(account.Contact, contact) {
// Only provide a non-nil updateAccountReq when there is an update to be made.
updateAccountReq = struct {
Contact []string `json:"contact,omitempty"`
}{
Contact: contact,
}
} else {
// Otherwise use "" to trigger a POST-as-GET to fetch up-to-date account
// information from the acme service.
updateAccountReq = ""
}
_, err := c.post(account.URL, account.URL, account.PrivateKey, updateAccountReq, &account, http.StatusOK)
if err != nil {
return account, err
}
if account.Thumbprint == "" {
account.Thumbprint, err = JWKThumbprint(account.PrivateKey.Public())
if err != nil {
return account, fmt.Errorf("acme: error computing account thumbprint: %v", err)
}
}
return account, nil
}
// AccountKeyChange rolls over an account to a new key.
func (c Client) AccountKeyChange(account Account, newPrivateKey crypto.Signer) (Account, error) {
oldJwkKeyPub, err := jwkEncode(account.PrivateKey.Public())
if err != nil {
return account, fmt.Errorf("acme: error encoding new private key: %v", err)
}
keyChangeReq := struct {
Account string `json:"account"`
OldKey json.RawMessage `json:"oldKey"`
}{
Account: account.URL,
OldKey: []byte(oldJwkKeyPub),
}
innerJws, err := jwsEncodeJSON(keyChangeReq, newPrivateKey, "", "", c.dir.KeyChange)
if err != nil {
return account, fmt.Errorf("acme: error encoding inner jws: %v", err)
}
if _, err := c.post(c.dir.KeyChange, account.URL, account.PrivateKey, json.RawMessage(innerJws), nil, http.StatusOK); err != nil {
return account, err
}
account.PrivateKey = newPrivateKey
return account, nil
}
// DeactivateAccount deactivates a given account.
func (c Client) DeactivateAccount(account Account) (Account, error) {
deactivateReq := struct {
Status string `json:"status"`
}{
Status: "deactivated",
}
_, err := c.post(account.URL, account.URL, account.PrivateKey, deactivateReq, &account, http.StatusOK)
return account, err
}
// FetchOrderList fetches a list of orders from the account url provided in the account Orders field
func (c Client) FetchOrderList(account Account) (OrderList, error) {
orderList := OrderList{}
if account.Orders == "" {
return orderList, errors.New("no order list for account")
}
_, err := c.post(account.Orders, account.URL, account.PrivateKey, "", &orderList, http.StatusOK)
return orderList, err
}

303
vendor/github.com/eggsampler/acme/v3/acme.go generated vendored Normal file
View File

@@ -0,0 +1,303 @@
package acme
import (
"bytes"
"crypto"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"os"
"regexp"
"strings"
"time"
)
const (
// LetsEncryptProduction holds the production directory url
LetsEncryptProduction = "https://acme-v02.api.letsencrypt.org/directory"
// LetsEncryptStaging holds the staging directory url
LetsEncryptStaging = "https://acme-staging-v02.api.letsencrypt.org/directory"
// ZeroSSLProduction holds the ZeroSSL directory url
ZeroSSLProduction = "https://acme.zerossl.com/v2/DV90"
userAgentString = "eggsampler-acme/v3 Go-http-client/1.1"
)
// NewClient creates a new acme client given a valid directory url.
func NewClient(directoryURL string, options ...OptionFunc) (Client, error) {
// Set a default http timeout of 60 seconds, this can be overridden
// via an OptionFunc eg: acme.NewClient(url, WithHTTPTimeout(10 * time.Second))
httpClient := &http.Client{
Timeout: 60 * time.Second,
}
acmeClient := Client{
httpClient: httpClient,
nonces: &nonceStack{},
retryCount: 5,
}
acmeClient.dir.URL = directoryURL
for _, opt := range options {
if err := opt(&acmeClient); err != nil {
return acmeClient, fmt.Errorf("acme: error setting option: %v", err)
}
}
if _, err := acmeClient.get(directoryURL, &acmeClient.dir, http.StatusOK); err != nil {
return acmeClient, err
}
return acmeClient, nil
}
// Directory is the object returned by the client connecting to a directory url.
func (c Client) Directory() Directory {
return c.dir
}
// Helper function to get the poll interval and poll timeout, defaulting if 0
func (c Client) getPollingDurations() (time.Duration, time.Duration) {
pollInterval := c.PollInterval
if pollInterval == 0 {
pollInterval = 500 * time.Millisecond
}
pollTimeout := c.PollTimeout
if pollTimeout == 0 {
pollTimeout = 30 * time.Second
}
return pollInterval, pollTimeout
}
// Helper function to have a central point for performing http requests. Stores
// any returned nonces in the stack. The caller is responsible for closing the
// body so they can read the response.
func (c Client) do(req *http.Request, addNonce bool) (*http.Response, error) {
// identifier for this client, as well as the default go user agent
if c.userAgentSuffix != "" {
req.Header.Set("User-Agent", userAgentString+" "+c.userAgentSuffix)
} else {
req.Header.Set("User-Agent", userAgentString)
}
if c.acceptLanguage != "" {
req.Header.Set("Accept-Language", c.acceptLanguage)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return resp, err
}
if addNonce {
c.nonces.push(resp.Header.Get("Replay-Nonce"))
}
return resp, nil
}
// Helper function to perform an HTTP get request and read the body. The caller
// is responsible for closing the body so they can read the response.
func (c Client) getRaw(url string, expectedStatus ...int) (*http.Response, []byte, error) {
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
return nil, nil, fmt.Errorf("acme: error creating request: %v", err)
}
resp, err := c.do(req, true)
if err != nil {
return resp, nil, fmt.Errorf("acme: error fetching response: %v", err)
}
defer resp.Body.Close()
if err := checkError(resp, expectedStatus...); err != nil {
return resp, nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return resp, body, fmt.Errorf("acme: error reading response body: %v", err)
}
return resp, body, nil
}
// Helper function for performing a http get on an acme resource. The caller is
// responsible for closing the body so they can read the response.
func (c Client) get(url string, out interface{}, expectedStatus ...int) (*http.Response, error) {
resp, body, err := c.getRaw(url, expectedStatus...)
if err != nil {
return resp, err
}
if len(body) > 0 && out != nil {
if err := json.Unmarshal(body, out); err != nil {
return resp, fmt.Errorf("acme: error parsing response body: %v", err)
}
}
return resp, nil
}
func (c Client) nonce() (string, error) {
nonce := c.nonces.pop()
if nonce != "" {
return nonce, nil
}
if c.dir.NewNonce == "" {
return "", errors.New("acme: no new nonce url")
}
req, err := http.NewRequest("HEAD", c.dir.NewNonce, nil)
if err != nil {
return "", fmt.Errorf("acme: error creating new nonce request: %v", err)
}
resp, err := c.do(req, false)
if err != nil {
return "", fmt.Errorf("acme: error fetching new nonce: %v", err)
}
nonce = resp.Header.Get("Replay-Nonce")
return nonce, nil
}
// Helper function to perform an HTTP post request and read the body. Will
// attempt to retry if error is badNonce. The caller is responsible for closing
// the body so they can read the response.
func (c Client) postRaw(retryCount int, requestURL, kid string, privateKey crypto.Signer, payload interface{}, expectedStatus []int) (*http.Response, []byte, error) {
nonce, err := c.nonce()
if err != nil {
return nil, nil, err
}
data, err := jwsEncodeJSON(payload, privateKey, KeyID(kid), nonce, requestURL)
if err != nil {
return nil, nil, fmt.Errorf("acme: error encoding json payload: %v", err)
}
req, err := http.NewRequest(http.MethodPost, requestURL, bytes.NewReader(data))
if err != nil {
return nil, nil, fmt.Errorf("acme: error creating request: %v", err)
}
req.Header.Set("Content-Type", "application/jose+json")
resp, err := c.do(req, true)
if err != nil {
return resp, nil, fmt.Errorf("acme: error sending request: %v", err)
}
defer resp.Body.Close()
if err := checkError(resp, expectedStatus...); err != nil {
prob, ok := err.(Problem)
if !ok {
// don't retry for an error we don't know about
return resp, nil, err
}
if retryCount >= c.retryCount {
// don't attempt to retry if too many retries
return resp, nil, err
}
if strings.HasSuffix(prob.Type, ":badNonce") {
// only retry if error is badNonce
return c.postRaw(retryCount+1, requestURL, kid, privateKey, payload, expectedStatus)
}
return resp, nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return resp, body, fmt.Errorf("acme: error reading response body: %v", err)
}
return resp, body, nil
}
// Helper function for performing a http post to an acme resource. The caller is
// responsible for closing the body so they can read the response.
func (c Client) post(requestURL, keyID string, privateKey crypto.Signer, payload interface{}, out interface{}, expectedStatus ...int) (*http.Response, error) {
resp, body, err := c.postRaw(0, requestURL, keyID, privateKey, payload, expectedStatus)
if err != nil {
return resp, err
}
if _, b := os.LookupEnv("ACME_DEBUG_POST"); b {
fmt.Println()
fmt.Println("========= " + requestURL)
fmt.Println(string(body))
fmt.Println()
}
if len(body) > 0 && out != nil {
if err := json.Unmarshal(body, out); err != nil {
return resp, fmt.Errorf("acme: error parsing response: %v - %s", err, string(body))
}
}
return resp, nil
}
var regLink = regexp.MustCompile(`<(.+?)>;\s*rel="(.+?)"`)
// Fetches a http Link header from an http response and closes the body.
func fetchLink(resp *http.Response, wantedLink string) string {
if resp == nil {
return ""
}
linkHeader := resp.Header["Link"]
if len(linkHeader) == 0 {
return ""
}
for _, l := range linkHeader {
matches := regLink.FindAllStringSubmatch(l, -1)
for _, m := range matches {
if len(m) != 3 {
continue
}
if m[2] == wantedLink {
return m[1]
}
}
}
return ""
}
// Fetch is a helper function to assist with POST-AS-GET requests
func (c Client) Fetch(account Account, requestURL string, result interface{}, expectedStatus ...int) error {
if len(expectedStatus) == 0 {
expectedStatus = []int{http.StatusOK}
}
_, err := c.post(requestURL, account.URL, account.PrivateKey, "", result, expectedStatus...)
return err
}
// Fetches all http Link header from a http response
func fetchLinks(resp *http.Response, wantedLink string) []string {
if resp == nil {
return nil
}
linkHeader := resp.Header["Link"]
if len(linkHeader) == 0 {
return nil
}
var links []string
for _, l := range linkHeader {
matches := regLink.FindAllStringSubmatch(l, -1)
for _, m := range matches {
if len(m) != 3 {
continue
}
if m[2] == wantedLink {
links = append(links, m[1])
}
}
}
return links
}

131
vendor/github.com/eggsampler/acme/v3/ari.go generated vendored Normal file
View File

@@ -0,0 +1,131 @@
package acme
import (
_ "crypto/sha1"
_ "crypto/sha256"
_ "crypto/sha512"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"fmt"
"math/rand"
"net/http"
"strconv"
"strings"
"time"
)
// GetRenewalInfo returns the renewal information (if present and supported by
// the ACME server), and a Retry-After time if indicated in the http response
// header.
func (c Client) GetRenewalInfo(cert *x509.Certificate) (RenewalInfo, error) {
if c.dir.RenewalInfo == "" {
return RenewalInfo{}, ErrRenewalInfoNotSupported
}
certID, err := GenerateARICertID(cert)
if err != nil {
return RenewalInfo{}, fmt.Errorf("acme: error generating certificate id: %v", err)
}
renewalURL := c.dir.RenewalInfo
if !strings.HasSuffix(renewalURL, "/") {
renewalURL += "/"
}
renewalURL += certID
var ri RenewalInfo
resp, err := c.get(renewalURL, &ri, http.StatusOK)
if err != nil {
return ri, err
}
defer resp.Body.Close()
ri.RetryAfter, err = parseRetryAfter(resp.Header.Get("Retry-After"))
return ri, err
}
// GenerateARICertID constructs a certificate identifier as described in
// draft-ietf-acme-ari-03, section 4.1.
func GenerateARICertID(cert *x509.Certificate) (string, error) {
if cert == nil {
return "", fmt.Errorf("certificate not found")
}
derBytes, err := asn1.Marshal(cert.SerialNumber)
if err != nil {
return "", err
}
if len(derBytes) < 3 {
return "", fmt.Errorf("invalid DER encoding of serial number")
}
// Extract only the integer bytes from the DER encoded Serial Number
// Skipping the first 2 bytes (tag and length). The result is base64url
// encoded without padding.
serial := base64.RawURLEncoding.EncodeToString(derBytes[2:])
// Convert the Authority Key Identifier to base64url encoding without
// padding.
aki := base64.RawURLEncoding.EncodeToString(cert.AuthorityKeyId)
// Construct the final identifier by concatenating AKI and Serial Number.
return fmt.Sprintf("%s.%s", aki, serial), nil
}
func (r RenewalInfo) ShouldRenewAt(now time.Time, willingToSleep time.Duration) *time.Time {
// Explicitly convert all times to UTC.
now = now.UTC()
start := r.SuggestedWindow.Start.UTC()
end := r.SuggestedWindow.End.UTC()
// Select a uniform random time within the suggested window.
window := end.Sub(start)
randomDuration := time.Duration(rand.Int63n(int64(window)))
randomTime := start.Add(randomDuration)
// If the selected time is in the past, attempt renewal immediately.
if randomTime.Before(now) {
return &now
}
// Otherwise, if the client can schedule itself to attempt renewal at
// exactly the selected time, do so.
willingToSleepUntil := now.Add(willingToSleep)
if willingToSleepUntil.After(randomTime) || willingToSleepUntil.Equal(randomTime) {
return &randomTime
}
return nil
}
// timeNow and implementations support testing
type timeNow interface {
Now() time.Time
}
type currentTimeNow struct{}
func (currentTimeNow) Now() time.Time {
return time.Now()
}
var systemTime timeNow = currentTimeNow{}
func parseRetryAfter(ra string) (time.Time, error) {
retryAfterString := strings.TrimSpace(ra)
if len(retryAfterString) == 0 {
return time.Time{}, nil
}
if retryAfterTime, err := time.Parse(time.RFC1123, retryAfterString); err == nil {
return retryAfterTime, nil
}
if retryAfterInt, err := strconv.Atoi(retryAfterString); err == nil {
return systemTime.Now().Add(time.Second * time.Duration(retryAfterInt)), nil
}
return time.Time{}, fmt.Errorf("invalid time format: %s", retryAfterString)
}

43
vendor/github.com/eggsampler/acme/v3/authorization.go generated vendored Normal file
View File

@@ -0,0 +1,43 @@
package acme
import "net/http"
// FetchAuthorization fetches an authorization from an authorization url provided in an order.
func (c Client) FetchAuthorization(account Account, authURL string) (Authorization, error) {
authResp := Authorization{}
_, err := c.post(authURL, account.URL, account.PrivateKey, "", &authResp, http.StatusOK)
if err != nil {
return authResp, err
}
for i := 0; i < len(authResp.Challenges); i++ {
if authResp.Challenges[i].KeyAuthorization == "" {
authResp.Challenges[i].KeyAuthorization = authResp.Challenges[i].Token + "." + account.Thumbprint
}
}
authResp.ChallengeMap = map[string]Challenge{}
authResp.ChallengeTypes = []string{}
for _, c := range authResp.Challenges {
authResp.ChallengeMap[c.Type] = c
authResp.ChallengeTypes = append(authResp.ChallengeTypes, c.Type)
}
authResp.URL = authURL
return authResp, nil
}
// DeactivateAuthorization deactivate a provided authorization url from an order.
func (c Client) DeactivateAuthorization(account Account, authURL string) (Authorization, error) {
deactivateReq := struct {
Status string `json:"status"`
}{
Status: "deactivated",
}
deactivateResp := Authorization{}
_, err := c.post(authURL, account.URL, account.PrivateKey, deactivateReq, &deactivateResp, http.StatusOK)
return deactivateResp, err
}

435
vendor/github.com/eggsampler/acme/v3/autocert.go generated vendored Normal file
View File

@@ -0,0 +1,435 @@
package acme
// Similar to golang.org/x/crypto/acme/autocert
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"
"io/ioutil"
"net/http"
"path"
"strings"
"sync"
)
// HostCheck function prototype to implement for checking hosts against before issuing certificates
type HostCheck func(host string) error
// WhitelistHosts implements a simple whitelist HostCheck
func WhitelistHosts(hosts ...string) HostCheck {
m := map[string]bool{}
for _, v := range hosts {
m[v] = true
}
return func(host string) error {
if !m[host] {
return errors.New("autocert: host not whitelisted")
}
return nil
}
}
// AutoCert is a stateful certificate manager for issuing certificates on connecting hosts
type AutoCert struct {
// Acme directory Url
// If nil, uses `LetsEncryptStaging`
DirectoryURL string
// Options contains the options used for creating the acme client
Options []OptionFunc
// A function to check whether a host is allowed or not
// If nil, all hosts allowed
// Use `WhitelistHosts(hosts ...string)` for a simple white list of hostnames
HostCheck HostCheck
// Cache dir to store account data and certificates
// If nil, does not write cache data to file
CacheDir string
// When using a staging environment, include a root certificate for verification purposes
RootCert string
// Called before updating challenges
PreUpdateChallengeHook func(Account, Challenge)
// Mapping of token -> keyauth
// Protected by a mutex, but not rwmutex because tokens are deleted once read
tokensLock sync.RWMutex
tokens map[string][]byte
// Mapping of cache key -> value
cacheLock sync.Mutex
cache map[string][]byte
// read lock around getting existing certs
// write lock around issuing new certificate
certLock sync.RWMutex
client Client
}
// HTTPHandler Wraps a handler and provides serving of http-01 challenge tokens from /.well-known/acme-challenge/
// If handler is nil, will redirect all traffic otherwise to https
func (m *AutoCert) HTTPHandler(handler http.Handler) http.Handler {
if handler == nil {
handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "https://"+r.Host+r.URL.RequestURI(), http.StatusMovedPermanently)
})
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
handler.ServeHTTP(w, r)
return
}
if err := m.checkHost(r.Host); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
token := path.Base(r.URL.Path)
m.tokensLock.RLock()
defer m.tokensLock.RUnlock()
keyAuth := m.tokens[token]
if len(keyAuth) == 0 {
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
return
}
_, _ = w.Write(keyAuth)
})
}
// GetCertificate implements a tls.Config.GetCertificate hook
func (m *AutoCert) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
name := strings.TrimSuffix(hello.ServerName, ".")
if name == "" {
return nil, errors.New("autocert: missing server name")
}
if !strings.Contains(strings.Trim(name, "."), ".") {
return nil, errors.New("autocert: server name component count invalid")
}
if strings.ContainsAny(name, `/\`) {
return nil, errors.New("autocert: server name contains invalid character")
}
// check the hostname is allowed
if err := m.checkHost(name); err != nil {
return nil, err
}
// check if there's an existing cert
m.certLock.RLock()
existingCert, _ := m.getExistingCert(name)
m.certLock.RUnlock()
if existingCert != nil {
return existingCert, nil
}
// if not, attempt to issue a new cert
m.certLock.Lock()
defer m.certLock.Unlock()
return m.issueCert(name)
}
func (m *AutoCert) getDirectoryURL() string {
if m.DirectoryURL != "" {
return m.DirectoryURL
}
return LetsEncryptStaging
}
func (m *AutoCert) getCache(keys ...string) []byte {
key := strings.Join(keys, "-")
m.cacheLock.Lock()
defer m.cacheLock.Unlock()
b := m.cache[key]
if len(b) > 0 {
return b
}
if m.CacheDir == "" {
return nil
}
b, _ = ioutil.ReadFile(path.Join(m.CacheDir, key))
if len(b) == 0 {
return nil
}
if m.cache == nil {
m.cache = map[string][]byte{}
}
m.cache[key] = b
return b
}
func (m *AutoCert) putCache(data []byte, keys ...string) context.Context {
ctx, cancel := context.WithCancel(context.Background())
key := strings.Join(keys, "-")
m.cacheLock.Lock()
defer m.cacheLock.Unlock()
if m.cache == nil {
m.cache = map[string][]byte{}
}
m.cache[key] = data
if m.CacheDir == "" {
cancel()
return ctx
}
go func() {
_ = ioutil.WriteFile(path.Join(m.CacheDir, key), data, 0700)
cancel()
}()
return ctx
}
func (m *AutoCert) checkHost(name string) error {
if m.HostCheck == nil {
return nil
}
return m.HostCheck(name)
}
func (m *AutoCert) getExistingCert(name string) (*tls.Certificate, error) {
// check for a stored cert
certData := m.getCache("cert", name)
if len(certData) == 0 {
return nil, errors.New("autocert: no existing certificate")
}
privBlock, pubData := pem.Decode(certData)
if len(pubData) == 0 {
return nil, errors.New("autocert: no public key data (cert/issuer)")
}
// decode pub chain
var pubDER [][]byte
var pub []byte
for len(pubData) > 0 {
var b *pem.Block
b, pubData = pem.Decode(pubData)
if b == nil {
break
}
pubDER = append(pubDER, b.Bytes)
pub = append(pub, b.Bytes...)
}
if len(pubData) > 0 {
return nil, errors.New("autocert: leftover data in file - possibly corrupt")
}
certs, err := x509.ParseCertificates(pub)
if err != nil {
return nil, fmt.Errorf("autocert: bad certificate: %v", err)
}
leaf := certs[0]
// add any intermediate certs if present
var intermediates *x509.CertPool
if len(certs) > 1 {
intermediates = x509.NewCertPool()
for i := 1; i < len(certs); i++ {
intermediates.AddCert(certs[i])
}
}
// add a root certificate if present
var roots *x509.CertPool
if m.RootCert != "" {
block, rest := pem.Decode([]byte(m.RootCert))
for block != nil {
rootCert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, errors.New("autocert: error parsing root certificate")
}
if roots == nil {
roots = x509.NewCertPool()
}
roots.AddCert(rootCert)
block, rest = pem.Decode(rest)
}
}
opts := x509.VerifyOptions{
DNSName: name,
Intermediates: intermediates,
Roots: roots,
}
if _, err := leaf.Verify(opts); err != nil {
return nil, fmt.Errorf("autocert: unable to verify: %v", err)
}
privKey, err := x509.ParseECPrivateKey(privBlock.Bytes)
if err != nil {
return nil, errors.New("autocert: invalid private key")
}
return &tls.Certificate{
Certificate: pubDER,
PrivateKey: privKey,
Leaf: leaf,
}, nil
}
func (m *AutoCert) issueCert(domainName string) (*tls.Certificate, error) {
// attempt to load an existing account key
var privKey *ecdsa.PrivateKey
if keyData := m.getCache("account"); len(keyData) > 0 {
block, _ := pem.Decode(keyData)
x509Encoded := block.Bytes
privKey, _ = x509.ParseECPrivateKey(x509Encoded)
}
// otherwise generate a new one
if privKey == nil {
var err error
privKey, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("autocert: error generating new account key: %v", err)
}
x509Encoded, _ := x509.MarshalECPrivateKey(privKey)
pemEncoded := pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: x509Encoded})
m.putCache(pemEncoded, "account")
}
// create a new client if one doesn't exist
if m.client.Directory().URL == "" {
var err error
m.client, err = NewClient(m.getDirectoryURL(), m.Options...)
if err != nil {
return nil, err
}
}
// create/fetch acme account
account, err := m.client.NewAccount(privKey, false, true)
if err != nil {
return nil, fmt.Errorf("autocert: error creating/fetching account: %v", err)
}
// start a new order process
order, err := m.client.NewOrderDomains(account, domainName)
if err != nil {
return nil, fmt.Errorf("autocert: error creating new order for domain %s: %v", domainName, err)
}
// loop through each of the provided authorization Urls
for _, authURL := range order.Authorizations {
auth, err := m.client.FetchAuthorization(account, authURL)
if err != nil {
return nil, fmt.Errorf("autocert: error fetching authorization Url %q: %v", authURL, err)
}
if auth.Status == "valid" {
continue
}
chal, ok := auth.ChallengeMap[ChallengeTypeHTTP01]
if !ok {
return nil, fmt.Errorf("autocert: unable to find http-01 challenge for auth %s, Url: %s", auth.Identifier.Value, authURL)
}
m.tokensLock.Lock()
if m.tokens == nil {
m.tokens = map[string][]byte{}
}
m.tokens[chal.Token] = []byte(chal.KeyAuthorization)
m.tokensLock.Unlock()
if m.PreUpdateChallengeHook != nil {
m.PreUpdateChallengeHook(account, chal)
}
chal, err = m.client.UpdateChallenge(account, chal)
if err != nil {
return nil, fmt.Errorf("autocert: error updating authorization %s challenge (Url: %s) : %v", auth.Identifier.Value, authURL, err)
}
m.tokensLock.Lock()
delete(m.tokens, chal.Token)
m.tokensLock.Unlock()
}
// generate private key for cert
certKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("autocert: error generating certificate key for %s: %v", domainName, err)
}
certKeyEnc, err := x509.MarshalECPrivateKey(certKey)
if err != nil {
return nil, fmt.Errorf("autocert: error encoding certificate key for %s: %v", domainName, err)
}
certKeyPem := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: certKeyEnc,
})
// create the new csr template
tpl := &x509.CertificateRequest{
SignatureAlgorithm: x509.ECDSAWithSHA256,
PublicKeyAlgorithm: x509.ECDSA,
PublicKey: certKey.Public(),
Subject: pkix.Name{CommonName: domainName},
DNSNames: []string{domainName},
}
csrDer, err := x509.CreateCertificateRequest(rand.Reader, tpl, certKey)
if err != nil {
return nil, fmt.Errorf("autocert: error creating certificate request for %s: %v", domainName, err)
}
csr, err := x509.ParseCertificateRequest(csrDer)
if err != nil {
return nil, fmt.Errorf("autocert: error parsing certificate request for %s: %v", domainName, err)
}
// finalize the order with the acme server given a csr
order, err = m.client.FinalizeOrder(account, order, csr)
if err != nil {
return nil, fmt.Errorf("autocert: error finalizing order for %s: %v", domainName, err)
}
// fetch the certificate chain from the finalized order provided by the acme server
certs, err := m.client.FetchCertificates(account, order.Certificate)
if err != nil {
return nil, fmt.Errorf("autocert: error fetching order certificates for %s: %v", domainName, err)
}
certPem := certKeyPem
// var certDer [][]byte
for _, c := range certs {
b := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: c.Raw,
})
certPem = append(certPem, b...)
// certDer = append(certDer, c.Raw)
}
m.putCache(certPem, "cert", domainName)
return m.getExistingCert(domainName)
}

106
vendor/github.com/eggsampler/acme/v3/certificate.go generated vendored Normal file
View File

@@ -0,0 +1,106 @@
package acme
import (
"crypto"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"net/http"
)
func (c Client) decodeCertificateChain(body []byte, resp *http.Response, account Account) ([]*x509.Certificate, error) {
var certs []*x509.Certificate
for {
var p *pem.Block
p, body = pem.Decode(body)
if p == nil {
break
}
cert, err := x509.ParseCertificate(p.Bytes)
if err != nil {
return certs, fmt.Errorf("acme: error parsing certificate: %v", err)
}
certs = append(certs, cert)
}
up := fetchLink(resp, "up")
if up != "" {
upCerts, err := c.FetchCertificates(account, up)
if err != nil {
return certs, fmt.Errorf("acme: error fetching up cert: %v", err)
}
if len(upCerts) != 0 {
certs = append(certs, upCerts...)
}
}
return certs, nil
}
// FetchCertificates downloads a certificate chain from a url given in an order certificate.
func (c Client) FetchCertificates(account Account, certificateURL string) ([]*x509.Certificate, error) {
resp, body, err := c.postRaw(0, certificateURL, account.URL, account.PrivateKey, "", []int{http.StatusOK})
if err != nil {
return nil, err
}
return c.decodeCertificateChain(body, resp, account)
}
// FetchAllCertificates downloads a certificate chain from a url given in an order certificate, as well as any alternate certificates if provided.
// Returns a mapping of certificate urls to the certificate chain.
func (c Client) FetchAllCertificates(account Account, certificateURL string) (map[string][]*x509.Certificate, error) {
resp, body, err := c.postRaw(0, certificateURL, account.URL, account.PrivateKey, "", []int{http.StatusOK})
if err != nil {
return nil, err
}
certChain, err := c.decodeCertificateChain(body, resp, account)
if err != nil {
return nil, err
}
certs := map[string][]*x509.Certificate{
certificateURL: certChain,
}
alternates := fetchLinks(resp, "alternate")
for _, altURL := range alternates {
altResp, altBody, err := c.postRaw(0, altURL, account.URL, account.PrivateKey, "", []int{http.StatusOK})
if err != nil {
return certs, fmt.Errorf("acme: error fetching alt cert chain at %q - %v", altURL, err)
}
altCertChain, err := c.decodeCertificateChain(altBody, altResp, account)
if err != nil {
return certs, fmt.Errorf("acme: error decoding alt cert chain at %q - %v", altURL, err)
}
certs[altURL] = altCertChain
}
return certs, nil
}
// RevokeCertificate revokes a given certificate given the certificate key or account key, and a reason.
func (c Client) RevokeCertificate(account Account, cert *x509.Certificate, key crypto.Signer, reason int) error {
revokeReq := struct {
Certificate string `json:"certificate"`
Reason int `json:"reason"`
}{
Certificate: base64.RawURLEncoding.EncodeToString(cert.Raw),
Reason: reason,
}
kid := ""
if key == account.PrivateKey {
kid = account.URL
}
if _, err := c.post(c.dir.RevokeCert, kid, key, revokeReq, nil, http.StatusOK); err != nil {
return err
}
return nil
}

102
vendor/github.com/eggsampler/acme/v3/challenge.go generated vendored Normal file
View File

@@ -0,0 +1,102 @@
package acme
import (
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"net/http"
"time"
)
// EncodeDNS01KeyAuthorization encodes a key authorization and provides a value to be put in the TXT record for the _acme-challenge DNS entry.
func EncodeDNS01KeyAuthorization(keyAuth string) string {
h := sha256.Sum256([]byte(keyAuth))
return base64.RawURLEncoding.EncodeToString(h[:])
}
// Helper function to determine whether a challenge is "finished" by its status.
func checkUpdatedChallengeStatus(challenge Challenge) (bool, error) {
switch challenge.Status {
case "pending":
// Challenge objects are created in the "pending" state.
// TODO: https://github.com/letsencrypt/boulder/issues/3346
// return true, errors.New("acme: unexpected 'pending' challenge state")
return false, nil
case "processing":
// They transition to the "processing" state when the client responds to the
// challenge and the server begins attempting to validate that the client has completed the challenge.
return false, nil
case "valid":
// If validation is successful, the challenge moves to the "valid" state
return true, nil
case "invalid":
// if there is an error, the challenge moves to the "invalid" state.
if challenge.Error.Type != "" {
return true, challenge.Error
}
return true, errors.New("acme: challenge is invalid, no error provided")
default:
return true, fmt.Errorf("acme: unknown challenge status: %s", challenge.Status)
}
}
// UpdateChallenge responds to a challenge to indicate to the server to complete the challenge.
func (c Client) UpdateChallenge(account Account, challenge Challenge) (Challenge, error) {
resp, err := c.post(challenge.URL, account.URL, account.PrivateKey, struct{}{}, &challenge, http.StatusOK)
if err != nil {
return challenge, err
}
if loc := resp.Header.Get("Location"); loc != "" {
challenge.URL = loc
}
challenge.AuthorizationURL = fetchLink(resp, "up")
if finished, err := checkUpdatedChallengeStatus(challenge); finished {
return challenge, err
}
pollInterval, pollTimeout := c.getPollingDurations()
end := time.Now().Add(pollTimeout)
for {
if time.Now().After(end) {
return challenge, errors.New("acme: challenge update timeout")
}
time.Sleep(pollInterval)
resp, err := c.post(challenge.URL, account.URL, account.PrivateKey, "", &challenge, http.StatusOK)
if err != nil {
// i don't think it's worth exiting the loop on this error
// it could just be connectivity issue that's resolved before the timeout duration
continue
}
if loc := resp.Header.Get("Location"); loc != "" {
challenge.URL = loc
}
challenge.AuthorizationURL = fetchLink(resp, "up")
if finished, err := checkUpdatedChallengeStatus(challenge); finished {
return challenge, err
}
}
}
// FetchChallenge fetches an existing challenge from the given url.
func (c Client) FetchChallenge(account Account, challengeURL string) (Challenge, error) {
challenge := Challenge{}
resp, err := c.post(challengeURL, account.URL, account.PrivateKey, "", &challenge, http.StatusOK)
if err != nil {
return challenge, err
}
challenge.URL = resp.Header.Get("Location")
challenge.AuthorizationURL = fetchLink(resp, "up")
return challenge, nil
}

View File

@@ -0,0 +1,5 @@
version: '3'
services:
boulder:
ports:
- "8055:8055"

257
vendor/github.com/eggsampler/acme/v3/jws.go generated vendored Normal file
View File

@@ -0,0 +1,257 @@
// Copyright 2015 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package acme
import (
"crypto"
"crypto/ecdsa"
"crypto/hmac"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
_ "crypto/sha512" // need for EC keys
"encoding/asn1"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/big"
)
// KeyID is the account key identity provided by a CA during registration.
type KeyID string
// noKeyID indicates that jwsEncodeJSON should compute and use JWK instead of a KID.
// See jwsEncodeJSON for details.
const noKeyID = KeyID("")
// noPayload indicates jwsEncodeJSON will encode zero-length octet string
// in a JWS request. This is called POST-as-GET in RFC 8555 and is used to make
// authenticated GET requests via POSTing with an empty payload.
// See https://tools.ietf.org/html/rfc8555#section-6.3 for more details.
const noPayload = ""
// noNonce indicates that the nonce should be omitted from the protected header.
// See jwsEncodeJSON for details.
const noNonce = ""
// jsonWebSignature can be easily serialized into a JWS following
// https://tools.ietf.org/html/rfc7515#section-3.2.
type jsonWebSignature struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Sig string `json:"signature"`
}
// jwsEncodeJSON signs claimset using provided key and a nonce.
// The result is serialized in JSON format containing either kid or jwk
// fields based on the provided KeyID value.
//
// The claimset is marshalled using json.Marshal unless it is a string.
// In which case it is inserted directly into the message.
//
// If kid is non-empty, its quoted value is inserted in the protected header
// as "kid" field value. Otherwise, JWK is computed using jwkEncode and inserted
// as "jwk" field value. The "jwk" and "kid" fields are mutually exclusive.
//
// If nonce is non-empty, its quoted value is inserted in the protected header.
//
// See https://tools.ietf.org/html/rfc7515#section-7.
func jwsEncodeJSON(claimset interface{}, key crypto.Signer, kid KeyID, nonce, url string) ([]byte, error) {
if key == nil {
return nil, errors.New("nil key")
}
alg, sha := jwsHasher(key.Public())
if alg == "" || !sha.Available() {
return nil, ErrUnsupportedKey
}
headers := struct {
Alg string `json:"alg"`
KID string `json:"kid,omitempty"`
JWK json.RawMessage `json:"jwk,omitempty"`
Nonce string `json:"nonce,omitempty"`
URL string `json:"url"`
}{
Alg: alg,
Nonce: nonce,
URL: url,
}
switch kid {
case noKeyID:
jwk, err := jwkEncode(key.Public())
if err != nil {
return nil, err
}
headers.JWK = json.RawMessage(jwk)
default:
headers.KID = string(kid)
}
phJSON, err := json.Marshal(headers)
if err != nil {
return nil, err
}
phead := base64.RawURLEncoding.EncodeToString([]byte(phJSON))
var payload string
if val, ok := claimset.(string); ok {
payload = val
} else {
cs, err := json.Marshal(claimset)
if err != nil {
return nil, err
}
payload = base64.RawURLEncoding.EncodeToString(cs)
}
hash := sha.New()
hash.Write([]byte(phead + "." + payload))
sig, err := jwsSign(key, sha, hash.Sum(nil))
if err != nil {
return nil, err
}
enc := jsonWebSignature{
Protected: phead,
Payload: payload,
Sig: base64.RawURLEncoding.EncodeToString(sig),
}
return json.Marshal(&enc)
}
// jwsWithMAC creates and signs a JWS using the given key and the HS256
// algorithm. kid and url are included in the protected header. rawPayload
// should not be base64-URL-encoded.
func jwsWithMAC(key []byte, kid, url string, rawPayload []byte) (*jsonWebSignature, error) {
if len(key) == 0 {
return nil, errors.New("acme: cannot sign JWS with an empty MAC key")
}
header := struct {
Algorithm string `json:"alg"`
KID string `json:"kid"`
URL string `json:"url,omitempty"`
}{
// Only HMAC-SHA256 is supported.
Algorithm: "HS256",
KID: kid,
URL: url,
}
rawProtected, err := json.Marshal(header)
if err != nil {
return nil, err
}
protected := base64.RawURLEncoding.EncodeToString(rawProtected)
payload := base64.RawURLEncoding.EncodeToString(rawPayload)
h := hmac.New(sha256.New, key)
if _, err := h.Write([]byte(protected + "." + payload)); err != nil {
return nil, err
}
mac := h.Sum(nil)
return &jsonWebSignature{
Protected: protected,
Payload: payload,
Sig: base64.RawURLEncoding.EncodeToString(mac),
}, nil
}
// jwkEncode encodes public part of an RSA or ECDSA key into a JWK.
// The result is also suitable for creating a JWK thumbprint.
// https://tools.ietf.org/html/rfc7517
func jwkEncode(pub crypto.PublicKey) (string, error) {
switch pub := pub.(type) {
case *rsa.PublicKey:
// https://tools.ietf.org/html/rfc7518#section-6.3.1
n := pub.N
e := big.NewInt(int64(pub.E))
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt.Sprintf(`{"e":"%s","kty":"RSA","n":"%s"}`,
base64.RawURLEncoding.EncodeToString(e.Bytes()),
base64.RawURLEncoding.EncodeToString(n.Bytes()),
), nil
case *ecdsa.PublicKey:
// https://tools.ietf.org/html/rfc7518#section-6.2.1
p := pub.Curve.Params()
n := p.BitSize / 8
if p.BitSize%8 != 0 {
n++
}
x := pub.X.Bytes()
if n > len(x) {
x = append(make([]byte, n-len(x)), x...)
}
y := pub.Y.Bytes()
if n > len(y) {
y = append(make([]byte, n-len(y)), y...)
}
// Field order is important.
// See https://tools.ietf.org/html/rfc7638#section-3.3 for details.
return fmt.Sprintf(`{"crv":"%s","kty":"EC","x":"%s","y":"%s"}`,
p.Name,
base64.RawURLEncoding.EncodeToString(x),
base64.RawURLEncoding.EncodeToString(y),
), nil
}
return "", ErrUnsupportedKey
}
// jwsSign signs the digest using the given key.
// The hash is unused for ECDSA keys.
func jwsSign(key crypto.Signer, hash crypto.Hash, digest []byte) ([]byte, error) {
switch pub := key.Public().(type) {
case *rsa.PublicKey:
return key.Sign(rand.Reader, digest, hash)
case *ecdsa.PublicKey:
sigASN1, err := key.Sign(rand.Reader, digest, hash)
if err != nil {
return nil, err
}
var rs struct{ R, S *big.Int }
if _, err := asn1.Unmarshal(sigASN1, &rs); err != nil {
return nil, err
}
rb, sb := rs.R.Bytes(), rs.S.Bytes()
size := pub.Params().BitSize / 8
if size%8 > 0 {
size++
}
sig := make([]byte, size*2)
copy(sig[size-len(rb):], rb)
copy(sig[size*2-len(sb):], sb)
return sig, nil
}
return nil, ErrUnsupportedKey
}
// jwsHasher indicates suitable JWS algorithm name and a hash function
// to use for signing a digest with the provided key.
// It returns ("", 0) if the key is not supported.
func jwsHasher(pub crypto.PublicKey) (string, crypto.Hash) {
switch pub := pub.(type) {
case *rsa.PublicKey:
return "RS256", crypto.SHA256
case *ecdsa.PublicKey:
switch pub.Params().Name {
case "P-256":
return "ES256", crypto.SHA256
case "P-384":
return "ES384", crypto.SHA384
case "P-521":
return "ES512", crypto.SHA512
}
}
return "", 0
}
// JWKThumbprint creates a JWK thumbprint out of pub
// as specified in https://tools.ietf.org/html/rfc7638.
func JWKThumbprint(pub crypto.PublicKey) (string, error) {
jwk, err := jwkEncode(pub)
if err != nil {
return "", err
}
b := sha256.Sum256([]byte(jwk))
return base64.RawURLEncoding.EncodeToString(b[:]), nil
}

45
vendor/github.com/eggsampler/acme/v3/nonce.go generated vendored Normal file
View File

@@ -0,0 +1,45 @@
package acme
import (
"sync"
)
// Simple thread-safe stack impl
type nonceStack struct {
lock sync.Mutex
stack []string
}
// Pushes a nonce to the stack.
// Doesn't push empty nonces, or if there's more than 100 nonces on the stack
func (ns *nonceStack) push(v string) {
if v == "" {
return
}
ns.lock.Lock()
defer ns.lock.Unlock()
if len(ns.stack) > 100 {
return
}
ns.stack = append(ns.stack, v)
}
// Pops a nonce from the stack.
// Returns empty string if there are no nonces
func (ns *nonceStack) pop() string {
ns.lock.Lock()
defer ns.lock.Unlock()
n := len(ns.stack)
if n == 0 {
return ""
}
v := ns.stack[n-1]
ns.stack = ns.stack[:n-1]
return v
}

173
vendor/github.com/eggsampler/acme/v3/options.go generated vendored Normal file
View File

@@ -0,0 +1,173 @@
package acme
import (
"crypto"
"crypto/hmac"
"crypto/tls"
"crypto/x509"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"net/http"
"time"
)
// OptionFunc function prototype for passing options to NewClient
type OptionFunc func(client *Client) error
// WithHTTPTimeout sets a timeout on the http client used by the Client
func WithHTTPTimeout(duration time.Duration) OptionFunc {
return func(client *Client) error {
client.httpClient.Timeout = duration
return nil
}
}
// WithInsecureSkipVerify sets InsecureSkipVerify on the http client transport tls client config used by the Client
func WithInsecureSkipVerify() OptionFunc {
return func(client *Client) error {
client.httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
return nil
}
}
// WithUserAgentSuffix appends a user agent suffix for http requests to acme resources
func WithUserAgentSuffix(userAgentSuffix string) OptionFunc {
return func(client *Client) error {
client.userAgentSuffix = userAgentSuffix
return nil
}
}
// WithAcceptLanguage sets an Accept-Language header on http requests
func WithAcceptLanguage(acceptLanguage string) OptionFunc {
return func(client *Client) error {
client.acceptLanguage = acceptLanguage
return nil
}
}
// WithRetryCount sets the number of times the acme client retries when receiving an api error (eg, nonce failures, etc).
// Default: 5
func WithRetryCount(retryCount int) OptionFunc {
return func(client *Client) error {
if retryCount < 1 {
return errors.New("retryCount must be > 0")
}
client.retryCount = retryCount
return nil
}
}
// WithHTTPClient Allows setting a custom http client for acme connections
func WithHTTPClient(httpClient *http.Client) OptionFunc {
return func(client *Client) error {
if httpClient == nil {
return errors.New("client must not be nil")
}
client.httpClient = httpClient
return nil
}
}
// WithRootCerts sets the httpclient transport to use a given certpool for root certs
func WithRootCerts(pool *x509.CertPool) OptionFunc {
return func(client *Client) error {
client.httpClient.Transport = &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: pool,
},
}
return nil
}
}
// NewAccountOptionFunc function prototype for passing options to NewClient
type NewAccountOptionFunc func(crypto.Signer, *Account, *NewAccountRequest, Client) error
// NewAcctOptOnlyReturnExisting sets the new client request to only return existing accounts
func NewAcctOptOnlyReturnExisting() NewAccountOptionFunc {
return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error {
request.OnlyReturnExisting = true
return nil
}
}
// NewAcctOptAgreeTOS sets the new account request as agreeing to the terms of service
func NewAcctOptAgreeTOS() NewAccountOptionFunc {
return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error {
request.TermsOfServiceAgreed = true
return nil
}
}
// NewAcctOptWithContacts adds contacts to a new account request
func NewAcctOptWithContacts(contacts ...string) NewAccountOptionFunc {
return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error {
request.Contact = contacts
return nil
}
}
// NewAcctOptExternalAccountBinding adds an external account binding to the new account request
// Code adopted from jwsEncodeJSON
func NewAcctOptExternalAccountBinding(binding ExternalAccountBinding) NewAccountOptionFunc {
return func(privateKey crypto.Signer, account *Account, request *NewAccountRequest, client Client) error {
if binding.KeyIdentifier == "" {
return errors.New("acme: NewAcctOptExternalAccountBinding has no KeyIdentifier set")
}
if binding.MacKey == "" {
return errors.New("acme: NewAcctOptExternalAccountBinding has no MacKey set")
}
if binding.Algorithm == "" {
return errors.New("acme: NewAcctOptExternalAccountBinding has no Algorithm set")
}
if binding.HashFunc == 0 {
return errors.New("acme: NewAcctOptExternalAccountBinding has no HashFunc set")
}
jwk, err := jwkEncode(privateKey.Public())
if err != nil {
return fmt.Errorf("acme: external account binding error encoding public key: %v", err)
}
payload := base64.RawURLEncoding.EncodeToString([]byte(jwk))
phead := fmt.Sprintf(`{"alg":%q,"kid":%q,"url":%q}`,
binding.Algorithm, binding.KeyIdentifier, client.Directory().NewAccount)
phead = base64.RawURLEncoding.EncodeToString([]byte(phead))
decodedAccountMac, err := base64.RawURLEncoding.DecodeString(binding.MacKey)
if err != nil {
return fmt.Errorf("acme: external account binding error decoding mac key: %v", err)
}
macHash := hmac.New(binding.HashFunc.New, decodedAccountMac)
if _, err := macHash.Write([]byte(phead + "." + payload)); err != nil {
return err
}
enc := struct {
Protected string `json:"protected"`
Payload string `json:"payload"`
Sig string `json:"signature"`
}{
Protected: phead,
Payload: payload,
Sig: base64.RawURLEncoding.EncodeToString(macHash.Sum(nil)),
}
jwsEab, err := json.Marshal(&enc)
if err != nil {
return fmt.Errorf("acme: external account binding error marshalling struct: %v", err)
}
request.ExternalAccountBinding = jwsEab
account.ExternalAccountBinding = binding
return nil
}
}

231
vendor/github.com/eggsampler/acme/v3/order.go generated vendored Normal file
View File

@@ -0,0 +1,231 @@
package acme
import (
"crypto/x509"
"encoding/base64"
"errors"
"fmt"
"net/http"
"time"
)
type OrderExtension struct {
Profile string
}
// NewOrder initiates a new order for a new certificate. This method does not use ACME Renewal Info.
func (c Client) NewOrder(account Account, identifiers []Identifier) (Order, error) {
return c.ReplacementOrder(account, nil, identifiers)
}
// NewOrderDomains takes a list of domain dns identifiers for a new certificate. Essentially a helper function.
func (c Client) NewOrderDomains(account Account, domains ...string) (Order, error) {
var identifiers []Identifier
for _, d := range domains {
identifiers = append(identifiers, Identifier{Type: "dns", Value: d})
}
return c.ReplacementOrder(account, nil, identifiers)
}
// NewOrderExtension takes a struct providing any extensions onto the order
func (c Client) NewOrderExtension(account Account, identifiers []Identifier, ext OrderExtension) (Order, error) {
return c.ReplacementOrderExtension(account, nil, identifiers, ext)
}
// ReplacementOrder takes an existing *x509.Certificate and initiates a new
// order for a new certificate, but with the order being marked as a
// replacement. Replacement orders which are valid replacements are (currently)
// exempt from Let's Encrypt NewOrder rate limits, but may not be exempt from
// other ACME CAs ACME Renewal Info implementations. At least one identifier
// must match the list of identifiers from the parent order to be considered as
// a valid replacement order.
// See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
func (c Client) ReplacementOrder(account Account, oldCert *x509.Certificate, identifiers []Identifier) (Order, error) {
return c.ReplacementOrderExtension(account, oldCert, identifiers, OrderExtension{})
}
// ReplacementOrderExtension takes a struct providing any extensions onto the order
func (c Client) ReplacementOrderExtension(account Account, oldCert *x509.Certificate, identifiers []Identifier, ext OrderExtension) (Order, error) {
// If an old cert being replaced is present and the acme directory doesn't list a RenewalInfo endpoint,
// throw an error. This endpoint being present indicates support for ARI.
if oldCert != nil && c.dir.RenewalInfo == "" {
return Order{}, ErrRenewalInfoNotSupported
}
// optional fields are listed as 'omitempty' so the json encoder doesn't
// include those keys if their values are not provided.
newOrderReq := struct {
Identifiers []Identifier `json:"identifiers"`
Replaces string `json:"replaces,omitempty"`
Profile string `json:"Profile,omitempty"`
}{
Identifiers: identifiers,
}
newOrderResp := Order{}
if ext.Profile != "" {
_, ok := c.Directory().Meta.Profiles[ext.Profile]
if !ok {
return Order{}, fmt.Errorf("requested Profile not advertised by directory: %v", ext.Profile)
}
newOrderReq.Profile = ext.Profile
}
// If present, add the ari cert ID from the original/old certificate
if oldCert != nil {
replacesCertID, err := GenerateARICertID(oldCert)
if err != nil {
return Order{}, fmt.Errorf("acme: error generating replacement certificate id: %v", err)
}
newOrderReq.Replaces = replacesCertID
newOrderResp.Replaces = replacesCertID // server does not appear to set this currently?
}
// Submit the order
resp, err := c.post(c.dir.NewOrder, account.URL, account.PrivateKey, newOrderReq, &newOrderResp, http.StatusCreated)
if err != nil {
return newOrderResp, err
}
defer resp.Body.Close()
newOrderResp.URL = resp.Header.Get("Location")
return newOrderResp, nil
}
// FetchOrder fetches an existing order given an order url.
func (c Client) FetchOrder(account Account, orderURL string) (Order, error) {
orderResp := Order{
URL: orderURL, // boulder response doesn't seem to contain location header for this request
}
_, err := c.post(orderURL, account.URL, account.PrivateKey, "", &orderResp, http.StatusOK)
return orderResp, err
}
// Helper function to determine whether an order is "finished" by its status.
func checkFinalizedOrderStatus(order Order) (bool, error) {
switch order.Status {
case "invalid":
// "invalid": The certificate will not be issued. Consider this
// order process abandoned.
if order.Error.Type != "" {
return true, order.Error
}
return true, errors.New("acme: finalized order is invalid, no error provided")
case "pending":
// "pending": The server does not believe that the client has
// fulfilled the requirements. Check the "authorizations" array for
// entries that are still pending.
return true, errors.New("acme: authorizations not fulfilled")
case "ready":
// "ready": The server agrees that the requirements have been
// fulfilled, and is awaiting finalization. Submit a finalization
// request.
return true, errors.New("acme: unexpected 'ready' state")
case "processing":
// "processing": The certificate is being issued. Send a GET request
// after the time given in the "Retry-After" header field of the
// response, if any.
return false, nil
case "valid":
// "valid": The server has issued the certificate and provisioned its
// URL to the "certificate" field of the order. Download the
// certificate.
return true, nil
default:
return true, fmt.Errorf("acme: unknown order status: %s", order.Status)
}
}
// FinalizeOrder indicates to the acme server that the client considers an order complete and "finalizes" it.
// If the server believes the authorizations have been filled successfully, a certificate should then be available.
// This function assumes that the order status is "ready".
func (c Client) FinalizeOrder(account Account, order Order, csr *x509.CertificateRequest) (Order, error) {
finaliseReq := struct {
Csr string `json:"csr"`
}{
Csr: base64.RawURLEncoding.EncodeToString(csr.Raw),
}
resp, err := c.post(order.Finalize, account.URL, account.PrivateKey, finaliseReq, &order, http.StatusOK)
if err != nil {
return order, err
}
order.URL = resp.Header.Get("Location")
updateOrder := func(resp *http.Response) (bool, error) {
if finished, err := checkFinalizedOrderStatus(order); finished {
return true, err
}
retryAfter, err := parseRetryAfter(resp.Header.Get("Retry-After"))
if err != nil {
return false, fmt.Errorf("acme: error parsing retry-after header: %v", err)
}
order.RetryAfter = retryAfter
return false, nil
}
if finished, err := updateOrder(resp); finished || err != nil {
return order, err
}
fetchOrder := func() (bool, error) {
resp, err := c.post(order.URL, account.URL, account.PrivateKey, "", &order, http.StatusOK)
if err != nil {
return false, nil
}
return updateOrder(resp)
}
if !c.IgnoreRetryAfter && !order.RetryAfter.IsZero() {
_, pollTimeout := c.getPollingDurations()
end := time.Now().Add(pollTimeout)
for {
if time.Now().After(end) {
return order, errors.New("acme: finalized order timeout")
}
diff := time.Until(order.RetryAfter)
_, pollTimeout := c.getPollingDurations()
if diff > pollTimeout {
return order, fmt.Errorf("acme: Retry-After (%v) longer than poll timeout (%v)", diff, c.PollTimeout)
}
if diff > 0 {
time.Sleep(diff)
}
if finished, err := fetchOrder(); finished || err != nil {
return order, err
}
}
}
if !c.IgnoreRetryAfter {
pollInterval, pollTimeout := c.getPollingDurations()
end := time.Now().Add(pollTimeout)
for {
if time.Now().After(end) {
return order, errors.New("acme: finalized order timeout")
}
time.Sleep(pollInterval)
if finished, err := fetchOrder(); finished || err != nil {
return order, err
}
}
}
return order, err
}

65
vendor/github.com/eggsampler/acme/v3/problem.go generated vendored Normal file
View File

@@ -0,0 +1,65 @@
package acme
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
// Problem document as defined in,
// https://tools.ietf.org/html/rfc7807
// Problem represents an error returned by an acme server.
type Problem struct {
Type string `json:"type"`
Detail string `json:"detail,omitempty"`
Status int `json:"status,omitempty"`
Instance string `json:"instance,omitempty"`
SubProblems []SubProblem `json:"subproblems,omitempty"`
}
type SubProblem struct {
Type string `json:"type"`
Detail string `json:"detail"`
Identifier Identifier `json:"identifier"`
}
// Returns a human readable error string.
func (err Problem) Error() string {
s := fmt.Sprintf("acme: error code %d %q: %s", err.Status, err.Type, err.Detail)
if len(err.SubProblems) > 0 {
for _, v := range err.SubProblems {
s += fmt.Sprintf(", problem %q: %s", v.Type, v.Detail)
}
}
if err.Instance != "" {
s += ", url: " + err.Instance
}
return s
}
// Helper function to determine if a response contains an expected status code, or otherwise an error object.
func checkError(resp *http.Response, expectedStatuses ...int) error {
for _, statusCode := range expectedStatuses {
if resp.StatusCode == statusCode {
return nil
}
}
if resp.StatusCode < 400 || resp.StatusCode >= 600 {
return fmt.Errorf("acme: expected status codes: %d, got: %d %s", expectedStatuses, resp.StatusCode, resp.Status)
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("acme: error reading error body: %v", err)
}
acmeError := Problem{}
if err := json.Unmarshal(body, &acmeError); err != nil {
return fmt.Errorf("acme: parsing error body: %v - %s", err, string(body))
}
return acmeError
}

236
vendor/github.com/eggsampler/acme/v3/types.go generated vendored Normal file
View File

@@ -0,0 +1,236 @@
package acme
import (
"crypto"
"encoding/json"
"errors"
"net/http"
"time"
)
var (
// ErrUnsupportedKey is returned when an unsupported key type is encountered.
ErrUnsupportedKey = errors.New("acme: unknown key type; only RSA and ECDSA are supported")
// ErrRenewalInfoNotSupported is returned by Client.GetRenewalInfo if the
// renewal info entry isn't present on the acme directory (ie, it's not
// supported by the acme server)
ErrRenewalInfoNotSupported = errors.New("renewal information endpoint not supported")
)
// Different possible challenge types provided by an ACME server.
// See https://tools.ietf.org/html/rfc8555#section-9.7.8
const (
ChallengeTypeDNS01 = "dns-01"
ChallengeTypeDNSAccount01 = "dns-account-01"
ChallengeTypeHTTP01 = "http-01"
ChallengeTypeTLSALPN01 = "tls-alpn-01"
// ChallengeTypeTLSSNI01 is deprecated and should not be used.
// See: https://community.letsencrypt.org/t/important-what-you-need-to-know-about-tls-sni-validation-issues/50811
ChallengeTypeTLSSNI01 = "tls-sni-01"
)
// Constants used for certificate revocation, used for RevokeCertificate
// See https://tools.ietf.org/html/rfc5280#section-5.3.1
const (
ReasonUnspecified = iota // 0
ReasonKeyCompromise // 1
ReasonCaCompromise // 2
ReasonAffiliationChanged // 3
ReasonSuperseded // 4
ReasonCessationOfOperation // 5
ReasonCertificateHold // 6
_ // 7 - Unused
ReasonRemoveFromCRL // 8
ReasonPrivilegeWithdrawn // 9
ReasonAaCompromise // 10
)
// Directory object as returned from the client's directory url upon creation of client.
// See https://tools.ietf.org/html/rfc8555#section-7.1.1
type Directory struct {
NewNonce string `json:"newNonce"` // url to new nonce endpoint
NewAccount string `json:"newAccount"` // url to new account endpoint
NewOrder string `json:"newOrder"` // url to new order endpoint
NewAuthz string `json:"newAuthz"` // url to new authz endpoint
RevokeCert string `json:"revokeCert"` // url to revoke cert endpoint
KeyChange string `json:"keyChange"` // url to key change endpoint
// https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03
RenewalInfo string `json:"renewalInfo"` // url to renewal info endpoint
// meta object containing directory metadata
Meta struct {
TermsOfService string `json:"termsOfService"`
Website string `json:"website"`
CaaIdentities []string `json:"caaIdentities"`
ExternalAccountRequired bool `json:"externalAccountRequired"`
Profiles map[string]string `json:"profiles"`
} `json:"meta"`
// Directory url provided when creating a new acme client.
URL string `json:"-"`
}
// Client structure to interact with an ACME server.
// This is typically how most, if not all, of the communication between the client and server occurs.
type Client struct {
httpClient *http.Client
nonces *nonceStack
dir Directory
userAgentSuffix string
acceptLanguage string
retryCount int
// The amount of total time the Client will wait at most for a challenge to be updated or a certificate to be issued.
// Default 30 seconds if duration is not set or if set to 0.
PollTimeout time.Duration
// The time between checking if a challenge has been updated or a certificate has been issued.
// Default 0.5 seconds if duration is not set or if set to 0.
PollInterval time.Duration
// IgnorePolling does not use any simple polling in order finalisation
IgnorePolling bool
// IgnoreRetryAfter does not use the retry-after header in order finalisation
IgnoreRetryAfter bool
}
// Account structure representing fields in an account object.
// See https://tools.ietf.org/html/rfc8555#section-7.1.2
// See also https://tools.ietf.org/html/rfc8555#section-9.7.1
type Account struct {
Status string `json:"status"`
Contact []string `json:"contact"`
Orders string `json:"orders"`
// Provided by the Location http header when creating a new account or fetching an existing account.
URL string `json:"-"`
// The private key used to create or fetch the account.
// Not fetched from server.
PrivateKey crypto.Signer `json:"-"`
// Thumbprint is the SHA-256 digest JWK_Thumbprint of the account key.
// See https://tools.ietf.org/html/rfc8555#section-8.1
Thumbprint string `json:"-"`
// ExternalAccountBinding is populated when using the NewAcctOptExternalAccountBinding option for NewAccountOption
// and is otherwise empty. Not populated when account is fetched or created otherwise.
ExternalAccountBinding ExternalAccountBinding `json:"-"`
}
// ExternalAccountBinding holds the key identifier and mac key provided for use in servers that support/require
// external account binding.
// The MacKey is a base64url-encoded string.
// Algorithm is a "MAC-based algorithm" as per RFC8555. Typically this is either,
// - "HS256" for HashFunc: crypto.SHA256
// - "HS384" for HashFunc: crypto.SHA384
// - "HS512" for HashFunc: crypto.SHA512
//
// However this is dependent on the acme server in question and is provided here to give more options for future compatibility.
type ExternalAccountBinding struct {
KeyIdentifier string `json:"-"`
MacKey string `json:"-"`
Algorithm string `json:"-"`
HashFunc crypto.Hash `json:"-"`
}
// Identifier object used in order and authorization objects
// See https://tools.ietf.org/html/rfc8555#section-7.1.4
type Identifier struct {
Type string `json:"type"`
Value string `json:"value"`
}
// Order object returned when fetching or creating a new order.
// See https://tools.ietf.org/html/rfc8555#section-7.1.3
type Order struct {
Status string `json:"status"`
Expires time.Time `json:"expires"`
Identifiers []Identifier `json:"identifiers"`
Profile string `json:"Profile,omitempty"`
NotBefore time.Time `json:"notBefore"`
NotAfter time.Time `json:"notAfter"`
Error Problem `json:"error"`
Authorizations []string `json:"authorizations"`
Finalize string `json:"finalize"`
Certificate string `json:"certificate"`
// URL for the order object.
// Provided by the rel="Location" Link http header
URL string `json:"-"`
// RetryAfter is the http Retry-After header from the order response
RetryAfter time.Time `json:"-"`
// Replaces (optional, string): A string uniquely identifying a
// previously-issued certificate which this order is intended to replace.
// See https://datatracker.ietf.org/doc/html/draft-ietf-acme-ari-03#section-5
Replaces string `json:"replaces,omitempty"`
}
// Authorization object returned when fetching an authorization in an order.
// See https://tools.ietf.org/html/rfc8555#section-7.1.4
type Authorization struct {
Identifier Identifier `json:"identifier"`
Status string `json:"status"`
Expires time.Time `json:"expires"`
Challenges []Challenge `json:"challenges"`
Wildcard bool `json:"wildcard"`
// For convenience access to the provided challenges
ChallengeMap map[string]Challenge `json:"-"`
ChallengeTypes []string `json:"-"`
URL string `json:"-"`
}
// Challenge object fetched in an authorization or directly from the challenge url.
// See https://tools.ietf.org/html/rfc8555#section-7.1.5
type Challenge struct {
Type string `json:"type"`
URL string `json:"url"`
Status string `json:"status"`
Validated string `json:"validated"`
Error Problem `json:"error"`
// Based on the challenge used
Token string `json:"token"`
KeyAuthorization string `json:"keyAuthorization"`
// Authorization url provided by the rel="up" Link http header
AuthorizationURL string `json:"-"`
}
// OrderList of challenge objects.
type OrderList struct {
Orders []string `json:"orders"`
// Order list pagination, url to next orders.
// Provided by the rel="next" Link http header
Next string `json:"-"`
}
// NewAccountRequest object used for submitting a request for a new account.
// Primarily used with NewAccountOptionFunc
type NewAccountRequest struct {
OnlyReturnExisting bool `json:"onlyReturnExisting"`
TermsOfServiceAgreed bool `json:"termsOfServiceAgreed"`
Contact []string `json:"contact,omitempty"`
ExternalAccountBinding json.RawMessage `json:"externalAccountBinding"`
}
// RenewalInfo stores the server-provided suggestions on when to renew
// certificates.
type RenewalInfo struct {
SuggestedWindow struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
} `json:"suggestedWindow"`
ExplanationURL string `json:"explanationURL,omitempty"`
RetryAfter time.Time `json:"-"`
}

7
vendor/modules.txt vendored
View File

@@ -518,6 +518,9 @@ github.com/eapache/queue
# github.com/edsrzf/mmap-go v1.0.0
## explicit
github.com/edsrzf/mmap-go
# github.com/eggsampler/acme/v3 v3.6.2
## explicit; go 1.11
github.com/eggsampler/acme/v3
# github.com/elgatito/upnp v0.0.0-20180711183757-2f244d205f9a
## explicit
github.com/elgatito/upnp
@@ -1849,7 +1852,7 @@ sigs.k8s.io/structured-merge-diff/v4/value
# sigs.k8s.io/yaml v1.2.0
## explicit; go 1.12
sigs.k8s.io/yaml
# yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250704072708-d4757966c574
# yunion.io/x/cloudmux v0.3.10-0-alpha.1.0.20250714064001-1e1f50030bed
## explicit; go 1.21
yunion.io/x/cloudmux/pkg/apis
yunion.io/x/cloudmux/pkg/apis/billing
@@ -1946,7 +1949,7 @@ yunion.io/x/log/hooks
yunion.io/x/ovsdb/cli_util
yunion.io/x/ovsdb/schema/ovn_nb
yunion.io/x/ovsdb/types
# yunion.io/x/pkg v1.10.4-0.20250618071935-840c253075b0
# yunion.io/x/pkg v1.10.4-0.20250712095850-7797c1f3ffa6
## explicit; go 1.18
yunion.io/x/pkg/appctx
yunion.io/x/pkg/errors

View File

@@ -310,6 +310,7 @@ type ICloudProvider interface {
GetMetrics(opts *MetricListOptions) ([]MetricValues, error)
GetISSLCertificates() ([]ICloudSSLCertificate, error)
CreateISSLCertificate(opts *SSLCertificateCreateOptions) (ICloudSSLCertificate, error)
GetIAiGateways() ([]IAiGateway, error)
GetIAiGatewayById(id string) (IAiGateway, error)
@@ -643,6 +644,10 @@ func (base *SBaseProvider) GetISSLCertificates() ([]ICloudSSLCertificate, error)
return nil, errors.Wrapf(ErrNotImplemented, "GetISSLCertificates")
}
func (base *SBaseProvider) CreateISSLCertificate(opts *SSLCertificateCreateOptions) (ICloudSSLCertificate, error) {
return nil, errors.Wrapf(ErrNotImplemented, "CreateISSLCertificate")
}
func (base *SBaseProvider) GetIAiGateways() ([]IAiGateway, error) {
return nil, errors.Wrapf(ErrNotImplemented, "GetIAiGateways")
}

View File

@@ -1814,7 +1814,6 @@ type ICloudSSLCertificate interface {
GetCommon() string
GetCountry() string
GetIssuer() string
GetExpired() bool
GetEndDate() time.Time
GetFingerprint() string
GetCity() string

25
vendor/yunion.io/x/cloudmux/pkg/cloudprovider/ssl.go generated vendored Normal file
View File

@@ -0,0 +1,25 @@
// 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 cloudprovider
const (
SSL_ISSUER_LETSENCRYPT = "Let's Encrypt"
SSL_ISSUER_ZEROSSL = "ZeroSSL"
)
type SSLCertificateCreateOptions struct {
Certificate string
PrivateKey string
}

View File

@@ -59,10 +59,6 @@ func (s *SSSLCertificate) GetIssuer() string {
return s.Issuer
}
func (s *SSSLCertificate) GetExpired() bool {
return s.Expired
}
func (s *SSSLCertificate) GetEndDate() time.Time {
return s.EndDate
}

View File

@@ -709,7 +709,7 @@ func (self *SDatastore) CheckFile(ctx context.Context, remotePath string) (*SDat
req, err := http.NewRequest("HEAD", url, nil)
if err != nil {
return nil, err
return nil, errors.Wrapf(err, "HEAD %s", url)
}
var size uint64

View File

@@ -684,6 +684,7 @@ func (svm *SVirtualMachine) doUnregister(ctx context.Context) error {
func (svm *SVirtualMachine) DeleteVM(ctx context.Context) error {
err := svm.CheckFileInfo(ctx)
if err != nil {
log.Errorf("CheckFileInfo %s error: %v", svm.GetName(), err)
return svm.doUnregister(ctx)
}
return svm.doDestroy(ctx)
@@ -1355,7 +1356,7 @@ func (svm *SVirtualMachine) CheckFileInfo(ctx context.Context) error {
if ds.HasFile(file.Name) {
_, err := ds.CheckFile(ctx, file.Name)
if err != nil {
return errors.Wrap(err, "ds.CheckFile")
return errors.Wrapf(err, "CheckFile %s", file.Name)
}
break
}

View File

@@ -125,9 +125,17 @@ func (addr *SAddress) GetINetworkId() string {
}
func (addr *SAddress) GetAssociationType() string {
if len(addr.GetAssociationExternalId()) > 0 {
if len(addr.instanceId) > 0 {
return api.EIP_ASSOCIATE_TYPE_SERVER
}
for _, user := range addr.Users {
if strings.HasPrefix(user, "/instances/") {
return api.EIP_ASSOCIATE_TYPE_SERVER
}
if strings.Contains(user, "/forwardingRules/") {
return api.EIP_ASSOCIATE_TYPE_LOADBALANCER
}
}
return ""
}

View File

@@ -23,6 +23,7 @@ import (
"yunion.io/x/jsonutils"
"yunion.io/x/log"
"yunion.io/x/pkg/errors"
"yunion.io/x/pkg/util/netutils"
"yunion.io/x/pkg/utils"
api "yunion.io/x/cloudmux/pkg/apis/compute"
@@ -248,9 +249,11 @@ func (self *SLoadbalancer) GetIEIPs() ([]cloudprovider.ICloudEIP, error) {
log.Errorf("GetAddress.GetForwardingRules %s", err)
}
ret := []cloudprovider.ICloudEIP{}
ret, addrs := []cloudprovider.ICloudEIP{}, []string{}
for i := range frs {
if strings.ToLower(frs[i].LoadBalancingScheme) == "external" {
ipAddr, _ := netutils.NewIPV4Addr(frs[i].IPAddress)
if netutils.IsExitAddress(ipAddr) && !utils.IsInStringArray(frs[i].IPAddress, addrs) {
addrs = append(addrs, frs[i].IPAddress)
eips, err := self.region.GetEips(frs[i].IPAddress, 0, "")
if err != nil {
log.Errorf("GetEips %s", err)

View File

@@ -559,15 +559,15 @@ func (self *SLoadbalancer) GetForwardingRules() ([]SForwardingRule, error) {
targets := make([]string, 0)
for i := range hps {
targets = append(targets, fmt.Sprintf(`(target="%s")`, hps[i].GetId()))
targets = append(targets, fmt.Sprintf(`(target = "%s")`, hps[i].SelfLink))
}
for i := range hsps {
targets = append(targets, fmt.Sprintf(`(target="%s")`, hsps[i].GetId()))
targets = append(targets, fmt.Sprintf(`(target = "%s")`, hsps[i].SelfLink))
}
if strings.Contains(self.GetId(), "/backendServices/") {
targets = append(targets, fmt.Sprintf(`(backendService="%s")`, self.GetId()))
if strings.Contains(self.SelfLink, "/backendServices/") {
targets = append(targets, fmt.Sprintf(`(backendService = "%s")`, self.SelfLink))
}
if len(targets) == 0 {

View File

@@ -9,6 +9,7 @@ import (
"net/url"
"time"
"yunion.io/x/cloudmux/pkg/apis"
"yunion.io/x/cloudmux/pkg/multicloud"
"yunion.io/x/pkg/errors"
)
@@ -66,10 +67,6 @@ func (s *SSSLCertificate) GetIssuer() string {
return s.Brand
}
func (s *SSSLCertificate) GetExpired() bool {
return time.Now().After(s.GetEndDate())
}
func (s *SSSLCertificate) GetEndDate() time.Time {
t, _ := time.Parse("2006-01-02 15:04:05", s.ExpireTime)
return t
@@ -111,11 +108,7 @@ func (s *SSSLCertificate) GetGlobalId() string {
}
func (s *SSSLCertificate) GetStatus() string {
if s.GetExpired() {
return "expired"
} else {
return "normal"
}
return apis.STATUS_AVAILABLE
}
func (s *SSSLCertificate) GetIsUpload() bool {

View File

@@ -220,14 +220,6 @@ func (self *SCertificate) GetIssuer() string {
return self.ProductZhName
}
func (self *SCertificate) GetExpired() bool {
if self.Status == 3 {
return true
} else {
return false
}
}
func (self *SCertificate) GetEndDate() time.Time {
return self.CERTEndTime
}

52
vendor/yunion.io/x/pkg/utils/misc.go generated vendored
View File

@@ -24,6 +24,8 @@ import (
"strconv"
"strings"
"time"
"yunion.io/x/pkg/errors"
)
type selectFunc func(obj interface{}) (string, error)
@@ -535,6 +537,31 @@ func GetSizeKB(sizeStr, defaultSize string) (int64, error) {
return bytes / 1024, nil
}
func transMysqlQuery(dburl string) (string, error) {
queryPos := strings.IndexByte(dburl, '?')
if queryPos == 0 {
return "", fmt.Errorf("Missing database name")
}
var query url.Values
if queryPos > 0 {
queryStr := dburl[queryPos+1:]
if len(queryStr) > 0 {
var err error
query, err = url.ParseQuery(queryStr)
if err != nil {
return "", errors.Wrap(err, "ParseQuery")
}
}
dburl = dburl[:queryPos]
} else {
query = url.Values{}
}
query.Set("parseTime", "true")
query.Set("charset", "utf8mb4")
query.Set("interpolateParams", "true")
return dburl + "?" + query.Encode(), nil
}
func TransSQLAchemyURL(pySQLSrc string) (dialect, ret string, err error) {
if len(pySQLSrc) == 0 {
err = fmt.Errorf("Empty input")
@@ -543,6 +570,10 @@ func TransSQLAchemyURL(pySQLSrc string) (dialect, ret string, err error) {
dialect = "mysql"
if !strings.Contains(pySQLSrc, `//`) {
pySQLSrc, err = transMysqlQuery(pySQLSrc)
if err != nil {
return
}
return dialect, pySQLSrc, nil
}
@@ -560,26 +591,11 @@ func TransSQLAchemyURL(pySQLSrc string) (dialect, ret string, err error) {
return
}
user, passwd, host, port, dburl := strs[1], strs[2], strs[3], strs[4], strs[5]
queryPos := strings.IndexByte(dburl, '?')
if queryPos == 0 {
err = fmt.Errorf("Missing database name")
dburl, err = transMysqlQuery(dburl)
if err != nil {
return
}
var query url.Values
if queryPos > 0 {
queryStr := dburl[queryPos+1:]
if len(queryStr) > 0 {
query, err = url.ParseQuery(queryStr)
if err != nil {
return
}
}
dburl = dburl[:queryPos]
} else {
query = url.Values{}
}
query.Set("parseTime", "True")
ret = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s", user, passwd, host, port, dburl, query.Encode())
ret = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s", user, passwd, host, port, dburl)
return
}