snapshot of https://github.com/centrifugal/centrifugo.git (master, 2026-09-16; blobs > 131072B omitted)
This commit is contained in:
19
internal/client/client.go
Normal file
19
internal/client/client.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
)
|
||||
|
||||
// Client represents client connection.
|
||||
type Client interface {
|
||||
ID() string
|
||||
UserID() string
|
||||
IsSubscribed(string) bool
|
||||
Context() context.Context
|
||||
Transport() centrifuge.TransportInfo
|
||||
AcquireStorage() (map[string]any, func(map[string]any))
|
||||
Unsubscribe(ch string, unsubscribe ...centrifuge.Unsubscribe)
|
||||
WritePublication(channel string, publication *centrifuge.Publication, sp centrifuge.StreamPosition) error
|
||||
}
|
||||
1332
internal/client/handler.go
Normal file
1332
internal/client/handler.go
Normal file
File diff suppressed because it is too large
Load Diff
1876
internal/client/handler_test.go
Normal file
1876
internal/client/handler_test.go
Normal file
File diff suppressed because it is too large
Load Diff
117
internal/client/track_signature.go
Normal file
117
internal/client/track_signature.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"hash"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const gracePeriodSeconds = 5
|
||||
|
||||
func parseSignatureTimestamps(sig string) (iat int64, expiry int64) {
|
||||
first := strings.IndexByte(sig, ':')
|
||||
if first < 0 {
|
||||
return 0, 0
|
||||
}
|
||||
rest := sig[first+1:]
|
||||
second := strings.IndexByte(rest, ':')
|
||||
if second < 0 {
|
||||
return 0, 0
|
||||
}
|
||||
iatVal, err := strconv.ParseInt(sig[:first], 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
expiryVal, err := strconv.ParseInt(rest[:second], 10, 64)
|
||||
if err != nil {
|
||||
return 0, 0
|
||||
}
|
||||
return iatVal, expiryVal
|
||||
}
|
||||
|
||||
// trackSignatureVerifier reuses HMAC hashers and buffers for signature verification.
|
||||
// Thread-safe via sync.Pool.
|
||||
type trackSignatureVerifier struct {
|
||||
macPool sync.Pool
|
||||
bufPool sync.Pool
|
||||
}
|
||||
|
||||
type verifyBufs struct {
|
||||
payload []byte
|
||||
hexBuf []byte
|
||||
expectedHex []byte
|
||||
}
|
||||
|
||||
func newTrackSignatureVerifier(secret string) *trackSignatureVerifier {
|
||||
return &trackSignatureVerifier{
|
||||
macPool: sync.Pool{
|
||||
New: func() any {
|
||||
return hmac.New(sha256.New, []byte(secret))
|
||||
},
|
||||
},
|
||||
bufPool: sync.Pool{
|
||||
New: func() any {
|
||||
return &verifyBufs{
|
||||
payload: make([]byte, 0, 256),
|
||||
hexBuf: make([]byte, hex.EncodedLen(sha256.Size)),
|
||||
expectedHex: make([]byte, hex.EncodedLen(sha256.Size)),
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func verifyTrackSignature(secret string, channel string, sig string, keys []string, userID string) bool {
|
||||
return newTrackSignatureVerifier(secret).verify(channel, sig, keys, userID)
|
||||
}
|
||||
|
||||
func (v *trackSignatureVerifier) verify(channel string, sig string, keys []string, userID string) bool {
|
||||
first := strings.IndexByte(sig, ':')
|
||||
if first < 0 {
|
||||
return false
|
||||
}
|
||||
rest := sig[first+1:]
|
||||
second := strings.IndexByte(rest, ':')
|
||||
if second < 0 {
|
||||
return false
|
||||
}
|
||||
iatStr := sig[:first]
|
||||
expiryStr := rest[:second]
|
||||
hmacHex := rest[second+1:]
|
||||
|
||||
keysHash := sha256.Sum256([]byte(strings.Join(keys, "\x00")))
|
||||
|
||||
mac := v.macPool.Get().(hash.Hash)
|
||||
bufs := v.bufPool.Get().(*verifyBufs)
|
||||
|
||||
// Payload fields are NUL-separated rather than ':'-separated so colons
|
||||
// inside userID or channel (e.g. "news:tech", "alice:org") can't shift
|
||||
// between fields and yield a colliding HMAC for a different (userID,
|
||||
// channel) tuple. The outer signature string itself stays ':'-separated
|
||||
// because its fields (iat / exp / hmac_hex) are colon-free by construction.
|
||||
mac.Reset()
|
||||
bufs.payload = bufs.payload[:0]
|
||||
bufs.payload = append(bufs.payload, iatStr...)
|
||||
bufs.payload = append(bufs.payload, 0)
|
||||
bufs.payload = append(bufs.payload, expiryStr...)
|
||||
bufs.payload = append(bufs.payload, 0)
|
||||
bufs.payload = append(bufs.payload, userID...)
|
||||
bufs.payload = append(bufs.payload, 0)
|
||||
bufs.payload = append(bufs.payload, channel...)
|
||||
bufs.payload = append(bufs.payload, 0)
|
||||
hex.Encode(bufs.hexBuf, keysHash[:])
|
||||
bufs.payload = append(bufs.payload, bufs.hexBuf...)
|
||||
mac.Write(bufs.payload)
|
||||
hex.Encode(bufs.expectedHex, mac.Sum(nil))
|
||||
|
||||
result := hmac.Equal([]byte(hmacHex), bufs.expectedHex)
|
||||
|
||||
v.macPool.Put(mac)
|
||||
v.bufPool.Put(bufs)
|
||||
|
||||
return result
|
||||
}
|
||||
332
internal/client/track_signature_bench_test.go
Normal file
332
internal/client/track_signature_bench_test.go
Normal file
@@ -0,0 +1,332 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/cristalhq/jwt/v5"
|
||||
sjson "github.com/segmentio/encoding/json"
|
||||
)
|
||||
|
||||
func generateKeys(n int) []string {
|
||||
keys := make([]string, n)
|
||||
for i := range keys {
|
||||
keys[i] = fmt.Sprintf("item_%05d", i)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// HMAC signature: create.
|
||||
func createTrackSignature(secret, channel string, keys []string, userID string, iat, expiry int64) string {
|
||||
keysHash := sha256.Sum256([]byte(strings.Join(keys, "\x00")))
|
||||
// Inner payload uses NUL separators; outer signature stays ':'-separated.
|
||||
payload := fmt.Sprintf("%d\x00%d\x00%s\x00%s\x00%x", iat, expiry, userID, channel, keysHash)
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(payload))
|
||||
return fmt.Sprintf("%d:%d:%x", iat, expiry, mac.Sum(nil))
|
||||
}
|
||||
|
||||
// JWT: claims struct that includes keys.
|
||||
type trackClaims struct {
|
||||
jwt.RegisteredClaims
|
||||
Channel string `json:"channel"`
|
||||
Keys []string `json:"keys"`
|
||||
}
|
||||
|
||||
func BenchmarkTrackSignature_HMAC_Create_10kKeys(b *testing.B) {
|
||||
keys := generateKeys(10_000)
|
||||
secret := "bench-secret-key"
|
||||
channel := "post_votes:feed1"
|
||||
userID := "user123"
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
createTrackSignature(secret, channel, keys, userID, now, expiry)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrackSignature_HMAC_Create_Reuse_10kKeys(b *testing.B) {
|
||||
keys := generateKeys(10_000)
|
||||
secret := "bench-secret-key"
|
||||
channel := "post_votes:feed1"
|
||||
userID := "user123"
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
|
||||
// Pre-compute keys hash.
|
||||
keysHash := sha256.Sum256([]byte(strings.Join(keys, "\x00")))
|
||||
|
||||
// Reuse HMAC hasher and buffers.
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
payload := make([]byte, 0, 256)
|
||||
result := make([]byte, 0, 128)
|
||||
hexBuf := make([]byte, hex.EncodedLen(sha256.Size))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
mac.Reset()
|
||||
// Inner payload uses NUL separators; outer signature stays ':'-separated.
|
||||
payload = payload[:0]
|
||||
payload = strconv.AppendInt(payload, now, 10)
|
||||
payload = append(payload, 0)
|
||||
payload = strconv.AppendInt(payload, expiry, 10)
|
||||
payload = append(payload, 0)
|
||||
payload = append(payload, userID...)
|
||||
payload = append(payload, 0)
|
||||
payload = append(payload, channel...)
|
||||
payload = append(payload, 0)
|
||||
hex.Encode(hexBuf, keysHash[:])
|
||||
payload = append(payload, hexBuf...)
|
||||
mac.Write(payload)
|
||||
|
||||
result = result[:0]
|
||||
result = strconv.AppendInt(result, now, 10)
|
||||
result = append(result, ':')
|
||||
result = strconv.AppendInt(result, expiry, 10)
|
||||
result = append(result, ':')
|
||||
hex.Encode(hexBuf, mac.Sum(nil))
|
||||
result = append(result, hexBuf...)
|
||||
_ = string(result)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrackSignature_HMAC_Verify_10kKeys(b *testing.B) {
|
||||
keys := generateKeys(10_000)
|
||||
secret := "bench-secret-key"
|
||||
channel := "post_votes:feed1"
|
||||
userID := "user123"
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := createTrackSignature(secret, channel, keys, userID, now, expiry)
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
if !verifyTrackSignature(secret, channel, sig, keys, userID) {
|
||||
b.Fatal("verification failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrackSignature_HMAC_Verify_Reuse_10kKeys(b *testing.B) {
|
||||
keys := generateKeys(10_000)
|
||||
secret := "bench-secret-key"
|
||||
channel := "post_votes:feed1"
|
||||
userID := "user123"
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := createTrackSignature(secret, channel, keys, userID, now, expiry)
|
||||
|
||||
// Pre-compute keys hash.
|
||||
keysHash := sha256.Sum256([]byte(strings.Join(keys, "\x00")))
|
||||
|
||||
// Reuse HMAC hasher and buffers.
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
payload := make([]byte, 0, 256)
|
||||
hexBuf := make([]byte, hex.EncodedLen(sha256.Size))
|
||||
expectedHex := make([]byte, hex.EncodedLen(sha256.Size))
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
// Parse iat:expiry:hmac from signature.
|
||||
first := strings.IndexByte(sig, ':')
|
||||
rest := sig[first+1:]
|
||||
second := strings.IndexByte(rest, ':')
|
||||
iatStr := sig[:first]
|
||||
expiryStr := rest[:second]
|
||||
hmacHex := rest[second+1:]
|
||||
|
||||
mac.Reset()
|
||||
// Inner payload uses NUL separators; outer signature parsed with ':' above.
|
||||
payload = payload[:0]
|
||||
payload = append(payload, iatStr...)
|
||||
payload = append(payload, 0)
|
||||
payload = append(payload, expiryStr...)
|
||||
payload = append(payload, 0)
|
||||
payload = append(payload, userID...)
|
||||
payload = append(payload, 0)
|
||||
payload = append(payload, channel...)
|
||||
payload = append(payload, 0)
|
||||
hex.Encode(hexBuf, keysHash[:])
|
||||
payload = append(payload, hexBuf...)
|
||||
mac.Write(payload)
|
||||
hex.Encode(expectedHex, mac.Sum(nil))
|
||||
|
||||
if !hmac.Equal([]byte(hmacHex), expectedHex) {
|
||||
b.Fatal("verification failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrackSignature_JWT_Create_10kKeys(b *testing.B) {
|
||||
keys := generateKeys(10_000)
|
||||
secret := "bench-secret-key"
|
||||
channel := "post_votes:feed1"
|
||||
userID := "user123"
|
||||
now := time.Now()
|
||||
expiry := now.Add(time.Hour)
|
||||
|
||||
signer, err := jwt.NewSignerHS(jwt.HS256, []byte(secret))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
builder := jwt.NewBuilder(signer)
|
||||
|
||||
claims := &trackClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: userID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(expiry),
|
||||
},
|
||||
Channel: channel,
|
||||
Keys: keys,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := builder.Build(claims)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrackSignature_JWT_Create_Segmentio_10kKeys(b *testing.B) {
|
||||
keys := generateKeys(10_000)
|
||||
secret := "bench-secret-key"
|
||||
channel := "post_votes:feed1"
|
||||
userID := "user123"
|
||||
now := time.Now()
|
||||
expiry := now.Add(time.Hour)
|
||||
|
||||
signer, err := jwt.NewSignerHS(jwt.HS256, []byte(secret))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
builder := jwt.NewBuilder(signer)
|
||||
|
||||
claims := &trackClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: userID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(expiry),
|
||||
},
|
||||
Channel: channel,
|
||||
Keys: keys,
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
rawClaims, err := sjson.Marshal(claims)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
_, err = builder.Build(rawClaims)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrackSignature_JWT_Verify_10kKeys(b *testing.B) {
|
||||
keys := generateKeys(10_000)
|
||||
secret := "bench-secret-key"
|
||||
channel := "post_votes:feed1"
|
||||
userID := "user123"
|
||||
now := time.Now()
|
||||
expiry := now.Add(time.Hour)
|
||||
|
||||
signer, err := jwt.NewSignerHS(jwt.HS256, []byte(secret))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
builder := jwt.NewBuilder(signer)
|
||||
|
||||
claims := &trackClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: userID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(expiry),
|
||||
},
|
||||
Channel: channel,
|
||||
Keys: keys,
|
||||
}
|
||||
|
||||
token, err := builder.Build(claims)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
tokenBytes := token.Bytes()
|
||||
|
||||
verifier, err := jwt.NewVerifierHS(jwt.HS256, []byte(secret))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
parsedToken, err := jwt.Parse(tokenBytes, verifier)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
var parsed trackClaims
|
||||
if err := parsedToken.DecodeClaims(&parsed); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkTrackSignature_JWT_Verify_Segmentio_10kKeys(b *testing.B) {
|
||||
keys := generateKeys(10_000)
|
||||
secret := "bench-secret-key"
|
||||
channel := "post_votes:feed1"
|
||||
userID := "user123"
|
||||
now := time.Now()
|
||||
expiry := now.Add(time.Hour)
|
||||
|
||||
signer, err := jwt.NewSignerHS(jwt.HS256, []byte(secret))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
builder := jwt.NewBuilder(signer)
|
||||
|
||||
claims := &trackClaims{
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
Subject: userID,
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
ExpiresAt: jwt.NewNumericDate(expiry),
|
||||
},
|
||||
Channel: channel,
|
||||
Keys: keys,
|
||||
}
|
||||
|
||||
token, err := builder.Build(claims)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
tokenBytes := token.Bytes()
|
||||
|
||||
verifier, err := jwt.NewVerifierHS(jwt.HS256, []byte(secret))
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
parsedToken, err := jwt.Parse(tokenBytes, verifier)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
var parsed trackClaims
|
||||
if _, err := sjson.Parse(parsedToken.Claims(), &parsed, sjson.ZeroCopy); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
229
internal/client/track_signature_test.go
Normal file
229
internal/client/track_signature_test.go
Normal file
@@ -0,0 +1,229 @@
|
||||
package client
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
const testSecret = "test-secret-key"
|
||||
|
||||
func makeTestSignature(secret, channel string, keys []string, userID string, iat, expiry int64) string {
|
||||
keysHash := sha256.Sum256([]byte(strings.Join(keys, "\x00")))
|
||||
// Inner payload uses NUL separators so colons in userID/channel can't
|
||||
// shift fields. Outer signature stays ':'-separated (colon-free fields).
|
||||
payload := fmt.Sprintf("%d\x00%d\x00%s\x00%s\x00%x", iat, expiry, userID, channel, keysHash)
|
||||
mac := hmac.New(sha256.New, []byte(secret))
|
||||
mac.Write([]byte(payload))
|
||||
return fmt.Sprintf("%d:%d:%x", iat, expiry, mac.Sum(nil))
|
||||
}
|
||||
|
||||
func TestTrackSignature_ValidHMAC(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"key1", "key2"}, "user1", now, expiry)
|
||||
require.True(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"key1", "key2"}, "user1"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_InvalidHMAC(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"key1", "key2"}, "user1", now, expiry)
|
||||
// Tamper with the signature by flipping its last hex character to a
|
||||
// different one. Replacing it with a fixed value would be a no-op when the
|
||||
// signature already happens to end in that value (~1/256 of runs), which made
|
||||
// this test flaky.
|
||||
repl := byte('f')
|
||||
if sig[len(sig)-1] == 'f' {
|
||||
repl = 'e'
|
||||
}
|
||||
sig = sig[:len(sig)-1] + string(repl)
|
||||
require.False(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"key1", "key2"}, "user1"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_ExpiredSignature(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
iat := now - 7200
|
||||
expiry := now - 3600 // Expired 1 hour ago.
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"key1"}, "user1", iat, expiry)
|
||||
|
||||
// Signature is valid HMAC-wise but the caller (OnTrack) would check expiry.
|
||||
// verifyTrackSignature itself only checks HMAC, not expiry.
|
||||
require.True(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"key1"}, "user1"))
|
||||
|
||||
// But parseSignatureTimestamps should return the expired value.
|
||||
_, parsedExpiry := parseSignatureTimestamps(sig)
|
||||
require.True(t, parsedExpiry < now)
|
||||
}
|
||||
|
||||
func TestTrackSignature_DifferentKeys(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"keyA", "keyB"}, "user1", now, expiry)
|
||||
// Client sends different keys — should fail.
|
||||
require.False(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"keyA", "keyC"}, "user1"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_DifferentUser(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"key1"}, "user1", now, expiry)
|
||||
// Different user — should fail.
|
||||
require.False(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"key1"}, "user2"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_AnonymousUser(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"key1"}, "", now, expiry)
|
||||
require.True(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"key1"}, ""))
|
||||
}
|
||||
|
||||
func TestTrackSignature_KeyOrderSensitive(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
// Sign with keys [B, A].
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"keyB", "keyA"}, "user1", now, expiry)
|
||||
// Verify with keys in the same order — succeeds.
|
||||
require.True(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"keyB", "keyA"}, "user1"))
|
||||
// Verify with keys in a different order — fails. Signature is bound to wire order, not set membership.
|
||||
require.False(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"keyA", "keyB"}, "user1"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_EmptySignature(t *testing.T) {
|
||||
require.False(t, verifyTrackSignature(testSecret, "test:channel", "", []string{"key1"}, "user1"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_MalformedSignature(t *testing.T) {
|
||||
require.False(t, verifyTrackSignature(testSecret, "test:channel", "not-a-valid-sig", []string{"key1"}, "user1"))
|
||||
require.False(t, verifyTrackSignature(testSecret, "test:channel", "123", []string{"key1"}, "user1"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_WrongSecret(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"key1"}, "user1", now, expiry)
|
||||
require.False(t, verifyTrackSignature("wrong-secret", "test:channel", sig, []string{"key1"}, "user1"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_WrongChannel(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"key1"}, "user1", now, expiry)
|
||||
require.False(t, verifyTrackSignature(testSecret, "other:channel", sig, []string{"key1"}, "user1"))
|
||||
}
|
||||
|
||||
func TestParseSignatureTimestamps(t *testing.T) {
|
||||
iat, expiry := parseSignatureTimestamps("1000:2000:abcdef")
|
||||
require.Equal(t, int64(1000), iat)
|
||||
require.Equal(t, int64(2000), expiry)
|
||||
}
|
||||
|
||||
func TestParseSignatureTimestamps_Invalid(t *testing.T) {
|
||||
tests := []string{
|
||||
"",
|
||||
"abc",
|
||||
"1000",
|
||||
"1000:",
|
||||
"1000:abc:def",
|
||||
"abc:2000:def",
|
||||
}
|
||||
for _, sig := range tests {
|
||||
iat, expiry := parseSignatureTimestamps(sig)
|
||||
require.Equal(t, int64(0), iat, "sig=%q", sig)
|
||||
require.Equal(t, int64(0), expiry, "sig=%q", sig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrackSignature_SingleKey(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "ch", []string{"only-key"}, "u", now, expiry)
|
||||
require.True(t, verifyTrackSignature(testSecret, "ch", sig, []string{"only-key"}, "u"))
|
||||
}
|
||||
|
||||
func TestTrackSignature_ManyKeys(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
keys := make([]string, 100)
|
||||
for i := range keys {
|
||||
keys[i] = fmt.Sprintf("key-%03d", i)
|
||||
}
|
||||
sig := makeTestSignature(testSecret, "ch", keys, "user1", now, expiry)
|
||||
require.True(t, verifyTrackSignature(testSecret, "ch", sig, keys, "user1"))
|
||||
}
|
||||
|
||||
func TestParseSignatureTimestamps_RealSignature(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
sig := makeTestSignature(testSecret, "ch", []string{"k"}, "u", now, expiry)
|
||||
parsedIat, parsedExpiry := parseSignatureTimestamps(sig)
|
||||
require.Equal(t, now, parsedIat)
|
||||
require.Equal(t, expiry, parsedExpiry)
|
||||
}
|
||||
|
||||
func TestTrackSignature_GracePeriod(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
// Expired 3 seconds ago — within 5-second grace period.
|
||||
iat := now - 3600
|
||||
expiry := now - 3
|
||||
sig := makeTestSignature(testSecret, "test:channel", []string{"key1"}, "user1", iat, expiry)
|
||||
|
||||
// HMAC is valid (verifyTrackSignature doesn't check expiry).
|
||||
require.True(t, verifyTrackSignature(testSecret, "test:channel", sig, []string{"key1"}, "user1"))
|
||||
|
||||
// The caller would check: expiry + gracePeriodSeconds >= now
|
||||
_, parsedExpiry := parseSignatureTimestamps(sig)
|
||||
withinGracePeriod := parsedExpiry+gracePeriodSeconds >= now
|
||||
require.True(t, withinGracePeriod)
|
||||
|
||||
// Outside grace period.
|
||||
iat2 := now - 7200
|
||||
expiry2 := now - 60 // Expired 60 seconds ago, past grace period.
|
||||
sig2 := makeTestSignature(testSecret, "test:channel", []string{"key1"}, "user1", iat2, expiry2)
|
||||
_, parsedExpiry2 := parseSignatureTimestamps(sig2)
|
||||
withinGracePeriod2 := parsedExpiry2+gracePeriodSeconds >= now
|
||||
require.False(t, withinGracePeriod2)
|
||||
}
|
||||
|
||||
func TestVerifyTrackSignature_HexEncoding(t *testing.T) {
|
||||
// Verify that the expected hex encoding matches between make and verify.
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
keys := []string{"key1"}
|
||||
keysHash := sha256.Sum256([]byte(strings.Join(keys, "\x00")))
|
||||
payload := fmt.Sprintf("%d\x00%d\x00%s\x00%s\x00%x", now, expiry, "u", "ch", keysHash)
|
||||
mac := hmac.New(sha256.New, []byte(testSecret))
|
||||
mac.Write([]byte(payload))
|
||||
expectedHex := hex.EncodeToString(mac.Sum(nil))
|
||||
|
||||
sig := fmt.Sprintf("%d:%d:%s", now, expiry, expectedHex)
|
||||
require.True(t, verifyTrackSignature(testSecret, "ch", sig, keys, "u"))
|
||||
}
|
||||
|
||||
// TestTrackSignature_ColonNoAmbiguity is a regression test for the previous
|
||||
// payload format that used ':' between every field. Under that format, a
|
||||
// signature issued for (user="alice", channel="news:tech") and one issued
|
||||
// for (user="alice:news", channel="tech") produced byte-identical payloads
|
||||
// and therefore interchangeable signatures. NUL separation eliminates this.
|
||||
func TestTrackSignature_ColonNoAmbiguity(t *testing.T) {
|
||||
now := time.Now().Unix()
|
||||
expiry := now + 3600
|
||||
keys := []string{"key1"}
|
||||
|
||||
sigA := makeTestSignature(testSecret, "news:tech", keys, "alice", now, expiry)
|
||||
sigB := makeTestSignature(testSecret, "tech", keys, "alice:news", now, expiry)
|
||||
require.NotEqual(t, sigA, sigB, "colon-overlap signatures must differ under NUL-separated payload")
|
||||
|
||||
// Signature A must NOT verify against B's (user, channel) tuple — proves
|
||||
// the two are independent under the new format.
|
||||
require.False(t, verifyTrackSignature(testSecret, "tech", sigA, keys, "alice:news"))
|
||||
// And vice versa.
|
||||
require.False(t, verifyTrackSignature(testSecret, "news:tech", sigB, keys, "alice"))
|
||||
}
|
||||
Reference in New Issue
Block a user