@@ -1513,6 +1513,134 @@ func TestSharedPollRefreshProxyDispatch_FallbackToDefault(t *testing.T) {
15131513 require .Equal (t , 1 , defaultCalls )
15141514}
15151515
1516+ // TestOnTrackPreviousSecretKeyRotation checks that shared_poll
1517+ // hmac_previous_secret_key_valid_until bounds the previous key usage in real
1518+ // server time. The iat inside a signature is chosen by whoever minted it, so a
1519+ // holder of the rotated-out key must not be able to keep minting accepted
1520+ // signatures after the cutoff by backdating iat.
1521+ func TestOnTrackPreviousSecretKeyRotation (t * testing.T ) {
1522+ const (
1523+ currentKey = "current-secret-key"
1524+ prevKey = "previous-secret-key"
1525+ channel = "shared:poll"
1526+ userID = "42"
1527+ )
1528+ keys := []string {"key1" , "key2" }
1529+
1530+ newHandlerWithClient := func (t * testing.T , validUntil int64 ) (* Handler , * centrifuge.Client , func ()) {
1531+ t .Helper ()
1532+ node := tools .NodeWithMemoryEngineNoHandlers ()
1533+
1534+ cfg := config .DefaultConfig ()
1535+ cfg .SharedPoll .HMACSecretKey = currentKey
1536+ cfg .SharedPoll .HMACPreviousSecretKey = prevKey
1537+ cfg .SharedPoll .HMACPreviousSecretKeyValidUntil = validUntil
1538+ cfg .Channel .Proxy .SharedPollRefresh .Endpoint = "http://localhost:9999"
1539+ cfg .Channel .Namespaces = []configtypes.ChannelNamespace {
1540+ {
1541+ Name : "shared" ,
1542+ ChannelOptions : configtypes.ChannelOptions {SubscriptionType : "shared_poll" },
1543+ },
1544+ }
1545+ cfgContainer , err := config .NewContainer (cfg )
1546+ require .NoError (t , err )
1547+ h := NewHandler (node , cfgContainer , hmacJWTVerifier (t , cfgContainer ), nil , & ProxyMap {})
1548+
1549+ node .OnConnecting (func (ctx context.Context , event centrifuge.ConnectEvent ) (centrifuge.ConnectReply , error ) {
1550+ return centrifuge.ConnectReply {Credentials : & centrifuge.Credentials {UserID : userID }}, nil
1551+ })
1552+
1553+ transport := tools .NewTestTransport ()
1554+ client , closeFn , err := centrifuge .NewClient (context .Background (), node , transport )
1555+ require .NoError (t , err )
1556+
1557+ encoder := protocol .NewJSONCommandEncoder ()
1558+ data , err := encoder .Encode (& protocol.Command {Id : 1 , Connect : & protocol.ConnectRequest {}})
1559+ require .NoError (t , err )
1560+ require .True (t , centrifuge .HandleReadFrame (client , bytes .NewReader (data ), 1 << 20 ))
1561+
1562+ return h , client , func () {
1563+ _ = closeFn ()
1564+ _ = node .Shutdown (context .Background ())
1565+ }
1566+ }
1567+
1568+ track := func (h * Handler , client * centrifuge.Client , sig string ) (centrifuge.TrackReply , error ) {
1569+ return h .OnTrack (client , centrifuge.TrackEvent {
1570+ Channel : channel ,
1571+ Batches : []centrifuge.TrackBatch {{
1572+ Items : []centrifuge.TrackItem {{Key : keys [0 ]}, {Key : keys [1 ]}},
1573+ Signature : sig ,
1574+ }},
1575+ })
1576+ }
1577+
1578+ now := time .Now ().Unix ()
1579+
1580+ t .Run ("previous key accepted inside grace period" , func (t * testing.T ) {
1581+ validUntil := now + 3600
1582+ h , client , cleanup := newHandlerWithClient (t , validUntil )
1583+ defer cleanup ()
1584+
1585+ sig := makeTestSignature (prevKey , channel , keys , userID , now - 60 , now + 300 )
1586+ reply , err := track (h , client , sig )
1587+ require .NoError (t , err )
1588+ require .Len (t , reply .Batches , 1 )
1589+ require .Equal (t , now + 300 , reply .Batches [0 ].ExpireAt )
1590+ })
1591+
1592+ t .Run ("previous key rejected for iat after cutoff" , func (t * testing.T ) {
1593+ validUntil := now + 3600
1594+ h , client , cleanup := newHandlerWithClient (t , validUntil )
1595+ defer cleanup ()
1596+
1597+ sig := makeTestSignature (prevKey , channel , keys , userID , validUntil + 1 , now + 300 )
1598+ _ , err := track (h , client , sig )
1599+ require .Equal (t , centrifuge .ErrorPermissionDenied , err )
1600+ })
1601+
1602+ t .Run ("previous key rejected after cutoff passed" , func (t * testing.T ) {
1603+ validUntil := now - 3600
1604+ h , client , cleanup := newHandlerWithClient (t , validUntil )
1605+ defer cleanup ()
1606+
1607+ // Honest signature – iat tells the truth about when it was minted.
1608+ sig := makeTestSignature (prevKey , channel , keys , userID , now , now + 300 )
1609+ _ , err := track (h , client , sig )
1610+ require .Equal (t , centrifuge .ErrorPermissionDenied , err )
1611+
1612+ // Backdated signature – minted now but claiming an iat before the cutoff.
1613+ // Must be rejected too, otherwise the cutoff protects nothing against
1614+ // someone who retained the rotated-out key.
1615+ sig = makeTestSignature (prevKey , channel , keys , userID , validUntil - 10 , now + 300 )
1616+ _ , err = track (h , client , sig )
1617+ require .Equal (t , centrifuge .ErrorPermissionDenied , err )
1618+ })
1619+
1620+ t .Run ("current key still accepted after cutoff passed" , func (t * testing.T ) {
1621+ validUntil := now - 3600
1622+ h , client , cleanup := newHandlerWithClient (t , validUntil )
1623+ defer cleanup ()
1624+
1625+ sig := makeTestSignature (currentKey , channel , keys , userID , now , now + 300 )
1626+ reply , err := track (h , client , sig )
1627+ require .NoError (t , err )
1628+ require .Len (t , reply .Batches , 1 )
1629+ require .Equal (t , now + 300 , reply .Batches [0 ].ExpireAt )
1630+ })
1631+
1632+ t .Run ("previous key accepted when no cutoff configured" , func (t * testing.T ) {
1633+ h , client , cleanup := newHandlerWithClient (t , 0 )
1634+ defer cleanup ()
1635+
1636+ sig := makeTestSignature (prevKey , channel , keys , userID , now , now + 300 )
1637+ reply , err := track (h , client , sig )
1638+ require .NoError (t , err )
1639+ require .Len (t , reply .Batches , 1 )
1640+ require .Equal (t , now + 300 , reply .Batches [0 ].ExpireAt )
1641+ })
1642+ }
1643+
15161644func TestSingleConnection (t * testing.T ) {
15171645 node := tools .NodeWithMemoryEngineNoHandlers ()
15181646 defer func () { _ = node .Shutdown (context .Background ()) }()
0 commit comments