Files

100 lines
3.0 KiB
Go

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[:])
}