Skip to content

Commit

Permalink
Use signed timestamp for nonce
Browse files Browse the repository at this point in the history
  • Loading branch information
paulwe authored and davidzhao committed Feb 6, 2024
1 parent a980322 commit fb0ab51
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 58 deletions.
5 changes: 0 additions & 5 deletions client_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,11 +177,6 @@ func TestClientNonceExpiration(t *testing.T) {
allocation, err := client.Allocate()
assert.NoError(t, err)

server.nonces.Range(func(key, value interface{}) bool {
server.nonces.Delete(key)
return true
})

_, err = allocation.WriteTo([]byte{0x00}, &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080})
assert.NoError(t, err)

Expand Down
2 changes: 1 addition & 1 deletion internal/server/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import "errors"

var (
errFailedToGenerateNonce = errors.New("failed to generate nonce")
errInvalidNonce = errors.New("invalid nonce")
errFailedToSendError = errors.New("failed to send error message")
errDuplicatedNonce = errors.New("duplicated Nonce generated, discarding request")
errNoSuchUser = errors.New("no such user exists")
errUnexpectedClass = errors.New("unexpected class")
errUnexpectedMethod = errors.New("unexpected method")
Expand Down
71 changes: 71 additions & 0 deletions internal/server/nonce.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package server

import (
"crypto/hmac"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"fmt"
"time"
)

const (
nonceLifetime = time.Hour // See: https://tools.ietf.org/html/rfc5766#section-4
nonceLength = 40
nonceKeyLength = 64
)

// NewNonceHash creates a NonceHash
func NewNonceHash() (*NonceHash, error) {
key := make([]byte, nonceKeyLength)
if _, err := rand.Read(key); err != nil {
return nil, err
}

return &NonceHash{key}, nil
}

// NonceHash is used to create and verify nonces
type NonceHash struct {
key []byte
}

// Generate a nonce
func (n *NonceHash) Generate() (string, error) {
nonce := make([]byte, 8, nonceLength)
binary.BigEndian.PutUint64(nonce, uint64(time.Now().UnixMilli()))

hash := hmac.New(sha256.New, n.key)
if _, err := hash.Write(nonce[:8]); err != nil {
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err) //nolint:errorlint
}
nonce = hash.Sum(nonce)

return hex.EncodeToString(nonce), nil
}

// Validate checks that nonce is signed and is not expired
func (n *NonceHash) Validate(nonce string) error {
b, err := hex.DecodeString(nonce)
if err != nil || len(b) != nonceLength {
return fmt.Errorf("%w: %v", errInvalidNonce, err) //nolint:errorlint
}

if ts := time.UnixMilli(int64(binary.BigEndian.Uint64(b))); time.Since(ts) > nonceLifetime {
return errInvalidNonce
}

hash := hmac.New(sha256.New, n.key)
if _, err = hash.Write(b[:8]); err != nil {
return fmt.Errorf("%w: %v", errInvalidNonce, err) //nolint:errorlint
}
if !hmac.Equal(b[8:], hash.Sum(nil)) {
return errInvalidNonce
}

return nil
}
20 changes: 20 additions & 0 deletions internal/server/nonce_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package server

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestNonceHash(t *testing.T) {
t.Run("generated hashes validate", func(t *testing.T) {
h, err := NewNonceHash()
assert.NoError(t, err)
nonce, err := h.Generate()
assert.NoError(t, err)
assert.NoError(t, h.Validate(nonce))
})
}
3 changes: 1 addition & 2 deletions internal/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ package server
import (
"fmt"
"net"
"sync"
"time"

"github.com/pion/logging"
Expand All @@ -25,7 +24,7 @@ type Request struct {

// Server State
AllocationManager *allocation.Manager
Nonces *sync.Map
NonceHash *NonceHash

// User Configuration
AuthHandler func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool)
Expand Down
12 changes: 7 additions & 5 deletions internal/server/turn_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ package server

import (
"net"
"sync"
"testing"
"time"

Expand Down Expand Up @@ -80,18 +79,21 @@ func TestAllocationLifeTime(t *testing.T) {
})
assert.NoError(t, err)

staticKey := []byte("ABC")
nonceHash, err := NewNonceHash()
assert.NoError(t, err)
staticKey, err := nonceHash.Generate()
assert.NoError(t, err)

r := Request{
AllocationManager: allocationManager,
Nonces: &sync.Map{},
NonceHash: nonceHash,
Conn: l,
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5000},
Log: logger,
AuthHandler: func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool) {
return staticKey, true
return []byte(staticKey), true
},
}
r.Nonces.Store(string(staticKey), time.Now())

fiveTuple := &allocation.FiveTuple{SrcAddr: r.SrcAddr, DstAddr: r.Conn.LocalAddr(), Protocol: allocation.UDP}

Expand Down
44 changes: 3 additions & 41 deletions internal/server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@
package server

import (
"crypto/md5" //nolint:gosec,gci
"crypto/rand"
"errors"
"fmt"
"io"
"math/big"
"net"
"strconv"
"time"

"github.com/pion/stun/v2"
Expand All @@ -20,29 +15,8 @@ import (

const (
maximumAllocationLifetime = time.Hour // See: https://tools.ietf.org/html/rfc5766#section-6.2 defines 3600 seconds recommendation
nonceLifetime = time.Hour // See: https://tools.ietf.org/html/rfc5766#section-4
)

func buildNonce() (string, error) {
/* #nosec */
h := md5.New()
if _, err := io.WriteString(h, strconv.FormatInt(time.Now().Unix(), 10)); err != nil {
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err) //nolint:errorlint
}

maxInt63 := big.NewInt(1<<63 - 1)
maxInt63.Add(maxInt63, big.NewInt(1))
randInt63, err := rand.Int(rand.Reader, maxInt63)
if err != nil {
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err) //nolint:errorlint
}

if _, err := io.WriteString(h, randInt63.String()); err != nil { //nolint:gosec
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err) //nolint:errorlint
}
return fmt.Sprintf("%x", h.Sum(nil)), nil
}

func buildAndSend(conn net.PacketConn, dst net.Addr, attrs ...stun.Setter) error {
msg, err := stun.Build(attrs...)
if err != nil {
Expand Down Expand Up @@ -70,16 +44,11 @@ func buildMsg(transactionID [stun.TransactionIDSize]byte, msgType stun.MessageTy

func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method) (stun.MessageIntegrity, bool, error) {
respondWithNonce := func(responseCode stun.ErrorCode) (stun.MessageIntegrity, bool, error) {
nonce, err := buildNonce()
nonce, err := r.NonceHash.Generate()
if err != nil {
return nil, false, err
}

// Nonce has already been taken
if _, keyCollision := r.Nonces.LoadOrStore(nonce, time.Now()); keyCollision {
return nil, false, errDuplicatedNonce
}

return nil, false, buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID,
stun.NewType(callingMethod, stun.ClassErrorResponse),
&stun.ErrorCodeAttribute{Code: responseCode},
Expand All @@ -101,15 +70,8 @@ func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method)
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
}

// Assert Nonce exists and is not expired
nonceCreationTime, nonceFound := r.Nonces.Load(string(*nonceAttr))
if !nonceFound {
r.Nonces.Delete(nonceAttr)
return respondWithNonce(stun.CodeStaleNonce)
}

if timeValue, ok := nonceCreationTime.(time.Time); !ok || time.Since(timeValue) >= nonceLifetime {
r.Nonces.Delete(nonceAttr)
// Assert Nonce is signed and is not expired
if err := r.NonceHash.Validate(nonceAttr.String()); err != nil {
return respondWithNonce(stun.CodeStaleNonce)
}

Expand Down
12 changes: 8 additions & 4 deletions server.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"net"
"sync"
"time"

"github.com/pion/logging"
Expand All @@ -27,7 +26,7 @@ type Server struct {
authHandler AuthHandler
realm string
channelBindTimeout time.Duration
nonces *sync.Map
nonceHash *server.NonceHash

packetConnConfigs []PacketConnConfig
listenerConfigs []ListenerConfig
Expand All @@ -53,14 +52,19 @@ func NewServer(config ServerConfig) (*Server, error) {
mtu = config.InboundMTU
}

nonceHash, err := server.NewNonceHash()
if err != nil {
return nil, err
}

s := &Server{
log: loggerFactory.NewLogger("turn"),
authHandler: config.AuthHandler,
realm: config.Realm,
channelBindTimeout: config.ChannelBindTimeout,
packetConnConfigs: config.PacketConnConfigs,
listenerConfigs: config.ListenerConfigs,
nonces: &sync.Map{},
nonceHash: nonceHash,
inboundMTU: mtu,
}

Expand Down Expand Up @@ -205,7 +209,7 @@ func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manage
Realm: s.realm,
AllocationManager: allocationManager,
ChannelBindTimeout: s.channelBindTimeout,
Nonces: s.nonces,
NonceHash: s.nonceHash,
}); err != nil {
s.log.Errorf("Failed to handle datagram: %v", err)
}
Expand Down

0 comments on commit fb0ab51

Please sign in to comment.