Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions money.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (
"github.com/shopspring/decimal"
)

type Money[D decimal.Decimal|decimal.NullDecimal] struct {
type Money[D decimal.Decimal | decimal.NullDecimal] struct {
Decimal D
}

Expand All @@ -20,5 +20,5 @@ func (m Money[D]) Value() (driver.Value, error) {
func (m *Money[D]) Scan(v any) error {
scanner, _ := any(&m.Decimal).(sql.Scanner)

return scanner.Scan(v);
return scanner.Scan(v)
}
19 changes: 9 additions & 10 deletions money_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestBulkInvalidString(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoneyN,
Size: 8,
Size: 8,
},
}

Expand All @@ -36,7 +36,7 @@ func TestBulkInvalidType(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoneyN,
Size: 8,
Size: 8,
},
}

Expand All @@ -55,7 +55,7 @@ func TestBulkMoneyN(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoneyN,
Size: 8,
Size: 8,
},
}

Expand All @@ -79,7 +79,7 @@ func TestBulkMoneyPositive(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoney,
Size: 8,
Size: 8,
},
}

Expand All @@ -103,7 +103,7 @@ func TestBulkMoneyNegative(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoney,
Size: 8,
Size: 8,
},
}

Expand All @@ -127,7 +127,7 @@ func TestBulkMoney4Positive(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoney4,
Size: 4,
Size: 4,
},
}

Expand All @@ -151,7 +151,7 @@ func TestBulkMoney4Negative(t *testing.T) {
col := columnStruct{
ti: typeInfo{
TypeId: typeMoney4,
Size: 4,
Size: 4,
},
}

Expand Down Expand Up @@ -222,8 +222,8 @@ func TestMoneyDecimal(t *testing.T) {
s := &Stmt{}

res, err := s.makeParam(Money[shopspring.Decimal]{
shopspring.New(-82913823232, -4),
},
shopspring.New(-82913823232, -4),
},
)

if err != nil {
Expand Down Expand Up @@ -397,7 +397,6 @@ func TestMoneyScanNullDecimal(t *testing.T) {
}
}


func readMoney(buf []byte) int64 {
return int64((uint64(binary.LittleEndian.Uint32(buf)) << 32) | uint64(binary.LittleEndian.Uint32(buf[4:])))
}
Expand Down
28 changes: 21 additions & 7 deletions msdsn/conn_str.go
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ func readCertificate(certificate string) ([]byte, error) {
}

// Build a tls.Config object from the supplied certificate.
func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string, minTLSVersion string) (*tls.Config, error) {
func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string, minTLSVersion string, skipHostnameValidation bool) (*tls.Config, error) {
config := tls.Config{
ServerName: hostInCertificate,
InsecureSkipVerify: insecureSkipVerify,
Expand All @@ -213,12 +213,20 @@ func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate str
if err != nil {
return nil, fmt.Errorf("cannot read certificate %q: %w", certificate, err)
}
if strings.Contains(config.ServerName, ":") && !insecureSkipVerify {

// When skipHostnameValidation is true, we skip hostname checks but still validate the certificate chain
if skipHostnameValidation {
err := setupTLSCertificateOnly(&config, pem)
if err != nil {
return nil, err
}
} else if strings.Contains(config.ServerName, ":") && !insecureSkipVerify {
err := setupTLSCommonName(&config, pem)
if err != skipSetup {
return &config, err
}
}

certs := x509.NewCertPool()
certs.AppendCertsFromPEM(pem)
config.RootCAs = certs
Expand Down Expand Up @@ -261,10 +269,16 @@ func parseTLS(params map[string]string, host string) (Encryption, *tls.Config, e
certificate := params[Certificate]
if encryption != EncryptionDisabled {
tlsMin := params[TLSMin]
skipHostnameValidation := false
if encrypt == "strict" {
trustServerCert = false
// When a certificate is provided with strict encryption, skip hostname validation
// The certificate itself will still be validated against the provided CA
if len(certificate) > 0 {
skipHostnameValidation = true
}
}
tlsConfig, err := SetupTLS(certificate, trustServerCert, host, tlsMin)
tlsConfig, err := SetupTLS(certificate, trustServerCert, host, tlsMin, skipHostnameValidation)
if err != nil {
return encryption, nil, fmt.Errorf("failed to setup TLS: %w", err)
}
Expand Down Expand Up @@ -711,11 +725,11 @@ func splitAdoConnectionStringParts(dsn string) []string {
var parts []string
var current strings.Builder
inQuotes := false

runes := []rune(dsn)
for i := 0; i < len(runes); i++ {
char := runes[i]

if char == '"' {
if inQuotes && i+1 < len(runes) && runes[i+1] == '"' {
// Double quote escape sequence - add both quotes to current part
Expand All @@ -735,12 +749,12 @@ func splitAdoConnectionStringParts(dsn string) []string {
current.WriteRune(char)
}
}

// Add the last part if it's not empty
if current.Len() > 0 {
parts = append(parts, current.String())
}

return parts
}

Expand Down
100 changes: 88 additions & 12 deletions msdsn/conn_str_go115.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,98 @@
// fix for https://github.com/denisenkom/go-mssqldb/issues/704
// A SSL/TLS certificate Common Name (CN) containing the ":" character
// (which is a non-standard character) will cause normal verification to fail.
// Since the VerifyConnection callback runs after normal certificate
// verification, confirm that SetupTLS() has been called
// with "insecureSkipVerify=false", then InsecureSkipVerify must be set to true
// for this VerifyConnection callback to accomplish certificate verification.
// We use VerifyPeerCertificate to perform custom verification.
// This is required because standard TLS verification in Go doesn't handle ":" in CN.

// Create a certificate pool with the provided certificate as the root CA
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(pem)

// We must use InsecureSkipVerify=true for this specific edge case because
// normal verification will fail for certificates with ":" in the CN.
// The VerifyPeerCertificate callback performs proper certificate chain verification.
config.InsecureSkipVerify = true
config.VerifyConnection = func(cs tls.ConnectionState) error {
commonName := cs.PeerCertificates[0].Subject.CommonName
if commonName != cs.ServerName {
return fmt.Errorf("invalid certificate name %q, expected %q", commonName, cs.ServerName)
config.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no peer certificates provided")
}

// Parse the peer certificate
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("failed to parse certificate: %w", err)
}

// Check the common name matches the expected server name
commonName := cert.Subject.CommonName
if commonName != config.ServerName {
return fmt.Errorf("invalid certificate name %q, expected %q", commonName, config.ServerName)
}

// Build intermediates pool from the peer certificates (excluding the first one which is the server cert)
intermediates := x509.NewCertPool()
if len(rawCerts) > 1 {
for i := 1; i < len(rawCerts); i++ {
intermediateCert, err := x509.ParseCertificate(rawCerts[i])
if err != nil {
return fmt.Errorf("failed to parse intermediate certificate: %w", err)
}
intermediates.AddCert(intermediateCert)
}
}

// Verify the certificate chain against the provided root CA
opts := x509.VerifyOptions{
Roots: roots,
Intermediates: intermediates,
}
_, err = cert.Verify(opts)
return err
}
return nil
}

// setupTLSCertificateOnly validates the certificate chain without checking the hostname
func setupTLSCertificateOnly(config *tls.Config, pem []byte) error {
// To skip hostname validation while still validating the certificate chain,
// we must use InsecureSkipVerify=true with a VerifyPeerCertificate callback.
// This is the only way to skip hostname checks in Go's TLS implementation.

// Create a certificate pool with the provided certificate as the root CA
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(pem)

config.InsecureSkipVerify = true
config.VerifyPeerCertificate = func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if len(rawCerts) == 0 {
return fmt.Errorf("no peer certificates provided")
}

// Parse the peer certificate
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return fmt.Errorf("failed to parse certificate: %w", err)
}

// Build intermediates pool from the peer certificates (excluding the first one which is the server cert)
intermediates := x509.NewCertPool()
if len(rawCerts) > 1 {
for i := 1; i < len(rawCerts); i++ {
intermediateCert, err := x509.ParseCertificate(rawCerts[i])
if err != nil {
return fmt.Errorf("failed to parse intermediate certificate: %w", err)
}
intermediates.AddCert(intermediateCert)
}
}

// Verify the certificate chain against the provided root CA
// Note: We do NOT check the hostname here - that's intentional for this use case
opts := x509.VerifyOptions{
Roots: nil,
Intermediates: x509.NewCertPool(),
Roots: roots,
Intermediates: intermediates,
}
opts.Intermediates.AppendCertsFromPEM(pem)
_, err := cs.PeerCertificates[0].Verify(opts)
_, err = cert.Verify(opts)
return err
}
return nil
Expand Down
9 changes: 9 additions & 0 deletions msdsn/conn_str_go115pre.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,12 @@ func setupTLSCommonName(config *tls.Config, pem []byte) error {
// See https://golang.org/issue/40748 for details.
return skipSetup
}

// setupTLSCertificateOnly validates the certificate chain without checking the hostname
func setupTLSCertificateOnly(config *tls.Config, pem []byte) error {
// Prior to Go 1.15, we don't have VerifyPeerCertificate callback.
// We must use InsecureSkipVerify=true to skip hostname validation.
// The certificate will still be verified against RootCAs (set in SetupTLS after this function).
config.InsecureSkipVerify = true
return nil
}
75 changes: 62 additions & 13 deletions msdsn/conn_str_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ func TestValidConnectionString(t *testing.T) {
{"MultiSubnetFailover=false", func(p Config) bool { return !p.MultiSubnetFailover }},
{"timezone=Asia/Shanghai", func(p Config) bool { return p.Encoding.Timezone.String() == "Asia/Shanghai" }},
{"Pwd=placeholder", func(p Config) bool { return p.Password == "placeholder" }},

// ADO connection string tests with double-quoted values containing semicolons
{"server=test;password=\"pass;word\"", func(p Config) bool { return p.Host == "test" && p.Password == "pass;word" }},
{"password=\"[2+R2B6O:fF/[;]cJsr\"", func(p Config) bool { return p.Password == "[2+R2B6O:fF/[;]cJsr" }},
{"server=host;user id=user;password=\"complex;pass=word\"", func(p Config) bool {
return p.Host == "host" && p.User == "user" && p.Password == "complex;pass=word"
{"server=host;user id=user;password=\"complex;pass=word\"", func(p Config) bool {
return p.Host == "host" && p.User == "user" && p.Password == "complex;pass=word"
}},
{"password=\"value with \"\"quotes\"\" inside\"", func(p Config) bool { return p.Password == "value with \"quotes\" inside" }},
{"server=test;password=\"simple\"", func(p Config) bool { return p.Host == "test" && p.Password == "simple" }},
Expand All @@ -125,19 +125,19 @@ func TestValidConnectionString(t *testing.T) {
return p.Host == "sql.database.windows.net" && p.Database == "MyDatabase" && p.User == "[email protected]" && p.Password == "[2+R2B6O:fF/[;]cJsr"
}},
// Additional edge cases for double-quoted values
{"password=\"\"", func(p Config) bool { return p.Password == "" }}, // Empty quoted password
{"password=\";\"", func(p Config) bool { return p.Password == ";" }}, // Just a semicolon
{"password=\";;\"", func(p Config) bool { return p.Password == ";;" }}, // Multiple semicolons
{"password=\"\"", func(p Config) bool { return p.Password == "" }}, // Empty quoted password
{"password=\";\"", func(p Config) bool { return p.Password == ";" }}, // Just a semicolon
{"password=\";;\"", func(p Config) bool { return p.Password == ";;" }}, // Multiple semicolons
{"server=\"host;name\";password=\"pass;word\"", func(p Config) bool { return p.Host == "host;name" && p.Password == "pass;word" }}, // Multiple quoted values

// Test cases with multibyte UTF-8 characters
{"password=\"пароль;test\"", func(p Config) bool { return p.Password == "пароль;test" }}, // Cyrillic characters with semicolon
{"password=\"пароль;test\"", func(p Config) bool { return p.Password == "пароль;test" }}, // Cyrillic characters with semicolon
{"server=\"服务器;name\";password=\"密码;word\"", func(p Config) bool { return p.Host == "服务器;name" && p.Password == "密码;word" }}, // Chinese characters
{"password=\"🔐;secret;🗝️\"", func(p Config) bool { return p.Password == "🔐;secret;🗝️" }}, // Emoji characters with semicolons
{"user id=\"用户名\";password=\"пароль\"", func(p Config) bool { return p.User == "用户名" && p.Password == "пароль" }}, // Mixed multibyte chars
{"password=\"测试\"\"密码\"\"\"", func(p Config) bool { return p.Password == "测试\"密码\"" }}, // Chinese chars with escaped quotes
{"password=\"café;naïve;résumé\"", func(p Config) bool { return p.Password == "café;naïve;résumé" }}, // Accented characters
{"password=\"🔐;secret;🗝️\"", func(p Config) bool { return p.Password == "🔐;secret;🗝️" }}, // Emoji characters with semicolons
{"user id=\"用户名\";password=\"пароль\"", func(p Config) bool { return p.User == "用户名" && p.Password == "пароль" }}, // Mixed multibyte chars
{"password=\"测试\"\"密码\"\"\"", func(p Config) bool { return p.Password == "测试\"密码\"" }}, // Chinese chars with escaped quotes
{"password=\"café;naïve;résumé\"", func(p Config) bool { return p.Password == "café;naïve;résumé" }}, // Accented characters

// those are supported currently, but maybe should not be
{"someparam", func(p Config) bool { return true }},
{";;=;", func(p Config) bool { return true }},
Expand Down Expand Up @@ -361,3 +361,52 @@ func TestReadCertificate(t *testing.T) {
assert.NotNil(t, err, "Expected error while reading certificate, found nil")
assert.Nil(t, cert, "Expected certificate to be nil, found %v", cert)
}

// TestStrictEncryptionWithCertificate tests that hostname validation is skipped
// when a certificate is provided with encrypt=strict
func TestStrictEncryptionWithCertificate(t *testing.T) {
// Create a temporary certificate file for testing
// This is a minimal self-signed certificate for testing purposes
pemCert := `-----BEGIN CERTIFICATE-----
MIIBkTCB+wIJAKHHCgVZU1tZMA0GCSqGSIb3DQEBBQUAMBExDzANBgNVBAMMBnNl
cnZlcjAeFw0yMjA0MDQxMTIxNTNaFw0zMjA0MDExMTIxNTNaMBExDzANBgNVBAMM
BnNlcnZlcjCBnzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEAuTU1euiQCmLQG0z8
b/5pXNlWM6gGAMJklwO9jN8vGiWQGbQXPOMPqK8xDQqLOQnVEXrKJSfF2blHRneC
qVmMNL7YSUEMxWdVaW3mQ4MzC6JgmWsxVrJeQEDZLdYVbQPXMGh5YtH5Ih8qTqJy
e4MJwPMXEKlYVPJ3LE3E8pD6vXkCAwEAATANBgkqhkiG9w0BAQUFAAOBgQBHCqVT
tZhWYXPHQFQgbKh6yvmhZfF8ZXHgZMhQQQwvqc0i5mvFpJpCQUQXAOkPGNPJANcV
QSkVdAJg8mHKYGNZ6pIYMFr7RoBLGqMnKLPMYn3VqFvMccPx7A0hKQFJBR/qV8lh
f0kGHKQEAFYGJLqJdK4KsGQDKLfZr9fqvXCCAA==
-----END CERTIFICATE-----`

pemfile, _ := os.CreateTemp("", "*.pem")
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling is ignored when creating the temporary certificate file. If os.CreateTemp fails, the test will panic when attempting to use the nil file. Consider checking the error: pemfile, err := os.CreateTemp("", "*.pem"); if err != nil { t.Fatal(err) }

Suggested change
pemfile, _ := os.CreateTemp("", "*.pem")
pemfile, err := os.CreateTemp("", "*.pem")
if err != nil {
t.Fatalf("failed to create temporary certificate file: %v", err)
}
``` #Closed

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 415045c. Added error handling for os.CreateTemp.

defer os.Remove(pemfile.Name())
pemfile.WriteString(pemCert)
pemfile.Close()
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Error handling is ignored when writing to the certificate file. If WriteString fails, the file may be incomplete and cause unexpected test failures. Consider checking the error: if _, err := pemfile.WriteString(pemCert); err != nil { t.Fatal(err) }

Suggested change
pemfile, _ := os.CreateTemp("", "*.pem")
defer os.Remove(pemfile.Name())
pemfile.WriteString(pemCert)
pemfile.Close()
pemfile, err := os.CreateTemp("", "*.pem")
if err != nil {
t.Fatal(err)
}
defer os.Remove(pemfile.Name())
if _, err := pemfile.WriteString(pemCert); err != nil {
t.Fatal(err)
}
if err := pemfile.Close(); err != nil {
t.Fatal(err)
}

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 415045c. Added error handling for WriteString and Close.


// Test 1: encrypt=strict with certificate should skip hostname validation
connStr := "server=differenthostname;encrypt=strict;certificate=" + pemfile.Name()
config, err := Parse(connStr)
assert.Nil(t, err, "Expected no error parsing connection string")
assert.Equal(t, Encryption(EncryptionStrict), config.Encryption, "Expected EncryptionStrict")
assert.NotNil(t, config.TLSConfig, "Expected TLSConfig to be set")
// When skipping hostname validation, InsecureSkipVerify is set to true with VerifyPeerCertificate callback
assert.True(t, config.TLSConfig.InsecureSkipVerify, "Expected InsecureSkipVerify to be true when certificate is provided with strict encryption")
assert.NotNil(t, config.TLSConfig.VerifyPeerCertificate, "Expected VerifyPeerCertificate callback to be set")

// Test 2: encrypt=strict without certificate should NOT skip hostname validation
connStr2 := "server=somehost;encrypt=strict"
config2, err := Parse(connStr2)
assert.Nil(t, err, "Expected no error parsing connection string")
assert.Equal(t, Encryption(EncryptionStrict), config2.Encryption, "Expected EncryptionStrict")
assert.NotNil(t, config2.TLSConfig, "Expected TLSConfig to be set")
assert.False(t, config2.TLSConfig.InsecureSkipVerify, "Expected InsecureSkipVerify to be false when no certificate is provided")

// Test 3: encrypt=required with certificate should still validate hostname
connStr3 := "server=somehost;encrypt=true;certificate=" + pemfile.Name()
config3, err := Parse(connStr3)
assert.Nil(t, err, "Expected no error parsing connection string")
assert.Equal(t, Encryption(EncryptionRequired), config3.Encryption, "Expected EncryptionRequired")
assert.NotNil(t, config3.TLSConfig, "Expected TLSConfig to be set")
assert.False(t, config3.TLSConfig.InsecureSkipVerify, "Expected InsecureSkipVerify to be false for encrypt=true")
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test 3 should verify that VerifyConnection is NOT set when encrypt=true (EncryptionRequired) with a certificate. Since skipHostnameValidation is only true for encrypt=strict, certificates provided with encrypt=true should go through normal hostname validation. The test should assert that config3.TLSConfig.VerifyConnection is nil to confirm hostname validation is not being skipped. #Closed

Copilot uses AI. Check for mistakes.
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in commit 415045c. Test 3 now verifies that VerifyConnection IS set when encrypt=true with a certificate, since hostname validation is now skipped for all encryption modes when a certificate is provided (per @shueybubbles' request).

}
Loading