Skip to content

Commit fc589cb

Browse files
author
Gusted
authoredDec 12, 2023
Add client_ed25519 authentication (go-sql-driver#1518)
Implements the necessary client code for [ed25519 authentication](https://mariadb.com/kb/en/authentication-plugin-ed25519/). 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.
1 parent d9f4383 commit fc589cb

File tree

6 files changed

+108
-5
lines changed

6 files changed

+108
-5
lines changed
 

‎AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ Evan Elias <evan at skeema.net>
3939
Evan Shaw <evan at vendhq.com>
4040
Frederick Mayle <frederickmayle at gmail.com>
4141
Gustavo Kristic <gkristic at gmail.com>
42+
Gusted <postmaster at gusted.xyz>
4243
Hajime Nakagami <nakagami at gmail.com>
4344
Hanno Braun <mail at hannobraun.com>
4445
Henri Yandell <flamefew at gmail.com>

‎auth.go

+47
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,13 @@ import (
1313
"crypto/rsa"
1414
"crypto/sha1"
1515
"crypto/sha256"
16+
"crypto/sha512"
1617
"crypto/x509"
1718
"encoding/pem"
1819
"fmt"
1920
"sync"
21+
22+
"filippo.io/edwards25519"
2023
)
2124

2225
// server pub keys registry
@@ -225,6 +228,44 @@ func encryptPassword(password string, seed []byte, pub *rsa.PublicKey) ([]byte,
225228
return rsa.EncryptOAEP(sha1, rand.Reader, pub, plain, nil)
226229
}
227230

231+
// authEd25519 does ed25519 authentication used by MariaDB.
232+
func authEd25519(scramble []byte, password string) ([]byte, error) {
233+
// Derived from https://github.com/MariaDB/server/blob/d8e6bb00888b1f82c031938f4c8ac5d97f6874c3/plugin/auth_ed25519/ref10/sign.c
234+
// Code style is from https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/crypto/ed25519/ed25519.go;l=207
235+
h := sha512.Sum512([]byte(password))
236+
237+
s, err := edwards25519.NewScalar().SetBytesWithClamping(h[:32])
238+
if err != nil {
239+
return nil, err
240+
}
241+
A := (&edwards25519.Point{}).ScalarBaseMult(s)
242+
243+
mh := sha512.New()
244+
mh.Write(h[32:])
245+
mh.Write(scramble)
246+
messageDigest := mh.Sum(nil)
247+
r, err := edwards25519.NewScalar().SetUniformBytes(messageDigest)
248+
if err != nil {
249+
return nil, err
250+
}
251+
252+
R := (&edwards25519.Point{}).ScalarBaseMult(r)
253+
254+
kh := sha512.New()
255+
kh.Write(R.Bytes())
256+
kh.Write(A.Bytes())
257+
kh.Write(scramble)
258+
hramDigest := kh.Sum(nil)
259+
k, err := edwards25519.NewScalar().SetUniformBytes(hramDigest)
260+
if err != nil {
261+
return nil, err
262+
}
263+
264+
S := k.MultiplyAdd(k, s, r)
265+
266+
return append(R.Bytes(), S.Bytes()...), nil
267+
}
268+
228269
func (mc *mysqlConn) sendEncryptedPassword(seed []byte, pub *rsa.PublicKey) error {
229270
enc, err := encryptPassword(mc.cfg.Passwd, seed, pub)
230271
if err != nil {
@@ -290,6 +331,12 @@ func (mc *mysqlConn) auth(authData []byte, plugin string) ([]byte, error) {
290331
enc, err := encryptPassword(mc.cfg.Passwd, authData, pubKey)
291332
return enc, err
292333

334+
case "client_ed25519":
335+
if len(authData) != 32 {
336+
return nil, ErrMalformPkt
337+
}
338+
return authEd25519(authData, mc.cfg.Passwd)
339+
293340
default:
294341
mc.cfg.Logger.Print("unknown auth plugin:", plugin)
295342
return nil, ErrUnknownPlugin

‎auth_test.go

+51
Original file line numberDiff line numberDiff line change
@@ -1328,3 +1328,54 @@ func TestAuthSwitchSHA256PasswordSecure(t *testing.T) {
13281328
t.Errorf("got unexpected data: %v", conn.written)
13291329
}
13301330
}
1331+
1332+
// Derived from https://github.com/MariaDB/server/blob/6b2287fff23fbdc362499501c562f01d0d2db52e/plugin/auth_ed25519/ed25519-t.c
1333+
func TestEd25519Auth(t *testing.T) {
1334+
conn, mc := newRWMockConn(1)
1335+
mc.cfg.User = "root"
1336+
mc.cfg.Passwd = "foobar"
1337+
1338+
authData := []byte("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")
1339+
plugin := "client_ed25519"
1340+
1341+
// Send Client Authentication Packet
1342+
authResp, err := mc.auth(authData, plugin)
1343+
if err != nil {
1344+
t.Fatal(err)
1345+
}
1346+
err = mc.writeHandshakeResponsePacket(authResp, plugin)
1347+
if err != nil {
1348+
t.Fatal(err)
1349+
}
1350+
1351+
// check written auth response
1352+
authRespStart := 4 + 4 + 4 + 1 + 23 + len(mc.cfg.User) + 1
1353+
authRespEnd := authRespStart + 1 + len(authResp)
1354+
writtenAuthRespLen := conn.written[authRespStart]
1355+
writtenAuthResp := conn.written[authRespStart+1 : authRespEnd]
1356+
expectedAuthResp := []byte{
1357+
232, 61, 201, 63, 67, 63, 51, 53, 86, 73, 238, 35, 170, 117, 146,
1358+
214, 26, 17, 35, 9, 8, 132, 245, 141, 48, 99, 66, 58, 36, 228, 48,
1359+
84, 115, 254, 187, 168, 88, 162, 249, 57, 35, 85, 79, 238, 167, 106,
1360+
68, 117, 56, 135, 171, 47, 20, 14, 133, 79, 15, 229, 124, 160, 176,
1361+
100, 138, 14,
1362+
}
1363+
if writtenAuthRespLen != 64 {
1364+
t.Fatalf("expected 64 bytes from client, got %d", writtenAuthRespLen)
1365+
}
1366+
if !bytes.Equal(writtenAuthResp, expectedAuthResp) {
1367+
t.Fatalf("auth response did not match expected value:\n%v\n%v", writtenAuthResp, expectedAuthResp)
1368+
}
1369+
conn.written = nil
1370+
1371+
// auth response
1372+
conn.data = []byte{
1373+
7, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, // OK
1374+
}
1375+
conn.maxReads = 1
1376+
1377+
// Handle response to auth packet
1378+
if err := mc.handleAuthResult(authData, plugin); err != nil {
1379+
t.Errorf("got error: %v", err)
1380+
}
1381+
}

‎driver_test.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -165,14 +165,14 @@ func runTests(t *testing.T, dsn string, tests ...func(dbt *DBTest)) {
165165
for _, test := range tests {
166166
t.Run("default", func(t *testing.T) {
167167
dbt := &DBTest{t, db}
168+
defer dbt.db.Exec("DROP TABLE IF EXISTS test")
168169
test(dbt)
169-
dbt.db.Exec("DROP TABLE IF EXISTS test")
170170
})
171171
if db2 != nil {
172172
t.Run("interpolateParams", func(t *testing.T) {
173173
dbt2 := &DBTest{t, db2}
174+
defer dbt2.db.Exec("DROP TABLE IF EXISTS test")
174175
test(dbt2)
175-
dbt2.db.Exec("DROP TABLE IF EXISTS test")
176176
})
177177
}
178178
}
@@ -3181,14 +3181,14 @@ func TestRawBytesAreNotModified(t *testing.T) {
31813181

31823182
rows, err := dbt.db.QueryContext(ctx, `SELECT id, value FROM test`)
31833183
if err != nil {
3184-
t.Fatal(err)
3184+
dbt.Fatal(err)
31853185
}
31863186

31873187
var b int
31883188
var raw sql.RawBytes
31893189
for rows.Next() {
31903190
if err := rows.Scan(&b, &raw); err != nil {
3191-
t.Fatal(err)
3191+
dbt.Fatal(err)
31923192
}
31933193

31943194
before := string(raw)
@@ -3198,7 +3198,7 @@ func TestRawBytesAreNotModified(t *testing.T) {
31983198
after := string(raw)
31993199

32003200
if before != after {
3201-
t.Fatalf("the backing storage for sql.RawBytes has been modified (i=%v)", i)
3201+
dbt.Fatalf("the backing storage for sql.RawBytes has been modified (i=%v)", i)
32023202
}
32033203
}
32043204
rows.Close()

‎go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
11
module github.com/go-sql-driver/mysql
22

33
go 1.18
4+
5+
require filippo.io/edwards25519 v1.1.0

‎go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
2+
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=

0 commit comments

Comments
 (0)
Please sign in to comment.