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