Skip to content

Commit

Permalink
Add client_ed25519 authentication
Browse files Browse the repository at this point in the history
- 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 #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.
  • Loading branch information
Gusted committed Dec 3, 2023
1 parent 98d7289 commit 7f66ff9
Show file tree
Hide file tree
Showing 4 changed files with 103 additions and 0 deletions.
48 changes: 48 additions & 0 deletions auth.go
Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
51 changes: 51 additions & 0 deletions auth_test.go
Expand Up @@ -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)
}
}
2 changes: 2 additions & 0 deletions go.mod
@@ -1,3 +1,5 @@
module github.com/go-sql-driver/mysql

go 1.18

require filippo.io/edwards25519 v1.0.0
2 changes: 2 additions & 0 deletions 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=

0 comments on commit 7f66ff9

Please sign in to comment.