mirror of
https://github.com/yusing/godoxy.git
synced 2026-01-11 22:30:47 +01:00
Extra providers were not being properly initialized during NewProvider(), causing certificate registration and renewal scheduling to be skipped. - Add ConfigExtra type with idx field for provider indexing - Add MergeExtraConfig() for inheriting main provider settings - Add setupExtraProviders() for recursive extra provider initialization - Refactor NewProvider to return error and call setupExtraProviders() - Add provider-scoped logger with "main" or "extra[N]" name - Add batch operations: ObtainCertIfNotExistsAll(), ObtainCertAll() - Add ForceExpiryAll() with completion tracking via WaitRenewalDone() - Add RenewMode (force/ifNeeded) for controlling renewal behavior - Add PrintCertExpiriesAll() for logging all provider certificate expiries Summary of staged changes: - config.go: Added ConfigExtra type, MergeExtraConfig(), recursive validation with path uniqueness checking - provider.go: Added provider indexing, scoped logger, batch cert operations, force renewal with completion tracking, RenewMode control - setup.go: New file with setupExtraProviders() for proper extra provider initialization - setup_test.go: New tests for extra provider setup - multi_cert_test.go: New tests for multi-certificate functionality - renew.go: Updated to use new provider API with error handling - state.go: Updated to handle NewProvider error return
417 lines
12 KiB
Go
417 lines
12 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.ConfigExtra{
|
|
{CertPath: extraCert, KeyPath: extraKey},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, cfg.Validate())
|
|
|
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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.ConfigExtra{
|
|
{CertPath: extraCert, KeyPath: extraKey},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, cfg.Validate())
|
|
|
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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.ConfigExtra{
|
|
{CertPath: extraCert, KeyPath: extraKey},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, cfg.Validate())
|
|
|
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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.ConfigExtra{
|
|
{CertPath: extraCert, KeyPath: extraKey},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, cfg.Validate())
|
|
|
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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.ConfigExtra{
|
|
{CertPath: extraCert, KeyPath: extraKey},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, cfg.Validate())
|
|
|
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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.ConfigExtra{
|
|
{CertPath: extraCert1, KeyPath: extraKey1},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, cfg.Validate())
|
|
|
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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.ConfigExtra{
|
|
{CertPath: extraCert1, KeyPath: extraKey1},
|
|
{CertPath: extraCert2, KeyPath: extraKey2},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, cfg.Validate())
|
|
|
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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.ConfigExtra{
|
|
{CertPath: extraCert, KeyPath: extraKey},
|
|
},
|
|
}
|
|
|
|
require.NoError(t, cfg.Validate())
|
|
|
|
p, err := autocert.NewProvider(cfg, nil, nil)
|
|
require.NoError(t, err)
|
|
|
|
err = p.LoadCert()
|
|
require.NoError(t, err)
|
|
|
|
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")
|
|
})
|
|
}
|