21	"golang.org/x/text/secure/precis"
 
23	"github.com/mjl-/mox/imapclient"
 
24	"github.com/mjl-/mox/mox-"
 
25	"github.com/mjl-/mox/scram"
 
26	"github.com/mjl-/mox/store"
 
29func TestAuthenticateLogin(t *testing.T) {
 
30	// NFD username and PRECIS-cleaned password.
 
32	tc.client.Login("mo\u0301x@mox.example", password1)
 
36func TestAuthenticatePlain(t *testing.T) {
 
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")))
 
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)))
 
59	tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example\u0000mjl@mox.example\u0000"+password0)))
 
62	// NFD username and PRECIS-cleaned password.
 
64	tc.transactf("ok", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("mo\u0301x@mox.example\u0000mo\u0301x@mox.example\u0000"+password1)))
 
68	tc.client.AuthenticatePlain("mjl@mox.example", password0)
 
74	tc.cmdf("", "authenticate plain")
 
75	tc.readprefixline("+ ")
 
76	tc.writelinef("*") // Aborts.
 
79	tc.cmdf("", "authenticate plain")
 
80	tc.readprefixline("+ ")
 
81	tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte("\u0000mjl@mox.example\u0000"+password0)))
 
85func TestLoginDisabled(t *testing.T) {
 
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")
 
94	tcheck(t, err, "close account")
 
96	tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000test1234")))
 
98	tc.transactf("no", "authenticate plain %s", base64.StdEncoding.EncodeToString([]byte("\u0000disabled@mox.example\u0000bogus")))
 
99	tc.xcodeWord("AUTHENTICATIONFAILED")
 
101	tc.transactf("no", "login disabled@mox.example test1234")
 
103	tc.transactf("no", "login disabled@mox.example bogus")
 
104	tc.xcodeWord("AUTHENTICATIONFAILED")
 
107func TestAuthenticateSCRAMSHA1(t *testing.T) {
 
108	testAuthenticateSCRAM(t, false, "SCRAM-SHA-1", sha1.New)
 
111func TestAuthenticateSCRAMSHA256(t *testing.T) {
 
112	testAuthenticateSCRAM(t, false, "SCRAM-SHA-256", sha256.New)
 
115func TestAuthenticateSCRAMSHA1PLUS(t *testing.T) {
 
116	testAuthenticateSCRAM(t, true, "SCRAM-SHA-1-PLUS", sha1.New)
 
119func TestAuthenticateSCRAMSHA256PLUS(t *testing.T) {
 
120	testAuthenticateSCRAM(t, true, "SCRAM-SHA-256-PLUS", sha256.New)
 
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)
 
128	auth := func(status string, serverFinalError error, username, password string) {
 
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)))
 
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")
 
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)))
 
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)
 
157		if serverFinalError != nil {
 
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))
 
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)
 
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")))
 
179	// NFD username, with PRECIS-cleaned password.
 
180	auth("ok", nil, "mo\u0301x@mox.example", password1)
 
185func TestAuthenticateCRAMMD5(t *testing.T) {
 
186	tc := start(t, false)
 
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")))
 
193	auth := func(status string, username, password string) {
 
196		tc.client.WriteCommandf("", "authenticate CRAM-MD5")
 
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")
 
206		chal := xreadContinuation()
 
207		pw, err := precis.OpaqueString.String(password)
 
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)))
 
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))
 
223	auth("no", "mjl@mox.example", "badpass")
 
224	auth("no", "mjl@mox.example", "")
 
225	auth("no", "other@mox.example", password0)
 
227	auth("ok", "mjl@mox.example", password0)
 
231	// NFD username, with PRECIS-cleaned password.
 
233	auth("ok", "mo\u0301x@mox.example", password1)
 
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.
 
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},
 
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
 
256	addClientCert := func() error {
 
257		return store.TLSPublicKeyAdd(ctxbg, &tlspubkey)
 
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")
 
265	tc.transactf("ok", "authenticate external ")
 
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")
 
273	tc.transactf("ok", "authenticate external %s", base64.StdEncoding.EncodeToString([]byte("mjl@mox.example")))
 
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)))
 
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)))
 
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")
 
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")))
 
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 =")
 
303	tlspubkey.NoIMAPPreauth = false
 
304	err = store.TLSPublicKeyUpdate(ctxbg, &tlspubkey)
 
305	tcheck(t, err, "update tls public key")
 
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)},
 
312	ctx, cancel := context.WithCancel(ctxbg)
 
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")
 
320	cs := tc.conn.(*tls.Conn).ConnectionState()
 
322		t.Fatalf("tls connection was resumed")
 
324	tc.transactf("no", "authenticate external ") // Not allowed, already in authenticated state.
 
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")
 
332	cs = tc.conn.(*tls.Conn).ConnectionState()
 
334		t.Fatalf("tls connection was not resumed")
 
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")
 
344	// Authentication with unknown key should fail.
 
345	// todo: less duplication, change startArgs so this can be merged into it.
 
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()
 
356	serverConn, clientConn := net.Pipe()
 
357	defer clientConn.Close()
 
359	done := make(chan struct{})
 
360	defer func() { <-done }()
 
364		defer serverConn.Close()
 
365		serve("test", cid, &serverConfig, serverConn, true, false, false, false, "")
 
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)
 
377		t.Fatalf("tls handshake with unknown client certificate succeeded")
 
379	if alert, ok := mox.AsTLSAlert(err); !ok || alert != 42 {
 
380		t.Fatalf("got err %#v, expected tls 'bad certificate' alert", err)