Implement basic TOFU

This commit is contained in:
adnano 2020-09-25 21:43:13 -04:00
parent 4a95fe4a90
commit b4295dd2dc
5 changed files with 73 additions and 17 deletions

View File

@ -163,14 +163,14 @@ func (resp *Response) read(r *bufio.Reader) error {
} }
// Client represents a Gemini client. // Client represents a Gemini client.
type Client struct { type Client interface {
// VerifyCertificate, if not nil, will be called to verify the server certificate. // VerifyCertificate will be called to verify the server certificate.
// If error is not nil, the connection will be aborted. // 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. // 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 // Connect to the host
config := &tls.Config{ config := &tls.Config{
InsecureSkipVerify: true, InsecureSkipVerify: true,

View File

@ -14,12 +14,12 @@ import (
) )
var ( var (
client = &gemini.Client{ client = &gemini.TOFUClient{
VerifyCertificate: func(cert *x509.Certificate, req *gemini.Request) error { Trusts: func(cert *x509.Certificate, req *gemini.Request) bool {
return nil // Trust all certificates
return true
}, },
} }
cert tls.Certificate cert tls.Certificate
) )
@ -29,7 +29,7 @@ func init() {
// //
// openssl genrsa -out client.key 2048 // openssl genrsa -out client.key 2048
// openssl ecparam -genkey -name secp384r1 -out client.key // 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 var err error
cert, err = tls.LoadX509KeyPair("examples/client/client.crt", "examples/client/client.key") cert, err = tls.LoadX509KeyPair("examples/client/client.crt", "examples/client/client.key")
@ -45,13 +45,11 @@ func makeRequest(url string) {
} }
req.Certificate = cert req.Certificate = cert
resp, err := client.Send(req) resp, err := gemini.Send(client, req)
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
fmt.Println(gemini.Fingerprint(resp.TLS.PeerCertificates[0]))
fmt.Println("Status code:", resp.Status) fmt.Println("Status code:", resp.Status)
fmt.Println("Meta:", resp.Meta) fmt.Println("Meta:", resp.Meta)

View File

@ -15,7 +15,7 @@ func main() {
// //
// openssl genrsa -out server.key 2048 // openssl genrsa -out server.key 2048
// openssl ecparam -genkey -name secp384r1 -out server.key // 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") cert, err := tls.LoadX509KeyPair("examples/server/server.crt", "examples/server/server.key")
if err != nil { if err != nil {
@ -27,9 +27,6 @@ func main() {
rw.WriteHeader(gemini.StatusSuccess, "text/gemini") rw.WriteHeader(gemini.StatusSuccess, "text/gemini")
rw.Write([]byte("You requested " + req.URL.String())) rw.Write([]byte("You requested " + req.URL.String()))
log.Printf("Request from %s for %s", req.RemoteAddr.String(), req.URL) 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{ server := gemini.Server{

View File

@ -1,5 +1,13 @@
package gemini package gemini
import (
"crypto/x509"
"errors"
"log"
"os"
"path/filepath"
)
// Status codes. // Status codes.
const ( const (
StatusInput = 10 StatusInput = 10
@ -35,3 +43,47 @@ const (
var ( var (
crlf = []byte("\r\n") 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)
}

11
tofu.go
View File

@ -71,9 +71,18 @@ type KnownHost struct {
Expires int64 // unix time of certificate notAfter date 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. // Write writes the known host to the provided io.Writer.
func (k KnownHost) Write(w io.Writer) (int, error) { 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)) return w.Write([]byte(s))
} }