//go:build linux || freebsd // +build linux freebsd package transport import ( "crypto/sha256" "crypto/tls" "crypto/x509" "encoding/hex" "fmt" "os" "strings" ) // TLSConfig configures TLS type TLSConfig struct { // Minimum TLS version MinVersion uint16 // Maximum TLS version MaxVersion uint16 // Path to CA certificates CACertFile string // Skip certificate verification InsecureSkipVerify bool // Client certificate for mTLS CertFile string // Client private key for mTLS KeyFile string // Certificate pinning (SHA-256 hash of certificate) PinnedCertHash string // TLSServerNameOverride forces the SNI server name and the hostname used // for certificate verification. Leave empty (the default) so that Go's // stdlib derives the server name from the dial target, which is the safe // behavior. Setting this to a value that does not match the host you are // connecting to enables a man-in-the-middle attacker with a valid cert // for the overridden name to impersonate the target. Only set this when // you genuinely need to override SNI (e.g. connecting to an IP address // whose certificate carries a different hostname). TLSServerNameOverride string // Path to SSLKEYLOGFILE for Wireshark debugging KeyLogFile string } // DefaultTLSConfig returns a safe default configuration func DefaultTLSConfig() *TLSConfig { return &TLSConfig{ MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, InsecureSkipVerify: false, } } // ToTLSConfig converts to tls.Config func (c *TLSConfig) ToTLSConfig() (*tls.Config, error) { tlsCfg := &tls.Config{ MinVersion: c.MinVersion, MaxVersion: c.MaxVersion, InsecureSkipVerify: c.InsecureSkipVerify, } // Only apply the override if explicitly set. When empty, Go's net/http // transport derives ServerName from the dial address, which matches the // certificate the server is expected to present. if c.TLSServerNameOverride != "" { tlsCfg.ServerName = c.TLSServerNameOverride } // Load custom CA certificates if c.CACertFile != "" { caCert, err := os.ReadFile(c.CACertFile) if err != nil { return nil, fmt.Errorf("failed to read ca cert: %w", err) } caCertPool := x509.NewCertPool() if !caCertPool.AppendCertsFromPEM(caCert) { return nil, fmt.Errorf("failed to parse ca cert") } tlsCfg.RootCAs = caCertPool } // Certificate pinning: verify SHA-256 hash of server certificate if c.PinnedCertHash != "" { expectedHash := strings.ToLower(strings.TrimSpace(c.PinnedCertHash)) tlsCfg.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error { for _, rawCert := range rawCerts { cert, err := x509.ParseCertificate(rawCert) if err != nil { continue } hash := sha256.Sum256(cert.Raw) actual := hex.EncodeToString(hash[:]) if actual == expectedHash { return nil } } return fmt.Errorf("certificate pinning failed: no certificate matches sha-256 %s", expectedHash) } } // Load client certificate for mTLS if c.CertFile != "" && c.KeyFile != "" { cert, err := tls.LoadX509KeyPair(c.CertFile, c.KeyFile) if err != nil { return nil, fmt.Errorf("failed to load client certificate: %w", err) } tlsCfg.Certificates = []tls.Certificate{cert} } // SSLKEYLOGFILE for Wireshark/TLS debugging if c.KeyLogFile != "" { f, err := os.OpenFile(c.KeyLogFile, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600) if err != nil { return nil, fmt.Errorf("failed to open keylog file: %w", err) } tlsCfg.KeyLogWriter = f } return tlsCfg, nil } // GetTLSVersionName returns a human-readable TLS version name func GetTLSVersionName(version uint16) string { switch version { case tls.VersionTLS10: return "TLS 1.0" case tls.VersionTLS11: return "TLS 1.1" case tls.VersionTLS12: return "TLS 1.2" case tls.VersionTLS13: return "TLS 1.3" default: return fmt.Sprintf("TLS 0x%04X", version) } }