package rpc import ( "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "fmt" "os" ) // NewServerTLSConfig builds the mTLS configuration used by a Dispatcher or // Agent listener. ServerName is intentionally not used to disable verification; // callers still verify the peer certificate against the supplied CA. // LoadServerTLSConfig reads deployment-provided certificate files without // exposing their contents to logs or repository state. func LoadServerTLSConfig(caFile, certFile, keyFile string) (*tls.Config, error) { caPEM, err := os.ReadFile(caFile) if err != nil { return nil, fmt.Errorf("read CA file: %w", err) } certPEM, err := os.ReadFile(certFile) if err != nil { return nil, fmt.Errorf("read certificate file: %w", err) } keyPEM, err := os.ReadFile(keyFile) if err != nil { return nil, fmt.Errorf("read key file: %w", err) } return NewServerTLSConfig(caPEM, certPEM, keyPEM) } func NewServerTLSConfig(caPEM, certPEM, keyPEM []byte) (*tls.Config, error) { pool, err := certPool(caPEM) if err != nil { return nil, err } certificate, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { return nil, fmt.Errorf("load server certificate: %w", err) } return &tls.Config{ MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, ClientCAs: pool, ClientAuth: tls.RequireAndVerifyClientCert, NextProtos: []string{"h2"}, VerifyConnection: func(state tls.ConnectionState) error { if len(state.VerifiedChains) == 0 || len(state.VerifiedChains[0]) == 0 { return fmt.Errorf("verified peer certificate is required") } leaf := state.VerifiedChains[0][0] if len(leaf.DNSNames) == 0 && len(leaf.URIs) == 0 && len(leaf.IPAddresses) == 0 { return fmt.Errorf("peer certificate has no SAN") } return nil }, }, nil } // NewClientTLSConfig builds a peer-verifying mTLS client configuration. func NewClientTLSConfig(caPEM, certPEM, keyPEM []byte, serverName string) (*tls.Config, error) { pool, err := certPool(caPEM) if err != nil { return nil, err } certificate, err := tls.X509KeyPair(certPEM, keyPEM) if err != nil { return nil, fmt.Errorf("load client certificate: %w", err) } if serverName == "" { return nil, fmt.Errorf("server name is required for mTLS peer verification") } return &tls.Config{ MinVersion: tls.VersionTLS13, Certificates: []tls.Certificate{certificate}, RootCAs: pool, ServerName: serverName, NextProtos: []string{"h2"}, }, nil } func certPool(pemBytes []byte) (*x509.CertPool, error) { pool := x509.NewCertPool() if !pool.AppendCertsFromPEM(pemBytes) { return nil, fmt.Errorf("CA bundle contains no certificates") } return pool, nil } // CertificateFingerprint returns a stable identifier for a verified leaf. It // is suitable for an allow-list lookup, not for logging certificate contents. func CertificateFingerprint(cert *x509.Certificate) string { if cert == nil { return "" } digest := sha256.Sum256(cert.Raw) return hex.EncodeToString(digest[:]) }