@@ -17,8 +17,12 @@ package mysql_db
1717import (
1818 "bytes"
1919 "crypto/sha1"
20+ "crypto/tls"
21+ "crypto/x509/pkix"
2022 "encoding/hex"
23+ "fmt"
2124 "net"
25+ "strings"
2226
2327 "github.com/dolthub/vitess/go/mysql"
2428 "github.com/sirupsen/logrus"
@@ -107,7 +111,7 @@ var _ mysql.CachingStorage = (*noopCachingStorage)(nil)
107111//
108112// This implementation also handles authentication when a client doesn't send an auth response and
109113// 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 ) {
114+ func (n noopCachingStorage ) UserEntryWithCacheHash (conn * mysql.Conn , _ []byte , user string , authResponse []byte , remoteAddr net.Addr ) (mysql.Getter , mysql.CacheState , error ) {
111115 db := n .db
112116
113117 // If there is no mysql database of user info, then don't approve or reject, since we can't look at
@@ -131,7 +135,12 @@ func (n noopCachingStorage) UserEntryWithCacheHash(_ *mysql.Conn, _ []byte, user
131135
132136 userEntry := db .GetUser (rd , user , host , false )
133137 if userEntry == nil || userEntry .Locked {
134- return nil , mysql .AuthRejected , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
138+ return nil , mysql .AuthRejected , newAccessDeniedError (user )
139+ }
140+
141+ // validate any extra connection security requirements, such as SSL or a client cert
142+ if err = validateConnectionSecurity (userEntry , conn ); err != nil {
143+ return nil , mysql .AuthRejected , err
135144 }
136145
137146 if userEntry .AuthString == "" {
@@ -166,7 +175,7 @@ var _ mysql.PlainTextStorage = (*sha2PlainTextStorage)(nil)
166175
167176// UserEntryWithPassword implements the mysql.PlainTextStorage interface.
168177// 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 ) {
178+ func (s sha2PlainTextStorage ) UserEntryWithPassword (conn * mysql.Conn , user string , password string , remoteAddr net.Addr ) (mysql.Getter , error ) {
170179 db := s .db
171180
172181 host , err := extractHostAddress (remoteAddr )
@@ -183,7 +192,12 @@ func (s sha2PlainTextStorage) UserEntryWithPassword(_ *mysql.Conn, user string,
183192
184193 userEntry := db .GetUser (rd , user , host , false )
185194 if userEntry == nil || userEntry .Locked {
186- return nil , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
195+ return nil , newAccessDeniedError (userEntry .User )
196+ }
197+
198+ // validate any extra connection security requirements, such as SSL or a client cert
199+ if err = validateConnectionSecurity (userEntry , conn ); err != nil {
200+ return nil , err
187201 }
188202
189203 if len (userEntry .AuthString ) > 0 {
@@ -202,12 +216,12 @@ func (s sha2PlainTextStorage) UserEntryWithPassword(_ *mysql.Conn, user string,
202216 }
203217
204218 if userEntry .AuthString != string (authString ) {
205- return nil , mysql . NewSQLError ( mysql . ERAccessDeniedError , mysql . SSAccessDeniedError , "Access denied for user '%v'" , user )
219+ return nil , newAccessDeniedError ( user )
206220 }
207221 } else if len (password ) > 0 {
208222 // password is nil or empty, therefore no password is set
209223 // 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 )
224+ return nil , newAccessDeniedError ( user )
211225 }
212226
213227 return sql.MysqlConnectionUser {User : userEntry .User , Host : userEntry .Host }, nil
@@ -269,8 +283,7 @@ func (f extendedAuthPlainTextStorage) UserEntryWithPassword(conn *mysql.Conn, us
269283 "Access denied for user '%v': %v" , user , err )
270284 }
271285 if ! authed {
272- return nil , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError ,
273- "Access denied for user '%v'" , user )
286+ return nil , newAccessDeniedError (user )
274287 }
275288 return connUser , nil
276289}
@@ -329,7 +342,7 @@ var _ mysql.HashStorage = (*nativePasswordHashStorage)(nil)
329342
330343// UserEntryWithHash implements the mysql.HashStorage interface. This implementation is called by the MySQL
331344// 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 ) {
345+ func (nphs * nativePasswordHashStorage ) UserEntryWithHash (conn * mysql.Conn , salt []byte , user string , authResponse []byte , remoteAddr net.Addr ) (mysql.Getter , error ) {
333346 db := nphs .db
334347
335348 host , err := extractHostAddress (remoteAddr )
@@ -346,21 +359,94 @@ func (nphs *nativePasswordHashStorage) UserEntryWithHash(_ *mysql.Conn, salt []b
346359
347360 userEntry := db .GetUser (rd , user , host , false )
348361 if userEntry == nil || userEntry .Locked {
349- return nil , mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , user )
362+ return nil , newAccessDeniedError (user )
363+ }
364+
365+ // validate any extra connection security requirements, such as SSL or a client cert
366+ if err = validateConnectionSecurity (userEntry , conn ); err != nil {
367+ return nil , err
350368 }
369+
351370 if len (userEntry .AuthString ) > 0 {
352371 if ! validateMysqlNativePassword (authResponse , salt , userEntry .AuthString ) {
353- return nil , mysql . NewSQLError ( mysql . ERAccessDeniedError , mysql . SSAccessDeniedError , "Access denied for user '%v'" , user )
372+ return nil , newAccessDeniedError ( user )
354373 }
355374 } else if len (authResponse ) > 0 {
356375 // password is nil or empty, therefore no password is set
357376 // 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 )
377+ return nil , newAccessDeniedError ( user )
359378 }
360379
361380 return sql.MysqlConnectionUser {User : userEntry .User , Host : userEntry .Host }, nil
362381}
363382
383+ // validateConnectionSecurity examines the security properties of |conn| (e.g. TLS,
384+ // selected cipher, X509 client certs) and validates specific connection properties
385+ // based on what |userEntry| has configured. An error is returned if any validation
386+ // issues were detected, otherwise nil is returned.
387+ func validateConnectionSecurity (userEntry * User , conn * mysql.Conn ) error {
388+ switch userEntry .SslType {
389+ case "" :
390+ // No connection security validation needed
391+ return nil
392+ case "ANY" :
393+ // ANY indicates that we need any form of secure socket
394+ if ! conn .TLSEnabled () {
395+ return newAccessDeniedError (userEntry .User )
396+ }
397+ case "X509" :
398+ // X509 means that a valid X509 client certificate is required
399+ // NOTE: cert validation (e.g. expiration date, CA chain) is handled
400+ // in the Go networking stack, so long as tls.VerifyClientCertIfGiven
401+ // is specified in the TLS configuration for the server.
402+ clientCerts := conn .GetTLSClientCerts ()
403+ if len (clientCerts ) == 0 {
404+ return newAccessDeniedError (userEntry .User )
405+ }
406+ case "SPECIFIED" :
407+ // Specified means that we have additional requirements on either the SSL cipher
408+ // or the X509 cert, so we need to perform additional validation checks.
409+ if ! conn .TLSEnabled () {
410+ return newAccessDeniedError (userEntry .User )
411+ }
412+ if userEntry .SslCipher != "" {
413+ tlsConn , ok := conn .Conn .(* tls.Conn )
414+ if ! ok {
415+ return newAccessDeniedError (userEntry .User )
416+ }
417+ state := tlsConn .ConnectionState ()
418+ cipherSuiteName := tls .CipherSuiteName (state .CipherSuite )
419+ if cipherSuiteName != userEntry .SslCipher {
420+ return newAccessDeniedError (userEntry .User )
421+ }
422+ }
423+ if userEntry .X509Issuer != "" {
424+ if len (conn .GetTLSClientCerts ()) == 0 {
425+ return newAccessDeniedError (userEntry .User )
426+ }
427+ clientCert := conn .GetTLSClientCerts ()[0 ]
428+ normalizedIssuer := formatDistinguishedNameForMySQL (clientCert .Issuer )
429+ if normalizedIssuer != userEntry .X509Issuer {
430+ return newAccessDeniedError (userEntry .User )
431+ }
432+ }
433+ if userEntry .X509Subject != "" {
434+ if len (conn .GetTLSClientCerts ()) == 0 {
435+ return newAccessDeniedError (userEntry .User )
436+ }
437+ clientCert := conn .GetTLSClientCerts ()[0 ]
438+ normalizedSubject := formatDistinguishedNameForMySQL (clientCert .Subject )
439+ if normalizedSubject != userEntry .X509Subject {
440+ return newAccessDeniedError (userEntry .User )
441+ }
442+ }
443+ default :
444+ return fmt .Errorf ("unsupported ssl_type: %v" , userEntry .SslType )
445+ }
446+
447+ return nil
448+ }
449+
364450// userValidator implements the mysql.UserValidator interface. It looks up a user and host from the
365451// associated mysql database (|db|) and validates that a user entry exists and that it is configured
366452// for the specified authentication plugin (|authMethod|).
@@ -408,7 +494,13 @@ func (uv *userValidator) HandleUser(user string, remoteAddr net.Addr) bool {
408494 }
409495 userEntry := db .GetUser (rd , user , host , false )
410496
411- return userEntry != nil && userEntry .Plugin == string (uv .authMethod )
497+ // If we don't find a matching user, or we find one, but it's for a different auth method,
498+ // then return false to indicate this auth method can't handle that user.
499+ if userEntry == nil || userEntry .Plugin != string (uv .authMethod ) {
500+ return false
501+ }
502+
503+ return true
412504}
413505
414506// extractHostAddress extracts the host address from |addr|, checking to see if it is a unix socket, and if
@@ -429,6 +521,30 @@ func extractHostAddress(addr net.Addr) (host string, err error) {
429521 return host , nil
430522}
431523
524+ // newAccessDeniedError returns an "access denied" error, including the |userName| trying to authenticate,
525+ // matching MySQL's error message. Note that MySQL tends to return a generic "access denied" error message
526+ // for authentication failures, without leaking more details about why so that attackers can't exploit that
527+ // information to determine how a user is configured for authentication.
528+ func newAccessDeniedError (userName string ) error {
529+ return mysql .NewSQLError (mysql .ERAccessDeniedError , mysql .SSAccessDeniedError , "Access denied for user '%v'" , userName )
530+ }
531+
532+ // formatDistinguishedNameForMySQL returns a distinguished name, created from |name|, that matches
533+ // MySQL's formatting style (e.g. "/C=US/ST=Washington/L=Seattle/O=Test CA/CN=MySQL Test CA").
534+ // By default, Golang's stack uses a different format when converting a pkix.Name to a string. This
535+ // function reverses the order of the elements and uses a "/" prefix for each element, instead of a
536+ // "," in between elements.
537+ func formatDistinguishedNameForMySQL (name pkix.Name ) string {
538+ parts := strings .Split (name .String (), "," )
539+
540+ b := strings.Builder {}
541+ for i := len (parts ) - 1 ; i >= 0 ; i -- {
542+ b .WriteString ("/" )
543+ b .WriteString (parts [i ])
544+ }
545+ return b .String ()
546+ }
547+
432548// validateMysqlNativePassword was taken from vitess and validates the password hash for the mysql_native_password
433549// auth protocol. Note that this implementation has diverged slightly from the original code in Vitess.
434550func validateMysqlNativePassword (authResponse , salt []byte , mysqlNativePassword string ) bool {
0 commit comments