230 lines
9.0 KiB
Go
230 lines
9.0 KiB
Go
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"))
|
|
}
|