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 }