tailscale/tka/builder_test.go
Alex Chan 8749d19029 tka,ipn: reduce boilerplate in Tailnet Lock tests
The `CreateStateForTest` helper reduces boilerplate in cases where the test
only cares about the trusted keys and not the disablement values (and makes
it more obvious where the disablement values are meaningful).

The `setupChonkStorage` helper reduces the boilerplate when creating on-disk
TKA storage in tests.

The `fakeLocalBackend` helper reduces the boilerplate when setting up a
`LocalBackend` instance in the IPN tests.

Updates #cleanup

Change-Id: Iacfba1be5f7fab208eec11e4369d63c7d7519da5
Signed-off-by: Alex Chan <alexc@tailscale.com>
2026-04-30 12:43:07 +01:00

316 lines
8.9 KiB
Go

// Copyright (c) Tailscale Inc & contributors
// SPDX-License-Identifier: BSD-3-Clause
package tka
import (
"crypto/ed25519"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
"tailscale.com/types/tkatype"
)
type signer25519 ed25519.PrivateKey
func (s signer25519) SignAUM(sigHash tkatype.AUMSigHash) ([]tkatype.Signature, error) {
priv := ed25519.PrivateKey(s)
key := Key{Kind: Key25519, Public: priv.Public().(ed25519.PublicKey)}
return []tkatype.Signature{{
KeyID: key.MustID(),
Signature: ed25519.Sign(priv, sigHash[:]),
}}, nil
}
func TestAuthorityBuilderAddKey(t *testing.T) {
pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2}
state := CreateStateForTest(key)
storage := ChonkMem()
a, _, err := Create(storage, state, signer25519(priv))
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
pub2, _ := testingKey25519(t, 2)
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
b := a.NewUpdater(signer25519(priv))
if err := b.AddKey(key2); err != nil {
t.Fatalf("AddKey(%v) failed: %v", key2, err)
}
updates, err := b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
// See if the update is valid by applying it to the authority
// + checking if the new key is there.
if err := a.Inform(storage, updates); err != nil {
t.Fatalf("could not apply generated updates: %v", err)
}
if _, err := a.state.GetKey(key2.MustID()); err != nil {
t.Errorf("could not read new key: %v", err)
}
}
func TestAuthorityBuilderMaxKey(t *testing.T) {
pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2}
state := CreateStateForTest(key)
storage := ChonkMem()
a, _, err := Create(storage, state, signer25519(priv))
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
for i := 0; i <= maxKeys; i++ {
pub2, _ := testingKey25519(t, int64(2+i))
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
b := a.NewUpdater(signer25519(priv))
err := b.AddKey(key2)
if i < maxKeys-1 {
if err != nil {
t.Fatalf("AddKey(%v) failed: %v", key2, err)
}
} else {
// Too many keys.
if err == nil {
t.Fatalf("AddKey(%v) succeeded unexpectedly", key2)
}
continue
}
updates, err := b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
if err := a.Inform(storage, updates); err != nil {
t.Fatalf("could not apply generated updates: %v", err)
}
if _, err := a.state.GetKey(key2.MustID()); err != nil {
t.Errorf("could not read new key: %v", err)
}
}
}
func TestAuthorityBuilderRemoveKey(t *testing.T) {
pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2}
pub2, _ := testingKey25519(t, 2)
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
state := CreateStateForTest(key, key2)
storage := ChonkMem()
a, _, err := Create(storage, state, signer25519(priv))
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
b := a.NewUpdater(signer25519(priv))
if err := b.RemoveKey(key2.MustID()); err != nil {
t.Fatalf("RemoveKey(%v) failed: %v", key2, err)
}
updates, err := b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
// See if the update is valid by applying it to the authority
// + checking if the key has been removed.
if err := a.Inform(storage, updates); err != nil {
t.Fatalf("could not apply generated updates: %v", err)
}
if _, err := a.state.GetKey(key2.MustID()); err != ErrNoSuchKey {
t.Errorf("GetKey(key2).err = %v, want %v", err, ErrNoSuchKey)
}
// Check that removing the remaining key errors out.
b = a.NewUpdater(signer25519(priv))
if err := b.RemoveKey(key.MustID()); err != nil {
t.Fatalf("RemoveKey(%v) failed: %v", key, err)
}
updates, err = b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
wantErr := "cannot remove the last key"
if err := a.Inform(storage, updates); err == nil || !strings.Contains(err.Error(), wantErr) {
t.Fatalf("expected Inform() to return error %q, got: %v", wantErr, err)
}
}
func TestAuthorityBuilderSetKeyVote(t *testing.T) {
pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2}
state := CreateStateForTest(key)
storage := ChonkMem()
a, _, err := Create(storage, state, signer25519(priv))
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
b := a.NewUpdater(signer25519(priv))
if err := b.SetKeyVote(key.MustID(), 5); err != nil {
t.Fatalf("SetKeyVote(%v) failed: %v", key.MustID(), err)
}
updates, err := b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
// See if the update is valid by applying it to the authority
// + checking if the update is there.
if err := a.Inform(storage, updates); err != nil {
t.Fatalf("could not apply generated updates: %v", err)
}
k, err := a.state.GetKey(key.MustID())
if err != nil {
t.Fatal(err)
}
if got, want := k.Votes, uint(5); got != want {
t.Errorf("key.Votes = %d, want %d", got, want)
}
}
func TestAuthorityBuilderSetKeyMeta(t *testing.T) {
pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2, Meta: map[string]string{"a": "b"}}
state := CreateStateForTest(key)
storage := ChonkMem()
a, _, err := Create(storage, state, signer25519(priv))
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
b := a.NewUpdater(signer25519(priv))
if err := b.SetKeyMeta(key.MustID(), map[string]string{"b": "c"}); err != nil {
t.Fatalf("SetKeyMeta(%v) failed: %v", key, err)
}
updates, err := b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
// See if the update is valid by applying it to the authority
// + checking if the update is there.
if err := a.Inform(storage, updates); err != nil {
t.Fatalf("could not apply generated updates: %v", err)
}
k, err := a.state.GetKey(key.MustID())
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(map[string]string{"b": "c"}, k.Meta); diff != "" {
t.Errorf("updated meta differs (-want, +got):\n%s", diff)
}
}
func TestAuthorityBuilderMultiple(t *testing.T) {
pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2}
state := CreateStateForTest(key)
storage := ChonkMem()
a, _, err := Create(storage, state, signer25519(priv))
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
pub2, _ := testingKey25519(t, 2)
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
b := a.NewUpdater(signer25519(priv))
if err := b.AddKey(key2); err != nil {
t.Fatalf("AddKey(%v) failed: %v", key2, err)
}
if err := b.SetKeyVote(key2.MustID(), 42); err != nil {
t.Fatalf("SetKeyVote(%v) failed: %v", key2, err)
}
if err := b.RemoveKey(key.MustID()); err != nil {
t.Fatalf("RemoveKey(%v) failed: %v", key, err)
}
updates, err := b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
// See if the update is valid by applying it to the authority
// + checking if the update is there.
if err := a.Inform(storage, updates); err != nil {
t.Fatalf("could not apply generated updates: %v", err)
}
k, err := a.state.GetKey(key2.MustID())
if err != nil {
t.Fatal(err)
}
if got, want := k.Votes, uint(42); got != want {
t.Errorf("key.Votes = %d, want %d", got, want)
}
if _, err := a.state.GetKey(key.MustID()); err != ErrNoSuchKey {
t.Errorf("GetKey(key).err = %v, want %v", err, ErrNoSuchKey)
}
}
func TestAuthorityBuilderCheckpointsAfterXUpdates(t *testing.T) {
pub, priv := testingKey25519(t, 1)
key := Key{Kind: Key25519, Public: pub, Votes: 2}
state := CreateStateForTest(key)
storage := ChonkMem()
a, _, err := Create(storage, state, signer25519(priv))
if err != nil {
t.Fatalf("Create() failed: %v", err)
}
for i := 0; i <= checkpointEvery; i++ {
pub2, _ := testingKey25519(t, int64(i+2))
key2 := Key{Kind: Key25519, Public: pub2, Votes: 1}
b := a.NewUpdater(signer25519(priv))
if err := b.AddKey(key2); err != nil {
t.Fatalf("AddKey(%v) failed: %v", key2, err)
}
updates, err := b.Finalize(storage)
if err != nil {
t.Fatalf("Finalize() failed: %v", err)
}
// See if the update is valid by applying it to the authority
// + checking if the new key is there.
if err := a.Inform(storage, updates); err != nil {
t.Fatalf("could not apply generated updates: %v", err)
}
if _, err := a.state.GetKey(key2.MustID()); err != nil {
t.Fatal(err)
}
wantKind := AUMAddKey
if i == checkpointEvery-1 { // Genesis + 49 updates == 50 (the value of checkpointEvery)
wantKind = AUMCheckpoint
}
lastAUM, err := storage.AUM(a.Head())
if err != nil {
t.Fatal(err)
}
if lastAUM.MessageKind != wantKind {
t.Errorf("[%d] HeadAUM.MessageKind = %v, want %v", i, lastAUM.MessageKind, wantKind)
}
}
// Try starting an authority just based on storage.
a2, err := Open(storage)
if err != nil {
t.Fatalf("Failed to open from stored AUMs: %v", err)
}
if a.Head() != a2.Head() {
t.Errorf("stored and computed HEAD differ: got %v, want %v", a2.Head(), a.Head())
}
}