Files
netbird/shared/management/certposture/challenge_test.go
T

109 lines
4.6 KiB
Go

package certposture
import (
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResolveWindow(t *testing.T) {
// An end-to-end run shortens the window so a renewal can be watched in seconds
// rather than half a day. Anything it cannot make sense of leaves the default in
// place, because a window nobody intended is a security property nobody chose.
tests := []struct {
name string
env string
want time.Duration
}{
{name: "unset keeps the default", env: "", want: Window},
{name: "a test-sized window is taken", env: "30s", want: 30 * time.Second},
{name: "garbage keeps the default", env: "soon", want: Window},
{name: "below the floor keeps the default", env: "10ms", want: Window},
{name: "too short for the refresh tick keeps the default", env: "1s", want: Window},
{name: "a fractional second keeps the default", env: "30500ms", want: Window},
{name: "above the ceiling keeps the default", env: "100h", want: Window},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv(EnvWindow, tt.env)
if tt.env == "" {
require.NoError(t, os.Unsetenv(EnvWindow))
}
assert.Equal(t, tt.want, resolveWindow())
})
}
}
func TestChallenger_HonoursAShortenedWindow(t *testing.T) {
// The window is what a nonce is stamped with, so a shortened one has to make a
// nonce expire sooner, not just change a number in a log line.
short := 2 * time.Second
c := &Challenger{secret: []byte("secret"), window: short}
peerKey := []byte("peer-key")
issued := time.Unix(1_790_000_000, 0)
nonce := c.Nonce(peerKey, issued)
require.NoError(t, c.verifyNonce(nonce, peerKey, issued), "a nonce is valid when issued")
require.NoError(t, c.verifyNonce(nonce, peerKey, issued.Add(short)), "and through the window after it")
assert.ErrorIs(t, c.verifyNonce(nonce, peerKey, issued.Add(3*short)), ErrNonceExpired,
"a shortened window must actually expire the nonce sooner")
}
// heldNonceAlwaysAccepted walks ten windows and, at every step, checks the nonce the peer
// would be holding if it were re-stamped every period starting at offset. It returns how
// many of those checks would have rejected the peer.
func heldNonceAlwaysAccepted(c *Challenger, peerKey []byte, period, offset time.Duration) int {
start := time.Unix(0, 0).UTC().Add(offset)
var rejected int
for elapsed := time.Duration(0); elapsed < 10*Window; elapsed += 7 * time.Minute {
periods := int64(elapsed / period)
lastRefresh := start.Add(time.Duration(periods) * period)
if c.verifyNonce(c.Nonce(peerKey, lastRefresh), peerKey, start.Add(elapsed)) != nil {
rejected++
}
}
return rejected
}
// TestNonce_StaysValidWhenRestampedWithinAWindow is the reason management keeps no per-peer
// nonce state. A nonce carries the window it was minted in, not the instant, and is accepted
// for that window and the next. A peer re-stamped at least once per window therefore can
// never be holding one that has fallen outside the accepted pair, whoever it is and whenever
// it was last served. There is nothing to track and nothing to search for: the guarantee
// comes from the cadence alone.
//
// The offsets matter: each account is given a phase of its own so a restart does not fan out
// to every account at once, so the property has to hold off the window boundary too.
func TestNonce_StaysValidWhenRestampedWithinAWindow(t *testing.T) {
c := &Challenger{secret: []byte("secret"), window: Window}
peerKey := []byte("peer-key")
for _, period := range []time.Duration{Window / 3, Window / 2, Window} {
for _, offset := range []time.Duration{0, Window / 7, Window / 2, Window - time.Minute} {
t.Run(period.String()+"+"+offset.String(), func(t *testing.T) {
assert.Zero(t, heldNonceAlwaysAccepted(c, peerKey, period, offset),
"a peer re-stamped every %v is never left holding an expired nonce", period)
})
}
}
}
func TestNonce_ExpiresWhenRestampedTooSlowly(t *testing.T) {
// The counterpart, which is what gives the test above its teeth. Note the aligned case
// does not fail: a cadence of exactly two windows that lands on the boundaries is
// covered by the grace window. Off the boundary it is not, and real refreshes are off
// the boundary by design.
c := &Challenger{secret: []byte("secret"), window: Window}
peerKey := []byte("peer-key")
assert.Zero(t, heldNonceAlwaysAccepted(c, peerKey, 2*Window, 0),
"two windows exactly on the boundary happens to be covered by the grace window")
assert.NotZero(t, heldNonceAlwaysAccepted(c, peerKey, 2*Window, Window/2),
"the same cadence off the boundary must leave the peer rejected for a stretch")
}