Refactor TOFU
This commit is contained in:
parent
b4295dd2dc
commit
927dfd29c5
34
README.md
34
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
|
||||
},
|
||||
}
|
||||
```
|
||||
|
44
client.go
44
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
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
|
52
gemini.go
52
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)
|
||||
}
|
||||
|
68
tofu.go
68
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
|
||||
}
|
||||
|
||||
// Has reports whether the given hostname and certificate are in the list.
|
||||
func (k KnownHosts) Has(hostname string, cert *x509.Certificate) bool {
|
||||
// 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 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,
|
||||
|
Loading…
Reference in New Issue
Block a user