Skip to content

Commit c8be609

Browse files
committed
Use signed timestamp for nonce
1 parent d87d0f3 commit c8be609

File tree

8 files changed

+111
-48
lines changed

8 files changed

+111
-48
lines changed

client_test.go

-5
Original file line numberDiff line numberDiff line change
@@ -177,11 +177,6 @@ func TestClientNonceExpiration(t *testing.T) {
177177
allocation, err := client.Allocate()
178178
assert.NoError(t, err)
179179

180-
server.nonces.Range(func(key, value interface{}) bool {
181-
server.nonces.Delete(key)
182-
return true
183-
})
184-
185180
_, err = allocation.WriteTo([]byte{0x00}, &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 8080})
186181
assert.NoError(t, err)
187182

internal/server/errors.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ import "errors"
77

88
var (
99
errFailedToGenerateNonce = errors.New("failed to generate nonce")
10+
errInvalidNonce = errors.New("invalid nonce")
1011
errFailedToSendError = errors.New("failed to send error message")
11-
errDuplicatedNonce = errors.New("duplicated Nonce generated, discarding request")
1212
errNoSuchUser = errors.New("no such user exists")
1313
errUnexpectedClass = errors.New("unexpected class")
1414
errUnexpectedMethod = errors.New("unexpected method")

internal/server/nonce.go

+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package server
5+
6+
import (
7+
"crypto/hmac"
8+
"crypto/rand"
9+
"crypto/sha256"
10+
"encoding/binary"
11+
"encoding/hex"
12+
"fmt"
13+
"time"
14+
)
15+
16+
const (
17+
nonceLifetime = time.Hour // See: https://tools.ietf.org/html/rfc5766#section-4
18+
nonceLength = 40
19+
nonceKeyLength = 64
20+
)
21+
22+
// NewNonceHash creates a NonceHash
23+
func NewNonceHash() (*NonceHash, error) {
24+
key := make([]byte, nonceKeyLength)
25+
if _, err := rand.Read(key); err != nil {
26+
return nil, err
27+
}
28+
29+
return &NonceHash{key}, nil
30+
}
31+
32+
// NonceHash is used to create and verify nonces
33+
type NonceHash struct {
34+
key []byte
35+
}
36+
37+
// Generate a nonce
38+
func (n *NonceHash) Generate() (string, error) {
39+
nonce := make([]byte, 8, nonceLength)
40+
binary.BigEndian.PutUint64(nonce, uint64(time.Now().UnixMilli()))
41+
42+
hash := hmac.New(sha256.New, n.key)
43+
if _, err := hash.Write(nonce[:8]); err != nil {
44+
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err) //nolint:errorlint
45+
}
46+
nonce = hash.Sum(nonce)
47+
48+
return hex.EncodeToString(nonce), nil
49+
}
50+
51+
// Validate checks that nonce is signed and is not expired
52+
func (n *NonceHash) Validate(nonce string) error {
53+
b, err := hex.DecodeString(nonce)
54+
if err != nil || len(b) != nonceLength {
55+
return fmt.Errorf("%w: %v", errInvalidNonce, err) //nolint:errorlint
56+
}
57+
58+
if ts := time.UnixMilli(int64(binary.BigEndian.Uint64(b))); time.Since(ts) > nonceLifetime {
59+
return errInvalidNonce
60+
}
61+
62+
hash := hmac.New(sha256.New, n.key)
63+
if _, err = hash.Write(b[:8]); err != nil {
64+
return fmt.Errorf("%w: %v", errInvalidNonce, err) //nolint:errorlint
65+
}
66+
if !hmac.Equal(b[8:], hash.Sum(nil)) {
67+
return errInvalidNonce
68+
}
69+
70+
return nil
71+
}

internal/server/nonce_test.go

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package server
5+
6+
import (
7+
"testing"
8+
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestNonceHash(t *testing.T) {
13+
t.Run("generated hashes validate", func(t *testing.T) {
14+
h, err := NewNonceHash()
15+
assert.NoError(t, err)
16+
nonce, err := h.Generate()
17+
assert.NoError(t, err)
18+
assert.NoError(t, h.Validate(nonce))
19+
})
20+
}

internal/server/server.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ package server
77
import (
88
"fmt"
99
"net"
10-
"sync"
1110
"time"
1211

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

2625
// Server State
2726
AllocationManager *allocation.Manager
28-
Nonces *sync.Map
27+
NonceHash *NonceHash
2928

3029
// User Configuration
3130
AuthHandler func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool)

internal/server/turn_test.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ package server
88

99
import (
1010
"net"
11-
"sync"
1211
"testing"
1312
"time"
1413

@@ -80,18 +79,21 @@ func TestAllocationLifeTime(t *testing.T) {
8079
})
8180
assert.NoError(t, err)
8281

83-
staticKey := []byte("ABC")
82+
nonceHash, err := NewNonceHash()
83+
assert.NoError(t, err)
84+
staticKey, err := nonceHash.Generate()
85+
assert.NoError(t, err)
86+
8487
r := Request{
8588
AllocationManager: allocationManager,
86-
Nonces: &sync.Map{},
89+
NonceHash: nonceHash,
8790
Conn: l,
8891
SrcAddr: &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 5000},
8992
Log: logger,
9093
AuthHandler: func(username string, realm string, srcAddr net.Addr) (key []byte, ok bool) {
91-
return staticKey, true
94+
return []byte(staticKey), true
9295
},
9396
}
94-
r.Nonces.Store(string(staticKey), time.Now())
9597

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

internal/server/util.go

+3-31
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,10 @@
44
package server
55

66
import (
7-
"crypto/md5" //nolint:gosec,gci
87
"errors"
98
"fmt"
10-
"io"
119
"math/rand"
1210
"net"
13-
"strconv"
1411
"time"
1512

1613
"github.com/pion/stun"
@@ -19,7 +16,6 @@ import (
1916

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

2521
func randSeq(n int) string {
@@ -31,18 +27,6 @@ func randSeq(n int) string {
3127
return string(b)
3228
}
3329

34-
func buildNonce() (string, error) {
35-
/* #nosec */
36-
h := md5.New()
37-
if _, err := io.WriteString(h, strconv.FormatInt(time.Now().Unix(), 10)); err != nil {
38-
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err) //nolint:errorlint
39-
}
40-
if _, err := io.WriteString(h, strconv.FormatInt(rand.Int63(), 10)); err != nil { //nolint:gosec
41-
return "", fmt.Errorf("%w: %v", errFailedToGenerateNonce, err) //nolint:errorlint
42-
}
43-
return fmt.Sprintf("%x", h.Sum(nil)), nil
44-
}
45-
4630
func buildAndSend(conn net.PacketConn, dst net.Addr, attrs ...stun.Setter) error {
4731
msg, err := stun.Build(attrs...)
4832
if err != nil {
@@ -70,16 +54,11 @@ func buildMsg(transactionID [stun.TransactionIDSize]byte, msgType stun.MessageTy
7054

7155
func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method) (stun.MessageIntegrity, bool, error) {
7256
respondWithNonce := func(responseCode stun.ErrorCode) (stun.MessageIntegrity, bool, error) {
73-
nonce, err := buildNonce()
57+
nonce, err := r.NonceHash.Generate()
7458
if err != nil {
7559
return nil, false, err
7660
}
7761

78-
// Nonce has already been taken
79-
if _, keyCollision := r.Nonces.LoadOrStore(nonce, time.Now()); keyCollision {
80-
return nil, false, errDuplicatedNonce
81-
}
82-
8362
return nil, false, buildAndSend(r.Conn, r.SrcAddr, buildMsg(m.TransactionID,
8463
stun.NewType(callingMethod, stun.ClassErrorResponse),
8564
&stun.ErrorCodeAttribute{Code: responseCode},
@@ -101,15 +80,8 @@ func authenticateRequest(r Request, m *stun.Message, callingMethod stun.Method)
10180
return nil, false, buildAndSendErr(r.Conn, r.SrcAddr, err, badRequestMsg...)
10281
}
10382

104-
// Assert Nonce exists and is not expired
105-
nonceCreationTime, nonceFound := r.Nonces.Load(string(*nonceAttr))
106-
if !nonceFound {
107-
r.Nonces.Delete(nonceAttr)
108-
return respondWithNonce(stun.CodeStaleNonce)
109-
}
110-
111-
if timeValue, ok := nonceCreationTime.(time.Time); !ok || time.Since(timeValue) >= nonceLifetime {
112-
r.Nonces.Delete(nonceAttr)
83+
// Assert Nonce is signed and is not expired
84+
if err := r.NonceHash.Validate(nonceAttr.String()); err != nil {
11385
return respondWithNonce(stun.CodeStaleNonce)
11486
}
11587

server.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"errors"
99
"fmt"
1010
"net"
11-
"sync"
1211
"time"
1312

1413
"github.com/pion/logging"
@@ -27,7 +26,7 @@ type Server struct {
2726
authHandler AuthHandler
2827
realm string
2928
channelBindTimeout time.Duration
30-
nonces *sync.Map
29+
nonceHash *server.NonceHash
3130

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

55+
nonceHash, err := server.NewNonceHash()
56+
if err != nil {
57+
return nil, err
58+
}
59+
5660
s := &Server{
5761
log: loggerFactory.NewLogger("turn"),
5862
authHandler: config.AuthHandler,
5963
realm: config.Realm,
6064
channelBindTimeout: config.ChannelBindTimeout,
6165
packetConnConfigs: config.PacketConnConfigs,
6266
listenerConfigs: config.ListenerConfigs,
63-
nonces: &sync.Map{},
67+
nonceHash: nonceHash,
6468
inboundMTU: mtu,
6569
}
6670

@@ -198,7 +202,7 @@ func (s *Server) readLoop(p net.PacketConn, allocationManager *allocation.Manage
198202
Realm: s.realm,
199203
AllocationManager: allocationManager,
200204
ChannelBindTimeout: s.channelBindTimeout,
201-
Nonces: s.nonces,
205+
NonceHash: s.nonceHash,
202206
}); err != nil {
203207
s.log.Errorf("Failed to handle datagram: %v", err)
204208
}

0 commit comments

Comments
 (0)