From 839ccf19fefae04a2b150d49f139cbb010c16e35 Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 11:00:01 +0400 Subject: [PATCH 01/12] Move the handler into a struct --- pollen.go | 8 ++++++-- pollen_test.go | 8 +------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/pollen.go b/pollen.go index 4f0c04c..d469052 100644 --- a/pollen.go +++ b/pollen.go @@ -42,7 +42,10 @@ var ( dev *os.File ) -func handler(w http.ResponseWriter, r *http.Request) { +type PollenServer struct { +} + +func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { challenge := r.FormValue("challenge") if challenge == "" { http.Error(w, "Please use the pollinate client. 'sudo apt-get install pollinate' or download from: https://bazaar.launchpad.net/~pollinate/pollinate/trunk/view/head:/pollinate", http.StatusBadRequest) @@ -91,9 +94,10 @@ func main() { if *httpPort == "" && *httpsPort == "" { fatal("Nothing to do if http and https are both disabled") } + handler := &PollenServer{} httpAddr := fmt.Sprintf(":%s", *httpPort) httpsAddr := fmt.Sprintf(":%s", *httpsPort) - http.HandleFunc("/", handler) + http.Handle("/", handler) go func() { fatal(http.ListenAndServe(httpAddr, nil)) }() diff --git a/pollen_test.go b/pollen_test.go index a096e12..279f952 100644 --- a/pollen_test.go +++ b/pollen_test.go @@ -16,14 +16,8 @@ type Suite struct { t *testing.T } -type testHandler struct{} - -func (h testHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { - handler(w, req) -} - func NewSuite(t *testing.T) *Suite { - return &Suite{httptest.NewServer(testHandler{}), t} + return &Suite{httptest.NewServer(&PollenServer{}), t} } func (s *Suite) Assert(v bool, args ...interface{}) { From bea0b774278a637938ae462cd2ae1a396c315e09 Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 11:17:48 +0400 Subject: [PATCH 02/12] Move the dev object as an io.ReadWriter attribute of the server --- pollen.go | 22 +++++++++++++--------- pollen_test.go | 32 +++++++++++++++++++++++++++++++- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/pollen.go b/pollen.go index d469052..79dd698 100644 --- a/pollen.go +++ b/pollen.go @@ -39,10 +39,11 @@ var ( cert = flag.String("cert", "/etc/pollen/cert.pem", "The full path to cert.pem") key = flag.String("key", "/etc/pollen/key.pem", "The full path to key.pem") log *syslog.Writer - dev *os.File ) type PollenServer struct { + // randomSource is usually /dev/urandom + randomSource io.ReadWriter } func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -55,14 +56,14 @@ func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { io.WriteString(checksum, challenge) challengeResponse := checksum.Sum(nil) var err error - _, err = dev.Write(challengeResponse) + _, err = p.randomSource.Write(challengeResponse) if err != nil { /* Non-fatal error, but let's log this to syslog */ log.Err(fmt.Sprintf("Cannot write to random device at [%v]", time.Now().UnixNano())) } log.Info(fmt.Sprintf("Server received challenge from [%s, %s] at [%v]", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano())) data := make([]byte, *size) - _, err = io.ReadFull(dev, data) + _, err = io.ReadFull(p.randomSource, data) if err != nil { /* Fatal error for this connection, if we can't read from device */ log.Err(fmt.Sprintf("Cannot read from random device at [%v]", time.Now().UnixNano())) @@ -82,19 +83,22 @@ func init() { if err != nil { fatalf("Cannot open syslog:", err) } - dev, err = os.OpenFile(*device, os.O_RDWR, 0) - if err != nil { - fatalf("Cannot open device: %s\n", err) - } } func main() { flag.Parse() - defer dev.Close() if *httpPort == "" && *httpsPort == "" { fatal("Nothing to do if http and https are both disabled") } - handler := &PollenServer{} + dev, err := os.OpenFile(*device, os.O_RDWR, 0) + if err != nil { + fatalf("Cannot open device: %s\n", err) + } + defer dev.Close() + handler := &PollenServer{randomSource: dev} + // TODO: jam 2014-02-26 + // if httpPort == "" (or httpsPort) we shouldn't start the associated + // listener httpAddr := fmt.Sprintf(":%s", *httpPort) httpsAddr := fmt.Sprintf(":%s", *httpsPort) http.Handle("/", handler) diff --git a/pollen_test.go b/pollen_test.go index 279f952..09906c6 100644 --- a/pollen_test.go +++ b/pollen_test.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "io" + "os" "net/http" "net/http/httptest" "net/url" @@ -14,10 +15,15 @@ import ( type Suite struct { *httptest.Server t *testing.T + dev *os.File } func NewSuite(t *testing.T) *Suite { - return &Suite{httptest.NewServer(&PollenServer{}), t} + dev, err := os.OpenFile(*device, os.O_RDWR, 0) + if err != nil { + t.Fatalf("Cannot open device: %s\n", err) + } + return &Suite{httptest.NewServer(&PollenServer{randomSource: dev}), t, dev} } func (s *Suite) Assert(v bool, args ...interface{}) { @@ -28,6 +34,7 @@ func (s *Suite) Assert(v bool, args ...interface{}) { func (s *Suite) TearDown() { s.Server.Close() + s.dev.Close() } // MustScan scans a single token. There must be a token available and it must @@ -141,3 +148,26 @@ func TestUniqueChaining(t *testing.T) { 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") +} From a8a753d5eda9f06c58c32eaf77e398edbf7eb9dd Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 11:29:38 +0400 Subject: [PATCH 03/12] abstract the logging behind an interface This allows the test suite to not spam syslog while testing. It will also let us test that messages actually get logged, etc. --- pollen.go | 49 ++++++++++++++++++++++++++++++++----------------- pollen_test.go | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 69 insertions(+), 18 deletions(-) diff --git a/pollen.go b/pollen.go index 79dd698..dfe8e18 100644 --- a/pollen.go +++ b/pollen.go @@ -41,9 +41,19 @@ var ( log *syslog.Writer ) +// this matches the syslog.Writer functions +type logger interface { + Close() error + Info(string) error + Err(string) error + Crit(string) error + Emerg(string) error +} + type PollenServer struct { // randomSource is usually /dev/urandom randomSource io.ReadWriter + log logger } func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -59,14 +69,14 @@ func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { _, err = p.randomSource.Write(challengeResponse) if err != nil { /* Non-fatal error, but let's log this to syslog */ - log.Err(fmt.Sprintf("Cannot write to random device at [%v]", time.Now().UnixNano())) + p.log.Err(fmt.Sprintf("Cannot write to random device at [%v]", time.Now().UnixNano())) } - log.Info(fmt.Sprintf("Server received challenge from [%s, %s] at [%v]", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano())) + p.log.Info(fmt.Sprintf("Server received challenge from [%s, %s] at [%v]", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano())) data := make([]byte, *size) _, err = io.ReadFull(p.randomSource, data) if err != nil { /* Fatal error for this connection, if we can't read from device */ - log.Err(fmt.Sprintf("Cannot read from random device at [%v]", time.Now().UnixNano())) + p.log.Err(fmt.Sprintf("Cannot read from random device at [%v]", time.Now().UnixNano())) http.Error(w, "Failed to read from random device", 500) return } @@ -74,15 +84,7 @@ func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { /* The checksum of the bytes from /dev/urandom is simply for print-ability, when debugging */ seed := checksum.Sum(nil) fmt.Fprintf(w, "%x\n%x\n", challengeResponse, seed) - log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v]", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano())) -} - -func init() { - var err error - log, err = syslog.New(syslog.LOG_ERR, "pollen") - if err != nil { - fatalf("Cannot open syslog:", err) - } + p.log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v]", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano())) } func main() { @@ -90,12 +92,17 @@ func main() { if *httpPort == "" && *httpsPort == "" { fatal("Nothing to do if http and https are both disabled") } + log, err := syslog.New(syslog.LOG_ERR, "pollen") + if err != nil { + fatalf("Cannot open syslog:", err) + } + defer log.Close() dev, err := os.OpenFile(*device, os.O_RDWR, 0) if err != nil { fatalf("Cannot open device: %s\n", err) } defer dev.Close() - handler := &PollenServer{randomSource: dev} + handler := &PollenServer{randomSource: dev, log: log} // TODO: jam 2014-02-26 // if httpPort == "" (or httpsPort) we shouldn't start the associated // listener @@ -103,19 +110,27 @@ func main() { httpsAddr := fmt.Sprintf(":%s", *httpsPort) http.Handle("/", handler) go func() { - fatal(http.ListenAndServe(httpAddr, nil)) + handler.fatal(http.ListenAndServe(httpAddr, nil)) }() - fatal(http.ListenAndServeTLS(httpsAddr, *cert, *key, nil)) + handler.fatal(http.ListenAndServeTLS(httpsAddr, *cert, *key, nil)) +} + +func (p *PollenServer) fatal(args ...interface{}) { + p.log.Crit(fmt.Sprint(args...)) + fatal(args...) +} + +func (p *PollenServer) fatalf(format string, args ...interface{}) { + p.log.Emerg(fmt.Sprintf(format, args...)) + fatalf(format, args...) } func fatal(args ...interface{}) { - log.Crit(fmt.Sprint(args...)) fmt.Fprint(os.Stderr, args...) os.Exit(1) } func fatalf(format string, args ...interface{}) { - log.Emerg(fmt.Sprintf(format, args...)) fmt.Fprintf(os.Stderr, format, args...) os.Exit(1) } diff --git a/pollen_test.go b/pollen_test.go index 09906c6..08d39be 100644 --- a/pollen_test.go +++ b/pollen_test.go @@ -12,10 +12,45 @@ import ( "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 *os.File + logger *localLogger } func NewSuite(t *testing.T) *Suite { @@ -23,7 +58,8 @@ func NewSuite(t *testing.T) *Suite { if err != nil { t.Fatalf("Cannot open device: %s\n", err) } - return &Suite{httptest.NewServer(&PollenServer{randomSource: dev}), t, dev} + logger := &localLogger{} + return &Suite{httptest.NewServer(&PollenServer{randomSource: dev, log: logger}), t, dev, logger} } func (s *Suite) Assert(v bool, args ...interface{}) { From a04eb28213b9f96e04fd08a9a6704c1ec3ee4b7d Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 11:31:37 +0400 Subject: [PATCH 04/12] fix the size help text --- pollen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pollen.go b/pollen.go index dfe8e18..97ac665 100644 --- a/pollen.go +++ b/pollen.go @@ -35,7 +35,7 @@ var ( httpPort = flag.String("http-port", "80", "The HTTP port on which to listen") httpsPort = flag.String("https-port", "443", "The HTTPS port on which to listen") device = flag.String("device", "/dev/urandom", "The device to use for reading and writing random data") - size = flag.Int("bytes", 64, "The size in bytes to transmit and receive each time") + size = flag.Int("bytes", 64, "The size in bytes to read from the random device") cert = flag.String("cert", "/etc/pollen/cert.pem", "The full path to cert.pem") key = flag.String("key", "/etc/pollen/key.pem", "The full path to key.pem") log *syslog.Writer From 9f44ff05d1d6d904d95a5d6b551300f97e93ae7f Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 11:39:07 +0400 Subject: [PATCH 05/12] allow disabling https-port or http-port by passing "" This makes it easier to test the actual binary (for me at least) Use a WaitGroup so that we wait for whatever we *do* run. --- .gitignore | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9c3f7e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +pollen From 3ec3ebbb4b98cc030a1e2e60a488cc41d4119f14 Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 11:39:12 +0400 Subject: [PATCH 06/12] allow disabling https-port or http-port by passing "" This makes it easier to test the actual binary (for me at least) Use a WaitGroup so that we wait for whatever we *do* run. --- pollen.go | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/pollen.go b/pollen.go index 97ac665..7c602d9 100644 --- a/pollen.go +++ b/pollen.go @@ -28,6 +28,7 @@ import ( "log/syslog" "net/http" "os" + "sync" "time" ) @@ -103,16 +104,25 @@ func main() { } defer dev.Close() handler := &PollenServer{randomSource: dev, log: log} - // TODO: jam 2014-02-26 - // if httpPort == "" (or httpsPort) we shouldn't start the associated - // listener - httpAddr := fmt.Sprintf(":%s", *httpPort) - httpsAddr := fmt.Sprintf(":%s", *httpsPort) http.Handle("/", handler) - go func() { - handler.fatal(http.ListenAndServe(httpAddr, nil)) - }() - handler.fatal(http.ListenAndServeTLS(httpsAddr, *cert, *key, nil)) + var httpListeners sync.WaitGroup + if *httpPort != "" { + httpAddr := fmt.Sprintf(":%s", *httpPort) + httpListeners.Add(1) + go func() { + handler.fatal(http.ListenAndServe(httpAddr, nil)) + httpListeners.Done() + }() + } + if *httpsPort != "" { + httpsAddr := fmt.Sprintf(":%s", *httpsPort) + httpListeners.Add(1) + go func() { + handler.fatal(http.ListenAndServeTLS(httpsAddr, *cert, *key, nil)) + httpListeners.Done() + }() + } + httpListeners.Wait() } func (p *PollenServer) fatal(args ...interface{}) { From 85cfda9e2789ae229726ccae7a03b9d7e203d289 Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 11:58:29 +0400 Subject: [PATCH 07/12] go fmt and test with canned data It is nice to know how the random data is handled. --- pollen.go | 5 ++--- pollen_test.go | 44 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 41 insertions(+), 8 deletions(-) diff --git a/pollen.go b/pollen.go index 7c602d9..0412671 100644 --- a/pollen.go +++ b/pollen.go @@ -39,10 +39,9 @@ var ( size = flag.Int("bytes", 64, "The size in bytes to read from the random device") cert = flag.String("cert", "/etc/pollen/cert.pem", "The full path to cert.pem") key = flag.String("key", "/etc/pollen/key.pem", "The full path to key.pem") - log *syslog.Writer ) -// this matches the syslog.Writer functions +// this matches the syslog.Writer functions type logger interface { Close() error Info(string) error @@ -54,7 +53,7 @@ type logger interface { type PollenServer struct { // randomSource is usually /dev/urandom randomSource io.ReadWriter - log logger + log logger } func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/pollen_test.go b/pollen_test.go index 08d39be..fee9d67 100644 --- a/pollen_test.go +++ b/pollen_test.go @@ -2,19 +2,21 @@ package main import ( "bufio" + "bytes" + "crypto/sha512" "encoding/hex" "fmt" "io" - "os" "net/http" "net/http/httptest" "net/url" + "os" "testing" ) type logEntry struct { severity string - message string + message string } type localLogger struct { @@ -48,8 +50,8 @@ func (l *localLogger) Emerg(msg string) error { type Suite struct { *httptest.Server - t *testing.T - dev *os.File + t *testing.T + dev io.ReadWriter logger *localLogger } @@ -58,6 +60,10 @@ func NewSuite(t *testing.T) *Suite { 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{} return &Suite{httptest.NewServer(&PollenServer{randomSource: dev, log: logger}), t, dev, logger} } @@ -70,7 +76,9 @@ func (s *Suite) Assert(v bool, args ...interface{}) { func (s *Suite) TearDown() { s.Server.Close() - s.dev.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 @@ -207,3 +215,29 @@ func TestUniqueSeeds(t *testing.T) { 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) + 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) +} From 475e150cd43b1c745778d8bf4a552f0bd42af19d Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 12:14:09 +0400 Subject: [PATCH 08/12] Assert that changing the size actually changes how the backend works. --- pollen.go | 7 ++++--- pollen_test.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/pollen.go b/pollen.go index 0412671..b8ed156 100644 --- a/pollen.go +++ b/pollen.go @@ -54,6 +54,7 @@ type PollenServer struct { // randomSource is usually /dev/urandom randomSource io.ReadWriter log logger + readSize int } func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -72,7 +73,7 @@ func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { p.log.Err(fmt.Sprintf("Cannot write to random device at [%v]", time.Now().UnixNano())) } p.log.Info(fmt.Sprintf("Server received challenge from [%s, %s] at [%v]", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano())) - data := make([]byte, *size) + data := make([]byte, p.readSize) _, err = io.ReadFull(p.randomSource, data) if err != nil { /* Fatal error for this connection, if we can't read from device */ @@ -80,7 +81,7 @@ func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { http.Error(w, "Failed to read from random device", 500) return } - checksum.Write(data[:*size]) + checksum.Write(data) /* The checksum of the bytes from /dev/urandom is simply for print-ability, when debugging */ seed := checksum.Sum(nil) fmt.Fprintf(w, "%x\n%x\n", challengeResponse, seed) @@ -102,7 +103,7 @@ func main() { fatalf("Cannot open device: %s\n", err) } defer dev.Close() - handler := &PollenServer{randomSource: dev, log: log} + handler := &PollenServer{randomSource: dev, log: log, readSize: *size} http.Handle("/", handler) var httpListeners sync.WaitGroup if *httpPort != "" { diff --git a/pollen_test.go b/pollen_test.go index fee9d67..21a3405 100644 --- a/pollen_test.go +++ b/pollen_test.go @@ -53,6 +53,7 @@ type Suite struct { t *testing.T dev io.ReadWriter logger *localLogger + pollen *PollenServer } func NewSuite(t *testing.T) *Suite { @@ -65,7 +66,8 @@ func NewSuite(t *testing.T) *Suite { func NewSuiteWithDev(t *testing.T, dev io.ReadWriter) *Suite { logger := &localLogger{} - return &Suite{httptest.NewServer(&PollenServer{randomSource: dev, log: logger}), t, dev, logger} + 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{}) { @@ -233,6 +235,8 @@ func TestCannedContent(t *testing.T) { 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() @@ -240,4 +244,45 @@ func TestCannedContent(t *testing.T) { 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)) } From f22ca8411e0351fa43c78d705116c9eca4000f4c Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 12:56:02 +0400 Subject: [PATCH 09/12] more error message testing Test that we actually get an error code in the no-challenge case. Test that we handle when we can't write to the device. Test that we fail and log a message when we can't read from the device, and we send the appropriate message across the wire. --- pollen.go | 6 ++-- pollen_test.go | 79 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 82 insertions(+), 3 deletions(-) diff --git a/pollen.go b/pollen.go index b8ed156..adc6850 100644 --- a/pollen.go +++ b/pollen.go @@ -57,10 +57,12 @@ type PollenServer struct { readSize int } +const usePollinateError = "Please use the pollinate client. 'sudo apt-get install pollinate' or download from: https://bazaar.launchpad.net/~pollinate/pollinate/trunk/view/head:/pollinate" + func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { challenge := r.FormValue("challenge") if challenge == "" { - http.Error(w, "Please use the pollinate client. 'sudo apt-get install pollinate' or download from: https://bazaar.launchpad.net/~pollinate/pollinate/trunk/view/head:/pollinate", http.StatusBadRequest) + http.Error(w, usePollinateError, http.StatusBadRequest) return } checksum := sha512.New() @@ -78,7 +80,7 @@ func (p *PollenServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { if err != nil { /* Fatal error for this connection, if we can't read from device */ p.log.Err(fmt.Sprintf("Cannot read from random device at [%v]", time.Now().UnixNano())) - http.Error(w, "Failed to read from random device", 500) + http.Error(w, "Failed to read from random device", http.StatusInternalServerError) return } checksum.Write(data) diff --git a/pollen_test.go b/pollen_test.go index 21a3405..bbc5d11 100644 --- a/pollen_test.go +++ b/pollen_test.go @@ -123,8 +123,11 @@ func TestNoChallenge(t *testing.T) { res, err := http.Get(s.URL) s.Assert(err == nil, "http client error:", err) defer res.Body.Close() - _, _, err = ReadResp(res.Body) + 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) { @@ -286,3 +289,77 @@ func TestExtraSize(t *testing.T) { 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: "", 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: "", 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: "", 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]) +} From 17070d92c1d6e184cfd3065ef87fc5f43403b93c Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 16:25:40 +0400 Subject: [PATCH 10/12] go fmt, and some formatting tweaks when using fatal it wasn't appending a newline, so messages were confusing this fixes that. also, add millisecond-level timing for how long it takes us to respond to requests. The *data* is available in UnixNano() but it requires doing the subtraction somehow, and also misses some of the overhead as the 'received challenge' only triggers after we've processed some of the request. --- pollen.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/pollen.go b/pollen.go index 4f0c04c..8398a92 100644 --- a/pollen.go +++ b/pollen.go @@ -38,11 +38,12 @@ var ( size = flag.Int("bytes", 64, "The size in bytes to transmit and receive each time") cert = flag.String("cert", "/etc/pollen/cert.pem", "The full path to cert.pem") key = flag.String("key", "/etc/pollen/key.pem", "The full path to key.pem") - log *syslog.Writer - dev *os.File + log *syslog.Writer + dev *os.File ) func handler(w http.ResponseWriter, r *http.Request) { + startTime := time.Now() challenge := r.FormValue("challenge") if challenge == "" { http.Error(w, "Please use the pollinate client. 'sudo apt-get install pollinate' or download from: https://bazaar.launchpad.net/~pollinate/pollinate/trunk/view/head:/pollinate", http.StatusBadRequest) @@ -70,14 +71,14 @@ func handler(w http.ResponseWriter, r *http.Request) { /* The checksum of the bytes from /dev/urandom is simply for print-ability, when debugging */ seed := checksum.Sum(nil) fmt.Fprintf(w, "%x\n%x\n", challengeResponse, seed) - log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v]", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano())) + log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v] in %.3fs", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano(), time.Since(startTime))) } func init() { var err error log, err = syslog.New(syslog.LOG_ERR, "pollen") if err != nil { - fatalf("Cannot open syslog:", err) + fatalf("Cannot open syslog: %s\n", err) } dev, err = os.OpenFile(*device, os.O_RDWR, 0) if err != nil { @@ -102,6 +103,7 @@ func main() { func fatal(args ...interface{}) { log.Crit(fmt.Sprint(args...)) + args = append(args, "\n") fmt.Fprint(os.Stderr, args...) os.Exit(1) } From 055ed371fbaa972c955f9e387f5b651aa638230a Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 16:30:47 +0400 Subject: [PATCH 11/12] actually do the right formatting --- pollen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pollen.go b/pollen.go index 8398a92..312bc56 100644 --- a/pollen.go +++ b/pollen.go @@ -71,7 +71,7 @@ func handler(w http.ResponseWriter, r *http.Request) { /* The checksum of the bytes from /dev/urandom is simply for print-ability, when debugging */ seed := checksum.Sum(nil) fmt.Fprintf(w, "%x\n%x\n", challengeResponse, seed) - log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v] in %.3fs", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano(), time.Since(startTime))) + log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v] in %.3fs", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano(), time.Since(startTime).Seconds())) } func init() { From cd0117f280634ed8e810e75dfdced8bc15b4a5e3 Mon Sep 17 00:00:00 2001 From: John Arbash Meinel Date: Wed, 26 Feb 2014 17:11:50 +0400 Subject: [PATCH 12/12] use microsecond timing (ms was always 0) also, add a log when the process starts --- pollen.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pollen.go b/pollen.go index 312bc56..a3c76aa 100644 --- a/pollen.go +++ b/pollen.go @@ -71,7 +71,8 @@ func handler(w http.ResponseWriter, r *http.Request) { /* The checksum of the bytes from /dev/urandom is simply for print-ability, when debugging */ seed := checksum.Sum(nil) fmt.Fprintf(w, "%x\n%x\n", challengeResponse, seed) - log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v] in %.3fs", r.RemoteAddr, r.UserAgent(), time.Now().UnixNano(), time.Since(startTime).Seconds())) + log.Info(fmt.Sprintf("Server sent response to [%s, %s] at [%v] in %.6fs", + r.RemoteAddr, r.UserAgent(), time.Now().UnixNano(), time.Since(startTime).Seconds())) } func init() { @@ -88,6 +89,7 @@ func init() { func main() { flag.Parse() + log.Info(fmt.Sprintf("pollen starting at [%v]", time.Now().UnixNano())) defer dev.Close() if *httpPort == "" && *httpsPort == "" { fatal("Nothing to do if http and https are both disabled")