-
Notifications
You must be signed in to change notification settings - Fork 10
/
pollen_test.go
366 lines (316 loc) · 12 KB
/
pollen_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
package main
import (
"bufio"
"bytes"
"crypto/sha512"
"encoding/hex"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"os"
"testing"
)
type logEntry struct {
severity string
message string
}
type localLogger struct {
logs []logEntry
}
func (l *localLogger) Close() error {
l.logs = append(l.logs, logEntry{"close", ""})
return nil
}
func (l *localLogger) Info(msg string) error {
l.logs = append(l.logs, logEntry{"info", msg})
return nil
}
func (l *localLogger) Err(msg string) error {
l.logs = append(l.logs, logEntry{"err", msg})
return nil
}
func (l *localLogger) Crit(msg string) error {
l.logs = append(l.logs, logEntry{"crit", msg})
return nil
}
func (l *localLogger) Emerg(msg string) error {
l.logs = append(l.logs, logEntry{"emerg", msg})
return nil
}
type Suite struct {
*httptest.Server
t *testing.T
dev io.ReadWriter
logger *localLogger
pollen *PollenServer
}
func NewSuite(t *testing.T) *Suite {
/* hardcode /dev/urandom for testing purposes */
dev, err := os.OpenFile("/dev/urandom", os.O_RDWR, 0)
if err != nil {
t.Fatalf("Cannot open device: %s\n", err)
}
return NewSuiteWithDev(t, dev)
}
func NewSuiteWithDev(t *testing.T, dev io.ReadWriter) *Suite {
logger := &localLogger{}
handler := &PollenServer{randomSource: dev, log: logger, readSize: 64}
return &Suite{httptest.NewServer(handler), t, dev, logger, handler}
}
func (s *Suite) Assert(v bool, args ...interface{}) {
if !v {
s.t.Error(args...)
}
}
func (s *Suite) TearDown() {
s.Server.Close()
if closer, ok := s.dev.(io.Closer); ok {
closer.Close()
}
}
// MustScan scans a single token. There must be a token available and it must
// scan successfully or an error is returned.
func MustScan(s *bufio.Scanner) error {
if !s.Scan() {
return fmt.Errorf("Missing expected text")
}
return s.Err()
}
// ParseResp parses the pollen response to the challenge & response
// in the output, as well as any error that occurred with reading or
// validating it.
func ReadResp(r io.Reader) (challenge, response string, err error) {
scanner := bufio.NewScanner(r)
if err = MustScan(scanner); err != nil {
return
}
challenge = scanner.Text()
if err = MustScan(scanner); err != nil {
return
}
response = scanner.Text()
return
}
// CheckHex returns an error if the given string is not valid hex.
func CheckHex(s string) error {
_, err := hex.DecodeString(s)
return err
}
// TestNoChallenge tests the pollen service when no challenge is given
// in the request.
func TestNoChallenge(t *testing.T) {
s := NewSuite(t)
defer s.TearDown()
res, err := http.Get(s.URL)
s.Assert(err == nil, "http client error:", err)
defer res.Body.Close()
chal, seed, err := ReadResp(res.Body)
s.Assert(err != nil, "response error:", err)
s.Assert(res.StatusCode == http.StatusBadRequest, "didn't get Bad Request, got: ", res.Status)
s.Assert(chal == usePollinateError, "got the wrong error message:", chal)
s.Assert(seed == "", "got extra messages:", seed)
}
func (s *Suite) SanityCheck(chal, seed string) {
s.Assert(chal != seed, "challenge response and seed were the same!")
s.Assert(len(chal) == len(seed), "challenge response and seed length not equal")
s.Assert(CheckHex(chal) == nil, "invalid hex:", chal)
s.Assert(CheckHex(seed) == nil, "invalid hex:", seed)
}
// PorkChopSha512 is $(echo -n "pork chop sandwiches" | sha512sum)
const PorkChopSha512 = "a75751ccd71ba00d7b6c3b74cc0c02373f3f26c14dfe47afd580b0d87bf9fd8cebc73ea29b1cae15586e0d118922342ea7e94d0cb73a0f918d7d8c7ec065e873"
// TestPorkChopSandwiches tests the pollen service when given
// pork chop sandwiches.
func TestPorkChopSandwiches(t *testing.T) {
s := NewSuite(t)
defer s.TearDown()
res, err := http.Get(s.URL + "?challenge=pork+chop+sandwiches")
s.Assert(err == nil, "http client error:", err)
defer res.Body.Close()
chal, resp, err := ReadResp(res.Body)
s.Assert(err == nil, "response error:", err)
s.Assert(chal == PorkChopSha512, "expected:", PorkChopSha512, "got:", chal)
s.SanityCheck(chal, resp)
}
// TestPorkChopPost tests the pollen service when the
// pork chop sandwiches are POSTed.
func TestPostChopSandwiches(t *testing.T) {
s := NewSuite(t)
defer s.TearDown()
res, err := http.PostForm(s.URL, url.Values{"challenge": []string{"pork chop sandwiches"}})
s.Assert(err == nil, "http client error:", err)
defer res.Body.Close()
chal, resp, err := ReadResp(res.Body)
s.Assert(err == nil, "response error:", err)
s.Assert(chal == PorkChopSha512, "expected:", PorkChopSha512, "got:", chal)
s.SanityCheck(chal, resp)
}
const UniqueChainRounds = 100
// TestUniqueChaining tests the uniqueness of seeds and challenge responses
// when fed into successive requests as challenges.
func TestUniqueChaining(t *testing.T) {
s := NewSuite(t)
defer s.TearDown()
challengeResps := make(map[string]bool)
seeds := make(map[string]bool)
challenge := "the bassomatic '76"
for i := 0; i < UniqueChainRounds; i++ {
res, err := http.Get(fmt.Sprintf("%s/?challenge=%s", s.URL, url.QueryEscape(challenge)))
s.Assert(err == nil, "http client error:", err)
challengeResp, seed, err := ReadResp(res.Body)
err = res.Body.Close()
s.Assert(err == nil, "response error:", err)
challengeResps[challengeResp] = true
seeds[seed] = true
challenge = seed
}
s.Assert(len(challengeResps) == UniqueChainRounds, "non-unique challenge response")
s.Assert(len(seeds) == UniqueChainRounds, "non-unique seed response")
}
// TestUniqueSeeds tests the uniqueness of responses to the same challenge
func TestUniqueSeeds(t *testing.T) {
s := NewSuite(t)
defer s.TearDown()
challengeResps := make(map[string]bool)
seeds := make(map[string]bool)
challenge := "the bassomatic '76"
for i := 0; i < UniqueChainRounds; i++ {
res, err := http.Get(fmt.Sprintf("%s/?challenge=%s", s.URL, url.QueryEscape(challenge)))
s.Assert(err == nil, "http client error:", err)
challengeResp, seed, err := ReadResp(res.Body)
err = res.Body.Close()
s.Assert(err == nil, "response error:", err)
challengeResps[challengeResp] = true
seeds[seed] = true
}
s.Assert(len(challengeResps) == 1, "more than one sha sum for the same challenge")
s.Assert(len(seeds) == UniqueChainRounds, "non-unique seed response")
}
// DilbertRandom is 64 bytes of pure nines
var DilbertRandom = "ninenineninenineninenineninenineninenineninenineninenineninenine"
var DilbertRandomSHA1 = "f73655d899f0f3d181d8e94b163e774a05abdd3b55123d0b9b2f18ad8c05c76e6fde93ba9dfc350acc2e378b59dd6962fc305b741f9a5b7edb16435e61a86b96"
// TestCannedContent exercises the input and output removing the randomness of rand
func TestCannedContent(t *testing.T) {
b := bytes.NewBufferString(DilbertRandom)
s := NewSuiteWithDev(t, b)
defer s.TearDown()
res, err := http.Get(s.URL + "?challenge=pork+chop+sandwiches")
s.Assert(err == nil, "http client error:", err)
defer res.Body.Close()
chal, seed, err := ReadResp(res.Body)
s.Assert(err == nil, "response error:", err)
s.Assert(chal == PorkChopSha512, "expected:", PorkChopSha512, "got:", chal)
s.SanityCheck(chal, seed)
// Check that the 'random' seed we got back was appropriately mixed
// with the challenge
s.Assert(seed != DilbertRandom, "got the raw random content")
s.Assert(seed != DilbertRandomSHA1, "got the sha of random content without the challenge")
expectedSum := sha512.New()
io.WriteString(expectedSum, "pork chop sandwiches")
io.WriteString(expectedSum, DilbertRandom)
expectedSeed := fmt.Sprintf("%x", expectedSum.Sum(nil))
s.Assert(seed == expectedSeed, "expected:", expectedSeed, "got:", seed)
// We can also check that the challenge was correctly written to our random device
// b.Bytes() is the remainder of our buffer, and Buffer writes at the end
// This also shows that we didn't write the raw request
writtenBytesInHex := fmt.Sprintf("%x", string(b.Bytes()))
s.Assert(PorkChopSha512 == writtenBytesInHex, "expected:", PorkChopSha512, "got:", writtenBytesInHex)
}
// TestSizeMatters asserts that changing 'size' changes how many bytes we read
func TestSizeMatters(t *testing.T) {
b := bytes.NewBufferString(DilbertRandom)
s := NewSuiteWithDev(t, b)
defer s.TearDown()
s.pollen.readSize = 32
res, err := http.Get(s.URL + "?challenge=xxx")
s.Assert(err == nil, "http client error:", err)
defer res.Body.Close()
_, _, err = ReadResp(res.Body)
s.Assert(err == nil, "response err:", err)
// If we set the readSize to 32 bytes, then we should only have that
// much data read from the buffer
remaining := b.Bytes()
// We have to add the 64 bytes that we wrote because of the challenge
s.Assert(len(remaining) == 32+64, "wrong number of bytes remaining, expected 96 got:", len(remaining))
}
// TestExtraSize asserts that you can make size 'big'
func TestExtraSize(t *testing.T) {
b := bytes.NewBufferString(DilbertRandom)
s := NewSuiteWithDev(t, b)
defer s.TearDown()
// We only start with 64 bytes of "nine" but we add the challenge to the pool
s.pollen.readSize = 128
res, err := http.Get(s.URL + "?challenge=xxx")
s.Assert(err == nil, "http client error:", err)
defer res.Body.Close()
_, _, err = ReadResp(res.Body)
s.Assert(err == nil, "response err:", err)
remaining := b.Bytes()
s.Assert(len(remaining) == 0, "wrong number of bytes remaining, expected 0 got:", len(remaining))
}
type OnlyReader struct {
*bytes.Buffer
}
func (o *OnlyReader) Write([]byte) (int, error) {
return 0, &os.PathError{Op: "write", Path: "<mem>", Err: os.ErrPermission}
}
// We have to implement this because bytes.Buffer does, and io.WriteString can chose to use it
func (o *OnlyReader) WriteString(string) (int, error) {
return 0, &os.PathError{Op: "write", Path: "<mem>", Err: os.ErrPermission}
}
// TestWriteFailure tests that if we can't write to our random device, we keep going
func TestWriteFailure(t *testing.T) {
b := &OnlyReader{bytes.NewBufferString(DilbertRandom)}
s := NewSuiteWithDev(t, b)
defer s.TearDown()
res, err := http.Get(s.URL + "?challenge=xxx")
s.Assert(err == nil, "http client error:", err)
defer res.Body.Close()
chal, seed, err := ReadResp(res.Body)
s.Assert(err == nil, "response err:", err)
s.SanityCheck(chal, seed)
// Failing to write to the random device is logged
s.Assert(len(s.logger.logs) == 3, "expected 3 log messages, got:", len(s.logger.logs))
start := "Cannot write to random device at ["
s.Assert(s.logger.logs[0].severity == "err" &&
s.logger.logs[0].message[:len(start)] == start,
"didn't get the expected error message, got:", s.logger.logs[0])
start = "Server received challenge from ["
s.Assert(s.logger.logs[1].severity == "info" &&
s.logger.logs[1].message[:len(start)] == start,
"didn't get the expected error message, got:", s.logger.logs[1])
start = "Server sent response to ["
s.Assert(s.logger.logs[2].severity == "info" &&
s.logger.logs[2].message[:len(start)] == start,
"didn't get the expected error message, got:", s.logger.logs[2])
}
type FailingReader struct {
*bytes.Buffer
}
func (o *FailingReader) Read([]byte) (int, error) {
return 0, &os.PathError{Op: "read", Path: "<mem>", Err: os.ErrPermission}
}
// TestReadFailure tests that if we can't read from our random device it is immediately fatal
func TestReadFailure(t *testing.T) {
// No random data to give to the client
b := &FailingReader{bytes.NewBufferString("")}
s := NewSuiteWithDev(t, b)
defer s.TearDown()
res, err := http.Get(s.URL + "?challenge=xxx")
s.Assert(err == nil, "http client error:", err)
defer res.Body.Close()
errMsg, _, err := ReadResp(res.Body)
s.Assert(err != nil, "response error:", err)
s.Assert(errMsg == "Failed to read from random device", "wrong error: ", errMsg)
s.Assert(res.StatusCode == http.StatusInternalServerError, "wrong status: ", res.Status)
s.Assert(len(s.logger.logs) == 2, "expected 2 log messages, got: ", len(s.logger.logs))
start := "Server received challenge from ["
s.Assert(s.logger.logs[0].severity == "info" &&
s.logger.logs[0].message[:len(start)] == start,
"didn't get the expected error message, got:", s.logger.logs[0])
start = "Cannot read from random device at ["
s.Assert(s.logger.logs[1].severity == "err" &&
s.logger.logs[1].message[:len(start)] == start,
"didn't get the expected error message, got:", s.logger.logs[1])
}