Repository navigation
Expand file tree
/
Copy pathauth.go
More file actions
254 lines (229 loc) · 7.08 KB
/
Copy pathauth.go
File metadata and controls
254 lines (229 loc) · 7.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
// Go MySQL Driver - A MySQL-Driver for Go's database/sql package
//
// Copyright 2018 The Go-MySQL-Driver Authors. All rights reserved.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package mysql
import (
"bytes"
"context"
"crypto/rsa"
"fmt"
"strings"
"sync"
)
// server pub keys registry
var (
serverPubKeyLock sync.RWMutex
serverPubKeyRegistry map[string]*rsa.PublicKey
)
// RegisterServerPubKey registers a server RSA public key which can be used to
// send data in a secure manner to the server without receiving the public key
// in a potentially insecure way from the server first.
// Registered keys can afterwards be used adding serverPubKey=<name> to the DSN.
//
// Note: The provided rsa.PublicKey instance is exclusively owned by the driver
// after registering it and may not be modified.
//
// data, err := os.ReadFile("mykey.pem")
// if err != nil {
// log.Fatal(err)
// }
//
// block, _ := pem.Decode(data)
// if block == nil || block.Type != "PUBLIC KEY" {
// log.Fatal("failed to decode PEM block containing public key")
// }
//
// pub, err := x509.ParsePKIXPublicKey(block.Bytes)
// if err != nil {
// log.Fatal(err)
// }
//
// if rsaPubKey, ok := pub.(*rsa.PublicKey); ok {
// mysql.RegisterServerPubKey("mykey", rsaPubKey)
// } else {
// log.Fatal("not a RSA public key")
// }
func RegisterServerPubKey(name string, pubKey *rsa.PublicKey) {
serverPubKeyLock.Lock()
if serverPubKeyRegistry == nil {
serverPubKeyRegistry = make(map[string]*rsa.PublicKey)
}
serverPubKeyRegistry[name] = pubKey
serverPubKeyLock.Unlock()
}
// DeregisterServerPubKey removes the public key registered with the given name.
func DeregisterServerPubKey(name string) {
serverPubKeyLock.Lock()
if serverPubKeyRegistry != nil {
delete(serverPubKeyRegistry, name)
}
serverPubKeyLock.Unlock()
}
func getServerPubKey(name string) (pubKey *rsa.PublicKey) {
serverPubKeyLock.RLock()
if v, ok := serverPubKeyRegistry[name]; ok {
pubKey = v
}
serverPubKeyLock.RUnlock()
return
}
func readAuthPacket(mc *mysqlConn) ([]byte, error) {
pkt, err := mc.readPacket()
if err != nil {
return nil, err
}
if len(pkt) == 0 {
return nil, fmt.Errorf("%w: empty auth response packet", ErrMalformPkt)
}
return pkt, nil
}
// initAuth enforces the connection's authentication policy before invoking a
// plugin. The same checks apply to the server greeting and every auth switch.
func (mc *mysqlConn) initAuth(ctx context.Context, plugin string, authData []byte, auth *AuthContext) (AuthPlugin, []byte, error) {
if err := ctx.Err(); err != nil {
return nil, nil, err
}
switch plugin {
case "mysql_native_password":
if !mc.cfg.AllowNativePasswords {
return nil, nil, ErrNativePassword
}
case "mysql_old_password":
if !mc.cfg.AllowOldPasswords {
return nil, nil, ErrOldPassword
}
case "mysql_clear_password":
if !mc.cfg.AllowCleartextPasswords {
return nil, nil, ErrCleartextPassword
}
}
pluginImpl, exists := globalPluginRegistry.GetPlugin(plugin)
if !exists {
return nil, nil, fmt.Errorf("authentication plugin %q: %w", plugin, ErrUnknownPlugin)
}
response, err := pluginImpl.InitAuth(ctx, authData, auth)
if err == nil {
err = ctx.Err()
}
return pluginImpl, response, err
}
// handleAuthResult processes the initial authentication packet and manages subsequent
// authentication flow. It reads the first authentication packet and hands off processing
// to the appropriate auth plugin.
func (mc *mysqlConn) handleAuthResult(ctx context.Context, initialSeed []byte, authPlugin AuthPlugin, auth *AuthContext) error {
data, err := readAuthPacket(mc)
if err != nil {
return err
}
const maxSwitch = 5
remainingSwitch := maxSwitch
// Process continuations and auth switches until we receive OK or ERR.
for {
if err := ctx.Err(); err != nil {
return err
}
// Check for terminal packets first
switch data[0] {
case iOK:
return mc.resultUnchanged().handleOkPacket(data)
case iERR:
err := mc.handleErrorPacket(data)
if mc.cfg.openIDToken != "" {
// Preserve the server error code without exposing an echoed token.
if serverErr, ok := err.(*MySQLError); ok {
serverErr.Message = strings.ReplaceAll(serverErr.Message, mc.cfg.openIDToken, "[redacted]")
}
}
return err
case iEOF:
if mc.cfg.openIDToken != "" {
return ErrOpenIDConnectSwitch
}
// Auth switch request. Enforce the switch limit before doing any
// work.
if remainingSwitch == 0 {
return fmt.Errorf("maximum of %d authentication switch reached", maxSwitch)
}
remainingSwitch--
plugin, authData := mc.parseAuthSwitchData(data, initialSeed)
if plugin == "" {
return fmt.Errorf("%w: malformed auth switch request", ErrMalformPkt)
}
// A switch stays within the current factor. Keep its selected password
// and transport information, but give the new plugin its own snapshot.
nextAuth := *auth
newPlugin, initialAuthResponse, err := mc.initAuth(ctx, plugin, authData, &nextAuth)
if err != nil {
return err
}
if err := mc.writeAuthSwitchPacket(initialAuthResponse); err != nil {
return err
}
// Continue iteratively with the new plugin and seed.
authPlugin = newPlugin
auth = &nextAuth
initialSeed = authData
data, err = readAuthPacket(mc)
if err != nil {
return err
}
continue
}
// Not a terminal packet, let the plugin process it
// If the packet starts with 0x01 (iAuthMoreData), strip it and pass the remaining data
// MySQL is prepending all authentication with 0x01 since 8.0,
// MariaDB was prepending only when required (when data begins with OK/EOF/ERR),
// and since 11.4.9, 11.8.4, always add 0x01
pluginData := data
if len(data) > 0 && data[0] == iAuthMoreData {
pluginData = data[1:]
}
nextPacket, err := authPlugin.ContinuationAuth(ctx, pluginData, initialSeed, auth)
if err != nil {
return err
}
if err := ctx.Err(); err != nil {
return err
}
// If plugin returned a packet to send, send it and read the response
if nextPacket != nil {
if err := mc.writeAuthSwitchPacket(nextPacket); err != nil {
return err
}
data, err = readAuthPacket(mc)
if err != nil {
return err
}
continue
}
// Plugin wants to read the next packet
data, err = readAuthPacket(mc)
if err != nil {
return err
}
}
}
// parseAuthSwitchData extracts the authentication plugin name and associated data
// from an authentication switch request packet.
func (mc *mysqlConn) parseAuthSwitchData(data []byte, initialSeed []byte) (string, []byte) {
if len(data) == 1 {
// Special case for the old authentication protocol
return "mysql_old_password", initialSeed
}
pluginEndIndex := bytes.IndexByte(data, 0x00)
if pluginEndIndex < 0 {
return "", nil
}
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)
return plugin, savedAuthData
}