proxy-pool/internal/controlplane/tlsreload/trust.go
2026-08-07 18:26:09 +08:00

91 lines
2.3 KiB
Go

package tlsreload
import (
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"os"
"sync"
)
var ErrInvalidTrustPath = errors.New("invalid TLS trust bundle path")
// TrustProvider reloads a PEM trust bundle for each new control-plane
// handshake. It keeps the previous valid bundle while a projected volume is
// momentarily incomplete during an update.
type TrustProvider struct {
path string
mu sync.RWMutex
last *x509.CertPool
}
func NewTrustProvider(path string) (*TrustProvider, error) {
if path == "" {
return nil, ErrInvalidTrustPath
}
pool, err := loadTrustPool(path)
if err != nil {
return nil, err
}
return &TrustProvider{path: path, last: pool}, nil
}
// Pool returns the latest valid roots. Returned pools are immutable after
// construction, so callers can safely use them for an in-flight handshake.
func (provider *TrustProvider) Pool() (*x509.CertPool, error) {
if provider == nil {
return nil, ErrInvalidTrustPath
}
pool, err := loadTrustPool(provider.path)
if err == nil {
provider.mu.Lock()
provider.last = pool
provider.mu.Unlock()
return pool, nil
}
provider.mu.RLock()
fallback := provider.last
provider.mu.RUnlock()
if fallback == nil {
return nil, err
}
return fallback, nil
}
// VerifyServer returns a TLS verifier that uses the latest trust bundle while
// retaining normal DNS-name and server-authentication verification.
func (provider *TrustProvider) VerifyServer(serverName string) func(tls.ConnectionState) error {
return func(state tls.ConnectionState) error {
if len(state.PeerCertificates) == 0 {
return errors.New("control-plane server did not provide a certificate")
}
roots, err := provider.Pool()
if err != nil {
return fmt.Errorf("load control-plane trust bundle: %w", err)
}
intermediates := x509.NewCertPool()
for _, certificate := range state.PeerCertificates[1:] {
intermediates.AddCert(certificate)
}
_, err = state.PeerCertificates[0].Verify(x509.VerifyOptions{
DNSName: serverName, Roots: roots, Intermediates: intermediates,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
})
return err
}
}
func loadTrustPool(path string) (*x509.CertPool, error) {
payload, err := os.ReadFile(path)
if err != nil {
return nil, err
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(payload) {
return nil, errors.New("parse TLS trust bundle")
}
return pool, nil
}