Turn credentials generation (#102)

* abstract peer channel

* remove wip code

* refactor NewServer with Peer updates channel

* feature: add TURN credentials manager

* hmac logic

* example test function

* test: add TimeBasedAuthSecretsManager_GenerateCredentials  test

* test: make tests for now with hardcoded secret

* test: add TimeBasedAuthSecretsManager_SetupRefresh test

* test: add TimeBasedAuthSecretsManager_SetupRefresh test

* test: add TimeBasedAuthSecretsManager_CancelRefresh test

* feature: extract TURNConfig to the management config

* feature: return hash based TURN credentials only on initial sync

* feature: make TURN time based secret credentials optional

Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
This commit is contained in:
Mikhail Bragin 2021-09-02 14:41:54 +02:00 committed by GitHub
parent 86f3b1e5c8
commit b17424d630
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 399 additions and 57 deletions

View File

@ -39,7 +39,8 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste
accountManager := mgmt.NewManager(store)
peersUpdateManager := mgmt.NewPeersUpdateManager()
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager)
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {
t.Fatal(err)
}

View File

@ -7,14 +7,19 @@
"Password": null
}
],
"Turns": [
{
"Proto": "udp",
"URI": "turn:stun.wiretrustee.com:3468",
"Username": "some_user",
"Password": "c29tZV9wYXNzd29yZA=="
}
],
"TURNConfig": {
"Turns": [
{
"Proto": "udp",
"URI": "turn:stun.wiretrustee.com:3468",
"Username": "some_user",
"Password": "c29tZV9wYXNzd29yZA=="
}
],
"CredentialsTTL": "1h",
"Secret": "c29tZV9wYXNzd29yZA==",
"TimeBasedCredentials": true
},
"Signal": {
"Proto": "http",
"URI": "signal.wiretrustee.com:10000",

View File

@ -62,7 +62,8 @@ func startManagement(config *mgmt.Config, t *testing.T) (*grpc.Server, net.Liste
accountManager := mgmt.NewManager(store)
peersUpdateManager := mgmt.NewPeersUpdateManager()
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager)
turnManager := mgmt.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := mgmt.NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {
t.Fatal(err)
}

View File

@ -79,7 +79,8 @@ var (
opts = append(opts, grpc.KeepaliveEnforcementPolicy(kaep), grpc.KeepaliveParams(kasp))
grpcServer := grpc.NewServer(opts...)
peersUpdateManager := server.NewPeersUpdateManager()
server, err := server.NewServer(config, accountManager, peersUpdateManager)
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
server, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
if err != nil {
log.Fatalf("failed creating new server: %v", err)
}

View File

@ -1,5 +1,9 @@
package server
import (
"github.com/wiretrustee/wiretrustee/util"
)
type Protocol string
const (
@ -12,15 +16,23 @@ const (
// Config of the Management service
type Config struct {
Stuns []*Host
Turns []*Host
Signal *Host
Stuns []*Host
TURNConfig *TURNConfig
Signal *Host
Datadir string
HttpConfig *HttpServerConfig
}
// TURNConfig is a config of the TURNCredentialsManager
type TURNConfig struct {
TimeBasedCredentials bool
CredentialsTTL util.Duration
Secret []byte
Turns []*Host
}
// HttpServerConfig is a config of the HTTP Management service server
type HttpServerConfig struct {
LetsEncryptDomain string

View File

@ -19,15 +19,16 @@ type Server struct {
accountManager *AccountManager
wgKey wgtypes.Key
proto.UnimplementedManagementServiceServer
peersUpdateManager *PeersUpdateManager
config *Config
peersUpdateManager *PeersUpdateManager
config *Config
turnCredentialsManager TURNCredentialsManager
}
// AllowedIPsFormat generates Wireguard AllowedIPs format (e.g. 100.30.30.1/32)
const AllowedIPsFormat = "%s/32"
// NewServer creates a new Management server
func NewServer(config *Config, accountManager *AccountManager, peersUpdateManager *PeersUpdateManager) (*Server, error) {
func NewServer(config *Config, accountManager *AccountManager, peersUpdateManager *PeersUpdateManager, turnCredentialsManager TURNCredentialsManager) (*Server, error) {
key, err := wgtypes.GeneratePrivateKey()
if err != nil {
return nil, err
@ -36,9 +37,10 @@ func NewServer(config *Config, accountManager *AccountManager, peersUpdateManage
return &Server{
wgKey: key,
// peerKey -> event channel
peersUpdateManager: peersUpdateManager,
accountManager: accountManager,
config: config,
peersUpdateManager: peersUpdateManager,
accountManager: accountManager,
config: config,
turnCredentialsManager: turnCredentialsManager,
}, nil
}
@ -89,7 +91,8 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
if err != nil {
log.Warnf("failed marking peer as connected %s %v", peerKey, err)
}
// Todo start turn credentials goroutine
s.turnCredentialsManager.SetupRefresh(peerKey.String())
// keep a connection to the peer and send updates when available
for {
select {
@ -118,7 +121,8 @@ func (s *Server) Sync(req *proto.EncryptedMessage, srv proto.ManagementService_S
// happens when connection drops, e.g. client disconnects
log.Debugf("stream of peer %s has been closed", peerKey.String())
s.peersUpdateManager.CloseChannel(peerKey.String())
err := s.accountManager.MarkPeerConnected(peerKey.String(), false)
s.turnCredentialsManager.CancelRefresh(peerKey.String())
err = s.accountManager.MarkPeerConnected(peerKey.String(), false)
if err != nil {
log.Warnf("failed marking peer as disconnected %s %v", peerKey, err)
}
@ -165,7 +169,7 @@ func (s *Server) registerPeer(peerKey wgtypes.Key, req *proto.LoginRequest) (*Pe
peersToSend = append(peersToSend, p)
}
}
update := toSyncResponse(s.config, peer, peersToSend)
update := toSyncResponse(s.config, peer, peersToSend, nil)
err = s.peersUpdateManager.SendUpdate(remotePeer.Key, &UpdateMessage{Update: update})
if err != nil {
// todo rethink if we should keep this return
@ -215,11 +219,10 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
return nil, status.Error(codes.Internal, "internal server error")
}
}
// Todo fill up turn credentials
// if peer has reached this point then it has logged in
loginResp := &proto.LoginResponse{
WiretrusteeConfig: toWiretrusteeConfig(s.config),
WiretrusteeConfig: toWiretrusteeConfig(s.config, nil),
PeerConfig: toPeerConfig(peer),
}
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, loginResp)
@ -233,7 +236,7 @@ func (s *Server) Login(ctx context.Context, req *proto.EncryptedMessage) (*proto
}, nil
}
func toResponseProto(configProto Protocol) proto.HostConfig_Protocol {
func ToResponseProto(configProto Protocol) proto.HostConfig_Protocol {
switch configProto {
case UDP:
return proto.HostConfig_UDP
@ -251,24 +254,33 @@ func toResponseProto(configProto Protocol) proto.HostConfig_Protocol {
}
}
func toWiretrusteeConfig(config *Config) *proto.WiretrusteeConfig {
func toWiretrusteeConfig(config *Config, turnCredentials *TURNCredentials) *proto.WiretrusteeConfig {
var stuns []*proto.HostConfig
for _, stun := range config.Stuns {
stuns = append(stuns, &proto.HostConfig{
Uri: stun.URI,
Protocol: toResponseProto(stun.Proto),
Protocol: ToResponseProto(stun.Proto),
})
}
var turns []*proto.ProtectedHostConfig
for _, turn := range config.Turns {
for _, turn := range config.TURNConfig.Turns {
var username string
var password string
if turnCredentials != nil {
username = turnCredentials.Username
password = turnCredentials.Password
} else {
username = turn.Username
password = string(turn.Password)
}
turns = append(turns, &proto.ProtectedHostConfig{
HostConfig: &proto.HostConfig{
Uri: turn.URI,
Protocol: toResponseProto(turn.Proto),
Protocol: ToResponseProto(turn.Proto),
},
User: turn.Username,
Password: string(turn.Password),
User: username,
Password: password,
})
}
@ -277,7 +289,7 @@ func toWiretrusteeConfig(config *Config) *proto.WiretrusteeConfig {
Turns: turns,
Signal: &proto.HostConfig{
Uri: config.Signal.URI,
Protocol: toResponseProto(config.Signal.Proto),
Protocol: ToResponseProto(config.Signal.Proto),
},
}
}
@ -288,9 +300,9 @@ func toPeerConfig(peer *Peer) *proto.PeerConfig {
}
}
func toSyncResponse(config *Config, peer *Peer, peers []*Peer) *proto.SyncResponse {
func toSyncResponse(config *Config, peer *Peer, peers []*Peer, turnCredentials *TURNCredentials) *proto.SyncResponse {
wtConfig := toWiretrusteeConfig(config)
wtConfig := toWiretrusteeConfig(config, turnCredentials)
pConfig := toPeerConfig(peer)
@ -322,13 +334,22 @@ func (s *Server) sendInitialSync(peerKey wgtypes.Key, peer *Peer, srv proto.Mana
log.Warnf("error getting a list of peers for a peer %s", peer.Key)
return err
}
plainResp := toSyncResponse(s.config, peer, peers)
// make secret time based TURN credentials optional
var turnCredentials *TURNCredentials
if s.config.TURNConfig.TimeBasedCredentials {
creds := s.turnCredentialsManager.GenerateCredentials()
turnCredentials = &creds
} else {
turnCredentials = nil
}
plainResp := toSyncResponse(s.config, peer, peers, turnCredentials)
encryptedResp, err := encryption.EncryptMessage(peerKey, s.wgKey, plainResp)
if err != nil {
return status.Errorf(codes.Internal, "error handling request")
}
// Todo fill up the turn credentials
err = srv.Send(&proto.EncryptedMessage{
WgPubKey: s.wgKey.PublicKey().String(),
Body: encryptedResp,

View File

@ -119,19 +119,18 @@ var _ = Describe("Management service", func() {
Uri: "stun:stun.wiretrustee.com:3468",
Protocol: mgmtProto.HostConfig_UDP,
}
expectedTurnsConfig := &mgmtProto.ProtectedHostConfig{
HostConfig: &mgmtProto.HostConfig{
Uri: "turn:stun.wiretrustee.com:3468",
Protocol: mgmtProto.HostConfig_UDP,
},
User: "some_user",
Password: "some_password",
expectedTRUNHost := &mgmtProto.HostConfig{
Uri: "turn:stun.wiretrustee.com:3468",
Protocol: mgmtProto.HostConfig_UDP,
}
Expect(resp.WiretrusteeConfig.Signal).To(BeEquivalentTo(expectedSignalConfig))
Expect(resp.WiretrusteeConfig.Stuns).To(ConsistOf(expectedStunsConfig))
Expect(resp.WiretrusteeConfig.Turns).To(ConsistOf(expectedTurnsConfig))
// TURN validation is special because credentials are dynamically generated
Expect(resp.WiretrusteeConfig.Turns).To(HaveLen(1))
actualTURN := resp.WiretrusteeConfig.Turns[0]
Expect(len(actualTURN.User) > 0).To(BeTrue())
Expect(actualTURN.HostConfig).To(BeEquivalentTo(expectedTRUNHost))
})
})
@ -368,7 +367,10 @@ var _ = Describe("Management service", func() {
resp := &mgmtProto.SyncResponse{}
err = pb.Unmarshal(decryptedBytes, resp)
Expect(err).NotTo(HaveOccurred())
wg.Done()
if len(resp.GetRemotePeers()) > 0 {
//only consider peer updates
wg.Done()
}
}
}()
}
@ -388,7 +390,6 @@ var _ = Describe("Management service", func() {
err := syncClient.CloseSend()
Expect(err).NotTo(HaveOccurred())
}
})
})
})
@ -486,13 +487,15 @@ func startServer(config *server.Config) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", ":0")
Expect(err).NotTo(HaveOccurred())
s := grpc.NewServer()
store, err := server.NewStore(config.Datadir)
if err != nil {
log.Fatalf("failed creating a store: %s: %v", config.Datadir, err)
}
accountManager := server.NewManager(store)
peersUpdateManager := server.NewPeersUpdateManager()
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager)
turnManager := server.NewTimeBasedAuthSecretsManager(peersUpdateManager, config.TURNConfig)
mgmtServer, err := server.NewServer(config, accountManager, peersUpdateManager, turnManager)
Expect(err).NotTo(HaveOccurred())
mgmtProto.RegisterManagementServiceServer(s, mgmtServer)
go func() {

View File

@ -7,14 +7,19 @@
"Password": null
}
],
"Turns": [
{
"Proto": "udp",
"URI": "turn:stun.wiretrustee.com:3468",
"Username": "some_user",
"Password": "c29tZV9wYXNzd29yZA=="
}
],
"TURNConfig": {
"Turns": [
{
"Proto": "udp",
"URI": "turn:stun.wiretrustee.com:3468",
"Username": "some_user",
"Password": "c29tZV9wYXNzd29yZA=="
}
],
"CredentialsTTL": "1h",
"Secret": "c29tZV9wYXNzd29yZA==",
"TimeBasedCredentials": true
},
"Signal": {
"Proto": "http",
"URI": "signal.wiretrustee.com:10000",

View File

@ -0,0 +1,123 @@
package server
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
log "github.com/sirupsen/logrus"
"github.com/wiretrustee/wiretrustee/management/proto"
"sync"
"time"
)
//TURNCredentialsManager used to manage TURN credentials
type TURNCredentialsManager interface {
GenerateCredentials() TURNCredentials
SetupRefresh(peerKey string)
CancelRefresh(peerKey string)
}
//TimeBasedAuthSecretsManager generates credentials with TTL and using pre-shared secret known to TURN server
type TimeBasedAuthSecretsManager struct {
mux sync.Mutex
config *TURNConfig
updateManager *PeersUpdateManager
cancelMap map[string]chan struct{}
}
type TURNCredentials struct {
Username string
Password string
}
func NewTimeBasedAuthSecretsManager(updateManager *PeersUpdateManager, config *TURNConfig) *TimeBasedAuthSecretsManager {
return &TimeBasedAuthSecretsManager{
mux: sync.Mutex{},
config: config,
updateManager: updateManager,
cancelMap: make(map[string]chan struct{}),
}
}
//GenerateCredentials generates new time-based secret credentials - basically username is a unix timestamp and password is a HMAC hash of a timestamp with a preshared TURN secret
func (m *TimeBasedAuthSecretsManager) GenerateCredentials() TURNCredentials {
mac := hmac.New(sha1.New, m.config.Secret)
timeAuth := time.Now().Add(m.config.CredentialsTTL.Duration).Unix()
username := fmt.Sprint(timeAuth)
_, err := mac.Write([]byte(username))
if err != nil {
log.Errorln("Generating turn password failed with error: ", err)
}
bytePassword := mac.Sum(nil)
password := base64.StdEncoding.EncodeToString(bytePassword)
return TURNCredentials{
Username: username,
Password: password,
}
}
func (m *TimeBasedAuthSecretsManager) cancel(peerKey string) {
if channel, ok := m.cancelMap[peerKey]; ok {
close(channel)
delete(m.cancelMap, peerKey)
}
}
//CancelRefresh cancels scheduled peer credentials refresh
func (m *TimeBasedAuthSecretsManager) CancelRefresh(peerKey string) {
m.mux.Lock()
defer m.mux.Unlock()
m.cancel(peerKey)
}
//SetupRefresh starts peer credentials refresh. Since credentials are expiring (TTL) it is necessary to always generate them and send to the peer.
//A goroutine is created and put into TimeBasedAuthSecretsManager.cancelMap. This routine should be cancelled if peer is gone.
func (m *TimeBasedAuthSecretsManager) SetupRefresh(peerKey string) {
m.mux.Lock()
defer m.mux.Unlock()
m.cancel(peerKey)
cancel := make(chan struct{}, 1)
m.cancelMap[peerKey] = cancel
go func() {
for {
select {
case <-cancel:
return
default:
//we don't want to regenerate credentials right on expiration, so we do it slightly before (at 3/4 of TTL)
time.Sleep(m.config.CredentialsTTL.Duration / 4 * 3)
c := m.GenerateCredentials()
var turns []*proto.ProtectedHostConfig
for _, host := range m.config.Turns {
turns = append(turns, &proto.ProtectedHostConfig{
HostConfig: &proto.HostConfig{
Uri: host.URI,
Protocol: ToResponseProto(host.Proto),
},
User: c.Username,
Password: c.Password,
})
}
update := &proto.SyncResponse{
WiretrusteeConfig: &proto.WiretrusteeConfig{
Turns: turns,
},
}
err := m.updateManager.SendUpdate(peerKey, &UpdateMessage{Update: update})
if err != nil {
log.Errorf("error while sending TURN update to peer %s %v", peerKey, err)
// todo maybe continue trying?
}
}
}
}()
}

View File

@ -0,0 +1,133 @@
package server
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"github.com/wiretrustee/wiretrustee/util"
"testing"
"time"
)
var TurnTestHost = &Host{
Proto: UDP,
URI: "turn:turn.wiretrustee.com:77777",
Username: "username",
Password: nil,
}
func TestTimeBasedAuthSecretsManager_GenerateCredentials(t *testing.T) {
ttl := util.Duration{Duration: time.Hour}
secret := []byte("some_secret")
peersManager := NewPeersUpdateManager()
tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{
CredentialsTTL: ttl,
Secret: secret,
Turns: []*Host{TurnTestHost},
})
credentials := tested.GenerateCredentials()
if credentials.Username == "" {
t.Errorf("expected generated TURN username not to be empty, got empty")
}
if credentials.Password == "" {
t.Errorf("expected generated TURN password not to be empty, got empty")
}
validateMAC(credentials.Username, credentials.Password, secret, t)
}
func TestTimeBasedAuthSecretsManager_SetupRefresh(t *testing.T) {
ttl := util.Duration{Duration: 2 * time.Second}
secret := []byte("some_secret")
peersManager := NewPeersUpdateManager()
peer := "some_peer"
updateChannel := peersManager.CreateChannel(peer)
tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{
CredentialsTTL: ttl,
Secret: secret,
Turns: []*Host{TurnTestHost},
})
tested.SetupRefresh(peer)
if _, ok := tested.cancelMap[peer]; !ok {
t.Errorf("expecting peer to be present in a cancel map, got not present")
}
var updates []*UpdateMessage
loop:
for timeout := time.After(5 * time.Second); ; {
select {
case update := <-updateChannel:
updates = append(updates, update)
case <-timeout:
break loop
}
if len(updates) >= 2 {
break loop
}
}
if len(updates) < 2 {
t.Errorf("expecting 2 peer credentials updates, got %v", len(updates))
}
firstUpdate := updates[0].Update.GetWiretrusteeConfig().Turns[0]
secondUpdate := updates[1].Update.GetWiretrusteeConfig().Turns[0]
if firstUpdate.Password == secondUpdate.Password {
t.Errorf("expecting first credential update password %v to be diffeerent from second, got equal", firstUpdate.Password)
}
}
func TestTimeBasedAuthSecretsManager_CancelRefresh(t *testing.T) {
ttl := util.Duration{Duration: time.Hour}
secret := []byte("some_secret")
peersManager := NewPeersUpdateManager()
peer := "some_peer"
tested := NewTimeBasedAuthSecretsManager(peersManager, &TURNConfig{
CredentialsTTL: ttl,
Secret: secret,
Turns: []*Host{TurnTestHost},
})
tested.SetupRefresh(peer)
if _, ok := tested.cancelMap[peer]; !ok {
t.Errorf("expecting peer to be present in a cancel map, got not present")
}
tested.CancelRefresh(peer)
if _, ok := tested.cancelMap[peer]; ok {
t.Errorf("expecting peer to be not present in a cancel map, got present")
}
}
func validateMAC(username string, actualMAC string, key []byte, t *testing.T) {
mac := hmac.New(sha1.New, key)
_, err := mac.Write([]byte(username))
if err != nil {
t.Fatal(err)
}
expectedMAC := mac.Sum(nil)
decodedMAC, err := base64.StdEncoding.DecodeString(actualMAC)
if err != nil {
t.Fatal(err)
}
equal := hmac.Equal(decodedMAC, expectedMAC)
if !equal {
t.Errorf("expected password MAC to be %s. got %s", expectedMAC, decodedMAC)
}
}

37
util/duration.go Normal file
View File

@ -0,0 +1,37 @@
package util
import (
"encoding/json"
"errors"
"time"
)
//Duration is used strictly for JSON requests/responses due to duration marshalling issues
type Duration struct {
time.Duration
}
func (d Duration) MarshalJSON() ([]byte, error) {
return json.Marshal(d.String())
}
func (d *Duration) UnmarshalJSON(b []byte) error {
var v interface{}
if err := json.Unmarshal(b, &v); err != nil {
return err
}
switch value := v.(type) {
case float64:
d.Duration = time.Duration(value)
return nil
case string:
var err error
d.Duration, err = time.ParseDuration(value)
if err != nil {
return err
}
return nil
default:
return errors.New("invalid duration")
}
}