Differentiate between unknown and untrusted certificates

This commit is contained in:
adnano 2020-09-26 13:27:03 -04:00
parent fc96076472
commit bf3e6b3c5c
4 changed files with 62 additions and 7 deletions

View File

@ -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
},
}
```

View File

@ -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
},

View File

@ -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

27
tofu.go
View File

@ -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) {