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