From e4ea5114cb0c0a8ada5cf1cb15991f48c05f35c5 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 3 Dec 2023 16:18:00 +0100 Subject: [PATCH] Add client_ed25519 authentication - Implements the necessary client code for [ed25519 authentication](https://mariadb.com/kb/en/authentication-plugin-ed25519/). - Add a test directly from the reference implementation to verify it works. - A continuation of https://github.com/go-sql-driver/mysql/pull/1220, but doesn't use CGO. - This patch uses filippo.io/edwards25519 to implement the crypto bits. The standard library `crypto/ed25519` cannot be used as MariaDB chose a scheme that is simply not compatible with what the standard library provides. --- AUTHORS | 1 + auth.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ auth_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 2 ++ go.sum | 2 ++ 5 files changed, 104 insertions(+) create mode 100644 go.sum diff --git a/AUTHORS b/AUTHORS index 2caa7d70..954e7ac7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -39,6 +39,7 @@ Evan Elias Evan Shaw Frederick Mayle Gustavo Kristic +Gusted Hajime Nakagami Hanno Braun Henri Yandell diff --git a/auth.go b/auth.go index bab282bd..ff641577 100644 --- a/auth.go +++ b/auth.go @@ -13,10 +13,13 @@ import ( "crypto/rsa" "crypto/sha1" "crypto/sha256" + "crypto/sha512" "crypto/x509" "encoding/pem" "fmt" "sync" + + "filippo.io/edwards25519" ) // server pub keys registry @@ -225,6 +228,44 @@ func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte, return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil) } +// Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c +func doEd25519Auth(scramble []byte, password string) ([]byte, error) { + h := sha512.Sum512([]byte(password)) + + s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) + if err != nil { + return nil, err + } + + nonceHash := sha512.New() + nonceHash.Write(h[32:]) + nonceHash.Write(scramble) + nonce := nonceHash.Sum(nil) + + r, err := edwards25519.NewScalar().SetUniformBytes(nonce) + if err != nil { + return nil, err + } + R := (&edwards25519.Point{}).ScalarBaseMult(r) + + A := (&edwards25519.Point{}).ScalarBaseMult(s) + + kHash := sha512.New() + kHash.Write(R.Bytes()) + kHash.Write(A.Bytes()) + kHash.Write(scramble) + k := kHash.Sum(nil) + + K, err := edwards25519.NewScalar().SetUniformBytes(k) + if err != nil { + return nil, err + } + + S := K.MultiplyAdd(K, s, r) + + return append(R.Bytes(), S.Bytes()...), nil +} + func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error { enc, err := encryptPassword(mc.cfg.Passwd, seed, pub) if err != nil { @@ -290,6 +331,13 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) { enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey) return enc, err + case "client_ed25519": + if len(authData) != 32 { + return nil, ErrMalformPkt + } + + return doEd25519Auth(authData, mc.cfg.Passwd) + default: mc.cfg.Logger.Print("unknown auth plugin:", plugin) return nil, ErrUnknownPlugin diff --git a/auth_test.go b/auth_test.go index 3ce0ea6e..8caed1ff 100644 --- a/auth_test.go +++ b/auth_test.go @@ -1328,3 +1328,54 @@ func TestAuthSwitchSHA256PasswordSecure(t *testing.T) { t.Errorf("got unexpected data: %v", conn.written) } } + +// Derived from https://github.com/MariaDB/server/blob/6b2287fff23fbdc362499501c562f01d0d2db52e/plugin/auth_ed25519/ed25519-t.c +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 + authResp, err := mc.auth(authData, plugin) + if err != nil { + t.Fatal(err) + } + err = mc.writeHandshakeResponsePacket(authResp, plugin) + if err != nil { + t.Fatal(err) + } + + // check written auth response + authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1 + authRespEnd := authRespStart + 1 + len(authResp) + writtenAuthRespLen := conn.written[authRespStart] + writtenAuthResp := conn.written[authRespStart+1 : authRespEnd] + 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 writtenAuthRespLen != 64 { + t.Fatalf("expected 64 bytes from client, got %d", writtenAuthRespLen) + } + if !bytes.Equal(writtenAuthResp, expectedAuthResp) { + t.Fatalf("auth response did not match expected value:\n%v\n%v", writtenAuthResp, 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(authData, plugin); err != nil { + t.Errorf("got error: %v", err) + } +} diff --git a/go.mod b/go.mod index 77bbb8db..ac5c9b62 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/go-sql-driver/mysql go 1.18 + +require filippo.io/edwards25519 v1.0.0 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..67f27131 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +filippo.io/edwards25519 v1.0.0 h1:0wAIcmJUqRdI8IJ/3eGi5/HwXZWPujYXXlkrQogz0Ek= +filippo.io/edwards25519 v1.0.0/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=