Merge branch 'tofu' of sloum/bombadillo into develop

Adds the following:
- Storing/retrieving of TLS certs from servers in `.bombadillo.ini`
- `purge` command is added to clear out stale certificates
- Bombadillo now validates that certs match the ones on file
- Bombadillo now validates certificates are within valid window
- Bombadillo now validates that certificate hostnames match the host that is requested
- Logic to get a new certificate (if one is presented) when an expired certificate is still present
- Logic to get a certificate if none is present
This commit is contained in:
Sloom Sloum Sluom IV 2019-10-02 15:49:35 -04:00 committed by Gitea
commit 66acf6102f
7 changed files with 230 additions and 45 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
@ -279,6 +279,8 @@ func (c *client) simpleCommand(action string) {
case "B", "BOOKMARKS": case "B", "BOOKMARKS":
c.BookMarks.ToggleOpen() c.BookMarks.ToggleOpen()
c.Draw() c.Draw()
case "R", "REFRESH":
// TODO build refresh code
case "SEARCH": case "SEARCH":
c.search("", "", "?") c.search("", "", "?")
case "HELP", "?": case "HELP", "?":
@ -299,8 +301,27 @@ func (c *client) doCommand(action string, values []string) {
switch action { switch action {
case "CHECK", "C": case "CHECK", "C":
c.displayConfigValue(values[0]) c.displayConfigValue(values[0])
case "PURGE", "P":
err := c.Certs.Purge(values[0])
if err != nil {
c.SetMessage(err.Error(), true)
c.DrawMessage()
return
}
if values[0] == "*" {
c.SetMessage("All certificates have been purged", false)
c.DrawMessage()
} else {
c.SetMessage(fmt.Sprintf("The certificate for %q has been purged", strings.ToLower(values[0])), false)
c.DrawMessage()
}
err = saveConfig()
if err != nil {
c.SetMessage("Error saving purge to file", true)
c.DrawMessage()
}
case "SEARCH": case "SEARCH":
c.search(strings.Join(values, " "), "", "") c.search(values[0], "", "")
case "WRITE", "W": case "WRITE", "W":
if values[0] == "." { if values[0] == "." {
values[0] = c.PageState.History[c.PageState.Position].Location.Full values[0] = c.PageState.History[c.PageState.Position].Location.Full
@ -360,7 +381,8 @@ func (c *client) doCommandAs(action string, values []string) {
if c.BookMarks.IsOpen { if c.BookMarks.IsOpen {
c.Draw() c.Draw()
} }
case "SEARCH":
c.search(strings.Join(values, " "), "", "")
case "WRITE", "W": case "WRITE", "W":
u, err := MakeUrl(values[0]) u, err := MakeUrl(values[0])
if err != nil { if err != nil {
@ -472,7 +494,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()
@ -551,7 +573,7 @@ func (c *client) doLinkCommand(action, target string) {
num -= 1 num -= 1
links := c.PageState.History[c.PageState.Position].Links links := c.PageState.History[c.PageState.Position].Links
if num >= len(links) || num < 0 { if num >= len(links) || num < 1 {
c.SetMessage(fmt.Sprintf("Invalid link id: %s", target), true) c.SetMessage(fmt.Sprintf("Invalid link id: %s", target), true)
c.DrawMessage() c.DrawMessage()
return return
@ -832,12 +854,13 @@ 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)
if err != nil { if err != nil {
c.SetMessage(err.Error(), true) c.SetMessage(err.Error(), true)
c.DrawMessage() c.DrawMessage()
return return
} }
go saveConfig()
switch capsule.Status { switch capsule.Status {
case 1: case 1:
c.search("", u.Full, capsule.Content) c.search("", u.Full, capsule.Content)
@ -955,7 +978,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

@ -68,9 +68,11 @@ func (s *scanner) scanText() Token {
capInput := strings.ToUpper(buf.String()) capInput := strings.ToUpper(buf.String())
switch capInput { switch capInput {
case "DELETE", "ADD", "WRITE", "SET", "RECALL", "R", "SEARCH", case "D", "DELETE", "A", "ADD","W", "WRITE",
"W", "A", "D", "S", "Q", "QUIT", "B", "BOOKMARKS", "H", "S", "SET", "R", "REFRESH", "SEARCH",
"HOME", "?", "HELP", "C", "CHECK": "Q", "QUIT", "B", "BOOKMARKS", "H",
"HOME", "?", "HELP", "C", "CHECK",
"P", "PURGE":
return Token{Action, capInput} return Token{Action, capInput}
} }

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,28 +1,138 @@
package gemini package gemini
import ( import (
"bytes"
"crypto/sha1"
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"strconv" "strconv"
"strings" "strings"
"time"
// "tildegit.org/sloum/mailcap"
) )
type Capsule struct { type Capsule struct {
MimeMaj string MimeMaj string
MimeMin string MimeMin string
Status int Status int
Content string Content string
Links []string Links []string
} }
type TofuDigest struct {
certs map[string]string
}
//------------------------------------------------\\
// + + + R E C E I V E R S + + + \\
//--------------------------------------------------\\
func (t *TofuDigest) Purge(host string) error {
host = strings.ToLower(host)
if host == "*" {
t.certs = make(map[string]string)
return nil
} else if _, ok := t.certs[strings.ToLower(host)]; ok {
delete(t.certs, host)
return nil
}
return fmt.Errorf("Invalid host %q", host)
}
func (t *TofuDigest) Add(host, hash string) {
t.certs[strings.ToLower(host)] = hash
}
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 string, cState *tls.ConnectionState) error {
host = strings.ToLower(host)
now := time.Now()
for _, cert := range cState.PeerCertificates {
if t.certs[host] != hashCert(cert.Raw) {
continue
}
if now.Before(cert.NotBefore) {
return fmt.Errorf("Certificate is not valid yet")
}
if now.After(cert.NotAfter) {
return fmt.Errorf("EXP")
}
if err := cert.VerifyHostname(host); err != nil {
return fmt.Errorf("Certificate error: %s", err)
}
return nil
}
return fmt.Errorf("No matching certificate was found for host %q", host)
}
func (t *TofuDigest) newCert(host string, cState *tls.ConnectionState) error {
host = strings.ToLower(host)
now := time.Now()
for _, cert := range cState.PeerCertificates {
if now.Before(cert.NotBefore) {
continue
}
if now.After(cert.NotAfter) {
continue
}
if err := cert.VerifyHostname(host); err != nil {
continue
}
t.Add(host, hashCert(cert.Raw))
return nil
}
return fmt.Errorf("No valid certificates were offered by host %q", host)
}
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")
} }
@ -41,6 +151,38 @@ func Retrieve(host, port, resource string) (string, error) {
defer conn.Close() defer conn.Close()
connState := conn.ConnectionState()
// Begin TOFU screening...
// If no certificates are offered, bail out
if len(connState.PeerCertificates) < 1 {
return "", fmt.Errorf("Insecure, no certificates offered by server")
}
if td.Exists(host) {
// See if we have a matching cert
err := td.Match(host, &connState)
if err != nil && err.Error() != "EXP" {
// If there is no match and it isnt because of an expiration
// just return the error
return "", err
} else if err != nil {
// The cert expired, see if they are offering one that is valid...
err := td.newCert(host, &connState)
if err != nil {
// If there are no valid certs to offer, let the client know
return "", err
}
}
} else {
err = td.newCert(host, &connState)
if err != nil {
// If there are no valid certs to offer, let the client know
return "", err
}
}
send := "gemini://" + addr + "/" + resource + "\r\n" send := "gemini://" + addr + "/" + resource + "\r\n"
_, err = conn.Write([]byte(send)) _, err = conn.Write([]byte(send))
@ -56,8 +198,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
} }
@ -103,9 +245,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
} }
@ -226,8 +368,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
} }

39
url.go
View File

@ -37,7 +37,7 @@ type Url struct {
// an error (or nil). // an error (or nil).
func MakeUrl(u string) (Url, error) { func MakeUrl(u string) (Url, error) {
var out Url var out Url
re := regexp.MustCompile(`^((?P<scheme>gopher|telnet|http|https|gemini):\/\/)?(?P<host>[\w\-\.\d]+)(?::(?P<port>\d+)?)?(?:/(?P<type>[01345679gIhisp])?)?(?P<resource>.*)?$`) re := regexp.MustCompile(`^((?P<scheme>[a-zA-Z]+):\/\/)?(?P<host>[\w\-\.\d]+)(?::(?P<port>\d+)?)?(?:/(?P<type>[01345679gIhisp])?)?(?P<resource>.*)?$`)
match := re.FindStringSubmatch(u) match := re.FindStringSubmatch(u)
if valid := re.MatchString(u); !valid { if valid := re.MatchString(u); !valid {
@ -59,14 +59,16 @@ func MakeUrl(u string) (Url, error) {
} }
} }
if out.Scheme == "" {
out.Scheme = "gopher"
}
if out.Host == "" { if out.Host == "" {
return out, fmt.Errorf("no host") return out, fmt.Errorf("no host")
} }
out.Scheme = strings.ToLower(out.Scheme)
if out.Scheme == "" {
out.Scheme = "gopher"
}
if out.Scheme == "gopher" && out.Port == "" { if out.Scheme == "gopher" && out.Port == "" {
out.Port = "70" out.Port = "70"
} else if out.Scheme == "http" && out.Port == "" { } else if out.Scheme == "http" && out.Port == "" {
@ -75,21 +77,20 @@ func MakeUrl(u string) (Url, error) {
out.Port = "443" out.Port = "443"
} else if out.Scheme == "gemini" && out.Port == "" { } else if out.Scheme == "gemini" && out.Port == "" {
out.Port = "1965" out.Port = "1965"
} } else if out.Scheme == "telnet" && out.Port == "" {
out.Port = "23"
if out.Scheme == "gopher" && out.Mime == "" {
out.Mime = "1"
}
if out.Mime == "" && (out.Resource == "" || out.Resource == "/") && out.Scheme == "gopher" {
out.Mime = "1"
}
if out.Mime == "7" && strings.Contains(out.Resource, "\t") {
out.Mime = "1"
} }
if out.Scheme == "gopher" { if out.Scheme == "gopher" {
if out.Mime == "" {
out.Mime = "1"
}
if out.Resource == "" || out.Resource == "/" {
out.Mime = "1"
}
if out.Mime == "7" && strings.Contains(out.Resource, "\t") {
out.Mime = "1"
}
switch out.Mime { switch out.Mime {
case "1", "0", "h", "7": case "1", "0", "h", "7":
out.DownloadOnly = false out.DownloadOnly = false
@ -101,10 +102,6 @@ func MakeUrl(u string) (Url, error) {
out.Mime = "" out.Mime = ""
} }
if out.Scheme == "http" || out.Scheme == "https" {
out.Mime = ""
}
out.Full = out.Scheme + "://" + out.Host + ":" + out.Port + "/" + out.Mime + out.Resource out.Full = out.Scheme + "://" + out.Host + ":" + out.Port + "/" + out.Mime + out.Resource
return out, nil return out, nil