~sotirisp/kindleto

c283b5aa1d5cc53b85cc48c9b9620c08420e29af — Sotiris Papatheodorou 2 months ago f179717 fingerprint-changed-prompt
[WIP] Prompt before accepting a changed server certificate
6 files changed, 71 insertions(+), 9 deletions(-)

M gemini/gemini.go
M handlers.go
M kindleto.go
M templates/template_strings.go
M templates/templates.go
M tofu/tofu.go
M gemini/gemini.go => gemini/gemini.go +8 -3
@@ 62,10 62,11 @@ func ProxyGemini(w http.ResponseWriter, r *http.Request, u *url.URL) (*url.URL, 
		return u, err
	}

	var warning string
	var newCert *x509.Certificate
	client := gemini.Client{
		TrustCertificate: func(hostname string, cert *x509.Certificate) error {
			err, warning = tofu.CheckHostCertificate(hostname, cert)
			var err error
			err, newCert = tofu.CheckHostCertificate(hostname, cert)
			return err
		},
		DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {


@@ 86,6 87,11 @@ func ProxyGemini(w http.ResponseWriter, r *http.Request, u *url.URL) (*url.URL, 

	response, err := client.Do(ctx, &request)
	if err != nil {
		if errors.Is(err, tofu.ErrCertChanged) {
			// TODO send newCert somehow, post?
			http.Redirect(w, r, "/tofu?url="+gemini.QueryEscape(u.String()), http.StatusFound)
			return u, nil
		}
		return u, fmt.Errorf("ProxyGemini: gemini.Client.Do error to %s: %v", u.String(), err)
	}
	defer response.Body.Close()


@@ 102,7 108,6 @@ func ProxyGemini(w http.ResponseWriter, r *http.Request, u *url.URL) (*url.URL, 
		Charset:     "utf-8",
		Lang:        DefaultLang,
		ManageCerts: certificates.NumClientCerts() > 0,
		Warning: warning,
	}

	switch response.Status.Class() {

M handlers.go => handlers.go +39 -0
@@ 167,6 167,45 @@ func manageClientCertificates(w http.ResponseWriter, r *http.Request) {
	}
}

// TODO
func certificateChanged(w http.ResponseWriter, r *http.Request) {
	if r.FormValue("url") == "" {
		e := `certificateChanged: missing URL parameter "url"`
		log.Printf(e)
		http.Error(w, e, http.StatusBadRequest)
		return
	}
	u, err := url.Parse(r.FormValue("url"))
	if err != nil {
		e := fmt.Sprintf(`certificateChanged: failed to parse URL "%s": %v`,
			r.FormValue("url"), err)
		log.Printf(e)
		http.Error(w, e, http.StatusInternalServerError)
		return
	}
	switch r.Method {
	case http.MethodGet:
		td := templates.TemplateData{
			URL: u.String(),
		}
		err = templates.Templates.ExecuteTemplate(w, "tofu.html.tmpl", td)
		if err != nil {
			e := fmt.Sprintf("certificateChanged: failed to execute template tofu.html.tmpl: %v", err)
			log.Printf(e)
			http.Error(w, e, http.StatusInternalServerError)
		}
	case http.MethodPost:
		// TODO update known hosts, will need the certificate data here
		// maybe put certificate in post data?
		//tofu.AddHostCertificate(u.Hostname(), cert *x509.Certificate)
		http.Redirect(w, r, "/?url="+gogemini.QueryEscape(u.String()), http.StatusFound)
	default:
		e := fmt.Sprintf(`certificateChanged: invalid method %v`, r.Method)
		log.Printf(e)
		http.Error(w, e, http.StatusBadRequest)
	}
}

// proxy handles requests not covered by another handler.
func proxy(w http.ResponseWriter, r *http.Request) {
	var err error

M kindleto.go => kindleto.go +1 -0
@@ 74,6 74,7 @@ func main() {
	mux.HandleFunc("/", proxy)
	mux.HandleFunc("/certificate", clientCertificateRequired)
	mux.HandleFunc("/settings/certificates", manageClientCertificates)
	mux.HandleFunc("/tofu", certificateChanged)
	mux.HandleFunc("/kindleto.css", css)
	mux.HandleFunc("/about", about)
	mux.HandleFunc("/help", help)

M templates/template_strings.go => templates/template_strings.go +8 -0
@@ 176,3 176,11 @@ kindleto/certificates/
</code></pre>
{{template "footer"}}`

const Tofu string = `{{template "header" .}}
<h1>Server certificate changed</h1>
<p>The server certificate for {{.URL}} has changed. Do you want to accept the new certificate and connect to the server?</p>
<form action="/tofu" method="POST">
<input type="hidden" name="url" value="{{.URL}}">
<input type="submit" value="Accept">
</form>
{{template "footer"}}`

M templates/templates.go => templates/templates.go +2 -0
@@ 42,6 42,7 @@ func LoadTemplates(webRoot string) {
	Templates = template.Must(Templates.New("header.html.tmpl").Parse(Header))
	Templates = template.Must(Templates.New("help.html.tmpl").Parse(Help))
	Templates = template.Must(Templates.New("home.html.tmpl").Parse(Home))
	Templates = template.Must(Templates.New("tofu.html.tmpl").Parse(Tofu))
	if webRoot != "" {
		templateFiles := []string{
			webRoot + "/about.html.tmpl",


@@ 55,6 56,7 @@ func LoadTemplates(webRoot string) {
			webRoot + "/header.html.tmpl",
			webRoot + "/help.html.tmpl",
			webRoot + "/home.html.tmpl",
			webRoot + "/tofu.html.tmpl",
		}
		for _, filename := range templateFiles {
			if _, err := os.Stat(filename); err == nil {

M tofu/tofu.go => tofu/tofu.go +13 -6
@@ 6,6 6,7 @@ package tofu

import (
	"crypto/x509"
	"errors"
	"git.sr.ht/~adnano/go-gemini/tofu"
	"log"
	"os"


@@ 17,6 18,8 @@ var TrustAllCerts bool = false
// KnownHostsFile is the file known host fingerprints are loaded from and saved
// to.
var KnownHostsFile string = DefaultKnownHostsFile()
var ErrCertChanged error = errors.New("Server certificate changed")

var hosts tofu.KnownHosts

// DefaultKnownHostsFile returns the default path to the known hosts file or


@@ 51,20 54,24 @@ func LoadKnownHosts() {

// CheckHostCertificate checks the server's certificate fingerprint against the
// ones in the known hosts. It will be added to the known hosts if it's not
// found as per Gemini's TOFU model.
func CheckHostCertificate(hostname string, cert *x509.Certificate) (error, string) {
	var warning string
// found as per Gemini's TOFU model. The certificate will be returned for
// further processing along with ErrCertChanged if its fingerprint has changed.
func CheckHostCertificate(hostname string, cert *x509.Certificate) (error, *x509.Certificate) {
	if !TrustAllCerts {
		host := tofu.NewHost(hostname, cert.Raw)
		knownHost, found := hosts.Lookup(hostname)
		if !found {
			addHost(host)
		} else if knownHost.Fingerprint != host.Fingerprint {
			warning = "The TLS certificate " + hostname + " sent does not match the certificate it sent last time. However, we will proceed with the request."
			addHost(host)
			return ErrCertChanged, cert
		}
	}
	return nil, warning
	return nil, nil
}

// AddHostCertificate adds the certificate to the known hosts.
func AddHostCertificate(hostname string, cert *x509.Certificate) {
	addHost(tofu.NewHost(hostname, cert.Raw))
}

// addHost adds the server's certificate fingerprint to the known hosts and