mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +01:00
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
384 lines
11 KiB
Go
384 lines
11 KiB
Go
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")
|
|
})
|
|
}
|