From ace3e682deefbfc15b9f5ee01c73250dcb4f7a5a Mon Sep 17 00:00:00 2001 From: adnano Date: Fri, 25 Sep 2020 18:53:20 -0400 Subject: [PATCH] Remove TLSConfig fields --- README.md | 36 +++----------------- examples/client/client.go | 7 ++-- examples/server/server.go | 16 ++++----- gemini.go | 51 ++++++++++------------------ tofu.go | 70 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 79 deletions(-) create mode 100644 tofu.go diff --git a/README.md b/README.md index 666f6b0..cd49501 100644 --- a/README.md +++ b/README.md @@ -32,40 +32,12 @@ 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 can verify server - certificates in the Request options, see [Recommended TLS - configuration](#recommended-tls-configuration). +1. Client makes a request with `NewRequest`. The client then sends the request + with `Do(*Request) (*Response, error)`. 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. 5. Client recieves the response as a `*Response`. The client then handles the - response. The client can now verify the certificate of the server using a - Trust-On-First-Use method. - -## Recommended TLS configuration - -For clients, the recommended TLS configuration is as follows: - -```go -// Accept self-signed server certificates -req.TLSConfig.InsecureSkipVerify = true -// Manually verify server certificates, using TOFU -req.TLSConfig.VerifyPeerCertificate = func(rawCerts [][]byte, chains [][]*x509.Certificate) error { - // Verify the server certificate here - // Return an error on failure, or nil on success - return nil -} -``` - -Note that `gemini.Get` does not verify server certificates. - -For servers, the recommended TLS configuration is as follows: - -```go -// Specify a certificate -// To load a certificate, use `tls.LoadX509KeyPair`. -srv.TLSConfig.Certificates = append(srv.TLSConfig.Certificates, cert) -// Request client certificates -srv.TLSConfig.ClientAuth = tls.RequestClientCert -``` + response. The client can now verify the certificate of the server using a + Trust-On-First-Use method. diff --git a/examples/client/client.go b/examples/client/client.go index 1716955..a90af18 100644 --- a/examples/client/client.go +++ b/examples/client/client.go @@ -34,8 +34,7 @@ func makeRequest(url string) { if err != nil { log.Fatal(err) } - req.TLSConfig.InsecureSkipVerify = true - req.TLSConfig.Certificates = append(req.TLSConfig.Certificates, cert) + req.Certificate = cert resp, err := gemini.Do(req) if err != nil { log.Fatal(err) @@ -63,9 +62,9 @@ func makeRequest(url string) { case gemini.StatusClassPermanentFailure: log.Fatal("Permanent failure") case gemini.StatusClassClientCertificateRequired: - log.Fatal("Client Certificate Required") + log.Fatal("Client certificate required") default: - log.Fatal("Protocol Error") + log.Fatal("Protocol error") } } diff --git a/examples/server/server.go b/examples/server/server.go index c587c69..02e8643 100644 --- a/examples/server/server.go +++ b/examples/server/server.go @@ -4,7 +4,6 @@ package main import ( "crypto/tls" - "crypto/x509" "log" "git.sr.ht/~adnano/go-gemini" @@ -18,27 +17,24 @@ func main() { // openssl ecparam -genkey -name secp384r1 -out server.key // openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650 // - config := tls.Config{} cert, err := tls.LoadX509KeyPair("examples/server/server.crt", "examples/server/server.key") if err != nil { log.Fatal(err) } - config.Certificates = append(config.Certificates, cert) - config.ClientAuth = tls.RequestClientCert - config.VerifyPeerCertificate = func(rawCerts [][]byte, chains [][]*x509.Certificate) error { - return nil - } mux := &gemini.ServeMux{} mux.HandleFunc("/", func(rw *gemini.ResponseWriter, req *gemini.Request) { - log.Printf("Request from %s for %s with certificates %v", req.RemoteAddr.String(), req.URL.String(), req.TLS.PeerCertificates) 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{ - TLSConfig: config, - Handler: mux, + Handler: mux, + Certificate: cert, } server.ListenAndServe() } diff --git a/gemini.go b/gemini.go index 76b0098..1adf284 100644 --- a/gemini.go +++ b/gemini.go @@ -78,12 +78,8 @@ type Request struct { // This field is ignored by the server. Host string - // TLSConfig provides a TLS configuration for use by the client. - // It is recommended that clients set `InsecureSkipVerify` to true to skip - // verifying TLS certificates, and instead adopt a Trust-On-First-Use - // method of verifying certificates. - // This field is ignored by the server. - TLSConfig tls.Config + // The certificate to use for the request. + Certificate tls.Certificate // RemoteAddr allows servers and other software to record the network // address that sent the request. @@ -133,7 +129,7 @@ func NewProxyRequest(host, rawurl string) (*Request, error) { // write writes the Gemini request to the provided buffered writer. func (r *Request) write(w *bufio.Writer) error { url := r.URL.String() - // UserInfo is invalid + // User is invalid if r.URL.User != nil || len(url) > 1024 { return ErrInvalidURL } @@ -165,32 +161,14 @@ type Response struct { TLS tls.ConnectionState } -// Get makes a request for the provided URL. The host is inferred from the URL. -// -// Get does not verify server certificates. To verify certificates, use a Request. -func Get(url string) (*Response, error) { - req, err := NewRequest(url) - if err != nil { - return nil, err - } - req.TLSConfig.InsecureSkipVerify = true - return Do(req) -} - -// ProxyGet requests the provided URL from the provided host. -func ProxyGet(host, url string) (*Response, error) { - req, err := NewProxyRequest(host, url) - if err != nil { - return nil, err - } - req.TLSConfig.InsecureSkipVerify = true - return Do(req) -} - // Do sends a Gemini request and returns a Gemini response. func Do(req *Request) (*Response, error) { // Connect to the host - conn, err := tls.Dial("tcp", req.Host, &req.TLSConfig) + config := &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{req.Certificate}, + } + conn, err := tls.Dial("tcp", req.Host, config) if err != nil { return nil, err } @@ -267,8 +245,9 @@ type Server struct { // If Addr is empty, the server will listen on the address ":1965". Addr string - // TLSConfig provides a TLS configuration for use by the server. - TLSConfig tls.Config + // Certificate provides a TLS certificate for use by the server. + // Using a self-signed certificate is recommended. + Certificate tls.Certificate // Handler specifies the Handler for requests. // If Handler is not set, the server will error. @@ -278,6 +257,7 @@ type Server struct { // ListenAndServe listens for requests at the server's configured address. func (s *Server) ListenAndServe() error { addr := s.Addr + if addr == "" { addr = ":1965" } @@ -288,7 +268,12 @@ func (s *Server) ListenAndServe() error { } defer ln.Close() - tlsListener := tls.NewListener(ln, &s.TLSConfig) + config := &tls.Config{ + InsecureSkipVerify: true, + Certificates: []tls.Certificate{s.Certificate}, + ClientAuth: tls.RequestClientCert, + } + tlsListener := tls.NewListener(ln, config) return s.Serve(tlsListener) } diff --git a/tofu.go b/tofu.go new file mode 100644 index 0000000..e4ff39f --- /dev/null +++ b/tofu.go @@ -0,0 +1,70 @@ +package gemini + +import ( + "bufio" + "bytes" + "crypto/sha512" + "crypto/x509" + "errors" + "fmt" + "io" + "strconv" + "strings" +) + +var ( + ErrInvalidKnownHosts = errors.New("gemini: invalid known hosts") +) + +// KnownHost represents a known host. +type KnownHost struct { + Hostname string // e.g. gemini.circumlunar.space + Algorithm string // fingerprint algorithm + Fingerprint string // fingerprint in hexadecimal, with ':' between each octet + NotAfter int64 // unix time of certificate notAfter date +} + +// ParseKnownHosts parses and returns a list of known hosts from the provided io.Reader. +func ParseKnownHosts(r io.Reader) ([]KnownHost, error) { + hosts := []KnownHost{} + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + text := scanner.Text() + + parts := strings.Split(text, " ") + if len(parts) < 4 { + return nil, ErrInvalidKnownHosts + } + + hostname := parts[0] + algorithm := parts[1] + fingerprint := parts[2] + notAfter, err := strconv.ParseInt(parts[3], 10, 0) + if err != nil { + return nil, ErrInvalidKnownHosts + } + + hosts = append(hosts, KnownHost{ + Hostname: hostname, + Algorithm: algorithm, + Fingerprint: fingerprint, + NotAfter: notAfter, + }) + } + + return hosts, nil +} + +// Fingerprint returns the SHA-512 fingerprint of the provided certificate. +func Fingerprint(cert *x509.Certificate) string { + sum512 := sha512.Sum512(cert.Raw) + var buf bytes.Buffer + for i, f := range sum512 { + if i > 0 { + fmt.Fprintf(&buf, ":") + } + fmt.Fprintf(&buf, "%02X", f) + } + return buf.String() +}