wip: migrate to mono-repo. SPN has already been moved to spn/
This commit is contained in:
392
spn/cabin/config-public.go
Normal file
392
spn/cabin/config-public.go
Normal file
@@ -0,0 +1,392 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"github.com/safing/portbase/config"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/service/netenv"
|
||||
"github.com/safing/portmaster/service/profile/endpoints"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
// Configuration Keys.
|
||||
var (
|
||||
// Name of the node.
|
||||
publicCfgOptionNameKey = "spn/publicHub/name"
|
||||
publicCfgOptionName config.StringOption
|
||||
publicCfgOptionNameDefault = ""
|
||||
publicCfgOptionNameOrder = 512
|
||||
|
||||
// Person or organisation, who is in control of the node (should be same for all nodes of this person or organisation).
|
||||
publicCfgOptionGroupKey = "spn/publicHub/group"
|
||||
publicCfgOptionGroup config.StringOption
|
||||
publicCfgOptionGroupDefault = ""
|
||||
publicCfgOptionGroupOrder = 513
|
||||
|
||||
// Contact possibility (recommended, but optional).
|
||||
publicCfgOptionContactAddressKey = "spn/publicHub/contactAddress"
|
||||
publicCfgOptionContactAddress config.StringOption
|
||||
publicCfgOptionContactAddressDefault = ""
|
||||
publicCfgOptionContactAddressOrder = 514
|
||||
|
||||
// Type of service of the contact address, if not email.
|
||||
publicCfgOptionContactServiceKey = "spn/publicHub/contactService"
|
||||
publicCfgOptionContactService config.StringOption
|
||||
publicCfgOptionContactServiceDefault = ""
|
||||
publicCfgOptionContactServiceOrder = 515
|
||||
|
||||
// Hosters - supply chain (reseller, hosting provider, datacenter operator, ...).
|
||||
publicCfgOptionHostersKey = "spn/publicHub/hosters"
|
||||
publicCfgOptionHosters config.StringArrayOption
|
||||
publicCfgOptionHostersDefault = []string{}
|
||||
publicCfgOptionHostersOrder = 516
|
||||
|
||||
// Datacenter
|
||||
// Format: CC-COMPANY-INTERNALCODE
|
||||
// Eg: DE-Hetzner-FSN1-DC5
|
||||
//.
|
||||
publicCfgOptionDatacenterKey = "spn/publicHub/datacenter"
|
||||
publicCfgOptionDatacenter config.StringOption
|
||||
publicCfgOptionDatacenterDefault = ""
|
||||
publicCfgOptionDatacenterOrder = 517
|
||||
|
||||
// Network Location and Access.
|
||||
|
||||
// IPv4 must be global and accessible.
|
||||
publicCfgOptionIPv4Key = "spn/publicHub/ip4"
|
||||
publicCfgOptionIPv4 config.StringOption
|
||||
publicCfgOptionIPv4Default = ""
|
||||
publicCfgOptionIPv4Order = 518
|
||||
|
||||
// IPv6 must be global and accessible.
|
||||
publicCfgOptionIPv6Key = "spn/publicHub/ip6"
|
||||
publicCfgOptionIPv6 config.StringOption
|
||||
publicCfgOptionIPv6Default = ""
|
||||
publicCfgOptionIPv6Order = 519
|
||||
|
||||
// Transports.
|
||||
publicCfgOptionTransportsKey = "spn/publicHub/transports"
|
||||
publicCfgOptionTransports config.StringArrayOption
|
||||
publicCfgOptionTransportsDefault = []string{
|
||||
"tcp:17",
|
||||
}
|
||||
publicCfgOptionTransportsOrder = 520
|
||||
|
||||
// Entry Policy.
|
||||
publicCfgOptionEntryKey = "spn/publicHub/entry"
|
||||
publicCfgOptionEntry config.StringArrayOption
|
||||
publicCfgOptionEntryDefault = []string{}
|
||||
publicCfgOptionEntryOrder = 521
|
||||
|
||||
// Exit Policy.
|
||||
publicCfgOptionExitKey = "spn/publicHub/exit"
|
||||
publicCfgOptionExit config.StringArrayOption
|
||||
publicCfgOptionExitDefault = []string{"- * TCP/25"}
|
||||
publicCfgOptionExitOrder = 522
|
||||
|
||||
// Allow Unencrypted.
|
||||
publicCfgOptionAllowUnencryptedKey = "spn/publicHub/allowUnencrypted"
|
||||
publicCfgOptionAllowUnencrypted config.BoolOption
|
||||
publicCfgOptionAllowUnencryptedDefault = false
|
||||
publicCfgOptionAllowUnencryptedOrder = 523
|
||||
)
|
||||
|
||||
func prepPublicHubConfig() error {
|
||||
err := config.Register(&config.Option{
|
||||
Name: "Name",
|
||||
Key: publicCfgOptionNameKey,
|
||||
Description: "Human readable name of the Hub.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionNameDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionNameOrder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionName = config.GetAsString(publicCfgOptionNameKey, publicCfgOptionNameDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Group",
|
||||
Key: publicCfgOptionGroupKey,
|
||||
Description: "Name of the hub group this Hub belongs to.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionGroupDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionGroupOrder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionGroup = config.GetAsString(publicCfgOptionGroupKey, publicCfgOptionGroupDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Contact Address",
|
||||
Key: publicCfgOptionContactAddressKey,
|
||||
Description: "Contact address where the Hub operator can be reached.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionContactAddressDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionContactAddressOrder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionContactAddress = config.GetAsString(publicCfgOptionContactAddressKey, publicCfgOptionContactAddressDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Contact Service",
|
||||
Key: publicCfgOptionContactServiceKey,
|
||||
Description: "Name of the service the contact address corresponds to, if not email.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionContactServiceDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionContactServiceOrder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionContactService = config.GetAsString(publicCfgOptionContactServiceKey, publicCfgOptionContactServiceDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Hosters",
|
||||
Key: publicCfgOptionHostersKey,
|
||||
Description: "List of all involved entities and organisations that are involved in hosting this Hub.",
|
||||
OptType: config.OptTypeStringArray,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionHostersDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionHostersOrder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionHosters = config.GetAsStringArray(publicCfgOptionHostersKey, publicCfgOptionHostersDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Datacenter",
|
||||
Key: publicCfgOptionDatacenterKey,
|
||||
Description: "Identifier of the datacenter this Hub is hosted in.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionDatacenterDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionDatacenterOrder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionDatacenter = config.GetAsString(publicCfgOptionDatacenterKey, publicCfgOptionDatacenterDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "IPv4",
|
||||
Key: publicCfgOptionIPv4Key,
|
||||
Description: "IPv4 address of this Hub. Must be globally reachable.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionIPv4Default,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionIPv4Order,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionIPv4 = config.GetAsString(publicCfgOptionIPv4Key, publicCfgOptionIPv4Default)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "IPv6",
|
||||
Key: publicCfgOptionIPv6Key,
|
||||
Description: "IPv6 address of this Hub. Must be globally reachable.",
|
||||
OptType: config.OptTypeString,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionIPv6Default,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionIPv6Order,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionIPv6 = config.GetAsString(publicCfgOptionIPv6Key, publicCfgOptionIPv6Default)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Transports",
|
||||
Key: publicCfgOptionTransportsKey,
|
||||
Description: "List of transports this Hub supports.",
|
||||
OptType: config.OptTypeStringArray,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionTransportsDefault,
|
||||
ValidationFunc: func(value any) error {
|
||||
if transports, ok := value.([]string); ok {
|
||||
for i, transport := range transports {
|
||||
if _, err := hub.ParseTransport(transport); err != nil {
|
||||
return fmt.Errorf("failed to parse transport #%d: %w", i, err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("not a []string, but %T", value)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionTransportsOrder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionTransports = config.GetAsStringArray(publicCfgOptionTransportsKey, publicCfgOptionTransportsDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Entry",
|
||||
Key: publicCfgOptionEntryKey,
|
||||
Description: "Define an entry policy. The format is the same for the endpoint lists. Default is permit.",
|
||||
OptType: config.OptTypeStringArray,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionEntryDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionEntryOrder,
|
||||
config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionEntry = config.GetAsStringArray(publicCfgOptionEntryKey, publicCfgOptionEntryDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Exit",
|
||||
Key: publicCfgOptionExitKey,
|
||||
Description: "Define an exit policy. The format is the same for the endpoint lists. Default is permit.",
|
||||
OptType: config.OptTypeStringArray,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionExitDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionExitOrder,
|
||||
config.DisplayHintAnnotation: endpoints.DisplayHintEndpointList,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionExit = config.GetAsStringArray(publicCfgOptionExitKey, publicCfgOptionExitDefault)
|
||||
|
||||
err = config.Register(&config.Option{
|
||||
Name: "Allow Unencrypted Connections",
|
||||
Key: publicCfgOptionAllowUnencryptedKey,
|
||||
Description: "Advertise that this Hub is available for handling unencrypted connections, as detected by clients.",
|
||||
OptType: config.OptTypeBool,
|
||||
ExpertiseLevel: config.ExpertiseLevelExpert,
|
||||
RequiresRestart: true,
|
||||
DefaultValue: publicCfgOptionAllowUnencryptedDefault,
|
||||
Annotations: config.Annotations{
|
||||
config.DisplayOrderAnnotation: publicCfgOptionAllowUnencryptedOrder,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
publicCfgOptionAllowUnencrypted = config.GetAsBool(publicCfgOptionAllowUnencryptedKey, publicCfgOptionAllowUnencryptedDefault)
|
||||
|
||||
// update defaults from system
|
||||
setDynamicPublicDefaults()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getPublicHubInfo() *hub.Announcement {
|
||||
// get configuration
|
||||
info := &hub.Announcement{
|
||||
Name: publicCfgOptionName(),
|
||||
Group: publicCfgOptionGroup(),
|
||||
ContactAddress: publicCfgOptionContactAddress(),
|
||||
ContactService: publicCfgOptionContactService(),
|
||||
Hosters: publicCfgOptionHosters(),
|
||||
Datacenter: publicCfgOptionDatacenter(),
|
||||
Transports: publicCfgOptionTransports(),
|
||||
Entry: publicCfgOptionEntry(),
|
||||
Exit: publicCfgOptionExit(),
|
||||
Flags: []string{},
|
||||
}
|
||||
|
||||
if publicCfgOptionAllowUnencrypted() {
|
||||
info.Flags = append(info.Flags, hub.FlagAllowUnencrypted)
|
||||
}
|
||||
|
||||
ip4 := publicCfgOptionIPv4()
|
||||
if ip4 != "" {
|
||||
ip := net.ParseIP(ip4)
|
||||
if ip == nil {
|
||||
log.Warningf("spn/cabin: invalid %s config: %s", publicCfgOptionIPv4Key, ip4)
|
||||
} else {
|
||||
info.IPv4 = ip
|
||||
}
|
||||
}
|
||||
|
||||
ip6 := publicCfgOptionIPv6()
|
||||
if ip6 != "" {
|
||||
ip := net.ParseIP(ip6)
|
||||
if ip == nil {
|
||||
log.Warningf("spn/cabin: invalid %s config: %s", publicCfgOptionIPv6Key, ip6)
|
||||
} else {
|
||||
info.IPv6 = ip
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
}
|
||||
|
||||
func setDynamicPublicDefaults() {
|
||||
// name
|
||||
hostname, err := os.Hostname()
|
||||
if err == nil {
|
||||
err := config.SetDefaultConfigOption(publicCfgOptionNameKey, hostname)
|
||||
if err != nil {
|
||||
log.Warningf("spn/cabin: failed to set %s default to %s", publicCfgOptionNameKey, hostname)
|
||||
}
|
||||
}
|
||||
|
||||
// IPs
|
||||
v4IPs, v6IPs, err := netenv.GetAssignedGlobalAddresses()
|
||||
if err != nil {
|
||||
log.Warningf("spn/cabin: failed to get assigned addresses: %s", err)
|
||||
return
|
||||
}
|
||||
if len(v4IPs) == 1 {
|
||||
err = config.SetDefaultConfigOption(publicCfgOptionIPv4Key, v4IPs[0].String())
|
||||
if err != nil {
|
||||
log.Warningf("spn/cabin: failed to set %s default to %s", publicCfgOptionIPv4Key, v4IPs[0].String())
|
||||
}
|
||||
}
|
||||
if len(v6IPs) == 1 {
|
||||
err = config.SetDefaultConfigOption(publicCfgOptionIPv6Key, v6IPs[0].String())
|
||||
if err != nil {
|
||||
log.Warningf("spn/cabin: failed to set %s default to %s", publicCfgOptionIPv6Key, v6IPs[0].String())
|
||||
}
|
||||
}
|
||||
}
|
||||
98
spn/cabin/database.go
Normal file
98
spn/cabin/database.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/portbase/database"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
var db = database.NewInterface(nil)
|
||||
|
||||
// LoadIdentity loads an identify with the given key.
|
||||
func LoadIdentity(key string) (id *Identity, changed bool, err error) {
|
||||
r, err := db.Get(key)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
id, err = EnsureIdentity(r)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to parse identity: %w", err)
|
||||
}
|
||||
|
||||
// Check if required fields are present.
|
||||
switch {
|
||||
case id.Hub == nil:
|
||||
return nil, false, errors.New("missing id.Hub")
|
||||
case id.Signet == nil:
|
||||
return nil, false, errors.New("missing id.Signet")
|
||||
case id.Hub.Info == nil:
|
||||
return nil, false, errors.New("missing hub.Info")
|
||||
case id.Hub.Status == nil:
|
||||
return nil, false, errors.New("missing hub.Status")
|
||||
case id.ID != id.Hub.ID:
|
||||
return nil, false, errors.New("hub.ID mismatch")
|
||||
case id.ID != id.Hub.Info.ID:
|
||||
return nil, false, errors.New("hub.Info.ID mismatch")
|
||||
case id.Map == "":
|
||||
return nil, false, errors.New("invalid id.Map")
|
||||
case id.Hub.Map == "":
|
||||
return nil, false, errors.New("invalid hub.Map")
|
||||
case id.Hub.FirstSeen.IsZero():
|
||||
return nil, false, errors.New("missing hub.FirstSeen")
|
||||
case id.Hub.Info.Timestamp == 0:
|
||||
return nil, false, errors.New("missing hub.Info.Timestamp")
|
||||
case id.Hub.Status.Timestamp == 0:
|
||||
return nil, false, errors.New("missing hub.Status.Timestamp")
|
||||
}
|
||||
|
||||
// Run a initial maintenance routine.
|
||||
infoChanged, err := id.MaintainAnnouncement(nil, true)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to initialize announcement: %w", err)
|
||||
}
|
||||
statusChanged, err := id.MaintainStatus(nil, nil, nil, true)
|
||||
if err != nil {
|
||||
return nil, false, fmt.Errorf("failed to initialize status: %w", err)
|
||||
}
|
||||
|
||||
// Ensure the Measurements reset the values.
|
||||
measurements := id.Hub.GetMeasurements()
|
||||
measurements.SetLatency(0)
|
||||
measurements.SetCapacity(0)
|
||||
measurements.SetCalculatedCost(hub.MaxCalculatedCost)
|
||||
|
||||
return id, infoChanged || statusChanged, nil
|
||||
}
|
||||
|
||||
// EnsureIdentity makes sure a database record is an Identity.
|
||||
func EnsureIdentity(r record.Record) (*Identity, error) {
|
||||
// unwrap
|
||||
if r.IsWrapped() {
|
||||
// only allocate a new struct, if we need it
|
||||
id := &Identity{}
|
||||
err := record.Unwrap(r, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// or adjust type
|
||||
id, ok := r.(*Identity)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("record not of type *Identity, but %T", r)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Save saves the Identity to the database.
|
||||
func (id *Identity) Save() error {
|
||||
if !id.KeyIsSet() {
|
||||
return errors.New("no key set")
|
||||
}
|
||||
|
||||
return db.Put(id)
|
||||
}
|
||||
311
spn/cabin/identity.go
Normal file
311
spn/cabin/identity.go
Normal file
@@ -0,0 +1,311 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/safing/jess"
|
||||
"github.com/safing/jess/tools"
|
||||
"github.com/safing/portbase/database/record"
|
||||
"github.com/safing/portbase/info"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultIDKeyScheme is the default jess tool for creating ID keys.
|
||||
DefaultIDKeyScheme = "Ed25519"
|
||||
|
||||
// DefaultIDKeySecurityLevel is the default security level for creating ID keys.
|
||||
DefaultIDKeySecurityLevel = 256 // Ed25519 security level is fixed, setting is ignored.
|
||||
)
|
||||
|
||||
// Identity holds the identity of a Hub.
|
||||
type Identity struct {
|
||||
record.Base
|
||||
|
||||
ID string
|
||||
Map string
|
||||
Hub *hub.Hub
|
||||
Signet *jess.Signet
|
||||
|
||||
ExchKeys map[string]*ExchKey
|
||||
|
||||
infoExportCache []byte
|
||||
statusExportCache []byte
|
||||
}
|
||||
|
||||
// Lock locks the Identity through the Hub lock.
|
||||
func (id *Identity) Lock() {
|
||||
id.Hub.Lock()
|
||||
}
|
||||
|
||||
// Unlock unlocks the Identity through the Hub lock.
|
||||
func (id *Identity) Unlock() {
|
||||
id.Hub.Unlock()
|
||||
}
|
||||
|
||||
// ExchKey holds the private information of a HubKey.
|
||||
type ExchKey struct {
|
||||
Created time.Time
|
||||
Expires time.Time
|
||||
key *jess.Signet
|
||||
tool *tools.Tool
|
||||
}
|
||||
|
||||
// CreateIdentity creates a new identity.
|
||||
func CreateIdentity(ctx context.Context, mapName string) (*Identity, error) {
|
||||
id := &Identity{
|
||||
Map: mapName,
|
||||
ExchKeys: make(map[string]*ExchKey),
|
||||
}
|
||||
|
||||
// create signet
|
||||
signet, recipient, err := hub.CreateHubSignet(DefaultIDKeyScheme, DefaultIDKeySecurityLevel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
id.Signet = signet
|
||||
id.ID = signet.ID
|
||||
id.Hub = &hub.Hub{
|
||||
ID: id.ID,
|
||||
Map: mapName,
|
||||
PublicKey: recipient,
|
||||
}
|
||||
|
||||
// initial maintenance routine
|
||||
_, err = id.MaintainAnnouncement(nil, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize announcement: %w", err)
|
||||
}
|
||||
_, err = id.MaintainStatus([]*hub.Lane{}, new(int), nil, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize status: %w", err)
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// MaintainAnnouncement maintains the Hub's Announcenemt and returns whether
|
||||
// there was a change that should be communicated to other Hubs.
|
||||
// If newInfo is nil, it will be derived from configuration.
|
||||
func (id *Identity) MaintainAnnouncement(newInfo *hub.Announcement, selfcheck bool) (changed bool, err error) {
|
||||
id.Lock()
|
||||
defer id.Unlock()
|
||||
|
||||
// Populate new info with data.
|
||||
if newInfo == nil {
|
||||
newInfo = getPublicHubInfo()
|
||||
}
|
||||
newInfo.ID = id.Hub.ID
|
||||
if id.Hub.Info != nil {
|
||||
newInfo.Timestamp = id.Hub.Info.Timestamp
|
||||
}
|
||||
if !newInfo.Equal(id.Hub.Info) {
|
||||
changed = true
|
||||
}
|
||||
|
||||
if changed {
|
||||
// Update timestamp.
|
||||
newInfo.Timestamp = time.Now().Unix()
|
||||
}
|
||||
|
||||
if changed || selfcheck {
|
||||
// Export new data.
|
||||
newInfoData, err := newInfo.Export(id.signingEnvelope())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to export: %w", err)
|
||||
}
|
||||
|
||||
// Apply the status as all other Hubs would in order to check if it's valid.
|
||||
_, _, _, err = hub.ApplyAnnouncement(id.Hub, newInfoData, conf.MainMapName, conf.MainMapScope, true)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to apply new announcement: %w", err)
|
||||
}
|
||||
id.infoExportCache = newInfoData
|
||||
|
||||
// Save message to hub message storage.
|
||||
err = hub.SaveHubMsg(id.ID, conf.MainMapName, hub.MsgTypeAnnouncement, newInfoData)
|
||||
if err != nil {
|
||||
log.Warningf("spn/cabin: failed to save own new/updated announcement of %s: %s", id.ID, err)
|
||||
}
|
||||
}
|
||||
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// MaintainStatus maintains the Hub's Status and returns whether there was a change that should be communicated to other Hubs.
|
||||
func (id *Identity) MaintainStatus(lanes []*hub.Lane, load *int, flags []string, selfcheck bool) (changed bool, err error) {
|
||||
id.Lock()
|
||||
defer id.Unlock()
|
||||
|
||||
// Create a new status or make a copy of the status for editing.
|
||||
var newStatus *hub.Status
|
||||
if id.Hub.Status != nil {
|
||||
newStatus = id.Hub.Status.Copy()
|
||||
} else {
|
||||
newStatus = &hub.Status{}
|
||||
}
|
||||
|
||||
// Update software version.
|
||||
if newStatus.Version != info.Version() {
|
||||
newStatus.Version = info.Version()
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Update keys.
|
||||
keysChanged, err := id.MaintainExchKeys(newStatus, time.Now())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to maintain keys: %w", err)
|
||||
}
|
||||
if keysChanged {
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Update lanes.
|
||||
if lanes != nil && !hub.LanesEqual(newStatus.Lanes, lanes) {
|
||||
newStatus.Lanes = lanes
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Update load.
|
||||
if load != nil && newStatus.Load != *load {
|
||||
newStatus.Load = *load
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Update flags.
|
||||
if !hub.FlagsEqual(newStatus.Flags, flags) {
|
||||
newStatus.Flags = flags
|
||||
changed = true
|
||||
}
|
||||
|
||||
// Update timestamp if something changed.
|
||||
if changed {
|
||||
newStatus.Timestamp = time.Now().Unix()
|
||||
}
|
||||
|
||||
if changed || selfcheck {
|
||||
// Export new data.
|
||||
newStatusData, err := newStatus.Export(id.signingEnvelope())
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to export: %w", err)
|
||||
}
|
||||
|
||||
// Apply the status as all other Hubs would in order to check if it's valid.
|
||||
_, _, _, err = hub.ApplyStatus(id.Hub, newStatusData, conf.MainMapName, conf.MainMapScope, true)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to apply new status: %w", err)
|
||||
}
|
||||
id.statusExportCache = newStatusData
|
||||
|
||||
// Save message to hub message storage.
|
||||
err = hub.SaveHubMsg(id.ID, conf.MainMapName, hub.MsgTypeStatus, newStatusData)
|
||||
if err != nil {
|
||||
log.Warningf("spn/cabin: failed to save own new/updated status: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
// MakeOfflineStatus creates and signs an offline status message.
|
||||
func (id *Identity) MakeOfflineStatus() (offlineStatusExport []byte, err error) {
|
||||
// Make offline status.
|
||||
newStatus := &hub.Status{
|
||||
Timestamp: time.Now().Unix(),
|
||||
Version: info.Version(),
|
||||
Flags: []string{hub.FlagOffline},
|
||||
}
|
||||
|
||||
// Export new data.
|
||||
newStatusData, err := newStatus.Export(id.signingEnvelope())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to export: %w", err)
|
||||
}
|
||||
|
||||
return newStatusData, nil
|
||||
}
|
||||
|
||||
func (id *Identity) signingEnvelope() *jess.Envelope {
|
||||
env := jess.NewUnconfiguredEnvelope()
|
||||
env.SuiteID = jess.SuiteSignV1
|
||||
env.Senders = []*jess.Signet{id.Signet}
|
||||
|
||||
return env
|
||||
}
|
||||
|
||||
// ExportAnnouncement serializes and signs the Announcement.
|
||||
func (id *Identity) ExportAnnouncement() ([]byte, error) {
|
||||
id.Lock()
|
||||
defer id.Unlock()
|
||||
|
||||
if id.infoExportCache == nil {
|
||||
return nil, errors.New("announcement not exported")
|
||||
}
|
||||
|
||||
return id.infoExportCache, nil
|
||||
}
|
||||
|
||||
// ExportStatus serializes and signs the Status.
|
||||
func (id *Identity) ExportStatus() ([]byte, error) {
|
||||
id.Lock()
|
||||
defer id.Unlock()
|
||||
|
||||
if id.statusExportCache == nil {
|
||||
return nil, errors.New("status not exported")
|
||||
}
|
||||
|
||||
return id.statusExportCache, nil
|
||||
}
|
||||
|
||||
// SignHubMsg signs a data blob with the identity's private key.
|
||||
func (id *Identity) SignHubMsg(data []byte) ([]byte, error) {
|
||||
return hub.SignHubMsg(data, id.signingEnvelope(), false)
|
||||
}
|
||||
|
||||
// GetSignet returns the private exchange key with the given ID.
|
||||
func (id *Identity) GetSignet(keyID string, recipient bool) (*jess.Signet, error) {
|
||||
if recipient {
|
||||
return nil, errors.New("cabin.Identity only serves private keys")
|
||||
}
|
||||
|
||||
id.Lock()
|
||||
defer id.Unlock()
|
||||
|
||||
key, ok := id.ExchKeys[keyID]
|
||||
if !ok {
|
||||
return nil, errors.New("the requested key does not exist")
|
||||
}
|
||||
if time.Now().After(key.Expires) || key.key == nil {
|
||||
return nil, errors.New("the requested key has expired")
|
||||
}
|
||||
|
||||
return key.key, nil
|
||||
}
|
||||
|
||||
func (ek *ExchKey) toHubKey() (*hub.Key, error) {
|
||||
if ek.key == nil {
|
||||
return nil, errors.New("no key")
|
||||
}
|
||||
|
||||
// export public key
|
||||
rcpt, err := ek.key.AsRecipient()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = rcpt.StoreKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// repackage
|
||||
return &hub.Key{
|
||||
Scheme: rcpt.Scheme,
|
||||
Key: rcpt.Key,
|
||||
Expires: ek.Expires.Unix(),
|
||||
}, nil
|
||||
}
|
||||
129
spn/cabin/identity_test.go
Normal file
129
spn/cabin/identity_test.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
func TestIdentity(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Register config options for public hub.
|
||||
if err := prepPublicHubConfig(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create new identity.
|
||||
identityTestKey := "core:spn/public/identity"
|
||||
id, err := CreateIdentity(module.Ctx, conf.MainMapName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id.SetKey(identityTestKey)
|
||||
|
||||
// Check values
|
||||
// Identity
|
||||
assert.NotEmpty(t, id.ID, "id.ID must be set")
|
||||
assert.NotEmpty(t, id.Map, "id.Map must be set")
|
||||
assert.NotNil(t, id.Signet, "id.Signet must be set")
|
||||
assert.NotNil(t, id.infoExportCache, "id.infoExportCache must be set")
|
||||
assert.NotNil(t, id.statusExportCache, "id.statusExportCache must be set")
|
||||
// Hub
|
||||
assert.NotEmpty(t, id.Hub.ID, "hub.ID must be set")
|
||||
assert.NotEmpty(t, id.Hub.Map, "hub.Map must be set")
|
||||
assert.NotZero(t, id.Hub.FirstSeen, "hub.FirstSeen must be set")
|
||||
// Info
|
||||
assert.NotEmpty(t, id.Hub.Info.ID, "info.ID must be set")
|
||||
assert.NotEqual(t, 0, id.Hub.Info.Timestamp, "info.Timestamp must be set")
|
||||
assert.NotEqual(t, "", id.Hub.Info.Name, "info.Name must be set (to hostname)")
|
||||
// Status
|
||||
assert.NotEqual(t, 0, id.Hub.Status.Timestamp, "status.Timestamp must be set")
|
||||
assert.NotEmpty(t, id.Hub.Status.Keys, "status.Keys must be set")
|
||||
|
||||
fmt.Printf("id: %+v\n", id)
|
||||
fmt.Printf("id.hub: %+v\n", id.Hub)
|
||||
fmt.Printf("id.Hub.Info: %+v\n", id.Hub.Info)
|
||||
fmt.Printf("id.Hub.Status: %+v\n", id.Hub.Status)
|
||||
|
||||
// Maintenance is run in creation, so nothing should change now.
|
||||
changed, err := id.MaintainAnnouncement(nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changed {
|
||||
t.Error("unexpected change of announcement")
|
||||
}
|
||||
changed, err = id.MaintainStatus(nil, nil, nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changed {
|
||||
t.Error("unexpected change of status")
|
||||
}
|
||||
|
||||
// Change lanes.
|
||||
lanes := []*hub.Lane{
|
||||
{
|
||||
ID: "A",
|
||||
Capacity: 1,
|
||||
Latency: 2,
|
||||
},
|
||||
{
|
||||
ID: "B",
|
||||
Capacity: 3,
|
||||
Latency: 4,
|
||||
},
|
||||
{
|
||||
ID: "C",
|
||||
Capacity: 5,
|
||||
Latency: 6,
|
||||
},
|
||||
}
|
||||
changed, err = id.MaintainStatus(lanes, new(int), nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !changed {
|
||||
t.Error("status should have changed")
|
||||
}
|
||||
|
||||
// Change nothing.
|
||||
changed, err = id.MaintainStatus(lanes, new(int), nil, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changed {
|
||||
t.Error("unexpected change of status")
|
||||
}
|
||||
|
||||
// Exporting
|
||||
_, err = id.ExportAnnouncement()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, err = id.ExportStatus()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Save to and load from database.
|
||||
err = id.Save()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, changed, err := LoadIdentity(identityTestKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changed {
|
||||
t.Error("unexpected change")
|
||||
}
|
||||
|
||||
// Check if they match
|
||||
assert.Equal(t, id, id2, "identities should be equal")
|
||||
}
|
||||
179
spn/cabin/keys.go
Normal file
179
spn/cabin/keys.go
Normal file
@@ -0,0 +1,179 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/safing/jess"
|
||||
"github.com/safing/jess/tools"
|
||||
"github.com/safing/portbase/log"
|
||||
"github.com/safing/portbase/rng"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
type providedExchKeyScheme struct {
|
||||
id string
|
||||
securityLevel int //nolint:structcheck // TODO
|
||||
tool *tools.Tool
|
||||
}
|
||||
|
||||
var (
|
||||
// validFor defines how long keys are valid for use by clients.
|
||||
validFor = 48 * time.Hour // 2 days
|
||||
// renewBeforeExpiry defines the duration how long before expiry keys should be renewed.
|
||||
renewBeforeExpiry = 24 * time.Hour // 1 day
|
||||
|
||||
// burnAfter defines how long after expiry keys are burnt/deleted.
|
||||
burnAfter = 12 * time.Hour // 1/2 day
|
||||
// reuseAfter defines how long IDs should be blocked after expiry (and not be reused for new keys).
|
||||
reuseAfter = 2 * 7 * 24 * time.Hour // 2 weeks
|
||||
|
||||
// provideExchKeySchemes defines the jess tools for creating exchange keys.
|
||||
provideExchKeySchemes = []*providedExchKeyScheme{
|
||||
{
|
||||
id: "ECDH-X25519",
|
||||
securityLevel: 128, // informative only, security level of ECDH-X25519 is fixed
|
||||
},
|
||||
// TODO: test with rsa keys
|
||||
}
|
||||
)
|
||||
|
||||
func initProvidedExchKeySchemes() error {
|
||||
for _, eks := range provideExchKeySchemes {
|
||||
tool, err := tools.Get(eks.id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
eks.tool = tool
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaintainExchKeys maintains the exchange keys, creating new ones and
|
||||
// deprecating and deleting old ones.
|
||||
func (id *Identity) MaintainExchKeys(newStatus *hub.Status, now time.Time) (changed bool, err error) {
|
||||
// create Keys map
|
||||
if id.ExchKeys == nil {
|
||||
id.ExchKeys = make(map[string]*ExchKey)
|
||||
}
|
||||
|
||||
// lifecycle management
|
||||
for keyID, exchKey := range id.ExchKeys {
|
||||
if exchKey.key != nil && now.After(exchKey.Expires.Add(burnAfter)) {
|
||||
// delete key
|
||||
err := exchKey.tool.StaticLogic.BurnKey(exchKey.key)
|
||||
if err != nil {
|
||||
log.Warningf(
|
||||
"spn/cabin: failed to burn key %s (%s) of %s: %s",
|
||||
keyID,
|
||||
exchKey.tool.Info.Name,
|
||||
id.Hub.ID,
|
||||
err,
|
||||
)
|
||||
}
|
||||
// remove reference
|
||||
exchKey.key = nil
|
||||
}
|
||||
if now.After(exchKey.Expires.Add(reuseAfter)) {
|
||||
// remove key
|
||||
delete(id.ExchKeys, keyID)
|
||||
}
|
||||
}
|
||||
|
||||
// find or create current keys
|
||||
for _, eks := range provideExchKeySchemes {
|
||||
found := false
|
||||
for _, exchKey := range id.ExchKeys {
|
||||
if exchKey.key != nil &&
|
||||
exchKey.key.Scheme == eks.id &&
|
||||
now.Before(exchKey.Expires.Add(-renewBeforeExpiry)) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
err := id.createExchKey(eks, now)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to create %s exchange key: %w", eks.tool.Info.Name, err)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
||||
// export most recent keys to HubStatus
|
||||
if changed || len(newStatus.Keys) == 0 {
|
||||
// reset
|
||||
newStatus.Keys = make(map[string]*hub.Key)
|
||||
|
||||
// find longest valid key for every provided scheme
|
||||
for _, eks := range provideExchKeySchemes {
|
||||
// find key of scheme that is valid the longest
|
||||
longestValid := &ExchKey{
|
||||
Expires: now,
|
||||
}
|
||||
for _, exchKey := range id.ExchKeys {
|
||||
if exchKey.key != nil &&
|
||||
exchKey.key.Scheme == eks.id &&
|
||||
exchKey.Expires.After(longestValid.Expires) {
|
||||
longestValid = exchKey
|
||||
}
|
||||
}
|
||||
|
||||
// check result
|
||||
if longestValid.key == nil {
|
||||
log.Warningf("spn/cabin: could not find export candidate for exchange key scheme %s", eks.id)
|
||||
continue
|
||||
}
|
||||
|
||||
// export
|
||||
hubKey, err := longestValid.toHubKey()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to export %s exchange key: %w", longestValid.tool.Info.Name, err)
|
||||
}
|
||||
// add
|
||||
newStatus.Keys[longestValid.key.ID] = hubKey
|
||||
}
|
||||
}
|
||||
|
||||
return changed, nil
|
||||
}
|
||||
|
||||
func (id *Identity) createExchKey(eks *providedExchKeyScheme, now time.Time) error {
|
||||
// get ID
|
||||
var keyID string
|
||||
for i := 0; i < 1000000; i++ { // not forever
|
||||
// generate new ID
|
||||
b, err := rng.Bytes(3)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get random data for key ID: %w", err)
|
||||
}
|
||||
keyID = base64.RawURLEncoding.EncodeToString(b)
|
||||
_, exists := id.ExchKeys[keyID]
|
||||
if !exists {
|
||||
break
|
||||
}
|
||||
}
|
||||
if keyID == "" {
|
||||
return errors.New("unable to find available exchange key ID")
|
||||
}
|
||||
|
||||
// generate key
|
||||
signet := jess.NewSignetBase(eks.tool)
|
||||
signet.ID = keyID
|
||||
// TODO: use security level for key generation
|
||||
if err := signet.GenerateKey(); err != nil {
|
||||
return fmt.Errorf("failed to get new exchange key: %w", err)
|
||||
}
|
||||
|
||||
// add to key map
|
||||
id.ExchKeys[keyID] = &ExchKey{
|
||||
Created: now,
|
||||
Expires: now.Add(validFor),
|
||||
key: signet,
|
||||
tool: eks.tool,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
43
spn/cabin/keys_test.go
Normal file
43
spn/cabin/keys_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
)
|
||||
|
||||
func TestKeyMaintenance(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
id, err := CreateIdentity(module.Ctx, conf.MainMapName)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
iterations := 1000
|
||||
changeCnt := 0
|
||||
|
||||
now := time.Now()
|
||||
for i := 0; i < iterations; i++ {
|
||||
changed, err := id.MaintainExchKeys(id.Hub.Status, now)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if changed {
|
||||
changeCnt++
|
||||
t.Logf("===== exchange keys updated at %s:\n", now)
|
||||
for keyID, exchKey := range id.ExchKeys {
|
||||
t.Logf("[%s] %s %v\n", exchKey.Created, keyID, exchKey.key)
|
||||
}
|
||||
}
|
||||
now = now.Add(1 * time.Hour)
|
||||
}
|
||||
|
||||
if iterations/changeCnt > 25 { // one new key every 24 hours/ticks
|
||||
t.Fatal("more changes than expected")
|
||||
}
|
||||
if len(id.ExchKeys) > 17 { // one new key every day for two weeks + 3 in use
|
||||
t.Fatal("more keys than expected")
|
||||
}
|
||||
}
|
||||
26
spn/cabin/module.go
Normal file
26
spn/cabin/module.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"github.com/safing/portbase/modules"
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
)
|
||||
|
||||
var module *modules.Module
|
||||
|
||||
func init() {
|
||||
module = modules.Register("cabin", prep, nil, nil, "base", "rng")
|
||||
}
|
||||
|
||||
func prep() error {
|
||||
if err := initProvidedExchKeySchemes(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conf.PublicHub() {
|
||||
if err := prepPublicHubConfig(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
13
spn/cabin/module_test.go
Normal file
13
spn/cabin/module_test.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/safing/portmaster/service/core/pmtesting"
|
||||
"github.com/safing/portmaster/spn/conf"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
conf.EnablePublicHub(true)
|
||||
pmtesting.TestMain(m, module)
|
||||
}
|
||||
157
spn/cabin/verification.go
Normal file
157
spn/cabin/verification.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"crypto/subtle"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/safing/jess"
|
||||
"github.com/safing/portbase/formats/dsd"
|
||||
"github.com/safing/portbase/rng"
|
||||
"github.com/safing/portmaster/spn/hub"
|
||||
)
|
||||
|
||||
var (
|
||||
verificationChallengeSize = 32
|
||||
verificationChallengeMinSize = 16
|
||||
verificationSigningSuite = jess.SuiteSignV1
|
||||
verificationRequirements = jess.NewRequirements().
|
||||
Remove(jess.Confidentiality).
|
||||
Remove(jess.Integrity).
|
||||
Remove(jess.RecipientAuthentication)
|
||||
)
|
||||
|
||||
// Verification is used to verify certain aspects of another Hub.
|
||||
type Verification struct {
|
||||
// Challenge is a random value chosen by the client.
|
||||
Challenge []byte `json:"c"`
|
||||
// Purpose defines the purpose of the verification. Protects against using verification for other purposes.
|
||||
Purpose string `json:"p"`
|
||||
// ClientReference is an optional field for exchanging metadata about the client. Protects against forwarding/relay attacks.
|
||||
ClientReference string `json:"cr"`
|
||||
// ServerReference is an optional field for exchanging metadata about the server. Protects against forwarding/relay attacks.
|
||||
ServerReference string `json:"sr"`
|
||||
}
|
||||
|
||||
// CreateVerificationRequest creates a new verification request with the given
|
||||
// purpose and references.
|
||||
func CreateVerificationRequest(purpose, clientReference, serverReference string) (v *Verification, request []byte, err error) {
|
||||
// Generate random challenge.
|
||||
challenge, err := rng.Bytes(verificationChallengeSize)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to generate challenge: %w", err)
|
||||
}
|
||||
|
||||
// Create verification object.
|
||||
v = &Verification{
|
||||
Purpose: purpose,
|
||||
ClientReference: clientReference,
|
||||
Challenge: challenge,
|
||||
}
|
||||
|
||||
// Serialize verification.
|
||||
request, err = dsd.Dump(v, dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to serialize verification request: %w", err)
|
||||
}
|
||||
|
||||
// The server reference is not sent to the server, but needs to be supplied
|
||||
// by the server.
|
||||
v.ServerReference = serverReference
|
||||
|
||||
return v, request, nil
|
||||
}
|
||||
|
||||
// SignVerificationRequest sign a verification request.
|
||||
// The purpose and references must match the request, else the verification
|
||||
// will fail.
|
||||
func (id *Identity) SignVerificationRequest(request []byte, purpose, clientReference, serverReference string) (response []byte, err error) {
|
||||
// Parse request.
|
||||
v := new(Verification)
|
||||
_, err = dsd.Load(request, v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse request: %w", err)
|
||||
}
|
||||
|
||||
// Validate request.
|
||||
if len(v.Challenge) < verificationChallengeMinSize {
|
||||
return nil, errors.New("challenge too small")
|
||||
}
|
||||
if v.Purpose != purpose {
|
||||
return nil, errors.New("purpose mismatch")
|
||||
}
|
||||
if v.ClientReference != clientReference {
|
||||
return nil, errors.New("client reference mismatch")
|
||||
}
|
||||
|
||||
// Assign server reference and serialize.
|
||||
v.ServerReference = serverReference
|
||||
dataToSign, err := dsd.Dump(v, dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize verification response: %w", err)
|
||||
}
|
||||
|
||||
// Sign response.
|
||||
e := jess.NewUnconfiguredEnvelope()
|
||||
e.SuiteID = verificationSigningSuite
|
||||
e.Senders = []*jess.Signet{id.Signet}
|
||||
jession, err := e.Correspondence(nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to setup signer: %w", err)
|
||||
}
|
||||
letter, err := jession.Close(dataToSign)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign: %w", err)
|
||||
}
|
||||
|
||||
// Serialize and return.
|
||||
signedResponse, err := letter.ToDSD(dsd.JSON)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to serialize letter: %w", err)
|
||||
}
|
||||
|
||||
return signedResponse, nil
|
||||
}
|
||||
|
||||
// Verify verifies the verification response and checks if everything is valid.
|
||||
func (v *Verification) Verify(response []byte, h *hub.Hub) error {
|
||||
// Parse response.
|
||||
letter, err := jess.LetterFromDSD(response)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
// Verify response.
|
||||
responseData, err := letter.Open(
|
||||
verificationRequirements,
|
||||
&hub.SingleTrustStore{
|
||||
Signet: h.PublicKey,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify response: %w", err)
|
||||
}
|
||||
|
||||
// Parse verified response.
|
||||
responseV := new(Verification)
|
||||
_, err = dsd.Load(responseData, responseV)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse verified response: %w", err)
|
||||
}
|
||||
|
||||
// Validate request.
|
||||
if subtle.ConstantTimeCompare(v.Challenge, responseV.Challenge) != 1 {
|
||||
return errors.New("challenge mismatch")
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(v.Purpose), []byte(responseV.Purpose)) != 1 {
|
||||
return errors.New("purpose mismatch")
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(v.ClientReference), []byte(responseV.ClientReference)) != 1 {
|
||||
return errors.New("client reference mismatch")
|
||||
}
|
||||
if subtle.ConstantTimeCompare([]byte(v.ServerReference), []byte(responseV.ServerReference)) != 1 {
|
||||
return errors.New("server reference mismatch")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
127
spn/cabin/verification_test.go
Normal file
127
spn/cabin/verification_test.go
Normal file
@@ -0,0 +1,127 @@
|
||||
package cabin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestVerification(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
id, err := CreateIdentity(module.Ctx, "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := testVerificationWith(
|
||||
t, id,
|
||||
"a", "b", "c",
|
||||
"a", "b", "c",
|
||||
"", "", "", nil,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := testVerificationWith(
|
||||
t, id,
|
||||
"a", "b", "c",
|
||||
"x", "b", "c",
|
||||
"", "", "", nil,
|
||||
); err == nil {
|
||||
t.Fatal("should fail on purpose mismatch")
|
||||
}
|
||||
|
||||
if err := testVerificationWith(
|
||||
t, id,
|
||||
"a", "b", "c",
|
||||
"a", "x", "c",
|
||||
"", "", "", nil,
|
||||
); err == nil {
|
||||
t.Fatal("should fail on client ref mismatch")
|
||||
}
|
||||
|
||||
if err := testVerificationWith(
|
||||
t, id,
|
||||
"a", "b", "c",
|
||||
"a", "b", "x",
|
||||
"", "", "", nil,
|
||||
); err == nil {
|
||||
t.Fatal("should fail on server ref mismatch")
|
||||
}
|
||||
|
||||
if err := testVerificationWith(
|
||||
t, id,
|
||||
"a", "b", "c",
|
||||
"a", "b", "c",
|
||||
"x", "", "", nil,
|
||||
); err == nil {
|
||||
t.Fatal("should fail on purpose mismatch")
|
||||
}
|
||||
|
||||
if err := testVerificationWith(
|
||||
t, id,
|
||||
"a", "b", "c",
|
||||
"a", "b", "c",
|
||||
"", "x", "", nil,
|
||||
); err == nil {
|
||||
t.Fatal("should fail on client ref mismatch")
|
||||
}
|
||||
|
||||
if err := testVerificationWith(
|
||||
t, id,
|
||||
"a", "b", "c",
|
||||
"a", "b", "c",
|
||||
"", "", "x", nil,
|
||||
); err == nil {
|
||||
t.Fatal("should fail on server ref mismatch")
|
||||
}
|
||||
|
||||
if err := testVerificationWith(
|
||||
t, id,
|
||||
"a", "b", "c",
|
||||
"a", "b", "c",
|
||||
"", "", "", []byte{1, 2, 3, 4},
|
||||
); err == nil {
|
||||
t.Fatal("should fail on challenge mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
func testVerificationWith(
|
||||
t *testing.T, id *Identity,
|
||||
purpose1, clientRef1, serverRef1 string, //nolint:unparam
|
||||
purpose2, clientRef2, serverRef2 string,
|
||||
mitmPurpose, mitmClientRef, mitmServerRef string,
|
||||
mitmChallenge []byte,
|
||||
) error {
|
||||
t.Helper()
|
||||
|
||||
v, request, err := CreateVerificationRequest(purpose1, clientRef1, serverRef1)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create verification request: %w", err)
|
||||
}
|
||||
|
||||
response, err := id.SignVerificationRequest(request, purpose2, clientRef2, serverRef2)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to sign verification response: %w", err)
|
||||
}
|
||||
|
||||
if mitmPurpose != "" {
|
||||
v.Purpose = mitmPurpose
|
||||
}
|
||||
if mitmClientRef != "" {
|
||||
v.ClientReference = mitmClientRef
|
||||
}
|
||||
if mitmServerRef != "" {
|
||||
v.ServerReference = mitmServerRef
|
||||
}
|
||||
if mitmChallenge != nil {
|
||||
v.Challenge = mitmChallenge
|
||||
}
|
||||
|
||||
err = v.Verify(response, id.Hub)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to verify: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user