1// Package scram implements the SCRAM-SHA-* SASL authentication mechanisms, including the PLUS variants, RFC 7677 and RFC 5802.
2//
3// SCRAM-SHA-256 and SCRAM-SHA-1 allow a client to authenticate to a server using a
4// password without handing plaintext password over to the server. The client also
5// verifies the server knows (a derivative of) the password. The *-PLUS variants
6// bind the authentication exchange to the TLS session, preventing MitM attempts.
7// Both the client and server side are implemented.
8package scram
9
10// todo: test with messages that contains extensions
11// todo: some tests for the parser
12// todo: figure out how invalid parameters etc should be handled. just abort? perhaps mostly a problem for imap.
13
14import (
15 "bytes"
16 "crypto/hmac"
17 cryptorand "crypto/rand"
18 "crypto/tls"
19 "encoding/base64"
20 "errors"
21 "fmt"
22 "hash"
23 "strings"
24
25 "golang.org/x/crypto/pbkdf2"
26 "golang.org/x/text/secure/precis"
27 "golang.org/x/text/unicode/norm"
28)
29
30// Errors at scram protocol level. Can be exchanged between client and server.
31var (
32 ErrInvalidEncoding Error = "invalid-encoding"
33 ErrExtensionsNotSupported Error = "extensions-not-supported"
34 ErrInvalidProof Error = "invalid-proof"
35 ErrChannelBindingsDontMatch Error = "channel-bindings-dont-match"
36 ErrServerDoesSupportChannelBinding Error = "server-does-support-channel-binding"
37 ErrChannelBindingNotSupported Error = "channel-binding-not-supported"
38 ErrUnsupportedChannelBindingType Error = "unsupported-channel-binding-type"
39 ErrUnknownUser Error = "unknown-user"
40 ErrNoResources Error = "no-resources"
41 ErrOtherError Error = "other-error"
42)
43
44var scramErrors = makeErrors()
45
46func makeErrors() map[string]Error {
47 l := []Error{
48 ErrInvalidEncoding,
49 ErrExtensionsNotSupported,
50 ErrInvalidProof,
51 ErrChannelBindingsDontMatch,
52 ErrServerDoesSupportChannelBinding,
53 ErrChannelBindingNotSupported,
54 ErrUnsupportedChannelBindingType,
55 ErrUnknownUser,
56 ErrNoResources,
57 ErrOtherError,
58 }
59 m := map[string]Error{}
60 for _, e := range l {
61 m[string(e)] = e
62 }
63 return m
64}
65
66var (
67 ErrNorm = errors.New("parameter not unicode normalized") // E.g. if client sends non-normalized username or authzid.
68 ErrUnsafe = errors.New("unsafe parameter") // E.g. salt, nonce too short, or too few iterations.
69 ErrProtocol = errors.New("protocol error") // E.g. server responded with a nonce not prefixed by the client nonce.
70)
71
72type Error string
73
74func (e Error) Error() string {
75 return string(e)
76}
77
78// MakeRandom returns a cryptographically random buffer for use as salt or as
79// nonce.
80func MakeRandom() []byte {
81 buf := make([]byte, 12)
82 _, err := cryptorand.Read(buf)
83 if err != nil {
84 panic("generate random")
85 }
86 return buf
87}
88
89// Cleanup password with precis, like remote should have done. If the password
90// appears invalid, we'll return the original, there is a chance the server also
91// doesn't enforce requirements and accepts it. ../rfc/8265:679
92func precisPassword(password string) string {
93 pw, err := precis.OpaqueString.String(password)
94 if err != nil {
95 return password
96 }
97 return pw
98}
99
100// SaltPassword returns a salted password.
101func SaltPassword(h func() hash.Hash, password string, salt []byte, iterations int) []byte {
102 password = precisPassword(password)
103 return pbkdf2.Key([]byte(password), salt, iterations, h().Size(), h)
104}
105
106// hmac0 returns the hmac with key over msg.
107func hmac0(h func() hash.Hash, key []byte, msg string) []byte {
108 mac := hmac.New(h, key)
109 mac.Write([]byte(msg))
110 return mac.Sum(nil)
111}
112
113func xor(a, b []byte) {
114 for i := range a {
115 a[i] ^= b[i]
116 }
117}
118
119func channelBindData(cs *tls.ConnectionState) ([]byte, error) {
120 if cs.Version <= tls.VersionTLS12 {
121 if cs.TLSUnique == nil {
122 return nil, fmt.Errorf("no channel binding data available")
123 }
124 return cs.TLSUnique, nil
125 }
126
127 // "tls-exporter", ../rfc/9266:95
128 // Since TLS 1.3, a zero-length and absent context have the same behaviour. ../rfc/8446:5385 ../rfc/8446:5405
129 // This is different from TLS 1.2 and earlier. ../rfc/5705:206 ../rfc/5705:245
130 return cs.ExportKeyingMaterial("EXPORTER-Channel-Binding", []byte{}, 32)
131}
132
133// Server represents the server-side of a SCRAM-SHA-* authentication.
134type Server struct {
135 Authentication string // Username for authentication, "authc". Always set and non-empty.
136 Authorization string // If set, role of user to assume after authentication, "authz".
137
138 h func() hash.Hash // sha1.New or sha256.New
139
140 // Messages used in hash calculations.
141 clientFirstBare string
142 serverFirst string
143 clientFinalWithoutProof string
144
145 gs2header string
146 clientNonce string // Client-part of the nonce.
147 serverNonceOverride string // If set, server does not generate random nonce, but uses this. For tests with the test vector.
148 nonce string // Full client + server nonce.
149 channelBinding []byte
150}
151
152// NewServer returns a server given the first SCRAM message from a client.
153//
154// If cs is set, the PLUS variant can be negotiated, binding the authentication
155// exchange to the TLS channel (preventing MitM attempts). If a client
156// indicates it supports the PLUS variant, but thinks the server does not, the
157// authentication attempt will fail.
158//
159// If channelBindingRequired is set, the client has indicated it will do channel
160// binding and not doing so will cause the authentication to fail.
161//
162// The sequence for data and calls on a server:
163//
164// - Read initial data from client, call NewServer (this call), then ServerFirst and write to the client.
165// - Read response from client, call Finish or FinishFinal and write the resulting string.
166func NewServer(h func() hash.Hash, clientFirst []byte, cs *tls.ConnectionState, channelBindingRequired bool) (server *Server, rerr error) {
167 p := newParser(clientFirst)
168 defer p.recover(&rerr)
169
170 server = &Server{h: h}
171
172 // ../rfc/5802:949 ../rfc/5802:910
173 gs2cbindFlag := p.xbyte()
174 switch gs2cbindFlag {
175 case 'n':
176 // Client does not support channel binding.
177 if channelBindingRequired {
178 p.xerrorf("channel binding is required when specifying scram plus: %w", ErrChannelBindingsDontMatch)
179 }
180 case 'y':
181 // Client supports channel binding but thinks we as server do not.
182 p.xerrorf("gs2 channel bind flag is y, client believes server does not support channel binding: %w", ErrServerDoesSupportChannelBinding)
183 case 'p':
184 // Use channel binding.
185 // It seems a cyrus-sasl client tells a server it is using the bare (non-PLUS)
186 // scram authentication mechanism, but then does use channel binding. It seems to
187 // use the server announcement of the plus variant only to learn the server
188 // supports channel binding.
189 p.xtake("=")
190 cbname := p.xcbname()
191 // Assume the channel binding name is case-sensitive, and lower-case as used in
192 // examples. The ABNF rule accepts both lower and upper case. But the ABNF for
193 // attribute names also allows that, while the text claims they are case
194 // sensitive... ../rfc/5802:547
195 switch cbname {
196 case "tls-unique":
197 if cs == nil {
198 p.xerrorf("no tls connection: %w", ErrChannelBindingsDontMatch)
199 } else if cs.Version >= tls.VersionTLS13 {
200 // ../rfc/9266:122
201 p.xerrorf("tls-unique not defined for tls 1.3 and later, use tls-exporter: %w", ErrChannelBindingsDontMatch)
202 } else if cs.TLSUnique == nil {
203 // As noted in the crypto/tls documentation.
204 p.xerrorf("no tls-unique channel binding value for this tls connection, possibly due to missing extended master key support and/or resumed connection: %w", ErrChannelBindingsDontMatch)
205 }
206 case "tls-exporter":
207 if cs == nil {
208 p.xerrorf("no tls connection: %w", ErrChannelBindingsDontMatch)
209 } else if cs.Version < tls.VersionTLS13 {
210 // Using tls-exporter with pre-1.3 TLS would require more precautions. Perhaps later.
211 // ../rfc/9266:201
212 p.xerrorf("tls-exporter with tls before 1.3 not implemented, use tls-unique: %w", ErrChannelBindingsDontMatch)
213 }
214 default:
215 p.xerrorf("unknown parameter p %s: %w", cbname, ErrUnsupportedChannelBindingType)
216 }
217 cb, err := channelBindData(cs)
218 if err != nil {
219 // We can pass back the error, it should never contain sensitive data, and only
220 // happen due to incorrect calling or a TLS config that is currently impossible
221 // (renegotiation enabled).
222 p.xerrorf("error fetching channel binding data: %v: %w", err, ErrOtherError)
223 }
224 server.channelBinding = cb
225 default:
226 p.xerrorf("unrecognized gs2 channel bind flag")
227 }
228 p.xtake(",")
229 if !p.take(",") {
230 server.Authorization = p.xauthzid()
231 if norm.NFC.String(server.Authorization) != server.Authorization {
232 return nil, fmt.Errorf("%w: authzid", ErrNorm)
233 }
234 p.xtake(",")
235 }
236 server.gs2header = p.s[:p.o]
237 server.clientFirstBare = p.s[p.o:]
238
239 // ../rfc/5802:632
240 // ../rfc/5802:946
241 if p.take("m=") {
242 p.xerrorf("unexpected mandatory extension: %w", ErrExtensionsNotSupported) // ../rfc/5802:973
243 }
244 server.Authentication = p.xusername()
245 if norm.NFC.String(server.Authentication) != server.Authentication {
246 return nil, fmt.Errorf("%w: username", ErrNorm)
247 }
248 p.xtake(",")
249 server.clientNonce = p.xnonce()
250 if len(server.clientNonce) < 8 {
251 return nil, fmt.Errorf("%w: client nonce too short", ErrUnsafe)
252 }
253 // Extensions, we don't recognize them.
254 for p.take(",") {
255 p.xattrval()
256 }
257 p.xempty()
258 return server, nil
259}
260
261// ServerFirst returns the string to send back to the client. To be called after NewServer.
262func (s *Server) ServerFirst(iterations int, salt []byte) (string, error) {
263 // ../rfc/5802:959
264 serverNonce := s.serverNonceOverride
265 if serverNonce == "" {
266 serverNonce = base64.StdEncoding.EncodeToString(MakeRandom())
267 }
268 s.nonce = s.clientNonce + serverNonce
269 s.serverFirst = fmt.Sprintf("r=%s,s=%s,i=%d", s.nonce, base64.StdEncoding.EncodeToString(salt), iterations)
270 return s.serverFirst, nil
271}
272
273// Finish takes the final client message, and the salted password (probably
274// from server storage), verifies the client, and returns a message to return
275// to the client. If err is nil, authentication was successful. If the
276// authorization requested is not acceptable, the server should call
277// FinishError instead.
278func (s *Server) Finish(clientFinal []byte, saltedPassword []byte) (serverFinal string, rerr error) {
279 p := newParser(clientFinal)
280 defer p.recover(&rerr)
281
282 // If there is any channel binding, and it doesn't match, this may be a
283 // MitM-attack. If the MitM would replace the channel binding, the signature
284 // calculated below would not match.
285 cbind := p.xchannelBinding()
286 cbindExp := append([]byte(s.gs2header), s.channelBinding...)
287 if !bytes.Equal(cbind, cbindExp) {
288 return "e=" + string(ErrChannelBindingsDontMatch), ErrChannelBindingsDontMatch
289 }
290 p.xtake(",")
291 nonce := p.xnonce()
292 if nonce != s.nonce {
293 return "e=" + string(ErrInvalidProof), ErrInvalidProof
294 }
295 for !p.peek(",p=") {
296 p.xtake(",")
297 p.xattrval() // Ignored.
298 }
299 s.clientFinalWithoutProof = p.s[:p.o]
300 p.xtake(",")
301 proof := p.xproof()
302 p.xempty()
303
304 authMsg := s.clientFirstBare + "," + s.serverFirst + "," + s.clientFinalWithoutProof
305
306 clientKey := hmac0(s.h, saltedPassword, "Client Key")
307 h := s.h()
308 h.Write(clientKey)
309 storedKey := h.Sum(nil)
310
311 clientSig := hmac0(s.h, storedKey, authMsg)
312 xor(clientSig, clientKey) // Now clientProof.
313 if !bytes.Equal(clientSig, proof) {
314 return "e=" + string(ErrInvalidProof), ErrInvalidProof
315 }
316
317 serverKey := hmac0(s.h, saltedPassword, "Server Key")
318 serverSig := hmac0(s.h, serverKey, authMsg)
319 return fmt.Sprintf("v=%s", base64.StdEncoding.EncodeToString(serverSig)), nil
320}
321
322// FinishError returns an error message to write to the client for the final
323// server message.
324func (s *Server) FinishError(err Error) string {
325 return "e=" + string(err)
326}
327
328// Client represents the client-side of a SCRAM-SHA-* authentication.
329type Client struct {
330 authc string
331 authz string
332
333 h func() hash.Hash // sha1.New or sha256.New
334 noServerPlus bool // Server did not announce support for PLUS-variant.
335 cs *tls.ConnectionState // If set, use PLUS-variant.
336
337 // Messages used in hash calculations.
338 clientFirstBare string
339 serverFirst string
340 clientFinalWithoutProof string
341 authMessage string
342
343 gs2header string
344 clientNonce string
345 nonce string // Full client + server nonce.
346 saltedPassword []byte
347 channelBindData []byte // For PLUS-variant.
348}
349
350// NewClient returns a client for authentication authc, optionally for
351// authorization with role authz, for the hash (sha1.New or sha256.New).
352//
353// If noServerPlus is true, the client would like to have used the PLUS-variant,
354// that binds the authentication attempt to the TLS connection, but the client did
355// not see support for the PLUS variant announced by the server. Used during
356// negotiation to detect possible MitM attempt.
357//
358// If cs is not nil, the SCRAM PLUS-variant is negotiated, with channel binding to
359// the unique TLS connection, either using "tls-exporter" for TLS 1.3 and later, or
360// "tls-unique" otherwise.
361//
362// If cs is nil, no channel binding is done. If noServerPlus is also false, the
363// client is configured to not attempt/"support" the PLUS-variant, ensuring servers
364// that do support the PLUS-variant do not abort the connection.
365//
366// The sequence for data and calls on a client:
367//
368// - ClientFirst, write result to server.
369// - Read response from server, feed to ServerFirst, write response to server.
370// - Read response from server, feed to ServerFinal.
371func NewClient(h func() hash.Hash, authc, authz string, noServerPlus bool, cs *tls.ConnectionState) *Client {
372 authc = norm.NFC.String(authc)
373 authz = norm.NFC.String(authz)
374 return &Client{authc: authc, authz: authz, h: h, noServerPlus: noServerPlus, cs: cs}
375}
376
377// ClientFirst returns the first client message to write to the server.
378// No channel binding is done/supported.
379// A random nonce is generated.
380func (c *Client) ClientFirst() (clientFirst string, rerr error) {
381 if c.noServerPlus && c.cs != nil {
382 return "", fmt.Errorf("cannot set both claim channel binding is not supported, and use channel binding")
383 }
384 // The first byte of the gs2header indicates if/how channel binding should be used.
385 // ../rfc/5802:903
386 if c.cs != nil {
387 if c.cs.Version >= tls.VersionTLS13 {
388 c.gs2header = "p=tls-exporter"
389 } else {
390 c.gs2header = "p=tls-unique"
391 }
392 cbdata, err := channelBindData(c.cs)
393 if err != nil {
394 return "", fmt.Errorf("get channel binding data: %v", err)
395 }
396 c.channelBindData = cbdata
397 } else if c.noServerPlus {
398 // We support it, but we think server does not. If server does support it, we may
399 // have been downgraded, and the server will tell us.
400 c.gs2header = "y"
401 } else {
402 // We don't want to do channel binding.
403 c.gs2header = "n"
404 }
405 c.gs2header += fmt.Sprintf(",%s,", saslname(c.authz))
406 if c.clientNonce == "" {
407 c.clientNonce = base64.StdEncoding.EncodeToString(MakeRandom())
408 }
409 c.clientFirstBare = fmt.Sprintf("n=%s,r=%s", saslname(c.authc), c.clientNonce)
410 return c.gs2header + c.clientFirstBare, nil
411}
412
413// ServerFirst processes the first response message from the server. The
414// provided nonce, salt and iterations are checked. If valid, a final client
415// message is calculated and returned. This message must be written to the
416// server. It includes proof that the client knows the password.
417func (c *Client) ServerFirst(serverFirst []byte, password string) (clientFinal string, rerr error) {
418 c.serverFirst = string(serverFirst)
419 p := newParser(serverFirst)
420 defer p.recover(&rerr)
421
422 // ../rfc/5802:632
423 // ../rfc/5802:959
424 if p.take("m=") {
425 p.xerrorf("unsupported mandatory extension: %w", ErrExtensionsNotSupported) // ../rfc/5802:973
426 }
427
428 c.nonce = p.xnonce()
429 p.xtake(",")
430 salt := p.xsalt()
431 p.xtake(",")
432 iterations := p.xiterations()
433 // We ignore extensions that we don't know about.
434 for p.take(",") {
435 p.xattrval()
436 }
437 p.xempty()
438
439 if !strings.HasPrefix(c.nonce, c.clientNonce) {
440 return "", fmt.Errorf("%w: server dropped our nonce", ErrProtocol)
441 }
442 if len(c.nonce)-len(c.clientNonce) < 8 {
443 return "", fmt.Errorf("%w: server nonce too short", ErrUnsafe)
444 }
445 if len(salt) < 8 {
446 return "", fmt.Errorf("%w: salt too short", ErrUnsafe)
447 }
448 if iterations < 2048 {
449 return "", fmt.Errorf("%w: too few iterations", ErrUnsafe)
450 }
451
452 // We send our channel binding data if present. If the server has different values,
453 // we'll get an error. If any MitM would try to modify the channel binding data,
454 // the server cannot verify our signature and will fail the attempt.
455 // ../rfc/5802:925 ../rfc/5802:1015
456 cbindInput := append([]byte(c.gs2header), c.channelBindData...)
457 c.clientFinalWithoutProof = fmt.Sprintf("c=%s,r=%s", base64.StdEncoding.EncodeToString(cbindInput), c.nonce)
458
459 c.authMessage = c.clientFirstBare + "," + c.serverFirst + "," + c.clientFinalWithoutProof
460
461 c.saltedPassword = SaltPassword(c.h, password, salt, iterations)
462 clientKey := hmac0(c.h, c.saltedPassword, "Client Key")
463 h := c.h()
464 h.Write(clientKey)
465 storedKey := h.Sum(nil)
466 clientSig := hmac0(c.h, storedKey, c.authMessage)
467 xor(clientSig, clientKey) // Now clientProof.
468 clientProof := clientSig
469
470 r := c.clientFinalWithoutProof + ",p=" + base64.StdEncoding.EncodeToString(clientProof)
471 return r, nil
472}
473
474// ServerFinal processes the final message from the server, verifying that the
475// server knows the password.
476func (c *Client) ServerFinal(serverFinal []byte) (rerr error) {
477 p := newParser(serverFinal)
478 defer p.recover(&rerr)
479
480 if p.take("e=") {
481 errstr := p.xvalue()
482 var err error = scramErrors[errstr]
483 if err == Error("") {
484 err = errors.New(errstr)
485 }
486 return fmt.Errorf("error from server: %w", err)
487 }
488 p.xtake("v=")
489 verifier := p.xbase64()
490
491 serverKey := hmac0(c.h, c.saltedPassword, "Server Key")
492 serverSig := hmac0(c.h, serverKey, c.authMessage)
493 if !bytes.Equal(verifier, serverSig) {
494 return fmt.Errorf("incorrect server signature")
495 }
496 return nil
497}
498
499// Convert "," to =2C and "=" to =3D.
500func saslname(s string) string {
501 var r string
502 for _, c := range s {
503 if c == ',' {
504 r += "=2C"
505 } else if c == '=' {
506 r += "=3D"
507 } else {
508 r += string(c)
509 }
510 }
511 return r
512}
513