2
0
Эх сурвалжийг харах

:gear: Add wip/experimental peerguard and peergater

Peerguard and peergater are two components that work together to gate
peers and add them to a trusted zone.

This allows to isolate nodes from the p2p network and avoid to rotate
network tokens in case of leaks.

For the moment an ECDSA auth provider is implemented as sample purpose,
documentation will follow up on how to use them and how to write them
up.
mudler 3 жил өмнө
parent
commit
8826daf815

+ 19 - 0
api/api.go

@@ -66,6 +66,7 @@ const (
 	DNSURL        = "/api/dns"
 	MetricsURL    = "/api/metrics"
 	PeerstoreURL  = "/api/peerstore"
+	PeerGateURL   = "/api/peergate"
 )
 
 func API(ctx context.Context, l string, defaultInterval, timeout time.Duration, e *node.Node, bwc metrics.Reporter, debugMode bool) error {
@@ -115,6 +116,24 @@ func API(ctx context.Context, l string, defaultInterval, timeout time.Duration,
 		return c.JSON(http.StatusOK, list)
 	})
 
+	if e.PeerGater() != nil {
+		ec.PUT(fmt.Sprintf("%s/:state", PeerGateURL), func(c echo.Context) error {
+			state := c.Param("state")
+
+			switch state {
+			case "enable":
+				e.PeerGater().Enable()
+			case "disable":
+				e.PeerGater().Disable()
+			}
+			return c.JSON(http.StatusOK, e.PeerGater().Enabled())
+		})
+
+		ec.GET(PeerGateURL, func(c echo.Context) error {
+			return c.JSON(http.StatusOK, e.PeerGater().Enabled())
+		})
+	}
+
 	ec.GET(SummaryURL, func(c echo.Context) error {
 		files := len(ledger.CurrentData()[protocol.FilesLedgerKey])
 		machines := len(ledger.CurrentData()[protocol.MachinesLedgerKey])

+ 56 - 0
cmd/peergate.go

@@ -0,0 +1,56 @@
+// Copyright © 2021 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package cmd
+
+import (
+	"fmt"
+
+	"github.com/mudler/edgevpn/pkg/trustzone/authprovider/ecdsa"
+	"github.com/urfave/cli"
+)
+
+func Peergate() cli.Command {
+	return cli.Command{
+		Name:        "peergater",
+		Usage:       "peergater ecdsa-genkey",
+		Description: `Peergater auth utilities`,
+		Subcommands: cli.Commands{
+			{
+				Name: "ecdsa-genkey",
+				Flags: []cli.Flag{
+					&cli.BoolFlag{
+						Name: "privkey",
+					},
+					&cli.BoolFlag{
+						Name: "pubkey",
+					},
+				},
+				Action: func(c *cli.Context) error {
+					priv, pub, err := ecdsa.GenerateKeys()
+					if !c.Bool("privkey") && !c.Bool("pubkey") {
+						fmt.Printf("Private key: %s\n", string(priv))
+						fmt.Printf("Public key: %s\n", string(pub))
+					} else if c.Bool("privkey") {
+						fmt.Printf(string(priv))
+					} else if c.Bool("pubkey") {
+						fmt.Printf(string(pub))
+					}
+					return err
+				},
+			},
+		},
+	}
+}

+ 46 - 0
cmd/util.go

@@ -16,6 +16,7 @@
 package cmd
 
 import (
+	"encoding/json"
 	"runtime"
 	"time"
 
@@ -308,6 +309,38 @@ var CommonFlags []cli.Flag = []cli.Flag{
 		EnvVar: "LIMITCONFIGFD",
 		Value:  30,
 	},
+	&cli.BoolFlag{
+		Name:   "peerguard",
+		Usage:  "Enable peerguard. (Experimental)",
+		EnvVar: "PEERGUARD",
+	},
+	&cli.BoolFlag{
+		Name:   "peergate",
+		Usage:  "Enable peergating. (Experimental)",
+		EnvVar: "PEERGATE",
+	},
+	&cli.BoolFlag{
+		Name:   "peergate-autoclean",
+		Usage:  "Enable peergating autoclean. (Experimental)",
+		EnvVar: "PEERGATE_AUTOCLEAN",
+	},
+	&cli.BoolFlag{
+		Name:   "peergate-relaxed",
+		Usage:  "Enable peergating relaxation. (Experimental)",
+		EnvVar: "PEERGATE_RELAXED",
+	},
+	&cli.StringFlag{
+		Name:   "peergate-auth",
+		Usage:  "Peergate auth",
+		EnvVar: "PEERGATE_AUTH",
+		Value:  "",
+	},
+	&cli.IntFlag{
+		Name:   "peergate-interval",
+		Usage:  "Peergater interval time",
+		EnvVar: "EDGEVPNPEERGATEINTERVAL",
+		Value:  120,
+	},
 }
 
 func displayStart(ll *logger.Logger) {
@@ -342,6 +375,11 @@ func cliToOpts(c *cli.Context) ([]node.Option, []vpn.Option, *logger.Logger) {
 		}
 	}
 
+	// Authproviders are supposed to be passed as a json object
+	pa := c.String("peergate-auth")
+	d := map[string]map[string]interface{}{}
+	json.Unmarshal([]byte(pa), &d)
+
 	nc := nodeConfig.Config{
 		NetworkConfig:     c.String("config"),
 		NetworkToken:      c.String("token"),
@@ -395,6 +433,14 @@ func cliToOpts(c *cli.Context) ([]node.Option, []vpn.Option, *logger.Logger) {
 			MaxConns:    c.Int("max-connections"), // Turn to 0 to use other way of limiting. Files take precedence
 			LimitConfig: limitConfig,
 		},
+		PeerGuard: config.PeerGuard{
+			Enable:        c.Bool("peerguard"),
+			PeerGate:      c.Bool("peergate"),
+			Relaxed:       c.Bool("peergate-relaxed"),
+			Autocleanup:   c.Bool("peergate-autoclean"),
+			SyncInterval:  time.Duration(c.Int("peergate-interval")) * time.Second,
+			AuthProviders: d,
+		},
 	}
 
 	lvl, err := log.LevelFromString(nc.LogLevel)

+ 1 - 0
main.go

@@ -45,6 +45,7 @@ func main() {
 			cmd.Proxy(),
 			cmd.FileSend(),
 			cmd.DNS(),
+			cmd.Peergate(),
 		},
 
 		Action: cmd.Main(),

+ 62 - 0
pkg/config/config.go

@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"math/bits"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/ipfs/go-log"
@@ -37,6 +38,8 @@ import (
 	"github.com/mudler/edgevpn/pkg/logger"
 	"github.com/mudler/edgevpn/pkg/node"
 	"github.com/mudler/edgevpn/pkg/services"
+	"github.com/mudler/edgevpn/pkg/trustzone"
+	"github.com/mudler/edgevpn/pkg/trustzone/authprovider/ecdsa"
 	"github.com/mudler/edgevpn/pkg/vpn"
 	"github.com/peterbourgon/diskv"
 	"github.com/songgao/water"
@@ -60,6 +63,21 @@ type Config struct {
 	Discovery                                  Discovery
 	Ledger                                     Ledger
 	Limit                                      ResourceLimit
+	// PeerGuard (experimental)
+	// enable peerguardian and add specific auth options
+	PeerGuard PeerGuard
+}
+
+type PeerGuard struct {
+	Enable      bool
+	Relaxed     bool
+	Autocleanup bool
+	PeerGate    bool
+	// AuthProviders in the freemap form:
+	// ecdsa:
+	//   private_key: "foo_bar"
+	AuthProviders map[string]map[string]interface{}
+	SyncInterval  time.Duration
 }
 
 type ResourceLimit struct {
@@ -374,9 +392,53 @@ func (c Config) ToOpts(l *logger.Logger) ([]node.Option, []vpn.Option, error) {
 		opts = append(opts, node.WithStore(&blockchain.MemoryStore{}))
 	}
 
+	if c.PeerGuard.Enable {
+		pg := trustzone.NewPeerGater(c.PeerGuard.Relaxed)
+		dur := c.PeerGuard.SyncInterval
+
+		// Build up the authproviders for the peerguardian
+		aps := []trustzone.AuthProvider{}
+		for ap, providerOpts := range c.PeerGuard.AuthProviders {
+			a, err := authProvider(llger, ap, providerOpts)
+			if err != nil {
+				return opts, vpnOpts, fmt.Errorf("invalid authprovider: %w", err)
+			}
+			aps = append(aps, a)
+		}
+
+		pguardian := trustzone.NewPeerGuardian(llger, aps...)
+
+		opts = append(opts,
+			node.WithNetworkService(
+				pg.UpdaterService(dur),
+				pguardian.Challenger(dur, c.PeerGuard.Autocleanup),
+			),
+			node.EnableGenericHub,
+			node.GenericChannelHandlers(pguardian.ReceiveMessage),
+		)
+		// We always pass a PeerGater such will be registered to the API if necessary
+		opts = append(opts, node.WithPeerGater(pg))
+		// IF it's not enabled, we just disable it right away.
+		if !c.PeerGuard.PeerGate {
+			pg.Disable()
+		}
+	}
+
 	return opts, vpnOpts, nil
 }
 
+func authProvider(ll log.StandardLogger, s string, opts map[string]interface{}) (trustzone.AuthProvider, error) {
+	switch strings.ToLower(s) {
+	case "ecdsa":
+		pk, exists := opts["private_key"]
+		if !exists {
+			return nil, fmt.Errorf("No private key provided")
+		}
+		return ecdsa.ECDSA521Provider(ll, fmt.Sprint(pk))
+	}
+	return nil, fmt.Errorf("not supported")
+}
+
 func logScale(val int) int {
 	bitlen := bits.Len(uint(val))
 	return 1 << bitlen

+ 41 - 16
pkg/hub/hub.go

@@ -33,26 +33,31 @@ import (
 type MessageHub struct {
 	sync.Mutex
 
-	r         *room
-	otpKey    string
-	maxsize   int
-	keyLength int
-	interval  int
-
-	ctxCancel context.CancelFunc
-	Messages  chan *Message
+	blockchain, public *room
+	ps                 *pubsub.PubSub
+	otpKey             string
+	maxsize            int
+	keyLength          int
+	interval           int
+	joinPublic         bool
+
+	ctxCancel                context.CancelFunc
+	Messages, PublicMessages chan *Message
 }
 
 // roomBufSize is the number of incoming messages to buffer for each topic.
 const roomBufSize = 128
 
-func NewHub(otp string, maxsize, keyLength, interval int) *MessageHub {
+func NewHub(otp string, maxsize, keyLength, interval int, joinPublic bool) *MessageHub {
 	return &MessageHub{otpKey: otp, maxsize: maxsize, keyLength: keyLength, interval: interval,
-		Messages: make(chan *Message, roomBufSize)}
+		Messages: make(chan *Message, roomBufSize), PublicMessages: make(chan *Message, roomBufSize), joinPublic: joinPublic}
 }
 
-func (m *MessageHub) topicKey() string {
+func (m *MessageHub) topicKey(salts ...string) string {
 	totp := gotp.NewTOTP(strings.ToUpper(m.otpKey), m.keyLength, m.interval, nil)
+	if len(salts) > 0 {
+		return crypto.MD5(totp.Now() + strings.Join(salts, ":"))
+	}
 	return crypto.MD5(totp.Now())
 }
 
@@ -78,7 +83,18 @@ func (m *MessageHub) joinRoom(host host.Host) error {
 	if err != nil {
 		return err
 	}
-	m.r = cr
+
+	m.blockchain = cr
+
+	if m.joinPublic {
+		cr2, err := connect(ctx, ps, host.ID(), m.topicKey("public"), m.PublicMessages)
+		if err != nil {
+			return err
+		}
+		m.public = cr2
+	}
+
+	m.ps = ps
 
 	return nil
 }
@@ -117,8 +133,17 @@ func (m *MessageHub) Start(ctx context.Context, host host.Host) error {
 func (m *MessageHub) PublishMessage(mess *Message) error {
 	m.Lock()
 	defer m.Unlock()
-	if m.r != nil {
-		return m.r.publishMessage(mess)
+	if m.blockchain != nil {
+		return m.blockchain.publishMessage(mess)
+	}
+	return errors.New("no message room available")
+}
+
+func (m *MessageHub) PublishPublicMessage(mess *Message) error {
+	m.Lock()
+	defer m.Unlock()
+	if m.public != nil {
+		return m.public.publishMessage(mess)
 	}
 	return errors.New("no message room available")
 }
@@ -126,8 +151,8 @@ func (m *MessageHub) PublishMessage(mess *Message) error {
 func (m *MessageHub) ListPeers() ([]peer.ID, error) {
 	m.Lock()
 	defer m.Unlock()
-	if m.r != nil {
-		return m.r.Topic.ListPeers(), nil
+	if m.blockchain != nil {
+		return m.blockchain.Topic.ListPeers(), nil
 	}
 	return nil, errors.New("no message room available")
 }

+ 3 - 0
pkg/node/config.go

@@ -79,6 +79,9 @@ type Config struct {
 
 type Gater interface {
 	Gate(*Node, peer.ID) bool
+	Enable()
+	Disable()
+	Enabled() bool
 }
 
 type Sealer interface {

+ 3 - 3
pkg/node/connection.go

@@ -120,7 +120,7 @@ func (e *Node) sealkey() string {
 	return internalCrypto.MD5(gotp.NewTOTP(e.config.ExchangeKey, e.config.SealKeyLength, e.config.SealKeyInterval, nil).Now())
 }
 
-func (e *Node) handleEvents(ctx context.Context, inputChannel chan *hub.Message, h *hub.MessageHub, handlers []Handler, peerGater bool) {
+func (e *Node) handleEvents(ctx context.Context, inputChannel chan *hub.Message, roomMessages chan *hub.Message, pub func(*hub.Message) error, handlers []Handler, peerGater bool) {
 	for {
 		select {
 		case m := <-inputChannel:
@@ -134,11 +134,11 @@ func (e *Node) handleEvents(ctx context.Context, inputChannel chan *hub.Message,
 			}
 			c.Message = str
 
-			if err := h.PublishMessage(c); err != nil {
+			if err := pub(c); err != nil {
 				e.config.Logger.Warnf("publish error: %s", err)
 			}
 
-		case m := <-h.Messages:
+		case m := <-roomMessages:
 			if m == nil {
 				continue
 			}

+ 8 - 6
pkg/node/node.go

@@ -38,7 +38,6 @@ import (
 type Node struct {
 	config     Config
 	MessageHub *hub.MessageHub
-	GenericHub *hub.MessageHub
 
 	//HubRoom *hub.Room
 	inputCh      chan *hub.Message
@@ -102,6 +101,11 @@ func (e *Node) Ledger() (*blockchain.Ledger, error) {
 	return e.ledger, nil
 }
 
+// PeerGater returns the node peergater
+func (e *Node) PeerGater() Gater {
+	return e.config.PeerGater
+}
+
 // Start joins the node over the p2p network
 func (e *Node) Start(ctx context.Context) error {
 
@@ -174,7 +178,7 @@ func (e *Node) startNetwork(ctx context.Context) error {
 	// Hub rotates within sealkey interval.
 	// this time length should be enough to make room for few block exchanges. This is ideally on minutes (10, 20, etc. )
 	// it makes sure that if a bruteforce is attempted over the encrypted messages, the real key is not exposed.
-	e.MessageHub = hub.NewHub(e.config.RoomName, e.config.MaxMessageSize, e.config.SealKeyLength, e.config.SealKeyInterval)
+	e.MessageHub = hub.NewHub(e.config.RoomName, e.config.MaxMessageSize, e.config.SealKeyLength, e.config.SealKeyInterval, e.config.GenericHub)
 
 	for _, sd := range e.config.ServiceDiscovery {
 		if err := sd.Run(e.config.Logger, ctx, host); err != nil {
@@ -182,15 +186,13 @@ func (e *Node) startNetwork(ctx context.Context) error {
 		}
 	}
 
-	go e.handleEvents(ctx, e.inputCh, e.MessageHub, e.config.Handlers, true)
+	go e.handleEvents(ctx, e.inputCh, e.MessageHub.Messages, e.MessageHub.PublishMessage, e.config.Handlers, true)
 	go e.MessageHub.Start(ctx, host)
 
 	// If generic hub is enabled one is created separately with a set of generic channel handlers associated with.
 	// note peergating is disabled in order to freely exchange messages that can be used for authentication or for other public means.
 	if e.config.GenericHub {
-		e.GenericHub = hub.NewHub(fmt.Sprintf("%s-generic", e.config.RoomName), e.config.MaxMessageSize, e.config.SealKeyLength, e.config.SealKeyInterval)
-		go e.handleEvents(ctx, e.genericHubCh, e.GenericHub, e.config.GenericChannelHandler, false)
-		go e.GenericHub.Start(ctx, host)
+		go e.handleEvents(ctx, e.genericHubCh, e.MessageHub.PublicMessages, e.MessageHub.PublishPublicMessage, e.config.GenericChannelHandler, false)
 	}
 
 	e.config.Logger.Debug("Network started")

+ 2 - 0
pkg/protocol/protocol.go

@@ -34,6 +34,8 @@ const (
 	HealthCheckKey    = "healthcheck"
 	DNSKey            = "dns"
 	EgressService     = "egress"
+	TrustZoneKey      = "trustzone"
+	TrustZoneAuthKey  = "trustzoneAuth"
 )
 
 type Protocol string

+ 216 - 0
pkg/trustzone/authprovider/ecdsa/crypto.go

@@ -0,0 +1,216 @@
+// Copyright (C) 2015 The Syncthing Authors.
+// Copyright (C) 2022 Ettore Di Giacinto
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this file,
+// You can obtain one at https://mozilla.org/MPL/2.0/.
+
+// Package signature provides simple methods to create and verify signatures
+// in PEM format.
+// Extracted https://github.com/syncthing/syncthing/blob/main/lib/signature/signature.go and adapted to encode directly into base64
+
+package ecdsa
+
+import (
+	"crypto/ecdsa"
+	"crypto/elliptic"
+	"crypto/rand"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/asn1"
+	"encoding/base64"
+	"encoding/pem"
+	"errors"
+	"fmt"
+	"io"
+	"math/big"
+)
+
+// GenerateKeys returns a new key pair, with the private and public key
+// encoded in PEM format.
+func GenerateKeys() (privKey []byte, pubKey []byte, err error) {
+	// Generate a new key pair
+	key, err := ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Marshal the private key
+	bs, err := x509.MarshalECPrivateKey(key)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Encode it in PEM format
+	privKey = pem.EncodeToMemory(&pem.Block{
+		Type:  "EC PRIVATE KEY",
+		Bytes: bs,
+	})
+
+	// Marshal the public key
+	bs, err = x509.MarshalPKIXPublicKey(key.Public())
+	if err != nil {
+		return nil, nil, err
+	}
+
+	// Encode it in PEM format
+	pubKey = pem.EncodeToMemory(&pem.Block{
+		Type:  "EC PUBLIC KEY",
+		Bytes: bs,
+	})
+
+	privKey = []byte(base64.URLEncoding.EncodeToString(privKey))
+	pubKey = []byte(base64.URLEncoding.EncodeToString(pubKey))
+
+	return
+}
+
+// Sign computes the hash of data and signs it with the private key, returning
+// a signature in PEM format.
+func sign(privKeyPEM []byte, data io.Reader) ([]byte, error) {
+	// Parse the private key
+	key, err := loadPrivateKey(privKeyPEM)
+	if err != nil {
+		return nil, err
+	}
+
+	// Hash the reader data
+	hash, err := hashReader(data)
+	if err != nil {
+		return nil, err
+	}
+
+	// Sign the hash
+	r, s, err := ecdsa.Sign(rand.Reader, key, hash)
+	if err != nil {
+		return nil, err
+	}
+
+	// Marshal the signature using ASN.1
+	sig, err := marshalSignature(r, s)
+	if err != nil {
+		return nil, err
+	}
+
+	// Encode it in a PEM block
+	bs := pem.EncodeToMemory(&pem.Block{
+		Type:  "SIGNATURE",
+		Bytes: sig,
+	})
+
+	return []byte(base64.URLEncoding.EncodeToString(bs)), nil
+}
+
+// Verify computes the hash of data and compares it to the signature using the
+// given public key. Returns nil if the signature is correct.
+func verify(pubKeyPEM []byte, signature []byte, data io.Reader) error {
+	// Parse the public key
+	key, err := loadPublicKey(pubKeyPEM)
+	if err != nil {
+		return err
+	}
+
+	bsDec, err := base64.URLEncoding.DecodeString(string(signature))
+	if err != nil {
+		return err
+	}
+	// Parse the signature
+	block, _ := pem.Decode(bsDec)
+	r, s, err := unmarshalSignature(block.Bytes)
+	if err != nil {
+		return err
+	}
+
+	// Compute the hash of the data
+	hash, err := hashReader(data)
+	if err != nil {
+		return err
+	}
+
+	// Verify the signature
+	if !ecdsa.Verify(key, hash, r, s) {
+		return errors.New("incorrect signature")
+	}
+
+	return nil
+}
+
+// hashReader returns the SHA256 hash of the reader
+func hashReader(r io.Reader) ([]byte, error) {
+	h := sha256.New()
+	if _, err := io.Copy(h, r); err != nil {
+		return nil, err
+	}
+	hash := []byte(fmt.Sprintf("%x", h.Sum(nil)))
+	return hash, nil
+}
+
+// loadPrivateKey returns the ECDSA private key structure for the given PEM
+// data.
+func loadPrivateKey(bs []byte) (*ecdsa.PrivateKey, error) {
+	bDecoded, err := base64.URLEncoding.DecodeString(string(bs))
+	if err != nil {
+		return nil, err
+	}
+	block, _ := pem.Decode([]byte(bDecoded))
+	return x509.ParseECPrivateKey(block.Bytes)
+}
+
+// loadPublicKey returns the ECDSA public key structure for the given PEM
+// data.
+func loadPublicKey(bs []byte) (*ecdsa.PublicKey, error) {
+	bDecoded := []byte{}
+	bDecoded, err := base64.URLEncoding.DecodeString(string(bs))
+	if err != nil {
+		return nil, err
+	}
+
+	// Decode and parse the public key PEM block
+	block, _ := pem.Decode(bDecoded)
+	intf, err := x509.ParsePKIXPublicKey(block.Bytes)
+	if err != nil {
+		return nil, err
+	}
+
+	// It should be an ECDSA public key
+	pk, ok := intf.(*ecdsa.PublicKey)
+	if !ok {
+		return nil, errors.New("unsupported public key format")
+	}
+
+	return pk, nil
+}
+
+// A wrapper around the signature integers so that we can marshal and
+// unmarshal them.
+type signature struct {
+	R, S *big.Int
+}
+
+// marhalSignature returns ASN.1 encoded bytes for the given integers,
+// suitable for PEM encoding.
+func marshalSignature(r, s *big.Int) ([]byte, error) {
+	sig := signature{
+		R: r,
+		S: s,
+	}
+
+	bs, err := asn1.Marshal(sig)
+	if err != nil {
+		return nil, err
+	}
+
+	return bs, nil
+}
+
+// unmarshalSignature returns the R and S integers from the given ASN.1
+// encoded signature.
+func unmarshalSignature(sig []byte) (r *big.Int, s *big.Int, err error) {
+	var ts signature
+	_, err = asn1.Unmarshal(sig, &ts)
+	if err != nil {
+		return nil, nil, err
+	}
+
+	return ts.R, ts.S, nil
+}

+ 101 - 0
pkg/trustzone/authprovider/ecdsa/provider.go

@@ -0,0 +1,101 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package ecdsa
+
+import (
+	"bytes"
+	"fmt"
+	"strings"
+
+	"github.com/ipfs/go-log/v2"
+	"github.com/mudler/edgevpn/pkg/blockchain"
+	"github.com/mudler/edgevpn/pkg/hub"
+	"github.com/mudler/edgevpn/pkg/node"
+)
+
+type ECDSA521 struct {
+	privkey string
+	logger  log.StandardLogger
+}
+
+// ECDSA521Provider returns an ECDSA521 auth provider.
+// To use it, use the following configuration to provide a
+// private key: AuthProviders: map[string]map[string]interface{}{"ecdsa": {"private_key": "<key>"}},
+// While running, keys can be added from a TZ node also from the api, for example:
+// curl -X PUT 'http://localhost:8081/api/ledger/trustzoneAuth/ecdsa_1/<key>'
+// Note: privkey and pubkeys are in the format generated by GenerateKeys() down below
+// The provider resolves "ecdsa" keys in the trustzone auth area, and
+// uses each one as pubkey to try to auth against
+func ECDSA521Provider(ll log.StandardLogger, privkey string) (*ECDSA521, error) {
+	return &ECDSA521{privkey: privkey, logger: ll}, nil
+}
+
+// Authenticate a message against a set of pubkeys.
+// It cycles over all the Trusted zone Auth data ( providers options, not where senders ID are stored)
+// and detects any key with ecdsa prefix. Values are assumed to be string and parsed as pubkeys.
+// The pubkeys are then used to authenticate nodes and verify if any of the pubkeys validates the challenge.
+func (e *ECDSA521) Authenticate(m *hub.Message, c chan *hub.Message, tzdata map[string]blockchain.Data) bool {
+
+	sigs, ok := m.Annotations["sigs"]
+	if !ok {
+		e.logger.Debug("No signature in message", m.Message, m.Annotations)
+
+		return false
+	}
+
+	e.logger.Debug("ECDSA auth Received", m)
+
+	pubKeys := []string{}
+	for k, t := range tzdata {
+		if strings.Contains(k, "ecdsa") {
+			var s string
+			t.Unmarshal(&s)
+			pubKeys = append(pubKeys, s)
+		}
+	}
+	if len(pubKeys) == 0 {
+		e.logger.Debug("ECDSA auth: No pubkeys to auth against")
+		// no pubkeys to authenticate present in the ledger
+		return false
+	}
+	for _, pubkey := range pubKeys {
+		// Try verifying the signature
+		if err := verify([]byte(pubkey), []byte(fmt.Sprint(sigs)), bytes.NewBufferString(m.Message)); err == nil {
+			e.logger.Debug("ECDSA auth: Signature verified")
+			return true
+		}
+		e.logger.Debug("ECDSA auth: Signature not verified")
+	}
+	return false
+}
+
+// Challenger sends ECDSA521 challenges over the public channel if the current node is not in the trusted zone.
+// This start a challenge which eventually should get the node into the TZ
+func (e *ECDSA521) Challenger(inTrustZone bool, c node.Config, n *node.Node, b *blockchain.Ledger, trustData map[string]blockchain.Data) {
+	if !inTrustZone {
+		e.logger.Debug("ECDSA auth: current node not in trustzone, sending challanges")
+		signature, err := sign([]byte(e.privkey), bytes.NewBufferString("challenge"))
+		if err != nil {
+			e.logger.Error("Error signing message: ", err.Error())
+			return
+		}
+		msg := hub.NewMessage("challenge")
+		msg.Annotations = make(map[string]interface{})
+		msg.Annotations["sigs"] = string(signature)
+		n.PublishMessage(msg)
+		return
+	}
+}

+ 104 - 0
pkg/trustzone/peergater.go

@@ -0,0 +1,104 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package trustzone
+
+import (
+	"context"
+	"sync"
+	"time"
+
+	"github.com/libp2p/go-libp2p-core/peer"
+	"github.com/mudler/edgevpn/pkg/blockchain"
+	"github.com/mudler/edgevpn/pkg/node"
+	"github.com/mudler/edgevpn/pkg/protocol"
+)
+
+type PeerGater struct {
+	sync.Mutex
+	trustDB          []peer.ID
+	enabled, relaxed bool
+}
+
+// NewPeerGater returns a new peergater
+// In relaxed mode won't gate until the trustDB contains some auth data.
+func NewPeerGater(relaxed bool) *PeerGater {
+	return &PeerGater{enabled: true, relaxed: relaxed}
+}
+
+// Enabled returns true if the PeerGater is enabled
+func (pg *PeerGater) Enabled() bool {
+	pg.Lock()
+	defer pg.Unlock()
+	return pg.enabled
+}
+
+// Disables turn off the peer gating mechanism
+func (pg *PeerGater) Disable() {
+	pg.Lock()
+	defer pg.Unlock()
+	pg.enabled = false
+}
+
+// Enable turns on peer gating mechanism
+func (pg *PeerGater) Enable() {
+	pg.Lock()
+	defer pg.Unlock()
+	pg.enabled = true
+}
+
+// Implements peergating interface
+// resolves to peers in the trustDB. if peer is absent will return true
+func (pg *PeerGater) Gate(n *node.Node, p peer.ID) bool {
+	pg.Lock()
+	defer pg.Unlock()
+	if !pg.enabled {
+		return false
+	}
+
+	if pg.relaxed && len(pg.trustDB) == 0 {
+		return false
+	}
+
+	for _, pp := range pg.trustDB {
+		if pp == p {
+			return false
+		}
+	}
+
+	return true
+}
+
+// UpdaterService is a service responsible to sync back trustDB from the ledger state.
+// It is a network service which retrieves the senders ID listed in the Trusted Zone
+// and fills it in the trustDB used to gate blockchain messages
+func (pg *PeerGater) UpdaterService(duration time.Duration) node.NetworkService {
+	return func(ctx context.Context, c node.Config, n *node.Node, b *blockchain.Ledger) error {
+		b.Announce(ctx, duration, func() {
+			db := []peer.ID{}
+			tz, found := b.CurrentData()[protocol.TrustZoneKey]
+			if found {
+				for k, _ := range tz {
+					db = append(db, peer.ID(k))
+				}
+			}
+			pg.Lock()
+			pg.trustDB = db
+			pg.Unlock()
+		})
+
+		return nil
+	}
+}

+ 107 - 0
pkg/trustzone/peerguardian.go

@@ -0,0 +1,107 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package trustzone
+
+import (
+	"context"
+	"time"
+
+	"github.com/ipfs/go-log"
+	"github.com/mudler/edgevpn/pkg/blockchain"
+	"github.com/mudler/edgevpn/pkg/hub"
+	"github.com/mudler/edgevpn/pkg/node"
+	"github.com/mudler/edgevpn/pkg/protocol"
+)
+
+// PeerGuardian provides auth for peers from blockchain data
+type PeerGuardian struct {
+	authProviders []AuthProvider
+	logger        log.StandardLogger
+}
+
+func NewPeerGuardian(logger log.StandardLogger, authProviders ...AuthProvider) *PeerGuardian {
+	return &PeerGuardian{
+		authProviders: authProviders,
+		logger:        logger,
+	}
+}
+
+// ReceiveMessage is a GenericHandler for public channel to provide authentication.
+// We receive messages here and we select them based on 2 criterias:
+// - messages that are supposed to generate challenges for auth mechanisms.
+//   Auth mechanisms should get user auth data from a special TZ dedicated to hashes that are manually added
+// - messages that are answers to such challenges and then means that the sender.ID should be added to the trust zone
+func (pg *PeerGuardian) ReceiveMessage(l *blockchain.Ledger, m *hub.Message, c chan *hub.Message) error {
+	pg.logger.Debug("Peerguardian received message from", m.SenderID)
+
+	for _, a := range pg.authProviders {
+
+		_, exists := l.GetKey(protocol.TrustZoneKey, m.SenderID)
+		trustAuth := l.CurrentData()[protocol.TrustZoneAuthKey]
+		if !exists && a.Authenticate(m, c, trustAuth) {
+			// try to authenticate it
+			// Note we can also not be in a TZ here as we are not able to check (we miss node information at hand)
+			// In any way nodes would ignore the messages, and that we hit Authenticate is useful for two (or more)
+			// steps authenticators.
+			l.Persist(context.Background(), 5*time.Second, 120*time.Second, protocol.TrustZoneKey, m.SenderID, "")
+			return nil
+		}
+	}
+
+	return nil
+}
+
+// Challenger is a NetworkService that should send challenges with all enabled authenticators until we are in TZ
+// note that might never happen as node might not have a satisfying authentication mechanism
+func (pg *PeerGuardian) Challenger(duration time.Duration, autocleanup bool) node.NetworkService {
+	return func(ctx context.Context, c node.Config, n *node.Node, b *blockchain.Ledger) error {
+		b.Announce(ctx, duration, func() {
+			trustAuth := b.CurrentData()[protocol.TrustZoneAuthKey]
+			_, exists := b.GetKey(protocol.TrustZoneKey, n.Host().ID().String())
+			for _, a := range pg.authProviders {
+				a.Challenger(exists, c, n, b, trustAuth)
+			}
+
+			// Automatically cleanup TZ from peers not anymore in the hub
+			if autocleanup {
+				peers, err := n.MessageHub.ListPeers()
+				if err != nil {
+					return
+				}
+				tz := b.CurrentData()[protocol.TrustZoneKey]
+
+				for k, _ := range tz {
+				PEER:
+					for _, p := range peers {
+						if p.String() == k {
+							break PEER
+						}
+					}
+					b.Delete(protocol.TrustZoneKey, k)
+				}
+			}
+		})
+		return nil
+	}
+}
+
+// AuthProvider is a generic Blockchain authentity provider
+type AuthProvider interface {
+	// Authenticate either generates challanges to pick up later or authenticates a node
+	// from a message with the available auth data in the blockchain
+	Authenticate(*hub.Message, chan *hub.Message, map[string]blockchain.Data) bool
+	Challenger(inTrustZone bool, c node.Config, n *node.Node, b *blockchain.Ledger, trustData map[string]blockchain.Data)
+}

+ 28 - 0
pkg/trustzone/services_suite_test.go

@@ -0,0 +1,28 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package trustzone_test
+
+import (
+	"testing"
+
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+)
+
+func TestTrustzone(t *testing.T) {
+	RegisterFailHandler(Fail)
+	RunSpecs(t, "Trustzone Suite")
+}

+ 170 - 0
pkg/trustzone/trustzone_test.go

@@ -0,0 +1,170 @@
+// Copyright © 2022 Ettore Di Giacinto <[email protected]>
+//
+// This program is free software; you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation; either version 2 of the License, or
+// (at your option) any later version.
+//
+// This program is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License along
+// with this program; if not, see <http://www.gnu.org/licenses/>.
+
+package trustzone_test
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/ipfs/go-log"
+	"github.com/libp2p/go-libp2p"
+	connmanager "github.com/libp2p/go-libp2p-connmgr"
+	. "github.com/onsi/ginkgo/v2"
+	. "github.com/onsi/gomega"
+
+	"github.com/mudler/edgevpn/pkg/blockchain"
+	"github.com/mudler/edgevpn/pkg/logger"
+	node "github.com/mudler/edgevpn/pkg/node"
+	"github.com/mudler/edgevpn/pkg/protocol"
+	"github.com/mudler/edgevpn/pkg/trustzone"
+	. "github.com/mudler/edgevpn/pkg/trustzone"
+	. "github.com/mudler/edgevpn/pkg/trustzone/authprovider/ecdsa"
+)
+
+var _ = Describe("trustzone", func() {
+	token := node.GenerateNewConnectionData().Base64()
+
+	logg := logger.New(log.LevelDebug)
+	ll := node.Logger(logg)
+
+	Context("ECDSA auth", func() {
+		It("authorize nodes", func() {
+			ctx, cancel := context.WithCancel(context.Background())
+			defer cancel()
+
+			ctx2, cancel2 := context.WithCancel(context.Background())
+			defer cancel2()
+
+			privKey, pubKey, err := GenerateKeys()
+			Expect(err).ToNot(HaveOccurred())
+
+			pg := NewPeerGater(false)
+			dur := 5 * time.Second
+			provider, err := ECDSA521Provider(logg, string(privKey))
+			aps := []trustzone.AuthProvider{provider}
+
+			pguardian := trustzone.NewPeerGuardian(logg, aps...)
+
+			cm, err := connmanager.NewConnManager(
+				1,
+				5,
+				connmanager.WithGracePeriod(80*time.Second),
+			)
+
+			permStore := &blockchain.MemoryStore{}
+
+			e, _ := node.New(
+				node.WithLibp2pAdditionalOptions(libp2p.ConnectionManager(cm)),
+				node.WithNetworkService(
+					pg.UpdaterService(dur),
+					pguardian.Challenger(dur, false),
+				),
+				node.EnableGenericHub,
+				node.GenericChannelHandlers(pguardian.ReceiveMessage),
+				//	node.WithPeerGater(pg),
+				node.WithDiscoveryInterval(10*time.Second),
+				node.FromBase64(true, true, token), node.WithStore(permStore), ll)
+
+			pguardian2 := trustzone.NewPeerGuardian(logg, aps...)
+
+			e2, _ := node.New(
+				node.WithLibp2pAdditionalOptions(libp2p.ConnectionManager(cm)),
+
+				node.WithNetworkService(
+					pg.UpdaterService(dur),
+					pguardian2.Challenger(dur, false),
+				),
+				node.EnableGenericHub,
+				node.GenericChannelHandlers(pguardian2.ReceiveMessage),
+				//	node.WithPeerGater(pg),
+				node.WithDiscoveryInterval(10*time.Second),
+				node.FromBase64(true, true, token), node.WithStore(&blockchain.MemoryStore{}), ll)
+
+			l, err := e.Ledger()
+			Expect(err).ToNot(HaveOccurred())
+
+			l2, err := e2.Ledger()
+			Expect(err).ToNot(HaveOccurred())
+
+			go e.Start(ctx2)
+
+			time.Sleep(10 * time.Second)
+			go e2.Start(ctx)
+
+			l.Persist(ctx, 2*time.Second, 20*time.Second, protocol.TrustZoneAuthKey, "ecdsa", string(pubKey))
+
+			Eventually(func() bool {
+				_, exists := l2.GetKey(protocol.TrustZoneAuthKey, "ecdsa")
+				fmt.Println("Ledger2", l2.CurrentData())
+				fmt.Println("Ledger1", l.CurrentData())
+				return exists
+			}, 60*time.Second, 1*time.Second).Should(BeTrue())
+
+			Eventually(func() bool {
+				_, exists := l2.GetKey(protocol.TrustZoneKey, e.Host().ID().String())
+				fmt.Println("Ledger2", l2.CurrentData())
+				fmt.Println("Ledger1", l.CurrentData())
+				return exists
+			}, 60*time.Second, 1*time.Second).Should(BeTrue())
+
+			Eventually(func() bool {
+				_, exists := l.GetKey(protocol.TrustZoneKey, e2.Host().ID().String())
+				fmt.Println("Ledger2", l2.CurrentData())
+				fmt.Println("Ledger1", l.CurrentData())
+				return exists
+			}, 60*time.Second, 1*time.Second).Should(BeTrue())
+
+			cancel2()
+
+			e, err = node.New(
+				node.WithLibp2pAdditionalOptions(libp2p.ConnectionManager(cm)),
+				node.WithNetworkService(
+					pg.UpdaterService(dur),
+					pguardian.Challenger(dur, false),
+				),
+				node.EnableGenericHub,
+				node.GenericChannelHandlers(pguardian.ReceiveMessage),
+				node.WithPeerGater(pg),
+				node.WithDiscoveryInterval(10*time.Second),
+				node.FromBase64(true, true, token), node.WithStore(permStore), ll)
+
+			Expect(err).ToNot(HaveOccurred())
+
+			l, err = e.Ledger()
+			Expect(err).ToNot(HaveOccurred())
+
+			go e.Start(ctx)
+
+			Eventually(func() bool {
+				if e.Host() == nil {
+					return false
+				}
+				_, exists := l2.GetKey(protocol.TrustZoneKey, e.Host().ID().String())
+				fmt.Println("Ledger2", l2.CurrentData())
+				fmt.Println("Ledger1", l.CurrentData())
+				return exists
+			}, 60*time.Second, 1*time.Second).Should(BeTrue())
+
+			Eventually(func() bool {
+				_, exists := l.GetKey(protocol.TrustZoneKey, e.Host().ID().String())
+				fmt.Println("Ledger2", l2.CurrentData())
+				fmt.Println("Ledger1", l.CurrentData())
+				return exists
+			}, 60*time.Second, 1*time.Second).Should(BeTrue())
+		})
+	})
+})