Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,8 @@ Custom plugins implement the two methods of `AuthPlugin`, `InitAuth` and `Contin

The driver passes the connection's `context.Context` for cancellation and an `AuthContext` snapshot containing the selected credentials, established transport information, and configured RSA public key. Plugins do not receive or modify the connection's `Config`; the driver checks authentication-method permissions before starting a plugin. Custom plugins that require a particular transport must check it in `InitAuth` before returning credentials.

`InitAuth` receives the server challenge exactly as sent, including any trailing NUL byte; plugins interpret it themselves.

Comment on lines +675 to +676

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this too.

`AuthContext.TLS()` reports a completed MySQL TLS handshake, not merely a TLS configuration or an external tunnel. A continuation returning `nil, nil` waits for another server packet without sending; a non-nil empty slice sends an empty packet. Authentication succeeds only when the driver receives the server's OK packet.

### `LOAD DATA LOCAL INFILE` support
Expand Down
3 changes: 0 additions & 3 deletions auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,6 @@ func (mc *mysqlConn) parseAuthSwitchData(data []byte, initialSeed []byte) (strin

plugin := string(data[1:pluginEndIndex])
authData := data[pluginEndIndex+1:]
if len(authData) > 0 && authData[len(authData)-1] == 0 {
authData = authData[:len(authData)-1]
}

savedAuthData := make([]byte, len(authData))
copy(savedAuthData, authData)
Expand Down
5 changes: 4 additions & 1 deletion auth_caching_sha2.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,10 @@ func init() {
// 3. XOR(SHA256(password), SHA256(SHA256(SHA256(password)), scramble))
func (p *cachingSha2PasswordPlugin) InitAuth(ctx context.Context, authData []byte, auth *AuthContext) ([]byte, error) {
p.state = cachingSha2Initial
return scrambleSHA256Password(authData, auth.Password()), nil
if len(authData) < 20 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(authData) < 20 {
// Older MySQL authentication methods append a NUL byte to the auth data,
// and `caching_sha2_password` follows the same convention.
if len(authData) < 20 or len(authData) > 21 {

return nil, ErrMalformPkt
}
return scrambleSHA256Password(authData[:20], auth.Password()), nil
}

// ContinuationAuth processes the server's response to our authentication attempt.
Expand Down
5 changes: 3 additions & 2 deletions auth_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ import (
// while a method is running. Plugins should use it to cancel blocking work.
type AuthPlugin interface {
// InitAuth initializes the authentication process and returns the initial response.
// authData is the challenge data from the server. It must not be modified
// and must be copied if retained after the call.
// authData is the challenge data exactly as sent by the server, including
// any trailing NUL. It must not be modified and must be copied if retained
// after the call.
Comment on lines -22 to +24

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Explaining the NUL byte here won't make sense to readers who aren't already aware of this issue. Please revert this change.

// auth is the snapshot for this exchange, shared with ContinuationAuth.
// A nil or empty response sends an empty authentication response.
// Plugins must check any transport requirements here before returning credentials.
Expand Down
4 changes: 2 additions & 2 deletions auth_plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -273,14 +273,14 @@ func TestAuthSwitchContextSnapshot(t *testing.T) {
registerTestAuthPlugin(t, name, func() AuthPlugin {
return &authTestPlugin{
init: func(gotCtx context.Context, seed []byte, gotAuth *AuthContext) ([]byte, error) {
if gotCtx != ctx || gotAuth == auth || gotAuth.User() != "user" || gotAuth.Password() != "selected-password" || !gotAuth.TLS() || string(seed) != "challenge" {
if gotCtx != ctx || gotAuth == auth || gotAuth.User() != "user" || gotAuth.Password() != "selected-password" || !gotAuth.TLS() || string(seed) != "challenge\x00" {
return nil, errors.New("auth switch did not preserve the authentication snapshot")
}
switchedAuth = gotAuth
return []byte{42}, nil
},
next: func(gotCtx context.Context, packet, seed []byte, gotAuth *AuthContext) ([]byte, error) {
if gotCtx != ctx || gotAuth != switchedAuth || string(seed) != "challenge" {
if gotCtx != ctx || gotAuth != switchedAuth || string(seed) != "challenge\x00" {
return nil, errors.New("continuation did not retain the switched context")
}
continued = true
Expand Down
2 changes: 1 addition & 1 deletion auth_security_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ func TestAuthSwitchRejectsShortChallenge(t *testing.T) {
payload := append([]byte{iEOF}, tc.plugin...)
payload = append(payload, 0)
payload = append(payload, bytes.Repeat([]byte{'x'}, tc.length)...)
conn.data = makePacket(2, append(payload, 0))
conn.data = makePacket(2, payload)
conn.maxReads = 1
err := mc.handleAuthResult(t.Context(), []byte("0123456789abcdefghij"), &nativePasswordPlugin{}, newAuthContext(mc.cfg, mc.cfg.Passwd, false))
if !errors.Is(err, ErrMalformPkt) || conn.writes != 0 {
Expand Down
7 changes: 4 additions & 3 deletions auth_sha256.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,17 +117,18 @@ func (p *sha256PasswordPlugin) ContinuationAuth(ctx context.Context, packet, aut
// encryptPassword encrypts the password using RSA-OAEP with SHA1 hash.
//
// The process:
// 1. XORs the password with the auth seed to prevent replay attacks
// 1. XORs the password with the 20-byte scramble to prevent replay attacks
// 2. Encrypts the XORed password using RSA-OAEP with SHA1
//
// The encryption uses OAEP padding which is more secure than PKCS#1 v1.5 padding.
func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, error) {
if pub == nil {
return nil, fmt.Errorf("public key is nil")
}
if len(seed) == 0 {
return nil, fmt.Errorf("%w: empty auth seed", ErrMalformPkt)
if len(seed) < 20 {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if len(seed) < 20 {
// Older MySQL authentication methods append a NUL byte to the auth data,
// and `sha256_password` follows the same convention.
if len(seed) < 20 or len(seed) > 21 {

return nil, ErrMalformPkt
}
seed = seed[:20]

// Create the plaintext by XORing password with seed
plain := make([]byte, len(password)+1)
Expand Down
101 changes: 57 additions & 44 deletions auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1321,51 +1321,64 @@ func TestAuthSwitchSHA256PasswordSecure(t *testing.T) {
}
}

// Derived from https://github.com/MariaDB/server/blob/6b2287fff23fbdc362499501c562f01d0d2db52e/plugin/auth_ed25519/ed25519-t.c
// The first vector is derived from
// https://github.com/MariaDB/server/blob/6b2287fff23fbdc362499501c562f01d0d2db52e/plugin/auth_ed25519/ed25519-t.c
// The second one uses a nonce ending with 0x00, which the auth switch parser
// must not strip.
func TestEd25519Auth(t *testing.T) {
conn, mc := newRWMockConn(1)
mc.cfg.User = "root"
mc.cfg.Passwd = "foobar"

authData := []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
plugin := "client_ed25519"

// Send Client Authentication Packet
auth := newAuthContext(mc.cfg, mc.cfg.Passwd, false)
authPlugin, _ := globalPluginRegistry.GetPlugin(plugin)
authResp, err := authPlugin.InitAuth(context.Background(), authData, auth)
if err != nil {
t.Fatal(err)
}
err = mc.writeHandshakeResponsePacket(authResp, plugin)
if err != nil {
t.Fatal(err)
}

expectedAuthResp := []byte{
232, 61, 201, 63, 67, 63, 51, 53, 86, 73, 238, 35, 170, 117, 146,
214, 26, 17, 35, 9, 8, 132, 245, 141, 48, 99, 66, 58, 36, 228, 48,
84, 115, 254, 187, 168, 88, 162, 249, 57, 35, 85, 79, 238, 167, 106,
68, 117, 56, 135, 171, 47, 20, 14, 133, 79, 15, 229, 124, 160, 176,
100, 138, 14,
}
if len(authResp) != 64 {
t.Fatalf("expected 64 bytes from client, got %d", len(authResp))
}
if !bytes.Equal(authResp, expectedAuthResp) {
t.Fatalf("auth response did not match expected value:\n%v\n%v", authResp, expectedAuthResp)
}
conn.written = nil

// auth response
conn.data = []byte{
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
}
conn.maxReads = 1

// Handle response to auth packet
if err = mc.handleAuthResult(context.Background(), []byte{}, authPlugin, auth); err != nil {
t.Errorf("got error: %v", err)
for _, tc := range []struct {
name string
nonce []byte
signature []byte
}{
{
name: "nonce",
nonce: []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"),
signature: []byte{
232, 61, 201, 63, 67, 63, 51, 53, 86, 73, 238, 35, 170, 117, 146,
214, 26, 17, 35, 9, 8, 132, 245, 141, 48, 99, 66, 58, 36, 228, 48,
84, 115, 254, 187, 168, 88, 162, 249, 57, 35, 85, 79, 238, 167, 106,
68, 117, 56, 135, 171, 47, 20, 14, 133, 79, 15, 229, 124, 160, 176,
100, 138, 14,
},
},
{
name: "nonce ending with 0x00",
nonce: append([]byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"), 0),
signature: []byte{
96, 74, 190, 202, 77, 225, 185, 115, 213, 135, 190, 172, 250, 219,
249, 233, 156, 237, 180, 231, 14, 9, 197, 137, 203, 189, 212, 102,
147, 147, 132, 170, 249, 40, 142, 200, 35, 232, 195, 99, 207, 92, 96,
223, 252, 165, 109, 83, 127, 33, 227, 29, 169, 192, 249, 171, 239,
206, 228, 32, 153, 209, 214, 7,
},
},
} {
t.Run(tc.name, func(t *testing.T) {
conn, mc := newRWMockConn(2)
mc.cfg.User = "root"
mc.cfg.Passwd = "foobar"

// auth switch request: client_ed25519 with a 32-byte nonce
payload := append([]byte{iEOF}, "client_ed25519"...)
payload = append(payload, 0)
payload = append(payload, tc.nonce...)
conn.data = makePacket(2, payload)

// auth response
conn.queuedReplies = [][]byte{{7, 0, 0, 4, 0, 0, 0, 2, 0, 0, 0}}
conn.maxReads = 2

auth := newAuthContext(mc.cfg, mc.cfg.Passwd, false)
if err := mc.handleAuthResult(context.Background(), nil, &nativePasswordPlugin{}, auth); err != nil {
t.Fatalf("got error: %v", err)
}

expectedReply := append([]byte{64, 0, 0, 3}, tc.signature...)
if !bytes.Equal(conn.written, expectedReply) {
t.Fatalf("auth response did not match expected value:\n%v\n%v", conn.written, expectedReply)
}
})
}
}

Expand Down
Loading