Browse Source

Merge pull request #8 from tam7t/some-tests

add tests
Tommy Murphy 3 years ago
parent
commit
6bc01e2da7
3 changed files with 505 additions and 5 deletions
  1. 13 5
      header.go
  2. 224 0
      header_test.go
  3. 268 0
      storage_test.go

+ 13 - 5
header.go

@@ -26,16 +26,24 @@ func (h *Header) Matches(pin string) bool {
 	return false
 }
 
-// ParseHeader parses the hpkp information from an http.Response. It should only
-// be used on HTTPS connections.
+// ParseHeader parses the hpkp information from an http.Response.
 func ParseHeader(resp *http.Response) *Header {
-	header := &Header{
-		Sha256Pins: []string{},
+	if resp == nil {
+		return nil
+	}
+
+	// only make a header when using TLS
+	if resp.TLS == nil {
+		return nil
 	}
 
 	v, ok := resp.Header["Public-Key-Pins"]
 	if !ok {
-		return header
+		return nil
+	}
+
+	header := &Header{
+		Sha256Pins: []string{},
 	}
 
 	for _, field := range strings.Split(v[0], ";") {

+ 224 - 0
header_test.go

@@ -0,0 +1,224 @@
+package hpkp
+
+import (
+	"crypto/tls"
+	"net/http"
+	"testing"
+)
+
+func TestHeader_Matches(t *testing.T) {
+	tests := []struct {
+		name     string
+		header   *Header
+		pin      string
+		expected bool
+	}{
+		{
+			name:     "no match",
+			header:   &Header{},
+			pin:      "d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=",
+			expected: false,
+		},
+		{
+			name: "match",
+			header: &Header{
+				Sha256Pins: []string{
+					"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=",
+					"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=",
+				},
+			},
+			pin:      "E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=",
+			expected: true,
+		},
+	}
+
+	for _, test := range tests {
+		out := test.header.Matches(test.pin)
+		if out != test.expected {
+			t.Logf("want:%v", test.expected)
+			t.Logf("got:%v", out)
+			t.Fatalf("test case failed: %s", test.name)
+		}
+	}
+}
+
+func TestParseHeader(t *testing.T) {
+	tests := []struct {
+		name     string
+		response *http.Response
+		expected *Header
+	}{
+		{
+			name:     "nil everything",
+			response: nil,
+			expected: nil,
+		},
+		{
+			name: "no header",
+			response: &http.Response{
+				StatusCode: 200,
+			},
+			expected: nil,
+		},
+		{
+			name: "hpkp header, but over http",
+			response: &http.Response{
+				StatusCode: 200,
+				Header:     map[string][]string{},
+			},
+			expected: nil,
+		},
+		// https://tools.ietf.org/html/rfc7469#section-2.1.5
+		{
+			name: "hpkp header (1)",
+			response: &http.Response{
+				StatusCode: 200,
+				Header: map[string][]string{
+					"Public-Key-Pins": []string{`max-age=3000; pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="`},
+				},
+				TLS: &tls.ConnectionState{},
+			},
+			expected: &Header{
+				MaxAge:            3000,
+				IncludeSubDomains: false,
+				Permanent:         false,
+				Sha256Pins: []string{
+					"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=",
+					"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=",
+				},
+			},
+		},
+		{
+			name: "hpkp header (2)",
+			response: &http.Response{
+				StatusCode: 200,
+				Header: map[string][]string{
+					"Public-Key-Pins": []string{`max-age=2592000; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="; pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="`},
+				},
+				TLS: &tls.ConnectionState{},
+			},
+			expected: &Header{
+				MaxAge:            2592000,
+				IncludeSubDomains: false,
+				Permanent:         false,
+				Sha256Pins: []string{
+					"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=",
+					"LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=",
+				},
+			},
+		},
+		{
+			name: "hpkp header (3)",
+			response: &http.Response{
+				StatusCode: 200,
+				Header: map[string][]string{
+					"Public-Key-Pins": []string{`max-age=2592000; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="; pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="; report-uri="http://example.com/pkp-report"`},
+				},
+				TLS: &tls.ConnectionState{},
+			},
+			expected: &Header{
+				MaxAge:            2592000,
+				IncludeSubDomains: false,
+				Permanent:         false,
+				Sha256Pins: []string{
+					"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=",
+					"LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=",
+				},
+			},
+		},
+		{
+			name: "hpkp header (4)",
+			response: &http.Response{
+				StatusCode: 200,
+				Header: map[string][]string{
+					"Public-Key-Pins-Report-Only": []string{`max-age=2592000; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="; pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="; report-uri="http://example.com/pkp-report"`},
+				},
+				TLS: &tls.ConnectionState{},
+			},
+			// TODO: support Public-Key-Pins-Report-Only
+			expected: nil,
+		},
+		{
+			name: "hpkp header (5)",
+			response: &http.Response{
+				StatusCode: 200,
+				Header: map[string][]string{
+					"Public-Key-Pins": []string{`pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="; max-age=259200`},
+				},
+				TLS: &tls.ConnectionState{},
+			},
+			expected: &Header{
+				MaxAge:            259200,
+				IncludeSubDomains: false,
+				Permanent:         false,
+				Sha256Pins: []string{
+					"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=",
+					"LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=",
+				},
+			},
+		},
+		{
+			name: "hpkp header (6)",
+			response: &http.Response{
+				StatusCode: 200,
+				Header: map[string][]string{
+					"Public-Key-Pins": []string{`pin-sha256="d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM="; pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g="; pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="; max-age=10000; includeSubDomains`},
+				},
+				TLS: &tls.ConnectionState{},
+			},
+			expected: &Header{
+				MaxAge:            10000,
+				IncludeSubDomains: true,
+				Permanent:         false,
+				Sha256Pins: []string{
+					"d6qzRu9zOECb90Uez27xWltNsj0e1Md7GkYYkVoZWmM=",
+					"E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=",
+					"LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ=",
+				},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		out := ParseHeader(test.response)
+		if !equalHeaders(out, test.expected) {
+			t.Logf("want:%v", test.expected)
+			t.Logf("got:%v", out)
+			t.Fatalf("test case failed: %s", test.name)
+		}
+	}
+}
+
+func equalHeaders(a, b *Header) bool {
+	if a == nil && b == nil {
+		return true
+	}
+
+	if a == nil || b == nil {
+		return false
+	}
+
+	if a.IncludeSubDomains != b.IncludeSubDomains {
+		return false
+	}
+
+	if a.MaxAge != b.MaxAge {
+		return false
+	}
+
+	if a.Permanent != b.Permanent {
+		return false
+	}
+
+	if len(a.Sha256Pins) != len(b.Sha256Pins) {
+		return false
+	}
+
+	for i := range a.Sha256Pins {
+		if a.Sha256Pins[i] != b.Sha256Pins[i] {
+			return false
+		}
+	}
+
+	return true
+}

+ 268 - 0
storage_test.go

@@ -0,0 +1,268 @@
+package hpkp
+
+import (
+	"fmt"
+	"reflect"
+	"testing"
+	"time"
+)
+
+var createdAt = time.Now().Unix()
+
+func TestMemStorage_Lookup(t *testing.T) {
+	m := NewMemStorage()
+	m.Add("example.org", &Header{
+		IncludeSubDomains: false,
+		Permanent:         false,
+		Created:           createdAt,
+		MaxAge:            100,
+	})
+	m.Add("a.example.org", &Header{
+		IncludeSubDomains: true,
+		Permanent:         false,
+		Created:           createdAt,
+		MaxAge:            100,
+	})
+	m.Add("a.example.com", &Header{
+		IncludeSubDomains: true,
+		Permanent:         false,
+		Created:           createdAt,
+		MaxAge:            100,
+	})
+	m.Add("b.example.com", &Header{
+		IncludeSubDomains: false,
+		Permanent:         false,
+		Created:           createdAt,
+		MaxAge:            100,
+	})
+
+	done := make(chan bool)
+
+	var orgErr error
+	go func() {
+		orgErr = orgTest(m, t)
+		// try to make a data race
+		m.Add("example.org", &Header{
+			IncludeSubDomains: false,
+			Permanent:         false,
+			Created:           createdAt,
+			MaxAge:            100,
+		})
+		done <- true
+	}()
+
+	var comErr error
+	go func() {
+		comErr = comTest(m, t)
+		// try to make a data race
+		m.Add("a.example.com", &Header{
+			IncludeSubDomains: true,
+			Permanent:         false,
+			Created:           createdAt,
+			MaxAge:            100,
+		})
+		done <- true
+	}()
+
+	// wait for tests to finish
+	<-done
+	<-done
+
+	if orgErr != nil {
+		t.Fatal(orgErr)
+	}
+
+	if comErr != nil {
+		t.Fatal(comErr)
+	}
+}
+
+func orgTest(m Storage, t *testing.T) error {
+	tests := []struct {
+		name     string
+		host     string
+		expected *Header
+	}{
+		{
+			name: "root match org",
+			host: "example.org",
+			expected: &Header{
+				IncludeSubDomains: false,
+				Permanent:         false,
+				Created:           createdAt,
+				MaxAge:            100,
+			},
+		},
+		{
+			name: "subdomain match org",
+			host: "a.example.org",
+			expected: &Header{
+				IncludeSubDomains: true,
+				Permanent:         false,
+				Created:           createdAt,
+				MaxAge:            100,
+			},
+		},
+		{
+			name:     "subdomain miss-match org",
+			host:     "b.example.org",
+			expected: nil,
+		},
+	}
+
+	for _, test := range tests {
+		out := m.Lookup(test.host)
+		if !reflect.DeepEqual(out, test.expected) {
+			t.Logf("host: %s", test.host)
+			t.Logf("want:%v", test.expected)
+			t.Logf("got:%v", out)
+			return fmt.Errorf("test case failed: %s", test.name)
+		}
+	}
+	return nil
+}
+
+func comTest(m Storage, t *testing.T) error {
+	tests := []struct {
+		name     string
+		host     string
+		expected *Header
+	}{
+		{
+			name: "subdomain enabled",
+			host: "z.a.example.com",
+			expected: &Header{
+				IncludeSubDomains: true,
+				Permanent:         false,
+				Created:           createdAt,
+				MaxAge:            100,
+			},
+		},
+		{
+			name: "sub-subdomain",
+			host: "z.y.a.example.com",
+			expected: &Header{
+				IncludeSubDomains: true,
+				Permanent:         false,
+				Created:           createdAt,
+				MaxAge:            100,
+			},
+		},
+		{
+			name:     "subdomain disabled",
+			host:     "z.b.example.com",
+			expected: nil,
+		},
+		{
+			name: "exact match",
+			host: "b.example.com",
+			expected: &Header{
+				IncludeSubDomains: false,
+				Permanent:         false,
+				Created:           createdAt,
+				MaxAge:            100,
+			},
+		},
+		{
+			name:     "complete missmatch",
+			host:     "z.example.com",
+			expected: nil,
+		},
+	}
+
+	for _, test := range tests {
+		out := m.Lookup(test.host)
+		if !reflect.DeepEqual(out, test.expected) {
+			t.Logf("host: %s", test.host)
+			t.Logf("want:%v", test.expected)
+			t.Logf("got:%v", out)
+			return fmt.Errorf("test case failed: %s", test.name)
+		}
+	}
+	return nil
+}
+
+func TestMemStorage_Add(t *testing.T) {
+	m := &MemStorage{}
+
+	// permanent
+	permanentDomain := Header{
+		IncludeSubDomains: false,
+		Permanent:         true,
+		Created:           time.Now().Unix(),
+		MaxAge:            0,
+	}
+
+	m.Add("example.org", &permanentDomain)
+
+	expected := map[string]Header{
+		"example.org": permanentDomain,
+	}
+
+	if !reflect.DeepEqual(m.domains, expected) {
+		t.Logf("want:%v", expected)
+		t.Logf("got:%v", m.domains)
+		t.Fatal("Add failed after permanent")
+	}
+
+	// normal
+	normalDomain := Header{
+		IncludeSubDomains: false,
+		Permanent:         false,
+		Created:           time.Now().Unix(),
+		MaxAge:            100,
+	}
+
+	m.Add("a.example.org", &normalDomain)
+
+	expected = map[string]Header{
+		"example.org":   permanentDomain,
+		"a.example.org": normalDomain,
+	}
+
+	if !reflect.DeepEqual(m.domains, expected) {
+		t.Logf("want:%v", expected)
+		t.Logf("got:%v", m.domains)
+		t.Fatal("Add failed after adding normal")
+	}
+
+	// remove normal
+	removeNormalDomain := Header{
+		IncludeSubDomains: false,
+		Permanent:         false,
+		Created:           time.Now().Unix(),
+		MaxAge:            0,
+	}
+
+	m.Add("a.example.org", &removeNormalDomain)
+
+	expected = map[string]Header{
+		"example.org": permanentDomain,
+	}
+
+	if !reflect.DeepEqual(m.domains, expected) {
+		t.Logf("want:%v", expected)
+		t.Logf("got:%v", m.domains)
+		t.Fatal("Add failed after removing normal")
+	}
+
+	// attempt to remove the permanent
+	removePermanetDomain := Header{
+		IncludeSubDomains: false,
+		Permanent:         false,
+		Created:           time.Now().Unix(),
+		MaxAge:            0,
+	}
+
+	m.Add("example.org", &removePermanetDomain)
+
+	expected = map[string]Header{
+		"example.org": permanentDomain,
+	}
+
+	if !reflect.DeepEqual(m.domains, expected) {
+		t.Logf("want:%v", expected)
+		t.Logf("got:%v", m.domains)
+		t.Fatal("Add failed after attempting to remove the permanent domain")
+	}
+}