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