mirror of
https://github.com/yusing/godoxy.git
synced 2026-04-24 09:18:31 +02:00
feat(autocert): add multi-certificate support (#185)
Multi-certificate, SNI matching with exact map and suffix tree Add support for multiple TLS certificates with SNI-based selection. The root provider maintains a single centralized SNI matcher that uses an exact match map for O(1) lookups, falling back to a suffix tree for wildcard matching. Key features: - Add `Extra []Config` field to autocert.Config for additional certificates - Each extra entry must specify unique `cert_path` and `key_path` - Extra certs inherit main config (except `email` and `extra` fields) - Extra certs participate in ACME obtain/renew cycles independently - SNI selection precedence: exact match > wildcard match, main > extra - Single centralized SNI matcher on root provider rebuilt after cert changes The SNI matcher structure: - Exact match map: O(1) lookup for exact domain matches - Suffix tree: Efficient wildcard matching (e.g., *.example.com) Implementation details: - Provider.GetCert() now uses SNI from ClientHelloInfo for selection - Main cert is returned as fallback when no SNI match is found - Extra providers are created as child providers with merged configs - SNI matcher is rebuilt after Setup() and after ObtainCert() completes
This commit is contained in:
@@ -24,6 +24,7 @@ type Config struct {
|
|||||||
Domains []string `json:"domains,omitempty"`
|
Domains []string `json:"domains,omitempty"`
|
||||||
CertPath string `json:"cert_path,omitempty"`
|
CertPath string `json:"cert_path,omitempty"`
|
||||||
KeyPath string `json:"key_path,omitempty"`
|
KeyPath string `json:"key_path,omitempty"`
|
||||||
|
Extra []Config `json:"extra,omitempty"`
|
||||||
ACMEKeyPath string `json:"acme_key_path,omitempty"`
|
ACMEKeyPath string `json:"acme_key_path,omitempty"`
|
||||||
Provider string `json:"provider,omitempty"`
|
Provider string `json:"provider,omitempty"`
|
||||||
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
Options map[string]strutils.Redacted `json:"options,omitempty"`
|
||||||
@@ -48,6 +49,9 @@ var (
|
|||||||
ErrMissingEmail = gperr.New("missing field 'email'")
|
ErrMissingEmail = gperr.New("missing field 'email'")
|
||||||
ErrMissingProvider = gperr.New("missing field 'provider'")
|
ErrMissingProvider = gperr.New("missing field 'provider'")
|
||||||
ErrMissingCADirURL = gperr.New("missing field 'ca_dir_url'")
|
ErrMissingCADirURL = gperr.New("missing field 'ca_dir_url'")
|
||||||
|
ErrMissingCertPath = gperr.New("missing field 'cert_path'")
|
||||||
|
ErrMissingKeyPath = gperr.New("missing field 'key_path'")
|
||||||
|
ErrDuplicatedPath = gperr.New("duplicated path")
|
||||||
ErrInvalidDomain = gperr.New("invalid domain")
|
ErrInvalidDomain = gperr.New("invalid domain")
|
||||||
ErrUnknownProvider = gperr.New("unknown provider")
|
ErrUnknownProvider = gperr.New("unknown provider")
|
||||||
)
|
)
|
||||||
@@ -68,10 +72,36 @@ func (cfg *Config) Validate() gperr.Error {
|
|||||||
|
|
||||||
if cfg.Provider == "" {
|
if cfg.Provider == "" {
|
||||||
cfg.Provider = ProviderLocal
|
cfg.Provider = ProviderLocal
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
b := gperr.NewBuilder("autocert errors")
|
b := gperr.NewBuilder("autocert errors")
|
||||||
|
if len(cfg.Extra) > 0 {
|
||||||
|
seenCertPaths := make(map[string]int, len(cfg.Extra))
|
||||||
|
seenKeyPaths := make(map[string]int, len(cfg.Extra))
|
||||||
|
for i := range cfg.Extra {
|
||||||
|
if cfg.Extra[i].CertPath == "" {
|
||||||
|
b.Add(ErrMissingCertPath.Subjectf("extra[%d].cert_path", i))
|
||||||
|
}
|
||||||
|
if cfg.Extra[i].KeyPath == "" {
|
||||||
|
b.Add(ErrMissingKeyPath.Subjectf("extra[%d].key_path", i))
|
||||||
|
}
|
||||||
|
if cfg.Extra[i].CertPath != "" {
|
||||||
|
if first, ok := seenCertPaths[cfg.Extra[i].CertPath]; ok {
|
||||||
|
b.Add(ErrDuplicatedPath.Subjectf("extra[%d].cert_path", i).Withf("first: %d", first))
|
||||||
|
} else {
|
||||||
|
seenCertPaths[cfg.Extra[i].CertPath] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cfg.Extra[i].KeyPath != "" {
|
||||||
|
if first, ok := seenKeyPaths[cfg.Extra[i].KeyPath]; ok {
|
||||||
|
b.Add(ErrDuplicatedPath.Subjectf("extra[%d].key_path", i).Withf("first: %d", first))
|
||||||
|
} else {
|
||||||
|
seenKeyPaths[cfg.Extra[i].KeyPath] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if cfg.Provider == ProviderCustom && cfg.CADirURL == "" {
|
if cfg.Provider == ProviderCustom && cfg.CADirURL == "" {
|
||||||
b.Add(ErrMissingCADirURL)
|
b.Add(ErrMissingCADirURL)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
package autocert
|
package autocert
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
"maps"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
@@ -33,9 +34,14 @@ type (
|
|||||||
client *lego.Client
|
client *lego.Client
|
||||||
lastFailure time.Time
|
lastFailure time.Time
|
||||||
|
|
||||||
|
lastFailureFile string
|
||||||
|
|
||||||
legoCert *certificate.Resource
|
legoCert *certificate.Resource
|
||||||
tlsCert *tls.Certificate
|
tlsCert *tls.Certificate
|
||||||
certExpiries CertExpiries
|
certExpiries CertExpiries
|
||||||
|
|
||||||
|
extraProviders []*Provider
|
||||||
|
sniMatcher sniMatcher
|
||||||
}
|
}
|
||||||
|
|
||||||
CertExpiries map[string]time.Time
|
CertExpiries map[string]time.Time
|
||||||
@@ -55,16 +61,23 @@ var ActiveProvider atomic.Pointer[Provider]
|
|||||||
|
|
||||||
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) *Provider {
|
func NewProvider(cfg *Config, user *User, legoCfg *lego.Config) *Provider {
|
||||||
return &Provider{
|
return &Provider{
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
user: user,
|
user: user,
|
||||||
legoCfg: legoCfg,
|
legoCfg: legoCfg,
|
||||||
|
lastFailureFile: lastFailureFileFor(cfg.CertPath, cfg.KeyPath),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) GetCert(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (p *Provider) GetCert(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
if p.tlsCert == nil {
|
if p.tlsCert == nil {
|
||||||
return nil, ErrGetCertFailure
|
return nil, ErrGetCertFailure
|
||||||
}
|
}
|
||||||
|
if hello == nil || hello.ServerName == "" {
|
||||||
|
return p.tlsCert, nil
|
||||||
|
}
|
||||||
|
if prov := p.sniMatcher.match(hello.ServerName); prov != nil && prov.tlsCert != nil {
|
||||||
|
return prov.tlsCert, nil
|
||||||
|
}
|
||||||
return p.tlsCert, nil
|
return p.tlsCert, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +103,7 @@ func (p *Provider) GetLastFailure() (time.Time, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if p.lastFailure.IsZero() {
|
if p.lastFailure.IsZero() {
|
||||||
data, err := os.ReadFile(LastFailureFile)
|
data, err := os.ReadFile(p.lastFailureFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return time.Time{}, err
|
return time.Time{}, err
|
||||||
@@ -108,7 +121,7 @@ func (p *Provider) UpdateLastFailure() error {
|
|||||||
}
|
}
|
||||||
t := time.Now()
|
t := time.Now()
|
||||||
p.lastFailure = t
|
p.lastFailure = t
|
||||||
return os.WriteFile(LastFailureFile, t.AppendFormat(nil, time.RFC3339), 0o600)
|
return os.WriteFile(p.lastFailureFile, t.AppendFormat(nil, time.RFC3339), 0o600)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) ClearLastFailure() error {
|
func (p *Provider) ClearLastFailure() error {
|
||||||
@@ -116,10 +129,26 @@ func (p *Provider) ClearLastFailure() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
p.lastFailure = time.Time{}
|
p.lastFailure = time.Time{}
|
||||||
return os.Remove(LastFailureFile)
|
return os.Remove(p.lastFailureFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) ObtainCert() error {
|
func (p *Provider) ObtainCert() error {
|
||||||
|
if len(p.extraProviders) > 0 {
|
||||||
|
errs := gperr.NewGroup("autocert errors")
|
||||||
|
errs.Go(p.obtainCertSelf)
|
||||||
|
for _, ep := range p.extraProviders {
|
||||||
|
errs.Go(ep.obtainCertSelf)
|
||||||
|
}
|
||||||
|
if err := errs.Wait().Error(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.rebuildSNIMatcher()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.obtainCertSelf()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) obtainCertSelf() error {
|
||||||
if p.cfg.Provider == ProviderLocal {
|
if p.cfg.Provider == ProviderLocal {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -239,7 +268,7 @@ func (p *Provider) ScheduleRenewal(parent task.Parent) {
|
|||||||
timer := time.NewTimer(time.Until(renewalTime))
|
timer := time.NewTimer(time.Until(renewalTime))
|
||||||
defer timer.Stop()
|
defer timer.Stop()
|
||||||
|
|
||||||
task := parent.Subtask("cert-renew-scheduler", true)
|
task := parent.Subtask("cert-renew-scheduler:"+filepath.Base(p.cfg.CertPath), true)
|
||||||
defer task.Finish(nil)
|
defer task.Finish(nil)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -282,6 +311,9 @@ func (p *Provider) ScheduleRenewal(parent task.Parent) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
for _, ep := range p.extraProviders {
|
||||||
|
ep.ScheduleRenewal(parent)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Provider) initClient() error {
|
func (p *Provider) initClient() error {
|
||||||
@@ -334,10 +366,10 @@ func (p *Provider) saveCert(cert *certificate.Resource) error {
|
|||||||
}
|
}
|
||||||
/* This should have been done in setup
|
/* This should have been done in setup
|
||||||
but double check is always a good choice.*/
|
but double check is always a good choice.*/
|
||||||
_, err := os.Stat(path.Dir(p.cfg.CertPath))
|
_, err := os.Stat(filepath.Dir(p.cfg.CertPath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
if err = os.MkdirAll(path.Dir(p.cfg.CertPath), 0o755); err != nil {
|
if err = os.MkdirAll(filepath.Dir(p.cfg.CertPath), 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -391,7 +423,7 @@ func (p *Provider) renewIfNeeded() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.ObtainCert()
|
return p.obtainCertSelf()
|
||||||
}
|
}
|
||||||
|
|
||||||
func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
||||||
@@ -411,3 +443,20 @@ func getCertExpiries(cert *tls.Certificate) (CertExpiries, error) {
|
|||||||
}
|
}
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lastFailureFileFor(certPath, keyPath string) string {
|
||||||
|
if certPath == "" && keyPath == "" {
|
||||||
|
return LastFailureFile
|
||||||
|
}
|
||||||
|
dir := filepath.Dir(certPath)
|
||||||
|
sum := sha256.Sum256([]byte(certPath + "|" + keyPath))
|
||||||
|
return filepath.Join(dir, fmt.Sprintf(".last_failure-%x", sum[:6]))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Provider) rebuildSNIMatcher() {
|
||||||
|
p.sniMatcher = sniMatcher{}
|
||||||
|
p.sniMatcher.addProvider(p)
|
||||||
|
for _, ep := range p.extraProviders {
|
||||||
|
p.sniMatcher.addProvider(ep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
32
internal/autocert/provider_test/extra_validation_test.go
Normal file
32
internal/autocert/provider_test/extra_validation_test.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package provider_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestExtraCertKeyPathsUnique(t *testing.T) {
|
||||||
|
t.Run("duplicate cert_path rejected", func(t *testing.T) {
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: "a.crt", KeyPath: "a.key"},
|
||||||
|
{CertPath: "a.crt", KeyPath: "b.key"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Error(t, cfg.Validate())
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("duplicate key_path rejected", func(t *testing.T) {
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: "a.crt", KeyPath: "a.key"},
|
||||||
|
{CertPath: "b.crt", KeyPath: "a.key"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
require.Error(t, cfg.Validate())
|
||||||
|
})
|
||||||
|
}
|
||||||
383
internal/autocert/provider_test/sni_test.go
Normal file
383
internal/autocert/provider_test/sni_test.go
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
package provider_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"github.com/yusing/godoxy/internal/autocert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func writeSelfSignedCert(t *testing.T, dir string, dnsNames []string) (string, string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cn := ""
|
||||||
|
if len(dnsNames) > 0 {
|
||||||
|
cn = dnsNames[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: cn,
|
||||||
|
},
|
||||||
|
NotBefore: time.Now().Add(-time.Minute),
|
||||||
|
NotAfter: time.Now().Add(24 * time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
DNSNames: dnsNames,
|
||||||
|
}
|
||||||
|
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
certPath := filepath.Join(dir, "cert.pem")
|
||||||
|
keyPath := filepath.Join(dir, "key.pem")
|
||||||
|
|
||||||
|
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
|
||||||
|
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})
|
||||||
|
|
||||||
|
require.NoError(t, os.WriteFile(certPath, certPEM, 0o644))
|
||||||
|
require.NoError(t, os.WriteFile(keyPath, keyPEM, 0o600))
|
||||||
|
|
||||||
|
return certPath, keyPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetCertBySNI(t *testing.T) {
|
||||||
|
t.Run("extra cert used when main does not match", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
extraDir := t.TempDir()
|
||||||
|
extraCert, extraKey := writeSelfSignedCert(t, extraDir, []string{"*.internal.example.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: extraCert, KeyPath: extraKey},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "a.internal.example.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "*.internal.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("exact match wins over wildcard match", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
extraDir := t.TempDir()
|
||||||
|
extraCert, extraKey := writeSelfSignedCert(t, extraDir, []string{"foo.example.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: extraCert, KeyPath: extraKey},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "foo.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("main cert fallback when no match", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
extraDir := t.TempDir()
|
||||||
|
extraCert, extraKey := writeSelfSignedCert(t, extraDir, []string{"*.test.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: extraCert, KeyPath: extraKey},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "unknown.domain.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "*.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("nil ServerName returns main cert", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(nil)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "*.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty ServerName returns main cert", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: ""})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "*.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("case insensitive matching", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
extraDir := t.TempDir()
|
||||||
|
extraCert, extraKey := writeSelfSignedCert(t, extraDir, []string{"Foo.Example.COM"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: extraCert, KeyPath: extraKey},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "FOO.EXAMPLE.COM"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "Foo.Example.COM")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("normalization with trailing dot and whitespace", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
extraDir := t.TempDir()
|
||||||
|
extraCert, extraKey := writeSelfSignedCert(t, extraDir, []string{"foo.example.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: extraCert, KeyPath: extraKey},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: " foo.example.com. "})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "foo.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("longest wildcard match wins", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
extraDir1 := t.TempDir()
|
||||||
|
extraCert1, extraKey1 := writeSelfSignedCert(t, extraDir1, []string{"*.a.example.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: extraCert1, KeyPath: extraKey1},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.a.example.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "*.a.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("main cert wildcard match", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "bar.example.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf.DNSNames, "*.example.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple extra certs", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
extraDir1 := t.TempDir()
|
||||||
|
extraCert1, extraKey1 := writeSelfSignedCert(t, extraDir1, []string{"*.test.com"})
|
||||||
|
|
||||||
|
extraDir2 := t.TempDir()
|
||||||
|
extraCert2, extraKey2 := writeSelfSignedCert(t, extraDir2, []string{"*.dev.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: extraCert1, KeyPath: extraKey1},
|
||||||
|
{CertPath: extraCert2, KeyPath: extraKey2},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.test.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
leaf1, err := x509.ParseCertificate(cert1.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf1.DNSNames, "*.test.com")
|
||||||
|
|
||||||
|
cert2, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "bar.dev.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
leaf2, err := x509.ParseCertificate(cert2.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf2.DNSNames, "*.dev.com")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple DNSNames in cert", func(t *testing.T) {
|
||||||
|
mainDir := t.TempDir()
|
||||||
|
mainCert, mainKey := writeSelfSignedCert(t, mainDir, []string{"*.example.com"})
|
||||||
|
|
||||||
|
extraDir := t.TempDir()
|
||||||
|
extraCert, extraKey := writeSelfSignedCert(t, extraDir, []string{"foo.example.com", "bar.example.com", "*.test.com"})
|
||||||
|
|
||||||
|
cfg := &autocert.Config{
|
||||||
|
Provider: autocert.ProviderLocal,
|
||||||
|
CertPath: mainCert,
|
||||||
|
KeyPath: mainKey,
|
||||||
|
Extra: []autocert.Config{
|
||||||
|
{CertPath: extraCert, KeyPath: extraKey},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, cfg.Validate())
|
||||||
|
|
||||||
|
p := autocert.NewProvider(cfg, nil, nil)
|
||||||
|
require.NoError(t, p.Setup())
|
||||||
|
|
||||||
|
cert1, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "foo.example.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
leaf1, err := x509.ParseCertificate(cert1.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf1.DNSNames, "foo.example.com")
|
||||||
|
|
||||||
|
cert2, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "bar.example.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
leaf2, err := x509.ParseCertificate(cert2.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf2.DNSNames, "bar.example.com")
|
||||||
|
|
||||||
|
cert3, err := p.GetCert(&tls.ClientHelloInfo{ServerName: "baz.test.com"})
|
||||||
|
require.NoError(t, err)
|
||||||
|
leaf3, err := x509.ParseCertificate(cert3.Certificate[0])
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Contains(t, leaf3.DNSNames, "*.test.com")
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -2,9 +2,11 @@ package autocert
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
|
gperr "github.com/yusing/goutils/errs"
|
||||||
strutils "github.com/yusing/goutils/strings"
|
strutils "github.com/yusing/goutils/strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +21,10 @@ func (p *Provider) Setup() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err = p.setupExtraProviders(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
for _, expiry := range p.GetExpiries() {
|
for _, expiry := range p.GetExpiries() {
|
||||||
log.Info().Msg("certificate expire on " + strutils.FormatTime(expiry))
|
log.Info().Msg("certificate expire on " + strutils.FormatTime(expiry))
|
||||||
break
|
break
|
||||||
@@ -26,3 +32,70 @@ func (p *Provider) Setup() (err error) {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *Provider) setupExtraProviders() error {
|
||||||
|
p.extraProviders = nil
|
||||||
|
p.sniMatcher = sniMatcher{}
|
||||||
|
if len(p.cfg.Extra) == 0 {
|
||||||
|
p.rebuildSNIMatcher()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range p.cfg.Extra {
|
||||||
|
merged := mergeExtraConfig(p.cfg, &p.cfg.Extra[i])
|
||||||
|
user, legoCfg, err := merged.GetLegoConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err.Subjectf("extra[%d]", i)
|
||||||
|
}
|
||||||
|
ep := NewProvider(&merged, user, legoCfg)
|
||||||
|
if err := ep.Setup(); err != nil {
|
||||||
|
return gperr.PrependSubject(fmt.Sprintf("extra[%d]", i), err)
|
||||||
|
}
|
||||||
|
p.extraProviders = append(p.extraProviders, ep)
|
||||||
|
}
|
||||||
|
p.rebuildSNIMatcher()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeExtraConfig(mainCfg *Config, extraCfg *Config) Config {
|
||||||
|
merged := *mainCfg
|
||||||
|
merged.Extra = nil
|
||||||
|
merged.CertPath = extraCfg.CertPath
|
||||||
|
merged.KeyPath = extraCfg.KeyPath
|
||||||
|
|
||||||
|
if merged.Email == "" {
|
||||||
|
merged.Email = mainCfg.Email
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(extraCfg.Domains) > 0 {
|
||||||
|
merged.Domains = extraCfg.Domains
|
||||||
|
}
|
||||||
|
if extraCfg.ACMEKeyPath != "" {
|
||||||
|
merged.ACMEKeyPath = extraCfg.ACMEKeyPath
|
||||||
|
}
|
||||||
|
if extraCfg.Provider != "" {
|
||||||
|
merged.Provider = extraCfg.Provider
|
||||||
|
}
|
||||||
|
if len(extraCfg.Options) > 0 {
|
||||||
|
merged.Options = extraCfg.Options
|
||||||
|
}
|
||||||
|
if len(extraCfg.Resolvers) > 0 {
|
||||||
|
merged.Resolvers = extraCfg.Resolvers
|
||||||
|
}
|
||||||
|
if extraCfg.CADirURL != "" {
|
||||||
|
merged.CADirURL = extraCfg.CADirURL
|
||||||
|
}
|
||||||
|
if len(extraCfg.CACerts) > 0 {
|
||||||
|
merged.CACerts = extraCfg.CACerts
|
||||||
|
}
|
||||||
|
if extraCfg.EABKid != "" {
|
||||||
|
merged.EABKid = extraCfg.EABKid
|
||||||
|
}
|
||||||
|
if extraCfg.EABHmac != "" {
|
||||||
|
merged.EABHmac = extraCfg.EABHmac
|
||||||
|
}
|
||||||
|
if extraCfg.HTTPClient != nil {
|
||||||
|
merged.HTTPClient = extraCfg.HTTPClient
|
||||||
|
}
|
||||||
|
return merged
|
||||||
|
}
|
||||||
|
|||||||
129
internal/autocert/sni_matcher.go
Normal file
129
internal/autocert/sni_matcher.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/x509"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type sniMatcher struct {
|
||||||
|
exact map[string]*Provider
|
||||||
|
root sniTreeNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type sniTreeNode struct {
|
||||||
|
children map[string]*sniTreeNode
|
||||||
|
wildcard *Provider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *sniMatcher) match(serverName string) *Provider {
|
||||||
|
if m == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
serverName = normalizeServerName(serverName)
|
||||||
|
if serverName == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if m.exact != nil {
|
||||||
|
if p, ok := m.exact[serverName]; ok {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return m.matchSuffixTree(serverName)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *sniMatcher) matchSuffixTree(serverName string) *Provider {
|
||||||
|
n := &m.root
|
||||||
|
labels := strings.Split(serverName, ".")
|
||||||
|
|
||||||
|
var best *Provider
|
||||||
|
for i := len(labels) - 1; i >= 0; i-- {
|
||||||
|
if n.children == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
next := n.children[labels[i]]
|
||||||
|
if next == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
n = next
|
||||||
|
|
||||||
|
consumed := len(labels) - i
|
||||||
|
remaining := len(labels) - consumed
|
||||||
|
if remaining == 1 && n.wildcard != nil {
|
||||||
|
best = n.wildcard
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return best
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeServerName(s string) string {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
s = strings.TrimSuffix(s, ".")
|
||||||
|
return strings.ToLower(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *sniMatcher) addProvider(p *Provider) {
|
||||||
|
if p == nil || p.tlsCert == nil || len(p.tlsCert.Certificate) == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
leaf, err := x509.ParseCertificate(p.tlsCert.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addName := func(name string) {
|
||||||
|
name = normalizeServerName(name)
|
||||||
|
if name == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if after, ok := strings.CutPrefix(name, "*."); ok {
|
||||||
|
suffix := after
|
||||||
|
if suffix == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.insertWildcardSuffix(suffix, p)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
m.insertExact(name, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
if leaf.Subject.CommonName != "" {
|
||||||
|
addName(leaf.Subject.CommonName)
|
||||||
|
}
|
||||||
|
for _, n := range leaf.DNSNames {
|
||||||
|
addName(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *sniMatcher) insertExact(name string, p *Provider) {
|
||||||
|
if name == "" || p == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if m.exact == nil {
|
||||||
|
m.exact = make(map[string]*Provider)
|
||||||
|
}
|
||||||
|
if _, exists := m.exact[name]; !exists {
|
||||||
|
m.exact[name] = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *sniMatcher) insertWildcardSuffix(suffix string, p *Provider) {
|
||||||
|
if suffix == "" || p == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
n := &m.root
|
||||||
|
labels := strings.Split(suffix, ".")
|
||||||
|
for i := len(labels) - 1; i >= 0; i-- {
|
||||||
|
if n.children == nil {
|
||||||
|
n.children = make(map[string]*sniTreeNode)
|
||||||
|
}
|
||||||
|
next := n.children[labels[i]]
|
||||||
|
if next == nil {
|
||||||
|
next = &sniTreeNode{}
|
||||||
|
n.children[labels[i]] = next
|
||||||
|
}
|
||||||
|
n = next
|
||||||
|
}
|
||||||
|
if n.wildcard == nil {
|
||||||
|
n.wildcard = p
|
||||||
|
}
|
||||||
|
}
|
||||||
104
internal/autocert/sni_matcher_bench_test.go
Normal file
104
internal/autocert/sni_matcher_bench_test.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
package autocert
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"math/big"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createTLSCert(dnsNames []string) (*tls.Certificate, error) {
|
||||||
|
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
cn := ""
|
||||||
|
if len(dnsNames) > 0 {
|
||||||
|
cn = dnsNames[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
template := &x509.Certificate{
|
||||||
|
SerialNumber: serial,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: cn,
|
||||||
|
},
|
||||||
|
NotBefore: time.Now().Add(-time.Minute),
|
||||||
|
NotAfter: time.Now().Add(24 * time.Hour),
|
||||||
|
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
DNSNames: dnsNames,
|
||||||
|
}
|
||||||
|
|
||||||
|
der, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tls.Certificate{
|
||||||
|
Certificate: [][]byte{der},
|
||||||
|
PrivateKey: key,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkSNIMatcher(b *testing.B) {
|
||||||
|
matcher := sniMatcher{}
|
||||||
|
|
||||||
|
wildcard1Cert, err := createTLSCert([]string{"*.example.com"})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
wildcard1 := &Provider{tlsCert: wildcard1Cert}
|
||||||
|
|
||||||
|
wildcard2Cert, err := createTLSCert([]string{"*.test.com"})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
wildcard2 := &Provider{tlsCert: wildcard2Cert}
|
||||||
|
|
||||||
|
wildcard3Cert, err := createTLSCert([]string{"*.foo.com"})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
wildcard3 := &Provider{tlsCert: wildcard3Cert}
|
||||||
|
|
||||||
|
exact1Cert, err := createTLSCert([]string{"bar.example.com"})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
exact1 := &Provider{tlsCert: exact1Cert}
|
||||||
|
|
||||||
|
exact2Cert, err := createTLSCert([]string{"baz.test.com"})
|
||||||
|
if err != nil {
|
||||||
|
b.Fatal(err)
|
||||||
|
}
|
||||||
|
exact2 := &Provider{tlsCert: exact2Cert}
|
||||||
|
|
||||||
|
matcher.addProvider(wildcard1)
|
||||||
|
matcher.addProvider(wildcard2)
|
||||||
|
matcher.addProvider(wildcard3)
|
||||||
|
matcher.addProvider(exact1)
|
||||||
|
matcher.addProvider(exact2)
|
||||||
|
|
||||||
|
b.Run("MatchWildcard", func(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = matcher.match("sub.example.com")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
b.Run("MatchExact", func(b *testing.B) {
|
||||||
|
for b.Loop() {
|
||||||
|
_ = matcher.match("bar.example.com")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user