1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-12-04 14:46:29 -05:00

Add ports to TOFU and change what's hashed, fixes #7

This commit is contained in:
makeworld 2020-06-19 23:44:04 -04:00
parent e719346bc4
commit cc17b1693b
3 changed files with 47 additions and 34 deletions

View File

@ -34,6 +34,7 @@ The project keeps many standard terminal keybindings and is intuitive. Press <kb
It is designed with large or fullscreen terminals in mind. For optimal usage, make your terminal fullscreen. It was also designed with a dark background terminal in mind, but please file an issue if the colour choices look bad on your terminal setup. It is designed with large or fullscreen terminals in mind. For optimal usage, make your terminal fullscreen. It was also designed with a dark background terminal in mind, but please file an issue if the colour choices look bad on your terminal setup.
## Features / Roadmap ## Features / Roadmap
Features in italics are in the master branch, but not in the latest release.
- [x] URL browsing with TOFU and error handling - [x] URL browsing with TOFU and error handling
- [x] Tabbed browsing - [x] Tabbed browsing
@ -41,7 +42,7 @@ It is designed with large or fullscreen terminals in mind. For optimal usage, ma
- [x] Styled page content (headings, links) - [x] Styled page content (headings, links)
- [x] Basic forward/backward history, for each tab - [x] Basic forward/backward history, for each tab
- [x] Input (Status Code 10 & 11) - [x] Input (Status Code 10 & 11)
- [x] Multiple charset support (over 55) - [x] *Multiple charset support (over 55)*
- [ ] Built-in search using GUS - [ ] Built-in search using GUS
- [ ] Bookmarks - [ ] Bookmarks
- [ ] Search in pages with <kbd>Ctrl-F</kbd> - [ ] Search in pages with <kbd>Ctrl-F</kbd>

View File

@ -2,17 +2,21 @@
package client package client
import ( import (
"net/url"
"github.com/makeworld-the-better-one/go-gemini" "github.com/makeworld-the-better-one/go-gemini"
) )
// Fetch returns response data and an error. // Fetch returns response data and an error.
// The error text is human friendly and should be displayed. // The error text is human friendly and should be displayed.
func Fetch(url string) (*gemini.Response, error) { func Fetch(u string) (*gemini.Response, error) {
resp, err := gemini.Fetch(url) resp, err := gemini.Fetch(u)
if err != nil { if err != nil {
return nil, err return nil, err
} }
ok := handleTofu(resp.Cert)
parsed, _ := url.Parse(u)
ok := handleTofu(resp.Cert, parsed.Port())
if !ok { if !ok {
return nil, ErrTofu return nil, ErrTofu
} }

View File

@ -15,6 +15,7 @@ import (
// Stores cert hash and expiry for now, like Bombadillo. // Stores cert hash and expiry for now, like Bombadillo.
// There is ongoing TOFU discussion on the mailing list about better // There is ongoing TOFU discussion on the mailing list about better
// ways to do this, and I will update this file once those are decided on. // ways to do this, and I will update this file once those are decided on.
// Update: See #7 for some small improvements made.
var ErrTofu = errors.New("server cert does not match TOFU database") var ErrTofu = errors.New("server cert does not match TOFU database")
@ -22,22 +23,28 @@ var tofuStore = config.TofuStore
// idKey returns the config/viper key needed to retrieve // idKey returns the config/viper key needed to retrieve
// a cert's ID / fingerprint. // a cert's ID / fingerprint.
func idKey(domain string) string { func idKey(domain string, port string) string {
return strings.ReplaceAll(domain, ".", "/") if port == "1965" || port == "" {
return strings.ReplaceAll(domain, ".", "/")
}
return strings.ReplaceAll(domain, ".", "/") + ":" + port
} }
func expiryKey(domain string) string { func expiryKey(domain string, port string) string {
return strings.ReplaceAll(strings.TrimSuffix(domain, "."), ".", "/") + "/expiry" if port == "1965" || port == "" {
return strings.ReplaceAll(strings.TrimSuffix(domain, "."), ".", "/") + "/expiry"
}
return strings.ReplaceAll(strings.TrimSuffix(domain, "."), ".", "/") + "/expiry" + ":" + port
} }
func loadTofuEntry(domain string) (string, time.Time, error) { func loadTofuEntry(domain string, port string) (string, time.Time, error) {
id := tofuStore.GetString(idKey(domain)) // Fingerprint id := tofuStore.GetString(idKey(domain, port)) // Fingerprint
if len(id) != 64 { if len(id) != 64 {
// Not set, or invalid // Not set, or invalid
return "", time.Time{}, errors.New("not found") return "", time.Time{}, errors.New("not found")
} }
expiry := tofuStore.GetTime(expiryKey(domain)) expiry := tofuStore.GetTime(expiryKey(domain, port))
if expiry.IsZero() { if expiry.IsZero() {
// Not set // Not set
return id, time.Time{}, errors.New("not found") return id, time.Time{}, errors.New("not found")
@ -48,24 +55,20 @@ func loadTofuEntry(domain string) (string, time.Time, error) {
// certID returns a generic string representing a cert or domain. // certID returns a generic string representing a cert or domain.
func certID(cert *x509.Certificate) string { func certID(cert *x509.Certificate) string {
h := sha256.New() h := sha256.New()
h.Write(cert.Raw) h.Write(cert.RawSubjectPublicKeyInfo) // Better than cert.Raw, see #7
return fmt.Sprintf("%X", h.Sum(nil)) return fmt.Sprintf("%X", h.Sum(nil))
// The old way that uses the cert public key:
// b, err := x509.MarshalPKIXPublicKey(cert.PublicKey)
// h := sha256.New()
// if err != nil {
// // Unsupported key type - try to store a hash of the struct instead
// h.Write([]byte(fmt.Sprint(cert.PublicKey)))
// return fmt.Sprintf("%X", h.Sum(nil))
// }
// h.Write(b)
// return fmt.Sprintf("%X", h.Sum(nil))
} }
func saveTofuEntry(cert *x509.Certificate) { // origCertID uses cert.Raw, which was used in v1.0.0 of the app.
tofuStore.Set(idKey(cert.Subject.CommonName), certID(cert)) func origCertID(cert *x509.Certificate) string {
tofuStore.Set(expiryKey(cert.Subject.CommonName), cert.NotAfter.UTC()) h := sha256.New()
h.Write(cert.Raw) // Better than cert.Raw, see #7
return fmt.Sprintf("%X", h.Sum(nil))
}
func saveTofuEntry(cert *x509.Certificate, port string) {
tofuStore.Set(idKey(cert.Subject.CommonName, port), certID(cert))
tofuStore.Set(expiryKey(cert.Subject.CommonName, port), cert.NotAfter.UTC())
err := tofuStore.WriteConfig() err := tofuStore.WriteConfig()
if err != nil { if err != nil {
panic(err) panic(err)
@ -77,21 +80,26 @@ func saveTofuEntry(cert *x509.Certificate) {
// It returns a bool indicating if the cert is valid according to // It returns a bool indicating if the cert is valid according to
// the TOFU database. // the TOFU database.
// If false is returned, the connection should not go ahead. // If false is returned, the connection should not go ahead.
func handleTofu(cert *x509.Certificate) bool { func handleTofu(cert *x509.Certificate, port string) bool {
id, expiry, err := loadTofuEntry(cert.Subject.CommonName) id, expiry, err := loadTofuEntry(cert.Subject.CommonName, port)
if err != nil { if err != nil {
// Cert isn't in database or data is malformed // Cert isn't in database or data is malformed
// So it can't be checked and anything is valid // So it can't be checked and anything is valid
saveTofuEntry(cert) saveTofuEntry(cert, port)
return true
}
if certID(cert) == id {
// Save cert as the one stored
return true return true
} }
if time.Now().After(expiry) { if time.Now().After(expiry) {
// Old cert expired, so anything is valid // Old cert expired, so anything is valid
saveTofuEntry(cert) saveTofuEntry(cert, port)
return true
}
if certID(cert) == id {
// Same cert as the one stored
return true
}
if origCertID(cert) == id {
// Valid but uses old ID type
saveTofuEntry(cert, port)
return true return true
} }
return false return false