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:
|
The way this is implemented in this package is like so:
|
||||||
|
|
||||||
1. Client makes a request with `NewRequest`. The client then sends the request
|
1. Client makes a request with `NewRequest`. The client then sends the request
|
||||||
with `Send(*Request) (*Response, error)`. The client can optionally verify
|
with `(*Client).Send(*Request) (*Response, error)`. The client then determines whether
|
||||||
the server certificate with `VerifyCertificate(*x509.Certificate, *Request)`
|
to trust the certificate in `TrustCertificte(*x509.Certificate, *KnownHosts) bool`.
|
||||||
|
(See [TOFU](#tofu)).
|
||||||
2. Server recieves the request and constructs a response.
|
2. Server recieves the request and constructs a response.
|
||||||
The server calls the `Serve(*ResponseWriter, *Request)` method on the
|
The server calls the `Serve(*ResponseWriter, *Request)` method on the
|
||||||
`Handler` field. The handler writes the response. The server then closes
|
`Handler` field. The handler writes the response. The server then closes
|
||||||
the connection.
|
the connection.
|
||||||
3. Client recieves the response as a `*Response`. The client then handles the
|
3. Client recieves the response as a `*Response`. The client then handles the
|
||||||
response.
|
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
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
40
client.go
40
client.go
@ -10,12 +10,15 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Errors.
|
// Errors.
|
||||||
var (
|
var (
|
||||||
ErrProtocol = errors.New("gemini: protocol error")
|
ErrProtocol = errors.New("gemini: protocol error")
|
||||||
ErrInvalidURL = errors.New("gemini: requested URL is invalid")
|
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.
|
// Request represents a Gemini request.
|
||||||
@ -163,24 +166,40 @@ func (resp *Response) read(r *bufio.Reader) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Client represents a Gemini client.
|
// Client represents a Gemini client.
|
||||||
type Client interface {
|
type Client struct {
|
||||||
// VerifyCertificate will be called to verify the server certificate.
|
// KnownHosts is a list of known hosts that the client trusts.
|
||||||
// If error is not nil, the connection will be aborted.
|
KnownHosts *KnownHosts
|
||||||
VerifyCertificate(cert *x509.Certificate, req *Request) error
|
|
||||||
|
// 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.
|
// 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
|
// Connect to the host
|
||||||
config := &tls.Config{
|
config := &tls.Config{
|
||||||
InsecureSkipVerify: true,
|
InsecureSkipVerify: true,
|
||||||
Certificates: []tls.Certificate{req.Certificate},
|
Certificates: []tls.Certificate{req.Certificate},
|
||||||
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
VerifyPeerCertificate: func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||||
|
// Parse the certificate
|
||||||
cert, err := x509.ParseCertificate(rawCerts[0])
|
cert, err := x509.ParseCertificate(rawCerts[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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)
|
conn, err := tls.Dial("tcp", req.Host, config)
|
||||||
@ -206,3 +225,12 @@ func Send(c Client, req *Request) (*Response, error) {
|
|||||||
}
|
}
|
||||||
return resp, nil
|
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 (
|
var (
|
||||||
client = &gemini.TOFUClient{
|
client = &gemini.Client{
|
||||||
Trusts: func(cert *x509.Certificate, req *gemini.Request) bool {
|
TrustCertificate: func(cert *x509.Certificate, knownHosts *gemini.KnownHosts) bool {
|
||||||
// Trust all certificates
|
// Trust all certificates
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
@ -45,7 +45,7 @@ func makeRequest(url string) {
|
|||||||
}
|
}
|
||||||
req.Certificate = cert
|
req.Certificate = cert
|
||||||
|
|
||||||
resp, err := gemini.Send(client, req)
|
resp, err := client.Send(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
52
gemini.go
52
gemini.go
@ -1,13 +1,5 @@
|
|||||||
package gemini
|
package gemini
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/x509"
|
|
||||||
"errors"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Status codes.
|
// Status codes.
|
||||||
const (
|
const (
|
||||||
StatusInput = 10
|
StatusInput = 10
|
||||||
@ -43,47 +35,3 @@ 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)
|
|
||||||
}
|
|
||||||
|
68
tofu.go
68
tofu.go
@ -5,37 +5,75 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Errors.
|
|
||||||
var (
|
|
||||||
ErrInvalidKnownHosts = errors.New("gemini: invalid known hosts")
|
|
||||||
)
|
|
||||||
|
|
||||||
// KnownHosts represents a list of 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.
|
// LoadKnownHosts loads the known hosts from the provided path.
|
||||||
func (k KnownHosts) Has(hostname string, cert *x509.Certificate) bool {
|
// 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()
|
now := time.Now().Unix()
|
||||||
|
hostname := cert.Subject.CommonName
|
||||||
fingerprint := Fingerprint(cert)
|
fingerprint := Fingerprint(cert)
|
||||||
for i := range k {
|
for i := range k.hosts {
|
||||||
if k[i].Expires > now && k[i].Hostname == hostname && k[i].Fingerprint == fingerprint {
|
if k.hosts[i].Expires > now && k.hosts[i].Hostname == hostname &&
|
||||||
|
k.hosts[i].Fingerprint == fingerprint {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
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.
|
// Invalid lines are ignored.
|
||||||
func ParseKnownHosts(r io.Reader) (hosts KnownHosts) {
|
func (k *KnownHosts) Parse(r io.Reader) {
|
||||||
scanner := bufio.NewScanner(r)
|
scanner := bufio.NewScanner(r)
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
text := scanner.Text()
|
text := scanner.Text()
|
||||||
@ -53,14 +91,13 @@ func ParseKnownHosts(r io.Reader) (hosts KnownHosts) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
hosts = append(hosts, KnownHost{
|
k.hosts = append(k.hosts, KnownHost{
|
||||||
Hostname: hostname,
|
Hostname: hostname,
|
||||||
Algorithm: algorithm,
|
Algorithm: algorithm,
|
||||||
Fingerprint: fingerprint,
|
Fingerprint: fingerprint,
|
||||||
Expires: expires,
|
Expires: expires,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// KnownHost represents a known host.
|
// KnownHost represents a known host.
|
||||||
@ -71,6 +108,7 @@ type KnownHost struct {
|
|||||||
Expires int64 // unix time of certificate notAfter date
|
Expires int64 // unix time of certificate notAfter date
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewKnownHost creates a new known host from a certificate.
|
||||||
func NewKnownHost(cert *x509.Certificate) KnownHost {
|
func NewKnownHost(cert *x509.Certificate) KnownHost {
|
||||||
return KnownHost{
|
return KnownHost{
|
||||||
Hostname: cert.Subject.CommonName,
|
Hostname: cert.Subject.CommonName,
|
||||||
|
Loading…
Reference in New Issue
Block a user