wip: migrate to mono-repo. SPN has already been moved to spn/

This commit is contained in:
Patrick Pacher
2024-03-15 11:55:13 +01:00
parent b30fd00ccf
commit 8579430db9
577 changed files with 35981 additions and 818 deletions

392
spn/cabin/config-public.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
}

View 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
}