100 lines
3.0 KiB
Go
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[:])
|
|
}
|