Skip to content

Commit c93c813

Browse files
Resolve coderabbit comments
1 parent ef39c76 commit c93c813

File tree

6 files changed

+256
-28
lines changed

6 files changed

+256
-28
lines changed

README.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ Valid Values: true, false, skip-verify, preferred, <name>
434434
Default: false
435435
```
436436

437-
`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).
437+
`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).
438438

439439
**TLS Verification Modes:**
440440

@@ -449,10 +449,12 @@ The `tls-verify` parameter controls how certificates are verified (works with bo
449449
- `tls-verify=ca`: Verifies CA only, skips hostname check - Equivalent to MySQL's VERIFY_CA mode
450450

451451
**Examples:**
452-
- `?tls=true` - System CA with full verification (default behavior)
453-
- `?tls=true&tls-verify=ca` - System CA with CA-only verification
454-
- `?tls=custom` - Custom CA with full verification (default behavior)
455-
- `?tls=custom&tls-verify=ca` - Custom CA with CA-only verification
452+
```text
453+
?tls=true - System CA with full verification (default behavior)
454+
?tls=true&tls-verify=ca - System CA with CA-only verification
455+
?tls=custom - Custom CA with full verification (default behavior)
456+
?tls=custom&tls-verify=ca - Custom CA with CA-only verification
457+
```
456458

457459
##### `tls-verify`
458460

@@ -462,7 +464,7 @@ Valid Values: identity, ca
462464
Default: identity
463465
```
464466

465-
Controls the TLS certificate verification level. This parameter works in conjunction with the `tls` parameter:
467+
Controls the TLS certificate verification level. This parameter works with the `tls` parameter:
466468
- `identity`: Full verification including hostname (default, most secure)
467469
- `ca`: CA verification only, without hostname checking (MySQL VERIFY_CA equivalent)
468470

driver_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1520,6 +1520,16 @@ func TestTLS(t *testing.T) {
15201520
InsecureSkipVerify: true,
15211521
})
15221522
runTests(t, dsn+"&tls=custom-skip-verify", tlsTestReq)
1523+
1524+
// Test tls-verify parameter with system CA
1525+
runTests(t, dsn+"&tls=true&tls-verify=ca", tlsTestReq)
1526+
runTests(t, dsn+"&tls=true&tls-verify=identity", tlsTestReq)
1527+
1528+
// Test tls-verify parameter with custom TLS config
1529+
RegisterTLSConfig("custom-ca-verify", &tls.Config{
1530+
InsecureSkipVerify: true,
1531+
})
1532+
runTests(t, dsn+"&tls=custom-ca-verify&tls-verify=ca", tlsTestReq)
15231533
}
15241534

15251535
func TestReuseClosedConnection(t *testing.T) {

dsn.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,7 @@ func (cfg *Config) normalize() error {
207207
case "true":
208208
// System CA pool
209209
if cfg.TLSVerify == "ca" {
210-
cfg.TLS = createVerifyCAConfig(nil)
210+
cfg.TLS = createVerifyCAConfig(nil, nil)
211211
} else {
212212
cfg.TLS = &tls.Config{}
213213
}
@@ -225,9 +225,9 @@ func (cfg *Config) normalize() error {
225225

226226
// Apply tls-verify to custom config
227227
if cfg.TLSVerify == "ca" {
228-
// Extract RootCAs from the custom config and create a CA-only verification config
228+
// Preserve all settings from custom config, only modify verification behavior
229229
rootCAs := cfg.TLS.RootCAs
230-
cfg.TLS = createVerifyCAConfig(rootCAs)
230+
cfg.TLS = createVerifyCAConfig(cfg.TLS, rootCAs)
231231
}
232232
}
233233
}

dsn_test.go

Lines changed: 92 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package mysql
1010

1111
import (
1212
"crypto/tls"
13+
"crypto/x509"
1314
"fmt"
1415
"net/url"
1516
"reflect"
@@ -80,6 +81,12 @@ var testDSNs = []struct {
8081
}, {
8182
"foo:bar@tcp(192.168.1.50:3307)/baz?timeout=10s&connectionAttributes=program_name:MySQLGoDriver%2FTest,program_version:1.2.3",
8283
&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"},
84+
}, {
85+
"user:password@tcp(localhost:5555)/dbname?tls=true&tls-verify=ca",
86+
&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"},
87+
}, {
88+
"user:password@tcp(localhost:5555)/dbname?tls=true&tls-verify=identity",
89+
&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"},
8390
},
8491
}
8592

@@ -456,15 +463,19 @@ func TestTLSVerifySystemCA(t *testing.T) {
456463
if cfg.TLS.VerifyPeerCertificate == nil {
457464
t.Error("ca mode should have VerifyPeerCertificate callback set")
458465
}
459-
// ca should not set ServerName (hostname verification is skipped)
466+
// ca mode does not auto-set ServerName (hostname verification is skipped)
467+
// ServerName remains empty unless explicitly set
460468
if cfg.TLS.ServerName != "" {
461-
t.Errorf("ca mode should not set ServerName, got %q", cfg.TLS.ServerName)
469+
t.Errorf("ca mode with system CA should not have ServerName set, got %q", cfg.TLS.ServerName)
462470
}
463471
} else {
464472
// identity (default) should set ServerName
465473
if cfg.TLS.ServerName != "example.com" {
466474
t.Errorf("identity mode should set ServerName to 'example.com', got %q", cfg.TLS.ServerName)
467475
}
476+
if cfg.TLS.VerifyPeerCertificate != nil {
477+
t.Error("identity mode should not have VerifyPeerCertificate callback set")
478+
}
468479
}
469480
})
470481
}
@@ -473,6 +484,7 @@ func TestTLSVerifySystemCA(t *testing.T) {
473484
func TestTLSVerifyCustomConfig(t *testing.T) {
474485
// Register a custom TLS config
475486
customConfig := &tls.Config{
487+
MinVersion: tls.VersionTLS12,
476488
ServerName: "customServer",
477489
RootCAs: nil, // Use system CA pool for this test
478490
}
@@ -505,15 +517,18 @@ func TestTLSVerifyCustomConfig(t *testing.T) {
505517
if cfg.TLS.VerifyPeerCertificate == nil {
506518
t.Error("ca mode should have VerifyPeerCertificate callback set")
507519
}
508-
// ca should not set ServerName (hostname verification is skipped)
509-
if cfg.TLS.ServerName != "" {
510-
t.Errorf("ca mode should not set ServerName, got %q", cfg.TLS.ServerName)
520+
// ca mode should preserve custom config's ServerName for SNI
521+
if cfg.TLS.ServerName != "customServer" {
522+
t.Errorf("ca mode should preserve custom ServerName 'customServer', got %q", cfg.TLS.ServerName)
511523
}
512524
} else {
513525
// identity (default) should preserve custom config's ServerName
514526
if cfg.TLS.ServerName != "customServer" {
515527
t.Errorf("identity mode should preserve custom ServerName 'customServer', got %q", cfg.TLS.ServerName)
516528
}
529+
if cfg.TLS.VerifyPeerCertificate != nil {
530+
t.Error("identity mode should not have VerifyPeerCertificate callback set")
531+
}
517532
}
518533
})
519534
}
@@ -569,10 +584,81 @@ func TestTLSVerifyInvalidValue(t *testing.T) {
569584
if err == nil {
570585
t.Error("expected error for invalid tls-verify value")
571586
}
587+
expectedMsg := "invalid value for tls-verify"
588+
if err != nil && !containsString(err.Error(), expectedMsg) {
589+
t.Errorf("error message should contain %q, got: %v", expectedMsg, err)
590+
}
591+
}
592+
593+
// containsString checks if s contains substr (simple implementation to avoid importing strings)
594+
func containsString(s, substr string) bool {
595+
for i := 0; i <= len(s)-len(substr); i++ {
596+
if s[i:i+len(substr)] == substr {
597+
return true
598+
}
599+
}
600+
return false
601+
}
602+
603+
func TestTLSVerifyPreservesCustomConfig(t *testing.T) {
604+
// Register a custom TLS config with various settings
605+
customConfig := &tls.Config{
606+
MinVersion: tls.VersionTLS12,
607+
MaxVersion: tls.VersionTLS13,
608+
ServerName: "customServer",
609+
CipherSuites: []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256},
610+
NextProtos: []string{"h2", "http/1.1"},
611+
RootCAs: x509.NewCertPool(),
612+
}
613+
RegisterTLSConfig("custom-full", customConfig)
614+
defer DeregisterTLSConfig("custom-full")
615+
616+
dsn := "tcp(example.com:1234)/?tls=custom-full&tls-verify=ca"
617+
cfg, err := ParseDSN(dsn)
618+
if err != nil {
619+
t.Fatal(err)
620+
}
621+
622+
if cfg.TLS == nil {
623+
t.Fatal("cfg.TLS should not be nil")
624+
}
625+
626+
// Verify VERIFY_CA mode is enabled
627+
if !cfg.TLS.InsecureSkipVerify {
628+
t.Error("ca mode should have InsecureSkipVerify=true")
629+
}
630+
if cfg.TLS.VerifyPeerCertificate == nil {
631+
t.Error("ca mode should have VerifyPeerCertificate callback set")
632+
}
633+
634+
// Verify all custom settings are preserved
635+
if cfg.TLS.MinVersion != tls.VersionTLS12 {
636+
t.Errorf("MinVersion not preserved: got %v, want %v", cfg.TLS.MinVersion, tls.VersionTLS12)
637+
}
638+
if cfg.TLS.MaxVersion != tls.VersionTLS13 {
639+
t.Errorf("MaxVersion not preserved: got %v, want %v", cfg.TLS.MaxVersion, tls.VersionTLS13)
640+
}
641+
if cfg.TLS.ServerName != "customServer" {
642+
t.Errorf("ServerName not preserved: got %q, want 'customServer'", cfg.TLS.ServerName)
643+
}
644+
if len(cfg.TLS.CipherSuites) != 1 || cfg.TLS.CipherSuites[0] != tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 {
645+
t.Error("CipherSuites not preserved")
646+
}
647+
if len(cfg.TLS.NextProtos) != 2 || cfg.TLS.NextProtos[0] != "h2" || cfg.TLS.NextProtos[1] != "http/1.1" {
648+
t.Error("NextProtos not preserved")
649+
}
650+
if cfg.TLS.RootCAs == nil {
651+
t.Error("RootCAs not preserved")
652+
}
572653
}
573654

574655
func TestRegisterTLSConfigReservedKey(t *testing.T) {
575-
reservedKeys := []string{"true", "false", "skip-verify", "preferred"}
656+
reservedKeys := []string{
657+
"true", "True", "TRUE",
658+
"false", "False", "FALSE",
659+
"skip-verify", "Skip-Verify", "SKIP-VERIFY",
660+
"preferred", "Preferred", "PREFERRED",
661+
}
576662

577663
for _, key := range reservedKeys {
578664
err := RegisterTLSConfig(key, &tls.Config{})

utils.go

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -88,21 +88,30 @@ func getTLSConfigClone(key string) (config *tls.Config) {
8888
return
8989
}
9090

91-
// createVerifyCAConfig creates a TLS config that verifies the CA certificate
92-
// but does not verify the hostname. This implements MySQL's VERIFY_CA mode.
91+
// createVerifyCAConfig creates or modifies a TLS config to verify the CA certificate
92+
// but not the hostname. This implements MySQL's VERIFY_CA mode.
9393
// It uses the recommended Go pattern from issues #21971, #31791, #31792, #35467:
9494
// 1. Set InsecureSkipVerify to disable default verification
9595
// 2. Use VerifyPeerCertificate callback to manually verify CA without hostname
9696
//
97-
// If rootCAs is nil, the system's default CA pool will be used.
98-
// If rootCAs is provided, it will be used for verification instead.
99-
func createVerifyCAConfig(rootCAs *x509.CertPool) *tls.Config {
100-
return &tls.Config{
101-
InsecureSkipVerify: true,
102-
VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
103-
return verifyCACallback(rawCerts, verifiedChains, rootCAs)
104-
},
97+
// If baseConfig is nil, creates a new minimal config.
98+
// If baseConfig is provided, clones it and preserves all settings except verification.
99+
// The rootCAs parameter specifies which CA pool to use for verification.
100+
func createVerifyCAConfig(baseConfig *tls.Config, rootCAs *x509.CertPool) *tls.Config {
101+
var cfg *tls.Config
102+
if baseConfig != nil {
103+
cfg = baseConfig.Clone()
104+
} else {
105+
cfg = &tls.Config{}
105106
}
107+
108+
// Override only the verification-related fields for VERIFY_CA mode
109+
cfg.InsecureSkipVerify = true
110+
cfg.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
111+
return verifyCACallback(rawCerts, verifiedChains, rootCAs)
112+
}
113+
114+
return cfg
106115
}
107116

108117
// verifyCACallback implements CA-only verification without hostname checking.
@@ -112,7 +121,7 @@ func createVerifyCAConfig(rootCAs *x509.CertPool) *tls.Config {
112121
//
113122
// If rootCAs is nil, the system's default CA pool will be used.
114123
// If rootCAs is provided, it will be used for verification instead.
115-
func verifyCACallback(rawCerts [][]byte, verifiedChains [][]*x509.Certificate, rootCAs *x509.CertPool) error {
124+
func verifyCACallback(rawCerts [][]byte, _ [][]*x509.Certificate, rootCAs *x509.CertPool) error {
116125
if len(rawCerts) == 0 {
117126
return errors.New("tls: no certificates from server")
118127
}
@@ -139,6 +148,7 @@ func verifyCACallback(rawCerts [][]byte, verifiedChains [][]*x509.Certificate, r
139148
opts := x509.VerifyOptions{
140149
Roots: rootCAs, // nil = use system CA pool
141150
Intermediates: intermediates,
151+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
142152
// DNSName is intentionally not set to skip hostname verification
143153
}
144154

0 commit comments

Comments
 (0)