aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md34
-rw-r--r--client.go44
-rw-r--r--examples/client/client.go6
-rw-r--r--gemini.go52
-rw-r--r--tofu.go68
5 files changed, 124 insertions, 80 deletions
diff --git a/README.md b/README.md
index cb83665..3366668 100644
--- a/README.md
+++ b/README.md
@@ -33,11 +33,41 @@ A quick overview of the Gemini protocol:
The way this is implemented in this package is like so:
1. Client makes a request with `NewRequest`. The client then sends the request
- with `Send(*Request) (*Response, error)`. The client can optionally verify
- the server certificate with `VerifyCertificate(*x509.Certificate, *Request)`
+ with `(*Client).Send(*Request) (*Response, error)`. The client then determines whether
+ to trust the certificate in `TrustCertificte(*x509.Certificate, *KnownHosts) bool`.
+ (See [TOFU](#tofu)).
2. Server recieves the request and constructs a response.
The server calls the `Serve(*ResponseWriter, *Request)` method on the
`Handler` field. The handler writes the response. The server then closes
the connection.
3. Client recieves the response as a `*Response`. The client then handles the
response.
+
+## TOFU
+
+This package provides an easy way to implement Trust-On-First-Use in your
+clients. Here is a simple example 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 {
+ // If the certificate is in the known hosts list, allow the connection
+ if knownHosts.Has(cert) {
+ return true
+ }
+ // Prompt the user
+ if userTrustsCertificateTemporarily() {
+ // Temporarily trust the certificate
+ return true
+ } else if userTrustsCertificatePermanently() {
+ // Add the certificate to the known hosts file
+ knownHosts.Add(cert)
+ return true
+ }
+ // User does not trust the certificate
+ return false
+ },
+}
+```
diff --git a/client.go b/client.go
index bd4cbd4..06f9a67 100644
--- a/client.go
+++ b/client.go
@@ -10,12 +10,15 @@ import (
"net"
"net/url"
"strconv"
+ "strings"
)
// Errors.
var (
- ErrProtocol = errors.New("gemini: protocol error")
- ErrInvalidURL = errors.New("gemini: requested URL is invalid")
+ ErrProtocol = errors.New("gemini: protocol error")
+ ErrInvalidURL = errors.New("gemini: requested URL is invalid")
+ ErrCertificateNotValid = errors.New("gemini: certificate is invalid")
+ ErrCertificateNotTrusted = errors.New("gemini: certificate is not trusted")
)
// Request represents a Gemini request.
@@ -163,24 +166,40 @@ func (resp *Response) read(r *bufio.Reader) error {
}
// Client represents a Gemini client.
-type Client interface {
- // VerifyCertificate will be called to verify the server certificate.
- // If error is not nil, the connection will be aborted.
- VerifyCertificate(cert *x509.Certificate, req *Request) error
+type Client struct {
+ // KnownHosts is a list of known hosts that the client trusts.
+ KnownHosts *KnownHosts
+
+ // 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
}
// Send sends a Gemini request and returns a Gemini response.
-func Send(c Client, req *Request) (*Response, error) {
+func (c *Client) Send(req *Request) (*Response, error) {
// Connect to the host
config := &tls.Config{
InsecureSkipVerify: true,
Certificates: []tls.Certificate{req.Certificate},
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
+ // Parse the certificate
cert, err := x509.ParseCertificate(rawCerts[0])
if err != nil {
return err
}
- return c.VerifyCertificate(cert, req)
+ // Check that the certificate is valid for the hostname
+ if cert.Subject.CommonName != hostname(req.Host) {
+ return ErrCertificateNotValid
+ }
+ // Check that the client trusts the certificate
+ if c.TrustCertificate == nil {
+ if c.KnownHosts == nil || !c.KnownHosts.Has(cert) {
+ return ErrCertificateNotTrusted
+ }
+ } else if !c.TrustCertificate(cert, c.KnownHosts) {
+ return ErrCertificateNotTrusted
+ }
+ return nil
},
}
conn, err := tls.Dial("tcp", req.Host, config)
@@ -206,3 +225,12 @@ func Send(c Client, req *Request) (*Response, error) {
}
return resp, nil
}
+
+// hostname extracts the host name from a valid host or host:port
+func hostname(host string) string {
+ i := strings.LastIndexByte(host, ':')
+ if i != -1 {
+ return host[:i]
+ }
+ return host
+}
diff --git a/examples/client/client.go b/examples/client/client.go
index 6887ebf..4f852bf 100644
--- a/examples/client/client.go
+++ b/examples/client/client.go
@@ -14,8 +14,8 @@ import (
)
var (
- client = &gemini.TOFUClient{
- Trusts: func(cert *x509.Certificate, req *gemini.Request) bool {
+ client = &gemini.Client{
+ TrustCertificate: func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) bool {
// Trust all certificates
return true
},
@@ -45,7 +45,7 @@ func makeRequest(url string) {
}
req.Certificate = cert
- resp, err := gemini.Send(client, req)
+ resp, err := client.Send(req)
if err != nil {
log.Fatal(err)
}
diff --git a/gemini.go b/gemini.go
index 5b95b6a..ccdb5df 100644
--- a/gemini.go
+++ b/gemini.go
@@ -1,13 +1,5 @@
package gemini
-import (
- "crypto/x509"
- "errors"
- "log"
- "os"
- "path/filepath"
-)
-
// Status codes.
const (
StatusInput = 10
@@ -43,47 +35,3 @@ const (
var (
crlf = []byte("\r\n")
)
-
-// TOFUClient is a client that implements Trust-On-First-Use.
-type TOFUClient struct {
- // Trusts, if not nil, will be called to determine whether the client should
- // trust the provided certificate.
- Trusts func(cert *x509.Certificate, req *Request) bool
-}
-
-func (t *TOFUClient) VerifyCertificate(cert *x509.Certificate, req *Request) error {
- if knownHosts.Has(req.URL.Host, cert) {
- return nil
- }
- if t.Trusts != nil && t.Trusts(cert, req) {
- host := NewKnownHost(cert)
- knownHosts = append(knownHosts, host)
- knownHostsFile, err := os.OpenFile(knownHostsPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
- if err != nil {
- log.Print(err)
- }
- if _, err := host.Write(knownHostsFile); err != nil {
- log.Print(err)
- }
- return nil
- }
- return errors.New("gemini: certificate not trusted")
-}
-
-var (
- knownHosts KnownHosts
- knownHostsPath string
- knownHostsFile *os.File
-)
-
-func init() {
- configDir, err := os.UserConfigDir()
- knownHostsPath = filepath.Join(configDir, "gemini")
- os.MkdirAll(knownHostsPath, 0755)
- knownHostsPath = filepath.Join(knownHostsPath, "known_hosts")
- knownHostsFile, err = os.OpenFile(knownHostsPath, os.O_CREATE|os.O_RDONLY, 0644)
- if err != nil {
- return
- }
- knownHosts = ParseKnownHosts(knownHostsFile)
-}
diff --git a/tofu.go b/tofu.go
index 30ec6a2..4461fdd 100644
--- a/tofu.go
+++ b/tofu.go
@@ -5,37 +5,75 @@ import (
"bytes"
"crypto/sha512"
"crypto/x509"
- "errors"
"fmt"
"io"
+ "os"
+ "path/filepath"
"strconv"
"strings"
"time"
)
-// Errors.
-var (
- ErrInvalidKnownHosts = errors.New("gemini: invalid known hosts")
-)
-
// KnownHosts represents a list of known hosts.
-type KnownHosts []KnownHost
+type KnownHosts struct {
+ hosts []KnownHost
+ file *os.File
+}
+
+// LoadKnownHosts loads the known hosts from the provided path.
+// It creates the path and any of its parent directories if they do not exist.
+// The returned KnownHosts appends to the file whenever a certificate is added.
+func LoadKnownHosts(path string) (*KnownHosts, error) {
+ if dir := filepath.Dir(path); dir != "." {
+ err := os.MkdirAll(dir, 0755)
+ if err != nil {
+ return nil, err
+ }
+ }
+ f, err := os.OpenFile(path, os.O_CREATE|os.O_RDONLY, 0644)
+ if err != nil {
+ return nil, err
+ }
+ k := &KnownHosts{}
+ k.Parse(f)
+ f.Close()
+ // Open the file for append-only use
+ f, err = os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
+ if err != nil {
+ return nil, err
+ }
+ k.file = f
+ return k, nil
+}
+
+// Add adds a certificate to the KnownHosts.
+// If KnownHosts was loaded from a file, Add will append to the file.
+func (k *KnownHosts) Add(cert *x509.Certificate) {
+ host := NewKnownHost(cert)
+ k.hosts = append(k.hosts, host)
+ // Append to the file
+ if k.file != nil {
+ host.Write(k.file)
+ }
+}
-// Has reports whether the given hostname and certificate are in the list.
-func (k KnownHosts) Has(hostname string, cert *x509.Certificate) bool {
+// Has reports whether the provided certificate is in the list.
+func (k *KnownHosts) Has(cert *x509.Certificate) bool {
now := time.Now().Unix()
+ hostname := cert.Subject.CommonName
fingerprint := Fingerprint(cert)
- for i := range k {
- if k[i].Expires > now && k[i].Hostname == hostname && k[i].Fingerprint == fingerprint {
+ for i := range k.hosts {
+ if k.hosts[i].Expires > now && k.hosts[i].Hostname == hostname &&
+ k.hosts[i].Fingerprint == fingerprint {
return true
}
}
return false
}
-// ParseKnownHosts parses and returns a list of known hosts from the provided io.Reader.
+// Parse parses the provided reader and adds the parsed known hosts to the list.
// Invalid lines are ignored.
-func ParseKnownHosts(r io.Reader) (hosts KnownHosts) {
+func (k *KnownHosts) Parse(r io.Reader) {
scanner := bufio.NewScanner(r)
for scanner.Scan() {
text := scanner.Text()
@@ -53,14 +91,13 @@ func ParseKnownHosts(r io.Reader) (hosts KnownHosts) {
continue
}
- hosts = append(hosts, KnownHost{
+ k.hosts = append(k.hosts, KnownHost{
Hostname: hostname,
Algorithm: algorithm,
Fingerprint: fingerprint,
Expires: expires,
})
}
- return
}
// KnownHost represents a known host.
@@ -71,6 +108,7 @@ type KnownHost struct {
Expires int64 // unix time of certificate notAfter date
}
+// NewKnownHost creates a new known host from a certificate.
func NewKnownHost(cert *x509.Certificate) KnownHost {
return KnownHost{
Hostname: cert.Subject.CommonName,