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 #371

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
}

Check warning on line 27 in internal/server/nonce.go

View check run for this annotation

Codecov / codecov/patch

internal/server/nonce.go#L26-L27

Added lines #L26 - L27 were not covered by tests

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
}

Check warning on line 45 in internal/server/nonce.go

View check run for this annotation

Codecov / codecov/patch

internal/server/nonce.go#L44-L45

Added lines #L44 - L45 were not covered by tests
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
}

Check warning on line 56 in internal/server/nonce.go

View check run for this annotation

Codecov / codecov/patch

internal/server/nonce.go#L55-L56

Added lines #L55 - L56 were not covered by tests

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

Check warning on line 60 in internal/server/nonce.go

View check run for this annotation

Codecov / codecov/patch

internal/server/nonce.go#L59-L60

Added lines #L59 - L60 were not covered by tests

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

Check warning on line 65 in internal/server/nonce.go

View check run for this annotation

Codecov / codecov/patch

internal/server/nonce.go#L64-L65

Added lines #L64 - L65 were not covered by tests
if !hmac.Equal(b[8:], hash.Sum(nil)) {
return errInvalidNonce
}

Check warning on line 68 in internal/server/nonce.go

View check run for this annotation

Codecov / codecov/patch

internal/server/nonce.go#L67-L68

Added lines #L67 - L68 were not covered by tests

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 @@
"errors"
"fmt"
"net"
"sync"
"time"

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

packetConnConfigs []PacketConnConfig
listenerConfigs []ListenerConfig
Expand All @@ -53,14 +52,19 @@
mtu = config.InboundMTU
}

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

Check warning on line 58 in server.go

View check run for this annotation

Codecov / codecov/patch

server.go#L57-L58

Added lines #L57 - L58 were not covered by tests

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 @@
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