21 "golang.org/x/text/secure/precis"
23 "github.com/mjl-/mox/mox-"
24 "github.com/mjl-/mox/scram"
25 "github.com/mjl-/mox/store"
28func TestAuthenticateLogin(t *testing.T) {
29 // NFD username and PRECIS-cleaned password.
31 tc.client.Login("mo\u0301x@mox.example", password1)
35func TestAuthenticatePlain(t *testing.T) {
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")))
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)))
58 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example\u0000mjl@mox.example\u0000"+password0)))
61 // NFD username and PRECIS-cleaned password.
63 tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mo\u0301x@mox.example\u0000mo\u0301x@mox.example\u0000"+password1)))
67 tc.client.AuthenticatePlain("mjl@mox.example", password0)
73 tc.cmdf("", "authenticate plain")
74 tc.readprefixline("+ ")
75 tc.writelinef("*") // Aborts.
78 tc.cmdf("", "authenticate plain")
79 tc.readprefixline("+ ")
80 tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
84func TestLoginDisabled(t *testing.T) {
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")
93 tcheck(t, err, "close account")
95 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000test1234")))
97 tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000bogus")))
98 tc.xcode("AUTHENTICATIONFAILED")
100 tc.transactf("no", "login disabled@mox.example test1234")
102 tc.transactf("no", "login disabled@mox.example bogus")
103 tc.xcode("AUTHENTICATIONFAILED")
106func TestAuthenticateSCRAMSHA1(t *testing.T) {
107 testAuthenticateSCRAM(t, false, "SCRAM-SHA-1", sha1.New)
110func TestAuthenticateSCRAMSHA256(t *testing.T) {
111 testAuthenticateSCRAM(t, false, "SCRAM-SHA-256", sha256.New)
114func TestAuthenticateSCRAMSHA1PLUS(t *testing.T) {
115 testAuthenticateSCRAM(t, true, "SCRAM-SHA-1-PLUS", sha1.New)
118func TestAuthenticateSCRAMSHA256PLUS(t *testing.T) {
119 testAuthenticateSCRAM(t, true, "SCRAM-SHA-256-PLUS", sha256.New)
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)
127 auth := func(status string, serverFinalError error, username, password string) {
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)))
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")
143 buf, err := base64.StdEncoding.DecodeString(line)
144 tc.check(err, "parsing base64 from remote")
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)))
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)
160 if serverFinalError != nil {
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))
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)
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")))
182 // NFD username, with PRECIS-cleaned password.
183 auth("ok", nil, "mo\u0301x@mox.example", password1)
188func TestAuthenticateCRAMMD5(t *testing.T) {
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")))
196 auth := func(status string, username, password string) {
199 tc.client.LastTag = "x001"
200 tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag)
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")
208 buf, err := base64.StdEncoding.DecodeString(line)
209 tc.check(err, "parsing base64 from remote")
213 chal := xreadContinuation()
214 pw, err := precis.OpaqueString.String(password)
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)))
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))
230 auth("no", "mjl@mox.example", "badpass")
231 auth("no", "mjl@mox.example", "")
232 auth("no", "other@mox.example", password0)
234 auth("ok", "mjl@mox.example", password0)
238 // NFD username, with PRECIS-cleaned password.
240 auth("ok", "mo\u0301x@mox.example", password1)
244func TestAuthenticateTLSClientCert(t *testing.T) {
245 tc := startArgs(t, true, true, true, true, "mjl")
246 tc.transactf("no", "authenticate external ") // No TLS auth.
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},
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
263 addClientCert := func() error {
264 return store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
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")
272 tc.transactf("ok", "authenticate external ")
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")
280 tc.transactf("ok", "authenticate external %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example")))
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)))
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)))
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")))
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 =")
308 tlspubkey.NoIMAPPreauth = false
309 err = store.TLSPublicKeyUpdate(ctxbg, &tlspubkey)
310 tcheck(t, err, "update tls public key")
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)},
317 ctx, cancel := context.WithCancel(ctxbg)
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")
325 cs := tc.conn.(*tls.Conn).ConnectionState()
327 t.Fatalf("tls connection was resumed")
329 tc.transactf("no", "authenticate external ") // Not allowed, already in authenticated state.
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")
337 cs = tc.conn.(*tls.Conn).ConnectionState()
339 t.Fatalf("tls connection was not resumed")
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")
349 // Authentication with unknown key should fail.
350 // todo: less duplication, change startArgs so this can be merged into it.
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()
361 serverConn, clientConn := net.Pipe()
362 defer clientConn.Close()
364 done := make(chan struct{})
365 defer func() { <-done }()
369 defer serverConn.Close()
370 serve("test", cid, &serverConfig, serverConn, true, false, false, "")
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)
382 t.Fatalf("tls handshake with unknown client certificate succeeded")
384 if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
385 t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)