@ -13,8 +13,11 @@ import (
"net/http/httptest"
"os"
"path/filepath"
"reflect"
"testing"
go4mem "go4.org/mem"
"github.com/google/go-cmp/cmp"
"tailscale.com/control/controlclient"
"tailscale.com/health"
@ -30,6 +33,7 @@ import (
"tailscale.com/types/persist"
"tailscale.com/types/tkatype"
"tailscale.com/util/must"
"tailscale.com/util/set"
)
type observerFunc func ( controlclient . Status )
@ -563,18 +567,32 @@ func TestTKAFilterNetmap(t *testing.T) {
}
n4Sig . Signature [ 3 ] = 42 // mess up the signature
n4Sig . Signature [ 4 ] = 42 // mess up the signature
n5GoodSig , err := signNodeKey ( tailcfg . TKASignInfo { NodePublic : n5 . Public ( ) } , nlPriv )
n5nl := key . NewNLPrivate ( )
n5InitialSig , err := signNodeKey ( tailcfg . TKASignInfo { NodePublic : n5 . Public ( ) , RotationPubkey : n5nl . Public ( ) . Verifier ( ) } , nlPriv )
if err != nil {
t . Fatal ( err )
}
resign := func ( nl key . NLPrivate , currentSig tkatype . MarshaledSignature ) ( key . NodePrivate , tkatype . MarshaledSignature ) {
nk := key . NewNode ( )
sig , err := tka . ResignNKS ( nl , nk . Public ( ) , currentSig )
if err != nil {
t . Fatal ( err )
}
return nk , sig
}
n5Rotated , n5RotatedSig := resign ( n5nl , n5InitialSig . Serialize ( ) )
nm := & netmap . NetworkMap {
Peers : nodeViews ( [ ] * tailcfg . Node {
{ ID : 1 , Key : n1 . Public ( ) , KeySignature : n1GoodSig . Serialize ( ) } ,
{ ID : 2 , Key : n2 . Public ( ) , KeySignature : nil } , // missing sig
{ ID : 3 , Key : n3 . Public ( ) , KeySignature : n1GoodSig . Serialize ( ) } , // someone elses sig
{ ID : 4 , Key : n4 . Public ( ) , KeySignature : n4Sig . Serialize ( ) } , // messed-up signature
{ ID : 5 , Key : n5 . Public ( ) , KeySignature : n5GoodSig . Serialize ( ) } ,
{ ID : 2 , Key : n2 . Public ( ) , KeySignature : nil } , // missing sig
{ ID : 3 , Key : n3 . Public ( ) , KeySignature : n1GoodSig . Serialize ( ) } , // someone elses sig
{ ID : 4 , Key : n4 . Public ( ) , KeySignature : n4Sig . Serialize ( ) } , // messed-up signature
{ ID : 50 , Key : n5 . Public ( ) , KeySignature : n5InitialSig . Serialize ( ) } , // rotated
{ ID : 51 , Key : n5Rotated . Public ( ) , KeySignature : n5RotatedSig } ,
} ) ,
}
@ -586,12 +604,39 @@ func TestTKAFilterNetmap(t *testing.T) {
want := nodeViews ( [ ] * tailcfg . Node {
{ ID : 1 , Key : n1 . Public ( ) , KeySignature : n1GoodSig . Serialize ( ) } ,
{ ID : 5 , Key : n5 . Public ( ) , KeySignature : n5 GoodSig. Serialize ( ) } ,
{ ID : 5 1 , Key : n5 Rotated . Public ( ) , KeySignature : n5 RotatedSig } ,
} )
nodePubComparer := cmp . Comparer ( func ( x , y key . NodePublic ) bool {
return x . Raw32 ( ) == y . Raw32 ( )
} )
if diff := cmp . Diff ( nm . Peers , want , nodePubComparer ) ; diff != "" {
if diff := cmp . Diff ( want , nm . Peers , nodePubComparer ) ; diff != "" {
t . Errorf ( "filtered netmap differs (-want, +got):\n%s" , diff )
}
// Create two more node signatures using the same wrapping key as n5.
// Since they have the same rotation chain, both will be filtered out.
n7 , n7Sig := resign ( n5nl , n5RotatedSig )
n8 , n8Sig := resign ( n5nl , n5RotatedSig )
nm = & netmap . NetworkMap {
Peers : nodeViews ( [ ] * tailcfg . Node {
{ ID : 1 , Key : n1 . Public ( ) , KeySignature : n1GoodSig . Serialize ( ) } ,
{ ID : 2 , Key : n2 . Public ( ) , KeySignature : nil } , // missing sig
{ ID : 3 , Key : n3 . Public ( ) , KeySignature : n1GoodSig . Serialize ( ) } , // someone elses sig
{ ID : 4 , Key : n4 . Public ( ) , KeySignature : n4Sig . Serialize ( ) } , // messed-up signature
{ ID : 50 , Key : n5 . Public ( ) , KeySignature : n5InitialSig . Serialize ( ) } , // rotated
{ ID : 51 , Key : n5Rotated . Public ( ) , KeySignature : n5RotatedSig } , // rotated
{ ID : 7 , Key : n7 . Public ( ) , KeySignature : n7Sig } , // same rotation chain as n8
{ ID : 8 , Key : n8 . Public ( ) , KeySignature : n8Sig } , // same rotation chain as n7
} ) ,
}
b . tkaFilterNetmapLocked ( nm )
want = nodeViews ( [ ] * tailcfg . Node {
{ ID : 1 , Key : n1 . Public ( ) , KeySignature : n1GoodSig . Serialize ( ) } ,
} )
if diff := cmp . Diff ( want , nm . Peers , nodePubComparer ) ; diff != "" {
t . Errorf ( "filtered netmap differs (-want, +got):\n%s" , diff )
}
}
@ -1130,3 +1175,85 @@ func TestTKARecoverCompromisedKeyFlow(t *testing.T) {
t . Errorf ( "NetworkLockSubmitRecoveryAUM() failed: %v" , err )
}
}
func TestRotationTracker ( t * testing . T ) {
newNK := func ( idx byte ) key . NodePublic {
// single-byte public key to make it human-readable in tests.
raw32 := [ 32 ] byte { idx }
return key . NodePublicFromRaw32 ( go4mem . B ( raw32 [ : ] ) )
}
n1 , n2 , n3 , n4 , n5 := newNK ( 1 ) , newNK ( 2 ) , newNK ( 3 ) , newNK ( 4 ) , newNK ( 5 )
pk1 , pk2 , pk3 := [ ] byte { 1 } , [ ] byte { 2 } , [ ] byte { 3 }
type addDetails struct {
np key . NodePublic
details * tka . RotationDetails
}
tests := [ ] struct {
name string
addDetails [ ] addDetails
want set . Set [ key . NodePublic ]
} {
{
name : "empty" ,
want : nil ,
} ,
{
name : "single_prev_key" ,
addDetails : [ ] addDetails {
{ np : n1 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n2 } , WrappingPubkey : pk1 } } ,
} ,
want : set . SetOf ( [ ] key . NodePublic { n2 } ) ,
} ,
{
name : "several_prev_keys" ,
addDetails : [ ] addDetails {
{ np : n1 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n2 } , WrappingPubkey : pk1 } } ,
{ np : n3 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n4 } , WrappingPubkey : pk2 } } ,
{ np : n2 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n3 , n4 } , WrappingPubkey : pk1 } } ,
} ,
want : set . SetOf ( [ ] key . NodePublic { n2 , n3 , n4 } ) ,
} ,
{
name : "several_per_pubkey_latest_wins" ,
addDetails : [ ] addDetails {
{ np : n2 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 } , WrappingPubkey : pk3 } } ,
{ np : n3 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 , n2 } , WrappingPubkey : pk3 } } ,
{ np : n4 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 , n2 , n3 } , WrappingPubkey : pk3 } } ,
{ np : n5 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n4 } , WrappingPubkey : pk3 } } ,
} ,
want : set . SetOf ( [ ] key . NodePublic { n1 , n2 , n3 , n4 } ) ,
} ,
{
name : "several_per_pubkey_same_chain_length_all_rejected" ,
addDetails : [ ] addDetails {
{ np : n2 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 } , WrappingPubkey : pk3 } } ,
{ np : n3 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 , n2 } , WrappingPubkey : pk3 } } ,
{ np : n4 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 , n2 } , WrappingPubkey : pk3 } } ,
{ np : n5 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 , n2 } , WrappingPubkey : pk3 } } ,
} ,
want : set . SetOf ( [ ] key . NodePublic { n1 , n2 , n3 , n4 , n5 } ) ,
} ,
{
name : "several_per_pubkey_longest_wins" ,
addDetails : [ ] addDetails {
{ np : n2 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 } , WrappingPubkey : pk3 } } ,
{ np : n3 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 , n2 } , WrappingPubkey : pk3 } } ,
{ np : n4 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 , n2 } , WrappingPubkey : pk3 } } ,
{ np : n5 , details : & tka . RotationDetails { PrevNodeKeys : [ ] key . NodePublic { n1 , n2 , n3 } , WrappingPubkey : pk3 } } ,
} ,
want : set . SetOf ( [ ] key . NodePublic { n1 , n2 , n3 , n4 } ) ,
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
r := & rotationTracker { logf : t . Logf }
for _ , ad := range tt . addDetails {
r . addRotationDetails ( ad . np , ad . details )
}
if got := r . obsoleteKeys ( ) ; ! reflect . DeepEqual ( got , tt . want ) {
t . Errorf ( "rotationTracker.obsoleteKeys() = %v, want %v" , got , tt . want )
}
} )
}
}