Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use signed timestamp for nonce #372

Merged
merged 1 commit into from
Feb 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
34 changes: 3 additions & 31 deletions internal/server/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@
package server

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

"github.com/pion/stun"
Expand All @@ -19,7 +16,6 @@ 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 randSeq(n int) string {
Expand All @@ -31,18 +27,6 @@ func randSeq(n int) string {
return string(b)
}

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
}
if _, err := io.WriteString(h, strconv.FormatInt(rand.Int63(), 10)); 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 +54,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 +80,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 @@ -198,7 +202,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
Loading