Adds basic tofu certificate pinning
This commit is contained in:
parent
4c92870790
commit
bef32b7ff5
|
@ -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"
|
||||
|
|
11
client.go
11
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
143
gemini/gemini.go
143
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)}
|
||||
}
|
||||
|
|
11
main.go
11
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
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue