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, _ := tc.client.ReadContinuation()
139 if result.Status != "" {
140 tc.t.Fatalf("expected continuation")
141 }
142 buf, err := base64.StdEncoding.DecodeString(line)
143 tc.check(err, "parsing base64 from remote")
144 return buf
145 }
146
147 serverFirst := xreadContinuation()
148 clientFinal, err := sc.ServerFirst(serverFirst, password)
149 tc.check(err, "scram clientFinal")
150 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(clientFinal)))
151
152 serverFinal := xreadContinuation()
153 err = sc.ServerFinal(serverFinal)
154 if serverFinalError == nil {
155 tc.check(err, "scram serverFinal")
156 } else if err == nil || !errors.Is(err, serverFinalError) {
157 t.Fatalf("server final, got err %#v, expected %#v", err, serverFinalError)
158 }
159 if serverFinalError != nil {
160 tc.writelinef("*")
161 } else {
162 tc.writelinef("")
163 }
164 _, result, err := tc.client.Response()
165 tc.check(err, "read response")
166 if string(result.Status) != strings.ToUpper(status) {
167 tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
168 }
169 }
170
171 tc = startArgs(t, true, tls, true, true, "mjl")
172 auth("no", scram.ErrInvalidProof, "mjl@mox.example", "badpass")
173 auth("no", scram.ErrInvalidProof, "mjl@mox.example", "")
174 // 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.
175 // auth("no", nil, "other@mox.example", password0)
176
177 tc.transactf("no", "authenticate bogus ")
178 tc.transactf("bad", "authenticate %s not base64...", method)
179 tc.transactf("no", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte("bad data")))
180
181 // NFD username, with PRECIS-cleaned password.
182 auth("ok", nil, "mo\u0301x@mox.example", password1)
183
184 tc.close()
185}
186
187func TestAuthenticateCRAMMD5(t *testing.T) {
188 tc := start(t)
189
190 tc.transactf("no", "authenticate bogus ")
191 tc.transactf("bad", "authenticate CRAM-MD5 not base64...")
192 tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("baddata")))
193 tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("bad data")))
194
195 auth := func(status string, username, password string) {
196 t.Helper()
197
198 tc.client.LastTag = "x001"
199 tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag)
200
201 xreadContinuation := func() []byte {
202 line, _, result, _ := tc.client.ReadContinuation()
203 if result.Status != "" {
204 tc.t.Fatalf("expected continuation")
205 }
206 buf, err := base64.StdEncoding.DecodeString(line)
207 tc.check(err, "parsing base64 from remote")
208 return buf
209 }
210
211 chal := xreadContinuation()
212 pw, err := precis.OpaqueString.String(password)
213 if err == nil {
214 password = pw
215 }
216 h := hmac.New(md5.New, []byte(password))
217 h.Write([]byte(chal))
218 resp := fmt.Sprintf("%s %x", username, h.Sum(nil))
219 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(resp)))
220
221 _, result, err := tc.client.Response()
222 tc.check(err, "read response")
223 if string(result.Status) != strings.ToUpper(status) {
224 tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status))
225 }
226 }
227
228 auth("no", "mjl@mox.example", "badpass")
229 auth("no", "mjl@mox.example", "")
230 auth("no", "other@mox.example", password0)
231
232 auth("ok", "mjl@mox.example", password0)
233
234 tc.close()
235
236 // NFD username, with PRECIS-cleaned password.
237 tc = start(t)
238 auth("ok", "mo\u0301x@mox.example", password1)
239 tc.close()
240}
241
242func TestAuthenticateTLSClientCert(t *testing.T) {
243 tc := startArgsMore(t, true, true, nil, nil, true, true, "mjl", nil)
244 tc.transactf("no", "authenticate external ") // No TLS auth.
245 tc.close()
246
247 // Create a certificate, register its public key with account, and make a tls
248 // client config that sends the certificate.
249 clientCert0 := fakeCert(t, true)
250 clientConfig := tls.Config{
251 InsecureSkipVerify: true,
252 Certificates: []tls.Certificate{clientCert0},
253 }
254
255 tlspubkey, err := store.ParseTLSPublicKeyCert(clientCert0.Certificate[0])
256 tcheck(t, err, "parse certificate")
257 tlspubkey.Account = "mjl"
258 tlspubkey.LoginAddress = "mjl@mox.example"
259 tlspubkey.NoIMAPPreauth = true
260
261 addClientCert := func() error {
262 return store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
263 }
264
265 // No preauth, explicit authenticate with TLS.
266 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
267 if tc.client.Preauth {
268 t.Fatalf("preauthentication while not configured for tls public key")
269 }
270 tc.transactf("ok", "authenticate external ")
271 tc.close()
272
273 // External with explicit username.
274 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
275 if tc.client.Preauth {
276 t.Fatalf("preauthentication while not configured for tls public key")
277 }
278 tc.transactf("ok", "authenticate external %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example")))
279 tc.close()
280
281 // No preauth, also allow other mechanisms.
282 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
283 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
284 tc.close()
285
286 // No preauth, also allow other username for same account.
287 tc = startArgsMore(t, true, true, nil, &clientConfig, false, true, "mjl", addClientCert)
288 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000móx@mox.example\u0000"+password0)))
289 tc.close()
290
291 // No preauth, other mechanism must be for same account.
292 acc, err := store.OpenAccount(pkglog, "other", false)
293 tcheck(t, err, "open account")
294 err = acc.SetPassword(pkglog, "test1234")
295 tcheck(t, err, "set password")
296 err = acc.Close()
297 tcheck(t, err, "close account")
298 tc = startArgsMore(t, true, true, nil, &clientConfig, false, 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, "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, "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, "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", makeAppendTime(exampleMsg, received))
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