From bf3e6b3c5c875d58732530cd98d8d0c026e9f773 Mon Sep 17 00:00:00 2001 From: adnano Date: Sat, 26 Sep 2020 13:27:03 -0400 Subject: [PATCH] Differentiate between unknown and untrusted certificates --- README.md | 30 ++++++++++++++++++++++++++++-- client.go | 8 +++++--- examples/client/client.go | 4 ++-- tofu.go | 27 +++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 596a5f7..bec4258 100644 --- a/README.md +++ b/README.md @@ -51,9 +51,9 @@ clients. Here is a simple client using TOFU to authenticate certificates: ```go client := &gemini.Client{ KnownHosts: gemini.LoadKnownHosts(".local/share/gemini/known_hosts"), - TrustCertificate: func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) bool { + TrustCertificate: func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) error { // If the certificate is in the known hosts list, allow the connection - if knownHosts.Has(cert) { + if err := knownHosts.Lookup(cert); { return true } // Prompt the user @@ -70,3 +70,29 @@ client := &gemini.Client{ }, } ``` + +```go +client := &gemini.Client{ + TrustCertificate: func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) error { + err := knownHosts.Lookup(cert) + if err != nil { + switch err { + case gemini.ErrCertificateNotTrusted: + // Alert the user that the certificate is not trusted + alertUser() + case gemini.ErrCertificateUnknown: + // Prompt the user to trust the certificate + if userTrustsCertificateTemporarily() { + // Temporarily trust the certificate + return nil + } else if user.TrustsCertificatePermanently() { + // Add the certificate to the known hosts file + knownHosts.Add(cert) + return nil + } + } + } + return err + }, +} +``` diff --git a/client.go b/client.go index 6d2fd4b..bb1fa90 100644 --- a/client.go +++ b/client.go @@ -19,6 +19,7 @@ var ( ErrInvalidURL = errors.New("gemini: requested URL is invalid") ErrCertificateNotValid = errors.New("gemini: certificate is invalid") ErrCertificateNotTrusted = errors.New("gemini: certificate is not trusted") + ErrCertificateUnknown = errors.New("gemini: certificate is unknown") ) // Request represents a Gemini request. @@ -171,7 +172,8 @@ type Client struct { // TrustCertificate, if not nil, will be called to determine whether the // client should trust the given certificate. - TrustCertificate func(cert *x509.Certificate, knownHosts *KnownHosts) bool + // If error is not nil, the connection will be aborted. + TrustCertificate func(cert *x509.Certificate, knownHosts *KnownHosts) error } // Send sends a Gemini request and returns a Gemini response. @@ -196,8 +198,8 @@ func (c *Client) Send(req *Request) (*Response, error) { if c.KnownHosts == nil || !c.KnownHosts.Has(cert) { return ErrCertificateNotTrusted } - } else if !c.TrustCertificate(cert, c.KnownHosts) { - return ErrCertificateNotTrusted + } else if err := c.TrustCertificate(cert, c.KnownHosts); err != nil { + return err } return nil }, diff --git a/examples/client/client.go b/examples/client/client.go index 4f852bf..31e24b2 100644 --- a/examples/client/client.go +++ b/examples/client/client.go @@ -15,9 +15,9 @@ import ( var ( client = &gemini.Client{ - TrustCertificate: func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) bool { + TrustCertificate: func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) error { // Trust all certificates - return true + return nil }, } cert tls.Certificate diff --git a/tofu.go b/tofu.go index 4461fdd..d3ab251 100644 --- a/tofu.go +++ b/tofu.go @@ -71,6 +71,33 @@ func (k *KnownHosts) Has(cert *x509.Certificate) bool { return false } +// Lookup looks for the provided certificate in the list of known hosts. +// If the hostname is in the list, but the fingerprint differs, +// Lookup returns ErrCertificateNotTrusted. +// If the hostname is not in the list, Lookup returns ErrCertificateUnknown. +// If the certificate is found and the fingerprint matches, error will be nil. +func (k *KnownHosts) Lookup(cert *x509.Certificate) error { + now := time.Now().Unix() + hostname := cert.Subject.CommonName + fingerprint := Fingerprint(cert) + for i := range k.hosts { + if k.hosts[i].Hostname != hostname { + continue + } + if k.hosts[i].Expires <= now { + // Certificate is expired + continue + } + if k.hosts[i].Fingerprint == fingerprint { + // Fingerprint matches + return nil + } + // Fingerprint does not match + return ErrCertificateNotTrusted + } + return ErrCertificateUnknown +} + // Parse parses the provided reader and adds the parsed known hosts to the list. // Invalid lines are ignored. func (k *KnownHosts) Parse(r io.Reader) {