Adding AuthServer support for dialog.

Mostly the refactoring of the AuthServer interface is in here. There is
no real test for the dialog plugin method.
This commit is contained in:
Alain Jobart 2017-03-29 11:38:49 -07:00
Родитель 26a4b33cab
Коммит d33b929e34
12 изменённых файлов: 219 добавлений и 139 удалений

Просмотреть файл

@ -3,8 +3,10 @@ package mysqlconn
import (
"crypto/rand"
"crypto/sha1"
"fmt"
log "github.com/golang/glog"
"github.com/youtube/vitess/go/sqldb"
)
// AuthServer is the interface that servers must implement to validate
@ -21,26 +23,40 @@ import (
// password is sent in the clear. That may not be suitable for some
// use cases.
type AuthServer interface {
// UseClearText returns true is Cleartext auth is used.
// - If it is not set, Salt() and ValidateHash() are called.
// The server starts up in mysql_native_password mode.
// (but ValidateClearText can also be called, if client
// switched to Cleartext).
// - If it is set, ValidateClearText() is called.
// The server starts up in mysql_clear_password mode.
UseClearText() bool
// AuthMethod returns the authentication method to use for the
// given user. If this returns MysqlNativePassword
// (mysql_native_password), then ValidateHash() will be
// called, and no further roundtrip with the client is
// expected. If anything else is returned, Negotiate()
// will be called on the connection, and the AuthServer
// needs to handle the packets.
AuthMethod(user string) (string, error)
// Salt returns the salt to use for a connection.
// It should be 20 bytes of data.
// Most implementations should just use mysqlconn.NewSalt().
// (this is meant to support a plugin that would use an
// existing MySQL server as the source of auth, and just forward
// the salt generated by that server).
// Do not return zero bytes, as a known salt can be the source
// of a crypto attack.
Salt() ([]byte, error)
// ValidateHash validates the data sent by the client matches
// what the server computes. It also returns the user data.
ValidateHash(salt []byte, user string, authResponse []byte) (Getter, error)
// ValidateClearText validates a user / password is correct.
// It also returns the user data.
ValidateClearText(user, password string) (Getter, error)
// Negotiate is called if AuthMethod returns anything else
// than MysqlNativePassword. It is handed the connection after the
// AuthSwitchRequest packet is sent.
// - If the negotiation fails, it should just return an error
// (should be a sqldb.SQLError if possible).
// The framework is responsible for writing the Error packet
// and closing the connection in that case.
// - If the negotiation works, it should return the Getter,
// and no error. The framework is responsible for writing the
// OK packet.
Negotiate(c *Conn, user string) (Getter, error)
}
// authServers is a registry of AuthServer implementations.
@ -63,8 +79,8 @@ func GetAuthServer(name string) AuthServer {
return authServer
}
// newSalt returns a 20 character salt.
func newSalt() ([]byte, error) {
// NewSalt returns a 20 character salt.
func NewSalt() ([]byte, error) {
salt := make([]byte, 20)
if _, err := rand.Read(salt); err != nil {
return nil, err
@ -109,3 +125,56 @@ func scramblePassword(salt, password []byte) []byte {
}
return scramble
}
// AuthServerWritePacketString is a helper method to write a null
// terminated string into a packet, mainly to be used with the dialog
// client plugin.
func AuthServerWritePacketString(c *Conn, message string) error {
data := c.startEphemeralPacket(len(message) + 1)
pos := writeNullString(data, 0, message)
if pos != len(data) {
return fmt.Errorf("error building AuthServerWritePacketString: got %v bytes expected %v", pos, len(data))
}
if err := c.writeEphemeralPacket(true); err != nil {
return sqldb.NewSQLError(CRServerGone, SSUnknownSQLState, err.Error())
}
return nil
}
// AuthServerReadPacketString is a helper method to read a packet
// as a null terminated string. It is used by the mysql_clear_password
// and dialog plugins.
func AuthServerReadPacketString(c *Conn) (string, error) {
// Read a packet, the password is the payload, as a
// zero terminated string.
data, err := c.ReadPacket()
if err != nil {
return "", err
}
if len(data) == 0 || data[len(data)-1] != 0 {
return "", fmt.Errorf("received invalid response packet")
}
return string(data[:len(data)-1]), nil
}
// AuthServerNegotiateClearOrDialog will finish a negotiation based on
// the method type for the connection. Only supports
// MysqlClearPassword and MysqlDialog.
func AuthServerNegotiateClearOrDialog(c *Conn, method string) (string, error) {
switch method {
case MysqlClearPassword:
// The password is the next packet in plain text.
return AuthServerReadPacketString(c)
case MysqlDialog:
// Send a question packet.
if err := AuthServerWritePacketString(c, "Enter Password:"); err != nil {
return "", err
}
// Read the password as the response.
return AuthServerReadPacketString(c)
default:
return "", fmt.Errorf("unrecognized method: %v", method)
}
}

Просмотреть файл

@ -8,18 +8,18 @@ import (
// It's meant to be used for testing and prototyping.
// With this config, you can connect to a local vtgate using
// the following command line: 'mysql -P port -h ::'.
type AuthServerNone struct {
ClearText bool
}
// It only uses MysqlNativePassword method.
type AuthServerNone struct{}
// UseClearText reports clear text status
func (a *AuthServerNone) UseClearText() bool {
return a.ClearText
// AuthMethod is part of the AuthServer interface.
// We always return MysqlNativePassword.
func (a *AuthServerNone) AuthMethod(user string) (string, error) {
return MysqlNativePassword, nil
}
// Salt makes salt
func (a *AuthServerNone) Salt() ([]byte, error) {
return make([]byte, 20), nil
return NewSalt()
}
// ValidateHash validates hash
@ -27,9 +27,10 @@ func (a *AuthServerNone) ValidateHash(salt []byte, user string, authResponse []b
return &NoneGetter{}, nil
}
// ValidateClearText validates clear text
func (a *AuthServerNone) ValidateClearText(user, password string) (Getter, error) {
return &NoneGetter{}, nil
// Negotiate is part of the AuthServer interface.
// It will never be called.
func (a *AuthServerNone) Negotiate(c *Conn, user string) (Getter, error) {
panic("Negotiate should not be called as AuthMethod returned mysql_native_password")
}
func init() {

Просмотреть файл

@ -13,14 +13,18 @@ import (
)
var (
mysqlAuthServerStaticFile = flag.String("mysql_auth_server_static_file", "", "JSON File to read the users/passwords from.")
mysqlAuthServerStaticString = flag.String("mysql_auth_server_static_string", "", "JSON representation of the users/passwords config.")
mysqlAuthServerStaticFile = flag.String("mysql_auth_server_static_file", "", "JSON File to read the users/passwords from.")
mysqlAuthServerStaticString = flag.String("mysql_auth_server_static_string", "", "JSON representation of the users/passwords config.")
)
// AuthServerStatic implements AuthServer using a static configuration.
type AuthServerStatic struct {
// ClearText can be set to force the use of ClearText auth.
ClearText bool
// Method can be set to:
// - MysqlNativePassword
// - MysqlClearPassword
// - MysqlDialog
// It defaults to MysqlNativePassword.
Method string
// Entries contains the users, passwords and user data.
Entries map[string]*AuthServerStaticEntry
@ -32,7 +36,7 @@ type AuthServerStaticEntry struct {
UserData string
}
// Init Handles initializing the AuthServerStatic if necessary.
// InitAuthServerStatic Handles initializing the AuthServerStatic if necessary.
func InitAuthServerStatic() {
// Check parameters.
if *mysqlAuthServerStaticFile == "" && *mysqlAuthServerStaticString == "" {
@ -52,8 +56,8 @@ func InitAuthServerStatic() {
// NewAuthServerStatic returns a new empty AuthServerStatic.
func NewAuthServerStatic() *AuthServerStatic {
return &AuthServerStatic{
ClearText: false,
Entries: make(map[string]*AuthServerStaticEntry),
Method: MysqlNativePassword,
Entries: make(map[string]*AuthServerStaticEntry),
}
}
@ -81,14 +85,14 @@ func RegisterAuthServerStaticFromParams(file, str string) {
RegisterAuthServerImpl("static", authServerStatic)
}
// UseClearText is part of the AuthServer interface.
func (a *AuthServerStatic) UseClearText() bool {
return a.ClearText
// AuthMethod is part of the AuthServer interface.
func (a *AuthServerStatic) AuthMethod(user string) (string, error) {
return a.Method, nil
}
// Salt is part of the AuthServer interface.
func (a *AuthServerStatic) Salt() ([]byte, error) {
return newSalt()
return NewSalt()
}
// ValidateHash is part of the AuthServer interface.
@ -108,8 +112,16 @@ func (a *AuthServerStatic) ValidateHash(salt []byte, user string, authResponse [
return &StaticUserData{entry.UserData}, nil
}
// ValidateClearText is part of the AuthServer interface.
func (a *AuthServerStatic) ValidateClearText(user, password string) (Getter, error) {
// Negotiate is part of the AuthServer interface.
// It will be called if Method is anything else than MysqlNativePassword.
// We only recognize MysqlClearPassword and MysqlDialog here.
func (a *AuthServerStatic) Negotiate(c *Conn, user string) (Getter, error) {
// Finish the negotiation.
password, err := AuthServerNegotiateClearOrDialog(c, a.Method)
if err != nil {
return nil, err
}
// Find the entry.
entry, ok := a.Entries[user]
if !ok {

Просмотреть файл

@ -262,7 +262,7 @@ func (c *Conn) clientHandshake(characterSet uint8, params *sqldb.ConnParams) err
if err != nil {
return sqldb.NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "cannot parse auth switch request: %v", err)
}
if pluginName != mysqlClearPassword {
if pluginName != MysqlClearPassword {
return sqldb.NewSQLError(CRServerHandshakeErr, SSUnknownSQLState, "server asked for unsupported auth method: %v", pluginName)
}
@ -437,8 +437,8 @@ func (c *Conn) parseInitialHandshakePacket(data []byte) (uint32, []byte, error)
authPluginName = string(data[pos : len(data)-1])
}
if authPluginName != mysqlNativePassword {
return 0, nil, sqldb.NewSQLError(CRMalformedPacket, SSUnknownSQLState, "parseInitialHandshakePacket: only support %v auth plugin name, but got %v", mysqlNativePassword, authPluginName)
if authPluginName != MysqlNativePassword {
return 0, nil, sqldb.NewSQLError(CRMalformedPacket, SSUnknownSQLState, "parseInitialHandshakePacket: only support %v auth plugin name, but got %v", MysqlNativePassword, authPluginName)
}
}
@ -572,7 +572,7 @@ func (c *Conn) writeHandshakeResponse41(capabilities uint32, salt []byte, charac
}
// Assume native client during response
pos = writeNullString(data, pos, mysqlNativePassword)
pos = writeNullString(data, pos, MysqlNativePassword)
// Sanity-check the length.
if pos != len(data) {

Просмотреть файл

@ -14,11 +14,15 @@ const (
// Supported auth forms.
const (
// mysqlNativePassword uses a salt and transmits a hash on the wire.
mysqlNativePassword = "mysql_native_password"
// MysqlNativePassword uses a salt and transmits a hash on the wire.
MysqlNativePassword = "mysql_native_password"
// mysqlClearPassword transmits the password in the clear.
mysqlClearPassword = "mysql_clear_password"
// MysqlClearPassword transmits the password in the clear.
MysqlClearPassword = "mysql_clear_password"
// MysqlDialog uses the dialog plugin on the client side.
// It transmits data in the clear.
MysqlDialog = "dialog"
)
// Capability flags.

Просмотреть файл

@ -46,17 +46,13 @@ Our client will expect the server to always use mysql_native_password
in its initial handshake. This is what a real server always does, even though
it's not technically mandatory.
Our server can then use the client's auth methods right away:
- mysql_native_password
- mysql_clear_password
If our server's AuthServer UseClearText() returns true, and the
client's auth method is not mysql_clear_password, we will
The server's AuthServer plugin method AuthMethod() will then return
what auth method the server wants to use. If it is
mysql_native_password, and the client already returned the data, we
use it. Otherwise we switch the auth to what the server wants (by
sending an Authentication Method Switch Request packet) and
re-negotiate.
If any of these methods doesn't work for the server, it will re-negotiate
by sending an Authentication Method Switch Request Packet.
The client will then handle that if it can.
--
Maximum Packet Size:

Просмотреть файл

@ -20,7 +20,11 @@ import (
func TestClearTextClientAuth(t *testing.T) {
th := &testHandler{}
authServer := &AuthServerNone{ClearText: true}
authServer := NewAuthServerStatic()
authServer.Method = MysqlClearPassword
authServer.Entries["user1"] = &AuthServerStaticEntry{
Password: "password1",
}
// Create the listener.
l, err := NewListener("tcp", ":0", authServer, th)
@ -75,7 +79,10 @@ func TestClearTextClientAuth(t *testing.T) {
func TestSSLConnection(t *testing.T) {
th := &testHandler{}
authServer := &AuthServerNone{ClearText: false}
authServer := NewAuthServerStatic()
authServer.Entries["user1"] = &AuthServerStaticEntry{
Password: "password1",
}
// Create the listener, so we can get its host.
l, err := NewListener("tcp", ":0", authServer, th)
@ -128,7 +135,7 @@ func TestSSLConnection(t *testing.T) {
// Make sure clear text auth works over SSL.
t.Run("ClearText", func(t *testing.T) {
authServer.ClearText = true
authServer.Method = MysqlClearPassword
testSSLConnectionClearText(t, params)
})
}

Просмотреть файл

@ -19,12 +19,14 @@ import (
var (
ldapAuthConfigFile = flag.String("mysql_ldap_auth_config_file", "", "JSON File from which to read LDAP server config.")
ldapAuthConfigString = flag.String("mysql_ldap_auth_config_string", "", "JSON representation of LDAP server config.")
ldapAuthMethod = flag.String("mysql_ldap_auth_method", mysqlconn.MysqlClearPassword, "client-side authentication method to use. Supported values: mysql_clear_password, dialog.")
)
// AuthServerLdap implements AuthServer with an LDAP backend
type AuthServerLdap struct {
Client
ServerConfig
Method string
User string
Password string
GroupQuery string
@ -42,7 +44,14 @@ func Init() {
log.Infof("Both mysql_ldap_auth_config_file and mysql_ldap_auth_config_string are non-empty, can only use one.")
return
}
ldapAuthServer := &AuthServerLdap{Client: &ClientImpl{}, ServerConfig: ServerConfig{}}
if *ldapAuthMethod != mysqlconn.MysqlClearPassword && *ldapAuthMethod != mysqlconn.MysqlDialog {
log.Fatalf("Invalid mysql_ldap_auth_method value: only support mysql_clear_password or dialog")
}
ldapAuthServer := &AuthServerLdap{
Client: &ClientImpl{},
ServerConfig: ServerConfig{},
Method: *ldapAuthMethod,
}
data := []byte(*ldapAuthConfigString)
if *ldapAuthConfigFile != "" {
@ -58,32 +67,37 @@ func Init() {
mysqlconn.RegisterAuthServerImpl("ldap", ldapAuthServer)
}
// UseClearText is always true for AuthServerLdap
func (asl *AuthServerLdap) UseClearText() bool {
return true
// AuthMethod is part of the AuthServer interface.
func (asl *AuthServerLdap) AuthMethod(user string) (string, error) {
return asl.Method, nil
}
// Salt is unimplemented for AuthServerLdap
// Salt will be unused in AuthServerLdap.
func (asl *AuthServerLdap) Salt() ([]byte, error) {
panic("unimplemented")
return mysqlconn.NewSalt()
}
// ValidateHash is unimplemented for AuthServerLdap
// ValidateHash is unimplemented for AuthServerLdap.
func (asl *AuthServerLdap) ValidateHash(salt []byte, user string, authResponse []byte) (mysqlconn.Getter, error) {
panic("unimplemented")
}
// ValidateClearText connects to the LDAP server over TLS
// and attempts to bind as that user with the supplied password.
// It returns the supplied username.
func (asl *AuthServerLdap) ValidateClearText(username, password string) (mysqlconn.Getter, error) {
err := asl.Client.Connect("tcp", &asl.ServerConfig)
// Negotiate is part of the AuthServer interface.
func (asl *AuthServerLdap) Negotiate(c *mysqlconn.Conn, user string) (mysqlconn.Getter, error) {
// Finish the negotiation.
password, err := mysqlconn.AuthServerNegotiateClearOrDialog(c, asl.Method)
if err != nil {
return nil, err
}
return asl.validate(user, password)
}
func (asl *AuthServerLdap) validate(username, password string) (mysqlconn.Getter, error) {
if err := asl.Client.Connect("tcp", &asl.ServerConfig); err != nil {
return nil, err
}
defer asl.Client.Close()
err = asl.Client.Bind(fmt.Sprintf(asl.UserDnPattern, username), password)
if err != nil {
if err := asl.Client.Bind(fmt.Sprintf(asl.UserDnPattern, username), password); err != nil {
return nil, err
}
groups, err := asl.getGroups(username)

Просмотреть файл

@ -22,13 +22,18 @@ func (mlc *MockLdapClient) Search(searchRequest *ldap.SearchRequest) (*ldap.Sear
}
func TestValidateClearText(t *testing.T) {
asl := &AuthServerLdap{Client: &MockLdapClient{}, UserDnPattern: "%s", User: "testuser", Password: "testpass"}
_, err := asl.ValidateClearText("testuser", "testpass")
asl := &AuthServerLdap{
Client: &MockLdapClient{},
User: "testuser",
Password: "testpass",
UserDnPattern: "%s",
}
_, err := asl.validate("testuser", "testpass")
if err != nil {
t.Fatalf("AuthServerLdap failed to validate valid credentials. Got: %v", err)
}
_, err = asl.ValidateClearText("invaliduser", "invalidpass")
_, err = asl.validate("invaliduser", "invalidpass")
if err == nil {
t.Fatalf("AuthServerLdap validated invalid credentials.")
}

Просмотреть файл

@ -128,7 +128,7 @@ func benchmarkOldParallelReads(b *testing.B, params sqldb.ConnParams, parallelCo
func BenchmarkParallelShortQueries(b *testing.B) {
th := &testHandler{}
authServer := &AuthServerNone{ClearText: false}
authServer := &AuthServerNone{}
l, err := NewListener("tcp", ":0", authServer, th)
if err != nil {

Просмотреть файл

@ -168,72 +168,53 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32) {
}
}
// See what method the client used.
renegotiateWithClearText := false
switch authMethod {
case mysqlNativePassword:
// This is what the server started with. Let's use it if we can.
if !l.authServer.UseClearText() {
userData, err := l.authServer.ValidateHash(salt, user, authResponse)
if err != nil {
c.writeErrorPacketFromError(err)
return
}
c.User = user
c.UserData = userData
// We're good.
break
}
// See what auth method the AuthServer wants to use for that user.
authServerMethod, err := l.authServer.AuthMethod(user)
if err != nil {
c.writeErrorPacketFromError(err)
return
}
// Our AuthServer cannot use mysql_native_password, it
// needs the real password. Let's request that.
renegotiateWithClearText = true
case mysqlClearPassword:
// Client sent us a clear text password. Let's use it if we can.
if !l.AllowClearTextWithoutTLS && c.Capabilities&CapabilityClientSSL == 0 {
c.writeErrorPacket(CRServerHandshakeErr, SSUnknownSQLState, "Cannot use clear text authentication over non-SSL connections.")
return
}
userData, err := l.authServer.ValidateClearText(user, string(authResponse))
// Compare with what the client sent back.
switch {
case authServerMethod == MysqlNativePassword && authMethod == MysqlNativePassword:
// Both server and client want to use MysqlNativePassword:
// the negotiation can be completed right away, using the
// ValidateHash() method.
userData, err := l.authServer.ValidateHash(salt, user, authResponse)
if err != nil {
c.writeErrorPacketFromError(err)
return
}
c.User = user
c.UserData = userData
break
default:
// Client decided to use something we don't understand.
// Let's try again with clear text password.
renegotiateWithClearText = true
}
// If we need to re-negotiate with clear text, do it.
if renegotiateWithClearText {
// Check error conditions.
case authServerMethod == MysqlNativePassword:
// The server really wants to use MysqlNativePassword,
// but the client returned a result for something else:
// not sure this can happen, so not supporting this now.
c.writeErrorPacket(CRServerHandshakeErr, SSUnknownSQLState, "Client asked for auth %v, but server wants auth mysql_native_password", authMethod)
return
default:
// The server wants to use something else, re-negotiate.
// The negotiation happens in clear text. Let's check we can.
if !l.AllowClearTextWithoutTLS && c.Capabilities&CapabilityClientSSL == 0 {
c.writeErrorPacket(CRServerHandshakeErr, SSUnknownSQLState, "Cannot use clear text authentication over non-SSL connections.")
return
}
if err := c.writeAuthSwitchRequest(mysqlClearPassword, nil); err != nil {
// Switch our auth method to what the server wants.
// Note: for now don't support the extra data.
if err := c.writeAuthSwitchRequest(authServerMethod, nil); err != nil {
log.Errorf("Error write auth switch packet for client %v: %v", c.ConnectionID, err)
return
}
// The client is supposed to just send the data in a single packet.
// It is a zero-terminated string.
data, err := c.readEphemeralPacket()
if err != nil {
log.Warningf("Error reading auth switch response packet from client %v: %v", c.ConnectionID, err)
return
}
password, pos, ok := readNullString(data, 0)
if !ok || pos != len(data) {
c.writeErrorPacket(CRServerHandshakeErr, SSUnknownSQLState, "Error parsing packet with password: %v", data)
return
}
userData, err := l.authServer.ValidateClearText(user, password)
// Then hand over the rest of the negotiation to the
// auth server.
userData, err := l.authServer.Negotiate(c, user)
if err != nil {
c.writeErrorPacketFromError(err)
return
@ -242,7 +223,7 @@ func (l *Listener) handle(conn net.Conn, connectionID uint32) {
c.UserData = userData
}
// Send an OK packet.
// Negotiation worked, send OK packet.
if err := c.writeOKPacket(0, 0, c.StatusFlags, 0); err != nil {
log.Errorf("Cannot write OK packet: %v", err)
return
@ -332,7 +313,7 @@ func (c *Conn) writeHandshakeV10(serverVersion string, authServer AuthServer, en
1 + // length of auth plugin data
10 + // reserved (0)
13 + // auth-plugin-data
lenNullString(mysqlNativePassword) // auth-plugin-name
lenNullString(MysqlNativePassword) // auth-plugin-name
data := c.startEphemeralPacket(length)
pos := 0
@ -346,17 +327,8 @@ func (c *Conn) writeHandshakeV10(serverVersion string, authServer AuthServer, en
// Add connectionID in.
pos = writeUint32(data, pos, c.ConnectionID)
// Generate the salt if needed, put 8 bytes in.
var salt []byte
var err error
if authServer.UseClearText() {
// salt will end up being unused, but we can't send
// just zero, as the client will still use it, and
// that may leak crypto information.
salt, err = newSalt()
} else {
salt, err = authServer.Salt()
}
// Generate the salt, put 8 bytes in.
salt, err := authServer.Salt()
if err != nil {
return nil, err
}
@ -391,7 +363,7 @@ func (c *Conn) writeHandshakeV10(serverVersion string, authServer AuthServer, en
pos++
// Copy authPluginName. We always start with mysql_native_password.
pos = writeNullString(data, pos, mysqlNativePassword)
pos = writeNullString(data, pos, MysqlNativePassword)
// Sanity check.
if pos != len(data) {
@ -504,7 +476,7 @@ func (l *Listener) parseClientHandshakePacket(c *Conn, firstTime bool, data []by
}
// authMethod (with default)
authMethod := mysqlNativePassword
authMethod := MysqlNativePassword
if clientFlags&CapabilityClientPluginAuth != 0 {
authMethod, pos, ok = readNullString(data, pos)
if !ok {

Просмотреть файл

@ -276,7 +276,7 @@ func TestClearTextServer(t *testing.T) {
Password: "password1",
UserData: "userData1",
}
authServer.ClearText = true
authServer.Method = MysqlClearPassword
l, err := NewListener("tcp", ":0", authServer, th)
if err != nil {
t.Fatalf("NewListener failed: %v", err)