From 68789d14bbc60dab71687aca913cd2048a941372 Mon Sep 17 00:00:00 2001 From: Gusted Date: Sun, 3 Dec 2023 16:18:00 +0100 Subject: [PATCH 1/4] 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 2caa7d706..954e7ac7a 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 bab282bd2..ff6415776 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 3ce0ea6e0..8caed1fff 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 77bbb8dbf..4629714c0 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.1.0 diff --git a/go.sum b/go.sum new file mode 100644 index 000000000..359ca94b4 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= From ee271ad36a5930614ff2af3d4526ee2e1f0ed433 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Mon, 11 Dec 2023 22:59:28 +0900 Subject: [PATCH 2/4] code style changes --- auth.go | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/auth.go b/auth.go index ff6415776..658259b24 100644 --- a/auth.go +++ b/auth.go @@ -228,40 +228,40 @@ 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) { +// authEd25519 does ed25519 authentication used by MariaDB. +func authEd25519(scramble []byte, password string) ([]byte, error) { + // Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c + // Code style is from https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/ed25519/ed25519.go;l=207 h := sha512.Sum512([]byte(password)) s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32]) if err != nil { return nil, err } + A := (&edwards25519.Point{}).ScalarBaseMult(s) - nonceHash := sha512.New() - nonceHash.Write(h[32:]) - nonceHash.Write(scramble) - nonce := nonceHash.Sum(nil) - - r, err := edwards25519.NewScalar().SetUniformBytes(nonce) + mh := sha512.New() + mh.Write(h[32:]) + mh.Write(scramble) + messageDigest := mh.Sum(nil) + r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest) 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) + R := (&edwards25519.Point{}).ScalarBaseMult(r) - K, err := edwards25519.NewScalar().SetUniformBytes(k) + kh := sha512.New() + kh.Write(R.Bytes()) + kh.Write(A.Bytes()) + kh.Write(scramble) + hramDigest := kh.Sum(nil) + k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest) if err != nil { return nil, err } - S := K.MultiplyAdd(K, s, r) + S := k.MultiplyAdd(k, s, r) return append(R.Bytes(), S.Bytes()...), nil } @@ -335,8 +335,7 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) { if len(authData) != 32 { return nil, ErrMalformPkt } - - return doEd25519Auth(authData, mc.cfg.Passwd) + return authEd25519(authData, mc.cfg.Passwd) default: mc.cfg.Logger.Print("unknown auth plugin:", plugin) From a5608432216fa11087a7a3b07e40a0cf8711c142 Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 12 Dec 2023 15:44:21 +0900 Subject: [PATCH 3/4] fix fragile test --- driver_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/driver_test.go b/driver_test.go index 87892a09a..ade7772b4 100644 --- a/driver_test.go +++ b/driver_test.go @@ -165,14 +165,14 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) { for _, test := range tests { t.Run("default", func(t *testing.T) { dbt := &DBTest{t, db} + defer dbt.db.Exec("DROP TABLE IF EXISTS test") test(dbt) - dbt.db.Exec("DROP TABLE IF EXISTS test") }) if db2 != nil { t.Run("interpolateParams", func(t *testing.T) { dbt2 := &DBTest{t, db2} + defer dbt2.db.Exec("DROP TABLE IF EXISTS test") test(dbt2) - dbt2.db.Exec("DROP TABLE IF EXISTS test") }) } } From ae976ad21610162fd88a0642ab1777c9cb6d518b Mon Sep 17 00:00:00 2001 From: Inada Naoki Date: Tue, 12 Dec 2023 16:25:04 +0900 Subject: [PATCH 4/4] fix TestRawBytesAreNotModified called parent t.Fatal() --- driver_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/driver_test.go b/driver_test.go index ade7772b4..97fd5a17a 100644 --- a/driver_test.go +++ b/driver_test.go @@ -3181,14 +3181,14 @@ func TestRawBytesAreNotModified(t *testing.T) { rows, err := dbt.db.QueryContext(ctx, `SELECT id, value FROM test`) if err != nil { - t.Fatal(err) + dbt.Fatal(err) } var b int var raw sql.RawBytes for rows.Next() { if err := rows.Scan(&b, &raw); err != nil { - t.Fatal(err) + dbt.Fatal(err) } before := string(raw) @@ -3198,7 +3198,7 @@ func TestRawBytesAreNotModified(t *testing.T) { after := string(raw) if before != after { - t.Fatalf("the backing storage for sql.RawBytes has been modified (i=%v)", i) + dbt.Fatalf("the backing storage for sql.RawBytes has been modified (i=%v)", i) } } rows.Close()