Adds basic tofu certificate pinning

This commit is contained in:
sloumdrone 2019-09-26 22:08:57 -07:00
parent 4c92870790
commit bef32b7ff5
5 changed files with 123 additions and 50 deletions

View File

@ -61,7 +61,7 @@ func (b *Bookmarks) ToggleFocused() {
} }
func (b Bookmarks) IniDump() string { func (b Bookmarks) IniDump() string {
if len(b.Titles) < 0 { if len(b.Titles) < 1 {
return "" return ""
} }
out := "[BOOKMARKS]\n" out := "[BOOKMARKS]\n"

View File

@ -16,7 +16,6 @@ import (
"tildegit.org/sloum/bombadillo/gopher" "tildegit.org/sloum/bombadillo/gopher"
"tildegit.org/sloum/bombadillo/http" "tildegit.org/sloum/bombadillo/http"
"tildegit.org/sloum/bombadillo/telnet" "tildegit.org/sloum/bombadillo/telnet"
// "tildegit.org/sloum/mailcap"
) )
//------------------------------------------------\\ //------------------------------------------------\\
@ -33,6 +32,7 @@ type client struct {
BookMarks Bookmarks BookMarks Bookmarks
TopBar Headbar TopBar Headbar
FootBar Footbar FootBar Footbar
Certs gemini.TofuDigest
} }
@ -154,7 +154,7 @@ func (c *client) TakeControlInput() {
c.ClearMessage() c.ClearMessage()
c.Scroll(-1) c.Scroll(-1)
case 'q', 'Q': case 'q', 'Q':
// quite bombadillo // quit bombadillo
cui.Exit() cui.Exit()
case 'g': case 'g':
// scroll to top // scroll to top
@ -472,7 +472,7 @@ func (c *client) saveFile(u Url, name string) {
case "gopher": case "gopher":
file, err = gopher.Retrieve(u.Host, u.Port, u.Resource) file, err = gopher.Retrieve(u.Host, u.Port, u.Resource)
case "gemini": 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: default:
c.SetMessage(fmt.Sprintf("Saving files over %s is not supported", u.Scheme), true) c.SetMessage(fmt.Sprintf("Saving files over %s is not supported", u.Scheme), true)
c.DrawMessage() c.DrawMessage()
@ -832,7 +832,8 @@ func (c *client) Visit(url string) {
c.Draw() c.Draw()
} }
case "gemini": 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 { if err != nil {
c.SetMessage(err.Error(), true) c.SetMessage(err.Error(), true)
c.DrawMessage() c.DrawMessage()
@ -955,7 +956,7 @@ func (c *client) Visit(url string) {
//--------------------------------------------------\\ //--------------------------------------------------\\
func MakeClient(name string) *client { 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 return &c
} }

View File

@ -24,8 +24,8 @@ type Config struct {
Bookmarks struct { Bookmarks struct {
Titles, Links []string Titles, Links []string
} }
Colors []KeyValue
Settings []KeyValue Settings []KeyValue
Certs []KeyValue
} }
type KeyValue struct { type KeyValue struct {
@ -90,8 +90,8 @@ func (p *Parser) Parse() (Config, error) {
case "BOOKMARKS": case "BOOKMARKS":
c.Bookmarks.Titles = append(c.Bookmarks.Titles, keyval.Value) c.Bookmarks.Titles = append(c.Bookmarks.Titles, keyval.Value)
c.Bookmarks.Links = append(c.Bookmarks.Links, keyval.Key) c.Bookmarks.Links = append(c.Bookmarks.Links, keyval.Key)
case "COLORS": case "CERTS":
c.Colors = append(c.Colors, keyval) c.Certs = append(c.Certs, keyval)
case "SETTINGS": case "SETTINGS":
c.Settings = append(c.Settings, keyval) c.Settings = append(c.Settings, keyval)
} }

View File

@ -1,6 +1,8 @@
package gemini package gemini
import ( import (
"bytes"
"crypto/sha1"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
@ -15,58 +17,78 @@ type Capsule struct {
MimeMin string MimeMin string
Status int Status int
Content string Content string
Links []string Links []string
} }
type TofuDigest struct { type TofuDigest struct {
db map[string][]map[string]string certs map[string]string
} }
//------------------------------------------------\\ //------------------------------------------------\\
// + + + R E C E I V E R S + + + \\ // + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\ //--------------------------------------------------\\
func (t *TofuDigest) Remove(host string, indexToRemove int) error { func (t *TofuDigest) Remove(host string) error {
if _, ok := t.db[host]; ok { if _, ok := t.certs[strings.ToLower(host)]; ok {
if indexToRemove < 0 || indexToRemove >= len(t.db[host]) { delete(t.certs, 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]
}
return nil return nil
} }
return fmt.Errorf("Invalid host") return fmt.Errorf("Invalid host")
} }
func (t *TofuDigest) Add(host, hash string, start, end int64) { func (t *TofuDigest) Add(host, hash string) {
s := strconv.FormatInt(start, 10) t.certs[strings.ToLower(host)] = hash
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)
} }
// Removes all entries that are expired func (t *TofuDigest) Exists(host string) bool {
func (t *TofuDigest) Clean() { if _, ok := t.certs[strings.ToLower(host)]; ok {
now := time.Now() return true
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)
}
}
} }
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 + + + \\ // + + + 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 == "" { if host == "" || port == "" {
return "", fmt.Errorf("Incomplete request url") return "", fmt.Errorf("Incomplete request url")
} }
@ -83,25 +105,54 @@ func Retrieve(host, port, resource string) (string, error) {
return "", err return "", err
} }
now := time.Now()
defer conn.Close() defer conn.Close()
// Verify that the handshake ahs completed and that // Verify that the handshake ahs completed and that
// the hostname on the certificate(s) from the server // the hostname on the certificate(s) from the server
// is the hostname we have requested // is the hostname we have requested
connState := conn.ConnectionState() connState := conn.ConnectionState()
if connState.HandshakeComplete { if len(connState.PeerCertificates) < 0 {
if len(connState.PeerCertificates) > 0 { return "", fmt.Errorf("Insecure, no certificates offered by server")
for _, cert := range connState.PeerCertificates { }
if err = cert.VerifyHostname(host); err == nil { hostCertExists := td.Exists(host)
break 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" send := "gemini://" + addr + "/" + resource + "\r\n"
@ -118,8 +169,8 @@ func Retrieve(host, port, resource string) (string, error) {
return string(result), nil return string(result), nil
} }
func Fetch(host, port, resource string) ([]byte, error) { func Fetch(host, port, resource string, td *TofuDigest) ([]byte, error) {
rawResp, err := Retrieve(host, port, resource) rawResp, err := Retrieve(host, port, resource, td)
if err != nil { if err != nil {
return make([]byte, 0), err 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() capsule := MakeCapsule()
rawResp, err := Retrieve(host, port, resource) rawResp, err := Retrieve(host, port, resource, td)
if err != nil { if err != nil {
return capsule, err return capsule, err
} }
@ -288,8 +339,20 @@ func handleRelativeUrl(u, root, current string) string {
return fmt.Sprintf("%s%s", current, u) 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 { func MakeCapsule() Capsule {
return Capsule{"", "", 0, "", make([]string, 0, 5)} return Capsule{"", "", 0, "", make([]string, 0, 5)}
} }
func MakeTofuDigest() TofuDigest {
return TofuDigest{make(map[string]string)}
}

11
main.go
View File

@ -24,6 +24,7 @@ import (
"os" "os"
"strings" "strings"
_ "tildegit.org/sloum/bombadillo/gemini"
"tildegit.org/sloum/bombadillo/config" "tildegit.org/sloum/bombadillo/config"
"tildegit.org/sloum/bombadillo/cui" "tildegit.org/sloum/bombadillo/cui"
"tildegit.org/sloum/mailcap" "tildegit.org/sloum/mailcap"
@ -39,8 +40,8 @@ var mc *mailcap.Mailcap
func saveConfig() error { func saveConfig() error {
var opts strings.Builder var opts strings.Builder
bkmrks := bombadillo.BookMarks.IniDump() bkmrks := bombadillo.BookMarks.IniDump()
certs := bombadillo.Certs.IniDump()
opts.WriteString(bkmrks)
opts.WriteString("\n[SETTINGS]\n") opts.WriteString("\n[SETTINGS]\n")
for k, v := range bombadillo.Options { for k, v := range bombadillo.Options {
if k == "theme" && v != "normal" && v != "inverse" { if k == "theme" && v != "normal" && v != "inverse" {
@ -53,6 +54,10 @@ func saveConfig() error {
opts.WriteRune('\n') opts.WriteRune('\n')
} }
opts.WriteString(bkmrks)
opts.WriteString(certs)
return ioutil.WriteFile(bombadillo.Options["configlocation"] + "/.bombadillo.ini", []byte(opts.String()), 0644) 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]}) bombadillo.BookMarks.Add([]string{v, settings.Bookmarks.Links[i]})
} }
for _, v := range settings.Certs {
bombadillo.Certs.Add(v.Key, v.Value)
}
return nil return nil
} }