@@ -18,6 +18,7 @@ import (
1818 "bytes"
1919 "crypto/sha1"
2020 "encoding/hex"
21+ "fmt"
2122 "net"
2223
2324 "github.com/dolthub/vitess/go/mysql"
@@ -107,7 +108,7 @@ var _ mysql.CachingStorage = (*noopCachingStorage)(nil)
107108//
108109// This implementation also handles authentication when a client doesn't send an auth response and
109110// the associated user account does not have a password set.
110- func (n noopCachingStorage ) UserEntryWithCacheHash (_ * mysql.Conn , _ []byte , user string , authResponse []byte , remoteAddr net.Addr ) (mysql.Getter , mysql.CacheState , error ) {
111+ func (n noopCachingStorage ) UserEntryWithCacheHash (conn * mysql.Conn , _ []byte , user string , authResponse []byte , remoteAddr net.Addr ) (mysql.Getter , mysql.CacheState , error ) {
111112 db := n .db
112113
113114 // If there is no mysql database of user info, then don't approve or reject, since we can't look at
@@ -134,6 +135,11 @@ func (n noopCachingStorage) UserEntryWithCacheHash(_ *mysql.Conn, _ []byte, user
134135 return nil , mysql .AuthRejected , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
135136 }
136137
138+ // validate any extra connection security requirements, such as SSL or a client cert
139+ if err = validateConnectionSecurity (userEntry , conn ); err != nil {
140+ return nil , mysql .AuthRejected , err
141+ }
142+
137143 if userEntry .AuthString == "" {
138144 return sql.MysqlConnectionUser {User : user , Host : host }, mysql .AuthAccepted , nil
139145 } else {
@@ -166,7 +172,7 @@ var _ mysql.PlainTextStorage = (*sha2PlainTextStorage)(nil)
166172
167173// UserEntryWithPassword implements the mysql.PlainTextStorage interface.
168174// The auth framework in Vitess also passes in user certificates, but we don't support that feature yet.
169- func (s sha2PlainTextStorage ) UserEntryWithPassword (_ * mysql.Conn , user string , password string , remoteAddr net.Addr ) (mysql.Getter , error ) {
175+ func (s sha2PlainTextStorage ) UserEntryWithPassword (conn * mysql.Conn , user string , password string , remoteAddr net.Addr ) (mysql.Getter , error ) {
170176 db := s .db
171177
172178 host , err := extractHostAddress (remoteAddr )
@@ -186,6 +192,11 @@ func (s sha2PlainTextStorage) UserEntryWithPassword(_ *mysql.Conn, user string,
186192 return nil , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
187193 }
188194
195+ // validate any extra connection security requirements, such as SSL or a client cert
196+ if err = validateConnectionSecurity (userEntry , conn ); err != nil {
197+ return nil , err
198+ }
199+
189200 if len (userEntry .AuthString ) > 0 {
190201 digestType , iterations , salt , _ , err := mysql .DeserializeCachingSha2PasswordAuthString ([]byte (userEntry .AuthString ))
191202 if err != nil {
@@ -329,7 +340,7 @@ var _ mysql.HashStorage = (*nativePasswordHashStorage)(nil)
329340
330341// UserEntryWithHash implements the mysql.HashStorage interface. This implementation is called by the MySQL
331342// native password auth method to validate a password hash with the user's stored password hash.
332- func (nphs * nativePasswordHashStorage ) UserEntryWithHash (_ * mysql.Conn , salt []byte , user string , authResponse []byte , remoteAddr net.Addr ) (mysql.Getter , error ) {
343+ func (nphs * nativePasswordHashStorage ) UserEntryWithHash (conn * mysql.Conn , salt []byte , user string , authResponse []byte , remoteAddr net.Addr ) (mysql.Getter , error ) {
333344 db := nphs .db
334345
335346 host , err := extractHostAddress (remoteAddr )
@@ -348,6 +359,12 @@ func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ *mysql.Conn, salt []b
348359 if userEntry == nil || userEntry .Locked {
349360 return nil , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
350361 }
362+
363+ // validate any extra connection security requirements, such as SSL or a client cert
364+ if err = validateConnectionSecurity (userEntry , conn ); err != nil {
365+ return nil , err
366+ }
367+
351368 if len (userEntry .AuthString ) > 0 {
352369 if ! validateMysqlNativePassword (authResponse , salt , userEntry .AuthString ) {
353370 return nil , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
@@ -361,6 +378,62 @@ func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ *mysql.Conn, salt []b
361378 return sql.MysqlConnectionUser {User : userEntry .User , Host : userEntry .Host }, nil
362379}
363380
381+ // validateConnectionSecurity examines the security properties of |conn| (e.g. TLS,
382+ // selected cipher, X509 client certs) and validates specific connection properties
383+ // based on what |userEntry| has configured. An error is returned if any validation
384+ // issues were detected, otherwise nil is returned.
385+ func validateConnectionSecurity (userEntry * User , conn * mysql.Conn ) error {
386+ switch userEntry .SslType {
387+ case "" :
388+ // No connection security validation needed
389+ return nil
390+ case "ANY" :
391+ // ANY indicates that we need any form of secure socket
392+ if ! conn .TLSEnabled () {
393+ return fmt .Errorf ("SSL/TLS connection required" )
394+ }
395+ case "X509" :
396+ // X509 means that a valid X509 client certificate is required
397+ // NOTE: cert validation (e.g. expiration date, CA chain) is handled
398+ // in the Go networking stack, so long as tls.VerifyClientCertIfGiven
399+ // is specified in the TLS configuration for the server.
400+ clientCerts := conn .GetTLSClientCerts ()
401+ if len (clientCerts ) == 0 {
402+ return fmt .Errorf ("no client certificate provided" )
403+ }
404+ // TODO: Do we need to do anything if there are multiple client certs provided?
405+ case "SPECIFIED" :
406+ // Specified means that we have additional requirements on either the SSL cipher
407+ // or the X509 cert, so we need to perform additional validation checks.
408+ if userEntry .SslCipher != "" {
409+ if ! conn .TLSEnabled () {
410+ return fmt .Errorf ("SSL/TLS connection required" )
411+ }
412+ // TODO: validate the ssl cipher
413+ // TODO: How do we get the SSL cipher in use? What are valid values you can specify in MySQL?
414+ return fmt .Errorf ("SSL cipher validation not supported yet" )
415+ }
416+ if userEntry .X509Issuer != "" {
417+ if len (conn .GetTLSClientCerts ()) == 0 {
418+ return fmt .Errorf ("no client certificate provided" )
419+ }
420+ // TODO: Validate the client cert issuer
421+ return fmt .Errorf ("X509 issuer validation not supported yet" )
422+ }
423+ if userEntry .X509Subject != "" {
424+ if len (conn .GetTLSClientCerts ()) == 0 {
425+ return fmt .Errorf ("no client certificate provided" )
426+ }
427+ // TODO: Validate the client cert subject
428+ return fmt .Errorf ("X509 subject validation not supported yet" )
429+ }
430+ default :
431+ return fmt .Errorf ("unsupported ssl_type: %v" , userEntry .SslType )
432+ }
433+
434+ return nil
435+ }
436+
364437// userValidator implements the mysql.UserValidator interface. It looks up a user and host from the
365438// associated mysql database (|db|) and validates that a user entry exists and that it is configured
366439// for the specified authentication plugin (|authMethod|).
@@ -408,7 +481,13 @@ func (uv *userValidator) HandleUser(user string, remoteAddr net.Addr) bool {
408481 }
409482 userEntry := db .GetUser (rd , user , host , false )
410483
411- return userEntry != nil && userEntry .Plugin == string (uv .authMethod )
484+ // If we don't find a matching user, or we find one, but it's for a different auth method,
485+ // then return false to indicate this auth method can't handle that user.
486+ if userEntry == nil || userEntry .Plugin != string (uv .authMethod ) {
487+ return false
488+ }
489+
490+ return true
412491}
413492
414493// extractHostAddress extracts the host address from |addr|, checking to see if it is a unix socket, and if
0 commit comments