From 2d408c3ae25a3af94a54322903b14c854e98b18c Mon Sep 17 00:00:00 2001 From: David Shiflet Date: Mon, 8 Aug 2022 09:13:04 -0400 Subject: [PATCH] add tlsmin connection parameter (#45) * add tlsmin connection parameter * fix typoe * fix param name * 1.3 is too new for old go * tls 1.3 is go 1.12+ --- README.md | 3 ++- msdsn/conn_str.go | 7 +++++-- msdsn/conn_str_go112.go | 22 ++++++++++++++++++++++ msdsn/conn_str_go112pre.go | 20 ++++++++++++++++++++ msdsn/conn_str_test.go | 22 +++++++++++++++++++++- tds.go | 2 +- 6 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 msdsn/conn_str_go112.go create mode 100644 msdsn/conn_str_go112pre.go diff --git a/README.md b/README.md index 7f1b6b3b..89a490ef 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,11 @@ Other supported formats are listed below. * 16 log statement parameters * 32 log transaction begin/end * `TrustServerCertificate` - * false - Server certificate is checked. Default is false if encypt is specified. + * false - Server certificate is checked. Default is false if encrypt is specified. * true - Server certificate is not checked. Default is true if encrypt is not specified. If trust server certificate is true, driver accepts any certificate presented by the server and any host name in that certificate. In this mode, TLS is susceptible to man-in-the-middle attacks. This should be used only for testing. * `certificate` - The file that contains the public key certificate of the CA that signed the SQL Server certificate. The specified certificate overrides the go platform specific CA certificates. * `hostNameInCertificate` - Specifies the Common Name (CN) in the server certificate. Default value is the server host. +* `tlsmin` - Specifies the minimum TLS version for negotiating encryption with the server. Recognized values are `1.0`, `1.1`, `1.2`, `1.3`. If not set to a recognized value the default value for the `tls` package will be used. The default is currently `1.2`. * `ServerSPN` - The kerberos SPN (Service Principal Name) for the server. Default is MSSQLSvc/host:port. * `Workstation ID` - The workstation name (default is the host name) * `ApplicationIntent` - Can be given the value `ReadOnly` to initiate a read-only connection to an Availability Group listener. The `database` must be specified when connecting with `Application Intent` set to `ReadOnly`. diff --git a/msdsn/conn_str.go b/msdsn/conn_str.go index d85f0865..0b5354c0 100644 --- a/msdsn/conn_str.go +++ b/msdsn/conn_str.go @@ -75,7 +75,7 @@ type Config struct { PacketSize uint16 } -func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string) (*tls.Config, error) { +func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate string, minTLSVersion string) (*tls.Config, error) { config := tls.Config{ ServerName: hostInCertificate, InsecureSkipVerify: insecureSkipVerify, @@ -85,7 +85,9 @@ func SetupTLS(certificate string, insecureSkipVerify bool, hostInCertificate str // while SQL Server seems to expect one TCP segment per encrypted TDS package. // Setting DynamicRecordSizingDisabled to true disables that algorithm and uses 16384 bytes per TLS package DynamicRecordSizingDisabled: true, + MinVersion: TLSVersionFromString(minTLSVersion), } + if len(certificate) == 0 { return &config, nil } @@ -256,8 +258,9 @@ func Parse(dsn string) (Config, map[string]string, error) { } if p.Encryption != EncryptionDisabled { + tlsMin := params["tlsmin"] var err error - p.TLSConfig, err = SetupTLS(certificate, trustServerCert, hostInCertificate) + p.TLSConfig, err = SetupTLS(certificate, trustServerCert, hostInCertificate, tlsMin) if err != nil { return p, params, fmt.Errorf("failed to setup TLS: %w", err) } diff --git a/msdsn/conn_str_go112.go b/msdsn/conn_str_go112.go new file mode 100644 index 00000000..3ae4dce9 --- /dev/null +++ b/msdsn/conn_str_go112.go @@ -0,0 +1,22 @@ +//go:build go1.12 +// +build go1.12 + +package msdsn + +import "crypto/tls" + +func TLSVersionFromString(minTLSVersion string) uint16 { + switch minTLSVersion { + case "1.0": + return tls.VersionTLS10 + case "1.1": + return tls.VersionTLS11 + case "1.2": + return tls.VersionTLS12 + case "1.3": + return tls.VersionTLS13 + default: + // use the tls package default + } + return 0 +} diff --git a/msdsn/conn_str_go112pre.go b/msdsn/conn_str_go112pre.go new file mode 100644 index 00000000..7b47b6bd --- /dev/null +++ b/msdsn/conn_str_go112pre.go @@ -0,0 +1,20 @@ +//go:build !go1.12 +// +build !go1.12 + +package msdsn + +import "crypto/tls" + +func TLSVersionFromString(minTLSVersion string) uint16 { + switch minTLSVersion { + case "1.0": + return tls.VersionTLS10 + case "1.1": + return tls.VersionTLS11 + case "1.2": + return tls.VersionTLS12 + default: + // use the tls package default + } + return 0 +} diff --git a/msdsn/conn_str_test.go b/msdsn/conn_str_test.go index 594b5b3d..a7601870 100644 --- a/msdsn/conn_str_test.go +++ b/msdsn/conn_str_test.go @@ -1,6 +1,7 @@ package msdsn import ( + "crypto/tls" "reflect" "testing" "time" @@ -59,7 +60,23 @@ func TestValidConnectionString(t *testing.T) { {"failoverpartner=fopartner;failoverport=2000", func(p Config) bool { return p.FailOverPartner == "fopartner" && p.FailOverPort == 2000 }}, {"app name=appname;applicationintent=ReadOnly;database=testdb", func(p Config) bool { return p.AppName == "appname" && p.ReadOnlyIntent }}, {"encrypt=disable", func(p Config) bool { return p.Encryption == EncryptionDisabled }}, - {"encrypt=true", func(p Config) bool { return p.Encryption == EncryptionRequired }}, + {"encrypt=disable;tlsmin=1.1", func(p Config) bool { return p.Encryption == EncryptionDisabled && p.TLSConfig == nil }}, + {"encrypt=true", func(p Config) bool { return p.Encryption == EncryptionRequired && p.TLSConfig.MinVersion == 0 }}, + {"encrypt=true;tlsmin=1.0", func(p Config) bool { + return p.Encryption == EncryptionRequired && p.TLSConfig.MinVersion == tls.VersionTLS10 + }}, + {"encrypt=false;tlsmin=1.0", func(p Config) bool { + return p.Encryption == EncryptionOff && p.TLSConfig.MinVersion == tls.VersionTLS10 + }}, + {"encrypt=true;tlsmin=1.1", func(p Config) bool { + return p.Encryption == EncryptionRequired && p.TLSConfig.MinVersion == tls.VersionTLS11 + }}, + {"encrypt=true;tlsmin=1.2", func(p Config) bool { + return p.Encryption == EncryptionRequired && p.TLSConfig.MinVersion == tls.VersionTLS12 + }}, + {"encrypt=true;tlsmin=1.4", func(p Config) bool { + return p.Encryption == EncryptionRequired && p.TLSConfig.MinVersion == 0 + }}, {"encrypt=false", func(p Config) bool { return p.Encryption == EncryptionOff }}, {"connection timeout=3;dial timeout=4;keepalive=5", func(p Config) bool { return p.ConnTimeout == 3*time.Second && p.DialTimeout == 4*time.Second && p.KeepAlive == 5*time.Second @@ -159,6 +176,9 @@ func TestValidConnectionString(t *testing.T) { {"sqlserver://someuser@somehost?connection+timeout=30&disableretry=1", func(p Config) bool { return p.Host == "somehost" && p.Port == 0 && p.Instance == "" && p.User == "someuser" && p.Password == "" && p.ConnTimeout == 30*time.Second && p.DisableRetry }}, + {"sqlserver://somehost?encrypt=true&tlsmin=1.1", func(p Config) bool { + return p.Host == "somehost" && p.Encryption == EncryptionRequired && p.TLSConfig.MinVersion == tls.VersionTLS11 + }}, } for _, ts := range connStrings { p, _, err := Parse(ts.connStr) diff --git a/tds.go b/tds.go index 98f2f94e..ced4a7de 100644 --- a/tds.go +++ b/tds.go @@ -1148,7 +1148,7 @@ initiate_connection: } } if config == nil { - config, err = msdsn.SetupTLS("", false, p.Host) + config, err = msdsn.SetupTLS("", false, p.Host, "") if err != nil { return nil, err }