13dd4c0a99
* Add HTTP challenge validator This will attempt to safely validate HTTP challenges, following a limited number of redirects and timing out after too much time has passed. Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add test for ValidateKeyAuthorization Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add test cases for ValidateHTTP01Challenge Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> * Add token to HTTP challenge Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com> --------- Signed-off-by: Alexander Scheel <alex.scheel@hashicorp.com>
185 lines
4.6 KiB
Go
185 lines
4.6 KiB
Go
package pki
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
type keyAuthorizationTestCase struct {
|
|
keyAuthz string
|
|
token string
|
|
thumbprint string
|
|
shouldFail bool
|
|
}
|
|
|
|
var keyAuthorizationTestCases = []keyAuthorizationTestCase{
|
|
{
|
|
// Entirely empty
|
|
"",
|
|
"non-empty-token",
|
|
"non-empty-thumbprint",
|
|
true,
|
|
},
|
|
{
|
|
// Both empty
|
|
".",
|
|
"non-empty-token",
|
|
"non-empty-thumbprint",
|
|
true,
|
|
},
|
|
{
|
|
// Not equal
|
|
"non-.non-",
|
|
"non-empty-token",
|
|
"non-empty-thumbprint",
|
|
true,
|
|
},
|
|
{
|
|
// Empty thumbprint
|
|
"non-.",
|
|
"non-empty-token",
|
|
"non-empty-thumbprint",
|
|
true,
|
|
},
|
|
{
|
|
// Empty token
|
|
".non-",
|
|
"non-empty-token",
|
|
"non-empty-thumbprint",
|
|
true,
|
|
},
|
|
{
|
|
// Wrong order
|
|
"non-empty-thumbprint.non-empty-token",
|
|
"non-empty-token",
|
|
"non-empty-thumbprint",
|
|
true,
|
|
},
|
|
{
|
|
// Too many pieces
|
|
"one.two.three",
|
|
"non-empty-token",
|
|
"non-empty-thumbprint",
|
|
true,
|
|
},
|
|
{
|
|
// Valid
|
|
"non-empty-token.non-empty-thumbprint",
|
|
"non-empty-token",
|
|
"non-empty-thumbprint",
|
|
false,
|
|
},
|
|
}
|
|
|
|
func TestAcmeValidateKeyAuthorization(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for index, tc := range keyAuthorizationTestCases {
|
|
isValid, err := ValidateKeyAuthorization(tc.keyAuthz, tc.token, tc.thumbprint)
|
|
if !isValid && err == nil {
|
|
t.Fatalf("[%d] expected failure to give reason via err (%v / %v)", index, isValid, err)
|
|
}
|
|
|
|
expectedValid := !tc.shouldFail
|
|
if expectedValid != isValid {
|
|
t.Fatalf("[%d] got ret=%v, expected ret=%v (shouldFail=%v)", index, isValid, expectedValid, tc.shouldFail)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAcmeValidateHTTP01Challenge(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
for index, tc := range keyAuthorizationTestCases {
|
|
validFunc := func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(tc.keyAuthz))
|
|
}
|
|
withPadding := func(w http.ResponseWriter, r *http.Request) {
|
|
w.Write([]byte(" " + tc.keyAuthz + " "))
|
|
}
|
|
withRedirect := func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.URL.Path, "/.well-known/") {
|
|
http.Redirect(w, r, "/my-http-01-challenge-response", 301)
|
|
return
|
|
}
|
|
|
|
w.Write([]byte(tc.keyAuthz))
|
|
}
|
|
withSleep := func(w http.ResponseWriter, r *http.Request) {
|
|
// Long enough to ensure any excessively short timeouts are hit,
|
|
// not long enough to trigger a failure (hopefully).
|
|
time.Sleep(5 * time.Second)
|
|
w.Write([]byte(tc.keyAuthz))
|
|
}
|
|
|
|
validHandlers := []http.HandlerFunc{
|
|
http.HandlerFunc(validFunc), http.HandlerFunc(withPadding),
|
|
http.HandlerFunc(withRedirect), http.HandlerFunc(withSleep),
|
|
}
|
|
|
|
for handlerIndex, handler := range validHandlers {
|
|
func() {
|
|
ts := httptest.NewServer(handler)
|
|
defer ts.Close()
|
|
|
|
host := ts.URL[7:]
|
|
isValid, err := ValidateHTTP01Challenge(host, tc.token, tc.thumbprint)
|
|
if !isValid && err == nil {
|
|
t.Fatalf("[tc=%d/handler=%d] expected failure to give reason via err (%v / %v)", handlerIndex, index, isValid, err)
|
|
}
|
|
|
|
expectedValid := !tc.shouldFail
|
|
if expectedValid != isValid {
|
|
t.Fatalf("[tc=%d/handler=%d] got ret=%v (err=%v), expected ret=%v (shouldFail=%v)", index, handlerIndex, isValid, err, expectedValid, tc.shouldFail)
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
|
|
// Negative test cases for various HTTP-specific scenarios.
|
|
redirectLoop := func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "/my-http-01-challenge-response", 301)
|
|
}
|
|
publicRedirect := func(w http.ResponseWriter, r *http.Request) {
|
|
http.Redirect(w, r, "http://hashicorp.com/", 301)
|
|
}
|
|
noData := func(w http.ResponseWriter, r *http.Request) {}
|
|
noContent := func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}
|
|
notFound := func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
simulateHang := func(w http.ResponseWriter, r *http.Request) {
|
|
time.Sleep(30 * time.Second)
|
|
w.Write([]byte("my-token.my-thumbprint"))
|
|
}
|
|
tooLarge := func(w http.ResponseWriter, r *http.Request) {
|
|
for i := 0; i < 512; i++ {
|
|
w.Write([]byte("my-token.my-thumbprint"))
|
|
}
|
|
}
|
|
|
|
validHandlers := []http.HandlerFunc{
|
|
http.HandlerFunc(redirectLoop), http.HandlerFunc(publicRedirect),
|
|
http.HandlerFunc(noData), http.HandlerFunc(noContent),
|
|
http.HandlerFunc(notFound), http.HandlerFunc(simulateHang),
|
|
http.HandlerFunc(tooLarge),
|
|
}
|
|
for handlerIndex, handler := range validHandlers {
|
|
func() {
|
|
ts := httptest.NewServer(handler)
|
|
defer ts.Close()
|
|
|
|
host := ts.URL[7:]
|
|
isValid, err := ValidateHTTP01Challenge(host, "my-token", "my-thumbprint")
|
|
if isValid || err == nil {
|
|
t.Fatalf("[handler=%d] expected failure validating challenge (%v / %v)", handlerIndex, isValid, err)
|
|
}
|
|
}()
|
|
}
|
|
}
|