1package imapserver
2
3import (
4 "context"
5 "crypto/hmac"
6 "crypto/md5"
7 "crypto/sha1"
8 "crypto/sha256"
9 "crypto/tls"
10 "encoding/base64"
11 "errors"
12 "fmt"
13 "hash"
14 "net"
15 "os"
16 "path/filepath"
17 "strings"
18 "testing"
19 "time"
20
21 "golang.org/x/text/secure/precis"
22
23 "github.com/mjl-/mox/mox-"
24 "github.com/mjl-/mox/scram"
25 "github.com/mjl-/mox/store"
26)
27
28func TestAuthenticateLogin(t *testing.T) {
29 // NFD username and PRECIS-cleaned password.
30 tc := start(t)
31 tc.client.Login("mo\u0301x@mox.example", password1)
32 tc.close()
33}
34
35func TestAuthenticatePlain(t *testing.T) {
36 tc := start(t)
37
38 tc.transactf("no", "authenticate bogus ")
39 tc.transactf("bad", "authenticate plain not base64...")
40 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000baduser\u0000badpass")))
41 tc.xcode("AUTHENTICATIONFAILED")
42 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000badpass")))
43 tc.xcode("AUTHENTICATIONFAILED")
44 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl\u0000badpass"))) // Need email, not account.
45 tc.xcode("AUTHENTICATIONFAILED")
46 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test")))
47 tc.xcode("AUTHENTICATIONFAILED")
48 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000test"+password0)))
49 tc.xcode("AUTHENTICATIONFAILED")
50 tc.transactf("bad", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000")))
51 tc.xcode("")
52 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("other\u0000mjl@mox.example\u0000"+password0)))
53 tc.xcode("AUTHORIZATIONFAILED")
54 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
55 tc.close()
56
57 tc = start(t)
58 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example\u0000mjl@mox.example\u0000"+password0)))
59 tc.close()
60
61 // NFD username and PRECIS-cleaned password.
62 tc = start(t)
63 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mo\u0301x@mox.example\u0000mo\u0301x@mox.example\u0000"+password1)))
64 tc.close()
65
66 tc = start(t)
67 tc.client.AuthenticatePlain("mjl@mox.example", password0)
68 tc.close()
69
70 tc = start(t)
71 defer tc.close()
72
73 tc.cmdf("", "authenticate plain")
74 tc.readprefixline("+ ")
75 tc.writelinef("*") // Aborts.
76 tc.readstatus("bad")
77
78 tc.cmdf("", "authenticate plain")
79 tc.readprefixline("+ ")
80 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
81 tc.readstatus("ok")
82}
83
84func TestLoginDisabled(t *testing.T) {
85 tc := start(t)
86 defer tc.close()
87
88 acc, err := store.OpenAccount(pkglog, "disabled", false)
89 tcheck(t, err, "open account")
90 err = acc.SetPassword(pkglog, "test1234")
91 tcheck(t, err, "set password")
92 err = acc.Close()
93 tcheck(t, err, "close account")
94
95 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000test1234")))
96 tc.xcode("")
97 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000bogus")))
98 tc.xcode("AUTHENTICATIONFAILED")
99
100 tc.transactf("no", "login disabled@mox.example test1234")
101 tc.xcode("")
102 tc.transactf("no", "login disabled@mox.example bogus")
103 tc.xcode("AUTHENTICATIONFAILED")
104}
105
106func TestAuthenticateSCRAMSHA1(t *testing.T) {
107 testAuthenticateSCRAM(t, false, "SCRAM-SHA-1", sha1.New)
108}
109
110func TestAuthenticateSCRAMSHA256(t *testing.T) {
111 testAuthenticateSCRAM(t, false, "SCRAM-SHA-256", sha256.New)
112}
113
114func TestAuthenticateSCRAMSHA1PLUS(t *testing.T) {
115 testAuthenticateSCRAM(t, true, "SCRAM-SHA-1-PLUS", sha1.New)
116}
117
118func TestAuthenticateSCRAMSHA256PLUS(t *testing.T) {
119 testAuthenticateSCRAM(t, true, "SCRAM-SHA-256-PLUS", sha256.New)
120}
121
122func testAuthenticateSCRAM(t *testing.T, tls bool, method string, h func() hash.Hash) {
123 tc := startArgs(t, true, tls, true, true, "mjl")
124 tc.client.AuthenticateSCRAM(method, h, "mjl@mox.example", password0)
125 tc.close()
126
127 auth := func(status string, serverFinalError error, username, password string) {
128 t.Helper()
129
130 noServerPlus := false
131 sc := scram.NewClient(h, username, "", noServerPlus, tc.client.TLSConnectionState())
132 clientFirst, err := sc.ClientFirst()
133 tc.check(err, "scram clientFirst")
134 tc.client.LastTag = "x001"
135 tc.writelinef("%s authenticate %s %s", tc.client.LastTag, method, base64.StdEncoding.EncodeToString([]byte(clientFirst)))
136
137 xreadContinuation := func() []byte {
138 line, _, result, rerr := tc.client.ReadContinuation()
139 tc.check(rerr, "read continuation")
140 if result.Status != "" {
141 tc.t.Fatalf("expected continuation")
142 }
143 buf, err := base64.StdEncoding.DecodeString(line)
144 tc.check(err, "parsing base64 from remote")
145 return buf
146 }
147
148 serverFirst := xreadContinuation()
149 clientFinal, err := sc.ServerFirst(serverFirst, password)
150 tc.check(err, "scram clientFinal")
151 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(clientFinal)))
152
153 serverFinal := xreadContinuation()
154 err = sc.ServerFinal(serverFinal)
155 if serverFinalError == nil {
156 tc.check(err, "scram serverFinal")
157 } else if err == nil || !errors.Is(err, serverFinalError) {
158 t.Fatalf("server final, got err %#v, expected %#v", err, serverFinalError)
159 }
160 if serverFinalError != nil {
161 tc.writelinef("*")
162 } else {
163 tc.writelinef("")
164 }
165 _, result, err := tc.client.Response()
166 tc.check(err, "read response")
167 if string(result.Status) != strings.ToUpper(status) {
168 tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
169 }
170 }
171
172 tc = startArgs(t, true, tls, true, true, "mjl")
173 auth("no", scram.ErrInvalidProof, "mjl@mox.example", "badpass")
174 auth("no", scram.ErrInvalidProof, "mjl@mox.example", "")
175 // todo: server aborts due to invalid username. we should probably make client continue with fake determinisitically generated salt and result in error in the end.
176 // auth("no", nil, "other@mox.example", password0)
177
178 tc.transactf("no", "authenticate bogus ")
179 tc.transactf("bad", "authenticate %s not base64...", method)
180 tc.transactf("no", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte("bad data")))
181
182 // NFD username, with PRECIS-cleaned password.
183 auth("ok", nil, "mo\u0301x@mox.example", password1)
184
185 tc.close()
186}
187
188func TestAuthenticateCRAMMD5(t *testing.T) {
189 tc := start(t)
190
191 tc.transactf("no", "authenticate bogus ")
192 tc.transactf("bad", "authenticate CRAM-MD5 not base64...")
193 tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("baddata")))
194 tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("bad data")))
195
196 auth := func(status string, username, password string) {
197 t.Helper()
198
199 tc.client.LastTag = "x001"
200 tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag)
201
202 xreadContinuation := func() []byte {
203 line, _, result, rerr := tc.client.ReadContinuation()
204 tc.check(rerr, "read continuation")
205 if result.Status != "" {
206 tc.t.Fatalf("expected continuation")
207 }
208 buf, err := base64.StdEncoding.DecodeString(line)
209 tc.check(err, "parsing base64 from remote")
210 return buf
211 }
212
213 chal := xreadContinuation()
214 pw, err := precis.OpaqueString.String(password)
215 if err == nil {
216 password = pw
217 }
218 h := hmac.New(md5.New, []byte(password))
219 h.Write([]byte(chal))
220 resp := fmt.Sprintf("%s %x", username, h.Sum(nil))
221 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(resp)))
222
223 _, result, err := tc.client.Response()
224 tc.check(err, "read response")
225 if string(result.Status) != strings.ToUpper(status) {
226 tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
227 }
228 }
229
230 auth("no", "mjl@mox.example", "badpass")
231 auth("no", "mjl@mox.example", "")
232 auth("no", "other@mox.example", password0)
233
234 auth("ok", "mjl@mox.example", password0)
235
236 tc.close()
237
238 // NFD username, with PRECIS-cleaned password.
239 tc = start(t)
240 auth("ok", "mo\u0301x@mox.example", password1)
241 tc.close()
242}
243
244func TestAuthenticateTLSClientCert(t *testing.T) {
245 tc := startArgs(t, true, true, true, true, "mjl")
246 tc.transactf("no", "authenticate external ") // No TLS auth.
247 tc.close()
248
249 // Create a certificate, register its public key with account, and make a tls
250 // client config that sends the certificate.
251 clientCert0 := fakeCert(t, true)
252 clientConfig := tls.Config{
253 InsecureSkipVerify: true,
254 Certificates: []tls.Certificate{clientCert0},
255 }
256
257 tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
258 tcheck(t, err, "parse certificate")
259 tlspubkey.Account = "mjl"
260 tlspubkey.LoginAddress = "mjl@mox.example"
261 tlspubkey.NoIMAPPreauth = true
262
263 addClientCert := func() error {
264 return store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
265 }
266
267 // No preauth, explicit authenticate with TLS.
268 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
269 if tc.client.Preauth {
270 t.Fatalf("preauthentication while not configured for tls public key")
271 }
272 tc.transactf("ok", "authenticate external ")
273 tc.close()
274
275 // External with explicit username.
276 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
277 if tc.client.Preauth {
278 t.Fatalf("preauthentication while not configured for tls public key")
279 }
280 tc.transactf("ok", "authenticate external %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example")))
281 tc.close()
282
283 // No preauth, also allow other mechanisms.
284 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
285 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
286 tc.close()
287
288 // No preauth, also allow other username for same account.
289 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
290 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000móx@mox.example\u0000"+password0)))
291 tc.close()
292
293 // No preauth, other mechanism must be for same account.
294 acc, err := store.OpenAccount(pkglog, "other", false)
295 tcheck(t, err, "open account")
296 err = acc.SetPassword(pkglog, "test1234")
297 tcheck(t, err, "set password")
298 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, true, "mjl", addClientCert)
299 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000other@mox.example\u0000test1234")))
300 tc.close()
301
302 // Starttls and external auth.
303 tc = startArgsMore(t, true, false, nil, &clientConfig, false, true, true, "mjl", addClientCert)
304 tc.client.Starttls(&clientConfig)
305 tc.transactf("ok", "authenticate external =")
306 tc.close()
307
308 tlspubkey.NoIMAPPreauth = false
309 err = store.TLSPublicKeyUpdate(ctxbg, &tlspubkey)
310 tcheck(t, err, "update tls public key")
311
312 // With preauth, no authenticate command needed/allowed.
313 // Already set up tls session ticket cache, for next test.
314 serverConfig := tls.Config{
315 Certificates: []tls.Certificate{fakeCert(t, false)},
316 }
317 ctx, cancel := context.WithCancel(ctxbg)
318 defer cancel()
319 mox.StartTLSSessionTicketKeyRefresher(ctx, pkglog, &serverConfig)
320 clientConfig.ClientSessionCache = tls.NewLRUClientSessionCache(10)
321 tc = startArgsMore(t, true, true, &serverConfig, &clientConfig, false, true, true, "mjl", addClientCert)
322 if !tc.client.Preauth {
323 t.Fatalf("not preauthentication while configured for tls public key")
324 }
325 cs := tc.conn.(*tls.Conn).ConnectionState()
326 if cs.DidResume {
327 t.Fatalf("tls connection was resumed")
328 }
329 tc.transactf("no", "authenticate external ") // Not allowed, already in authenticated state.
330 tc.close()
331
332 // Authentication works with TLS resumption.
333 tc = startArgsMore(t, true, true, &serverConfig, &clientConfig, false, true, true, "mjl", addClientCert)
334 if !tc.client.Preauth {
335 t.Fatalf("not preauthentication while configured for tls public key")
336 }
337 cs = tc.conn.(*tls.Conn).ConnectionState()
338 if !cs.DidResume {
339 t.Fatalf("tls connection was not resumed")
340 }
341 // Check that operations that require an account work.
342 tc.client.Enable("imap4rev2")
343 received, err := time.Parse(time.RFC3339, "2022-11-16T10:01:00+01:00")
344 tc.check(err, "parse time")
345 tc.client.Append("inbox", nil, &received, []byte(exampleMsg))
346 tc.client.Select("inbox")
347 tc.close()
348
349 // Authentication with unknown key should fail.
350 // todo: less duplication, change startArgs so this can be merged into it.
351 err = store.Close()
352 tcheck(t, err, "store close")
353 os.RemoveAll("../testdata/imap/data")
354 err = store.Init(ctxbg)
355 tcheck(t, err, "store init")
356 mox.ConfigStaticPath = filepath.FromSlash("../testdata/imap/mox.conf")
357 mox.MustLoadConfig(true, false)
358 switchStop := store.Switchboard()
359 defer switchStop()
360
361 serverConn, clientConn := net.Pipe()
362 defer clientConn.Close()
363
364 done := make(chan struct{})
365 defer func() { <-done }()
366 connCounter++
367 cid := connCounter
368 go func() {
369 defer serverConn.Close()
370 serve("test", cid, &serverConfig, serverConn, true, false, false, "")
371 close(done)
372 }()
373
374 clientConfig.ClientSessionCache = nil
375 clientConn = tls.Client(clientConn, &clientConfig)
376 // note: It's not enough to do a handshake and check if that was successful. If the
377 // client cert is not acceptable, we only learn after the handshake, when the first
378 // data messages are exchanged.
379 buf := make([]byte, 100)
380 _, err = clientConn.Read(buf)
381 if err == nil {
382 t.Fatalf("tls handshake with unknown client certificate succeeded")
383 }
384 if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
385 t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)
386 }
387}
388