147 lines
3.8 KiB
Go
147 lines
3.8 KiB
Go
//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)
|
||
|
|
}
|
||
|
|
}
|