From aeb80bf8cbf57ab75dfe9237301c2390aefa2156 Mon Sep 17 00:00:00 2001 From: Tom DNetto Date: Wed, 16 Nov 2022 15:38:25 -0800 Subject: [PATCH] ipn/ipnlocal,tka: generate a nonce for each TKA Signed-off-by: Tom DNetto --- ipn/ipnlocal/network-lock.go | 10 ++++++++++ tka/state.go | 19 ++++++++++++++++++- tka/state_test.go | 13 +++++++++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/ipn/ipnlocal/network-lock.go b/ipn/ipnlocal/network-lock.go index e83656f43..173f002a6 100644 --- a/ipn/ipnlocal/network-lock.go +++ b/ipn/ipnlocal/network-lock.go @@ -6,7 +6,9 @@ package ipnlocal import ( "bytes" + "crypto/rand" "context" + "encoding/binary" "encoding/json" "errors" "fmt" @@ -418,6 +420,11 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt return errors.New("no node-key: is tailscale logged in?") } + var entropy [16]byte + if _, err := rand.Read(entropy[:]); err != nil { + return err + } + // Generates a genesis AUM representing trust in the provided keys. // We use an in-memory tailchonk because we don't want to commit to // the filesystem until we've finished the initialization sequence, @@ -429,6 +436,9 @@ func (b *LocalBackend) NetworkLockInit(keys []tka.Key, disablementValues [][]byt // - DisablementSecret: value needed to disable. // - DisablementValue: the KDF of the disablement secret, a public value. DisablementSecrets: disablementValues, + + StateID1: binary.LittleEndian.Uint64(entropy[:8]), + StateID2: binary.LittleEndian.Uint64(entropy[8:]), }, nlPriv) if err != nil { return fmt.Errorf("tka.Create: %v", err) diff --git a/tka/state.go b/tka/state.go index 2ab4715da..fa9dd0d05 100644 --- a/tka/state.go +++ b/tka/state.go @@ -36,6 +36,13 @@ type State struct { // Keys are the public keys currently trusted by the TKA. Keys []Key `cbor:"3,keyasint"` + + // StateID's are nonce's, generated on enablement and fixed for + // the lifetime of the Tailnet Key Authority. We generate 16-bytes + // worth of keyspace here just in case we come up with a cool future + // use for this. + StateID1 uint64 `cbor:"4,keyasint,omitempty"` + StateID2 uint64 `cbor:"5,keyasint,omitempty"` } // GetKey returns the trusted key with the specified KeyID. @@ -55,7 +62,10 @@ func (s State) GetKey(key tkatype.KeyID) (Key, error) { // slice for encoding purposes, so an implementation of Clone() // must take care to preserve this. func (s State) Clone() State { - out := State{} + out := State{ + StateID1: s.StateID1, + StateID2: s.StateID2, + } if s.LastAUMHash != nil { dupe := *s.LastAUMHash @@ -149,6 +159,13 @@ func (s State) applyVerifiedAUM(update AUM) (State, error) { return out, nil case AUMCheckpoint: + if update.State == nil { + return State{}, errors.New("missing checkpoint state") + } + id1Match, id2Match := update.State.StateID1 == s.StateID1, update.State.StateID2 == s.StateID2 + if !id1Match || !id2Match { + return State{}, errors.New("checkpointed state has an incorrect stateID") + } return update.State.cloneForUpdate(&update), nil case AUMAddKey: diff --git a/tka/state_test.go b/tka/state_test.go index 1ccddf38f..a5f9a99c5 100644 --- a/tka/state_test.go +++ b/tka/state_test.go @@ -44,6 +44,13 @@ func TestCloneState(t *testing.T) { Keys: []Key{{Kind: Key25519, Votes: 2, Public: []byte{5, 6, 7, 8}, Meta: map[string]string{"a": "b"}}}, }, }, + { + "StateID", + State{ + StateID1: 42, + StateID2: 22, + }, + }, { "DisablementSecrets", State{ @@ -223,6 +230,12 @@ func TestApplyUpdateErrors(t *testing.T) { }, errors.New("parent AUMHash mismatch"), }, + { + "Bad StateID", + []AUM{{MessageKind: AUMCheckpoint, State: &State{StateID1: 1}}}, + State{Keys: []Key{{Kind: Key25519, Public: []byte{1}}}, StateID1: 42}, + errors.New("checkpointed state has an incorrect stateID"), + }, } for _, tc := range tcs {