Skip to content

Commit e7dc6a4

Browse files
Implement -J flag for server certificate pinning with strict encryption (microsoft#615)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: shueybubbles <2224906+shueybubbles@users.noreply.github.com>
1 parent ba5b460 commit e7dc6a4

File tree

6 files changed

+54
-4
lines changed

6 files changed

+54
-4
lines changed

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,12 @@ darwin-arm64/sqlcmd
3232
linux-amd64/sqlcmd
3333
linux-arm64/sqlcmd
3434
linux-s390x/sqlcmd
35+
36+
# Build artifacts in root
37+
/sqlcmd
38+
/sqlcmd_binary
39+
40+
# certificates used for local testing
41+
*.der
42+
*.pem
43+
*.pfx

cmd/sqlcmd/sqlcmd.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ type SQLCmdArguments struct {
5757
ApplicationIntent string
5858
EncryptConnection string
5959
HostNameInCertificate string
60+
ServerCertificate string
6061
DriverLoggingLevel int
6162
ExitOnError bool
6263
ErrorSeverityLevel uint8
@@ -127,6 +128,15 @@ const (
127128
removeControlCharacters = "remove-control-characters"
128129
)
129130

131+
func encryptConnectionAllowsTLS(value string) bool {
132+
switch strings.ToLower(value) {
133+
case "s", "strict", "m", "mandatory", "true", "t", "yes", "1":
134+
return true
135+
default:
136+
return false
137+
}
138+
}
139+
130140
// Validate arguments for settings not describe
131141
func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
132142
if a.ListServers != "" {
@@ -144,6 +154,8 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
144154
err = mutuallyExclusiveError("-E", `-U/-P`)
145155
case a.UseAad && len(a.AuthenticationMethod) > 0:
146156
err = mutuallyExclusiveError("-G", "--authentication-method")
157+
case len(a.HostNameInCertificate) > 0 && len(a.ServerCertificate) > 0:
158+
err = mutuallyExclusiveError("-F", "-J")
147159
case a.PacketSize != 0 && (a.PacketSize < 512 || a.PacketSize > 32767):
148160
err = localizer.Errorf(`'-a %#v': Packet size has to be a number between 512 and 32767.`, a.PacketSize)
149161
// Ignore 0 even though it's technically an invalid input
@@ -157,6 +169,8 @@ func (a *SQLCmdArguments) Validate(c *cobra.Command) (err error) {
157169
err = rangeParameterError("-y", fmt.Sprint(*a.VariableTypeWidth), 0, 8000, true)
158170
case a.QueryTimeout < 0 || a.QueryTimeout > 65534:
159171
err = rangeParameterError("-t", fmt.Sprint(a.QueryTimeout), 0, 65534, true)
172+
case a.ServerCertificate != "" && !encryptConnectionAllowsTLS(a.EncryptConnection):
173+
err = localizer.Errorf("The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict).")
160174
}
161175
}
162176
if err != nil {
@@ -429,6 +443,8 @@ func setFlags(rootCmd *cobra.Command, args *SQLCmdArguments) {
429443
rootCmd.Flags().StringVarP(&args.ApplicationIntent, applicationIntent, "K", "default", localizer.Sprintf("Declares the application workload type when connecting to a server. The only currently supported value is ReadOnly. If %s is not specified, the sqlcmd utility will not support connectivity to a secondary replica in an Always On availability group", localizer.ApplicationIntentFlagShort))
430444
rootCmd.Flags().StringVarP(&args.EncryptConnection, encryptConnection, "N", "default", localizer.Sprintf("This switch is used by the client to request an encrypted connection"))
431445
rootCmd.Flags().StringVarP(&args.HostNameInCertificate, "host-name-in-certificate", "F", "", localizer.Sprintf("Specifies the host name in the server certificate."))
446+
rootCmd.Flags().StringVarP(&args.ServerCertificate, "server-certificate", "J", "", localizer.Sprintf("Specifies the path to a server certificate file (PEM, DER, or CER) to match against the server's TLS certificate. Use when encryption is enabled (-N true, -N mandatory, or -N strict) for certificate pinning instead of standard certificate validation."))
447+
rootCmd.MarkFlagsMutuallyExclusive("host-name-in-certificate", "server-certificate")
432448
// Can't use NoOptDefVal until this fix: https://github.com/spf13/cobra/issues/866
433449
//rootCmd.Flags().Lookup(encryptConnection).NoOptDefVal = "true"
434450
rootCmd.Flags().BoolVarP(&args.Vertical, "vertical", "", false, localizer.Sprintf("Prints the output in vertical format. This option sets the sqlcmd scripting variable %s to '%s'. The default is false", sqlcmd.SQLCMDFORMAT, "vert"))
@@ -721,6 +737,7 @@ func setConnect(connect *sqlcmd.ConnectSettings, args *SQLCmdArguments, vars *sq
721737
connect.Encrypt = args.EncryptConnection
722738
}
723739
connect.HostNameInCertificate = args.HostNameInCertificate
740+
connect.ServerCertificate = args.ServerCertificate
724741
connect.PacketSize = args.PacketSize
725742
connect.WorkstationName = args.WorkstationName
726743
connect.LogLevel = args.DriverLoggingLevel

cmd/sqlcmd/sqlcmd_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,18 @@ func TestValidCommandLineToArgsConversion(t *testing.T) {
111111
{[]string{"-N", "s", "-F", "myserver.domain.com"}, func(args SQLCmdArguments) bool {
112112
return args.EncryptConnection == "s" && args.HostNameInCertificate == "myserver.domain.com"
113113
}},
114+
{[]string{"-N", "s", "-J", "/path/to/cert.pem"}, func(args SQLCmdArguments) bool {
115+
return args.EncryptConnection == "s" && args.ServerCertificate == "/path/to/cert.pem"
116+
}},
117+
{[]string{"-N", "strict", "-J", "/path/to/cert.der"}, func(args SQLCmdArguments) bool {
118+
return args.EncryptConnection == "strict" && args.ServerCertificate == "/path/to/cert.der"
119+
}},
120+
{[]string{"-N", "m", "-J", "/path/to/cert.cer"}, func(args SQLCmdArguments) bool {
121+
return args.EncryptConnection == "m" && args.ServerCertificate == "/path/to/cert.cer"
122+
}},
123+
{[]string{"-N", "true", "-J", "/path/to/cert2.pem"}, func(args SQLCmdArguments) bool {
124+
return args.EncryptConnection == "true" && args.ServerCertificate == "/path/to/cert2.pem"
125+
}},
114126
}
115127

116128
for _, test := range commands {
@@ -154,14 +166,18 @@ func TestInvalidCommandLine(t *testing.T) {
154166
{[]string{"-E", "-U", "someuser"}, "The -E and the -U/-P options are mutually exclusive."},
155167
{[]string{"-L", "-q", `"select 1"`}, "The -L parameter can not be used in combination with other parameters."},
156168
{[]string{"-i", "foo.sql", "-q", `"select 1"`}, "The i and the -Q/-q options are mutually exclusive."},
157-
{[]string{"-r", "5"}, `'-r 5': Unexpected argument. Argument value has to be one of [0 1].`},
169+
{[]string{"-r", "5"}, "'-r 5': Unexpected argument. Argument value has to be one of [0 1]."},
158170
{[]string{"-w", "x"}, "'-w x': value must be greater than 8 and less than 65536."},
159171
{[]string{"-y", "111111"}, "'-y 111111': value must be greater than or equal to 0 and less than or equal to 8000."},
160172
{[]string{"-Y", "-2"}, "'-Y -2': value must be greater than or equal to 0 and less than or equal to 8000."},
161173
{[]string{"-P"}, "'-P': Missing argument. Enter '-?' for help."},
162174
{[]string{"-;"}, "';': Unknown Option. Enter '-?' for help."},
163175
{[]string{"-t", "-2"}, "'-t -2': value must be greater than or equal to 0 and less than or equal to 65534."},
164176
{[]string{"-N", "invalid"}, "'-N invalid': Unexpected argument. Argument value has to be one of [m[andatory] yes 1 t[rue] disable o[ptional] no 0 f[alse] s[trict]]."},
177+
{[]string{"-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
178+
{[]string{"-N", "optional", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
179+
{[]string{"-N", "disable", "-J", "/path/to/cert.pem"}, "The -J parameter requires encryption to be enabled (-N true, -N mandatory, or -N strict)."},
180+
{[]string{"-N", "strict", "-F", "myserver.domain.com", "-J", "/path/to/cert.pem"}, "The -F and the -J options are mutually exclusive."},
165181
}
166182

167183
for _, test := range commands {

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ require (
1212
github.com/docker/go-connections v0.4.0
1313
github.com/golang-sql/sqlexp v0.1.0
1414
github.com/google/uuid v1.6.0
15-
github.com/microsoft/go-mssqldb v1.9.2
15+
github.com/microsoft/go-mssqldb v1.9.6
1616
github.com/opencontainers/image-spec v1.0.2
1717
github.com/peterh/liner v1.2.2
1818
github.com/pkg/errors v0.9.1
@@ -72,6 +72,7 @@ require (
7272
github.com/prometheus/client_model v0.2.0 // indirect
7373
github.com/prometheus/common v0.26.0 // indirect
7474
github.com/prometheus/procfs v0.6.0 // indirect
75+
github.com/shopspring/decimal v1.4.0 // indirect
7576
github.com/spf13/afero v1.9.2 // indirect
7677
github.com/spf13/cast v1.5.0 // indirect
7778
github.com/spf13/jwalterweatherman v1.1.0 // indirect

go.sum

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,8 @@ github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8Bz
257257
github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
258258
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
259259
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
260-
github.com/microsoft/go-mssqldb v1.9.2 h1:nY8TmFMQOHpm2qVWo6y4I2mAmVdZqlGiMGAYt64Ibbs=
261-
github.com/microsoft/go-mssqldb v1.9.2/go.mod h1:GBbW9ASTiDC+mpgWDGKdm3FnFLTUsLYN3iFL90lQ+PA=
260+
github.com/microsoft/go-mssqldb v1.9.6 h1:1MNQg5UiSsokiPz3++K2KPx4moKrwIqly1wv+RyCKTw=
261+
github.com/microsoft/go-mssqldb v1.9.6/go.mod h1:yYMPDufyoF2vVuVCUGtZARr06DKFIhMrluTcgWlXpr4=
262262
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
263263
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
264264
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -320,6 +320,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
320320
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
321321
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
322322
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
323+
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
324+
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
323325
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
324326
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
325327
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=

pkg/sqlcmd/connect.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ type ConnectSettings struct {
6060
ChangePassword string
6161
// The HostNameInCertificate is the name to use for the host in the certificate validation
6262
HostNameInCertificate string
63+
// ServerCertificate is the path to a certificate file to match against the server's TLS certificate
64+
ServerCertificate string
6365
}
6466

6567
func (c ConnectSettings) authenticationMethod() string {
@@ -150,6 +152,9 @@ func (connect ConnectSettings) ConnectionString() (connectionString string, err
150152
if connect.HostNameInCertificate != "" {
151153
query.Add(msdsn.HostNameInCertificate, connect.HostNameInCertificate)
152154
}
155+
if connect.ServerCertificate != "" {
156+
query.Add(msdsn.ServerCertificate, connect.ServerCertificate)
157+
}
153158
if connect.LogLevel > 0 {
154159
query.Add(msdsn.LogParam, fmt.Sprint(connect.LogLevel))
155160
}

0 commit comments

Comments
 (0)