1
0
mirror of https://github.com/makew0rld/amfora.git synced 2024-12-04 14:46:29 -05:00
amfora/client/tofu.go

116 lines
3.4 KiB
Go
Raw Normal View History

2020-06-18 16:54:48 -04:00
package client
import (
"crypto/sha256"
"crypto/x509"
"errors"
"fmt"
"strings"
"time"
"github.com/makeworld-the-better-one/amfora/config"
)
// TOFU implementation.
// Stores cert hash and expiry for now, like Bombadillo.
2020-06-18 17:23:54 -04:00
// There is ongoing TOFU discussion on the mailing list about better
2020-06-18 16:54:48 -04:00
// ways to do this, and I will update this file once those are decided on.
// Update: See #7 for some small improvements made.
2020-06-18 16:54:48 -04:00
var ErrTofu = errors.New("server cert does not match TOFU database")
var tofuStore = config.TofuStore
// idKey returns the config/viper key needed to retrieve
// a cert's ID / fingerprint.
func idKey(domain string, port string) string {
if port == "1965" || port == "" {
return strings.ReplaceAll(domain, ".", "/")
}
return strings.ReplaceAll(domain, ".", "/") + ":" + port
2020-06-18 16:54:48 -04:00
}
func expiryKey(domain string, port string) string {
if port == "1965" || port == "" {
return strings.ReplaceAll(strings.TrimSuffix(domain, "."), ".", "/") + "/expiry"
}
return strings.ReplaceAll(strings.TrimSuffix(domain, "."), ".", "/") + "/expiry" + ":" + port
2020-06-18 16:54:48 -04:00
}
func loadTofuEntry(domain string, port string) (string, time.Time, error) {
id := tofuStore.GetString(idKey(domain, port)) // Fingerprint
2020-06-18 16:54:48 -04:00
if len(id) != 64 {
// Not set, or invalid
return "", time.Time{}, errors.New("not found")
}
expiry := tofuStore.GetTime(expiryKey(domain, port))
2020-06-18 16:54:48 -04:00
if expiry.IsZero() {
// Not set
return id, time.Time{}, errors.New("not found")
}
return id, expiry, nil
}
// certID returns a generic string representing a cert or domain.
func certID(cert *x509.Certificate) string {
h := sha256.New()
h.Write(cert.RawSubjectPublicKeyInfo) // Better than cert.Raw, see #7
2020-06-18 16:54:48 -04:00
return fmt.Sprintf("%X", h.Sum(nil))
}
2020-06-18 16:54:48 -04:00
// origCertID uses cert.Raw, which was used in v1.0.0 of the app.
func origCertID(cert *x509.Certificate) string {
h := sha256.New()
h.Write(cert.Raw) // Better than cert.Raw, see #7
return fmt.Sprintf("%X", h.Sum(nil))
2020-06-18 16:54:48 -04:00
}
func saveTofuEntry(cert *x509.Certificate, port string) {
tofuStore.Set(idKey(cert.Subject.CommonName, port), certID(cert))
tofuStore.Set(expiryKey(cert.Subject.CommonName, port), cert.NotAfter.UTC())
2020-06-18 16:54:48 -04:00
err := tofuStore.WriteConfig()
if err != nil {
panic(err)
}
}
// handleTofu is the abstracted interface for taking care of TOFU.
// A cert is provided and storage, checking, etc, are taken care of.
// It returns a bool indicating if the cert is valid according to
// the TOFU database.
// If false is returned, the connection should not go ahead.
func handleTofu(cert *x509.Certificate, port string) bool {
id, expiry, err := loadTofuEntry(cert.Subject.CommonName, port)
2020-06-18 16:54:48 -04:00
if err != nil {
// Cert isn't in database or data is malformed
// So it can't be checked and anything is valid
saveTofuEntry(cert, port)
return true
}
if time.Now().After(expiry) {
// Old cert expired, so anything is valid
saveTofuEntry(cert, port)
2020-06-18 16:54:48 -04:00
return true
}
if certID(cert) == id {
// Same cert as the one stored
2020-06-18 16:54:48 -04:00
return true
}
if origCertID(cert) == id {
// Valid but uses old ID type
saveTofuEntry(cert, port)
2020-06-18 16:54:48 -04:00
return true
}
return false
}
// RemoveTofuEntry invalidates the TOFU entry in the database for the given cert and port.
// This will make any cert for that domain valid.
//
// The port string can be empty, to indicate port 1965.
func RemoveTofuEntry(cert *x509.Certificate, port string) {
tofuStore.Set(idKey(cert.Subject.CommonName, port), "")
tofuStore.WriteConfig()
}