@@ -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
@@ -131,7 +132,12 @@ func (n noopCachingStorage) UserEntryWithCacheHash(_ *mysql.Conn, _ []byte, user
131132
132133 userEntry := db .GetUser (rd , user , host , false )
133134 if userEntry == nil || userEntry .Locked {
134- return nil , mysql .AuthRejected , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
135+ return nil , mysql .AuthRejected , newAccessDeniedError (user )
136+ }
137+
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
135141 }
136142
137143 if userEntry .AuthString == "" {
@@ -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 )
@@ -183,7 +189,12 @@ func (s sha2PlainTextStorage) UserEntryWithPassword(_ *mysql.Conn, user string,
183189
184190 userEntry := db .GetUser (rd , user , host , false )
185191 if userEntry == nil || userEntry .Locked {
186- return nil , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
192+ return nil , newAccessDeniedError (userEntry .User )
193+ }
194+
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
187198 }
188199
189200 if len (userEntry .AuthString ) > 0 {
@@ -202,12 +213,12 @@ func (s sha2PlainTextStorage) UserEntryWithPassword(_ *mysql.Conn, user string,
202213 }
203214
204215 if userEntry .AuthString != string (authString ) {
205- return nil , mysql . NewSQLError ( mysql . ERAccessDeniedError , mysql . SSAccessDeniedError , "Access denied for user '%v'" , user )
216+ return nil , newAccessDeniedError ( user )
206217 }
207218 } else if len (password ) > 0 {
208219 // password is nil or empty, therefore no password is set
209220 // a password was given and the account has no password set, therefore access is denied
210- return nil , mysql . NewSQLError ( mysql . ERAccessDeniedError , mysql . SSAccessDeniedError , "Access denied for user '%v'" , user )
221+ return nil , newAccessDeniedError ( user )
211222 }
212223
213224 return sql.MysqlConnectionUser {User : userEntry .User , Host : userEntry .Host }, nil
@@ -269,8 +280,7 @@ func (f extendedAuthPlainTextStorage) UserEntryWithPassword(conn *mysql.Conn, us
269280 "Access denied for user '%v': %v" , user , err )
270281 }
271282 if ! authed {
272- return nil , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError ,
273- "Access denied for user '%v'" , user )
283+ return nil , newAccessDeniedError (user )
274284 }
275285 return connUser , nil
276286}
@@ -329,7 +339,7 @@ var _ mysql.HashStorage = (*nativePasswordHashStorage)(nil)
329339
330340// UserEntryWithHash implements the mysql.HashStorage interface. This implementation is called by the MySQL
331341// 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 ) {
342+ func (nphs * nativePasswordHashStorage ) UserEntryWithHash (conn * mysql.Conn , salt []byte , user string , authResponse []byte , remoteAddr net.Addr ) (mysql.Getter , error ) {
333343 db := nphs .db
334344
335345 host , err := extractHostAddress (remoteAddr )
@@ -346,21 +356,83 @@ func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ *mysql.Conn, salt []b
346356
347357 userEntry := db .GetUser (rd , user , host , false )
348358 if userEntry == nil || userEntry .Locked {
349- return nil , mysql . NewSQLError ( mysql . ERAccessDeniedError , mysql . SSAccessDeniedError , "Access denied for user '%v'" , user )
359+ return nil , newAccessDeniedError ( user )
350360 }
361+
362+ // validate any extra connection security requirements, such as SSL or a client cert
363+ if err = validateConnectionSecurity (userEntry , conn ); err != nil {
364+ return nil , err
365+ }
366+
351367 if len (userEntry .AuthString ) > 0 {
352368 if ! validateMysqlNativePassword (authResponse , salt , userEntry .AuthString ) {
353- return nil , mysql . NewSQLError ( mysql . ERAccessDeniedError , mysql . SSAccessDeniedError , "Access denied for user '%v'" , user )
369+ return nil , newAccessDeniedError ( user )
354370 }
355371 } else if len (authResponse ) > 0 {
356372 // password is nil or empty, therefore no password is set
357373 // a password was given and the account has no password set, therefore access is denied
358- return nil , mysql . NewSQLError ( mysql . ERAccessDeniedError , mysql . SSAccessDeniedError , "Access denied for user '%v'" , user )
374+ return nil , newAccessDeniedError ( user )
359375 }
360376
361377 return sql.MysqlConnectionUser {User : userEntry .User , Host : userEntry .Host }, nil
362378}
363379
380+ // validateConnectionSecurity examines the security properties of |conn| (e.g. TLS,
381+ // selected cipher, X509 client certs) and validates specific connection properties
382+ // based on what |userEntry| has configured. An error is returned if any validation
383+ // issues were detected, otherwise nil is returned.
384+ func validateConnectionSecurity (userEntry * User , conn * mysql.Conn ) error {
385+ switch userEntry .SslType {
386+ case "" :
387+ // No connection security validation needed
388+ return nil
389+ case "ANY" :
390+ // ANY indicates that we need any form of secure socket
391+ if ! conn .TLSEnabled () {
392+ return newAccessDeniedError (userEntry .User )
393+ }
394+ case "X509" :
395+ // X509 means that a valid X509 client certificate is required
396+ // NOTE: cert validation (e.g. expiration date, CA chain) is handled
397+ // in the Go networking stack, so long as tls.VerifyClientCertIfGiven
398+ // is specified in the TLS configuration for the server.
399+ clientCerts := conn .GetTLSClientCerts ()
400+ if len (clientCerts ) == 0 {
401+ return newAccessDeniedError (userEntry .User )
402+ }
403+ // TODO: Do we need to do anything if there are multiple client certs provided?
404+ case "SPECIFIED" :
405+ // Specified means that we have additional requirements on either the SSL cipher
406+ // or the X509 cert, so we need to perform additional validation checks.
407+ if userEntry .SslCipher != "" {
408+ if ! conn .TLSEnabled () {
409+ return newAccessDeniedError (userEntry .User )
410+ }
411+ // TODO: validate the ssl cipher
412+ // TODO: How do we get the SSL cipher in use? What are valid values you can specify in MySQL?
413+ return fmt .Errorf ("SSL cipher validation not supported yet" )
414+ }
415+ if userEntry .X509Issuer != "" {
416+ if len (conn .GetTLSClientCerts ()) == 0 {
417+ return newAccessDeniedError (userEntry .User )
418+ }
419+ // TODO: Validate the client cert issuer
420+ return fmt .Errorf ("X509 issuer validation not supported yet" )
421+ }
422+ if userEntry .X509Subject != "" {
423+ if len (conn .GetTLSClientCerts ()) == 0 {
424+ return newAccessDeniedError (userEntry .User )
425+ }
426+ // TODO: Validate the client cert subject
427+ return fmt .Errorf ("X509 subject validation not supported yet" )
428+ }
429+ default :
430+ return fmt .Errorf ("unsupported ssl_type: %v" , userEntry .SslType )
431+ }
432+
433+ return nil
434+ }
435+
364436// userValidator implements the mysql.UserValidator interface. It looks up a user and host from the
365437// associated mysql database (|db|) and validates that a user entry exists and that it is configured
366438// for the specified authentication plugin (|authMethod|).
@@ -408,7 +480,13 @@ func (uv *userValidator) HandleUser(user string, remoteAddr net.Addr) bool {
408480 }
409481 userEntry := db .GetUser (rd , user , host , false )
410482
411- return userEntry != nil && userEntry .Plugin == string (uv .authMethod )
483+ // If we don't find a matching user, or we find one, but it's for a different auth method,
484+ // then return false to indicate this auth method can't handle that user.
485+ if userEntry == nil || userEntry .Plugin != string (uv .authMethod ) {
486+ return false
487+ }
488+
489+ return true
412490}
413491
414492// extractHostAddress extracts the host address from |addr|, checking to see if it is a unix socket, and if
@@ -429,6 +507,14 @@ func extractHostAddress(addr net.Addr) (host string, err error) {
429507 return host , nil
430508}
431509
510+ // newAccessDeniedError returns an "access denied" error, including the |userName| trying to authenticate,
511+ // matching MySQL's error message. Note that MySQL tends to return a generic "access denied" error message
512+ // for authentication failures, without leaking more details about why so that attackers can't exploit that
513+ // information to determine how a user is configured for authentication.
514+ func newAccessDeniedError (userName string ) error {
515+ return mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , userName )
516+ }
517+
432518// validateMysqlNativePassword was taken from vitess and validates the password hash for the mysql_native_password
433519// auth protocol. Note that this implementation has diverged slightly from the original code in Vitess.
434520func validateMysqlNativePassword (authResponse , salt []byte , mysqlNativePassword string ) bool {
0 commit comments