From fcd0fba9c74bbcab271c9a05fe31cd07610d3ba0 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sat, 13 Jun 2015 18:37:30 +0200 Subject: [PATCH 1/4] Add a basic execution check to SimpleHTTP --- acme/client.go | 8 ++++---- acme/dvsni_challenge.go | 2 +- acme/simple_http_challenge.go | 35 +++++++++++++++++++++++++++++++++-- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/acme/client.go b/acme/client.go index 902d7af5..39d1288a 100644 --- a/acme/client.go +++ b/acme/client.go @@ -35,7 +35,7 @@ type User interface { // Interface for all challenge solvers to implement. type solver interface { - CanSolve() bool + CanSolve(domain string) bool Solve(challenge challenge, domain string) error } @@ -150,7 +150,7 @@ func (c *Client) solveChallenges(challenges []*authorizationResource) error { // loop through the resources, basically through the domains. for _, authz := range challenges { // no solvers - no solving - if solvers := c.chooseSolvers(authz.Body); solvers != nil { + if solvers := c.chooseSolvers(authz.Body, authz.Domain); solvers != nil { for i, solver := range solvers { // TODO: do not immediately fail if one domain fails to validate. err := solver.Solve(authz.Body.Challenges[i], authz.Domain) @@ -168,11 +168,11 @@ func (c *Client) solveChallenges(challenges []*authorizationResource) error { // Checks all combinations from the server and returns an array of // solvers which should get executed in series. -func (c *Client) chooseSolvers(auth authorization) map[int]solver { +func (c *Client) chooseSolvers(auth authorization, domain string) map[int]solver { for _, combination := range auth.Combinations { solvers := make(map[int]solver) for _, idx := range combination { - if solver, ok := c.Solvers[auth.Challenges[idx].Type]; ok { + if solver, ok := c.Solvers[auth.Challenges[idx].Type]; ok && solver.CanSolve(domain) { solvers[idx] = solver } else { logger().Printf("Could not find solver for: %s", auth.Challenges[idx].Type) diff --git a/acme/dvsni_challenge.go b/acme/dvsni_challenge.go index 1715554a..ae5bf77a 100644 --- a/acme/dvsni_challenge.go +++ b/acme/dvsni_challenge.go @@ -2,7 +2,7 @@ package acme type dvsniChallenge struct{} -func (s *dvsniChallenge) CanSolve() bool { +func (s *dvsniChallenge) CanSolve(domain string) bool { return false } diff --git a/acme/simple_http_challenge.go b/acme/simple_http_challenge.go index f02bd48b..0192d70c 100644 --- a/acme/simple_http_challenge.go +++ b/acme/simple_http_challenge.go @@ -10,6 +10,7 @@ import ( "encoding/pem" "errors" "fmt" + "io/ioutil" "math/big" "net" "net/http" @@ -21,8 +22,38 @@ type simpleHTTPChallenge struct { optPort string } -func (s *simpleHTTPChallenge) CanSolve() bool { - return true +// SimpleHTTPS checks for DNS, public IP and port bindings +func (s *simpleHTTPChallenge) CanSolve(domain string) bool { + // determine public ip + resp, err := http.Get("https://icanhazip.com/") + if err != nil { + logger().Printf("Could not get public IP -> %v", err) + return false + } + + ip, err := ioutil.ReadAll(resp.Body) + if err != nil { + logger().Printf("Could not get public IP -> %v", err) + return false + } + ipStr := string(ip) + + // resolve domain we should solve for + resolvedIPs, err := net.LookupHost(domain) + if err != nil { + logger().Printf("Could not lookup DNS A record for %s", domain) + return false + } + + // if the resolve does not resolve to our public ip, we can't solve. + for _, resolvedIP := range resolvedIPs { + if resolvedIP == ipStr { + return true + } + } + + logger().Printf("SimpleHTTPS: Domain %s does not resolve to the public ip of this server. Determined ip: %s", domain, ipStr) + return false } func (s *simpleHTTPChallenge) Solve(chlng challenge, domain string) error { From 53d7b59d3609e69d50966fe805526fa32bc52199 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sat, 13 Jun 2015 19:13:04 +0200 Subject: [PATCH 2/4] Initial SimpleHTTP test --- acme/simple_http_challenge.go | 2 ++ acme/simple_http_challenge_test.go | 33 ++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 acme/simple_http_challenge_test.go diff --git a/acme/simple_http_challenge.go b/acme/simple_http_challenge.go index 0192d70c..a74bf744 100644 --- a/acme/simple_http_challenge.go +++ b/acme/simple_http_challenge.go @@ -14,6 +14,7 @@ import ( "math/big" "net" "net/http" + "strings" "time" ) @@ -37,6 +38,7 @@ func (s *simpleHTTPChallenge) CanSolve(domain string) bool { return false } ipStr := string(ip) + ipStr = strings.Replace(ipStr, "\n", "", -1) // resolve domain we should solve for resolvedIPs, err := net.LookupHost(domain) diff --git a/acme/simple_http_challenge_test.go b/acme/simple_http_challenge_test.go new file mode 100644 index 00000000..5d2e8332 --- /dev/null +++ b/acme/simple_http_challenge_test.go @@ -0,0 +1,33 @@ +package acme + +import ( + "io/ioutil" + "net/http" + "strings" + "testing" +) + +func TestSimpleHTTPCanSolve(t *testing.T) { + challenge := &simpleHTTPChallenge{} + + // determine public ip + resp, err := http.Get("https://icanhazip.com/") + if err != nil { + t.Errorf("Could not get public IP -> %v", err) + } + + ip, err := ioutil.ReadAll(resp.Body) + if err != nil { + t.Errorf("Could not get public IP -> %v", err) + } + ipStr := string(ip) + + if expected, actual := false, challenge.CanSolve("google.com"); expected != actual { + t.Errorf("Expected CanSolve to return %t for domain 'google.com' but was %t", expected, actual) + } + + localResolv := strings.Replace(ipStr, "\n", "", -1) + ".xip.io" + if expected, actual := true, challenge.CanSolve(localResolv); expected != actual { + t.Errorf("Expected CanSolve to return %t for domain 'localhost' but was %t", expected, actual) + } +} From 2231118fdf12bfca21568b35b837eca47e0e9d84 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sat, 13 Jun 2015 21:06:47 +0200 Subject: [PATCH 3/4] Add SimpleHTTP tests --- acme/simple_http_challenge.go | 2 + acme/simple_http_challenge_test.go | 91 ++++++++++++++++++++++++++++++ 2 files changed, 93 insertions(+) diff --git a/acme/simple_http_challenge.go b/acme/simple_http_challenge.go index a74bf744..16ad35eb 100644 --- a/acme/simple_http_challenge.go +++ b/acme/simple_http_challenge.go @@ -97,8 +97,10 @@ loop: case "pending": break case "invalid": + listener.Close() return errors.New("The server could not validate our request.") default: + listener.Close() return errors.New("The server returned an unexpected state.") } diff --git a/acme/simple_http_challenge_test.go b/acme/simple_http_challenge_test.go index 5d2e8332..36a76536 100644 --- a/acme/simple_http_challenge_test.go +++ b/acme/simple_http_challenge_test.go @@ -1,10 +1,15 @@ package acme import ( + "crypto/tls" + "encoding/json" "io/ioutil" "net/http" + "net/http/httptest" "strings" "testing" + + "github.com/square/go-jose" ) func TestSimpleHTTPCanSolve(t *testing.T) { @@ -31,3 +36,89 @@ func TestSimpleHTTPCanSolve(t *testing.T) { t.Errorf("Expected CanSolve to return %t for domain 'localhost' but was %t", expected, actual) } } + +func TestSimpleHTTP(t *testing.T) { + privKey, err := generatePrivateKey(512) + if err != nil { + t.Errorf("Could not generate public key -> %v", err) + } + jws := &jws{privKey: privKey} + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + })) + + solver := &simpleHTTPChallenge{jws: jws} + clientChallenge := challenge{Type: "simpleHttps", Status: "pending", URI: ts.URL, Token: "123456789"} + + // validate error on non-root bind to 443 + if err = solver.Solve(clientChallenge, "test.domain"); err == nil { + t.Error("BIND: Expected Solve to return an error but the error was nil.") + } + + // Validate error on unexpected state + solver.optPort = "8080" + if err = solver.Solve(clientChallenge, "test.domain"); err == nil { + t.Error("UNEXPECTED: Expected Solve to return an error but the error was nil.") + } + + // Validate error on invalid status + ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + failed := challenge{Type: "simpleHttps", Status: "invalid", URI: ts.URL, Token: "123456789"} + jsonBytes, _ := json.Marshal(&failed) + w.Write(jsonBytes) + }) + if err = solver.Solve(clientChallenge, "test.domain"); err == nil { + t.Error("FAILED: Expected Solve to return an error but the error was nil.") + } + + // Validate no error on valid response + ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + valid := challenge{Type: "simpleHttps", Status: "valid", URI: ts.URL, Token: "123456789"} + jsonBytes, _ := json.Marshal(&valid) + w.Write(jsonBytes) + }) + if err = solver.Solve(clientChallenge, "test.domain"); err != nil { + t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err) + } + + // Validate server on port 8080 which responds appropriately + ts.Config.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var request challenge + + clientJws, _ := ioutil.ReadAll(r.Body) + j, err := jose.ParseSigned(string(clientJws)) + if err != nil { + t.Errorf("Client sent invalid JWS to the server. -> %v", err) + } + output, err := j.Verify(&privKey.PublicKey) + if err != nil { + t.Errorf("Unable to verify client data -> %v", err) + } + json.Unmarshal(output, &request) + + transport := &http.Transport{TLSClientConfig: &tls.Config{InsecureSkipVerify: true}} + client := &http.Client{Transport: transport} + + reqURL := "https://localhost:8080/.well-known/acme-challenge/" + request.Path + t.Logf("Request URL is: %s", reqURL) + req, _ := http.NewRequest("GET", reqURL, nil) + req.Host = "test.domain" + resp, err := client.Do(req) + if err != nil { + t.Errorf("Expected the solver to listen on port 8080 -> %v", err) + } + + body, _ := ioutil.ReadAll(resp.Body) + bodyStr := string(body) + if bodyStr != "123456789" { + t.Errorf("Expected the solver to return the token %s but instead returned '%s'", "123456789", bodyStr) + } + + valid := challenge{Type: "simpleHttps", Status: "valid", URI: ts.URL, Token: "123456789"} + jsonBytes, _ := json.Marshal(&valid) + w.Write(jsonBytes) + }) + if err = solver.Solve(clientChallenge, "test.domain"); err != nil { + t.Errorf("VALID: Expected Solve to return no error but the error was -> %v", err) + } +} From e6aaf7e2dda4348cf965f03a7ab2cb92621db652 Mon Sep 17 00:00:00 2001 From: xenolf Date: Sat, 13 Jun 2015 21:15:34 +0200 Subject: [PATCH 4/4] Program should not exit on bind error, but return the error to get handled --- acme/simple_http_challenge.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/acme/simple_http_challenge.go b/acme/simple_http_challenge.go index 16ad35eb..7713d762 100644 --- a/acme/simple_http_challenge.go +++ b/acme/simple_http_challenge.go @@ -129,7 +129,6 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp tempCertPEM, pemBytes) if err != nil { - logger().Print("error here!") return nil, err } @@ -146,7 +145,7 @@ func (s *simpleHTTPChallenge) startHTTPSServer(domain string, token string, resp tlsListener, err := tls.Listen("tcp", port, tlsConf) if err != nil { - logger().Fatalf("Could not start HTTP listener! -> %v", err) + return nil, fmt.Errorf("Could not start HTTP listener! -> %v", err) } // The handler validates the HOST header and request type.