From bef32b7ff56f60e6d1a7e24d326f49f710787755 Mon Sep 17 00:00:00 2001 From: sloumdrone Date: Thu, 26 Sep 2019 22:08:57 -0700 Subject: [PATCH] Adds basic tofu certificate pinning --- bookmarks.go | 2 +- client.go | 11 ++-- config/parser.go | 6 +- gemini/gemini.go | 143 ++++++++++++++++++++++++++++++++++------------- main.go | 11 +++- 5 files changed, 123 insertions(+), 50 deletions(-) diff --git a/bookmarks.go b/bookmarks.go index cb2b773..3e320c4 100644 --- a/bookmarks.go +++ b/bookmarks.go @@ -61,7 +61,7 @@ func (b *Bookmarks) ToggleFocused() { } func (b Bookmarks) IniDump() string { - if len(b.Titles) < 0 { + if len(b.Titles) < 1 { return "" } out := "[BOOKMARKS]\n" diff --git a/client.go b/client.go index 54eb354..05bee18 100644 --- a/client.go +++ b/client.go @@ -16,7 +16,6 @@ import ( "tildegit.org/sloum/bombadillo/gopher" "tildegit.org/sloum/bombadillo/http" "tildegit.org/sloum/bombadillo/telnet" - // "tildegit.org/sloum/mailcap" ) //------------------------------------------------\\ @@ -33,6 +32,7 @@ type client struct { BookMarks Bookmarks TopBar Headbar FootBar Footbar + Certs gemini.TofuDigest } @@ -154,7 +154,7 @@ func (c *client) TakeControlInput() { c.ClearMessage() c.Scroll(-1) case 'q', 'Q': - // quite bombadillo + // quit bombadillo cui.Exit() case 'g': // scroll to top @@ -472,7 +472,7 @@ func (c *client) saveFile(u Url, name string) { case "gopher": file, err = gopher.Retrieve(u.Host, u.Port, u.Resource) case "gemini": - file, err = gemini.Fetch(u.Host, u.Port, u.Resource) + file, err = gemini.Fetch(u.Host, u.Port, u.Resource, &c.Certs) default: c.SetMessage(fmt.Sprintf("Saving files over %s is not supported", u.Scheme), true) c.DrawMessage() @@ -832,7 +832,8 @@ func (c *client) Visit(url string) { c.Draw() } case "gemini": - capsule, err := gemini.Visit(u.Host, u.Port, u.Resource) + capsule, err := gemini.Visit(u.Host, u.Port, u.Resource, &c.Certs) + go saveConfig() if err != nil { c.SetMessage(err.Error(), true) c.DrawMessage() @@ -955,7 +956,7 @@ func (c *client) Visit(url string) { //--------------------------------------------------\\ func MakeClient(name string) *client { - c := client{0, 0, defaultOptions, "", false, MakePages(), MakeBookmarks(), MakeHeadbar(name), MakeFootbar()} + c := client{0, 0, defaultOptions, "", false, MakePages(), MakeBookmarks(), MakeHeadbar(name), MakeFootbar(), gemini.MakeTofuDigest()} return &c } diff --git a/config/parser.go b/config/parser.go index 038c889..780bfa7 100644 --- a/config/parser.go +++ b/config/parser.go @@ -24,8 +24,8 @@ type Config struct { Bookmarks struct { Titles, Links []string } - Colors []KeyValue Settings []KeyValue + Certs []KeyValue } type KeyValue struct { @@ -90,8 +90,8 @@ func (p *Parser) Parse() (Config, error) { case "BOOKMARKS": c.Bookmarks.Titles = append(c.Bookmarks.Titles, keyval.Value) c.Bookmarks.Links = append(c.Bookmarks.Links, keyval.Key) - case "COLORS": - c.Colors = append(c.Colors, keyval) + case "CERTS": + c.Certs = append(c.Certs, keyval) case "SETTINGS": c.Settings = append(c.Settings, keyval) } diff --git a/gemini/gemini.go b/gemini/gemini.go index 39cda6d..adce235 100644 --- a/gemini/gemini.go +++ b/gemini/gemini.go @@ -1,6 +1,8 @@ package gemini import ( + "bytes" + "crypto/sha1" "crypto/tls" "fmt" "io/ioutil" @@ -15,58 +17,78 @@ type Capsule struct { MimeMin string Status int Content string - Links []string + Links []string } + type TofuDigest struct { - db map[string][]map[string]string + certs map[string]string } + //------------------------------------------------\\ // + + + R E C E I V E R S + + + \\ //--------------------------------------------------\\ -func (t *TofuDigest) Remove(host string, indexToRemove int) error { - if _, ok := t.db[host]; ok { - if indexToRemove < 0 || indexToRemove >= len(t.db[host]) { - return fmt.Errorf("Invalid index") - } else if len(t.db[host]) > indexToRemove { - t.db[host] = append(t.db[host][:indexToRemove], t.db[host][indexToRemove+1:]...) - } else if len(t.db[host]) - 1 == indexToRemove { - t.db[host] = t.db[host][:indexToRemove] - } +func (t *TofuDigest) Remove(host string) error { + if _, ok := t.certs[strings.ToLower(host)]; ok { + delete(t.certs, host) return nil } return fmt.Errorf("Invalid host") } -func (t *TofuDigest) Add(host, hash string, start, end int64) { - s := strconv.FormatInt(start, 10) - e := strconv.FormatInt(end, 10) - added := strconv.FormatInt(time.Now().Unix(), 10) - entry := map[string]string{"hash": hash, "start": s, "end": e, "added": added} - t.db[host] = append(t.db[host], entry) +func (t *TofuDigest) Add(host, hash string) { + t.certs[strings.ToLower(host)] = hash } -// Removes all entries that are expired -func (t *TofuDigest) Clean() { - now := time.Now() - for host, slice := range t.db { - for index, entry := range slice { - intFromStringTime, err := strconv.ParseInt(entry["end"], 10, 64) - if err != nil || now.After(time.Unix(intFromStringTime, 0)) { - t.Remove(host, index) - } - } +func (t *TofuDigest) Exists(host string) bool { + if _, ok := t.certs[strings.ToLower(host)]; ok { + return true } + return false } +func (t *TofuDigest) Find(host string) (string, error) { + if hash, ok := t.certs[strings.ToLower(host)]; ok { + return hash, nil + } + return "", fmt.Errorf("Invalid hostname, no key saved") +} + +func (t *TofuDigest) Match(host, hash string) bool { + host = strings.ToLower(host) + if _, ok := t.certs[host]; !ok { + return false + } + if t.certs[host] == hash { + return true + } + return false +} + +func (t *TofuDigest) IniDump() string { + if len(t.certs) < 1 { + return "" + } + var out strings.Builder + out.WriteString("[CERTS]\n") + for k, v := range t.certs { + out.WriteString(k) + out.WriteString("=") + out.WriteString(v) + out.WriteString("\n") + } + return out.String() +} + + //------------------------------------------------\\ // + + + F U N C T I O N S + + + \\ //--------------------------------------------------\\ -func Retrieve(host, port, resource string) (string, error) { +func Retrieve(host, port, resource string, td *TofuDigest) (string, error) { if host == "" || port == "" { return "", fmt.Errorf("Incomplete request url") } @@ -83,25 +105,54 @@ func Retrieve(host, port, resource string) (string, error) { return "", err } + now := time.Now() + defer conn.Close() // Verify that the handshake ahs completed and that // the hostname on the certificate(s) from the server // is the hostname we have requested connState := conn.ConnectionState() - if connState.HandshakeComplete { - if len(connState.PeerCertificates) > 0 { - for _, cert := range connState.PeerCertificates { - if err = cert.VerifyHostname(host); err == nil { - break - } + if len(connState.PeerCertificates) < 0 { + return "", fmt.Errorf("Insecure, no certificates offered by server") + } + hostCertExists := td.Exists(host) + matched := false + + for _, cert := range connState.PeerCertificates { + if hostCertExists { + if td.Match(host, hashCert(cert.Raw)) { + matched = true + if now.Before(cert.NotBefore) { + return "", fmt.Errorf("Server certificate error: certificate not valid yet") + } + + if now.After(cert.NotAfter) { + return "", fmt.Errorf("Server certificate error: certificate expired") + } + + if err = cert.VerifyHostname(host); err != nil { + return "", fmt.Errorf("Server certificate error: %s", err) + } + break + } + } else { + if now.Before(cert.NotBefore) || now.After(cert.NotAfter) { + continue } - if err != nil { - return "", err + + if err = cert.VerifyHostname(host); err != nil { + return "", fmt.Errorf("Server certificate error: %s", err) } + + td.Add(host, hashCert(cert.Raw)) + matched = true } } + if !matched { + return "", fmt.Errorf("Server certificate error: No matching certificate provided") + } send := "gemini://" + addr + "/" + resource + "\r\n" @@ -118,8 +169,8 @@ func Retrieve(host, port, resource string) (string, error) { return string(result), nil } -func Fetch(host, port, resource string) ([]byte, error) { - rawResp, err := Retrieve(host, port, resource) +func Fetch(host, port, resource string, td *TofuDigest) ([]byte, error) { + rawResp, err := Retrieve(host, port, resource, td) if err != nil { return make([]byte, 0), err } @@ -165,9 +216,9 @@ func Fetch(host, port, resource string) ([]byte, error) { } -func Visit(host, port, resource string) (Capsule, error) { +func Visit(host, port, resource string, td *TofuDigest) (Capsule, error) { capsule := MakeCapsule() - rawResp, err := Retrieve(host, port, resource) + rawResp, err := Retrieve(host, port, resource, td) if err != nil { return capsule, err } @@ -288,8 +339,20 @@ func handleRelativeUrl(u, root, current string) string { return fmt.Sprintf("%s%s", current, u) } +func hashCert(cert []byte) string { + hash := sha1.Sum(cert) + hex := make([][]byte, len(hash)) + for i, data := range hash { + hex[i] = []byte(fmt.Sprintf("%02X", data)) + } + return fmt.Sprintf("%s", string(bytes.Join(hex, []byte("-")))) +} + func MakeCapsule() Capsule { return Capsule{"", "", 0, "", make([]string, 0, 5)} } +func MakeTofuDigest() TofuDigest { + return TofuDigest{make(map[string]string)} +} diff --git a/main.go b/main.go index 57d00b8..41f3be3 100644 --- a/main.go +++ b/main.go @@ -24,6 +24,7 @@ import ( "os" "strings" + _ "tildegit.org/sloum/bombadillo/gemini" "tildegit.org/sloum/bombadillo/config" "tildegit.org/sloum/bombadillo/cui" "tildegit.org/sloum/mailcap" @@ -39,8 +40,8 @@ var mc *mailcap.Mailcap func saveConfig() error { var opts strings.Builder bkmrks := bombadillo.BookMarks.IniDump() + certs := bombadillo.Certs.IniDump() - opts.WriteString(bkmrks) opts.WriteString("\n[SETTINGS]\n") for k, v := range bombadillo.Options { if k == "theme" && v != "normal" && v != "inverse" { @@ -53,6 +54,10 @@ func saveConfig() error { opts.WriteRune('\n') } + opts.WriteString(bkmrks) + + opts.WriteString(certs) + return ioutil.WriteFile(bombadillo.Options["configlocation"] + "/.bombadillo.ini", []byte(opts.String()), 0644) } @@ -122,6 +127,10 @@ func loadConfig() error { bombadillo.BookMarks.Add([]string{v, settings.Bookmarks.Links[i]}) } + for _, v := range settings.Certs { + bombadillo.Certs.Add(v.Key, v.Value) + } + return nil }