Tommy Murphy 4 years ago
parent
commit
47e029d8fe
6 changed files with 294 additions and 0 deletions
  1. 7 0
      README.md
  2. 85 0
      cmd/main.go
  3. 45 0
      dialer.go
  4. 13 0
      fingerprint.go
  5. 67 0
      header.go
  6. 77 0
      storage.go

+ 7 - 0
README.md

@@ -1,2 +1,9 @@
 # hpkp
 golang hpkp client library
+
+Library for performing certificate pin validation for golang applications.
+
+## References
+
+* https://tools.ietf.org/html/rfc7469
+* https://developer.mozilla.org/en-US/docs/Web/Security/Public_Key_Pinning

+ 85 - 0
cmd/main.go

@@ -0,0 +1,85 @@
+package main
+
+import (
+	"crypto/tls"
+	"crypto/x509"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+
+	"github.com/tam7t/hpkp"
+)
+
+func main() {
+	cmd := "error"
+	if len(os.Args) > 1 {
+		cmd = os.Args[1]
+	}
+	switch cmd {
+	case "example":
+		example()
+	case "cert":
+		cert()
+	case "headers":
+		headers()
+	default:
+		fmt.Println("usage: view the source code")
+	}
+}
+
+func example() {
+	s := hpkp.NewMemStorage()
+	s.Add("github.com", &hpkp.Header{
+		Permanent:  true,
+		Sha256Pins: []string{},
+	})
+	client := &http.Client{}
+	client.Transport = &http.Transport{
+		DialTLS: hpkp.NewPinDialer(s, true, nil),
+	}
+
+	req, err := http.NewRequest("GET", "https://www.github.com", nil)
+	resp, err := client.Do(req)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	log.Println(resp.StatusCode)
+}
+
+func cert() {
+	file := os.Args[2]
+	contents, err := ioutil.ReadFile(file)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	certs, err := x509.ParseCertificates(contents)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	for i := range certs {
+		fmt.Println(hpkp.Fingerprint(certs[i]))
+	}
+}
+
+func headers() {
+	addr := os.Args[2]
+
+	tr := &http.Transport{
+		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+	}
+	client := &http.Client{Transport: tr}
+	resp, err := client.Get(addr)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	h := hpkp.ParseHeader(resp)
+	j, _ := json.Marshal(h)
+	fmt.Println(string(j))
+}

+ 45 - 0
dialer.go

@@ -0,0 +1,45 @@
+package hpkp
+
+import (
+	"crypto/tls"
+	"errors"
+	"net"
+	"strings"
+)
+
+// Storage is threadsafe hsts storage interface
+type Storage interface {
+	Lookup(host string) *Header
+	Add(host string, d *Header)
+}
+
+// NewPinDialer returns a function suitable for use as DialTLS
+func NewPinDialer(s Storage, pinOnly bool, defaultTLSConfig *tls.Config) func(network, addr string) (net.Conn, error) {
+	return func(network, addr string) (net.Conn, error) {
+		// might need to strip ":https" from addr as well
+		h := s.Lookup(strings.TrimRight(addr, ":443"))
+
+		if h != nil {
+			c, err := tls.Dial(network, addr, &tls.Config{InsecureSkipVerify: pinOnly})
+			if err != nil {
+				return c, err
+			}
+			validPin := false
+			// intermediates can be pinned as well, loop through leaf-> root looking
+			// for pins
+			for _, peercert := range c.ConnectionState().PeerCertificates {
+				peerPin := Fingerprint(peercert)
+				if h.Matches(peerPin) {
+					validPin = true
+					break
+				}
+			}
+			if validPin == false {
+				return nil, errors.New("pin was not valid")
+			}
+			return c, nil
+		}
+		// do a normal dial
+		return tls.Dial(network, addr, defaultTLSConfig)
+	}
+}

+ 13 - 0
fingerprint.go

@@ -0,0 +1,13 @@
+package hpkp
+
+import (
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/base64"
+)
+
+// Fingerprint returns the hpkp signature of an x509 certificate
+func Fingerprint(c *x509.Certificate) string {
+	digest := sha256.Sum256(c.RawSubjectPublicKeyInfo)
+	return base64.StdEncoding.EncodeToString(digest[:])
+}

+ 67 - 0
header.go

@@ -0,0 +1,67 @@
+package hpkp
+
+import (
+	"net/http"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// Header holds a domain's hpkp information
+type Header struct {
+	Created           int64
+	MaxAge            int64
+	IncludeSubDomains bool
+	Permanent         bool
+	Sha256Pins        []string
+}
+
+// Matches checks whether the provided pin is in the header list
+func (h *Header) Matches(pin string) bool {
+	for i := range h.Sha256Pins {
+		if h.Sha256Pins[i] == pin {
+			return true
+		}
+	}
+	return false
+}
+
+// ParseHeader parses the hpkp information from an http.Response. It should only
+// be used on HTTPS connections.
+func ParseHeader(resp *http.Response) *Header {
+	header := &Header{
+		Sha256Pins: []string{},
+	}
+
+	v, ok := resp.Header["Public-Key-Pins"]
+	if !ok {
+		return header
+	}
+
+	for _, field := range strings.Split(v[0], ";") {
+		field = strings.TrimSpace(field)
+
+		i := strings.Index(field, "pin-sha256")
+		if i >= 0 {
+			header.Sha256Pins = append(header.Sha256Pins, field[i+12:len(field)-1])
+			continue
+		}
+
+		i = strings.Index(field, "max-age=")
+		if i >= 0 {
+			ma, err := strconv.Atoi(field[i+8:])
+			if err == nil {
+				header.MaxAge = int64(ma)
+			}
+			continue
+		}
+
+		if strings.Contains(field, "includeSubDomains") {
+			header.IncludeSubDomains = true
+			continue
+		}
+	}
+
+	header.Created = time.Now().Unix()
+	return header
+}

+ 77 - 0
storage.go

@@ -0,0 +1,77 @@
+package hpkp
+
+import (
+	"strings"
+	"sync"
+)
+
+// MemStorage is threadsafe hpkp host storage backed by an in-memory map
+type MemStorage struct {
+	domains map[string]Header
+	mutex   sync.Mutex
+}
+
+// NewMemStorage initializes hsts in-memory datastructure
+func NewMemStorage() *MemStorage {
+	m := &MemStorage{}
+	m.domains = make(map[string]Header)
+	return m
+}
+
+// Lookup returns the corresponding hpkp header information for a given host
+func (s *MemStorage) Lookup(host string) *Header {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	d, ok := s.domains[host]
+	if ok {
+		return copy(d)
+	}
+
+	// is h a subdomain of an hpkp domain, walk the domain to see if it is a sub
+	// sub ... sub domain of a domain that has the `includeSubDomains` rule
+	l := len(host)
+	for l > 0 {
+		i := strings.Index(host, ".")
+		if i > 0 {
+			host = host[i+1:]
+			d, ok := s.domains[host]
+			if ok {
+				if d.IncludeSubDomains {
+					return copy(d)
+				}
+			}
+			l = len(host)
+		} else {
+			break
+		}
+	}
+
+	return nil
+}
+
+func copy(h Header) *Header {
+	d := h
+	return &d
+}
+
+// Add a domain to hpkp storage
+func (s *MemStorage) Add(host string, d *Header) {
+	s.mutex.Lock()
+	defer s.mutex.Unlock()
+
+	if s.domains == nil {
+		s.domains = make(map[string]Header)
+	}
+
+	if d.MaxAge == 0 && !d.Permanent {
+		check, ok := s.domains[host]
+		if ok {
+			if !check.Permanent {
+				delete(s.domains, host)
+			}
+		}
+	} else {
+		s.domains[host] = *d
+	}
+}