Go 1.18 supports fuzzing natively, so this patch migrates our fuzzing tests to make use of it.
295 lines
7.4 KiB
Go
295 lines
7.4 KiB
Go
package auth
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"fmt"
|
|
"testing"
|
|
"time"
|
|
|
|
"blitiri.com.ar/go/chasquid/internal/dovecot"
|
|
"blitiri.com.ar/go/chasquid/internal/trace"
|
|
"blitiri.com.ar/go/chasquid/internal/userdb"
|
|
)
|
|
|
|
func TestDecodeResponse(t *testing.T) {
|
|
// Successful cases. Note we hard-code the response for extra assurance.
|
|
cases := []struct {
|
|
response, user, domain, passwd string
|
|
}{
|
|
{"dUBkAHVAZABwYXNz", "u", "d", "pass"}, // u@d\0u@d\0pass
|
|
{"dUBkAABwYXNz", "u", "d", "pass"}, // u@d\0\0pass
|
|
{"AHVAZABwYXNz", "u", "d", "pass"}, // \0u@d\0pass
|
|
{"dUBkAABwYXNz/w==", "u", "d", "pass\xff"}, // u@d\0\0pass\xff
|
|
{"dQB1AHBhc3M=", "u", "", "pass"}, // u\0u\0pass
|
|
|
|
// "ñaca@ñeque\0\0clavaré"
|
|
{"w7FhY2FAw7FlcXVlAABjbGF2YXLDqQ==", "ñaca", "ñeque", "clavaré"},
|
|
}
|
|
for _, c := range cases {
|
|
u, d, p, err := DecodeResponse(c.response)
|
|
if err != nil {
|
|
t.Errorf("Error in case %v: %v", c, err)
|
|
}
|
|
|
|
if u != c.user || d != c.domain || p != c.passwd {
|
|
t.Errorf("Expected %q %q %q ; got %q %q %q",
|
|
c.user, c.domain, c.passwd, u, d, p)
|
|
}
|
|
}
|
|
|
|
_, _, _, err := DecodeResponse("this is not base64 encoded")
|
|
if err == nil {
|
|
t.Errorf("invalid base64 did not fail as expected")
|
|
}
|
|
|
|
failedCases := []string{
|
|
"", "\x00", "\x00\x00", "\x00\x00\x00", "\x00\x00\x00\x00",
|
|
"a\x00b", "a\x00b\x00c", "a@a\x00b@b\x00pass",
|
|
"\xffa@b\x00\xffa@b\x00pass",
|
|
}
|
|
for _, c := range failedCases {
|
|
r := base64.StdEncoding.EncodeToString([]byte(c))
|
|
_, _, _, err := DecodeResponse(r)
|
|
if err == nil {
|
|
t.Errorf("Expected case %q to fail, but succeeded", c)
|
|
} else {
|
|
t.Logf("OK: %q failed with %v", c, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAuthenticate(t *testing.T) {
|
|
db := userdb.New("/dev/null")
|
|
db.AddUser("user", "password")
|
|
tr := trace.New("test", "TestAuthenticate")
|
|
defer tr.Finish()
|
|
|
|
a := NewAuthenticator()
|
|
a.Register("domain", WrapNoErrorBackend(db))
|
|
|
|
// Shorten the duration to speed up the test. This should still be long
|
|
// enough for it to fail if we don't sleep intentionally.
|
|
a.AuthDuration = 20 * time.Millisecond
|
|
|
|
// Test the correct case first
|
|
check(t, a, "user", "domain", "password", true)
|
|
|
|
// Wrong password, but valid user@domain.
|
|
ts := time.Now()
|
|
if ok, _ := a.Authenticate(tr, "user", "domain", "invalid"); ok {
|
|
t.Errorf("invalid password, but authentication succeeded")
|
|
}
|
|
if time.Since(ts) < a.AuthDuration {
|
|
t.Errorf("authentication was too fast (invalid case)")
|
|
}
|
|
|
|
// Incorrect cases, where the user@domain do not exist.
|
|
cases := []struct{ user, domain, password string }{
|
|
{"user", "unknown", "password"},
|
|
{"invalid", "domain", "p"},
|
|
{"invalid", "unknown", "p"},
|
|
{"user", "", "password"},
|
|
{"invalid", "", "p"},
|
|
{"", "domain", "password"},
|
|
{"", "", ""},
|
|
}
|
|
for _, c := range cases {
|
|
check(t, a, c.user, c.domain, c.password, false)
|
|
}
|
|
}
|
|
|
|
func check(t *testing.T, a *Authenticator, user, domain, passwd string, expect bool) {
|
|
c := fmt.Sprintf("{%s@%s %s}", user, domain, passwd)
|
|
ts := time.Now()
|
|
tr := trace.New("test", "check")
|
|
defer tr.Finish()
|
|
|
|
ok, err := a.Authenticate(tr, user, domain, passwd)
|
|
if time.Since(ts) < a.AuthDuration {
|
|
t.Errorf("auth on %v was too fast", c)
|
|
}
|
|
if ok != expect {
|
|
t.Errorf("auth on %v: got %v, expected %v", c, ok, expect)
|
|
}
|
|
if err != nil {
|
|
t.Errorf("auth on %v: got error %v", c, err)
|
|
}
|
|
|
|
ok, err = a.Exists(tr, user, domain)
|
|
if ok != expect {
|
|
t.Errorf("exists on %v: got %v, expected %v", c, ok, expect)
|
|
}
|
|
if err != nil {
|
|
t.Errorf("exists on %v: error %v", c, err)
|
|
}
|
|
}
|
|
|
|
func TestInterfaces(t *testing.T) {
|
|
var _ NoErrorBackend = userdb.New("/dev/null")
|
|
var _ Backend = dovecot.NewAuth("/dev/null", "/dev/null")
|
|
}
|
|
|
|
// Backend implementation for testing.
|
|
type TestBE struct {
|
|
users map[string]string
|
|
reloadCount int
|
|
nextError error
|
|
}
|
|
|
|
func NewTestBE() *TestBE {
|
|
return &TestBE{
|
|
users: map[string]string{},
|
|
}
|
|
}
|
|
func (d *TestBE) add(user, password string) {
|
|
d.users[user] = password
|
|
}
|
|
|
|
func (d *TestBE) Authenticate(user, password string) (bool, error) {
|
|
if d.nextError != nil {
|
|
return false, d.nextError
|
|
}
|
|
|
|
if validP, ok := d.users[user]; ok {
|
|
return validP == password, nil
|
|
}
|
|
return false, nil
|
|
}
|
|
|
|
func (d *TestBE) Exists(user string) (bool, error) {
|
|
if d.nextError != nil {
|
|
return false, d.nextError
|
|
}
|
|
_, ok := d.users[user]
|
|
return ok, nil
|
|
}
|
|
|
|
func (d *TestBE) Reload() error {
|
|
d.reloadCount++
|
|
if d.nextError != nil {
|
|
return d.nextError
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func TestMultipleBackends(t *testing.T) {
|
|
domain1 := NewTestBE()
|
|
domain2 := NewTestBE()
|
|
fallback := NewTestBE()
|
|
|
|
a := NewAuthenticator()
|
|
a.Register("domain1", domain1)
|
|
a.Register("domain2", domain2)
|
|
a.Fallback = fallback
|
|
|
|
// Shorten the duration to speed up the test. This should still be long
|
|
// enough for it to fail if we don't sleep intentionally.
|
|
a.AuthDuration = 20 * time.Millisecond
|
|
|
|
domain1.add("user1", "passwd1")
|
|
domain2.add("user2", "passwd2")
|
|
fallback.add("user3@fallback", "passwd3")
|
|
fallback.add("user4@domain1", "passwd4")
|
|
|
|
// Successful tests.
|
|
cases := []struct{ user, domain, password string }{
|
|
{"user1", "domain1", "passwd1"},
|
|
{"user2", "domain2", "passwd2"},
|
|
{"user3", "fallback", "passwd3"},
|
|
{"user4", "domain1", "passwd4"},
|
|
}
|
|
for _, c := range cases {
|
|
check(t, a, c.user, c.domain, c.password, true)
|
|
}
|
|
|
|
// Unsuccessful tests (users don't exist).
|
|
cases = []struct{ user, domain, password string }{
|
|
{"nobody", "domain1", "p"},
|
|
{"nobody", "domain2", "p"},
|
|
{"nobody", "fallback", "p"},
|
|
{"user3", "", "p"},
|
|
}
|
|
for _, c := range cases {
|
|
check(t, a, c.user, c.domain, c.password, false)
|
|
}
|
|
}
|
|
|
|
func TestErrors(t *testing.T) {
|
|
be := NewTestBE()
|
|
be.add("user", "passwd")
|
|
|
|
a := NewAuthenticator()
|
|
a.Register("domain", be)
|
|
a.AuthDuration = 0
|
|
|
|
tr := trace.New("test", "TestErrors")
|
|
defer tr.Finish()
|
|
|
|
ok, err := a.Authenticate(tr, "user", "domain", "passwd")
|
|
if err != nil || !ok {
|
|
t.Fatalf("failed auth")
|
|
}
|
|
|
|
expectedErr := fmt.Errorf("test error")
|
|
be.nextError = expectedErr
|
|
|
|
ok, err = a.Authenticate(tr, "user", "domain", "passwd")
|
|
if ok {
|
|
t.Errorf("authentication succeeded, expected error")
|
|
}
|
|
if err != expectedErr {
|
|
t.Errorf("expected error, got %v", err)
|
|
}
|
|
|
|
ok, err = a.Exists(tr, "user", "domain")
|
|
if ok {
|
|
t.Errorf("exists succeeded, expected error")
|
|
}
|
|
if err != expectedErr {
|
|
t.Errorf("expected error, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestReload(t *testing.T) {
|
|
be1 := NewTestBE()
|
|
be2 := NewTestBE()
|
|
fallback := NewTestBE()
|
|
|
|
a := NewAuthenticator()
|
|
a.Register("domain1", be1)
|
|
a.Register("domain2", be2)
|
|
a.Fallback = fallback
|
|
|
|
err := a.Reload()
|
|
if err != nil {
|
|
t.Errorf("unexpected error reloading: %v", err)
|
|
}
|
|
if be1.reloadCount != 1 || be2.reloadCount != 1 || fallback.reloadCount != 1 {
|
|
t.Errorf("unexpected reload counts: %d %d %d != 1 1 1",
|
|
be1.reloadCount, be2.reloadCount, fallback.reloadCount)
|
|
}
|
|
|
|
be2.nextError = fmt.Errorf("test error")
|
|
err = a.Reload()
|
|
if err == nil {
|
|
t.Errorf("expected error reloading, got nil")
|
|
}
|
|
if be1.reloadCount != 2 || be2.reloadCount != 2 || fallback.reloadCount != 2 {
|
|
t.Errorf("unexpected reload counts: %d %d %d != 2 2 2",
|
|
be1.reloadCount, be2.reloadCount, fallback.reloadCount)
|
|
}
|
|
|
|
a2 := NewAuthenticator()
|
|
a2.Register("domain", WrapNoErrorBackend(userdb.New("/dev/null")))
|
|
if err = a2.Reload(); err != nil {
|
|
t.Errorf("unexpected error reloading wrapped backend: %v", err)
|
|
}
|
|
}
|
|
|
|
// Fuzz testing for the response decoder, which handles user-provided data.
|
|
func FuzzDecodeResponse(f *testing.F) {
|
|
f.Fuzz(func(t *testing.T, response string) {
|
|
DecodeResponse(response)
|
|
})
|
|
}
|