diff options
| -rw-r--r-- | README.md | 30 | ||||
| -rw-r--r-- | client.go | 8 | ||||
| -rw-r--r-- | examples/client/client.go | 4 | ||||
| -rw-r--r-- | tofu.go | 27 |
4 files changed, 62 insertions, 7 deletions
@@ -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 + }, +} +``` @@ -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 @@ -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) { |