1//go:build linux
2
3package auth
4
5import (
6 "bytes"
7 "context"
8 "errors"
9 "io"
10 "os"
11 "os/exec"
12 "sync"
13 "testing"
14 "time"
15
16 "github.com/bnema/neferafk/internal/ports"
17 "golang.org/x/sys/unix"
18)
19
20const hardenChildEnv = "NEFERAFK_TEST_HARDEN_CHILD"
21
22// TestHardenProcessDisablesDumpsAndCoreLimit runs in a subprocess because the
23// helper changes process-wide state.
24func TestHardenProcessDisablesDumpsAndCoreLimit(t *testing.T) {
25 if os.Getenv(hardenChildEnv) == "1" {
26 if dumpable, err := unix.PrctlRetInt(unix.PR_GET_DUMPABLE, 0, 0, 0, 0); err != nil || dumpable != 1 {
27 t.Fatalf("precondition: dumpable=%d err=%v", dumpable, err)
28 }
29 if err := HardenProcess(); err != nil {
30 t.Fatal(err)
31 }
32 if dumpable, err := unix.PrctlRetInt(unix.PR_GET_DUMPABLE, 0, 0, 0, 0); err != nil || dumpable != 0 {
33 t.Fatalf("dumpable=%d err=%v", dumpable, err)
34 }
35 var lim unix.Rlimit
36 if err := unix.Getrlimit(unix.RLIMIT_CORE, &lim); err != nil || lim.Cur != 0 || lim.Max != 0 {
37 t.Fatalf("RLIMIT_CORE %+v err=%v", lim, err)
38 }
39 return
40 }
41 cmd := exec.Command(os.Args[0], "-test.run=^TestHardenProcessDisablesDumpsAndCoreLimit$")
42 cmd.Env = append(os.Environ(), hardenChildEnv+"=1")
43 if out, err := cmd.CombinedOutput(); err != nil {
44 t.Fatalf("child: %v\n%s", err, out)
45 }
46}
47
48func TestDenyDelaySchedule(t *testing.T) {
49 want := []time.Duration{0, time.Second, 2 * time.Second, 4 * time.Second, 8 * time.Second, 30 * time.Second, 30 * time.Second, 30 * time.Second}
50 for n, w := range want {
51 if got := denyDelay(n); got != w {
52 t.Fatalf("denyDelay(%d)=%v want %v", n, got, w)
53 }
54 }
55}
56
57func readyClock() <-chan time.Time {
58 c := make(chan time.Time, 1)
59 c <- time.Time{}
60 return c
61}
62
63func attemptFrames(t *testing.T, in *bytes.Buffer, pins ...string) {
64 t.Helper()
65 for i, p := range pins {
66 if err := WriteFrame(in, ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 8, Attempt: uint64(i + 1), Payload: []byte(p)}); err != nil {
67 t.Fatal(err)
68 }
69 }
70}
71
72func TestWrongPINDelaysGrowThenCapAndSuccessIsNotDelayed(t *testing.T) {
73 var in, out bytes.Buffer
74 attemptFrames(t, &in, "000001", "000002", "000003", "000004", "000005", "000006", "123456")
75 clock := newMockdelayClock(t)
76 for _, d := range []time.Duration{1, 2, 4, 8, 30, 30} {
77 clock.EXPECT().after(d * time.Second).Return(readyClock()).Once()
78 }
79 // The right PIN is answered without any delay (no further clock call).
80 if err := runWith(context.Background(), clock, &in, &out, 8, []byte("123456"), nil, "", false); err != nil {
81 t.Fatal(err)
82 }
83}
84
85func TestWrongPINDelayScheduleRestartsForAFreshWorker(t *testing.T) {
86 // Success ends the worker, so the reset is observable as: the first wrong
87 // answer of every fresh worker waits 1s again (no shared counter).
88 for range 2 {
89 var in, out bytes.Buffer
90 attemptFrames(t, &in, "000001", "123456")
91 clock := newMockdelayClock(t)
92 clock.EXPECT().after(time.Second).Return(readyClock()).Once()
93 if err := runWith(context.Background(), clock, &in, &out, 8, []byte("123456"), nil, "", false); err != nil {
94 t.Fatal(err)
95 }
96 }
97}
98
99func TestDelayedDenialIsAnsweredOnlyAfterTheClockFires(t *testing.T) {
100 inR, inW := io.Pipe()
101 var out lockedBuffer
102 fire := make(chan time.Time)
103 clock := newMockdelayClock(t)
104 clock.EXPECT().after(time.Second).Return((<-chan time.Time)(fire)).Once()
105 done := make(chan error, 1)
106 go func() { done <- runWith(context.Background(), clock, inR, &out, 8, []byte("123456"), nil, "", false) }()
107 if err := WriteFrame(inW, ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 8, Attempt: 1, Payload: []byte("000000")}); err != nil {
108 t.Fatal(err)
109 }
110 time.Sleep(50 * time.Millisecond)
111 if n := out.frames(); n != 1 { // ready only: no denial yet
112 t.Fatalf("%d frames before the delay elapsed", n)
113 }
114 fire <- time.Time{}
115 if err := WriteFrame(inW, ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 8, Attempt: 2, Payload: []byte("123456")}); err != nil {
116 t.Fatal(err)
117 }
118 if err := <-done; err != nil {
119 t.Fatal(err)
120 }
121 if n := out.frames(); n != 3 {
122 t.Fatalf("%d frames", n)
123 }
124}
125
126func TestDelayEndsOnEOFAndCancellationWithoutAnswer(t *testing.T) {
127 for _, name := range []string{"eof", "cancel"} {
128 inR, inW := io.Pipe()
129 var out lockedBuffer
130 ctx, cancel := context.WithCancel(context.Background())
131 clock := newMockdelayClock(t)
132 clock.EXPECT().after(time.Second).Return((<-chan time.Time)(make(chan time.Time))).Once() // never fires
133 done := make(chan error, 1)
134 go func() { done <- runWith(ctx, clock, inR, &out, 8, []byte("123456"), nil, "", false) }()
135 if err := WriteFrame(inW, ports.AuthFrame{Kind: ports.AuthAttempt, Generation: 8, Attempt: 1, Payload: []byte("000000")}); err != nil {
136 t.Fatal(err)
137 }
138 want := io.EOF
139 if name == "eof" {
140 time.Sleep(20 * time.Millisecond)
141 inW.Close()
142 } else {
143 want = context.Canceled
144 time.Sleep(20 * time.Millisecond)
145 cancel()
146 }
147 select {
148 case err := <-done:
149 if !errors.Is(err, want) {
150 t.Fatalf("%s: %v", name, err)
151 }
152 case <-time.After(5 * time.Second):
153 t.Fatalf("%s: delay did not end", name)
154 }
155 if n := out.frames(); n != 1 {
156 t.Fatalf("%s: answered denial during the delay (%d frames)", name, n)
157 }
158 cancel()
159 }
160}
161
162type lockedBuffer struct {
163 mu sync.Mutex
164 b bytes.Buffer
165}
166
167func (l *lockedBuffer) Write(p []byte) (int, error) {
168 l.mu.Lock()
169 defer l.mu.Unlock()
170 return l.b.Write(p)
171}
172
173func (l *lockedBuffer) frames() int {
174 l.mu.Lock()
175 cp := bytes.NewBuffer(bytes.Clone(l.b.Bytes()))
176 l.mu.Unlock()
177 n := 0
178 for {
179 if _, err := ReadFrame(cp); err != nil {
180 return n
181 }
182 n++
183 }
184}