diff --git a/client.go b/client.go index dd38f67..bd4cbd4 100644 --- a/client.go +++ b/client.go @@ -163,14 +163,14 @@ func (resp *Response) read(r *bufio.Reader) error { } // Client represents a Gemini client. -type Client struct { - // VerifyCertificate, if not nil, will be called to verify the server certificate. +type Client interface { + // VerifyCertificate will be called to verify the server certificate. // If error is not nil, the connection will be aborted. - VerifyCertificate func(cert *x509.Certificate, req *Request) error + VerifyCertificate(cert *x509.Certificate, req *Request) error } // Send sends a Gemini request and returns a Gemini response. -func (c *Client) Send(req *Request) (*Response, error) { +func Send(c Client, req *Request) (*Response, error) { // Connect to the host config := &tls.Config{ InsecureSkipVerify: true, diff --git a/examples/client/client.go b/examples/client/client.go index 89bdb12..6887ebf 100644 --- a/examples/client/client.go +++ b/examples/client/client.go @@ -14,12 +14,12 @@ import ( ) var ( - client = &gemini.Client{ - VerifyCertificate: func(cert *x509.Certificate, req *gemini.Request) error { - return nil + client = &gemini.TOFUClient{ + Trusts: func(cert *x509.Certificate, req *gemini.Request) bool { + // Trust all certificates + return true }, } - cert tls.Certificate ) @@ -29,7 +29,7 @@ func init() { // // openssl genrsa -out client.key 2048 // openssl ecparam -genkey -name secp384r1 -out client.key - // openssl req -new -x509 -sha256 -key client.key -out client.crt -days 3650 + // openssl req -new -x509 -sha512 -key client.key -out client.crt -days 365 // var err error cert, err = tls.LoadX509KeyPair("examples/client/client.crt", "examples/client/client.key") @@ -45,13 +45,11 @@ func makeRequest(url string) { } req.Certificate = cert - resp, err := client.Send(req) + resp, err := gemini.Send(client, req) if err != nil { log.Fatal(err) } - fmt.Println(gemini.Fingerprint(resp.TLS.PeerCertificates[0])) - fmt.Println("Status code:", resp.Status) fmt.Println("Meta:", resp.Meta) diff --git a/examples/server/server.go b/examples/server/server.go index 02e8643..f99c6cd 100644 --- a/examples/server/server.go +++ b/examples/server/server.go @@ -15,7 +15,7 @@ func main() { // // openssl genrsa -out server.key 2048 // openssl ecparam -genkey -name secp384r1 -out server.key - // openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 + // openssl req -new -x509 -sha512 -key server.key -out server.crt -days 365 // cert, err := tls.LoadX509KeyPair("examples/server/server.crt", "examples/server/server.key") if err != nil { @@ -27,9 +27,6 @@ func main() { rw.WriteHeader(gemini.StatusSuccess, "text/gemini") rw.Write([]byte("You requested " + req.URL.String())) log.Printf("Request from %s for %s", req.RemoteAddr.String(), req.URL) - if len(req.TLS.PeerCertificates) != 0 { - log.Print("Client certificate: ", gemini.Fingerprint(req.TLS.PeerCertificates[0])) - } }) server := gemini.Server{ diff --git a/gemini.go b/gemini.go index ccdb5df..5b95b6a 100644 --- a/gemini.go +++ b/gemini.go @@ -1,5 +1,13 @@ package gemini +import ( + "crypto/x509" + "errors" + "log" + "os" + "path/filepath" +) + // Status codes. const ( StatusInput = 10 @@ -35,3 +43,47 @@ 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 df0bec9..30ec6a2 100644 --- a/tofu.go +++ b/tofu.go @@ -71,9 +71,18 @@ type KnownHost struct { Expires int64 // unix time of certificate notAfter date } +func NewKnownHost(cert *x509.Certificate) KnownHost { + return KnownHost{ + Hostname: cert.Subject.CommonName, + Algorithm: "SHA-512", + Fingerprint: Fingerprint(cert), + Expires: cert.NotAfter.Unix(), + } +} + // Write writes the known host to the provided io.Writer. func (k KnownHost) Write(w io.Writer) (int, error) { - s := fmt.Sprintf("\n%s %s %s %d", k.Hostname, k.Algorithm, k.Fingerprint, k.Expires) + s := fmt.Sprintf("%s %s %s %d\n", k.Hostname, k.Algorithm, k.Fingerprint, k.Expires) return w.Write([]byte(s)) }