mirror of
https://github.com/SagerNet/sing-box.git
synced 2026-04-11 17:47:20 +10:00
619 lines
19 KiB
Go
619 lines
19 KiB
Go
package originca
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"net"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/sagernet/sing-box/adapter"
|
|
"github.com/sagernet/sing-box/adapter/certificate"
|
|
"github.com/sagernet/sing-box/common/dialer"
|
|
C "github.com/sagernet/sing-box/constant"
|
|
"github.com/sagernet/sing-box/log"
|
|
"github.com/sagernet/sing-box/option"
|
|
E "github.com/sagernet/sing/common/exceptions"
|
|
M "github.com/sagernet/sing/common/metadata"
|
|
"github.com/sagernet/sing/common/ntp"
|
|
|
|
"github.com/caddyserver/certmagic"
|
|
)
|
|
|
|
const (
|
|
cloudflareOriginCAEndpoint = "https://api.cloudflare.com/client/v4/certificates"
|
|
defaultRequestedValidity = option.CloudflareOriginCARequestValidity5475
|
|
// min of 30 days and certmagic's 1/3 lifetime ratio (maintain.go)
|
|
defaultRenewBefore = 30 * 24 * time.Hour
|
|
// from certmagic retry backoff range (async.go)
|
|
minimumRenewRetryDelay = time.Minute
|
|
maximumRenewRetryDelay = time.Hour
|
|
storageLockPrefix = "cloudflare-origin-ca"
|
|
)
|
|
|
|
func RegisterCertificateProvider(registry *certificate.Registry) {
|
|
certificate.Register[option.CloudflareOriginCACertificateProviderOptions](registry, C.TypeCloudflareOriginCA, NewCertificateProvider)
|
|
}
|
|
|
|
var _ adapter.CertificateProviderService = (*Service)(nil)
|
|
|
|
type Service struct {
|
|
certificate.Adapter
|
|
logger log.ContextLogger
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
done chan struct{}
|
|
timeFunc func() time.Time
|
|
httpClient *http.Client
|
|
storage certmagic.Storage
|
|
storageIssuerKey string
|
|
storageNamesKey string
|
|
storageLockKey string
|
|
apiToken string
|
|
originCAKey string
|
|
domain []string
|
|
requestType option.CloudflareOriginCARequestType
|
|
requestedValidity option.CloudflareOriginCARequestValidity
|
|
|
|
access sync.RWMutex
|
|
currentCertificate *tls.Certificate
|
|
currentLeaf *x509.Certificate
|
|
}
|
|
|
|
func NewCertificateProvider(ctx context.Context, logger log.ContextLogger, tag string, options option.CloudflareOriginCACertificateProviderOptions) (adapter.CertificateProviderService, error) {
|
|
domain, err := normalizeHostnames(options.Domain)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(domain) == 0 {
|
|
return nil, E.New("missing domain")
|
|
}
|
|
apiToken := strings.TrimSpace(options.APIToken)
|
|
originCAKey := strings.TrimSpace(options.OriginCAKey)
|
|
switch {
|
|
case apiToken == "" && originCAKey == "":
|
|
return nil, E.New("api_token or origin_ca_key is required")
|
|
case apiToken != "" && originCAKey != "":
|
|
return nil, E.New("api_token and origin_ca_key are mutually exclusive")
|
|
}
|
|
requestType := options.RequestType
|
|
if requestType == "" {
|
|
requestType = option.CloudflareOriginCARequestTypeOriginRSA
|
|
}
|
|
requestedValidity := options.RequestedValidity
|
|
if requestedValidity == 0 {
|
|
requestedValidity = defaultRequestedValidity
|
|
}
|
|
ctx, cancel := context.WithCancel(ctx)
|
|
serviceDialer, err := dialer.NewWithOptions(dialer.Options{
|
|
Context: ctx,
|
|
Options: option.DialerOptions{
|
|
Detour: options.Detour,
|
|
},
|
|
RemoteIsDomain: true,
|
|
})
|
|
if err != nil {
|
|
cancel()
|
|
return nil, E.Cause(err, "create Cloudflare Origin CA dialer")
|
|
}
|
|
var storage certmagic.Storage
|
|
if options.DataDirectory != "" {
|
|
storage = &certmagic.FileStorage{Path: options.DataDirectory}
|
|
} else {
|
|
storage = certmagic.Default.Storage
|
|
}
|
|
timeFunc := ntp.TimeFuncFromContext(ctx)
|
|
if timeFunc == nil {
|
|
timeFunc = time.Now
|
|
}
|
|
storageIssuerKey := C.TypeCloudflareOriginCA + "-" + string(requestType)
|
|
storageNamesKey := (&certmagic.CertificateResource{SANs: slices.Clone(domain)}).NamesKey()
|
|
storageLockKey := strings.Join([]string{
|
|
storageLockPrefix,
|
|
certmagic.StorageKeys.Safe(storageIssuerKey),
|
|
certmagic.StorageKeys.Safe(storageNamesKey),
|
|
}, "/")
|
|
return &Service{
|
|
Adapter: certificate.NewAdapter(C.TypeCloudflareOriginCA, tag),
|
|
logger: logger,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
timeFunc: timeFunc,
|
|
httpClient: &http.Client{Transport: &http.Transport{
|
|
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
|
|
return serviceDialer.DialContext(ctx, network, M.ParseSocksaddr(addr))
|
|
},
|
|
TLSClientConfig: &tls.Config{
|
|
RootCAs: adapter.RootPoolFromContext(ctx),
|
|
Time: timeFunc,
|
|
},
|
|
ForceAttemptHTTP2: true,
|
|
}},
|
|
storage: storage,
|
|
storageIssuerKey: storageIssuerKey,
|
|
storageNamesKey: storageNamesKey,
|
|
storageLockKey: storageLockKey,
|
|
apiToken: apiToken,
|
|
originCAKey: originCAKey,
|
|
domain: domain,
|
|
requestType: requestType,
|
|
requestedValidity: requestedValidity,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) Start(stage adapter.StartStage) error {
|
|
if stage != adapter.StartStateStart {
|
|
return nil
|
|
}
|
|
cachedCertificate, cachedLeaf, err := s.loadCachedCertificate()
|
|
if err != nil {
|
|
s.logger.Warn(E.Cause(err, "load cached Cloudflare Origin CA certificate"))
|
|
} else if cachedCertificate != nil {
|
|
s.setCurrentCertificate(cachedCertificate, cachedLeaf)
|
|
}
|
|
if cachedCertificate == nil {
|
|
err = s.issueAndStoreCertificate()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else if s.shouldRenew(cachedLeaf, s.timeFunc()) {
|
|
err = s.issueAndStoreCertificate()
|
|
if err != nil {
|
|
s.logger.Warn(E.Cause(err, "renew cached Cloudflare Origin CA certificate"))
|
|
}
|
|
}
|
|
s.done = make(chan struct{})
|
|
go s.refreshLoop()
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) Close() error {
|
|
s.cancel()
|
|
if done := s.done; done != nil {
|
|
<-done
|
|
}
|
|
if transport, loaded := s.httpClient.Transport.(*http.Transport); loaded {
|
|
transport.CloseIdleConnections()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
s.access.RLock()
|
|
certificate := s.currentCertificate
|
|
s.access.RUnlock()
|
|
if certificate == nil {
|
|
return nil, E.New("Cloudflare Origin CA certificate is unavailable")
|
|
}
|
|
return certificate, nil
|
|
}
|
|
|
|
func (s *Service) refreshLoop() {
|
|
defer close(s.done)
|
|
var retryDelay time.Duration
|
|
for {
|
|
waitDuration := retryDelay
|
|
if waitDuration == 0 {
|
|
s.access.RLock()
|
|
leaf := s.currentLeaf
|
|
s.access.RUnlock()
|
|
if leaf == nil {
|
|
waitDuration = minimumRenewRetryDelay
|
|
} else {
|
|
refreshAt := leaf.NotAfter.Add(-s.effectiveRenewBefore(leaf))
|
|
waitDuration = refreshAt.Sub(s.timeFunc())
|
|
if waitDuration < minimumRenewRetryDelay {
|
|
waitDuration = minimumRenewRetryDelay
|
|
}
|
|
}
|
|
}
|
|
timer := time.NewTimer(waitDuration)
|
|
select {
|
|
case <-s.ctx.Done():
|
|
if !timer.Stop() {
|
|
select {
|
|
case <-timer.C:
|
|
default:
|
|
}
|
|
}
|
|
return
|
|
case <-timer.C:
|
|
}
|
|
err := s.issueAndStoreCertificate()
|
|
if err != nil {
|
|
s.logger.Error(E.Cause(err, "renew Cloudflare Origin CA certificate"))
|
|
s.access.RLock()
|
|
leaf := s.currentLeaf
|
|
s.access.RUnlock()
|
|
if leaf == nil {
|
|
retryDelay = minimumRenewRetryDelay
|
|
} else {
|
|
remaining := leaf.NotAfter.Sub(s.timeFunc())
|
|
switch {
|
|
case remaining <= minimumRenewRetryDelay:
|
|
retryDelay = minimumRenewRetryDelay
|
|
case remaining < maximumRenewRetryDelay:
|
|
retryDelay = max(remaining/2, minimumRenewRetryDelay)
|
|
default:
|
|
retryDelay = maximumRenewRetryDelay
|
|
}
|
|
}
|
|
continue
|
|
}
|
|
retryDelay = 0
|
|
}
|
|
}
|
|
|
|
func (s *Service) shouldRenew(leaf *x509.Certificate, now time.Time) bool {
|
|
return !now.Before(leaf.NotAfter.Add(-s.effectiveRenewBefore(leaf)))
|
|
}
|
|
|
|
func (s *Service) effectiveRenewBefore(leaf *x509.Certificate) time.Duration {
|
|
lifetime := leaf.NotAfter.Sub(leaf.NotBefore)
|
|
if lifetime <= 0 {
|
|
return 0
|
|
}
|
|
return min(lifetime/3, defaultRenewBefore)
|
|
}
|
|
|
|
func (s *Service) issueAndStoreCertificate() error {
|
|
err := s.storage.Lock(s.ctx, s.storageLockKey)
|
|
if err != nil {
|
|
return E.Cause(err, "lock Cloudflare Origin CA certificate storage")
|
|
}
|
|
defer func() {
|
|
err = s.storage.Unlock(context.WithoutCancel(s.ctx), s.storageLockKey)
|
|
if err != nil {
|
|
s.logger.Warn(E.Cause(err, "unlock Cloudflare Origin CA certificate storage"))
|
|
}
|
|
}()
|
|
cachedCertificate, cachedLeaf, err := s.loadCachedCertificate()
|
|
if err != nil {
|
|
s.logger.Warn(E.Cause(err, "load cached Cloudflare Origin CA certificate"))
|
|
} else if cachedCertificate != nil && !s.shouldRenew(cachedLeaf, s.timeFunc()) {
|
|
s.setCurrentCertificate(cachedCertificate, cachedLeaf)
|
|
return nil
|
|
}
|
|
certificatePEM, privateKeyPEM, tlsCertificate, leaf, err := s.requestCertificate(s.ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
issuerData, err := json.Marshal(originCAIssuerData{
|
|
RequestType: s.requestType,
|
|
RequestedValidity: s.requestedValidity,
|
|
})
|
|
if err != nil {
|
|
return E.Cause(err, "encode Cloudflare Origin CA certificate metadata")
|
|
}
|
|
err = storeCertificateResource(s.ctx, s.storage, s.storageIssuerKey, certmagic.CertificateResource{
|
|
SANs: slices.Clone(s.domain),
|
|
CertificatePEM: certificatePEM,
|
|
PrivateKeyPEM: privateKeyPEM,
|
|
IssuerData: issuerData,
|
|
})
|
|
if err != nil {
|
|
return E.Cause(err, "store Cloudflare Origin CA certificate")
|
|
}
|
|
s.setCurrentCertificate(tlsCertificate, leaf)
|
|
s.logger.Info("updated Cloudflare Origin CA certificate, expires at ", leaf.NotAfter.Format(time.RFC3339))
|
|
return nil
|
|
}
|
|
|
|
func (s *Service) requestCertificate(ctx context.Context) ([]byte, []byte, *tls.Certificate, *x509.Certificate, error) {
|
|
var privateKey crypto.Signer
|
|
switch s.requestType {
|
|
case option.CloudflareOriginCARequestTypeOriginRSA:
|
|
rsaKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
privateKey = rsaKey
|
|
case option.CloudflareOriginCARequestTypeOriginECC:
|
|
ecKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
privateKey = ecKey
|
|
default:
|
|
return nil, nil, nil, nil, E.New("unsupported Cloudflare Origin CA request type: ", s.requestType)
|
|
}
|
|
privateKeyDER, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, E.Cause(err, "encode private key")
|
|
}
|
|
privateKeyPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "PRIVATE KEY",
|
|
Bytes: privateKeyDER,
|
|
})
|
|
certificateRequestDER, err := x509.CreateCertificateRequest(rand.Reader, &x509.CertificateRequest{
|
|
Subject: pkix.Name{CommonName: s.domain[0]},
|
|
DNSNames: s.domain,
|
|
}, privateKey)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, E.Cause(err, "create certificate request")
|
|
}
|
|
certificateRequestPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE REQUEST",
|
|
Bytes: certificateRequestDER,
|
|
})
|
|
requestBody, err := json.Marshal(originCARequest{
|
|
CSR: string(certificateRequestPEM),
|
|
Hostnames: s.domain,
|
|
RequestType: string(s.requestType),
|
|
RequestedValidity: uint16(s.requestedValidity),
|
|
})
|
|
if err != nil {
|
|
return nil, nil, nil, nil, E.Cause(err, "marshal request")
|
|
}
|
|
request, err := http.NewRequestWithContext(ctx, http.MethodPost, cloudflareOriginCAEndpoint, bytes.NewReader(requestBody))
|
|
if err != nil {
|
|
return nil, nil, nil, nil, E.Cause(err, "create request")
|
|
}
|
|
request.Header.Set("Accept", "application/json")
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.Header.Set("User-Agent", "sing-box/"+C.Version)
|
|
if s.apiToken != "" {
|
|
request.Header.Set("Authorization", "Bearer "+s.apiToken)
|
|
} else {
|
|
request.Header.Set("X-Auth-User-Service-Key", s.originCAKey)
|
|
}
|
|
response, err := s.httpClient.Do(request)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, E.Cause(err, "request certificate from Cloudflare")
|
|
}
|
|
defer response.Body.Close()
|
|
responseBody, err := io.ReadAll(response.Body)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, E.Cause(err, "read Cloudflare response")
|
|
}
|
|
var responseEnvelope originCAResponse
|
|
err = json.Unmarshal(responseBody, &responseEnvelope)
|
|
if err != nil && response.StatusCode >= http.StatusOK && response.StatusCode < http.StatusMultipleChoices {
|
|
return nil, nil, nil, nil, E.Cause(err, "decode Cloudflare response")
|
|
}
|
|
if response.StatusCode < http.StatusOK || response.StatusCode >= http.StatusMultipleChoices {
|
|
return nil, nil, nil, nil, buildOriginCAError(response.StatusCode, responseEnvelope.Errors, responseBody)
|
|
}
|
|
if !responseEnvelope.Success {
|
|
return nil, nil, nil, nil, buildOriginCAError(response.StatusCode, responseEnvelope.Errors, responseBody)
|
|
}
|
|
if responseEnvelope.Result.Certificate == "" {
|
|
return nil, nil, nil, nil, E.New("Cloudflare Origin CA response is missing certificate data")
|
|
}
|
|
certificatePEM := []byte(responseEnvelope.Result.Certificate)
|
|
tlsCertificate, leaf, err := parseKeyPair(certificatePEM, privateKeyPEM)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, E.Cause(err, "parse issued certificate")
|
|
}
|
|
if !s.matchesCertificate(leaf) {
|
|
return nil, nil, nil, nil, E.New("issued Cloudflare Origin CA certificate does not match requested hostnames or key type")
|
|
}
|
|
return certificatePEM, privateKeyPEM, tlsCertificate, leaf, nil
|
|
}
|
|
|
|
func (s *Service) loadCachedCertificate() (*tls.Certificate, *x509.Certificate, error) {
|
|
certificateResource, err := loadCertificateResource(s.ctx, s.storage, s.storageIssuerKey, s.storageNamesKey)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, nil, nil
|
|
}
|
|
return nil, nil, err
|
|
}
|
|
tlsCertificate, leaf, err := parseKeyPair(certificateResource.CertificatePEM, certificateResource.PrivateKeyPEM)
|
|
if err != nil {
|
|
return nil, nil, E.Cause(err, "parse cached key pair")
|
|
}
|
|
if s.timeFunc().After(leaf.NotAfter) {
|
|
return nil, nil, nil
|
|
}
|
|
if !s.matchesCertificate(leaf) {
|
|
return nil, nil, nil
|
|
}
|
|
return tlsCertificate, leaf, nil
|
|
}
|
|
|
|
func (s *Service) matchesCertificate(leaf *x509.Certificate) bool {
|
|
if leaf == nil {
|
|
return false
|
|
}
|
|
leafHostnames := leaf.DNSNames
|
|
if len(leafHostnames) == 0 && leaf.Subject.CommonName != "" {
|
|
leafHostnames = []string{leaf.Subject.CommonName}
|
|
}
|
|
normalizedLeafHostnames, err := normalizeHostnames(leafHostnames)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
if !slices.Equal(normalizedLeafHostnames, s.domain) {
|
|
return false
|
|
}
|
|
switch s.requestType {
|
|
case option.CloudflareOriginCARequestTypeOriginRSA:
|
|
return leaf.PublicKeyAlgorithm == x509.RSA
|
|
case option.CloudflareOriginCARequestTypeOriginECC:
|
|
return leaf.PublicKeyAlgorithm == x509.ECDSA
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func (s *Service) setCurrentCertificate(certificate *tls.Certificate, leaf *x509.Certificate) {
|
|
s.access.Lock()
|
|
s.currentCertificate = certificate
|
|
s.currentLeaf = leaf
|
|
s.access.Unlock()
|
|
}
|
|
|
|
func normalizeHostnames(hostnames []string) ([]string, error) {
|
|
normalizedHostnames := make([]string, 0, len(hostnames))
|
|
seen := make(map[string]struct{}, len(hostnames))
|
|
for _, hostname := range hostnames {
|
|
normalizedHostname := strings.ToLower(strings.TrimSpace(strings.TrimSuffix(hostname, ".")))
|
|
if normalizedHostname == "" {
|
|
return nil, E.New("hostname is empty")
|
|
}
|
|
if net.ParseIP(normalizedHostname) != nil {
|
|
return nil, E.New("hostname cannot be an IP address: ", normalizedHostname)
|
|
}
|
|
if strings.Contains(normalizedHostname, "*") {
|
|
if !strings.HasPrefix(normalizedHostname, "*.") || strings.Count(normalizedHostname, "*") != 1 {
|
|
return nil, E.New("invalid wildcard hostname: ", normalizedHostname)
|
|
}
|
|
suffix := strings.TrimPrefix(normalizedHostname, "*.")
|
|
if strings.Count(suffix, ".") == 0 {
|
|
return nil, E.New("wildcard hostname must cover a multi-label domain: ", normalizedHostname)
|
|
}
|
|
normalizedHostname = "*." + suffix
|
|
}
|
|
if _, loaded := seen[normalizedHostname]; loaded {
|
|
continue
|
|
}
|
|
seen[normalizedHostname] = struct{}{}
|
|
normalizedHostnames = append(normalizedHostnames, normalizedHostname)
|
|
}
|
|
slices.Sort(normalizedHostnames)
|
|
return normalizedHostnames, nil
|
|
}
|
|
|
|
func parseKeyPair(certificatePEM []byte, privateKeyPEM []byte) (*tls.Certificate, *x509.Certificate, error) {
|
|
keyPair, err := tls.X509KeyPair(certificatePEM, privateKeyPEM)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if len(keyPair.Certificate) == 0 {
|
|
return nil, nil, E.New("certificate chain is empty")
|
|
}
|
|
leaf, err := x509.ParseCertificate(keyPair.Certificate[0])
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
keyPair.Leaf = leaf
|
|
return &keyPair, leaf, nil
|
|
}
|
|
|
|
func storeCertificateResource(ctx context.Context, storage certmagic.Storage, issuerKey string, certificateResource certmagic.CertificateResource) error {
|
|
metaBytes, err := json.MarshalIndent(certificateResource, "", "\t")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
namesKey := certificateResource.NamesKey()
|
|
keyValueList := []struct {
|
|
key string
|
|
value []byte
|
|
}{
|
|
{
|
|
key: certmagic.StorageKeys.SitePrivateKey(issuerKey, namesKey),
|
|
value: certificateResource.PrivateKeyPEM,
|
|
},
|
|
{
|
|
key: certmagic.StorageKeys.SiteCert(issuerKey, namesKey),
|
|
value: certificateResource.CertificatePEM,
|
|
},
|
|
{
|
|
key: certmagic.StorageKeys.SiteMeta(issuerKey, namesKey),
|
|
value: metaBytes,
|
|
},
|
|
}
|
|
for i, item := range keyValueList {
|
|
err = storage.Store(ctx, item.key, item.value)
|
|
if err != nil {
|
|
for j := i - 1; j >= 0; j-- {
|
|
storage.Delete(ctx, keyValueList[j].key)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func loadCertificateResource(ctx context.Context, storage certmagic.Storage, issuerKey string, namesKey string) (certmagic.CertificateResource, error) {
|
|
privateKeyPEM, err := storage.Load(ctx, certmagic.StorageKeys.SitePrivateKey(issuerKey, namesKey))
|
|
if err != nil {
|
|
return certmagic.CertificateResource{}, err
|
|
}
|
|
certificatePEM, err := storage.Load(ctx, certmagic.StorageKeys.SiteCert(issuerKey, namesKey))
|
|
if err != nil {
|
|
return certmagic.CertificateResource{}, err
|
|
}
|
|
metaBytes, err := storage.Load(ctx, certmagic.StorageKeys.SiteMeta(issuerKey, namesKey))
|
|
if err != nil {
|
|
return certmagic.CertificateResource{}, err
|
|
}
|
|
var certificateResource certmagic.CertificateResource
|
|
err = json.Unmarshal(metaBytes, &certificateResource)
|
|
if err != nil {
|
|
return certmagic.CertificateResource{}, E.Cause(err, "decode Cloudflare Origin CA certificate metadata")
|
|
}
|
|
certificateResource.PrivateKeyPEM = privateKeyPEM
|
|
certificateResource.CertificatePEM = certificatePEM
|
|
return certificateResource, nil
|
|
}
|
|
|
|
func buildOriginCAError(statusCode int, responseErrors []originCAResponseError, responseBody []byte) error {
|
|
if len(responseErrors) > 0 {
|
|
messageList := make([]string, 0, len(responseErrors))
|
|
for _, responseError := range responseErrors {
|
|
if responseError.Message == "" {
|
|
continue
|
|
}
|
|
if responseError.Code != 0 {
|
|
messageList = append(messageList, responseError.Message+" (code "+strconv.Itoa(responseError.Code)+")")
|
|
} else {
|
|
messageList = append(messageList, responseError.Message)
|
|
}
|
|
}
|
|
if len(messageList) > 0 {
|
|
return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode, " ", strings.Join(messageList, ", "))
|
|
}
|
|
}
|
|
responseText := strings.TrimSpace(string(responseBody))
|
|
if responseText == "" {
|
|
return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode)
|
|
}
|
|
return E.New("Cloudflare Origin CA request failed: HTTP ", statusCode, " ", responseText)
|
|
}
|
|
|
|
type originCARequest struct {
|
|
CSR string `json:"csr"`
|
|
Hostnames []string `json:"hostnames"`
|
|
RequestType string `json:"request_type"`
|
|
RequestedValidity uint16 `json:"requested_validity"`
|
|
}
|
|
|
|
type originCAResponse struct {
|
|
Success bool `json:"success"`
|
|
Errors []originCAResponseError `json:"errors"`
|
|
Result originCAResponseResult `json:"result"`
|
|
}
|
|
|
|
type originCAResponseError struct {
|
|
Code int `json:"code"`
|
|
Message string `json:"message"`
|
|
}
|
|
|
|
type originCAResponseResult struct {
|
|
Certificate string `json:"certificate"`
|
|
}
|
|
|
|
type originCAIssuerData struct {
|
|
RequestType option.CloudflareOriginCARequestType `json:"request_type,omitempty"`
|
|
RequestedValidity option.CloudflareOriginCARequestValidity `json:"requested_validity,omitempty"`
|
|
}
|