diff --git a/AUTHORS b/AUTHORS index 05e71df4..3e0ae16c 100644 --- a/AUTHORS +++ b/AUTHORS @@ -43,6 +43,7 @@ Diego Dupin Dirkjan Bussink DisposaBoy Egor Smolyakov +Ehsan Pourtorab Erwan Martin Evan Elias Evan Shaw diff --git a/README.md b/README.md index 1d07bde1..ba352c1b 100644 --- a/README.md +++ b/README.md @@ -434,7 +434,41 @@ Valid Values: true, false, skip-verify, preferred, Default: false ``` -`tls=true` enables TLS / SSL encrypted connection to the server. Use `skip-verify` if you want to use a self-signed or invalid certificate (server side) or use `preferred` to use TLS only when advertised by the server. This is similar to `skip-verify`, but additionally allows a fallback to a connection which is not encrypted. Neither `skip-verify` nor `preferred` add any reliable security. You can use a custom TLS config after registering it with [`mysql.RegisterTLSConfig`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig). +`tls=true` enables TLS / SSL encrypted connection to the server with full certificate verification (including hostname). Use `skip-verify` if you want to use a self-signed or invalid certificate (server-side) or use `preferred` to use TLS only when advertised by the server. This is similar to `skip-verify`, but additionally allows a fallback to a connection which is not encrypted. Neither `skip-verify` nor `preferred` add any reliable security. You can use a custom TLS config after registering it with [`mysql.RegisterTLSConfig`](https://godoc.org/github.com/go-sql-driver/mysql#RegisterTLSConfig). + +**TLS Verification Modes:** + +The `tls` parameter selects which CA certificates to use: +- `tls=true`: Use system CA pool +- `tls=`: Use custom registered TLS config +- `tls=skip-verify`: Accept any certificate (insecure) +- `tls=preferred`: Attempt TLS, fall back to plaintext (insecure) + +The `tls-verify` parameter controls how certificates are verified (works with both `tls=true` and custom configs): +- `tls-verify=identity` (default): Verifies CA and hostname - Most secure, equivalent to MySQL's VERIFY_IDENTITY +- `tls-verify=ca`: Verifies CA only, skips hostname check - Equivalent to MySQL's VERIFY_CA mode + +**Examples:** +```text +?tls=true - System CA with full verification (default behavior) +?tls=true&tls-verify=ca - System CA with CA-only verification +?tls=custom - Custom CA with full verification (default behavior) +?tls=custom&tls-verify=ca - Custom CA with CA-only verification +``` + +##### `tls-verify` + +```text +Type: string +Valid Values: identity, ca +Default: identity +``` + +Controls the TLS certificate verification level. This parameter works with the `tls` parameter: +- `identity`: Full verification including hostname (default, most secure) +- `ca`: CA verification only, without hostname checking (MySQL VERIFY_CA equivalent) + +This parameter only applies when `tls=true` or `tls=`. It has no effect with `tls=skip-verify` or `tls=preferred`. ##### `writeTimeout` diff --git a/driver_test.go b/driver_test.go index ec0f2877..d7631985 100644 --- a/driver_test.go +++ b/driver_test.go @@ -1520,6 +1520,16 @@ func TestTLS(t *testing.T) { InsecureSkipVerify: true, }) runTests(t, dsn+"&tls=custom-skip-verify", tlsTestReq) + + // Test tls-verify parameter with system CA + runTests(t, dsn+"&tls=true&tls-verify=ca", tlsTestReq) + runTests(t, dsn+"&tls=true&tls-verify=identity", tlsTestReq) + + // Test tls-verify parameter with custom TLS config + RegisterTLSConfig("custom-ca-verify", &tls.Config{ + InsecureSkipVerify: true, + }) + runTests(t, dsn+"&tls=custom-ca-verify&tls-verify=ca", tlsTestReq) } func TestReuseClosedConnection(t *testing.T) { diff --git a/dsn.go b/dsn.go index 89556bfb..4067e871 100644 --- a/dsn.go +++ b/dsn.go @@ -49,6 +49,7 @@ type Config struct { MaxAllowedPacket int // Max packet size allowed ServerPubKey string // Server public key name TLSConfig string // TLS configuration name + TLSVerify string // TLS verification level: "identity" (default) or "ca" TLS *tls.Config // TLS configuration, its priority is higher than TLSConfig Timeout time.Duration // Dial timeout ReadTimeout time.Duration // I/O read timeout @@ -195,21 +196,39 @@ func (cfg *Config) normalize() error { } if cfg.TLS == nil { + // Default TLSVerify to identity if not specified + if cfg.TLSVerify == "" { + cfg.TLSVerify = "identity" + } + switch cfg.TLSConfig { case "false", "": // don't set anything case "true": - cfg.TLS = &tls.Config{} + // System CA pool + if cfg.TLSVerify == "ca" { + cfg.TLS = createVerifyCAConfig(nil, nil) + } else { + cfg.TLS = &tls.Config{} + } case "skip-verify": cfg.TLS = &tls.Config{InsecureSkipVerify: true} case "preferred": cfg.TLS = &tls.Config{InsecureSkipVerify: true} cfg.AllowFallbackToPlaintext = true default: + // Custom registered TLS config cfg.TLS = getTLSConfigClone(cfg.TLSConfig) if cfg.TLS == nil { return errors.New("invalid value / unknown config name: " + cfg.TLSConfig) } + + // Apply tls-verify to custom config + if cfg.TLSVerify == "ca" { + // Preserve all settings from custom config, only modify verification behavior + rootCAs := cfg.TLS.RootCAs + cfg.TLS = createVerifyCAConfig(cfg.TLS, rootCAs) + } } } @@ -370,6 +389,10 @@ func (cfg *Config) FormatDSN() string { writeDSNParam(&buf, &hasParam, "tls", url.QueryEscape(cfg.TLSConfig)) } + if len(cfg.TLSVerify) > 0 && cfg.TLSVerify != "identity" { + writeDSNParam(&buf, &hasParam, "tls-verify", cfg.TLSVerify) + } + if cfg.WriteTimeout > 0 { writeDSNParam(&buf, &hasParam, "writeTimeout", cfg.WriteTimeout.String()) } @@ -658,6 +681,14 @@ func parseDSNParams(cfg *Config, params string) (err error) { cfg.TLSConfig = name } + // TLS verification level + case "tls-verify": + mode := strings.ToLower(value) + if mode != "identity" && mode != "ca" { + return fmt.Errorf("invalid tls-verify value: %s (must be 'identity' or 'ca')", value) + } + cfg.TLSVerify = mode + // I/O write Timeout case "writeTimeout": cfg.WriteTimeout, err = time.ParseDuration(value) diff --git a/dsn_test.go b/dsn_test.go index 436f7799..2f24b821 100644 --- a/dsn_test.go +++ b/dsn_test.go @@ -10,9 +10,11 @@ package mysql import ( "crypto/tls" + "crypto/x509" "fmt" "net/url" "reflect" + "strings" "testing" "time" ) @@ -80,6 +82,12 @@ var testDSNs = []struct { }, { "foo:bar@tcp(192.168.1.50:3307)/baz?timeout=10s&connectionAttributes=program_name:MySQLGoDriver%2FTest,program_version:1.2.3", &Config{User: "foo", Passwd: "bar", Net: "tcp", Addr: "192.168.1.50:3307", DBName: "baz", Loc: time.UTC, Timeout: 10 * time.Second, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, ConnectionAttributes: "program_name:MySQLGoDriver/Test,program_version:1.2.3"}, +}, { + "user:password@tcp(localhost:5555)/dbname?tls=true&tls-verify=ca", + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "true", TLSVerify: "ca"}, +}, { + "user:password@tcp(localhost:5555)/dbname?tls=true&tls-verify=identity", + &Config{User: "user", Passwd: "password", Net: "tcp", Addr: "localhost:5555", DBName: "dbname", Loc: time.UTC, MaxAllowedPacket: defaultMaxAllowedPacket, Logger: defaultLogger, AllowNativePasswords: true, CheckConnLiveness: true, TLSConfig: "true", TLSVerify: "identity"}, }, } @@ -429,6 +437,229 @@ func TestNormalizeTLSConfig(t *testing.T) { } } +func TestTLSVerifySystemCA(t *testing.T) { + tests := []struct { + name string + dsn string + }{ + {"ca with system CA", "tcp(example.com:1234)/?tls=true&tls-verify=ca"}, + {"identity with system CA (explicit)", "tcp(example.com:1234)/?tls=true&tls-verify=identity"}, + {"identity with system CA (default)", "tcp(example.com:1234)/?tls=true"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg, err := ParseDSN(tc.dsn) + if err != nil { + t.Error(err.Error()) + } + if cfg.TLS == nil { + t.Error("cfg.TLS should not be nil") + } + + if cfg.TLSVerify == "ca" { + if !cfg.TLS.InsecureSkipVerify { + t.Error("ca mode should have InsecureSkipVerify=true") + } + if cfg.TLS.VerifyPeerCertificate == nil { + t.Error("ca mode should have VerifyPeerCertificate callback set") + } + // ca mode does not auto-set ServerName (hostname verification is skipped) + // ServerName remains empty unless explicitly set + if cfg.TLS.ServerName != "" { + t.Errorf("ca mode with system CA should not have ServerName set, got %q", cfg.TLS.ServerName) + } + } else { + // identity (default) should set ServerName + if cfg.TLS.ServerName != "example.com" { + t.Errorf("identity mode should set ServerName to 'example.com', got %q", cfg.TLS.ServerName) + } + if cfg.TLS.VerifyPeerCertificate != nil { + t.Error("identity mode should not have VerifyPeerCertificate callback set") + } + } + }) + } +} + +func TestTLSVerifyCustomConfig(t *testing.T) { + // Register a custom TLS config + customConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + ServerName: "customServer", + RootCAs: nil, // Use system CA pool for this test + } + RegisterTLSConfig("custom", customConfig) + defer DeregisterTLSConfig("custom") + + tests := []struct { + name string + dsn string + }{ + {"ca with custom config", "tcp(example.com:1234)/?tls=custom&tls-verify=ca"}, + {"identity with custom config (explicit)", "tcp(example.com:1234)/?tls=custom&tls-verify=identity"}, + {"identity with custom config (default)", "tcp(example.com:1234)/?tls=custom"}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg, err := ParseDSN(tc.dsn) + if err != nil { + t.Error(err.Error()) + } + if cfg.TLS == nil { + t.Error("cfg.TLS should not be nil") + } + + if cfg.TLSVerify == "ca" { + if !cfg.TLS.InsecureSkipVerify { + t.Error("ca mode should have InsecureSkipVerify=true") + } + if cfg.TLS.VerifyPeerCertificate == nil { + t.Error("ca mode should have VerifyPeerCertificate callback set") + } + // ca mode should preserve custom config's ServerName for SNI + if cfg.TLS.ServerName != "customServer" { + t.Errorf("ca mode should preserve custom ServerName 'customServer', got %q", cfg.TLS.ServerName) + } + } else { + // identity (default) should preserve custom config's ServerName + if cfg.TLS.ServerName != "customServer" { + t.Errorf("identity mode should preserve custom ServerName 'customServer', got %q", cfg.TLS.ServerName) + } + if cfg.TLS.VerifyPeerCertificate != nil { + t.Error("identity mode should not have VerifyPeerCertificate callback set") + } + } + }) + } +} + +func TestTLSVerifyBackwardsCompatibility(t *testing.T) { + tests := []struct { + name string + dsn string + expectTLSVerify string + expectServerName string + }{ + {"tls=true defaults to identity", "tcp(example.com:1234)/?tls=true", "identity", "example.com"}, + {"tls=false no TLS", "tcp(example.com:1234)/?tls=false", "identity", ""}, + {"tls=skip-verify unchanged", "tcp(example.com:1234)/?tls=skip-verify", "identity", ""}, + {"tls=preferred unchanged", "tcp(example.com:1234)/?tls=preferred", "identity", ""}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + cfg, err := ParseDSN(tc.dsn) + if err != nil { + t.Error(err.Error()) + } + + if cfg.TLSVerify != tc.expectTLSVerify { + t.Errorf("expected TLSVerify=%q, got %q", tc.expectTLSVerify, cfg.TLSVerify) + } + + if tc.expectServerName == "" { + if cfg.TLS == nil { + return // Expected no TLS + } + if cfg.TLS.ServerName != "" { + t.Errorf("expected no ServerName, got %q", cfg.TLS.ServerName) + } + } else { + if cfg.TLS == nil { + t.Error("expected TLS config but got nil") + return + } + if cfg.TLS.ServerName != tc.expectServerName { + t.Errorf("expected ServerName=%q, got %q", tc.expectServerName, cfg.TLS.ServerName) + } + } + }) + } +} + +func TestTLSVerifyInvalidValue(t *testing.T) { + dsn := "tcp(example.com:1234)/?tls=true&tls-verify=invalid" + _, err := ParseDSN(dsn) + if err == nil { + t.Error("expected error for invalid tls-verify value") + } + expectedMsg := "invalid value for tls-verify" + if err != nil && !strings.Contains(err.Error(), expectedMsg) { + t.Errorf("error message should contain %q, got: %v", expectedMsg, err) + } +} + +func TestTLSVerifyPreservesCustomConfig(t *testing.T) { + // Register a custom TLS config with various settings + customConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + ServerName: "customServer", + CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, + NextProtos: []string{"h2", "http/1.1"}, + RootCAs: x509.NewCertPool(), + } + RegisterTLSConfig("custom-full", customConfig) + defer DeregisterTLSConfig("custom-full") + + dsn := "tcp(example.com:1234)/?tls=custom-full&tls-verify=ca" + cfg, err := ParseDSN(dsn) + if err != nil { + t.Fatal(err) + } + + if cfg.TLS == nil { + t.Fatal("cfg.TLS should not be nil") + } + + // Verify VERIFY_CA mode is enabled + if !cfg.TLS.InsecureSkipVerify { + t.Error("ca mode should have InsecureSkipVerify=true") + } + if cfg.TLS.VerifyPeerCertificate == nil { + t.Error("ca mode should have VerifyPeerCertificate callback set") + } + + // Verify all custom settings are preserved + if cfg.TLS.MinVersion != tls.VersionTLS12 { + t.Errorf("MinVersion not preserved: got %v, want %v", cfg.TLS.MinVersion, tls.VersionTLS12) + } + if cfg.TLS.MaxVersion != tls.VersionTLS13 { + t.Errorf("MaxVersion not preserved: got %v, want %v", cfg.TLS.MaxVersion, tls.VersionTLS13) + } + if cfg.TLS.ServerName != "customServer" { + t.Errorf("ServerName not preserved: got %q, want 'customServer'", cfg.TLS.ServerName) + } + if len(cfg.TLS.CipherSuites) != 1 || cfg.TLS.CipherSuites[0] != tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 { + t.Error("CipherSuites not preserved") + } + if len(cfg.TLS.NextProtos) != 2 || cfg.TLS.NextProtos[0] != "h2" || cfg.TLS.NextProtos[1] != "http/1.1" { + t.Error("NextProtos not preserved") + } + if cfg.TLS.RootCAs == nil { + t.Error("RootCAs not preserved") + } +} + +func TestRegisterTLSConfigReservedKey(t *testing.T) { + reservedKeys := []string{ + "true", "True", "TRUE", + "false", "False", "FALSE", + "skip-verify", "Skip-Verify", "SKIP-VERIFY", + "preferred", "Preferred", "PREFERRED", + } + + for _, key := range reservedKeys { + err := RegisterTLSConfig(key, &tls.Config{}) + if err == nil { + t.Errorf("RegisterTLSConfig should reject reserved key %q", key) + } + DeregisterTLSConfig(key) // Clean up in case it was registered + } +} + func BenchmarkParseDSN(b *testing.B) { b.ReportAllocs() diff --git a/utils.go b/utils.go index b041804d..882a47ff 100644 --- a/utils.go +++ b/utils.go @@ -10,6 +10,7 @@ package mysql import ( "crypto/tls" + "crypto/x509" "database/sql" "database/sql/driver" "encoding/binary" @@ -87,6 +88,78 @@ func getTLSConfigClone(key string) (config *tls.Config) { return } +// createVerifyCAConfig creates or modifies a TLS config to verify the CA certificate +// but not the hostname. This implements MySQL's VERIFY_CA mode. +// It uses the recommended Go pattern from issues #21971, #31791, #31792, #35467: +// 1. Set InsecureSkipVerify to disable default verification +// 2. Use VerifyPeerCertificate callback to manually verify CA without hostname +// +// If baseConfig is nil, creates a new minimal config. +// If baseConfig is provided, clones it and preserves all settings except verification. +// The rootCAs parameter specifies which CA pool to use for verification. +func createVerifyCAConfig(baseConfig *tls.Config, rootCAs *x509.CertPool) *tls.Config { + var cfg *tls.Config + if baseConfig != nil { + cfg = baseConfig.Clone() + } else { + cfg = &tls.Config{} + } + + // Override only the verification-related fields for VERIFY_CA mode + cfg.InsecureSkipVerify = true + cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + return verifyCACallback(rawCerts, verifiedChains, rootCAs) + } + + return cfg +} + +// verifyCACallback implements CA-only verification without hostname checking. +// This verifies that the certificate chain is signed by a trusted CA but does +// not validate the hostname matches the certificate. This is the standard +// implementation pattern recommended by the Go team for VERIFY_CA behavior. +// +// If rootCAs is nil, the system's default CA pool will be used. +// If rootCAs is provided, it will be used for verification instead. +func verifyCACallback(rawCerts [][]byte, _ [][]*x509.Certificate, rootCAs *x509.CertPool) error { + if len(rawCerts) == 0 { + return errors.New("tls: no certificates from server") + } + + // Parse all certificates in the chain + certs := make([]*x509.Certificate, len(rawCerts)) + for i, rawCert := range rawCerts { + cert, err := x509.ParseCertificate(rawCert) + if err != nil { + return fmt.Errorf("tls: failed to parse certificate: %w", err) + } + certs[i] = cert + } + + // Build intermediates pool from all certificates except the first (leaf) + intermediates := x509.NewCertPool() + for _, cert := range certs[1:] { + intermediates.AddCert(cert) + } + + // Verify the certificate chain without hostname verification + // By not setting DNSName in VerifyOptions, we skip hostname validation + // This implements VERIFY_CA behavior (CA check only, no hostname check) + opts := x509.VerifyOptions{ + Roots: rootCAs, // nil = use system CA pool + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + // DNSName is intentionally not set to skip hostname verification + } + + _, err := certs[0].Verify(opts) + if err != nil { + return fmt.Errorf("tls: failed to verify certificate: %w", err) + } + + return nil +} + // Returns the bool value of the input. // The 2nd return value indicates if the input was a valid bool value func readBool(input string) (value bool, valid bool) { diff --git a/utils_test.go b/utils_test.go index 42a88393..1f4d7f31 100644 --- a/utils_test.go +++ b/utils_test.go @@ -10,9 +10,15 @@ package mysql import ( "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "database/sql" "database/sql/driver" "encoding/binary" + "math/big" "testing" "time" ) @@ -518,3 +524,186 @@ func TestParseDateTimeFail(t *testing.T) { }) } } + +func TestVerifyCACallback(t *testing.T) { + t.Run("no certificates", func(t *testing.T) { + err := verifyCACallback(nil, nil, nil) + if err == nil { + t.Error("expected error when no certificates provided") + } + if err.Error() != "tls: no certificates from server" { + t.Errorf("unexpected error message: %v", err) + } + }) + + t.Run("empty certificate list", func(t *testing.T) { + err := verifyCACallback([][]byte{}, nil, nil) + if err == nil { + t.Error("expected error when certificate list is empty") + } + }) + + t.Run("invalid certificate data", func(t *testing.T) { + invalidCert := []byte{0x00, 0x01, 0x02} + err := verifyCACallback([][]byte{invalidCert}, nil, nil) + if err == nil { + t.Error("expected error when certificate cannot be parsed") + } + }) + + t.Run("valid self-signed certificate", func(t *testing.T) { + // Create a minimal self-signed CA certificate for testing + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("failed to generate CA key: %v", err) + } + + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + Organization: []string{"Test CA"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + BasicConstraintsValid: true, + IsCA: true, + } + + caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + t.Fatalf("failed to create CA certificate: %v", err) + } + + caCert, err := x509.ParseCertificate(caCertDER) + if err != nil { + t.Fatalf("failed to parse CA certificate: %v", err) + } + + // Create a CA pool with our test CA + caPool := x509.NewCertPool() + caPool.AddCert(caCert) + + // Create a leaf certificate signed by the CA + leafKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("failed to generate leaf key: %v", err) + } + + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{ + Organization: []string{"Test Server"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().Add(24 * time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + + leafCertDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, caCert, &leafKey.PublicKey, caKey) + if err != nil { + t.Fatalf("failed to create leaf certificate: %v", err) + } + + // Test verification with the valid chain + err = verifyCACallback([][]byte{leafCertDER, caCertDER}, nil, caPool) + if err != nil { + t.Errorf("expected successful verification but got error: %v", err) + } + }) +} + +func TestCreateVerifyCAConfig(t *testing.T) { + t.Run("with system CA pool", func(t *testing.T) { + cfg := createVerifyCAConfig(nil, nil) + + if cfg == nil { + t.Fatal("createVerifyCAConfig returned nil") + } + + if !cfg.InsecureSkipVerify { + t.Error("CA-only verification config should have InsecureSkipVerify=true") + } + + if cfg.VerifyPeerCertificate == nil { + t.Error("CA-only verification config should have VerifyPeerCertificate callback set") + } + + // Verify it's the correct callback + err := cfg.VerifyPeerCertificate(nil, nil) + if err == nil { + t.Error("VerifyPeerCertificate callback should return error for nil certificates") + } + }) + + t.Run("with custom CA pool", func(t *testing.T) { + customPool := x509.NewCertPool() + cfg := createVerifyCAConfig(nil, customPool) + + if cfg == nil { + t.Fatal("createVerifyCAConfig returned nil") + } + + if !cfg.InsecureSkipVerify { + t.Error("CA-only verification config should have InsecureSkipVerify=true") + } + + if cfg.VerifyPeerCertificate == nil { + t.Error("CA-only verification config should have VerifyPeerCertificate callback set") + } + }) + + t.Run("preserves base config settings", func(t *testing.T) { + baseConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + MaxVersion: tls.VersionTLS13, + CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, + ServerName: "example.com", + NextProtos: []string{"h2", "http/1.1"}, + Certificates: []tls.Certificate{{}}, + } + + customPool := x509.NewCertPool() + cfg := createVerifyCAConfig(baseConfig, customPool) + + if cfg == nil { + t.Fatal("createVerifyCAConfig returned nil") + } + + // Verify verification fields are set correctly + if !cfg.InsecureSkipVerify { + t.Error("CA-only verification config should have InsecureSkipVerify=true") + } + + if cfg.VerifyPeerCertificate == nil { + t.Error("CA-only verification config should have VerifyPeerCertificate callback set") + } + + // Verify base config settings are preserved + if cfg.MinVersion != tls.VersionTLS12 { + t.Errorf("MinVersion not preserved: got %v, want %v", cfg.MinVersion, tls.VersionTLS12) + } + + if cfg.MaxVersion != tls.VersionTLS13 { + t.Errorf("MaxVersion not preserved: got %v, want %v", cfg.MaxVersion, tls.VersionTLS13) + } + + if len(cfg.CipherSuites) != 1 || cfg.CipherSuites[0] != tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 { + t.Error("CipherSuites not preserved") + } + + if cfg.ServerName != "example.com" { + t.Errorf("ServerName not preserved: got %v, want example.com", cfg.ServerName) + } + + if len(cfg.NextProtos) != 2 || cfg.NextProtos[0] != "h2" || cfg.NextProtos[1] != "http/1.1" { + t.Error("NextProtos not preserved") + } + + if len(cfg.Certificates) != 1 { + t.Error("Certificates not preserved") + } + }) +}