Skip to content

Commit 16344ed

Browse files
authored
Merge pull request #3298 from dolthub/fulghum/mutual_tls
Validate connection security properties
2 parents b3e1e88 + 41c8c33 commit 16344ed

File tree

1 file changed

+129
-13
lines changed

1 file changed

+129
-13
lines changed

sql/mysql_db/auth.go

Lines changed: 129 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@ package mysql_db
1717
import (
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.
434550
func validateMysqlNativePassword(authResponse, salt []byte, mysqlNativePassword string) bool {

0 commit comments

Comments
 (0)