Просмотр исходного кода

tka: implement Authority API surface

After this, there should be one final PR to implement the Sync algorithm!

Signed-off-by: Tom DNetto <[email protected]>
Tom DNetto 3 лет назад
Родитель
Сommit
165c8f898e
3 измененных файлов с 393 добавлено и 1 удалено
  1. 1 1
      tka/aum.go
  2. 207 0
      tka/tka.go
  3. 185 0
      tka/tka_test.go

+ 1 - 1
tka/aum.go

@@ -146,7 +146,7 @@ func (a *AUM) StaticValidate() error {
 	}
 
 	if a.State != nil {
-		if len(a.State.LastAUMHash) != 0 {
+		if a.State.LastAUMHash != nil {
 			return errors.New("checkpoint state cannot specify a parent AUM")
 		}
 		if len(a.State.DisablementSecrets) == 0 {

+ 207 - 0
tka/tka.go

@@ -7,12 +7,27 @@ package tka
 
 import (
 	"bytes"
+	"crypto/ed25519"
 	"errors"
 	"fmt"
 	"os"
 	"sort"
 )
 
+// Authority is a Tailnet Key Authority. This type is the main coupling
+// point to the rest of the tailscale client.
+//
+// Authority objects can either be created from an existing, non-empty
+// tailchonk (via tka.Open()), or created from scratch using tka.Bootstrap()
+// or tka.Create().
+type Authority struct {
+	head           AUM
+	oldestAncestor AUM
+	state          State
+
+	storage Chonk
+}
+
 // A chain describes a linear sequence of updates from Oldest to Head,
 // resulting in some State at Head.
 type chain struct {
@@ -336,3 +351,195 @@ func computeActiveChain(storage Chonk, lastKnownOldest *AUMHash, maxIter int) (c
 	}
 	return out, nil
 }
+
+// aumVerify verifies if an AUM is well-formed, correctly signed, and
+// can be accepted for storage.
+func aumVerify(aum AUM, state State, isGenesisAUM bool) error {
+	if err := aum.StaticValidate(); err != nil {
+		return fmt.Errorf("invalid: %v", err)
+	}
+	if !isGenesisAUM {
+		if err := checkParent(aum, state); err != nil {
+			return err
+		}
+	}
+
+	if len(aum.Signatures) == 0 {
+		return errors.New("unsigned AUM")
+	}
+	sigHash := aum.SigHash()
+	for i, sig := range aum.Signatures {
+		key, err := state.GetKey(sig.KeyID)
+		if err != nil {
+			return fmt.Errorf("bad keyID on signature %d: %v", i, err)
+		}
+		if err := sig.Verify(sigHash, key); err != nil {
+			return fmt.Errorf("signature %d: %v", i, err)
+		}
+	}
+	return nil
+}
+
+func checkParent(aum AUM, state State) error {
+	parent, hasParent := aum.Parent()
+	if !hasParent {
+		return errors.New("aum has no parent")
+	}
+	if state.LastAUMHash == nil {
+		return errors.New("cannot check update parent hash against a state with no previous AUM")
+	}
+	if *state.LastAUMHash != parent {
+		return fmt.Errorf("aum with parent %x cannot be applied to a state with parent %x", state.LastAUMHash, parent)
+	}
+	return nil
+}
+
+// Head returns the AUM digest of the latest update applied to the state
+// machine.
+func (a *Authority) Head() AUMHash {
+	return *a.state.LastAUMHash
+}
+
+// Open initializes an existing TKA from the given tailchonk.
+//
+// Only use this if the current node has initialized an Authority before.
+// If a TKA exists on other nodes but theres nothing locally, use Bootstrap().
+// If no TKA exists anywhere and you are creating it for the first
+// time, use New().
+func Open(storage Chonk) (*Authority, error) {
+	a, err := storage.LastActiveAncestor()
+	if err != nil {
+		return nil, fmt.Errorf("reading last ancestor: %v", err)
+	}
+
+	c, err := computeActiveChain(storage, a, 2000)
+	if err != nil {
+		return nil, fmt.Errorf("active chain: %v", err)
+	}
+
+	return &Authority{
+		head:           c.Head,
+		oldestAncestor: c.Oldest,
+		storage:        storage,
+		state:          c.state,
+	}, nil
+}
+
+// Create initializes a brand-new TKA, generating a genesis update
+// and committing it to the given storage.
+//
+// The given signer must also be present in state as a trusted key.
+//
+// Do not use this to initialize a TKA that already exists, use Open()
+// or Bootstrap() instead.
+func Create(storage Chonk, state State, signer ed25519.PrivateKey) (*Authority, AUM, error) {
+	// Generate & sign a checkpoint, our genesis update.
+	genesis := AUM{
+		MessageKind: AUMCheckpoint,
+		State:       &state,
+	}
+	if err := genesis.StaticValidate(); err != nil {
+		// This serves as an easy way to validate the given state.
+		return nil, AUM{}, fmt.Errorf("invalid state: %v", err)
+	}
+	genesis.sign25519(signer)
+
+	a, err := Bootstrap(storage, genesis)
+	return a, genesis, err
+}
+
+// Bootstrap initializes a TKA based on the given checkpoint.
+//
+// Call this when setting up a new nodes' TKA, but other nodes
+// with initialized TKA's exist.
+//
+// Pass the returned genesis AUM from Create(), or a later checkpoint AUM.
+//
+// TODO(tom): We should test an authority bootstrapped from a later checkpoint
+// works fine with sync and everything.
+func Bootstrap(storage Chonk, bootstrap AUM) (*Authority, error) {
+	heads, err := storage.Heads()
+	if err != nil {
+		return nil, fmt.Errorf("reading heads: %v", err)
+	}
+	if len(heads) != 0 {
+		return nil, errors.New("tailchonk is not empty")
+	}
+
+	// Check the AUM is well-formed.
+	if bootstrap.MessageKind != AUMCheckpoint {
+		return nil, fmt.Errorf("bootstrap AUMs must be checkpoint messages, got %v", bootstrap.MessageKind)
+	}
+	if bootstrap.State == nil {
+		return nil, errors.New("bootstrap AUM is missing state")
+	}
+	if err := aumVerify(bootstrap, *bootstrap.State, true); err != nil {
+		return nil, fmt.Errorf("invalid bootstrap: %v", err)
+	}
+
+	// Everything looks good, write it to storage.
+	if err := storage.CommitVerifiedAUMs([]AUM{bootstrap}); err != nil {
+		return nil, fmt.Errorf("commit: %v", err)
+	}
+	if err := storage.SetLastActiveAncestor(bootstrap.Hash()); err != nil {
+		return nil, fmt.Errorf("set ancestor: %v", err)
+	}
+
+	return Open(storage)
+}
+
+// Inform is called to tell the authority about new updates. Updates
+// should be ordered oldest to newest. An error is returned if any
+// of the updates could not be processed.
+func (a *Authority) Inform(updates []AUM) error {
+	stateAt := make(map[AUMHash]State, len(updates)+1)
+	toCommit := make([]AUM, 0, len(updates))
+
+	for i, update := range updates {
+		hash := update.Hash()
+		if _, err := a.storage.AUM(hash); err == nil {
+			// Already have this AUM.
+			continue
+		}
+
+		parent, hasParent := update.Parent()
+		if !hasParent {
+			return fmt.Errorf("update %d: missing parent", i)
+		}
+
+		state, hasState := stateAt[parent]
+		var err error
+		if !hasState {
+			if state, err = computeStateAt(a.storage, 2000, parent); err != nil {
+				return fmt.Errorf("update %d computing state: %v", i, err)
+			}
+			stateAt[parent] = state
+		}
+
+		if err := aumVerify(update, state, false); err != nil {
+			return fmt.Errorf("update %d invalid: %v", i, err)
+		}
+		if stateAt[hash], err = state.applyVerifiedAUM(update); err != nil {
+			return fmt.Errorf("update %d cannot be applied: %v", i, err)
+		}
+		toCommit = append(toCommit, update)
+	}
+
+	if err := a.storage.CommitVerifiedAUMs(toCommit); err != nil {
+		return fmt.Errorf("commit: %v", err)
+	}
+
+	// TODO(tom): Theres no need to recompute the state from scratch
+	//            in every case. We should detect when updates were
+	//            a linear, non-forking series applied to head, and
+	//            just use the last State we computed.
+	oldestAncestor := a.oldestAncestor.Hash()
+	c, err := computeActiveChain(a.storage, &oldestAncestor, 2000)
+	if err != nil {
+		return fmt.Errorf("recomputing active chain: %v", err)
+	}
+	a.head = c.Head
+	a.oldestAncestor = c.Oldest
+	a.state = c.state
+	return nil
+}

+ 185 - 0
tka/tka_test.go

@@ -185,3 +185,188 @@ func TestComputeStateAt(t *testing.T) {
 		}
 	}
 }
+
+// fakeAUM generates an AUM structure based on the template.
+// If parent is provided, PrevAUMHash is set to that value.
+//
+// If template is an AUM, the returned AUM is based on that.
+// If template is an int, a NOOP AUM is returned, and the
+// provided int can be used to tweak the resulting hash (needed
+// for tests you want one AUM to be 'lower' than another, so that
+// that chain is taken based on fork resolution rules).
+func fakeAUM(t *testing.T, template interface{}, parent *AUMHash) (AUM, AUMHash) {
+	if seed, ok := template.(int); ok {
+		a := AUM{MessageKind: AUMNoOp, KeyID: []byte{byte(seed)}}
+		if parent != nil {
+			a.PrevAUMHash = (*parent)[:]
+		}
+		h := a.Hash()
+		return a, h
+	}
+
+	if a, ok := template.(AUM); ok {
+		if parent != nil {
+			a.PrevAUMHash = (*parent)[:]
+		}
+		h := a.Hash()
+		return a, h
+	}
+
+	panic("template must be an int or an AUM")
+}
+
+func TestOpenAuthority(t *testing.T) {
+	pub, _ := testingKey25519(t, 1)
+	key := Key{Kind: Key25519, Public: pub, Votes: 2}
+
+	//        /- L1
+	// G1 - I1 - I2 - I3 -L2
+	//                  \-L3
+	// G2 - L4
+	//
+	// We set the previous-known ancestor to G1, so the
+	// ancestor to start from should be G1.
+	g1, g1H := fakeAUM(t, AUM{MessageKind: AUMAddKey, Key: &key}, nil)
+	i1, i1H := fakeAUM(t, 2, &g1H) // AUM{MessageKind: AUMAddKey, Key: &key2}
+	l1, l1H := fakeAUM(t, 13, &i1H)
+
+	i2, i2H := fakeAUM(t, 2, &i1H)
+	i3, i3H := fakeAUM(t, 5, &i2H)
+	l2, l2H := fakeAUM(t, AUM{MessageKind: AUMNoOp, KeyID: []byte{7}, Signatures: []Signature{{KeyID: key.ID()}}}, &i3H)
+	l3, l3H := fakeAUM(t, 4, &i3H)
+
+	g2, g2H := fakeAUM(t, 8, nil)
+	l4, _ := fakeAUM(t, 9, &g2H)
+
+	// We make sure that I2 has a lower hash than L1, so
+	// it should take that path rather than L1.
+	if bytes.Compare(l1H[:], i2H[:]) < 0 {
+		t.Fatal("failed assert: h(i2) > h(l1)\nTweak parameters to fakeAUM till this passes")
+	}
+	// We make sure L2 has a signature with key, so it should
+	// take that path over L3. We assert that the L3 hash
+	// is less than L2 so the test will fail if the signature
+	// preference logic is broken.
+	if bytes.Compare(l2H[:], l3H[:]) < 0 {
+		t.Fatal("failed assert: h(l3) > h(l2)\nTweak parameters to fakeAUM till this passes")
+	}
+
+	// Construct the state of durable storage.
+	chonk := &Mem{}
+	err := chonk.CommitVerifiedAUMs([]AUM{g1, i1, l1, i2, i3, l2, l3, g2, l4})
+	if err != nil {
+		t.Fatal(err)
+	}
+	chonk.SetLastActiveAncestor(i1H)
+
+	a, err := Open(chonk)
+	if err != nil {
+		t.Fatalf("New() failed: %v", err)
+	}
+	// Should include the key added in G1
+	if _, err := a.state.GetKey(key.ID()); err != nil {
+		t.Errorf("missing G1 key: %v", err)
+	}
+	// The head of the chain should be L2.
+	if a.Head() != l2H {
+		t.Errorf("head was %x, want %x", a.state.LastAUMHash, l2H)
+	}
+}
+
+func TestOpenAuthority_EmptyErrors(t *testing.T) {
+	_, err := Open(&Mem{})
+	if err == nil {
+		t.Error("Expected an error initializing an empty authority, got nil")
+	}
+}
+
+func TestAuthorityHead(t *testing.T) {
+	c := newTestchain(t, `
+        G1 -> L1
+         | -> L2
+
+        L1.hashSeed = 2
+    `)
+
+	a, _ := Open(c.Chonk())
+	if got, want := a.head.Hash(), a.Head(); got != want {
+		t.Errorf("Hash() returned %x, want %x", got, want)
+	}
+}
+
+func TestCreateBootstrapAuthority(t *testing.T) {
+	pub, priv := testingKey25519(t, 1)
+	key := Key{Kind: Key25519, Public: pub, Votes: 2}
+
+	a1, genesisAUM, err := Create(&Mem{}, State{
+		Keys:               []Key{key},
+		DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
+	}, priv)
+	if err != nil {
+		t.Fatalf("Create() failed: %v", err)
+	}
+
+	a2, err := Bootstrap(&Mem{}, genesisAUM)
+	if err != nil {
+		t.Fatalf("Bootstrap() failed: %v", err)
+	}
+
+	if a1.Head() != a2.Head() {
+		t.Fatal("created and bootstrapped authority differ")
+	}
+
+	// Both authorities should trust the key laid down in the genesis state.
+	if _, err := a1.state.GetKey(key.ID()); err != nil {
+		t.Errorf("reading genesis key from a1: %v", err)
+	}
+	if _, err := a2.state.GetKey(key.ID()); err != nil {
+		t.Errorf("reading genesis key from a2: %v", err)
+	}
+}
+
+func TestAuthorityInform(t *testing.T) {
+	pub, priv := testingKey25519(t, 1)
+	key := Key{Kind: Key25519, Public: pub, Votes: 2}
+
+	c := newTestchain(t, `
+        G1 -> L1
+         | -> L2 -> L3
+               | -> L4 -> L5
+
+        G1.template = genesis
+        L2.hashSeed = 1
+        L4.hashSeed = 2
+    `,
+		optTemplate("genesis", AUM{MessageKind: AUMCheckpoint, State: &State{
+			Keys:               []Key{key},
+			DisablementSecrets: [][]byte{disablementKDF([]byte{1, 2, 3})},
+		}}),
+		optKey("key", key, priv),
+		optSignAllUsing("key"))
+
+	storage := &Mem{}
+	a, err := Bootstrap(storage, c.AUMs["G1"])
+	if err != nil {
+		t.Fatalf("Bootstrap() failed: %v", err)
+	}
+
+	informAUMs := []AUM{c.AUMs["L1"], c.AUMs["L2"], c.AUMs["L3"], c.AUMs["L4"], c.AUMs["L5"]}
+
+	if err := a.Inform(informAUMs); err != nil {
+		t.Fatalf("Inform() failed: %v", err)
+	}
+	for i, update := range informAUMs {
+		stored, err := storage.AUM(update.Hash())
+		if err != nil {
+			t.Errorf("reading stored update %d: %v", i, err)
+			continue
+		}
+		if diff := cmp.Diff(update, stored); diff != "" {
+			t.Errorf("update %d differs (-want, +got):\n%s", i, diff)
+		}
+	}
+
+	if a.Head() != c.AUMHashes["L3"] {
+		t.Fatal("authority did not converge to correct AUM")
+	}
+}